greenpick 0.3.0__tar.gz → 0.4.0__tar.gz
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-0.3.0/src/greenpick.egg-info → greenpick-0.4.0}/PKG-INFO +11 -6
- {greenpick-0.3.0 → greenpick-0.4.0}/README.md +10 -5
- {greenpick-0.3.0 → greenpick-0.4.0}/pyproject.toml +1 -1
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/__init__.py +1 -1
- greenpick-0.4.0/src/greenpick/data/annual.json +13 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/regions.json +12 -3
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/static_data.py +9 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/typical_repository.py +14 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/server.py +1 -1
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/services/data_hint.py +16 -3
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/services/forecast_service.py +4 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/services/recommender.py +1 -1
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/services/residency_service.py +3 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/services/training_service.py +3 -0
- {greenpick-0.3.0 → greenpick-0.4.0/src/greenpick.egg-info}/PKG-INFO +11 -6
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick.egg-info/SOURCES.txt +1 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/tests/test_services.py +59 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/LICENSE +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/setup.cfg +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/__main__.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/config.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/__init__.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/_net.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/auto_repository.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/canada_repository.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/eia_repository.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/electricitymaps_repository.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/emission_factors.json +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/entsoe_repository.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/gpu_power.json +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/inference_energy.json +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/instance_families.json +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/mock_repository.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/network.json +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/power.json +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/pricing.json +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/repository.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/data/typical.json +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/models.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/services/__init__.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/services/carbon_service.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/services/cost_service.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/services/inference_service.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/services/instance_service.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/services/latency_service.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/services/locality_service.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/services/metric_scaffold_service.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/services/rightsizing_service.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/services/scheduling_service.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick/services/serving_service.py +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick.egg-info/dependency_links.txt +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick.egg-info/entry_points.txt +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick.egg-info/requires.txt +0 -0
- {greenpick-0.3.0 → greenpick-0.4.0}/src/greenpick.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: greenpick
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Design-time sustainability co-pilot (MCP server): greener, cheaper cloud infrastructure decisions with carbon + cost + latency tradeoffs.
|
|
5
5
|
Author-email: "Data Science Advisers, LLC" <info@datascienceadvisers.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -124,7 +124,7 @@ backfire guards** — is the part nothing else puts together.
|
|
|
124
124
|
|
|
125
125
|
## Scope
|
|
126
126
|
|
|
127
|
-
- Providers: **AWS, GCP, Azure** regions.
|
|
127
|
+
- Providers: **AWS, GCP, Azure** — 25 regions across the US, Europe, Canada, Asia and Australia.
|
|
128
128
|
- Workloads: long-running **web/API services**, **GPU training** runs, and **deferrable jobs**
|
|
129
129
|
(ETL / batch / backups) — placement and timing for each.
|
|
130
130
|
- Carbon = **grid carbon intensity by region** (directional, point-in-time; live US + EU), plus a
|
|
@@ -195,8 +195,12 @@ See `docs/user-questions-backlog.md`.
|
|
|
195
195
|
intensity and average hour-of-day profile (UTC), computed from 12 months of real hourly EIA-930 /
|
|
196
196
|
ENTSO-E data (`data/typical.json`, refreshed by `scripts/refresh_typical.py`). Labelled
|
|
197
197
|
`quality: "typical"` — a realistic norm, never presented as a current reading.
|
|
198
|
-
- **
|
|
199
|
-
|
|
198
|
+
- **Annual** — for Singapore, Tokyo and Mumbai/Pune, where no free hourly data exists: the country's
|
|
199
|
+
yearly lifecycle grid intensity from [Ember](https://ember-energy.org/) (via Our World in Data,
|
|
200
|
+
CC BY 4.0), with source and year (`data/annual.json`). Labelled `quality: "annual_average"`; fine for
|
|
201
|
+
comparing regions, but carries no timing signal.
|
|
202
|
+
- **Mock** — a hand-set illustrative value, labelled `quality: "mock"`, used only where nothing else
|
|
203
|
+
exists (currently Sydney).
|
|
200
204
|
- Cost: a **dated static pricing table** (`data/pricing.json`), refreshed from live Vantage
|
|
201
205
|
pricing via `scripts/refresh_pricing.py` using one matched method across all three clouds.
|
|
202
206
|
- Instance-family efficiency: directional perf-per-watt multipliers (`data/instance_families.json`).
|
|
@@ -300,8 +304,9 @@ Top-level `await` works in Jupyter. In a plain script, put the `with` block insi
|
|
|
300
304
|
and run it with `asyncio.run(...)`.
|
|
301
305
|
|
|
302
306
|
Every carbon figure includes a `quality` field: `live` means a current reading, `typical` means
|
|
303
|
-
this month's average from last year's real data,
|
|
304
|
-
with no
|
|
307
|
+
this month's average from last year's real data, `annual_average` means a sourced yearly national
|
|
308
|
+
figure (regions with no free hourly data), and `mock` means a hand-set value. Check it to confirm
|
|
309
|
+
live data is on. A result that isn't live also includes a
|
|
305
310
|
`live_data_hint` saying why (no key set, the source didn't respond, or no free source covers that
|
|
306
311
|
region) and what to do about it.
|
|
307
312
|
|
|
@@ -94,7 +94,7 @@ backfire guards** — is the part nothing else puts together.
|
|
|
94
94
|
|
|
95
95
|
## Scope
|
|
96
96
|
|
|
97
|
-
- Providers: **AWS, GCP, Azure** regions.
|
|
97
|
+
- Providers: **AWS, GCP, Azure** — 25 regions across the US, Europe, Canada, Asia and Australia.
|
|
98
98
|
- Workloads: long-running **web/API services**, **GPU training** runs, and **deferrable jobs**
|
|
99
99
|
(ETL / batch / backups) — placement and timing for each.
|
|
100
100
|
- Carbon = **grid carbon intensity by region** (directional, point-in-time; live US + EU), plus a
|
|
@@ -165,8 +165,12 @@ See `docs/user-questions-backlog.md`.
|
|
|
165
165
|
intensity and average hour-of-day profile (UTC), computed from 12 months of real hourly EIA-930 /
|
|
166
166
|
ENTSO-E data (`data/typical.json`, refreshed by `scripts/refresh_typical.py`). Labelled
|
|
167
167
|
`quality: "typical"` — a realistic norm, never presented as a current reading.
|
|
168
|
-
- **
|
|
169
|
-
|
|
168
|
+
- **Annual** — for Singapore, Tokyo and Mumbai/Pune, where no free hourly data exists: the country's
|
|
169
|
+
yearly lifecycle grid intensity from [Ember](https://ember-energy.org/) (via Our World in Data,
|
|
170
|
+
CC BY 4.0), with source and year (`data/annual.json`). Labelled `quality: "annual_average"`; fine for
|
|
171
|
+
comparing regions, but carries no timing signal.
|
|
172
|
+
- **Mock** — a hand-set illustrative value, labelled `quality: "mock"`, used only where nothing else
|
|
173
|
+
exists (currently Sydney).
|
|
170
174
|
- Cost: a **dated static pricing table** (`data/pricing.json`), refreshed from live Vantage
|
|
171
175
|
pricing via `scripts/refresh_pricing.py` using one matched method across all three clouds.
|
|
172
176
|
- Instance-family efficiency: directional perf-per-watt multipliers (`data/instance_families.json`).
|
|
@@ -270,8 +274,9 @@ Top-level `await` works in Jupyter. In a plain script, put the `with` block insi
|
|
|
270
274
|
and run it with `asyncio.run(...)`.
|
|
271
275
|
|
|
272
276
|
Every carbon figure includes a `quality` field: `live` means a current reading, `typical` means
|
|
273
|
-
this month's average from last year's real data,
|
|
274
|
-
with no
|
|
277
|
+
this month's average from last year's real data, `annual_average` means a sourced yearly national
|
|
278
|
+
figure (regions with no free hourly data), and `mock` means a hand-set value. Check it to confirm
|
|
279
|
+
live data is on. A result that isn't live also includes a
|
|
275
280
|
`live_data_hint` saying why (no key set, the source didn't respond, or no free source covers that
|
|
276
281
|
region) and what to do about it.
|
|
277
282
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "greenpick"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Design-time sustainability co-pilot (MCP server): greener, cheaper cloud infrastructure decisions with carbon + cost + latency tradeoffs."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -4,7 +4,7 @@ Greener cloud-region choices with honest carbon + cost + latency tradeoffs,
|
|
|
4
4
|
delivered inside the vibe-coding loop. Directional guidance, not audited accounting.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.4.0"
|
|
8
8
|
|
|
9
9
|
# httpx logs every request URL at INFO, and our live sources pass their API key / token as a URL
|
|
10
10
|
# query parameter — so at the default log level the secret would land in the MCP client's logs.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Yearly average grid carbon intensity for regions with no free hourly data source. Lifecycle gCO2e/kWh (full lifecycle incl. upstream methane), the same basis as GreenPick's per-fuel factors, so these compare fairly with live/typical regions. National figures: no hourly or sub-national lifecycle data is freely available for these grids. Used with quality='annual_average'; no hourly profile, so no timing advice.",
|
|
3
|
+
"_source": "Ember yearly electricity data (lifecycle CO2 intensity of electricity generation), via Our World in Data grapher 'carbon-intensity-electricity'. CC BY 4.0.",
|
|
4
|
+
"_as_of": "2026-10-27",
|
|
5
|
+
"zones": {
|
|
6
|
+
"SG": { "country": "Singapore", "g_co2_per_kwh": 497.1, "year": 2025,
|
|
7
|
+
"note": "About 95% natural gas; a yearly figure is a fair description of this steady grid." },
|
|
8
|
+
"JP": { "country": "Japan", "g_co2_per_kwh": 477.3, "year": 2025,
|
|
9
|
+
"note": "National average; the Tokyo (TEPCO) area mix differs somewhat and varies by hour." },
|
|
10
|
+
"IN": { "country": "India", "g_co2_per_kwh": 670.1, "year": 2025,
|
|
11
|
+
"note": "National average; Maharashtra (Mumbai, Pune) runs about 6% above it on Ember's state-level data." }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -7,20 +7,29 @@
|
|
|
7
7
|
"eu-west-1": { "name": "Ireland", "lat": 53.41, "lon": -8.24, "zone": "IE", "entsoe_eic": "10Y1001A1001A59C", "jurisdiction": "EEA" },
|
|
8
8
|
"eu-north-1": { "name": "Stockholm", "lat": 59.33, "lon": 18.07, "zone": "SE", "entsoe_eic": "10YSE-1--------K", "jurisdiction": "EEA" },
|
|
9
9
|
"ap-southeast-2": { "name": "Sydney", "lat": -33.87, "lon": 151.21, "zone": "AU-NSW", "jurisdiction": "AU" },
|
|
10
|
-
"ca-central-1": { "name": "Montreal", "lat": 45.50, "lon": -73.57, "zone": "CA-QC", "ca_source": "hq", "jurisdiction": "CA" }
|
|
10
|
+
"ca-central-1": { "name": "Montreal", "lat": 45.50, "lon": -73.57, "zone": "CA-QC", "ca_source": "hq", "jurisdiction": "CA" },
|
|
11
|
+
"ap-southeast-1": { "name": "Singapore", "lat": 1.29, "lon": 103.85, "zone": "SG", "annual_zone": "SG", "jurisdiction": "SG" },
|
|
12
|
+
"ap-northeast-1": { "name": "Tokyo", "lat": 35.68, "lon": 139.69, "zone": "JP-TK", "annual_zone": "JP", "jurisdiction": "JP" },
|
|
13
|
+
"ap-south-1": { "name": "Mumbai", "lat": 19.08, "lon": 72.88, "zone": "IN-WE", "annual_zone": "IN", "jurisdiction": "IN" }
|
|
11
14
|
},
|
|
12
15
|
"gcp": {
|
|
13
16
|
"us-central1": { "name": "Iowa", "lat": 41.26, "lon": -95.86, "zone": "US-MIDW-MISO", "eia_ba": "MISO", "jurisdiction": "US" },
|
|
14
17
|
"europe-north1": { "name": "Finland", "lat": 60.57, "lon": 27.19, "zone": "FI", "entsoe_eic": "10YFI-1--------U", "jurisdiction": "EEA" },
|
|
15
18
|
"us-west1": { "name": "Oregon", "lat": 45.60, "lon": -121.18, "zone": "US-NW-PACW", "eia_ba": "BPAT", "jurisdiction": "US" },
|
|
16
19
|
"northamerica-northeast1": { "name": "Montreal", "lat": 45.50, "lon": -73.57, "zone": "CA-QC", "ca_source": "hq", "jurisdiction": "CA" },
|
|
17
|
-
"northamerica-northeast2": { "name": "Toronto", "lat": 43.65, "lon": -79.38, "zone": "CA-ON", "ca_source": "ieso", "jurisdiction": "CA" }
|
|
20
|
+
"northamerica-northeast2": { "name": "Toronto", "lat": 43.65, "lon": -79.38, "zone": "CA-ON", "ca_source": "ieso", "jurisdiction": "CA" },
|
|
21
|
+
"asia-southeast1": { "name": "Singapore", "lat": 1.35, "lon": 103.70, "zone": "SG", "annual_zone": "SG", "jurisdiction": "SG" },
|
|
22
|
+
"asia-northeast1": { "name": "Tokyo", "lat": 35.68, "lon": 139.69, "zone": "JP-TK", "annual_zone": "JP", "jurisdiction": "JP" },
|
|
23
|
+
"asia-south1": { "name": "Mumbai", "lat": 19.08, "lon": 72.88, "zone": "IN-WE", "annual_zone": "IN", "jurisdiction": "IN" }
|
|
18
24
|
},
|
|
19
25
|
"azure": {
|
|
20
26
|
"eastus": { "name": "Virginia", "lat": 37.37, "lon": -79.82, "zone": "US-MIDA-PJM", "eia_ba": "PJM", "jurisdiction": "US" },
|
|
21
27
|
"northeurope": { "name": "Ireland", "lat": 53.35, "lon": -6.26, "zone": "IE", "entsoe_eic": "10Y1001A1001A59C", "jurisdiction": "EEA" },
|
|
22
28
|
"swedencentral": { "name": "Sweden", "lat": 60.67, "lon": 17.14, "zone": "SE", "entsoe_eic": "10YSE-1--------K", "jurisdiction": "EEA" },
|
|
23
29
|
"canadaeast": { "name": "Quebec City", "lat": 46.81, "lon": -71.21, "zone": "CA-QC", "ca_source": "hq", "jurisdiction": "CA" },
|
|
24
|
-
"canadacentral": { "name": "Toronto", "lat": 43.65, "lon": -79.38, "zone": "CA-ON", "ca_source": "ieso", "jurisdiction": "CA" }
|
|
30
|
+
"canadacentral": { "name": "Toronto", "lat": 43.65, "lon": -79.38, "zone": "CA-ON", "ca_source": "ieso", "jurisdiction": "CA" },
|
|
31
|
+
"southeastasia": { "name": "Singapore", "lat": 1.28, "lon": 103.83, "zone": "SG", "annual_zone": "SG", "jurisdiction": "SG" },
|
|
32
|
+
"japaneast": { "name": "Tokyo", "lat": 35.68, "lon": 139.77, "zone": "JP-TK", "annual_zone": "JP", "jurisdiction": "JP" },
|
|
33
|
+
"centralindia": { "name": "Pune", "lat": 18.52, "lon": 73.86, "zone": "IN-WE", "annual_zone": "IN", "jurisdiction": "IN" }
|
|
25
34
|
}
|
|
26
35
|
}
|
|
@@ -76,6 +76,15 @@ def typical() -> dict:
|
|
|
76
76
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
@lru_cache(maxsize=1)
|
|
80
|
+
def annual() -> dict:
|
|
81
|
+
"""Yearly average intensity for regions with no free hourly source (lifecycle, sourced)."""
|
|
82
|
+
path = _DATA_DIR / "annual.json"
|
|
83
|
+
if not path.exists():
|
|
84
|
+
return {}
|
|
85
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
86
|
+
|
|
87
|
+
|
|
79
88
|
@lru_cache(maxsize=1)
|
|
80
89
|
def gpu_power() -> dict:
|
|
81
90
|
"""Directional GPU/accelerator power model (TDP + utilization + host overhead) for training."""
|
|
@@ -77,6 +77,13 @@ def typical_context(provider: str, region: str, g_co2_per_kwh: float, timestamp:
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
|
|
80
|
+
def annual_entry(provider: str, region: str) -> Optional[dict]:
|
|
81
|
+
"""The annual.json zone for a region with no free hourly source (country, g, year), or None."""
|
|
82
|
+
from .static_data import annual, region_meta
|
|
83
|
+
zone = (region_meta(provider, region) or {}).get("annual_zone") # type: ignore[arg-type]
|
|
84
|
+
return annual().get("zones", {}).get(zone) if zone else None
|
|
85
|
+
|
|
86
|
+
|
|
80
87
|
class TypicalCarbonRepository:
|
|
81
88
|
def __init__(self) -> None:
|
|
82
89
|
self._mock = MockCarbonRepository()
|
|
@@ -85,6 +92,11 @@ class TypicalCarbonRepository:
|
|
|
85
92
|
entry = typical_entry(provider, region)
|
|
86
93
|
month = _month(entry) if entry else None
|
|
87
94
|
if not month:
|
|
95
|
+
yearly = annual_entry(provider, region)
|
|
96
|
+
if yearly:
|
|
97
|
+
return CarbonReading(region=region, provider=provider,
|
|
98
|
+
g_co2_per_kwh=yearly["g_co2_per_kwh"], source="ember_annual",
|
|
99
|
+
timestamp=str(yearly["year"]), quality="annual_average")
|
|
88
100
|
return await self._mock.carbon_now(provider, region)
|
|
89
101
|
return CarbonReading(
|
|
90
102
|
region=region,
|
|
@@ -101,6 +113,8 @@ class TypicalCarbonRepository:
|
|
|
101
113
|
entry = typical_entry(provider, region)
|
|
102
114
|
month = _month(entry) if entry else None
|
|
103
115
|
if not month:
|
|
116
|
+
if annual_entry(provider, region):
|
|
117
|
+
return [] # a yearly figure has no hourly shape — don't invent one
|
|
104
118
|
return await self._mock.carbon_history(provider, region)
|
|
105
119
|
return [HourlyCarbon(period=f"H{h:02d}Z", g_co2_per_kwh=g)
|
|
106
120
|
for h, g in enumerate(month["hourly_utc"]) if g is not None]
|
|
@@ -381,6 +381,6 @@ def explain_methodology_and_limits() -> dict:
|
|
|
381
381
|
"forecast": "Day-ahead forecast is RELATIVE: upcoming hours ranked by forecast wind+solar output (ENTSO-E A69), not an absolute gCO2/kWh forecast. EU only; US/uncovered zones degrade to the recent historical pattern (labelled 'historical').",
|
|
382
382
|
"data_residency": "GreenPick flags when a recommended move CROSSES a jurisdiction (esp. leaving the EEA) as a geographic heads-up only. It does NOT assess legal/GDPR compliance, does not encode adequacy decisions, and is not legal advice — confirm with your own data governance.",
|
|
383
383
|
"ignored": "Embodied/hardware carbon and provider renewable-energy certificates.",
|
|
384
|
-
"fallback_data": "Used when live data isn't available (no key, the source didn't respond, or GreenPick isn't set to live). 'typical' = this calendar month's average intensity and average hour-of-day profile (UTC) for the region, computed from 12 months of real hourly EIA-930 / ENTSO-E data with GreenPick's own emission factors (bundled typical.json, refreshed by scripts/refresh_typical.py) — a realistic norm, not a current reading. 'mock' = a hand-set illustrative value with a synthetic hourly pattern, used only
|
|
384
|
+
"fallback_data": "Used when live data isn't available (no key, the source didn't respond, or GreenPick isn't set to live). 'typical' = this calendar month's average intensity and average hour-of-day profile (UTC) for the region, computed from 12 months of real hourly EIA-930 / ENTSO-E data with GreenPick's own emission factors (bundled typical.json, refreshed by scripts/refresh_typical.py) — a realistic norm, not a current reading. 'annual_average' = the country's yearly lifecycle grid intensity from Ember (via Our World in Data; bundled annual.json with source and year), for regions with no free hourly data (Singapore, Tokyo, Mumbai/Pune) — sound for comparing regions, no timing signal (no hourly profile). 'mock' = a hand-set illustrative value with a synthetic hourly pattern, used only where nothing else exists (Sydney); timing advice on mock isn't meaningful. Both carry a live_data_hint saying why the result isn't live and how to turn live data on.",
|
|
385
385
|
"data_sources": "GREENPICK_CARBON_SOURCE=auto routes US regions to EIA-930 and EU regions to ENTSO-E (both free, key needed), and Canadian regions to Hydro-Québec (Québec) and IESO (Ontario) open data (free, no key). All mixes are weighted with the same per-fuel factors. Without live data a region uses its clearly-labelled typical value (or mock where no free source exists). Electricity Maps is available when selected explicitly (GREENPICK_CARBON_SOURCE=electricitymaps).",
|
|
386
386
|
}
|
|
@@ -53,13 +53,13 @@ def _walk(obj: Any) -> Iterator[dict]:
|
|
|
53
53
|
yield from _walk(v)
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
_FALLBACK = ("typical", "mock")
|
|
56
|
+
_FALLBACK = ("typical", "annual_average", "mock")
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def _scan(result: dict) -> tuple[dict[str, set[tuple[str, str]]], bool, set[str]]:
|
|
60
60
|
"""For each fallback kind ('typical' / 'mock'): the regions whose figures use it; whether any
|
|
61
61
|
such figure had no locatable region; and which kinds fed timing advice."""
|
|
62
|
-
regions: dict[str, set[tuple[str, str]]] = {k: set() for k in _FALLBACK}
|
|
62
|
+
regions: dict[str, set[tuple[str, str]]] = {k: set() for k in _FALLBACK} # noqa: C420
|
|
63
63
|
unlocated = False
|
|
64
64
|
timing: set[str] = set()
|
|
65
65
|
for d in _walk(result):
|
|
@@ -117,15 +117,26 @@ def _typical_sentence(locs: set[tuple[str, str]]) -> str:
|
|
|
117
117
|
f"real hourly data{span} — a realistic norm, not a current reading.")
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
def _annual_sentence(locs: set[tuple[str, str]]) -> str:
|
|
121
|
+
from ..data.typical_repository import annual_entry
|
|
122
|
+
found = sorted({(e["country"], e["year"]) for e in (annual_entry(p, r) for p, r in locs) if e})
|
|
123
|
+
which = ", ".join(f"{c} {y}" for c, y in found)
|
|
124
|
+
return (f"Annual figures here ({_names(list(locs))}) are the country's yearly average grid intensity "
|
|
125
|
+
f"(Ember, lifecycle basis{': ' + which if which else ''}) — no free hourly data exists for "
|
|
126
|
+
"these grids, so they're a sound basis for comparing regions but carry no timing signal.")
|
|
127
|
+
|
|
128
|
+
|
|
120
129
|
def build_hint(result: dict) -> Optional[str]:
|
|
121
130
|
by_kind, unlocated, timing = _scan(result)
|
|
122
|
-
all_locs = by_kind["typical"] | by_kind["mock"]
|
|
131
|
+
all_locs = by_kind["typical"] | by_kind["annual_average"] | by_kind["mock"]
|
|
123
132
|
if not all_locs and not unlocated:
|
|
124
133
|
return None
|
|
125
134
|
|
|
126
135
|
parts = []
|
|
127
136
|
if by_kind["typical"]:
|
|
128
137
|
parts.append(_typical_sentence(by_kind["typical"]))
|
|
138
|
+
if by_kind["annual_average"]:
|
|
139
|
+
parts.append(_annual_sentence(by_kind["annual_average"]))
|
|
129
140
|
if by_kind["mock"] or (unlocated and not by_kind["typical"]):
|
|
130
141
|
where = f" for {_names(list(by_kind['mock']))}" if by_kind["mock"] else ""
|
|
131
142
|
parts.append(f"Mock figures{where} are a hand-set illustrative value for the grid, used where "
|
|
@@ -155,6 +166,8 @@ def build_hint(result: dict) -> Optional[str]:
|
|
|
155
166
|
f"{_names(locs) or 'this region'}. Use GREENPICK_CARBON_SOURCE=auto to route "
|
|
156
167
|
"each region to its own live source.")
|
|
157
168
|
elif reason == "uncovered":
|
|
169
|
+
if all(loc in by_kind["annual_average"] for loc in locs):
|
|
170
|
+
continue # already explained by the annual sentence; no action to suggest
|
|
158
171
|
parts.append(f"No free live source covers {_names(locs)} yet, so GreenPick uses its "
|
|
159
172
|
"fallback there.")
|
|
160
173
|
if "typical" in timing:
|
|
@@ -67,6 +67,10 @@ async def carbon_forecast(
|
|
|
67
67
|
if pattern.cleanest_hour_of_day is not None:
|
|
68
68
|
note += (f"A typical day here is cleanest around {pattern.cleanest_hour_of_day:02d}:00 "
|
|
69
69
|
f"UTC - a reasonable default for a recurring job.")
|
|
70
|
+
elif pattern.quality == "annual_average":
|
|
71
|
+
source, quality = "ember_annual", "annual_average"
|
|
72
|
+
note = ("No day-ahead forecast or hourly data exists for this region — only a yearly "
|
|
73
|
+
"average — so GreenPick can't say which hours are cleaner here.")
|
|
70
74
|
elif pattern.quality == "mock":
|
|
71
75
|
# No live history either: the "pattern" is GreenPick's illustrative mock curve. Label it
|
|
72
76
|
# mock — calling it 'historical' would pass a synthetic shape off as real past data.
|
|
@@ -146,7 +146,7 @@ async def compare_regions(
|
|
|
146
146
|
if greener_improvement < MEANINGFUL_PCT:
|
|
147
147
|
rationale = (
|
|
148
148
|
f"Staying on {baseline.provider}/{baseline.region} is reasonable — no candidate "
|
|
149
|
-
f"region is meaningfully greener (
|
|
149
|
+
f"region is meaningfully greener (none is more than ~{MEANINGFUL_PCT:.0f}% cleaner)."
|
|
150
150
|
)
|
|
151
151
|
else:
|
|
152
152
|
extra_lat = greenest.added_latency_ms
|
|
@@ -186,6 +186,9 @@ async def estimate_training_carbon(
|
|
|
186
186
|
f"deferrable, so this stacks on the region choice."
|
|
187
187
|
+ scheduling_service.small_gap_note(advice.cleanest_g_co2_per_kwh or 0,
|
|
188
188
|
advice.dirtiest_g_co2_per_kwh or 0))
|
|
189
|
+
elif advice.hours_analyzed == 0:
|
|
190
|
+
time_txt = (" There's no hourly data for that region (only a yearly average), so GreenPick "
|
|
191
|
+
"can't advise on timing there — the region choice is the lever.")
|
|
189
192
|
else:
|
|
190
193
|
time_txt = (" That region's grid is fairly flat over the window, so when you run matters "
|
|
191
194
|
"little there — the region choice is the lever.")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: greenpick
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Design-time sustainability co-pilot (MCP server): greener, cheaper cloud infrastructure decisions with carbon + cost + latency tradeoffs.
|
|
5
5
|
Author-email: "Data Science Advisers, LLC" <info@datascienceadvisers.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -124,7 +124,7 @@ backfire guards** — is the part nothing else puts together.
|
|
|
124
124
|
|
|
125
125
|
## Scope
|
|
126
126
|
|
|
127
|
-
- Providers: **AWS, GCP, Azure** regions.
|
|
127
|
+
- Providers: **AWS, GCP, Azure** — 25 regions across the US, Europe, Canada, Asia and Australia.
|
|
128
128
|
- Workloads: long-running **web/API services**, **GPU training** runs, and **deferrable jobs**
|
|
129
129
|
(ETL / batch / backups) — placement and timing for each.
|
|
130
130
|
- Carbon = **grid carbon intensity by region** (directional, point-in-time; live US + EU), plus a
|
|
@@ -195,8 +195,12 @@ See `docs/user-questions-backlog.md`.
|
|
|
195
195
|
intensity and average hour-of-day profile (UTC), computed from 12 months of real hourly EIA-930 /
|
|
196
196
|
ENTSO-E data (`data/typical.json`, refreshed by `scripts/refresh_typical.py`). Labelled
|
|
197
197
|
`quality: "typical"` — a realistic norm, never presented as a current reading.
|
|
198
|
-
- **
|
|
199
|
-
|
|
198
|
+
- **Annual** — for Singapore, Tokyo and Mumbai/Pune, where no free hourly data exists: the country's
|
|
199
|
+
yearly lifecycle grid intensity from [Ember](https://ember-energy.org/) (via Our World in Data,
|
|
200
|
+
CC BY 4.0), with source and year (`data/annual.json`). Labelled `quality: "annual_average"`; fine for
|
|
201
|
+
comparing regions, but carries no timing signal.
|
|
202
|
+
- **Mock** — a hand-set illustrative value, labelled `quality: "mock"`, used only where nothing else
|
|
203
|
+
exists (currently Sydney).
|
|
200
204
|
- Cost: a **dated static pricing table** (`data/pricing.json`), refreshed from live Vantage
|
|
201
205
|
pricing via `scripts/refresh_pricing.py` using one matched method across all three clouds.
|
|
202
206
|
- Instance-family efficiency: directional perf-per-watt multipliers (`data/instance_families.json`).
|
|
@@ -300,8 +304,9 @@ Top-level `await` works in Jupyter. In a plain script, put the `with` block insi
|
|
|
300
304
|
and run it with `asyncio.run(...)`.
|
|
301
305
|
|
|
302
306
|
Every carbon figure includes a `quality` field: `live` means a current reading, `typical` means
|
|
303
|
-
this month's average from last year's real data,
|
|
304
|
-
with no
|
|
307
|
+
this month's average from last year's real data, `annual_average` means a sourced yearly national
|
|
308
|
+
figure (regions with no free hourly data), and `mock` means a hand-set value. Check it to confirm
|
|
309
|
+
live data is on. A result that isn't live also includes a
|
|
305
310
|
`live_data_hint` saying why (no key set, the source didn't respond, or no free source covers that
|
|
306
311
|
region) and what to do about it.
|
|
307
312
|
|
|
@@ -14,6 +14,7 @@ src/greenpick.egg-info/requires.txt
|
|
|
14
14
|
src/greenpick.egg-info/top_level.txt
|
|
15
15
|
src/greenpick/data/__init__.py
|
|
16
16
|
src/greenpick/data/_net.py
|
|
17
|
+
src/greenpick/data/annual.json
|
|
17
18
|
src/greenpick/data/auto_repository.py
|
|
18
19
|
src/greenpick/data/canada_repository.py
|
|
19
20
|
src/greenpick/data/eia_repository.py
|
|
@@ -1721,3 +1721,62 @@ def test_montreal_is_a_greener_option_for_new_york():
|
|
|
1721
1721
|
added = (latency_service.estimate_latency_ms("aws", "ca-central-1", nyc)
|
|
1722
1722
|
- latency_service.estimate_latency_ms("aws", "us-east-1", nyc))
|
|
1723
1723
|
assert added < 15 # within an East Coast latency budget
|
|
1724
|
+
|
|
1725
|
+
|
|
1726
|
+
# --- 0.4.0: Asia — Singapore, Tokyo, Mumbai/Pune on sourced yearly figures ---
|
|
1727
|
+
|
|
1728
|
+
from greenpick.data.typical_repository import annual_entry
|
|
1729
|
+
|
|
1730
|
+
_ASIA = [("aws", "ap-southeast-1", "SG"), ("aws", "ap-northeast-1", "JP"), ("aws", "ap-south-1", "IN"),
|
|
1731
|
+
("gcp", "asia-southeast1", "SG"), ("gcp", "asia-northeast1", "JP"), ("gcp", "asia-south1", "IN"),
|
|
1732
|
+
("azure", "southeastasia", "SG"), ("azure", "japaneast", "JP"), ("azure", "centralindia", "IN")]
|
|
1733
|
+
|
|
1734
|
+
|
|
1735
|
+
def test_asia_regions_have_a_sourced_yearly_figure():
|
|
1736
|
+
from greenpick.data.static_data import annual, region_meta
|
|
1737
|
+
assert "Ember" in annual()["_source"]
|
|
1738
|
+
for prov, reg, zone in _ASIA:
|
|
1739
|
+
meta = region_meta(prov, reg)
|
|
1740
|
+
assert meta["annual_zone"] == zone and meta["jurisdiction"] == zone
|
|
1741
|
+
e = annual_entry(prov, reg)
|
|
1742
|
+
assert e["year"] >= 2024 and 300 < e["g_co2_per_kwh"] < 900
|
|
1743
|
+
|
|
1744
|
+
|
|
1745
|
+
@pytest.mark.asyncio
|
|
1746
|
+
async def test_asia_reading_is_labelled_annual_and_has_no_hourly_shape():
|
|
1747
|
+
repo = TypicalCarbonRepository()
|
|
1748
|
+
r = await repo.carbon_now("aws", "ap-southeast-1")
|
|
1749
|
+
assert (r.quality, r.source) == ("annual_average", "ember_annual")
|
|
1750
|
+
assert r.g_co2_per_kwh == annual_entry("aws", "ap-southeast-1")["g_co2_per_kwh"]
|
|
1751
|
+
assert await repo.carbon_history("aws", "ap-southeast-1") == [] # never invent a curve
|
|
1752
|
+
|
|
1753
|
+
|
|
1754
|
+
@pytest.mark.asyncio
|
|
1755
|
+
async def test_timing_tools_say_there_is_no_hourly_data_for_asia():
|
|
1756
|
+
from greenpick.services import scheduling_service, forecast_service, training_service
|
|
1757
|
+
repo = TypicalCarbonRepository()
|
|
1758
|
+
tod = await scheduling_service.cleanest_time_of_day(repo, "aws", "ap-northeast-1")
|
|
1759
|
+
assert tod.hours_analyzed == 0 and "No recent hourly history" in tod.note
|
|
1760
|
+
fc = await forecast_service.carbon_forecast(repo, "aws", "ap-northeast-1")
|
|
1761
|
+
assert fc.quality == "annual_average" and "yearly average" in fc.note
|
|
1762
|
+
tr = await training_service.estimate_training_carbon(
|
|
1763
|
+
repo, gpu="a100", gpu_count=8, gpu_hours=40, region="ap-south-1", provider="aws")
|
|
1764
|
+
assert "no hourly data" in tr.rationale and "fairly flat" not in tr.rationale
|
|
1765
|
+
|
|
1766
|
+
|
|
1767
|
+
def test_hint_explains_annual_figures_without_asking_for_a_key(monkeypatch):
|
|
1768
|
+
monkeypatch.setattr(_config, "SETTINGS", _settings(source="auto", eia="k", entsoe="t"))
|
|
1769
|
+
r = {"provider": "gcp", "region": "asia-south1", "quality": "annual_average", "source": "ember_annual"}
|
|
1770
|
+
hint = data_hint.with_hint(r)["live_data_hint"]
|
|
1771
|
+
assert "yearly average grid intensity (Ember, lifecycle basis: India 2025)" in hint
|
|
1772
|
+
assert "no timing signal" in hint
|
|
1773
|
+
assert "set EIA_API_KEY" not in hint and "No free live source covers" not in hint
|
|
1774
|
+
|
|
1775
|
+
|
|
1776
|
+
@pytest.mark.asyncio
|
|
1777
|
+
async def test_asian_users_are_now_served_from_asia():
|
|
1778
|
+
from greenpick.services import serving_service
|
|
1779
|
+
hubs = [ServingHub(name="apac", lat=0, lon=0, share=1.0)]
|
|
1780
|
+
adv = await serving_service.serving_pattern(TypicalCarbonRepository(), hubs, Workload(vcpu=1, ram_gb=2))
|
|
1781
|
+
assert adv.single_region.region in ("ap-southeast-1", "asia-southeast1", "southeastasia")
|
|
1782
|
+
assert adv.single_region.weighted_latency_ms < 40 # was ~199 ms from Sydney
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|