dataforge-07 0.1.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.
- dataforge/__init__.py +204 -0
- dataforge/__main__.py +5 -0
- dataforge/agent/__init__.py +16 -0
- dataforge/agent/providers.py +259 -0
- dataforge/agent/scratchpad.py +183 -0
- dataforge/agent/tool_actions.py +343 -0
- dataforge/bench/__init__.py +31 -0
- dataforge/bench/core.py +426 -0
- dataforge/bench/groq_client.py +386 -0
- dataforge/bench/methods.py +443 -0
- dataforge/bench/report.py +309 -0
- dataforge/bench/runner.py +247 -0
- dataforge/causal/__init__.py +21 -0
- dataforge/causal/dag.py +174 -0
- dataforge/causal/pc.py +232 -0
- dataforge/causal/root_cause.py +193 -0
- dataforge/cli/__init__.py +50 -0
- dataforge/cli/audit.py +70 -0
- dataforge/cli/bench.py +154 -0
- dataforge/cli/common.py +267 -0
- dataforge/cli/constraints.py +407 -0
- dataforge/cli/profile.py +147 -0
- dataforge/cli/release.py +166 -0
- dataforge/cli/repair.py +407 -0
- dataforge/cli/revert.py +139 -0
- dataforge/cli/watch.py +144 -0
- dataforge/datasets/__init__.py +25 -0
- dataforge/datasets/embedded/hospital/clean.csv +11 -0
- dataforge/datasets/embedded/hospital/dirty.csv +11 -0
- dataforge/datasets/real_world.py +290 -0
- dataforge/datasets/registry.py +103 -0
- dataforge/detectors/__init__.py +80 -0
- dataforge/detectors/base.py +145 -0
- dataforge/detectors/decimal_shift.py +166 -0
- dataforge/detectors/fd_violation.py +157 -0
- dataforge/detectors/type_mismatch.py +173 -0
- dataforge/engine/__init__.py +39 -0
- dataforge/engine/repair.py +905 -0
- dataforge/env/__init__.py +22 -0
- dataforge/env/environment.py +883 -0
- dataforge/env/observation.py +61 -0
- dataforge/env/openenv_core.py +161 -0
- dataforge/env/reward.py +128 -0
- dataforge/env/server.py +176 -0
- dataforge/evaluation_contract.py +76 -0
- dataforge/fixtures/hospital_10rows.csv +11 -0
- dataforge/fixtures/hospital_schema.yaml +17 -0
- dataforge/http/__init__.py +1 -0
- dataforge/http/problem.py +103 -0
- dataforge/integrations/__init__.py +1 -0
- dataforge/integrations/dbt.py +164 -0
- dataforge/observability.py +76 -0
- dataforge/py.typed +1 -0
- dataforge/release/__init__.py +1 -0
- dataforge/release/doctor.py +367 -0
- dataforge/release/full_vision.py +702 -0
- dataforge/release/gate.py +861 -0
- dataforge/release/playground_check.py +411 -0
- dataforge/repair_contract.py +468 -0
- dataforge/repairers/__init__.py +88 -0
- dataforge/repairers/base.py +77 -0
- dataforge/repairers/decimal_shift.py +43 -0
- dataforge/repairers/fd_violation.py +225 -0
- dataforge/repairers/type_mismatch.py +73 -0
- dataforge/safety/__init__.py +5 -0
- dataforge/safety/adversarial/attack_01_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_02_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_03_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_04_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_05_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_06_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_07_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_08_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_09_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_10_phone_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_11_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_12_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_13_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_14_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_15_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_16_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_17_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_18_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_19_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_20_ssn_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_21_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_22_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_23_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_24_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_25_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_26_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_27_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_28_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_29_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_30_email_pii.yaml +8 -0
- dataforge/safety/adversarial/attack_31_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_32_row_delete.yaml +8 -0
- dataforge/safety/adversarial/attack_33_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_34_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_35_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_36_row_delete.yaml +11 -0
- dataforge/safety/adversarial/attack_37_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_38_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_39_row_delete.yaml +8 -0
- dataforge/safety/adversarial/attack_40_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_41_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_42_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_43_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_44_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_45_row_delete.yaml +8 -0
- dataforge/safety/adversarial/attack_46_row_delete.yaml +8 -0
- dataforge/safety/adversarial/attack_47_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_48_row_delete.yaml +7 -0
- dataforge/safety/adversarial/attack_49_row_delete.yaml +8 -0
- dataforge/safety/adversarial/attack_50_row_delete.yaml +7 -0
- dataforge/safety/constitution.py +307 -0
- dataforge/safety/constitutions/default.yaml +40 -0
- dataforge/safety/filter.py +134 -0
- dataforge/schema_inference.py +620 -0
- dataforge/stores/__init__.py +46 -0
- dataforge/stores/base.py +73 -0
- dataforge/stores/cloud.py +78 -0
- dataforge/stores/csv.py +94 -0
- dataforge/stores/duckdb.py +313 -0
- dataforge/stores/patch_plan.py +178 -0
- dataforge/stores/registry.py +82 -0
- dataforge/stores/repair.py +121 -0
- dataforge/stores/revert.py +22 -0
- dataforge/stores/sql.py +27 -0
- dataforge/table.py +228 -0
- dataforge/transactions/__init__.py +34 -0
- dataforge/transactions/files.py +96 -0
- dataforge/transactions/log.py +613 -0
- dataforge/transactions/revert.py +102 -0
- dataforge/transactions/txn.py +104 -0
- dataforge/ui/__init__.py +1 -0
- dataforge/ui/profile_view.py +136 -0
- dataforge/ui/repair_diff.py +91 -0
- dataforge/verifier/__init__.py +55 -0
- dataforge/verifier/constraint_ir.py +155 -0
- dataforge/verifier/explain.py +47 -0
- dataforge/verifier/gate.py +5 -0
- dataforge/verifier/schema.py +111 -0
- dataforge/verifier/smt.py +433 -0
- dataforge_07-0.1.0.dist-info/METADATA +436 -0
- dataforge_07-0.1.0.dist-info/RECORD +150 -0
- dataforge_07-0.1.0.dist-info/WHEEL +5 -0
- dataforge_07-0.1.0.dist-info/entry_points.txt +3 -0
- dataforge_07-0.1.0.dist-info/licenses/LICENSE +176 -0
- dataforge_07-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""Live Playground release and monitoring checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import asdict, dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
from urllib.parse import urlsplit
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from dataforge.release.doctor import run_doctor
|
|
14
|
+
|
|
15
|
+
DEFAULT_BACKEND_URL = "https://Praneshrajan15-dataforge-playground.hf.space"
|
|
16
|
+
DEFAULT_FRONTEND_URL = "https://dataforge.praneshrajan15.workers.dev/playground"
|
|
17
|
+
NEGATIVE_CORS_ORIGIN = "https://untrusted-dataforge.example"
|
|
18
|
+
REQUIRED_HEALTH_KEYS = {"status", "advanced_available", "max_upload_bytes"}
|
|
19
|
+
ENHANCED_HEALTH_KEYS = {
|
|
20
|
+
"service",
|
|
21
|
+
"api_version",
|
|
22
|
+
"contract_version",
|
|
23
|
+
"build_sha",
|
|
24
|
+
"server_time_utc",
|
|
25
|
+
"environment",
|
|
26
|
+
"limits",
|
|
27
|
+
"cors_configured",
|
|
28
|
+
"otel_enabled",
|
|
29
|
+
"metrics",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class PlaygroundCheck:
|
|
35
|
+
"""One Playground check result."""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
ok: bool
|
|
39
|
+
detail: str
|
|
40
|
+
metadata: dict[str, Any]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class PlaygroundCheckReport:
|
|
45
|
+
"""Machine-readable Playground release report."""
|
|
46
|
+
|
|
47
|
+
ok: bool
|
|
48
|
+
frontend_url: str
|
|
49
|
+
backend_url: str
|
|
50
|
+
checks: list[PlaygroundCheck]
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict[str, Any]:
|
|
53
|
+
"""Return a JSON-serializable report."""
|
|
54
|
+
return asdict(self)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def normalize_url(value: str) -> str:
|
|
58
|
+
"""Strip whitespace and trailing slashes from a URL."""
|
|
59
|
+
return value.strip().rstrip("/")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def frontend_origin(frontend_url: str) -> str:
|
|
63
|
+
"""Return the origin portion of a frontend URL."""
|
|
64
|
+
parts = urlsplit(frontend_url)
|
|
65
|
+
return f"{parts.scheme}://{parts.netloc}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def join_url(base_url: str, path: str) -> str:
|
|
69
|
+
"""Join a normalized base URL and absolute path fragment."""
|
|
70
|
+
return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _timed_request(
|
|
74
|
+
client: httpx.Client, method: str, url: str, **kwargs: Any
|
|
75
|
+
) -> tuple[httpx.Response, float]:
|
|
76
|
+
"""Run one HTTP request and return response plus elapsed milliseconds."""
|
|
77
|
+
started = time.perf_counter()
|
|
78
|
+
response = client.request(method, url, **kwargs)
|
|
79
|
+
return response, (time.perf_counter() - started) * 1000
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _check_frontend_deployed(
|
|
83
|
+
client: httpx.Client,
|
|
84
|
+
*,
|
|
85
|
+
frontend_url: str,
|
|
86
|
+
) -> PlaygroundCheck:
|
|
87
|
+
try:
|
|
88
|
+
response, latency_ms = _timed_request(client, "GET", frontend_url)
|
|
89
|
+
body = response.text
|
|
90
|
+
ok = (
|
|
91
|
+
response.status_code == 200
|
|
92
|
+
and "<!doctype html>" in body.lower()
|
|
93
|
+
and 'id="root"' in body
|
|
94
|
+
and "config.js" in body
|
|
95
|
+
and "/playground/assets/" in body
|
|
96
|
+
)
|
|
97
|
+
detail = (
|
|
98
|
+
"Frontend serves the React shell."
|
|
99
|
+
if ok
|
|
100
|
+
else "Frontend shell is missing required markers."
|
|
101
|
+
)
|
|
102
|
+
return PlaygroundCheck(
|
|
103
|
+
"frontend_deployed",
|
|
104
|
+
ok,
|
|
105
|
+
detail,
|
|
106
|
+
{
|
|
107
|
+
"status_code": response.status_code,
|
|
108
|
+
"latency_ms": round(latency_ms, 2),
|
|
109
|
+
"content_type": response.headers.get("content-type", ""),
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
return PlaygroundCheck("frontend_deployed", False, str(exc), {})
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _check_config_js(
|
|
117
|
+
client: httpx.Client,
|
|
118
|
+
*,
|
|
119
|
+
frontend_url: str,
|
|
120
|
+
backend_url: str,
|
|
121
|
+
) -> PlaygroundCheck:
|
|
122
|
+
try:
|
|
123
|
+
response, latency_ms = _timed_request(client, "GET", join_url(frontend_url, "config.js"))
|
|
124
|
+
cache_control = response.headers.get("cache-control", "")
|
|
125
|
+
ok = (
|
|
126
|
+
response.status_code == 200
|
|
127
|
+
and backend_url in response.text
|
|
128
|
+
and "no-store" in cache_control.lower()
|
|
129
|
+
)
|
|
130
|
+
detail = (
|
|
131
|
+
"config.js points at the expected backend and is uncached."
|
|
132
|
+
if ok
|
|
133
|
+
else "config.js is stale or cacheable."
|
|
134
|
+
)
|
|
135
|
+
return PlaygroundCheck(
|
|
136
|
+
"config_js_correct",
|
|
137
|
+
ok,
|
|
138
|
+
detail,
|
|
139
|
+
{
|
|
140
|
+
"status_code": response.status_code,
|
|
141
|
+
"latency_ms": round(latency_ms, 2),
|
|
142
|
+
"cache_control": cache_control,
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
except Exception as exc:
|
|
146
|
+
return PlaygroundCheck("config_js_correct", False, str(exc), {})
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _check_backend_deployed(
|
|
150
|
+
client: httpx.Client,
|
|
151
|
+
*,
|
|
152
|
+
backend_url: str,
|
|
153
|
+
latency_threshold_ms: float,
|
|
154
|
+
) -> PlaygroundCheck:
|
|
155
|
+
try:
|
|
156
|
+
root, root_latency_ms = _timed_request(client, "GET", backend_url)
|
|
157
|
+
health, health_latency_ms = _timed_request(client, "GET", f"{backend_url}/api/health")
|
|
158
|
+
payload = health.json() if health.status_code == 200 else {}
|
|
159
|
+
missing = sorted(REQUIRED_HEALTH_KEYS - set(payload))
|
|
160
|
+
enhanced_missing = sorted(ENHANCED_HEALTH_KEYS - set(payload))
|
|
161
|
+
ok = (
|
|
162
|
+
root.status_code == 200
|
|
163
|
+
and health.status_code == 200
|
|
164
|
+
and payload.get("status") == "ok"
|
|
165
|
+
and not missing
|
|
166
|
+
and health_latency_ms <= latency_threshold_ms
|
|
167
|
+
)
|
|
168
|
+
detail = (
|
|
169
|
+
"Backend root and health are reachable."
|
|
170
|
+
if ok
|
|
171
|
+
else "Backend root or health check failed."
|
|
172
|
+
)
|
|
173
|
+
return PlaygroundCheck(
|
|
174
|
+
"backend_deployed",
|
|
175
|
+
ok,
|
|
176
|
+
detail,
|
|
177
|
+
{
|
|
178
|
+
"root_status_code": root.status_code,
|
|
179
|
+
"health_status_code": health.status_code,
|
|
180
|
+
"root_latency_ms": round(root_latency_ms, 2),
|
|
181
|
+
"health_latency_ms": round(health_latency_ms, 2),
|
|
182
|
+
"latency_threshold_ms": latency_threshold_ms,
|
|
183
|
+
"required_missing": missing,
|
|
184
|
+
"enhanced_missing": enhanced_missing,
|
|
185
|
+
"metrics": payload.get("metrics", {}),
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
except Exception as exc:
|
|
189
|
+
return PlaygroundCheck("backend_deployed", False, str(exc), {})
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _check_cors(
|
|
193
|
+
client: httpx.Client,
|
|
194
|
+
*,
|
|
195
|
+
frontend_url: str,
|
|
196
|
+
backend_url: str,
|
|
197
|
+
) -> PlaygroundCheck:
|
|
198
|
+
origin = frontend_origin(frontend_url)
|
|
199
|
+
try:
|
|
200
|
+
positive, positive_latency_ms = _timed_request(
|
|
201
|
+
client,
|
|
202
|
+
"GET",
|
|
203
|
+
f"{backend_url}/api/health",
|
|
204
|
+
headers={"Origin": origin},
|
|
205
|
+
)
|
|
206
|
+
negative, negative_latency_ms = _timed_request(
|
|
207
|
+
client,
|
|
208
|
+
"GET",
|
|
209
|
+
f"{backend_url}/api/health",
|
|
210
|
+
headers={"Origin": NEGATIVE_CORS_ORIGIN},
|
|
211
|
+
)
|
|
212
|
+
preflight, preflight_latency_ms = _timed_request(
|
|
213
|
+
client,
|
|
214
|
+
"OPTIONS",
|
|
215
|
+
f"{backend_url}/api/health",
|
|
216
|
+
headers={
|
|
217
|
+
"Origin": NEGATIVE_CORS_ORIGIN,
|
|
218
|
+
"Access-Control-Request-Method": "GET",
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
allowed_origin = positive.headers.get("access-control-allow-origin")
|
|
222
|
+
negative_allowed_origin = negative.headers.get("access-control-allow-origin")
|
|
223
|
+
preflight_allowed_origin = preflight.headers.get("access-control-allow-origin")
|
|
224
|
+
negative_error = ""
|
|
225
|
+
try:
|
|
226
|
+
negative_error = str(negative.json().get("error", ""))
|
|
227
|
+
except ValueError:
|
|
228
|
+
negative_error = ""
|
|
229
|
+
negative_denied = negative.status_code == 403 and negative_error == "origin_not_allowed"
|
|
230
|
+
ok = allowed_origin == origin and positive.status_code == 200 and negative_denied
|
|
231
|
+
detail = (
|
|
232
|
+
"Configured origin is allowed and disallowed origins cannot read API data."
|
|
233
|
+
if ok
|
|
234
|
+
else "CORS origin enforcement is incorrect."
|
|
235
|
+
)
|
|
236
|
+
return PlaygroundCheck(
|
|
237
|
+
"cors_correct",
|
|
238
|
+
ok,
|
|
239
|
+
detail,
|
|
240
|
+
{
|
|
241
|
+
"frontend_origin": origin,
|
|
242
|
+
"positive_status_code": positive.status_code,
|
|
243
|
+
"negative_status_code": negative.status_code,
|
|
244
|
+
"allowed_origin": allowed_origin,
|
|
245
|
+
"negative_allowed_origin": negative_allowed_origin,
|
|
246
|
+
"negative_error": negative_error,
|
|
247
|
+
"negative_preflight_status_code": preflight.status_code,
|
|
248
|
+
"negative_preflight_allowed_origin": preflight_allowed_origin,
|
|
249
|
+
"positive_latency_ms": round(positive_latency_ms, 2),
|
|
250
|
+
"negative_latency_ms": round(negative_latency_ms, 2),
|
|
251
|
+
"negative_preflight_latency_ms": round(preflight_latency_ms, 2),
|
|
252
|
+
},
|
|
253
|
+
)
|
|
254
|
+
except Exception as exc:
|
|
255
|
+
return PlaygroundCheck("cors_correct", False, str(exc), {})
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _check_doctor() -> PlaygroundCheck:
|
|
259
|
+
report = run_doctor(core=True, maintainer_deploy=False)
|
|
260
|
+
return PlaygroundCheck(
|
|
261
|
+
"doctor_passing",
|
|
262
|
+
report.ok,
|
|
263
|
+
"Core release doctor passed." if report.ok else "Core release doctor failed.",
|
|
264
|
+
{"doctor": report.to_dict()},
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _check_smoke_flow(client: httpx.Client, *, backend_url: str) -> PlaygroundCheck:
|
|
269
|
+
try:
|
|
270
|
+
sample, sample_latency_ms = _timed_request(
|
|
271
|
+
client,
|
|
272
|
+
"GET",
|
|
273
|
+
f"{backend_url}/api/samples/hospital_10rows",
|
|
274
|
+
)
|
|
275
|
+
if sample.status_code != 200:
|
|
276
|
+
return PlaygroundCheck(
|
|
277
|
+
"smoke_flow_passing",
|
|
278
|
+
False,
|
|
279
|
+
"Sample endpoint failed.",
|
|
280
|
+
{"sample_status_code": sample.status_code},
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
files = {"file": ("hospital_10rows.csv", sample.content, "text/csv")}
|
|
284
|
+
analyze, analyze_latency_ms = _timed_request(
|
|
285
|
+
client,
|
|
286
|
+
"POST",
|
|
287
|
+
f"{backend_url}/api/analyze",
|
|
288
|
+
files=files,
|
|
289
|
+
)
|
|
290
|
+
files = {"file": ("hospital_10rows.csv", sample.content, "text/csv")}
|
|
291
|
+
profile, profile_latency_ms = _timed_request(
|
|
292
|
+
client,
|
|
293
|
+
"POST",
|
|
294
|
+
f"{backend_url}/api/profile",
|
|
295
|
+
files=files,
|
|
296
|
+
)
|
|
297
|
+
files = {"file": ("hospital_10rows.csv", sample.content, "text/csv")}
|
|
298
|
+
repair, repair_latency_ms = _timed_request(
|
|
299
|
+
client,
|
|
300
|
+
"POST",
|
|
301
|
+
f"{backend_url}/api/repair",
|
|
302
|
+
files=files,
|
|
303
|
+
)
|
|
304
|
+
analyze_payload = analyze.json() if analyze.status_code == 200 else {}
|
|
305
|
+
profile_payload = profile.json() if profile.status_code == 200 else {}
|
|
306
|
+
repair_payload = repair.json() if repair.status_code == 200 else {}
|
|
307
|
+
analyze_required_keys = {
|
|
308
|
+
"source",
|
|
309
|
+
"risk_summary",
|
|
310
|
+
"repairs",
|
|
311
|
+
"verification",
|
|
312
|
+
"receipt",
|
|
313
|
+
"apply_handoff",
|
|
314
|
+
}
|
|
315
|
+
analyze_missing = sorted(analyze_required_keys - set(analyze_payload))
|
|
316
|
+
ok = (
|
|
317
|
+
analyze.status_code == 200
|
|
318
|
+
and profile.status_code == 200
|
|
319
|
+
and repair.status_code == 200
|
|
320
|
+
and not analyze_missing
|
|
321
|
+
and "issues" in profile_payload
|
|
322
|
+
and "fixes" in repair_payload
|
|
323
|
+
and "txn_journal" in repair_payload
|
|
324
|
+
)
|
|
325
|
+
return PlaygroundCheck(
|
|
326
|
+
"smoke_flow_passing",
|
|
327
|
+
ok,
|
|
328
|
+
(
|
|
329
|
+
"Sample analyze, profile, and repair dry-run passed."
|
|
330
|
+
if ok
|
|
331
|
+
else "Sample smoke flow failed."
|
|
332
|
+
),
|
|
333
|
+
{
|
|
334
|
+
"sample_latency_ms": round(sample_latency_ms, 2),
|
|
335
|
+
"analyze_status_code": analyze.status_code,
|
|
336
|
+
"profile_status_code": profile.status_code,
|
|
337
|
+
"repair_status_code": repair.status_code,
|
|
338
|
+
"analyze_latency_ms": round(analyze_latency_ms, 2),
|
|
339
|
+
"profile_latency_ms": round(profile_latency_ms, 2),
|
|
340
|
+
"repair_latency_ms": round(repair_latency_ms, 2),
|
|
341
|
+
"analyze_missing": analyze_missing,
|
|
342
|
+
"issue_count": len(profile_payload.get("issues", []))
|
|
343
|
+
if isinstance(profile_payload, dict)
|
|
344
|
+
else None,
|
|
345
|
+
"repair_count": len(analyze_payload.get("repairs", []))
|
|
346
|
+
if isinstance(analyze_payload, dict)
|
|
347
|
+
else None,
|
|
348
|
+
"fix_count": len(repair_payload.get("fixes", []))
|
|
349
|
+
if isinstance(repair_payload, dict)
|
|
350
|
+
else None,
|
|
351
|
+
},
|
|
352
|
+
)
|
|
353
|
+
except Exception as exc:
|
|
354
|
+
return PlaygroundCheck("smoke_flow_passing", False, str(exc), {})
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def run_playground_check(
|
|
358
|
+
*,
|
|
359
|
+
frontend_url: str = DEFAULT_FRONTEND_URL,
|
|
360
|
+
backend_url: str = DEFAULT_BACKEND_URL,
|
|
361
|
+
latency_threshold_ms: float = 5_000.0,
|
|
362
|
+
include_doctor: bool = True,
|
|
363
|
+
include_smoke: bool = True,
|
|
364
|
+
client: httpx.Client | None = None,
|
|
365
|
+
) -> PlaygroundCheckReport:
|
|
366
|
+
"""Run the public Playground release checklist."""
|
|
367
|
+
normalized_frontend_url = normalize_url(frontend_url)
|
|
368
|
+
normalized_backend_url = normalize_url(backend_url)
|
|
369
|
+
|
|
370
|
+
def collect(active_client: httpx.Client) -> list[PlaygroundCheck]:
|
|
371
|
+
checks = [
|
|
372
|
+
_check_frontend_deployed(active_client, frontend_url=normalized_frontend_url),
|
|
373
|
+
_check_config_js(
|
|
374
|
+
active_client,
|
|
375
|
+
frontend_url=normalized_frontend_url,
|
|
376
|
+
backend_url=normalized_backend_url,
|
|
377
|
+
),
|
|
378
|
+
_check_backend_deployed(
|
|
379
|
+
active_client,
|
|
380
|
+
backend_url=normalized_backend_url,
|
|
381
|
+
latency_threshold_ms=latency_threshold_ms,
|
|
382
|
+
),
|
|
383
|
+
_check_cors(
|
|
384
|
+
active_client,
|
|
385
|
+
frontend_url=normalized_frontend_url,
|
|
386
|
+
backend_url=normalized_backend_url,
|
|
387
|
+
),
|
|
388
|
+
]
|
|
389
|
+
if include_doctor:
|
|
390
|
+
checks.append(_check_doctor())
|
|
391
|
+
if include_smoke:
|
|
392
|
+
checks.append(_check_smoke_flow(active_client, backend_url=normalized_backend_url))
|
|
393
|
+
return checks
|
|
394
|
+
|
|
395
|
+
if client is not None:
|
|
396
|
+
checks = collect(client)
|
|
397
|
+
else:
|
|
398
|
+
with httpx.Client(follow_redirects=True, timeout=30.0) as owned_client:
|
|
399
|
+
checks = collect(owned_client)
|
|
400
|
+
|
|
401
|
+
return PlaygroundCheckReport(
|
|
402
|
+
ok=all(check.ok for check in checks),
|
|
403
|
+
frontend_url=normalized_frontend_url,
|
|
404
|
+
backend_url=normalized_backend_url,
|
|
405
|
+
checks=checks,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def report_to_json(report: PlaygroundCheckReport) -> str:
|
|
410
|
+
"""Render a stable JSON report."""
|
|
411
|
+
return json.dumps(report.to_dict(), indent=2, sort_keys=True)
|