datadoom 0.1.0.dev0__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.
- datadoom/__init__.py +23 -0
- datadoom/adapters/__init__.py +29 -0
- datadoom/adapters/frameworks.py +94 -0
- datadoom/adapters/loaders.py +72 -0
- datadoom/api/__init__.py +11 -0
- datadoom/api/app.py +109 -0
- datadoom/api/deps.py +30 -0
- datadoom/api/errors.py +89 -0
- datadoom/api/estimate.py +82 -0
- datadoom/api/routes/__init__.py +7 -0
- datadoom/api/routes/artifacts.py +147 -0
- datadoom/api/routes/datasets.py +180 -0
- datadoom/api/routes/meta.py +45 -0
- datadoom/api/routes/plugins.py +22 -0
- datadoom/api/routes/runs.py +144 -0
- datadoom/api/routes/specs.py +73 -0
- datadoom/api/routes/templates.py +30 -0
- datadoom/api/schemas.py +230 -0
- datadoom/api/serializers.py +143 -0
- datadoom/api/state.py +24 -0
- datadoom/api/store_helpers.py +56 -0
- datadoom/api/ws.py +72 -0
- datadoom/cli/__init__.py +1 -0
- datadoom/cli/main.py +313 -0
- datadoom/config.py +108 -0
- datadoom/engine/__init__.py +38 -0
- datadoom/engine/advice.py +289 -0
- datadoom/engine/audit.py +290 -0
- datadoom/engine/causal/__init__.py +15 -0
- datadoom/engine/causal/execute.py +116 -0
- datadoom/engine/causal/functions.py +116 -0
- datadoom/engine/causal/graph.py +54 -0
- datadoom/engine/difficulty/__init__.py +36 -0
- datadoom/engine/difficulty/calibrate.py +235 -0
- datadoom/engine/difficulty/knobs.py +171 -0
- datadoom/engine/difficulty/probes.py +181 -0
- datadoom/engine/dist/__init__.py +35 -0
- datadoom/engine/dist/base.py +46 -0
- datadoom/engine/dist/builtins.py +172 -0
- datadoom/engine/dist/compliance.py +344 -0
- datadoom/engine/dist/providers.py +117 -0
- datadoom/engine/errors.py +32 -0
- datadoom/engine/export/__init__.py +27 -0
- datadoom/engine/export/base.py +49 -0
- datadoom/engine/export/checksums.py +18 -0
- datadoom/engine/export/csv_exporter.py +34 -0
- datadoom/engine/export/json_exporter.py +67 -0
- datadoom/engine/export/metadata.py +58 -0
- datadoom/engine/export/parquet_exporter.py +45 -0
- datadoom/engine/failure/__init__.py +18 -0
- datadoom/engine/failure/apply.py +37 -0
- datadoom/engine/failure/base.py +116 -0
- datadoom/engine/failure/modes.py +442 -0
- datadoom/engine/pipeline.py +418 -0
- datadoom/engine/profile.py +327 -0
- datadoom/engine/progress.py +14 -0
- datadoom/engine/reference.py +338 -0
- datadoom/engine/reports.py +206 -0
- datadoom/engine/rng.py +79 -0
- datadoom/engine/spec/__init__.py +45 -0
- datadoom/engine/spec/hashing.py +57 -0
- datadoom/engine/spec/models.py +238 -0
- datadoom/engine/spec/validate.py +345 -0
- datadoom/engine/timeseries.py +88 -0
- datadoom/jobs/__init__.py +14 -0
- datadoom/jobs/progress.py +155 -0
- datadoom/jobs/worker.py +162 -0
- datadoom/plugin.py +35 -0
- datadoom/plugins/__init__.py +47 -0
- datadoom/plugins/contracts.py +72 -0
- datadoom/plugins/loader.py +125 -0
- datadoom/plugins/registry.py +214 -0
- datadoom/plugins/scaffold.py +434 -0
- datadoom/store/__init__.py +47 -0
- datadoom/store/artifacts.py +67 -0
- datadoom/store/db.py +104 -0
- datadoom/store/migrations/__init__.py +0 -0
- datadoom/store/migrations/env.py +53 -0
- datadoom/store/migrations/script.py.mako +24 -0
- datadoom/store/migrations/versions/0001_init.py +149 -0
- datadoom/store/migrations/versions/0002_report_mutual_information.py +23 -0
- datadoom/store/migrations/versions/0003_run_name.py +23 -0
- datadoom/store/migrations/versions/0004_report_profile.py +24 -0
- datadoom/store/models.py +170 -0
- datadoom/store/repositories.py +279 -0
- datadoom/templates/__init__.py +239 -0
- datadoom/templates/ab_test.datadoom.yaml +46 -0
- datadoom/templates/clinical_deterioration.datadoom.yaml +124 -0
- datadoom/templates/credit_default_challenge.datadoom.yaml +147 -0
- datadoom/templates/customer_churn.datadoom.yaml +60 -0
- datadoom/templates/ecommerce_orders.datadoom.yaml +46 -0
- datadoom/templates/fraud_detection.datadoom.yaml +57 -0
- datadoom/templates/hospital_readmission.datadoom.yaml +61 -0
- datadoom/templates/insurance_claims.datadoom.yaml +43 -0
- datadoom/templates/iot_sensors.datadoom.yaml +44 -0
- datadoom/templates/people_directory.datadoom.yaml +56 -0
- datadoom/templates/predictive_maintenance.datadoom.yaml +107 -0
- datadoom/templates/telecom_churn_challenge.datadoom.yaml +125 -0
- datadoom/version.py +3 -0
- datadoom/webdist/assets/index-V8VAuTJG.js +445 -0
- datadoom/webdist/assets/index-doRjyG5s.css +1 -0
- datadoom/webdist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- datadoom/webdist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- datadoom/webdist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- datadoom/webdist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- datadoom/webdist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- datadoom/webdist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- datadoom/webdist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- datadoom/webdist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- datadoom/webdist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- datadoom/webdist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- datadoom/webdist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- datadoom/webdist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- datadoom/webdist/assets/space-grotesk-latin-ext-wght-normal-D9tNdqV9.woff2 +0 -0
- datadoom/webdist/assets/space-grotesk-latin-wght-normal-BhU9QXUp.woff2 +0 -0
- datadoom/webdist/assets/space-grotesk-vietnamese-wght-normal-D0rl6rjA.woff2 +0 -0
- datadoom/webdist/index.html +15 -0
- datadoom-0.1.0.dev0.dist-info/METADATA +143 -0
- datadoom-0.1.0.dev0.dist-info/RECORD +122 -0
- datadoom-0.1.0.dev0.dist-info/WHEEL +4 -0
- datadoom-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- datadoom-0.1.0.dev0.dist-info/licenses/LICENSE +202 -0
datadoom/api/schemas.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Request/response models — the typed surface FastAPI turns into OpenAPI.
|
|
2
|
+
|
|
3
|
+
The frontend generates its API client from ``/api/openapi.json``, so these
|
|
4
|
+
shapes ARE the contract (doc 08). Spec bodies travel as open ``dict`` payloads
|
|
5
|
+
(the authoritative validation lives in ``engine.spec``); these models describe
|
|
6
|
+
the persistence/metadata envelope around them.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
SpecBody = dict[str, Any]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# --- errors -------------------------------------------------------------------
|
|
19
|
+
class ErrorDetail(BaseModel):
|
|
20
|
+
code: str
|
|
21
|
+
message: str
|
|
22
|
+
locator: str | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ErrorResponse(BaseModel):
|
|
26
|
+
error: ErrorDetail
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# --- specs (stateless helpers) ------------------------------------------------
|
|
30
|
+
class ValidateResponse(BaseModel):
|
|
31
|
+
valid: bool = True
|
|
32
|
+
spec_hash: str
|
|
33
|
+
warnings: list[str] = Field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ParseTextRequest(BaseModel):
|
|
37
|
+
"""Raw spec text (YAML or JSON) for the web 'New from YAML' import flow."""
|
|
38
|
+
|
|
39
|
+
text: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ParseResponse(BaseModel):
|
|
43
|
+
valid: bool = True
|
|
44
|
+
spec_hash: str
|
|
45
|
+
spec: SpecBody # the parsed, validated spec body (JSON form)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class HashResponse(BaseModel):
|
|
49
|
+
spec_hash: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class EstimateResponse(BaseModel):
|
|
53
|
+
estimated_runtime_seconds: float
|
|
54
|
+
estimated_ram_mb: float
|
|
55
|
+
estimated_size_bytes: int
|
|
56
|
+
features: int
|
|
57
|
+
edges: int
|
|
58
|
+
gpu_required: bool = False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# --- specs of a dataset -------------------------------------------------------
|
|
62
|
+
class SpecSummary(BaseModel):
|
|
63
|
+
spec_id: str
|
|
64
|
+
spec_hash: str
|
|
65
|
+
version: int
|
|
66
|
+
datadoom_version: str
|
|
67
|
+
created_at: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class SpecDetail(SpecSummary):
|
|
71
|
+
body: SpecBody
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SaveSpecResponse(BaseModel):
|
|
75
|
+
spec_id: str
|
|
76
|
+
spec_hash: str
|
|
77
|
+
version: int
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# --- runs ---------------------------------------------------------------------
|
|
81
|
+
class RunSummary(BaseModel):
|
|
82
|
+
run_id: str
|
|
83
|
+
dataset_id: str
|
|
84
|
+
spec_id: str
|
|
85
|
+
spec_hash: str | None = None
|
|
86
|
+
name: str | None = None
|
|
87
|
+
seed: int
|
|
88
|
+
status: str
|
|
89
|
+
stage: str | None = None
|
|
90
|
+
progress_pct: int = 0
|
|
91
|
+
compliance_score: float | None = None
|
|
92
|
+
error: dict[str, Any] | None = None
|
|
93
|
+
metrics: dict[str, Any] | None = None
|
|
94
|
+
started_at: str | None = None
|
|
95
|
+
finished_at: str | None = None
|
|
96
|
+
created_at: str
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class CreateRunRequest(BaseModel):
|
|
100
|
+
seed: int | None = None
|
|
101
|
+
name: str | None = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class UpdateRunRequest(BaseModel):
|
|
105
|
+
name: str
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class CreateRunResponse(BaseModel):
|
|
109
|
+
run_id: str
|
|
110
|
+
status: str
|
|
111
|
+
seed: int
|
|
112
|
+
ws: str
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class CancelResponse(BaseModel):
|
|
116
|
+
status: str
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# --- datasets -----------------------------------------------------------------
|
|
120
|
+
class LatestRun(BaseModel):
|
|
121
|
+
run_id: str
|
|
122
|
+
status: str
|
|
123
|
+
compliance_score: float | None = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class DatasetSummary(BaseModel):
|
|
127
|
+
dataset_id: str
|
|
128
|
+
name: str
|
|
129
|
+
description: str | None = None
|
|
130
|
+
status: str
|
|
131
|
+
rows: int | None = None
|
|
132
|
+
features: int | None = None
|
|
133
|
+
compliance_score: float | None = None
|
|
134
|
+
created_at: str
|
|
135
|
+
updated_at: str
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class DatasetList(BaseModel):
|
|
139
|
+
items: list[DatasetSummary]
|
|
140
|
+
total: int
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class Dataset(BaseModel):
|
|
144
|
+
dataset_id: str
|
|
145
|
+
name: str
|
|
146
|
+
description: str | None = None
|
|
147
|
+
status: str
|
|
148
|
+
current_spec: SpecDetail | None = None
|
|
149
|
+
latest_run: LatestRun | None = None
|
|
150
|
+
created_at: str
|
|
151
|
+
updated_at: str
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class CreateDatasetRequest(BaseModel):
|
|
155
|
+
name: str
|
|
156
|
+
description: str | None = None
|
|
157
|
+
spec: SpecBody | None = None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class UpdateDatasetRequest(BaseModel):
|
|
161
|
+
name: str | None = None
|
|
162
|
+
description: str | None = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# --- artifacts & reports ------------------------------------------------------
|
|
166
|
+
class Artifact(BaseModel):
|
|
167
|
+
artifact_id: str
|
|
168
|
+
run_id: str
|
|
169
|
+
version: str
|
|
170
|
+
split: str | None = None
|
|
171
|
+
format: str
|
|
172
|
+
filename: str
|
|
173
|
+
size_bytes: int
|
|
174
|
+
checksum_sha256: str
|
|
175
|
+
created_at: str
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class Report(BaseModel):
|
|
179
|
+
report_id: str
|
|
180
|
+
run_id: str
|
|
181
|
+
compliance_score: float | None = None
|
|
182
|
+
distribution: dict[str, Any] | None = None
|
|
183
|
+
correlation: dict[str, Any] | None = None
|
|
184
|
+
mutual_information: dict[str, Any] | None = None
|
|
185
|
+
causal_truth: dict[str, Any] | None = None
|
|
186
|
+
difficulty: dict[str, Any] | None = None
|
|
187
|
+
failures: dict[str, Any] | None = None
|
|
188
|
+
profile: dict[str, Any] | None = None
|
|
189
|
+
determinism: dict[str, Any] | None = None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class PreviewResponse(BaseModel):
|
|
193
|
+
columns: list[str]
|
|
194
|
+
rows: list[list[Any]]
|
|
195
|
+
total: int
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# --- templates & plugins & meta ----------------------------------------------
|
|
199
|
+
class TemplateSummary(BaseModel):
|
|
200
|
+
id: str
|
|
201
|
+
name: str
|
|
202
|
+
domain: str
|
|
203
|
+
description: str
|
|
204
|
+
tags: list[str] = Field(default_factory=list)
|
|
205
|
+
level: str = "starter" # "starter" | "hackathon"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class TemplateDetail(TemplateSummary):
|
|
209
|
+
spec: dict[str, Any]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class PluginInfo(BaseModel):
|
|
213
|
+
name: str
|
|
214
|
+
kind: str
|
|
215
|
+
version: str | None = None
|
|
216
|
+
schema_: dict[str, Any] | None = Field(default=None, alias="schema")
|
|
217
|
+
source: str = "builtin" # builtin | entrypoint | local
|
|
218
|
+
builtin: bool = True
|
|
219
|
+
enabled: bool = True
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class HealthResponse(BaseModel):
|
|
223
|
+
status: str = "ok"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class VersionResponse(BaseModel):
|
|
227
|
+
version: str
|
|
228
|
+
datadoom_version: str
|
|
229
|
+
python: str
|
|
230
|
+
platform: str
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""ORM row -> API schema converters (keeps route bodies thin)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from datadoom.store import (
|
|
8
|
+
ArtifactRow,
|
|
9
|
+
DatasetRow,
|
|
10
|
+
GenerationRunRow,
|
|
11
|
+
ReportRow,
|
|
12
|
+
SpecRow,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from . import schemas
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def spec_summary(row: SpecRow) -> schemas.SpecSummary:
|
|
19
|
+
return schemas.SpecSummary(
|
|
20
|
+
spec_id=row.spec_id,
|
|
21
|
+
spec_hash=row.spec_hash,
|
|
22
|
+
version=row.version,
|
|
23
|
+
datadoom_version=row.datadoom_version,
|
|
24
|
+
created_at=row.created_at,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def spec_detail(row: SpecRow) -> schemas.SpecDetail:
|
|
29
|
+
return schemas.SpecDetail(
|
|
30
|
+
spec_id=row.spec_id,
|
|
31
|
+
spec_hash=row.spec_hash,
|
|
32
|
+
version=row.version,
|
|
33
|
+
datadoom_version=row.datadoom_version,
|
|
34
|
+
created_at=row.created_at,
|
|
35
|
+
body=dict(row.body),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run_summary(row: GenerationRunRow) -> schemas.RunSummary:
|
|
40
|
+
compliance = None
|
|
41
|
+
if row.metrics:
|
|
42
|
+
compliance = row.metrics.get("compliance_score")
|
|
43
|
+
return schemas.RunSummary(
|
|
44
|
+
run_id=row.run_id,
|
|
45
|
+
dataset_id=row.dataset_id,
|
|
46
|
+
spec_id=row.spec_id,
|
|
47
|
+
spec_hash=row.spec.spec_hash if row.spec is not None else None,
|
|
48
|
+
name=row.name,
|
|
49
|
+
seed=row.seed,
|
|
50
|
+
status=row.status,
|
|
51
|
+
stage=row.stage,
|
|
52
|
+
progress_pct=row.progress_pct,
|
|
53
|
+
compliance_score=compliance,
|
|
54
|
+
error=row.error,
|
|
55
|
+
metrics=row.metrics,
|
|
56
|
+
started_at=row.started_at,
|
|
57
|
+
finished_at=row.finished_at,
|
|
58
|
+
created_at=row.created_at,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def latest_run(row: GenerationRunRow | None) -> schemas.LatestRun | None:
|
|
63
|
+
if row is None:
|
|
64
|
+
return None
|
|
65
|
+
compliance = row.metrics.get("compliance_score") if row.metrics else None
|
|
66
|
+
return schemas.LatestRun(
|
|
67
|
+
run_id=row.run_id, status=row.status, compliance_score=compliance
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _spec_stats(body: dict[str, Any] | None) -> tuple[int | None, int | None]:
|
|
72
|
+
if not body:
|
|
73
|
+
return None, None
|
|
74
|
+
rows = body.get("rows")
|
|
75
|
+
features = body.get("features")
|
|
76
|
+
return rows, (len(features) if isinstance(features, dict) else None)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def dataset_summary(
|
|
80
|
+
row: DatasetRow, current_spec: SpecRow | None, latest: GenerationRunRow | None
|
|
81
|
+
) -> schemas.DatasetSummary:
|
|
82
|
+
rows, features = _spec_stats(current_spec.body if current_spec else None)
|
|
83
|
+
compliance = latest.metrics.get("compliance_score") if latest and latest.metrics else None
|
|
84
|
+
return schemas.DatasetSummary(
|
|
85
|
+
dataset_id=row.dataset_id,
|
|
86
|
+
name=row.name,
|
|
87
|
+
description=row.description,
|
|
88
|
+
status=row.status,
|
|
89
|
+
rows=rows,
|
|
90
|
+
features=features,
|
|
91
|
+
compliance_score=compliance,
|
|
92
|
+
created_at=row.created_at,
|
|
93
|
+
updated_at=row.updated_at,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def dataset(
|
|
98
|
+
row: DatasetRow, current_spec: SpecRow | None, latest: GenerationRunRow | None
|
|
99
|
+
) -> schemas.Dataset:
|
|
100
|
+
return schemas.Dataset(
|
|
101
|
+
dataset_id=row.dataset_id,
|
|
102
|
+
name=row.name,
|
|
103
|
+
description=row.description,
|
|
104
|
+
status=row.status,
|
|
105
|
+
current_spec=spec_detail(current_spec) if current_spec else None,
|
|
106
|
+
latest_run=latest_run(latest),
|
|
107
|
+
created_at=row.created_at,
|
|
108
|
+
updated_at=row.updated_at,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def artifact(row: ArtifactRow) -> schemas.Artifact:
|
|
113
|
+
# The real on-disk filename is the basename of the storage URI — the
|
|
114
|
+
# authoritative name (data.csv, data.injected.csv, metadata.json, …) so the
|
|
115
|
+
# UI never has to guess clean-vs-injected from version/format.
|
|
116
|
+
filename = row.storage_uri.replace("\\", "/").rsplit("/", 1)[-1]
|
|
117
|
+
return schemas.Artifact(
|
|
118
|
+
artifact_id=row.artifact_id,
|
|
119
|
+
run_id=row.run_id,
|
|
120
|
+
version=row.version,
|
|
121
|
+
split=row.split,
|
|
122
|
+
format=row.format,
|
|
123
|
+
filename=filename,
|
|
124
|
+
size_bytes=row.size_bytes,
|
|
125
|
+
checksum_sha256=row.checksum_sha256,
|
|
126
|
+
created_at=row.created_at,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def report(row: ReportRow) -> schemas.Report:
|
|
131
|
+
return schemas.Report(
|
|
132
|
+
report_id=row.report_id,
|
|
133
|
+
run_id=row.run_id,
|
|
134
|
+
compliance_score=row.compliance_score,
|
|
135
|
+
distribution=row.distribution,
|
|
136
|
+
correlation=row.correlation,
|
|
137
|
+
mutual_information=row.mutual_information,
|
|
138
|
+
causal_truth=row.causal_truth,
|
|
139
|
+
difficulty=row.difficulty,
|
|
140
|
+
failures=row.failures,
|
|
141
|
+
profile=row.profile,
|
|
142
|
+
determinism=row.determinism,
|
|
143
|
+
)
|
datadoom/api/state.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Shared application state, assembled by the app factory and hung on
|
|
2
|
+
``app.state.dd``. Holds the singletons the routes need: config, the database,
|
|
3
|
+
the artifact store, the event hub, and the worker pool.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
|
|
10
|
+
from datadoom.config import Config
|
|
11
|
+
from datadoom.jobs import EventHub, WorkerPool
|
|
12
|
+
from datadoom.store import ArtifactStore, Database
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class AppState:
|
|
17
|
+
config: Config
|
|
18
|
+
db: Database
|
|
19
|
+
artifacts: ArtifactStore
|
|
20
|
+
hub: EventHub
|
|
21
|
+
worker: WorkerPool
|
|
22
|
+
# In-process idempotency map: (dataset_id, key) -> run_id (08 §1). Sufficient
|
|
23
|
+
# for the single-process local server; team mode would persist this.
|
|
24
|
+
idempotency: dict[tuple[str, str], str] = field(default_factory=dict)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Thin helpers shared by routes: dataset loading + latest-run lookup.
|
|
2
|
+
|
|
3
|
+
Re-exports the store repositories so route modules import them from one place,
|
|
4
|
+
and centralizes the "404 if missing" and "latest run" patterns.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from sqlalchemy.orm import Session
|
|
10
|
+
|
|
11
|
+
from datadoom.store import (
|
|
12
|
+
ArtifactRepository,
|
|
13
|
+
DatasetRepository,
|
|
14
|
+
DatasetRow,
|
|
15
|
+
GenerationRunRow,
|
|
16
|
+
ReportRepository,
|
|
17
|
+
RunRepository,
|
|
18
|
+
SpecRepository,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from .errors import http_error
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"DatasetRepository",
|
|
25
|
+
"SpecRepository",
|
|
26
|
+
"RunRepository",
|
|
27
|
+
"ArtifactRepository",
|
|
28
|
+
"ReportRepository",
|
|
29
|
+
"load_dataset",
|
|
30
|
+
"load_run",
|
|
31
|
+
"latest_run_row",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_dataset(session: Session, dataset_id: str) -> DatasetRow:
|
|
36
|
+
row = DatasetRepository(session).get(dataset_id)
|
|
37
|
+
if row is None:
|
|
38
|
+
raise http_error(404, "not_found", f"dataset {dataset_id} not found")
|
|
39
|
+
return row
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_run(session: Session, run_id: str) -> GenerationRunRow:
|
|
43
|
+
row = RunRepository(session).get(run_id)
|
|
44
|
+
if row is None:
|
|
45
|
+
raise http_error(404, "not_found", f"run {run_id} not found")
|
|
46
|
+
return row
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def latest_run_row(runs: RunRepository, dataset: DatasetRow) -> GenerationRunRow | None:
|
|
50
|
+
"""Prefer the dataset's recorded latest run, else the most recent by time."""
|
|
51
|
+
if dataset.latest_run_id:
|
|
52
|
+
found = runs.get(dataset.latest_run_id)
|
|
53
|
+
if found is not None:
|
|
54
|
+
return found
|
|
55
|
+
rows = runs.list_for_dataset(dataset.dataset_id)
|
|
56
|
+
return rows[0] if rows else None
|
datadoom/api/ws.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Live progress transport (08 §7): WebSocket primary, SSE fallback.
|
|
2
|
+
|
|
3
|
+
Both subscribe to the :class:`~datadoom.jobs.progress.EventHub`, which replays
|
|
4
|
+
the stage events so far to a late subscriber, then streams live updates until a
|
|
5
|
+
terminal event (``completed`` / ``failed`` / ``cancelled``). The WS channel also
|
|
6
|
+
accepts ``{"type":"cancel"}`` from the client.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import contextlib
|
|
13
|
+
import json
|
|
14
|
+
|
|
15
|
+
from fastapi import APIRouter, Request, WebSocket, WebSocketDisconnect
|
|
16
|
+
from fastapi.responses import StreamingResponse
|
|
17
|
+
|
|
18
|
+
from .state import AppState
|
|
19
|
+
|
|
20
|
+
router = APIRouter(tags=["ws"])
|
|
21
|
+
|
|
22
|
+
_TERMINAL = {"completed", "failed", "cancelled"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.websocket("/api/ws/runs/{run_id}")
|
|
26
|
+
async def ws_run(websocket: WebSocket, run_id: str) -> None:
|
|
27
|
+
await websocket.accept()
|
|
28
|
+
state: AppState = websocket.app.state.dd
|
|
29
|
+
hub = state.hub
|
|
30
|
+
|
|
31
|
+
async def pump_client() -> None:
|
|
32
|
+
# Listen for client -> server messages (only "cancel" is meaningful).
|
|
33
|
+
try:
|
|
34
|
+
while True:
|
|
35
|
+
msg = await websocket.receive_text()
|
|
36
|
+
try:
|
|
37
|
+
data = json.loads(msg)
|
|
38
|
+
except ValueError:
|
|
39
|
+
continue
|
|
40
|
+
if data.get("type") == "cancel":
|
|
41
|
+
hub.request_cancel(run_id)
|
|
42
|
+
except WebSocketDisconnect:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
client_task = asyncio.create_task(pump_client())
|
|
46
|
+
try:
|
|
47
|
+
async for event in hub.subscribe(run_id):
|
|
48
|
+
await websocket.send_json(event)
|
|
49
|
+
if event.get("type") in _TERMINAL:
|
|
50
|
+
break
|
|
51
|
+
except WebSocketDisconnect:
|
|
52
|
+
pass
|
|
53
|
+
finally:
|
|
54
|
+
client_task.cancel()
|
|
55
|
+
with contextlib.suppress(RuntimeError):
|
|
56
|
+
await websocket.close()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@router.get("/api/runs/{run_id}/events")
|
|
60
|
+
async def sse_run(run_id: str, request: Request) -> StreamingResponse:
|
|
61
|
+
state: AppState = request.app.state.dd
|
|
62
|
+
hub = state.hub
|
|
63
|
+
|
|
64
|
+
async def event_stream(): # noqa: ANN202
|
|
65
|
+
async for event in hub.subscribe(run_id):
|
|
66
|
+
if await request.is_disconnected():
|
|
67
|
+
break
|
|
68
|
+
yield f"data: {json.dumps(event)}\n\n"
|
|
69
|
+
if event.get("type") in _TERMINAL:
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
return StreamingResponse(event_stream(), media_type="text/event-stream")
|
datadoom/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""DataDoom command-line interface."""
|