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.
Files changed (150) hide show
  1. dataforge/__init__.py +204 -0
  2. dataforge/__main__.py +5 -0
  3. dataforge/agent/__init__.py +16 -0
  4. dataforge/agent/providers.py +259 -0
  5. dataforge/agent/scratchpad.py +183 -0
  6. dataforge/agent/tool_actions.py +343 -0
  7. dataforge/bench/__init__.py +31 -0
  8. dataforge/bench/core.py +426 -0
  9. dataforge/bench/groq_client.py +386 -0
  10. dataforge/bench/methods.py +443 -0
  11. dataforge/bench/report.py +309 -0
  12. dataforge/bench/runner.py +247 -0
  13. dataforge/causal/__init__.py +21 -0
  14. dataforge/causal/dag.py +174 -0
  15. dataforge/causal/pc.py +232 -0
  16. dataforge/causal/root_cause.py +193 -0
  17. dataforge/cli/__init__.py +50 -0
  18. dataforge/cli/audit.py +70 -0
  19. dataforge/cli/bench.py +154 -0
  20. dataforge/cli/common.py +267 -0
  21. dataforge/cli/constraints.py +407 -0
  22. dataforge/cli/profile.py +147 -0
  23. dataforge/cli/release.py +166 -0
  24. dataforge/cli/repair.py +407 -0
  25. dataforge/cli/revert.py +139 -0
  26. dataforge/cli/watch.py +144 -0
  27. dataforge/datasets/__init__.py +25 -0
  28. dataforge/datasets/embedded/hospital/clean.csv +11 -0
  29. dataforge/datasets/embedded/hospital/dirty.csv +11 -0
  30. dataforge/datasets/real_world.py +290 -0
  31. dataforge/datasets/registry.py +103 -0
  32. dataforge/detectors/__init__.py +80 -0
  33. dataforge/detectors/base.py +145 -0
  34. dataforge/detectors/decimal_shift.py +166 -0
  35. dataforge/detectors/fd_violation.py +157 -0
  36. dataforge/detectors/type_mismatch.py +173 -0
  37. dataforge/engine/__init__.py +39 -0
  38. dataforge/engine/repair.py +905 -0
  39. dataforge/env/__init__.py +22 -0
  40. dataforge/env/environment.py +883 -0
  41. dataforge/env/observation.py +61 -0
  42. dataforge/env/openenv_core.py +161 -0
  43. dataforge/env/reward.py +128 -0
  44. dataforge/env/server.py +176 -0
  45. dataforge/evaluation_contract.py +76 -0
  46. dataforge/fixtures/hospital_10rows.csv +11 -0
  47. dataforge/fixtures/hospital_schema.yaml +17 -0
  48. dataforge/http/__init__.py +1 -0
  49. dataforge/http/problem.py +103 -0
  50. dataforge/integrations/__init__.py +1 -0
  51. dataforge/integrations/dbt.py +164 -0
  52. dataforge/observability.py +76 -0
  53. dataforge/py.typed +1 -0
  54. dataforge/release/__init__.py +1 -0
  55. dataforge/release/doctor.py +367 -0
  56. dataforge/release/full_vision.py +702 -0
  57. dataforge/release/gate.py +861 -0
  58. dataforge/release/playground_check.py +411 -0
  59. dataforge/repair_contract.py +468 -0
  60. dataforge/repairers/__init__.py +88 -0
  61. dataforge/repairers/base.py +77 -0
  62. dataforge/repairers/decimal_shift.py +43 -0
  63. dataforge/repairers/fd_violation.py +225 -0
  64. dataforge/repairers/type_mismatch.py +73 -0
  65. dataforge/safety/__init__.py +5 -0
  66. dataforge/safety/adversarial/attack_01_phone_pii.yaml +8 -0
  67. dataforge/safety/adversarial/attack_02_phone_pii.yaml +8 -0
  68. dataforge/safety/adversarial/attack_03_phone_pii.yaml +8 -0
  69. dataforge/safety/adversarial/attack_04_phone_pii.yaml +8 -0
  70. dataforge/safety/adversarial/attack_05_phone_pii.yaml +8 -0
  71. dataforge/safety/adversarial/attack_06_phone_pii.yaml +8 -0
  72. dataforge/safety/adversarial/attack_07_phone_pii.yaml +8 -0
  73. dataforge/safety/adversarial/attack_08_phone_pii.yaml +8 -0
  74. dataforge/safety/adversarial/attack_09_phone_pii.yaml +8 -0
  75. dataforge/safety/adversarial/attack_10_phone_pii.yaml +8 -0
  76. dataforge/safety/adversarial/attack_11_ssn_pii.yaml +8 -0
  77. dataforge/safety/adversarial/attack_12_ssn_pii.yaml +8 -0
  78. dataforge/safety/adversarial/attack_13_ssn_pii.yaml +8 -0
  79. dataforge/safety/adversarial/attack_14_ssn_pii.yaml +8 -0
  80. dataforge/safety/adversarial/attack_15_ssn_pii.yaml +8 -0
  81. dataforge/safety/adversarial/attack_16_ssn_pii.yaml +8 -0
  82. dataforge/safety/adversarial/attack_17_ssn_pii.yaml +8 -0
  83. dataforge/safety/adversarial/attack_18_ssn_pii.yaml +8 -0
  84. dataforge/safety/adversarial/attack_19_ssn_pii.yaml +8 -0
  85. dataforge/safety/adversarial/attack_20_ssn_pii.yaml +8 -0
  86. dataforge/safety/adversarial/attack_21_email_pii.yaml +8 -0
  87. dataforge/safety/adversarial/attack_22_email_pii.yaml +8 -0
  88. dataforge/safety/adversarial/attack_23_email_pii.yaml +8 -0
  89. dataforge/safety/adversarial/attack_24_email_pii.yaml +8 -0
  90. dataforge/safety/adversarial/attack_25_email_pii.yaml +8 -0
  91. dataforge/safety/adversarial/attack_26_email_pii.yaml +8 -0
  92. dataforge/safety/adversarial/attack_27_email_pii.yaml +8 -0
  93. dataforge/safety/adversarial/attack_28_email_pii.yaml +8 -0
  94. dataforge/safety/adversarial/attack_29_email_pii.yaml +8 -0
  95. dataforge/safety/adversarial/attack_30_email_pii.yaml +8 -0
  96. dataforge/safety/adversarial/attack_31_row_delete.yaml +7 -0
  97. dataforge/safety/adversarial/attack_32_row_delete.yaml +8 -0
  98. dataforge/safety/adversarial/attack_33_row_delete.yaml +7 -0
  99. dataforge/safety/adversarial/attack_34_row_delete.yaml +7 -0
  100. dataforge/safety/adversarial/attack_35_row_delete.yaml +7 -0
  101. dataforge/safety/adversarial/attack_36_row_delete.yaml +11 -0
  102. dataforge/safety/adversarial/attack_37_row_delete.yaml +7 -0
  103. dataforge/safety/adversarial/attack_38_row_delete.yaml +7 -0
  104. dataforge/safety/adversarial/attack_39_row_delete.yaml +8 -0
  105. dataforge/safety/adversarial/attack_40_row_delete.yaml +7 -0
  106. dataforge/safety/adversarial/attack_41_row_delete.yaml +7 -0
  107. dataforge/safety/adversarial/attack_42_row_delete.yaml +7 -0
  108. dataforge/safety/adversarial/attack_43_row_delete.yaml +7 -0
  109. dataforge/safety/adversarial/attack_44_row_delete.yaml +7 -0
  110. dataforge/safety/adversarial/attack_45_row_delete.yaml +8 -0
  111. dataforge/safety/adversarial/attack_46_row_delete.yaml +8 -0
  112. dataforge/safety/adversarial/attack_47_row_delete.yaml +7 -0
  113. dataforge/safety/adversarial/attack_48_row_delete.yaml +7 -0
  114. dataforge/safety/adversarial/attack_49_row_delete.yaml +8 -0
  115. dataforge/safety/adversarial/attack_50_row_delete.yaml +7 -0
  116. dataforge/safety/constitution.py +307 -0
  117. dataforge/safety/constitutions/default.yaml +40 -0
  118. dataforge/safety/filter.py +134 -0
  119. dataforge/schema_inference.py +620 -0
  120. dataforge/stores/__init__.py +46 -0
  121. dataforge/stores/base.py +73 -0
  122. dataforge/stores/cloud.py +78 -0
  123. dataforge/stores/csv.py +94 -0
  124. dataforge/stores/duckdb.py +313 -0
  125. dataforge/stores/patch_plan.py +178 -0
  126. dataforge/stores/registry.py +82 -0
  127. dataforge/stores/repair.py +121 -0
  128. dataforge/stores/revert.py +22 -0
  129. dataforge/stores/sql.py +27 -0
  130. dataforge/table.py +228 -0
  131. dataforge/transactions/__init__.py +34 -0
  132. dataforge/transactions/files.py +96 -0
  133. dataforge/transactions/log.py +613 -0
  134. dataforge/transactions/revert.py +102 -0
  135. dataforge/transactions/txn.py +104 -0
  136. dataforge/ui/__init__.py +1 -0
  137. dataforge/ui/profile_view.py +136 -0
  138. dataforge/ui/repair_diff.py +91 -0
  139. dataforge/verifier/__init__.py +55 -0
  140. dataforge/verifier/constraint_ir.py +155 -0
  141. dataforge/verifier/explain.py +47 -0
  142. dataforge/verifier/gate.py +5 -0
  143. dataforge/verifier/schema.py +111 -0
  144. dataforge/verifier/smt.py +433 -0
  145. dataforge_07-0.1.0.dist-info/METADATA +436 -0
  146. dataforge_07-0.1.0.dist-info/RECORD +150 -0
  147. dataforge_07-0.1.0.dist-info/WHEEL +5 -0
  148. dataforge_07-0.1.0.dist-info/entry_points.txt +3 -0
  149. dataforge_07-0.1.0.dist-info/licenses/LICENSE +176 -0
  150. dataforge_07-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,103 @@
1
+ """RFC 9457 problem details helpers for FastAPI surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ from fastapi import HTTPException, Request
9
+ from fastapi.responses import JSONResponse
10
+ from pydantic import BaseModel, ConfigDict, Field
11
+
12
+
13
+ class ProblemDetail(BaseModel):
14
+ """RFC 9457 problem detail response with extension members."""
15
+
16
+ type: str = Field(default="about:blank")
17
+ title: str
18
+ status: int
19
+ detail: str
20
+ instance: str | None = None
21
+
22
+ model_config = ConfigDict(strict=True, extra="allow")
23
+
24
+
25
+ def problem_body(
26
+ *,
27
+ status: int,
28
+ title: str,
29
+ detail: str,
30
+ type_: str = "about:blank",
31
+ instance: str | None = None,
32
+ **extensions: Any,
33
+ ) -> dict[str, Any]:
34
+ """Build a problem details JSON object."""
35
+ body = ProblemDetail(
36
+ type=type_,
37
+ title=title,
38
+ status=status,
39
+ detail=detail,
40
+ instance=instance,
41
+ **extensions,
42
+ )
43
+ return body.model_dump(mode="json", exclude_none=True)
44
+
45
+
46
+ def problem_response(
47
+ *,
48
+ status: int,
49
+ title: str,
50
+ detail: str,
51
+ type_: str = "about:blank",
52
+ instance: str | None = None,
53
+ headers: Mapping[str, str] | None = None,
54
+ **extensions: Any,
55
+ ) -> JSONResponse:
56
+ """Return an RFC 9457 JSON response."""
57
+ return JSONResponse(
58
+ status_code=status,
59
+ content=problem_body(
60
+ status=status,
61
+ title=title,
62
+ detail=detail,
63
+ type_=type_,
64
+ instance=instance,
65
+ **extensions,
66
+ ),
67
+ headers=headers,
68
+ media_type="application/problem+json",
69
+ )
70
+
71
+
72
+ async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
73
+ """Normalize FastAPI HTTPException values into problem details."""
74
+ raw_detail = exc.detail
75
+ extensions: dict[str, Any] = {}
76
+ if isinstance(raw_detail, dict):
77
+ error_code = str(raw_detail.get("error", "http_error"))
78
+ message = str(raw_detail.get("message") or raw_detail.get("detail") or error_code)
79
+ extensions.update(raw_detail)
80
+ else:
81
+ error_code = "http_error"
82
+ message = str(raw_detail)
83
+
84
+ request_id = getattr(request.state, "dataforge_request_id", None)
85
+ if isinstance(request_id, str) and request_id and "request_id" not in extensions:
86
+ extensions["request_id"] = request_id
87
+
88
+ return problem_response(
89
+ status=exc.status_code,
90
+ type_=f"https://dataforge.local/problems/{error_code}",
91
+ title=error_code.replace("_", " ").title(),
92
+ detail=message,
93
+ instance=str(request.url.path),
94
+ headers=exc.headers,
95
+ **extensions,
96
+ )
97
+
98
+
99
+ async def problem_exception_handler(request: Request, exc: Exception) -> JSONResponse:
100
+ """Adapter with the broad exception signature Starlette expects."""
101
+ if isinstance(exc, HTTPException):
102
+ return await http_exception_handler(request, exc)
103
+ raise exc
@@ -0,0 +1 @@
1
+ """Integration package scaffolding for DataForge."""
@@ -0,0 +1,164 @@
1
+ """dbt artifact helpers for DataForge's local repair contract.
2
+
3
+ The external ``dataforge_07_dbt`` distribution can call these helpers without
4
+ duplicating dbt manifest parsing rules. Only dbt generic tests that DataForge
5
+ can represent as local constraints are mapped; everything else is ignored
6
+ conservatively.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from collections.abc import Mapping
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from dataforge.verifier.schema import AcceptedValues, RelationshipConstraint, Schema
17
+
18
+
19
+ def schema_from_dbt_artifacts(
20
+ manifest_path: Path,
21
+ *,
22
+ model_name: str | None = None,
23
+ model_unique_id: str | None = None,
24
+ ) -> Schema:
25
+ """Load a dbt manifest and map supported generic tests into a ``Schema``."""
26
+ payload = json.loads(manifest_path.read_text(encoding="utf-8"))
27
+ if not isinstance(payload, dict):
28
+ raise ValueError("dbt manifest must be a JSON object.")
29
+ return schema_from_dbt_manifest(
30
+ payload,
31
+ model_name=model_name,
32
+ model_unique_id=model_unique_id,
33
+ )
34
+
35
+
36
+ def schema_from_dbt_manifest(
37
+ manifest: Mapping[str, Any],
38
+ *,
39
+ model_name: str | None = None,
40
+ model_unique_id: str | None = None,
41
+ ) -> Schema:
42
+ """Map supported dbt generic tests from a manifest into DataForge constraints.
43
+
44
+ Supported dbt tests:
45
+ - ``not_null`` -> ``Schema.not_null_columns``
46
+ - ``unique`` -> ``Schema.unique_columns``
47
+ - ``accepted_values`` -> ``Schema.accepted_values``
48
+ - ``relationships`` -> ``Schema.relationships``
49
+ """
50
+ target_model = _resolve_target_model(manifest, model_name, model_unique_id)
51
+ nodes = _mapping(manifest.get("nodes"))
52
+ columns = _model_columns(target_model)
53
+ not_null_columns: set[str] = set()
54
+ unique_columns: set[str] = set()
55
+ accepted_values: list[AcceptedValues] = []
56
+ relationships: list[RelationshipConstraint] = []
57
+
58
+ for raw_node in nodes.values():
59
+ node = _mapping(raw_node)
60
+ if node.get("resource_type") != "test":
61
+ continue
62
+ if target_model is not None and not _test_depends_on_model(node, target_model):
63
+ continue
64
+ metadata = _mapping(node.get("test_metadata"))
65
+ test_name = str(metadata.get("name", node.get("name", ""))).strip()
66
+ kwargs = _mapping(metadata.get("kwargs"))
67
+ column = _column_name(kwargs, node)
68
+ if not column:
69
+ continue
70
+
71
+ if test_name == "not_null":
72
+ not_null_columns.add(column)
73
+ elif test_name == "unique":
74
+ unique_columns.add(column)
75
+ elif test_name == "accepted_values":
76
+ values = _accepted_values(kwargs)
77
+ if values:
78
+ accepted_values.append(AcceptedValues(column=column, values=values))
79
+ elif test_name == "relationships":
80
+ reference = str(kwargs.get("to", "")).strip()
81
+ reference_column = str(kwargs.get("field", "")).strip()
82
+ if reference and reference_column:
83
+ relationships.append(
84
+ RelationshipConstraint(
85
+ column=column,
86
+ reference=reference,
87
+ reference_column=reference_column,
88
+ )
89
+ )
90
+
91
+ return Schema(
92
+ columns=columns,
93
+ not_null_columns=frozenset(not_null_columns),
94
+ unique_columns=frozenset(unique_columns),
95
+ accepted_values=tuple(accepted_values),
96
+ relationships=tuple(relationships),
97
+ )
98
+
99
+
100
+ def _mapping(value: object) -> Mapping[str, Any]:
101
+ """Return a mapping view for JSON objects, or an empty mapping."""
102
+ return value if isinstance(value, Mapping) else {}
103
+
104
+
105
+ def _resolve_target_model(
106
+ manifest: Mapping[str, Any],
107
+ model_name: str | None,
108
+ model_unique_id: str | None,
109
+ ) -> Mapping[str, Any] | None:
110
+ """Resolve the optional target model node from a dbt manifest."""
111
+ if model_name is None and model_unique_id is None:
112
+ return None
113
+ nodes = _mapping(manifest.get("nodes"))
114
+ for unique_id, raw_node in nodes.items():
115
+ node = _mapping(raw_node)
116
+ if node.get("resource_type") != "model":
117
+ continue
118
+ if model_unique_id is not None and str(unique_id) == model_unique_id:
119
+ return {**node, "unique_id": str(unique_id)}
120
+ if model_name is not None and str(node.get("name", "")) == model_name:
121
+ return {**node, "unique_id": str(unique_id)}
122
+ raise ValueError("Requested dbt model was not found in manifest.")
123
+
124
+
125
+ def _model_columns(model_node: Mapping[str, Any] | None) -> dict[str, str]:
126
+ """Extract declared column types from a dbt model node when available."""
127
+ if model_node is None:
128
+ return {}
129
+ columns: dict[str, str] = {}
130
+ for column_name, raw_column in _mapping(model_node.get("columns")).items():
131
+ column = _mapping(raw_column)
132
+ raw_type = column.get("data_type") or column.get("type")
133
+ if raw_type:
134
+ columns[str(column_name)] = str(raw_type)
135
+ return columns
136
+
137
+
138
+ def _test_depends_on_model(
139
+ test_node: Mapping[str, Any],
140
+ model_node: Mapping[str, Any],
141
+ ) -> bool:
142
+ """Return whether a dbt test node depends on the target model."""
143
+ target_unique_id = str(model_node.get("unique_id", ""))
144
+ if not target_unique_id:
145
+ target_unique_id = str(model_node.get("name", ""))
146
+ depends_on = _mapping(test_node.get("depends_on"))
147
+ raw_nodes = depends_on.get("nodes", [])
148
+ if not isinstance(raw_nodes, list):
149
+ return False
150
+ return target_unique_id in {str(node) for node in raw_nodes}
151
+
152
+
153
+ def _column_name(kwargs: Mapping[str, Any], test_node: Mapping[str, Any]) -> str:
154
+ """Extract the tested column name from dbt metadata variants."""
155
+ raw_column = kwargs.get("column_name") or kwargs.get("field") or test_node.get("column_name")
156
+ return str(raw_column or "").strip()
157
+
158
+
159
+ def _accepted_values(kwargs: Mapping[str, Any]) -> tuple[str, ...]:
160
+ """Extract dbt accepted_values values as a stable tuple of strings."""
161
+ raw_values = kwargs.get("values", [])
162
+ if not isinstance(raw_values, list):
163
+ return ()
164
+ return tuple(str(value) for value in raw_values)
@@ -0,0 +1,76 @@
1
+ """Optional OpenTelemetry hooks for DataForge backend surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from collections.abc import Iterator
7
+ from contextlib import contextmanager, nullcontext
8
+ from importlib import import_module
9
+ from typing import Any
10
+
11
+ _SENSITIVE_ATTR_FRAGMENTS = ("authorization", "cookie", "token", "key", "secret", "password")
12
+
13
+
14
+ def _otel_enabled() -> bool:
15
+ """Return whether optional OpenTelemetry instrumentation is enabled."""
16
+ return os.environ.get("DATAFORGE_OTEL_ENABLED", "").strip().lower() in {
17
+ "1",
18
+ "true",
19
+ "yes",
20
+ "on",
21
+ }
22
+
23
+
24
+ def _safe_attrs(attributes: dict[str, Any]) -> dict[str, str | int | float | bool]:
25
+ """Keep only scalar, non-sensitive telemetry attributes."""
26
+ safe: dict[str, str | int | float | bool] = {}
27
+ for key, value in attributes.items():
28
+ lowered = key.lower()
29
+ if any(fragment in lowered for fragment in _SENSITIVE_ATTR_FRAGMENTS):
30
+ continue
31
+ if lowered in {"row_values", "rows", "payload", "source_bytes", "csv"}:
32
+ continue
33
+ if isinstance(value, str | int | float | bool):
34
+ safe[key] = value
35
+ return safe
36
+
37
+
38
+ def configure_fastapi_observability(app: Any, *, service_name: str) -> bool:
39
+ """Instrument a FastAPI app when OpenTelemetry is explicitly enabled."""
40
+ if not _otel_enabled():
41
+ return False
42
+ try:
43
+ fastapi_instrumentation = import_module("opentelemetry.instrumentation.fastapi")
44
+ trace_module = import_module("opentelemetry.trace")
45
+ except ImportError:
46
+ return False
47
+
48
+ app.state.dataforge_service_name = service_name
49
+ fastapi_instrumentation.FastAPIInstrumentor.instrument_app(
50
+ app,
51
+ tracer_provider=trace_module.get_tracer_provider(),
52
+ excluded_urls="/api/docs,/docs,/redoc,/openapi.json",
53
+ )
54
+ return True
55
+
56
+
57
+ @contextmanager
58
+ def repair_stage_span(stage: str, **attributes: Any) -> Iterator[None]:
59
+ """Create a repair-stage span when OpenTelemetry is available."""
60
+ if not _otel_enabled():
61
+ with nullcontext():
62
+ yield
63
+ return
64
+
65
+ try:
66
+ trace_module = import_module("opentelemetry.trace")
67
+ except ImportError:
68
+ with nullcontext():
69
+ yield
70
+ return
71
+
72
+ tracer = trace_module.get_tracer("dataforge.repair")
73
+ with tracer.start_as_current_span(stage) as span:
74
+ for key, value in _safe_attrs(attributes).items():
75
+ span.set_attribute(key, value)
76
+ yield
dataforge/py.typed ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+ """Release verification helpers for DataForge."""