pg-play 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pg_play/__init__.py +5 -0
- pg_play/__main__.py +5 -0
- pg_play/cli.py +133 -0
- pg_play/contract.py +162 -0
- pg_play/manifest.py +550 -0
- pg_play/mcp_server.py +174 -0
- pg_play/report.py +168 -0
- pg_play/runner.py +110 -0
- pg_play/schema/pg_play-v1.schema.json +174 -0
- pg_play/service.py +1378 -0
- pg_play/skills/analyze-postgres-experiment/SKILL.md +37 -0
- pg_play/skills/analyze-postgres-experiment/agents/openai.yaml +4 -0
- pg_play/skills/analyze-postgres-experiment/references/interpretation.md +18 -0
- pg_play/skills/run-postgres-experiment/SKILL.md +27 -0
- pg_play/skills/run-postgres-experiment/agents/openai.yaml +4 -0
- pg_play/skills/run-postgres-experiment/references/manifest.md +84 -0
- pg_play/state.py +41 -0
- pg_play/version.py +3 -0
- pg_play-0.2.0.dist-info/METADATA +394 -0
- pg_play-0.2.0.dist-info/RECORD +24 -0
- pg_play-0.2.0.dist-info/WHEEL +5 -0
- pg_play-0.2.0.dist-info/entry_points.txt +3 -0
- pg_play-0.2.0.dist-info/licenses/LICENSE +21 -0
- pg_play-0.2.0.dist-info/top_level.txt +1 -0
pg_play/__init__.py
ADDED
pg_play/__main__.py
ADDED
pg_play/cli.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Human-oriented pg_play command-line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from pg_play.service import PgPlayService
|
|
10
|
+
from pg_play.version import __version__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="pg-play",
|
|
16
|
+
description="Orchestrate reproducible PostgreSQL experiments.",
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
19
|
+
commands = parser.add_subparsers(dest="command")
|
|
20
|
+
|
|
21
|
+
capabilities = commands.add_parser("capabilities", help="Show installed component capabilities")
|
|
22
|
+
capabilities.add_argument(
|
|
23
|
+
"--component",
|
|
24
|
+
choices=("pg_configurator", "pg_stand", "pg_workload", "pg_diag", "pg_perf_bench"),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
validate = commands.add_parser("validate", help="Validate an experiment manifest and inputs")
|
|
28
|
+
validate.add_argument("manifest")
|
|
29
|
+
|
|
30
|
+
plan = commands.add_parser("plan", help="Build a read-only experiment plan")
|
|
31
|
+
plan.add_argument("manifest")
|
|
32
|
+
|
|
33
|
+
run = commands.add_parser("run", help="Run an experiment from an unchanged plan")
|
|
34
|
+
run.add_argument("manifest")
|
|
35
|
+
run.add_argument("--plan-hash", required=True)
|
|
36
|
+
run.add_argument("--run-id", required=True)
|
|
37
|
+
|
|
38
|
+
status = commands.add_parser("status", help="Read one immutable experiment run state")
|
|
39
|
+
status.add_argument("manifest")
|
|
40
|
+
status.add_argument("--run-id", required=True)
|
|
41
|
+
|
|
42
|
+
inspect = commands.add_parser("inspect-report", help="Validate and summarize pg_diag JSON")
|
|
43
|
+
inspect.add_argument("report")
|
|
44
|
+
|
|
45
|
+
compare = commands.add_parser("compare-reports", help="Compare two pg_diag JSON artifacts")
|
|
46
|
+
compare.add_argument("baseline")
|
|
47
|
+
compare.add_argument("candidate")
|
|
48
|
+
|
|
49
|
+
inspect_benchmark = commands.add_parser(
|
|
50
|
+
"inspect-benchmark-report",
|
|
51
|
+
help="Validate and summarize pg_perf_bench JSON",
|
|
52
|
+
)
|
|
53
|
+
inspect_benchmark.add_argument("report")
|
|
54
|
+
|
|
55
|
+
compare_benchmarks = commands.add_parser(
|
|
56
|
+
"compare-benchmark-reports",
|
|
57
|
+
help="Compare two pg_perf_bench JSON artifacts",
|
|
58
|
+
)
|
|
59
|
+
compare_benchmarks.add_argument("baseline")
|
|
60
|
+
compare_benchmarks.add_argument("candidate")
|
|
61
|
+
commands.add_parser("benchmark-profiles", help="List pg_perf_bench workload profiles")
|
|
62
|
+
commands.add_parser("benchmark-join-tasks", help="List pg_perf_bench JOIN scenarios")
|
|
63
|
+
join_benchmarks = commands.add_parser(
|
|
64
|
+
"join-benchmark-reports",
|
|
65
|
+
help="Join an explicit set of pg_perf_bench JSON reports",
|
|
66
|
+
)
|
|
67
|
+
join_benchmarks.add_argument("--report", action="append", required=True)
|
|
68
|
+
join_benchmarks.add_argument("--join-task", required=True)
|
|
69
|
+
join_benchmarks.add_argument("--out", required=True)
|
|
70
|
+
join_benchmarks.add_argument("--report-name", required=True)
|
|
71
|
+
|
|
72
|
+
teardown = commands.add_parser(
|
|
73
|
+
"teardown",
|
|
74
|
+
help="Stop workload processes and remove an experiment stand",
|
|
75
|
+
)
|
|
76
|
+
teardown.add_argument("manifest")
|
|
77
|
+
teardown.add_argument("--clear-stand-data", action="store_true")
|
|
78
|
+
return parser
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main(argv: list[str] | None = None) -> int:
|
|
82
|
+
parser = build_parser()
|
|
83
|
+
args = parser.parse_args(argv)
|
|
84
|
+
if args.command is None:
|
|
85
|
+
parser.print_help()
|
|
86
|
+
return 0
|
|
87
|
+
service = PgPlayService()
|
|
88
|
+
try:
|
|
89
|
+
if args.command == "capabilities":
|
|
90
|
+
result = service.component_capabilities(args.component)
|
|
91
|
+
elif args.command == "validate":
|
|
92
|
+
result = service.validate_experiment(args.manifest)
|
|
93
|
+
elif args.command == "plan":
|
|
94
|
+
result = service.plan_experiment(args.manifest)
|
|
95
|
+
elif args.command == "run":
|
|
96
|
+
result = service.run_experiment(
|
|
97
|
+
args.manifest,
|
|
98
|
+
plan_hash=args.plan_hash,
|
|
99
|
+
run_id=args.run_id,
|
|
100
|
+
)
|
|
101
|
+
elif args.command == "status":
|
|
102
|
+
result = service.experiment_status(args.manifest, args.run_id)
|
|
103
|
+
elif args.command == "inspect-report":
|
|
104
|
+
result = service.inspect_report(args.report)
|
|
105
|
+
elif args.command == "compare-reports":
|
|
106
|
+
result = service.compare_reports(args.baseline, args.candidate)
|
|
107
|
+
elif args.command == "inspect-benchmark-report":
|
|
108
|
+
result = service.inspect_benchmark_report(args.report)
|
|
109
|
+
elif args.command == "compare-benchmark-reports":
|
|
110
|
+
result = service.compare_benchmark_reports(args.baseline, args.candidate)
|
|
111
|
+
elif args.command == "benchmark-profiles":
|
|
112
|
+
result = service.benchmark_profiles()
|
|
113
|
+
elif args.command == "benchmark-join-tasks":
|
|
114
|
+
result = service.benchmark_join_tasks()
|
|
115
|
+
elif args.command == "join-benchmark-reports":
|
|
116
|
+
result = service.join_benchmark_reports(
|
|
117
|
+
args.report,
|
|
118
|
+
args.join_task,
|
|
119
|
+
args.out,
|
|
120
|
+
args.report_name,
|
|
121
|
+
)
|
|
122
|
+
elif args.command == "teardown":
|
|
123
|
+
result = service.teardown_experiment(
|
|
124
|
+
args.manifest,
|
|
125
|
+
clear_stand_data=args.clear_stand_data,
|
|
126
|
+
)
|
|
127
|
+
else: # pragma: no cover
|
|
128
|
+
raise AssertionError(f"unhandled command: {args.command}")
|
|
129
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
130
|
+
print(f"pg-play: error: {exc}", file=sys.stderr)
|
|
131
|
+
return 2
|
|
132
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
133
|
+
return 0
|
pg_play/contract.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Shared pg_play component envelope validation and hashing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
CONTRACT_VERSION = "pg_play/component/v1"
|
|
10
|
+
CAPABILITY_SCHEMA_VERSION = "pg_play/capabilities/v1"
|
|
11
|
+
MACHINE_INTERFACE = {
|
|
12
|
+
"machine_flag": "--machine",
|
|
13
|
+
"request_id_option": "--request-id",
|
|
14
|
+
"capabilities_option": "--component-capabilities",
|
|
15
|
+
}
|
|
16
|
+
EXIT_CODES = {
|
|
17
|
+
"success": 0,
|
|
18
|
+
"validation_error": 2,
|
|
19
|
+
"precondition_failed": 3,
|
|
20
|
+
"unsupported": 4,
|
|
21
|
+
"partial": 5,
|
|
22
|
+
"execution_error": 6,
|
|
23
|
+
"cancelled": 7,
|
|
24
|
+
"ownership_error": 8,
|
|
25
|
+
}
|
|
26
|
+
COMPONENTS = {
|
|
27
|
+
"pg_configurator",
|
|
28
|
+
"pg_diag",
|
|
29
|
+
"pg_perf_bench",
|
|
30
|
+
"pg_stand",
|
|
31
|
+
"pg_workload",
|
|
32
|
+
}
|
|
33
|
+
STATUSES = {
|
|
34
|
+
"planned",
|
|
35
|
+
"running",
|
|
36
|
+
"succeeded",
|
|
37
|
+
"partial",
|
|
38
|
+
"failed",
|
|
39
|
+
"cancelled",
|
|
40
|
+
"skipped",
|
|
41
|
+
"blocked",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ContractError(RuntimeError):
|
|
46
|
+
"""A component returned output outside the versioned contract."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def canonical_json(value: Any) -> str:
|
|
50
|
+
return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def canonical_hash(value: Any) -> str:
|
|
54
|
+
return "sha256:" + hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def validate_envelope(value: Any, *, expected_component: str | None = None) -> dict[str, Any]:
|
|
58
|
+
if not isinstance(value, dict):
|
|
59
|
+
raise ContractError("component response must be a JSON object")
|
|
60
|
+
required = {
|
|
61
|
+
"contract_version",
|
|
62
|
+
"component",
|
|
63
|
+
"component_version",
|
|
64
|
+
"command",
|
|
65
|
+
"request_id",
|
|
66
|
+
"status",
|
|
67
|
+
"result",
|
|
68
|
+
"artifacts",
|
|
69
|
+
"warnings",
|
|
70
|
+
"error",
|
|
71
|
+
}
|
|
72
|
+
if set(value) != required:
|
|
73
|
+
missing = sorted(required.difference(value))
|
|
74
|
+
extra = sorted(set(value).difference(required))
|
|
75
|
+
raise ContractError(f"invalid component envelope fields: missing={missing}, extra={extra}")
|
|
76
|
+
if value["contract_version"] != CONTRACT_VERSION:
|
|
77
|
+
raise ContractError(f"unsupported component contract: {value['contract_version']!r}")
|
|
78
|
+
component = value["component"]
|
|
79
|
+
if component not in COMPONENTS:
|
|
80
|
+
raise ContractError(f"unknown component in envelope: {component!r}")
|
|
81
|
+
if expected_component is not None and component != expected_component:
|
|
82
|
+
raise ContractError(f"expected {expected_component}, received {component}")
|
|
83
|
+
if not isinstance(value["component_version"], str) or not value["component_version"]:
|
|
84
|
+
raise ContractError("component_version must be a non-empty string")
|
|
85
|
+
if not isinstance(value["command"], str) or not value["command"]:
|
|
86
|
+
raise ContractError("command must be a non-empty string")
|
|
87
|
+
if value["request_id"] is not None and not isinstance(value["request_id"], str):
|
|
88
|
+
raise ContractError("request_id must be a string or null")
|
|
89
|
+
if value["status"] not in STATUSES:
|
|
90
|
+
raise ContractError(f"unsupported component status: {value['status']!r}")
|
|
91
|
+
if not isinstance(value["artifacts"], list) or not all(
|
|
92
|
+
isinstance(artifact, dict) for artifact in value["artifacts"]
|
|
93
|
+
):
|
|
94
|
+
raise ContractError("component artifacts must be a list of objects")
|
|
95
|
+
if not isinstance(value["warnings"], list) or not all(
|
|
96
|
+
isinstance(warning, str) for warning in value["warnings"]
|
|
97
|
+
):
|
|
98
|
+
raise ContractError("component warnings must be a list of strings")
|
|
99
|
+
if value["error"] is not None and not isinstance(value["error"], dict):
|
|
100
|
+
raise ContractError("component error must be an object or null")
|
|
101
|
+
if isinstance(value["error"], dict) and not {
|
|
102
|
+
"code",
|
|
103
|
+
"message",
|
|
104
|
+
}.issubset(value["error"]):
|
|
105
|
+
raise ContractError("component error must contain code and message")
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def validate_capabilities(
|
|
110
|
+
value: Any,
|
|
111
|
+
*,
|
|
112
|
+
expected_component: str,
|
|
113
|
+
required_commands: set[str] | None = None,
|
|
114
|
+
) -> dict[str, Any]:
|
|
115
|
+
if not isinstance(value, dict):
|
|
116
|
+
raise ContractError("component capabilities must be a JSON object")
|
|
117
|
+
required_fields = {
|
|
118
|
+
"capability_schema_version",
|
|
119
|
+
"contract_version",
|
|
120
|
+
"component",
|
|
121
|
+
"component_version",
|
|
122
|
+
"commands",
|
|
123
|
+
"exit_codes",
|
|
124
|
+
"machine_interface",
|
|
125
|
+
"secret_policy",
|
|
126
|
+
}
|
|
127
|
+
missing = sorted(required_fields.difference(value))
|
|
128
|
+
if missing:
|
|
129
|
+
raise ContractError(f"component capabilities missing fields: {missing}")
|
|
130
|
+
if value["capability_schema_version"] != CAPABILITY_SCHEMA_VERSION:
|
|
131
|
+
raise ContractError(
|
|
132
|
+
f"unsupported capability schema: {value['capability_schema_version']!r}"
|
|
133
|
+
)
|
|
134
|
+
if value["contract_version"] != CONTRACT_VERSION:
|
|
135
|
+
raise ContractError(f"unsupported component contract: {value['contract_version']!r}")
|
|
136
|
+
if value["component"] != expected_component:
|
|
137
|
+
raise ContractError(
|
|
138
|
+
f"expected capabilities for {expected_component}, received {value['component']}"
|
|
139
|
+
)
|
|
140
|
+
if value["machine_interface"] != MACHINE_INTERFACE:
|
|
141
|
+
raise ContractError("component machine_interface must match the pg_play contract")
|
|
142
|
+
if not isinstance(value["component_version"], str) or not value["component_version"]:
|
|
143
|
+
raise ContractError("component capability version must be a non-empty string")
|
|
144
|
+
commands = value["commands"]
|
|
145
|
+
if not isinstance(commands, dict):
|
|
146
|
+
raise ContractError("component capability commands must be an object")
|
|
147
|
+
missing_commands = sorted((required_commands or set()).difference(commands))
|
|
148
|
+
if missing_commands:
|
|
149
|
+
raise ContractError(f"component capabilities missing commands: {missing_commands}")
|
|
150
|
+
for command, metadata in commands.items():
|
|
151
|
+
if not isinstance(command, str) or not isinstance(metadata, dict):
|
|
152
|
+
raise ContractError("component command capabilities must map names to objects")
|
|
153
|
+
for field in ("mutates_target", "machine_output", "accepts_plan_hash"):
|
|
154
|
+
if not isinstance(metadata.get(field), bool):
|
|
155
|
+
raise ContractError(f"component command {command!r} must declare boolean {field}")
|
|
156
|
+
if metadata["mutates_target"] and not metadata["machine_output"]:
|
|
157
|
+
raise ContractError(f"mutating component command {command!r} lacks machine output")
|
|
158
|
+
if value["exit_codes"] != EXIT_CODES:
|
|
159
|
+
raise ContractError("component exit_codes must match the pg_play capability contract")
|
|
160
|
+
if not isinstance(value["secret_policy"], dict):
|
|
161
|
+
raise ContractError("component secret_policy must be an object")
|
|
162
|
+
return value
|