greenpick 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.
- greenpick/__init__.py +7 -0
- greenpick/__main__.py +18 -0
- greenpick/config.py +58 -0
- greenpick/data/__init__.py +28 -0
- greenpick/data/auto_repository.py +47 -0
- greenpick/data/eia_repository.py +156 -0
- greenpick/data/electricitymaps_repository.py +104 -0
- greenpick/data/emission_factors.json +35 -0
- greenpick/data/entsoe_repository.py +401 -0
- greenpick/data/gpu_power.json +26 -0
- greenpick/data/inference_energy.json +50 -0
- greenpick/data/instance_families.json +32 -0
- greenpick/data/mock_repository.py +53 -0
- greenpick/data/network.json +7 -0
- greenpick/data/power.json +7 -0
- greenpick/data/pricing.json +31 -0
- greenpick/data/regions.json +22 -0
- greenpick/data/repository.py +27 -0
- greenpick/data/static_data.py +105 -0
- greenpick/models.py +342 -0
- greenpick/server.py +349 -0
- greenpick/services/__init__.py +5 -0
- greenpick/services/carbon_service.py +28 -0
- greenpick/services/cost_service.py +21 -0
- greenpick/services/forecast_service.py +72 -0
- greenpick/services/inference_service.py +100 -0
- greenpick/services/instance_service.py +132 -0
- greenpick/services/latency_service.py +33 -0
- greenpick/services/locality_service.py +160 -0
- greenpick/services/metric_scaffold_service.py +149 -0
- greenpick/services/recommender.py +203 -0
- greenpick/services/residency_service.py +53 -0
- greenpick/services/rightsizing_service.py +93 -0
- greenpick/services/scheduling_service.py +211 -0
- greenpick/services/serving_service.py +202 -0
- greenpick/services/training_service.py +209 -0
- greenpick-0.1.0.dist-info/METADATA +271 -0
- greenpick-0.1.0.dist-info/RECORD +42 -0
- greenpick-0.1.0.dist-info/WHEEL +5 -0
- greenpick-0.1.0.dist-info/entry_points.txt +2 -0
- greenpick-0.1.0.dist-info/licenses/LICENSE +21 -0
- greenpick-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Carbon data repository: the one interface every source implements.
|
|
2
|
+
|
|
3
|
+
Services depend ONLY on this Protocol, so sources (mock / Electricity Maps / ENTSO-E /
|
|
4
|
+
EIA) are swappable via config without touching business logic.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Protocol, runtime_checkable
|
|
9
|
+
from ..models import CarbonReading, Provider
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
from ..models import HourlyCarbon
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class CarbonRepository(Protocol):
|
|
17
|
+
async def carbon_now(self, provider: Provider, region: str) -> CarbonReading:
|
|
18
|
+
"""Current grid carbon intensity for one region. Implementations should
|
|
19
|
+
degrade gracefully (cached -> annual average) and set CarbonReading.quality
|
|
20
|
+
to reflect what was actually returned."""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
async def carbon_history(self, provider: Provider, region: str) -> list[HourlyCarbon]:
|
|
24
|
+
"""Recent hourly carbon intensity for a region (for scheduling deferrable work).
|
|
25
|
+
Return [] if the source doesn't support history for this region — callers must
|
|
26
|
+
handle the empty case. Default implementations may return []."""
|
|
27
|
+
...
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Loaders for the bundled static JSON (regions, pricing, power model).
|
|
2
|
+
|
|
3
|
+
Cached at import; these change rarely and are versioned in-repo.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
_DATA_DIR = Path(__file__).parent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@lru_cache(maxsize=1)
|
|
15
|
+
def regions() -> dict:
|
|
16
|
+
return json.loads((_DATA_DIR / "regions.json").read_text(encoding="utf-8"))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@lru_cache(maxsize=1)
|
|
20
|
+
def pricing() -> dict:
|
|
21
|
+
return json.loads((_DATA_DIR / "pricing.json").read_text(encoding="utf-8"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@lru_cache(maxsize=1)
|
|
25
|
+
def power_model() -> dict:
|
|
26
|
+
return json.loads((_DATA_DIR / "power.json").read_text(encoding="utf-8"))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@lru_cache(maxsize=1)
|
|
30
|
+
def emission_factors() -> dict:
|
|
31
|
+
"""Per-fuel gCO2/kWh factors (keyed by EIA fueltype code)."""
|
|
32
|
+
return json.loads((_DATA_DIR / "emission_factors.json").read_text(encoding="utf-8"))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@lru_cache(maxsize=1)
|
|
36
|
+
def instance_families() -> dict:
|
|
37
|
+
"""Directional perf-per-watt multipliers for general-purpose instance families."""
|
|
38
|
+
return json.loads((_DATA_DIR / "instance_families.json").read_text(encoding="utf-8"))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@lru_cache(maxsize=1)
|
|
42
|
+
def network_model() -> dict:
|
|
43
|
+
"""Directional data-transfer energy intensity (kWh/GB) for the egress carbon signal."""
|
|
44
|
+
return json.loads((_DATA_DIR / "network.json").read_text(encoding="utf-8"))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@lru_cache(maxsize=1)
|
|
48
|
+
def inference_energy() -> dict:
|
|
49
|
+
"""Directional LLM inference energy (Wh/1k output tokens) by model size class."""
|
|
50
|
+
return json.loads((_DATA_DIR / "inference_energy.json").read_text(encoding="utf-8"))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def resolve_size_class(name: str) -> tuple[str, dict] | None:
|
|
54
|
+
"""Resolve a size-class name or param-count alias (e.g. 'small', '8b', '70B', 'moe',
|
|
55
|
+
'mixtral') -> (canonical_class, spec), case-insensitive. None if unknown."""
|
|
56
|
+
if not name:
|
|
57
|
+
return None
|
|
58
|
+
data = inference_energy()
|
|
59
|
+
key = name.strip().lower()
|
|
60
|
+
classes = data.get("size_classes", {})
|
|
61
|
+
if key in classes:
|
|
62
|
+
return key, classes[key]
|
|
63
|
+
alias = data.get("_aliases", {}).get(key)
|
|
64
|
+
if alias and alias in classes:
|
|
65
|
+
return alias, classes[alias]
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@lru_cache(maxsize=1)
|
|
70
|
+
def gpu_power() -> dict:
|
|
71
|
+
"""Directional GPU/accelerator power model (TDP + utilization + host overhead) for training."""
|
|
72
|
+
return json.loads((_DATA_DIR / "gpu_power.json").read_text(encoding="utf-8"))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@lru_cache(maxsize=1)
|
|
76
|
+
def _gpu_index() -> dict:
|
|
77
|
+
"""Lowercased lookup from GPU key OR alias -> (canonical_key, spec dict)."""
|
|
78
|
+
idx: dict[str, tuple[str, dict]] = {}
|
|
79
|
+
for key, spec in gpu_power().get("gpus", {}).items():
|
|
80
|
+
idx[key.lower()] = (key, spec)
|
|
81
|
+
for alias in spec.get("aliases", []):
|
|
82
|
+
idx[alias.lower()] = (key, spec)
|
|
83
|
+
return idx
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def resolve_gpu(name: str) -> tuple[str, dict] | None:
|
|
87
|
+
"""Resolve a user-supplied GPU name (case-insensitive, alias-aware) -> (canonical_key, spec),
|
|
88
|
+
or None if unknown. e.g. 'A100', 'a100-sxm', 'tesla-t4' all resolve."""
|
|
89
|
+
if not name:
|
|
90
|
+
return None
|
|
91
|
+
return _gpu_index().get(name.strip().lower())
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def iter_regions():
|
|
95
|
+
"""Yield (provider, region_code, meta) for every known region."""
|
|
96
|
+
data = regions()
|
|
97
|
+
for provider, regs in data.items():
|
|
98
|
+
if provider.startswith("_"):
|
|
99
|
+
continue
|
|
100
|
+
for code, meta in regs.items():
|
|
101
|
+
yield provider, code, meta
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def region_meta(provider: str, region: str) -> dict | None:
|
|
105
|
+
return regions().get(provider, {}).get(region)
|
greenpick/models.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Domain models shared across data sources, services, and tools.
|
|
2
|
+
|
|
3
|
+
Pydantic for validation at the MCP boundary (inputs arrive as loosely-typed JSON).
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Literal, Optional
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
Provider = Literal["aws", "gcp", "azure"]
|
|
12
|
+
DataQuality = Literal[
|
|
13
|
+
"live", "live_estimated", "cached", "annual_average", "mock", "forecast", "historical",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Workload(BaseModel):
|
|
18
|
+
"""A long-running web/API service — the only workload shape in v1."""
|
|
19
|
+
vcpu: float = Field(gt=0, description="vCPUs provisioned")
|
|
20
|
+
ram_gb: float = Field(gt=0, description="RAM in GB")
|
|
21
|
+
est_monthly_egress_gb: float = Field(ge=0, default=0, description="Outbound GB/month")
|
|
22
|
+
hours_per_month: float = Field(gt=0, default=730, description="Running hours/month (730 = always-on)")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GeoPoint(BaseModel):
|
|
26
|
+
lat: float
|
|
27
|
+
lon: float
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CarbonReading(BaseModel):
|
|
31
|
+
"""Grid carbon intensity for a region at a point in time."""
|
|
32
|
+
region: str
|
|
33
|
+
provider: Provider
|
|
34
|
+
g_co2_per_kwh: float = Field(ge=0)
|
|
35
|
+
source: str # e.g. "electricitymaps", "eia", "mock"
|
|
36
|
+
timestamp: str # ISO 8601
|
|
37
|
+
quality: DataQuality # how trustworthy / fresh this reading is
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class HourlyCarbon(BaseModel):
|
|
41
|
+
"""One hour's grid carbon intensity, for scheduling deferrable work."""
|
|
42
|
+
period: str # the hour label from the source (e.g. "2026-10-12T03")
|
|
43
|
+
g_co2_per_kwh: float
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ForecastHour(BaseModel):
|
|
47
|
+
"""One upcoming hour's forecast renewable output (the relative-cleanliness signal)."""
|
|
48
|
+
period: str # ISO hour the forecast applies to (UTC)
|
|
49
|
+
renewable_mw: float # forecast wind+solar MW for that hour
|
|
50
|
+
renewable_share: Optional[float] = None # renewables / load, 0..1, when load is available
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ForecastAdvice(BaseModel):
|
|
54
|
+
"""Forward-looking cleanest hours from the day-ahead renewable forecast.
|
|
55
|
+
|
|
56
|
+
RELATIVE, not absolute: ranks upcoming hours by forecast wind+solar output (more renewables =
|
|
57
|
+
cleaner). Not a gCO2/kWh forecast — the free data doesn't support that reliably across zones."""
|
|
58
|
+
region: str
|
|
59
|
+
provider: Provider
|
|
60
|
+
source: str # "entsoe_forecast" | "historical_fallback" | "mock"
|
|
61
|
+
quality: DataQuality # "forecast" | "historical" | "mock"
|
|
62
|
+
hours_ahead: int
|
|
63
|
+
cleanest_upcoming: list[ForecastHour] = [] # greenest upcoming hours, cleanest first
|
|
64
|
+
peak_renewable_period: Optional[str] = None
|
|
65
|
+
low_renewable_period: Optional[str] = None
|
|
66
|
+
note: str = ""
|
|
67
|
+
limitations: str = ""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class CleanestWindow(BaseModel):
|
|
71
|
+
"""The lowest-average contiguous run of N hours — for timing a job of known duration."""
|
|
72
|
+
window_hours: int
|
|
73
|
+
start_period: str # the hour the window begins (start your job here)
|
|
74
|
+
end_period: str # the last hour in the window
|
|
75
|
+
avg_g_co2_per_kwh: float # mean intensity across the window
|
|
76
|
+
hours: list[HourlyCarbon] = [] # the per-hour breakdown of the chosen window
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class HourOfDayStat(BaseModel):
|
|
80
|
+
"""Average carbon for a given hour-of-day across the days in the recent window."""
|
|
81
|
+
hour_of_day: int # 0..23 (UTC, matching the source timestamps)
|
|
82
|
+
avg_g_co2_per_kwh: float
|
|
83
|
+
days_observed: int # how many days contributed to this average
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class DailyPatternAdvice(BaseModel):
|
|
87
|
+
"""The recurring time-of-day pattern for a region, for timing a RECURRING job (e.g. nightly).
|
|
88
|
+
|
|
89
|
+
This is a daily pattern over the N days we have, NOT a 7-day weekly pattern — the free data
|
|
90
|
+
windows are only a few days, so we honestly report 'cleanest time of day', not 'day of week'."""
|
|
91
|
+
region: str
|
|
92
|
+
provider: Provider
|
|
93
|
+
source: str
|
|
94
|
+
quality: DataQuality
|
|
95
|
+
days_analyzed: int
|
|
96
|
+
hours_analyzed: int
|
|
97
|
+
by_hour_of_day: list[HourOfDayStat] = [] # all 0..23 that had data, cleanest first
|
|
98
|
+
cleanest_hour_of_day: Optional[int] = None
|
|
99
|
+
dirtiest_hour_of_day: Optional[int] = None
|
|
100
|
+
potential_reduction_pct: Optional[float] = None
|
|
101
|
+
note: str = ""
|
|
102
|
+
limitations: str = ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class SchedulingAdvice(BaseModel):
|
|
106
|
+
"""Cleanest hours to run a deferrable job in a region, from recent history."""
|
|
107
|
+
region: str
|
|
108
|
+
provider: Provider
|
|
109
|
+
source: str
|
|
110
|
+
quality: DataQuality
|
|
111
|
+
hours_analyzed: int
|
|
112
|
+
cleanest: list[HourlyCarbon] # lowest-carbon hours, cleanest first
|
|
113
|
+
dirtiest_g_co2_per_kwh: Optional[float] = None
|
|
114
|
+
cleanest_g_co2_per_kwh: Optional[float] = None
|
|
115
|
+
potential_reduction_pct: Optional[float] = None # cleanest vs dirtiest
|
|
116
|
+
# Window mode (set when window_hours >= 2): the best contiguous run to start a job of that length.
|
|
117
|
+
window: Optional[CleanestWindow] = None
|
|
118
|
+
window_reduction_pct: Optional[float] = None # best vs worst same-length window
|
|
119
|
+
note: str = ""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class FamilyOption(BaseModel):
|
|
123
|
+
"""One instance family as a greener-compute candidate, same size & region."""
|
|
124
|
+
family: str
|
|
125
|
+
arch: str # "x86" | "arm"
|
|
126
|
+
cpu: str
|
|
127
|
+
relative_energy: float # energy for the same work; lower = greener
|
|
128
|
+
energy_saving_vs_current_pct: Optional[float] = None # vs. the user's current family
|
|
129
|
+
est_kg_co2_per_month: Optional[float] = None
|
|
130
|
+
co2_saving_vs_current_pct: Optional[float] = None
|
|
131
|
+
rating: Optional[str] = None # "green" | "yellow" | "red", relative to the options
|
|
132
|
+
note: str = ""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class GreenerComputeAdvice(BaseModel):
|
|
136
|
+
"""Greener same-size instance families in the same region, ranked by energy efficiency."""
|
|
137
|
+
provider: Provider
|
|
138
|
+
region: str
|
|
139
|
+
current_family: Optional[str] = None
|
|
140
|
+
grid_intensity_g_co2_per_kwh: Optional[float] = None
|
|
141
|
+
carbon_source: Optional[str] = None
|
|
142
|
+
carbon_quality: Optional[str] = None
|
|
143
|
+
options: list[FamilyOption] = [] # greenest first
|
|
144
|
+
recommended_family: Optional[str] = None
|
|
145
|
+
rationale: str = ""
|
|
146
|
+
limitations: str = ""
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TrainingRegionOption(BaseModel):
|
|
150
|
+
"""One region as a place to run a training job, with its carbon for this run."""
|
|
151
|
+
provider: Provider
|
|
152
|
+
region: str
|
|
153
|
+
grid_intensity_g_co2_per_kwh: float
|
|
154
|
+
carbon_source: str
|
|
155
|
+
carbon_quality: str
|
|
156
|
+
est_kg_co2: float # for the whole run, in this region
|
|
157
|
+
vs_baseline_pct: Optional[float] = None # negative = cleaner than baseline/current
|
|
158
|
+
rating: Optional[str] = None # "green" | "yellow" | "red", relative
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class TrainingCarbonAdvice(BaseModel):
|
|
162
|
+
"""Carbon of a GPU training run, with greener WHERE (region) and WHEN (hours) guidance."""
|
|
163
|
+
gpu: str # canonical GPU key
|
|
164
|
+
gpu_name: str
|
|
165
|
+
gpu_count: int
|
|
166
|
+
gpu_hours: float # total accelerator-hours for the run
|
|
167
|
+
utilization: float
|
|
168
|
+
est_energy_kwh: float
|
|
169
|
+
# Placement:
|
|
170
|
+
region: Optional[str] = None # the resolved/queried region (if one was given)
|
|
171
|
+
options: list[TrainingRegionOption] = [] # ranked greener-first (when ranking regions)
|
|
172
|
+
recommended_region: Optional[str] = None
|
|
173
|
+
residency_note: Optional[str] = None # heads-up if the pick is in a different jurisdiction
|
|
174
|
+
# Timing (reuses cleanest-hours scheduling for the chosen/greenest region):
|
|
175
|
+
cleanest_hours: list[HourlyCarbon] = []
|
|
176
|
+
cleanest_window: Optional["CleanestWindow"] = None # set when the run is >= 2h wall-clock
|
|
177
|
+
timing_reduction_pct: Optional[float] = None
|
|
178
|
+
timing_source: Optional[str] = None
|
|
179
|
+
timing_quality: Optional[str] = None
|
|
180
|
+
rationale: str = ""
|
|
181
|
+
limitations: str = ""
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class InferenceCarbonAdvice(BaseModel):
|
|
185
|
+
"""Directional energy + CO2 for an LLM INFERENCE workload, by model size class.
|
|
186
|
+
|
|
187
|
+
Estimate, not a measurement. Keyed on size class (not a named product); output tokens drive
|
|
188
|
+
the cost; reported as a LOW-HIGH band because serving choices move the same model many-fold."""
|
|
189
|
+
size_class: str # canonical class resolved
|
|
190
|
+
size_class_label: str
|
|
191
|
+
monthly_requests: float
|
|
192
|
+
avg_output_tokens: int
|
|
193
|
+
monthly_output_tokens: float # requests x avg_output_tokens
|
|
194
|
+
wh_per_1k_output_low: float
|
|
195
|
+
wh_per_1k_output_high: float
|
|
196
|
+
monthly_kwh_low: float
|
|
197
|
+
monthly_kwh_high: float
|
|
198
|
+
region: Optional[str] = None
|
|
199
|
+
grid_intensity_g_co2_per_kwh: Optional[float] = None
|
|
200
|
+
carbon_source: Optional[str] = None
|
|
201
|
+
carbon_quality: Optional[str] = None
|
|
202
|
+
monthly_kg_co2_low: Optional[float] = None
|
|
203
|
+
monthly_kg_co2_high: Optional[float] = None
|
|
204
|
+
rationale: str = ""
|
|
205
|
+
limitations: str = ""
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class RightsizingAdvice(BaseModel):
|
|
209
|
+
"""FinOps right-sizing with a carbon twin: if you're over-provisioned, a smaller size cuts
|
|
210
|
+
BOTH cost and carbon. The clearest efficiency dual-benefit. Directional."""
|
|
211
|
+
provider: Provider
|
|
212
|
+
region: str
|
|
213
|
+
provisioned_vcpu: float
|
|
214
|
+
provisioned_ram_gb: float
|
|
215
|
+
observed_cpu_utilization: float # 0..1, peak/typical the user reports
|
|
216
|
+
observed_ram_utilization: Optional[float] = None
|
|
217
|
+
recommended_vcpu: float = 0
|
|
218
|
+
recommended_ram_gb: float = 0
|
|
219
|
+
is_oversized: bool = False
|
|
220
|
+
cost_now_usd_per_month: Optional[float] = None
|
|
221
|
+
cost_rightsized_usd_per_month: Optional[float] = None
|
|
222
|
+
monthly_usd_saving: Optional[float] = None
|
|
223
|
+
carbon_now_kg_per_month: Optional[float] = None
|
|
224
|
+
carbon_rightsized_kg_per_month: Optional[float] = None
|
|
225
|
+
monthly_kg_co2_saving: Optional[float] = None
|
|
226
|
+
saving_pct: Optional[float] = None
|
|
227
|
+
carbon_source: Optional[str] = None
|
|
228
|
+
carbon_quality: Optional[str] = None
|
|
229
|
+
rationale: str = ""
|
|
230
|
+
limitations: str = ""
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class CarbonMetricScaffold(BaseModel):
|
|
234
|
+
"""Paste-in instrumentation so an app can emit its OWN estimated carbon metric.
|
|
235
|
+
|
|
236
|
+
GreenPick supplies the code + the coefficients that make it meaningful; the app measures the
|
|
237
|
+
observable proxies (CPU util, vCPU/RAM, uptime) and multiplies by GreenPick's power model and
|
|
238
|
+
the region's grid intensity. The emitted number is a DIRECTIONAL estimate, not metered energy."""
|
|
239
|
+
language: str
|
|
240
|
+
region: Optional[str] = None
|
|
241
|
+
grid_intensity_g_co2_per_kwh: Optional[float] = None # baked in if a region was given
|
|
242
|
+
carbon_source: Optional[str] = None
|
|
243
|
+
carbon_quality: Optional[str] = None
|
|
244
|
+
coefficients: dict = {} # the power-model constants used
|
|
245
|
+
code: str = "" # the snippet to paste in
|
|
246
|
+
how_to_use: str = ""
|
|
247
|
+
methodology: str = ""
|
|
248
|
+
limitations: str = ""
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class ServingHub(BaseModel):
|
|
252
|
+
"""One cluster of users, as a share of total traffic, located at a point."""
|
|
253
|
+
name: str # e.g. "eu", "us", "apac", or a city
|
|
254
|
+
lat: float
|
|
255
|
+
lon: float
|
|
256
|
+
share: float = Field(gt=0, le=1) # fraction of users here (shares sum ~1)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class ServingSinglePlan(BaseModel):
|
|
260
|
+
"""Serve ALL users from one region."""
|
|
261
|
+
provider: Provider
|
|
262
|
+
region: str
|
|
263
|
+
weighted_latency_ms: float # traffic-weighted latency across hubs
|
|
264
|
+
carbon_g_co2_per_kwh: float
|
|
265
|
+
carbon_source: str
|
|
266
|
+
carbon_quality: str
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class ServingPerHubChoice(BaseModel):
|
|
270
|
+
"""The region chosen to serve one hub, in a multi-region plan."""
|
|
271
|
+
hub: str
|
|
272
|
+
provider: Provider
|
|
273
|
+
region: str
|
|
274
|
+
latency_ms: float
|
|
275
|
+
carbon_g_co2_per_kwh: float
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class ServingPatternAdvice(BaseModel):
|
|
279
|
+
"""Design-time 'where should I serve my users from?' — single-region vs multi-region.
|
|
280
|
+
|
|
281
|
+
Directional. Reuses carbon + latency, and composes the egress (cross-region data sync) and
|
|
282
|
+
residency (cross-jurisdiction) caveats. NOT a runtime meter; NOT a verdict on legality."""
|
|
283
|
+
hubs: list[ServingHub]
|
|
284
|
+
single_region: Optional[ServingSinglePlan] = None # best one-region-for-all option
|
|
285
|
+
multi_region: list[ServingPerHubChoice] = [] # one region per hub (nearest+greener)
|
|
286
|
+
multi_region_weighted_latency_ms: Optional[float] = None
|
|
287
|
+
multi_region_blended_carbon_g_co2_per_kwh: Optional[float] = None
|
|
288
|
+
recommended_pattern: str = "" # "single_region" | "multi_region"
|
|
289
|
+
data_sync_egress_note: Optional[str] = None # multi-region implies shared-state sync
|
|
290
|
+
residency_note: Optional[str] = None # if any chosen region crosses a hub's jurisdiction
|
|
291
|
+
rationale: str = ""
|
|
292
|
+
limitations: str = ""
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class DataLocalityWarning(BaseModel):
|
|
296
|
+
"""Whether running compute away from where the data lives will cost you in egress.
|
|
297
|
+
|
|
298
|
+
The core 'does the greener-region move backfire?' check. Primary signal is egress COST;
|
|
299
|
+
transfer carbon is a secondary, deliberately-coarse figure."""
|
|
300
|
+
compute_location: str # "provider/region"
|
|
301
|
+
data_location: str # "provider/region"
|
|
302
|
+
monthly_cross_boundary_gb: float
|
|
303
|
+
locality: str # "same_region" | "cross_region_same_cloud" | "cross_cloud"
|
|
304
|
+
severity: str # "none" | "low" | "medium" | "high"
|
|
305
|
+
egress_rate_usd_per_gb_low: Optional[float] = None # per-GB rate range for this tier
|
|
306
|
+
egress_rate_usd_per_gb_high: Optional[float] = None
|
|
307
|
+
est_monthly_egress_usd_low: Optional[float] = None # monthly cost range = rate range x GB
|
|
308
|
+
est_monthly_egress_usd_high: Optional[float] = None
|
|
309
|
+
est_monthly_transfer_kg_co2: Optional[float] = None # secondary, coarse
|
|
310
|
+
carbon_saving_kg_co2_from_move: Optional[float] = None # echoed back if provided
|
|
311
|
+
net_carbon_verdict: Optional[str] = None # set only when a saving was supplied
|
|
312
|
+
residency_note: Optional[str] = None # neutral cross-jurisdiction heads-up, if any
|
|
313
|
+
warning: str = ""
|
|
314
|
+
recommendation: str = ""
|
|
315
|
+
limitations: str = ""
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class RegionCandidate(BaseModel):
|
|
319
|
+
"""One region scored on all three axes — the tri-axis payload."""
|
|
320
|
+
region: str
|
|
321
|
+
provider: Provider
|
|
322
|
+
carbon: CarbonReading
|
|
323
|
+
carbon_rank: Optional[int] = None # 1 = cleanest among candidates
|
|
324
|
+
carbon_vs_baseline_pct: Optional[float] = None # negative = cleaner than the baseline region
|
|
325
|
+
est_monthly_cost_usd: Optional[float] = None # compute + egress ballpark
|
|
326
|
+
latency_ms_estimate: Optional[float] = None # to the user's location
|
|
327
|
+
added_latency_ms: Optional[float] = None # vs. the baseline region
|
|
328
|
+
# Per-axis relative rating within the candidate set: "green" | "yellow" | "red".
|
|
329
|
+
# Relative, not absolute — "best available among your options", shown next to the raw value.
|
|
330
|
+
carbon_rating: Optional[str] = None
|
|
331
|
+
latency_rating: Optional[str] = None
|
|
332
|
+
cost_rating: Optional[str] = None
|
|
333
|
+
tradeoff: Optional[str] = None # one-line plain-English summary
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class Recommendation(BaseModel):
|
|
337
|
+
"""The full answer: ranked candidates + a pick + honest caveats."""
|
|
338
|
+
candidates: list[RegionCandidate]
|
|
339
|
+
recommended_region: Optional[str] = None
|
|
340
|
+
residency_note: Optional[str] = None # neutral heads-up if the pick crosses jurisdictions
|
|
341
|
+
rationale: str = ""
|
|
342
|
+
limitations: str = ""
|