eb-contracts 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.
Files changed (35) hide show
  1. eb_contracts/__init__.py +24 -0
  2. eb_contracts/api/__init__.py +27 -0
  3. eb_contracts/api/migrate_forecast.py +122 -0
  4. eb_contracts/api/validate.py +81 -0
  5. eb_contracts/contracts/__init__.py +9 -0
  6. eb_contracts/contracts/_internal/__init__.py +8 -0
  7. eb_contracts/contracts/_internal/runtime.py +48 -0
  8. eb_contracts/contracts/_internal/typing.py +37 -0
  9. eb_contracts/contracts/context/__init__.py +14 -0
  10. eb_contracts/contracts/context/v1/__init__.py +8 -0
  11. eb_contracts/contracts/context/v1/run_context.py +202 -0
  12. eb_contracts/contracts/costs/__init__.py +14 -0
  13. eb_contracts/contracts/costs/v1/__init__.py +8 -0
  14. eb_contracts/contracts/costs/v1/cost_asymmetry_spec.py +143 -0
  15. eb_contracts/contracts/demand_panel/__init__.py +24 -0
  16. eb_contracts/contracts/demand_panel/v1/__init__.py +9 -0
  17. eb_contracts/contracts/demand_panel/v1/panel_demand.py +239 -0
  18. eb_contracts/contracts/forecast_panel/__init__.py +24 -0
  19. eb_contracts/contracts/forecast_panel/v1/__init__.py +9 -0
  20. eb_contracts/contracts/forecast_panel/v1/forecast_panel.py +195 -0
  21. eb_contracts/contracts/readiness/v1/__init__.py +9 -0
  22. eb_contracts/contracts/readiness/v1/panel_fpc_result.py +316 -0
  23. eb_contracts/contracts/results/__init__.py +14 -0
  24. eb_contracts/contracts/results/v1/__init__.py +9 -0
  25. eb_contracts/contracts/results/v1/panel_point_result.py +117 -0
  26. eb_contracts/definitions/__init__.py +123 -0
  27. eb_contracts/definitions/conventions.py +129 -0
  28. eb_contracts/definitions/glossary.py +103 -0
  29. eb_contracts/definitions/semantics.py +134 -0
  30. eb_contracts/definitions/units.py +136 -0
  31. eb_contracts/validation/errors.py +29 -0
  32. eb_contracts-0.2.0.dist-info/METADATA +134 -0
  33. eb_contracts-0.2.0.dist-info/RECORD +35 -0
  34. eb_contracts-0.2.0.dist-info/WHEEL +4 -0
  35. eb_contracts-0.2.0.dist-info/licenses/LICENSE +28 -0
@@ -0,0 +1,24 @@
1
+ """
2
+ Public API for EB contracts.
3
+
4
+ This package provides contract artifacts and validation entrypoints for
5
+ forecasting and panel-based evaluation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from eb_contracts.api.validate import (
11
+ panel_point_v1,
12
+ panel_quantile_v1,
13
+ )
14
+
15
+ ######################################
16
+ # Public API
17
+ ######################################
18
+ from eb_contracts.contracts._internal.runtime import set_validation_mode
19
+
20
+ __all__ = [
21
+ "panel_point_v1",
22
+ "panel_quantile_v1",
23
+ "set_validation_mode",
24
+ ]
@@ -0,0 +1,27 @@
1
+ """
2
+ Public migration API.
3
+
4
+ This package provides explicit helpers for adapting external data into EB
5
+ contract artifacts.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ ######################################
11
+ # Public API
12
+ ######################################
13
+ from eb_contracts.api.migrate_forecast import (
14
+ PanelPointColumns,
15
+ PanelQuantileColumns,
16
+ to_panel_point_result_v1,
17
+ to_panel_point_v1,
18
+ to_panel_quantile_v1,
19
+ )
20
+
21
+ __all__ = [
22
+ "PanelPointColumns",
23
+ "PanelQuantileColumns",
24
+ "to_panel_point_result_v1",
25
+ "to_panel_point_v1",
26
+ "to_panel_quantile_v1",
27
+ ]
@@ -0,0 +1,122 @@
1
+ """
2
+ Forecast migration helpers.
3
+
4
+ This module contains explicit utilities for adapting "in the wild" forecast frames
5
+ into EB contract artifacts.
6
+
7
+ Migration is intentionally explicit:
8
+ - You provide column mappings.
9
+ - The output is a validated contract artifact (unless validation mode is off).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+ from typing import Final
16
+
17
+ import pandas as pd
18
+
19
+ from eb_contracts.contracts.forecast_panel.v1.forecast_panel import (
20
+ PanelPointForecastV1,
21
+ PanelQuantileForecastV1,
22
+ )
23
+ from eb_contracts.contracts.results.v1.panel_point_result import PanelPointResultV1
24
+
25
+ ######################################
26
+ # Mapping specs
27
+ ######################################
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class PanelPointColumns:
32
+ """Column mapping for point forecasts."""
33
+
34
+ entity_id: str
35
+ interval_start: str
36
+ y_true: str
37
+ y_pred: str
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class PanelQuantileColumns:
42
+ """Column mapping for quantile forecasts."""
43
+
44
+ entity_id: str
45
+ interval_start: str
46
+ y_true: str
47
+ q: str
48
+ y_pred_q: str
49
+
50
+
51
+ ######################################
52
+ # Public API
53
+ ######################################
54
+
55
+
56
+ _RESULT_COLUMNS: Final[set[str]] = {
57
+ PanelPointResultV1.Y_TRUE_COL,
58
+ PanelPointResultV1.Y_PRED_COL,
59
+ }
60
+
61
+
62
+ def to_panel_point_v1(frame: pd.DataFrame, *, columns: PanelPointColumns) -> PanelPointForecastV1:
63
+ """Adapt a frame into the PanelPointForecastV1 contract."""
64
+ out = frame.rename(
65
+ columns={
66
+ columns.entity_id: PanelPointForecastV1.ENTITY_COL,
67
+ columns.interval_start: PanelPointForecastV1.INTERVAL_START_COL,
68
+ columns.y_true: PanelPointForecastV1.Y_TRUE_COL,
69
+ columns.y_pred: PanelPointForecastV1.Y_PRED_COL,
70
+ }
71
+ )
72
+ keep = [
73
+ PanelPointForecastV1.ENTITY_COL,
74
+ PanelPointForecastV1.INTERVAL_START_COL,
75
+ PanelPointForecastV1.Y_TRUE_COL,
76
+ PanelPointForecastV1.Y_PRED_COL,
77
+ ]
78
+ return PanelPointForecastV1.from_frame(out.loc[:, keep])
79
+
80
+
81
+ def to_panel_quantile_v1(
82
+ frame: pd.DataFrame, *, columns: PanelQuantileColumns
83
+ ) -> PanelQuantileForecastV1:
84
+ """Adapt a frame into the PanelQuantileForecastV1 contract."""
85
+ out = frame.rename(
86
+ columns={
87
+ columns.entity_id: PanelQuantileForecastV1.ENTITY_COL,
88
+ columns.interval_start: PanelQuantileForecastV1.INTERVAL_START_COL,
89
+ columns.y_true: PanelQuantileForecastV1.Y_TRUE_COL,
90
+ columns.q: PanelQuantileForecastV1.Q_COL,
91
+ columns.y_pred_q: PanelQuantileForecastV1.Y_PRED_Q_COL,
92
+ }
93
+ )
94
+ keep = [
95
+ PanelQuantileForecastV1.ENTITY_COL,
96
+ PanelQuantileForecastV1.INTERVAL_START_COL,
97
+ PanelQuantileForecastV1.Y_TRUE_COL,
98
+ PanelQuantileForecastV1.Q_COL,
99
+ PanelQuantileForecastV1.Y_PRED_Q_COL,
100
+ ]
101
+ return PanelQuantileForecastV1.from_frame(out.loc[:, keep])
102
+
103
+
104
+ def to_panel_point_result_v1(
105
+ frame: pd.DataFrame, *, columns: PanelPointColumns
106
+ ) -> PanelPointResultV1:
107
+ """Adapt a frame into the PanelPointResultV1 contract."""
108
+ out = frame.rename(
109
+ columns={
110
+ columns.entity_id: PanelPointResultV1.ENTITY_COL,
111
+ columns.interval_start: PanelPointResultV1.INTERVAL_START_COL,
112
+ columns.y_true: PanelPointResultV1.Y_TRUE_COL,
113
+ columns.y_pred: PanelPointResultV1.Y_PRED_COL,
114
+ }
115
+ )
116
+ keep = [
117
+ PanelPointResultV1.ENTITY_COL,
118
+ PanelPointResultV1.INTERVAL_START_COL,
119
+ PanelPointResultV1.Y_TRUE_COL,
120
+ PanelPointResultV1.Y_PRED_COL,
121
+ ]
122
+ return PanelPointResultV1.from_frame(out.loc[:, keep])
@@ -0,0 +1,81 @@
1
+ """
2
+ Public validation entrypoints for contract artifacts.
3
+
4
+ This module defines stable, versioned entrypoints for validating and constructing
5
+ contract-wrapped data artifacts. Consumers should prefer these functions over
6
+ importing versioned contract modules directly.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import pandas as pd
12
+
13
+ from eb_contracts.contracts.costs.v1.cost_asymmetry_spec import CostAsymmetrySpecV1
14
+ from eb_contracts.contracts.demand_panel.v1.panel_demand import (
15
+ PanelDemandV1,
16
+ validate_panel_demand_v1,
17
+ )
18
+ from eb_contracts.contracts.forecast_panel.v1.forecast_panel import (
19
+ PanelPointForecastV1,
20
+ PanelQuantileForecastV1,
21
+ )
22
+ from eb_contracts.contracts.results.v1.panel_point_result import PanelPointResultV1
23
+
24
+ ######################################
25
+ # Public API
26
+ ######################################
27
+
28
+
29
+ def panel_demand_v1(panel: PanelDemandV1) -> None:
30
+ """Validate a V1 demand panel artifact."""
31
+ validate_panel_demand_v1(panel)
32
+
33
+
34
+ def panel_point_forecast_v1(frame: pd.DataFrame) -> PanelPointForecastV1:
35
+ """
36
+ Validate and construct a V1 panel point forecast artifact.
37
+
38
+ This is the canonical, explicit entrypoint for point forecast panels.
39
+ """
40
+ return PanelPointForecastV1.from_frame(frame)
41
+
42
+
43
+ def panel_quantile_forecast_v1(frame: pd.DataFrame) -> PanelQuantileForecastV1:
44
+ """
45
+ Validate and construct a V1 panel quantile forecast artifact.
46
+
47
+ This is the canonical, explicit entrypoint for quantile forecast panels.
48
+ """
49
+ return PanelQuantileForecastV1.from_frame(frame)
50
+
51
+
52
+ # Backwards-compatible aliases (keep these stable for downstream users).
53
+ def panel_point_v1(frame: pd.DataFrame) -> PanelPointForecastV1:
54
+ """Alias for `panel_point_forecast_v1` (kept for backwards compatibility)."""
55
+ return panel_point_forecast_v1(frame)
56
+
57
+
58
+ def panel_quantile_v1(frame: pd.DataFrame) -> PanelQuantileForecastV1:
59
+ """Alias for `panel_quantile_forecast_v1` (kept for backwards compatibility)."""
60
+ return panel_quantile_forecast_v1(frame)
61
+
62
+
63
+ def cost_asymmetry_v1(frame: pd.DataFrame) -> CostAsymmetrySpecV1:
64
+ """Validate and construct a V1 cost-asymmetry specification artifact."""
65
+ return CostAsymmetrySpecV1.from_frame(frame)
66
+
67
+
68
+ def panel_point_result_v1(frame: pd.DataFrame) -> PanelPointResultV1:
69
+ """Validate and construct a V1 panel point result artifact."""
70
+ return PanelPointResultV1.from_frame(frame)
71
+
72
+
73
+ __all__ = [
74
+ "cost_asymmetry_v1",
75
+ "panel_demand_v1",
76
+ "panel_point_forecast_v1",
77
+ "panel_point_result_v1",
78
+ "panel_point_v1",
79
+ "panel_quantile_forecast_v1",
80
+ "panel_quantile_v1",
81
+ ]
@@ -0,0 +1,9 @@
1
+ """
2
+ Contract artifacts and internal contract helpers.
3
+
4
+ This package contains versioned contract artifacts (e.g., demand panels, forecasts,
5
+ results, readiness diagnostics) and internal utilities used by those artifacts.
6
+
7
+ Do not treat this package as a stable public API surface. Prefer `eb_contracts.api`
8
+ for user-facing entrypoints.
9
+ """
@@ -0,0 +1,8 @@
1
+ """
2
+ Internal helpers for contract artifacts.
3
+
4
+ This package contains shared runtime, typing, and enforcement utilities used
5
+ internally by contract artifacts.
6
+
7
+ This is not a public API. Imports from this package may change without notice.
8
+ """
@@ -0,0 +1,48 @@
1
+ """
2
+ Runtime configuration for contract validation.
3
+
4
+ This module provides a minimal runtime surface used by contract validators to
5
+ determine validation behavior (e.g., strict, warn, off).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Iterator
11
+ from contextlib import contextmanager
12
+ import contextvars
13
+ from dataclasses import dataclass
14
+ from typing import Literal
15
+
16
+ ValidationMode = Literal["strict", "warn", "off"]
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class RuntimeConfig:
21
+ """Runtime configuration controlling validation behavior."""
22
+
23
+ validation: ValidationMode = "strict"
24
+
25
+
26
+ _runtime_var: contextvars.ContextVar[RuntimeConfig | None] = contextvars.ContextVar(
27
+ "eb_contracts_runtime",
28
+ default=None,
29
+ )
30
+
31
+
32
+ def get_runtime() -> RuntimeConfig:
33
+ """Return the active runtime configuration."""
34
+ cfg = _runtime_var.get()
35
+ if cfg is None:
36
+ cfg = RuntimeConfig()
37
+ _runtime_var.set(cfg)
38
+ return cfg
39
+
40
+
41
+ @contextmanager
42
+ def set_validation_mode(mode: ValidationMode) -> Iterator[None]:
43
+ """Temporarily set validation behavior within a context."""
44
+ token = _runtime_var.set(RuntimeConfig(validation=mode))
45
+ try:
46
+ yield
47
+ finally:
48
+ _runtime_var.reset(token)
@@ -0,0 +1,37 @@
1
+ """
2
+ Shared typing definitions for EB contracts.
3
+
4
+ This module centralizes type aliases and lightweight protocol definitions
5
+ used across contract artifacts, validators, and public entrypoints.
6
+
7
+ It contains no runtime logic.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Literal, TypeAlias
13
+
14
+ import pandas as pd
15
+
16
+ ######################################
17
+ # Core data structures
18
+ ######################################
19
+
20
+ # Canonical DataFrame type used throughout contract artifacts.
21
+ Frame: TypeAlias = pd.DataFrame
22
+
23
+
24
+ ######################################
25
+ # Validation behavior
26
+ ######################################
27
+
28
+ ValidationMode: TypeAlias = Literal["strict", "warn", "off"]
29
+
30
+
31
+ ######################################
32
+ # Contract markers
33
+ ######################################
34
+
35
+ # Marker alias for validated contract artifacts.
36
+ # Contract classes typically expose a `.frame` attribute of type Frame.
37
+ ContractArtifact: TypeAlias = object
@@ -0,0 +1,14 @@
1
+ """
2
+ Public context contract API.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ ######################################
8
+ # Public API
9
+ ######################################
10
+ from eb_contracts.contracts.context.v1.run_context import RunContextV1
11
+
12
+ __all__ = [
13
+ "RunContextV1",
14
+ ]
@@ -0,0 +1,8 @@
1
+ """
2
+ Context contract artifacts (version 1).
3
+
4
+ This package contains version 1 context-related contract artifacts, including
5
+ validated run-level metadata used for provenance and governance.
6
+
7
+ Artifacts in this package are versioned and may evolve in future releases.
8
+ """
@@ -0,0 +1,202 @@
1
+ """
2
+ Context contract models (V1).
3
+
4
+ This module defines V1 context artifacts for attaching non-tabular metadata to
5
+ forecasts, costs, and results.
6
+
7
+ V1 includes:
8
+ - RunContextV1: run-level metadata describing provenance and semantics
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from datetime import datetime
15
+ from typing import ClassVar, Final
16
+
17
+ from eb_contracts.contracts._internal.runtime import get_runtime
18
+ from eb_contracts.validation.errors import ContractViolation, ContractViolationError
19
+
20
+ ######################################
21
+ # Contract artifacts
22
+ ######################################
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class RunContextV1:
27
+ """Validated run-level context metadata."""
28
+
29
+ run_id: str
30
+ issued_at: datetime
31
+
32
+ model_id: str | None = None
33
+ dataset_id: str | None = None
34
+
35
+ horizon: int | None = None
36
+ interval_minutes: int | None = None
37
+ tz: str | None = None
38
+
39
+ tags: dict[str, str] | None = None
40
+
41
+ CONTRACT_NAME: ClassVar[Final[str]] = "RunContextV1"
42
+
43
+ @classmethod
44
+ def from_values(
45
+ cls,
46
+ *,
47
+ run_id: str,
48
+ issued_at: datetime,
49
+ model_id: str | None = None,
50
+ dataset_id: str | None = None,
51
+ horizon: int | None = None,
52
+ interval_minutes: int | None = None,
53
+ tz: str | None = None,
54
+ tags: dict[str, str] | None = None,
55
+ ) -> RunContextV1:
56
+ violations = validate_run_context_v1(
57
+ run_id=run_id,
58
+ issued_at=issued_at,
59
+ model_id=model_id,
60
+ dataset_id=dataset_id,
61
+ horizon=horizon,
62
+ interval_minutes=interval_minutes,
63
+ tz=tz,
64
+ tags=tags,
65
+ )
66
+ _raise_or_warn(cls.CONTRACT_NAME, violations)
67
+ return cls(
68
+ run_id=run_id,
69
+ issued_at=issued_at,
70
+ model_id=model_id,
71
+ dataset_id=dataset_id,
72
+ horizon=horizon,
73
+ interval_minutes=interval_minutes,
74
+ tz=tz,
75
+ tags=tags,
76
+ )
77
+
78
+
79
+ ######################################
80
+ # Validators
81
+ ######################################
82
+
83
+
84
+ def validate_run_context_v1(
85
+ *,
86
+ run_id: str,
87
+ issued_at: datetime,
88
+ model_id: str | None,
89
+ dataset_id: str | None,
90
+ horizon: int | None,
91
+ interval_minutes: int | None,
92
+ tz: str | None,
93
+ tags: dict[str, str] | None,
94
+ ) -> list[ContractViolation]:
95
+ """Validate values against the RunContextV1 contract."""
96
+ violations: list[ContractViolation] = []
97
+
98
+ if not run_id or not isinstance(run_id, str):
99
+ violations.append(
100
+ ContractViolation(
101
+ code="invalid_run_id",
102
+ message="run_id must be a non-empty string.",
103
+ )
104
+ )
105
+
106
+ if not isinstance(issued_at, datetime):
107
+ violations.append(
108
+ ContractViolation(
109
+ code="invalid_issued_at",
110
+ message="issued_at must be a datetime.",
111
+ )
112
+ )
113
+
114
+ if horizon is not None and horizon <= 0:
115
+ violations.append(
116
+ ContractViolation(
117
+ code="invalid_horizon",
118
+ message="horizon must be a positive integer when provided.",
119
+ )
120
+ )
121
+
122
+ if interval_minutes is not None and interval_minutes <= 0:
123
+ violations.append(
124
+ ContractViolation(
125
+ code="invalid_interval_minutes",
126
+ message="interval_minutes must be a positive integer when provided.",
127
+ )
128
+ )
129
+
130
+ if tags is not None:
131
+ if not isinstance(tags, dict):
132
+ violations.append(
133
+ ContractViolation(
134
+ code="invalid_tags",
135
+ message="tags must be a dict[str, str] when provided.",
136
+ )
137
+ )
138
+ else:
139
+ for k, v in tags.items():
140
+ if not isinstance(k, str) or not k:
141
+ violations.append(
142
+ ContractViolation(
143
+ code="invalid_tag_key",
144
+ message="All tag keys must be non-empty strings.",
145
+ )
146
+ )
147
+ break
148
+ if not isinstance(v, str):
149
+ violations.append(
150
+ ContractViolation(
151
+ code="invalid_tag_value",
152
+ message="All tag values must be strings.",
153
+ )
154
+ )
155
+ break
156
+
157
+ if model_id is not None and not isinstance(model_id, str):
158
+ violations.append(
159
+ ContractViolation(
160
+ code="invalid_model_id",
161
+ message="model_id must be a string when provided.",
162
+ )
163
+ )
164
+
165
+ if dataset_id is not None and not isinstance(dataset_id, str):
166
+ violations.append(
167
+ ContractViolation(
168
+ code="invalid_dataset_id",
169
+ message="dataset_id must be a string when provided.",
170
+ )
171
+ )
172
+
173
+ if tz is not None and (not isinstance(tz, str) or not tz):
174
+ violations.append(
175
+ ContractViolation(
176
+ code="invalid_tz",
177
+ message="tz must be a non-empty string when provided.",
178
+ )
179
+ )
180
+
181
+ return violations
182
+
183
+
184
+ ######################################
185
+ # Internal helpers
186
+ ######################################
187
+
188
+
189
+ def _raise_or_warn(contract: str, violations: list[ContractViolation]) -> None:
190
+ """Apply runtime validation behavior."""
191
+ if not violations:
192
+ return
193
+
194
+ mode = get_runtime().validation
195
+ if mode == "off":
196
+ return
197
+
198
+ if mode == "warn":
199
+ print(f"[eb-contracts] WARN: {contract}: " + "; ".join(v.message for v in violations))
200
+ return
201
+
202
+ raise ContractViolationError(contract=contract, violations=violations)
@@ -0,0 +1,14 @@
1
+ """
2
+ Public cost contract API.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ ######################################
8
+ # Public API
9
+ ######################################
10
+ from eb_contracts.contracts.costs.v1.cost_asymmetry_spec import CostAsymmetrySpecV1
11
+
12
+ __all__ = [
13
+ "CostAsymmetrySpecV1",
14
+ ]
@@ -0,0 +1,8 @@
1
+ """
2
+ Cost contract artifacts (version 1).
3
+
4
+ This package contains version 1 cost-related contract artifacts, including
5
+ validated cost-asymmetry specifications used for evaluation and optimization.
6
+
7
+ Artifacts in this package are versioned and may evolve in future releases.
8
+ """