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.
Files changed (42) hide show
  1. greenpick/__init__.py +7 -0
  2. greenpick/__main__.py +18 -0
  3. greenpick/config.py +58 -0
  4. greenpick/data/__init__.py +28 -0
  5. greenpick/data/auto_repository.py +47 -0
  6. greenpick/data/eia_repository.py +156 -0
  7. greenpick/data/electricitymaps_repository.py +104 -0
  8. greenpick/data/emission_factors.json +35 -0
  9. greenpick/data/entsoe_repository.py +401 -0
  10. greenpick/data/gpu_power.json +26 -0
  11. greenpick/data/inference_energy.json +50 -0
  12. greenpick/data/instance_families.json +32 -0
  13. greenpick/data/mock_repository.py +53 -0
  14. greenpick/data/network.json +7 -0
  15. greenpick/data/power.json +7 -0
  16. greenpick/data/pricing.json +31 -0
  17. greenpick/data/regions.json +22 -0
  18. greenpick/data/repository.py +27 -0
  19. greenpick/data/static_data.py +105 -0
  20. greenpick/models.py +342 -0
  21. greenpick/server.py +349 -0
  22. greenpick/services/__init__.py +5 -0
  23. greenpick/services/carbon_service.py +28 -0
  24. greenpick/services/cost_service.py +21 -0
  25. greenpick/services/forecast_service.py +72 -0
  26. greenpick/services/inference_service.py +100 -0
  27. greenpick/services/instance_service.py +132 -0
  28. greenpick/services/latency_service.py +33 -0
  29. greenpick/services/locality_service.py +160 -0
  30. greenpick/services/metric_scaffold_service.py +149 -0
  31. greenpick/services/recommender.py +203 -0
  32. greenpick/services/residency_service.py +53 -0
  33. greenpick/services/rightsizing_service.py +93 -0
  34. greenpick/services/scheduling_service.py +211 -0
  35. greenpick/services/serving_service.py +202 -0
  36. greenpick/services/training_service.py +209 -0
  37. greenpick-0.1.0.dist-info/METADATA +271 -0
  38. greenpick-0.1.0.dist-info/RECORD +42 -0
  39. greenpick-0.1.0.dist-info/WHEEL +5 -0
  40. greenpick-0.1.0.dist-info/entry_points.txt +2 -0
  41. greenpick-0.1.0.dist-info/licenses/LICENSE +21 -0
  42. 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 = ""