greenpick 0.4.0__tar.gz → 0.4.2__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.4.0/src/greenpick.egg-info → greenpick-0.4.2}/PKG-INFO +10 -9
- {greenpick-0.4.0 → greenpick-0.4.2}/README.md +8 -7
- {greenpick-0.4.0 → greenpick-0.4.2}/pyproject.toml +2 -2
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/__init__.py +1 -1
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/annual.json +3 -1
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/regions.json +1 -1
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/server.py +27 -16
- {greenpick-0.4.0 → greenpick-0.4.2/src/greenpick.egg-info}/PKG-INFO +10 -9
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick.egg-info/requires.txt +1 -1
- {greenpick-0.4.0 → greenpick-0.4.2}/tests/test_services.py +68 -4
- {greenpick-0.4.0 → greenpick-0.4.2}/LICENSE +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/setup.cfg +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/__main__.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/config.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/__init__.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/_net.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/auto_repository.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/canada_repository.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/eia_repository.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/electricitymaps_repository.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/emission_factors.json +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/entsoe_repository.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/gpu_power.json +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/inference_energy.json +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/instance_families.json +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/mock_repository.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/network.json +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/power.json +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/pricing.json +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/repository.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/static_data.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/typical.json +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/typical_repository.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/models.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/__init__.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/carbon_service.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/cost_service.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/data_hint.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/forecast_service.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/inference_service.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/instance_service.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/latency_service.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/locality_service.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/metric_scaffold_service.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/recommender.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/residency_service.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/rightsizing_service.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/scheduling_service.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/serving_service.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/training_service.py +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick.egg-info/SOURCES.txt +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick.egg-info/dependency_links.txt +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick.egg-info/entry_points.txt +0 -0
- {greenpick-0.4.0 → greenpick-0.4.2}/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.4.
|
|
3
|
+
Version: 0.4.2
|
|
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
|
|
@@ -19,7 +19,7 @@ Classifier: Topic :: Scientific/Engineering
|
|
|
19
19
|
Requires-Python: >=3.11
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
|
-
Requires-Dist: mcp<2,>=1.
|
|
22
|
+
Requires-Dist: mcp<2,>=1.7
|
|
23
23
|
Requires-Dist: pydantic>=2.0
|
|
24
24
|
Requires-Dist: httpx>=0.27
|
|
25
25
|
Provides-Extra: dev
|
|
@@ -60,11 +60,12 @@ seconds. (It responds when you ask; auto-nudging on infra-code changes is on the
|
|
|
60
60
|
> this month's average for each grid, from a year of real data.
|
|
61
61
|
|
|
62
62
|
<p align="center">
|
|
63
|
-
<img src="https://raw.githubusercontent.com/bithug01/greenpick/master/docs/img/region-carbon-30x.svg" alt="Same H100 training run, ~
|
|
63
|
+
<img src="https://raw.githubusercontent.com/bithug01/greenpick/master/docs/img/region-carbon-30x.svg" alt="Same H100 training run, ~28× the carbon depending only on region (Stockholm vs Sydney)" width="560" />
|
|
64
64
|
</p>
|
|
65
65
|
|
|
66
|
-
*The same 40-GPU-hour training run emits ~0.
|
|
67
|
-
difference from region choice alone
|
|
66
|
+
*The same 40-GPU-hour training run emits ~0.6 kg CO₂ in Stockholm vs ~17.5 kg in Sydney — a ~28×
|
|
67
|
+
difference from region choice alone (typical October grids; Sydney on Ember's 2025 national figure,
|
|
68
|
+
a lower bound for its coal-heavier state grid). Ask GreenPick and it surfaces that (with the cost and latency
|
|
68
69
|
tradeoff) while you're still in the editor, before you commit.*
|
|
69
70
|
|
|
70
71
|
**▶ [See the 5-minute demo](docs/demo.md)** — three scenarios with real tool output (training placement, the egress backfire, the forecast).
|
|
@@ -195,12 +196,12 @@ See `docs/user-questions-backlog.md`.
|
|
|
195
196
|
intensity and average hour-of-day profile (UTC), computed from 12 months of real hourly EIA-930 /
|
|
196
197
|
ENTSO-E data (`data/typical.json`, refreshed by `scripts/refresh_typical.py`). Labelled
|
|
197
198
|
`quality: "typical"` — a realistic norm, never presented as a current reading.
|
|
198
|
-
- **Annual** — for Singapore, Tokyo
|
|
199
|
+
- **Annual** — for Singapore, Tokyo, Mumbai/Pune and Sydney, where no free hourly data exists: the country's
|
|
199
200
|
yearly lifecycle grid intensity from [Ember](https://ember-energy.org/) (via Our World in Data,
|
|
200
201
|
CC BY 4.0), with source and year (`data/annual.json`). Labelled `quality: "annual_average"`; fine for
|
|
201
202
|
comparing regions, but carries no timing signal.
|
|
202
|
-
- **Mock** — a hand-set illustrative value, labelled `quality: "mock"`,
|
|
203
|
-
|
|
203
|
+
- **Mock** — a hand-set illustrative value, labelled `quality: "mock"`, kept only as a last resort if
|
|
204
|
+
every other source is missing. No catalog region relies on it.
|
|
204
205
|
- Cost: a **dated static pricing table** (`data/pricing.json`), refreshed from live Vantage
|
|
205
206
|
pricing via `scripts/refresh_pricing.py` using one matched method across all three clouds.
|
|
206
207
|
- Instance-family efficiency: directional perf-per-watt multipliers (`data/instance_families.json`).
|
|
@@ -318,7 +319,7 @@ carbon, set two free keys (`EIA_API_KEY` for US, `ENTSOE_TOKEN` for EU) and
|
|
|
318
319
|
`GREENPICK_CARBON_SOURCE=auto` — see [Turn on live data](#turn-on-live-data).
|
|
319
320
|
|
|
320
321
|
**Why "directional" and not audited numbers?** Because it's for a *design-time decision*, not a
|
|
321
|
-
compliance report. "Stockholm is ~
|
|
322
|
+
compliance report. "Stockholm is ~28× cleaner than Sydney" is what you need to pick a region in
|
|
322
323
|
seconds — a certified figure is a different job for a different tool (Electricity Maps, WattTime).
|
|
323
324
|
GreenPick is for *decisions*, not *disclosures*, and says so in every tool's limitations.
|
|
324
325
|
|
|
@@ -30,11 +30,12 @@ seconds. (It responds when you ask; auto-nudging on infra-code changes is on the
|
|
|
30
30
|
> this month's average for each grid, from a year of real data.
|
|
31
31
|
|
|
32
32
|
<p align="center">
|
|
33
|
-
<img src="https://raw.githubusercontent.com/bithug01/greenpick/master/docs/img/region-carbon-30x.svg" alt="Same H100 training run, ~
|
|
33
|
+
<img src="https://raw.githubusercontent.com/bithug01/greenpick/master/docs/img/region-carbon-30x.svg" alt="Same H100 training run, ~28× the carbon depending only on region (Stockholm vs Sydney)" width="560" />
|
|
34
34
|
</p>
|
|
35
35
|
|
|
36
|
-
*The same 40-GPU-hour training run emits ~0.
|
|
37
|
-
difference from region choice alone
|
|
36
|
+
*The same 40-GPU-hour training run emits ~0.6 kg CO₂ in Stockholm vs ~17.5 kg in Sydney — a ~28×
|
|
37
|
+
difference from region choice alone (typical October grids; Sydney on Ember's 2025 national figure,
|
|
38
|
+
a lower bound for its coal-heavier state grid). Ask GreenPick and it surfaces that (with the cost and latency
|
|
38
39
|
tradeoff) while you're still in the editor, before you commit.*
|
|
39
40
|
|
|
40
41
|
**▶ [See the 5-minute demo](docs/demo.md)** — three scenarios with real tool output (training placement, the egress backfire, the forecast).
|
|
@@ -165,12 +166,12 @@ See `docs/user-questions-backlog.md`.
|
|
|
165
166
|
intensity and average hour-of-day profile (UTC), computed from 12 months of real hourly EIA-930 /
|
|
166
167
|
ENTSO-E data (`data/typical.json`, refreshed by `scripts/refresh_typical.py`). Labelled
|
|
167
168
|
`quality: "typical"` — a realistic norm, never presented as a current reading.
|
|
168
|
-
- **Annual** — for Singapore, Tokyo
|
|
169
|
+
- **Annual** — for Singapore, Tokyo, Mumbai/Pune and Sydney, where no free hourly data exists: the country's
|
|
169
170
|
yearly lifecycle grid intensity from [Ember](https://ember-energy.org/) (via Our World in Data,
|
|
170
171
|
CC BY 4.0), with source and year (`data/annual.json`). Labelled `quality: "annual_average"`; fine for
|
|
171
172
|
comparing regions, but carries no timing signal.
|
|
172
|
-
- **Mock** — a hand-set illustrative value, labelled `quality: "mock"`,
|
|
173
|
-
|
|
173
|
+
- **Mock** — a hand-set illustrative value, labelled `quality: "mock"`, kept only as a last resort if
|
|
174
|
+
every other source is missing. No catalog region relies on it.
|
|
174
175
|
- Cost: a **dated static pricing table** (`data/pricing.json`), refreshed from live Vantage
|
|
175
176
|
pricing via `scripts/refresh_pricing.py` using one matched method across all three clouds.
|
|
176
177
|
- Instance-family efficiency: directional perf-per-watt multipliers (`data/instance_families.json`).
|
|
@@ -288,7 +289,7 @@ carbon, set two free keys (`EIA_API_KEY` for US, `ENTSOE_TOKEN` for EU) and
|
|
|
288
289
|
`GREENPICK_CARBON_SOURCE=auto` — see [Turn on live data](#turn-on-live-data).
|
|
289
290
|
|
|
290
291
|
**Why "directional" and not audited numbers?** Because it's for a *design-time decision*, not a
|
|
291
|
-
compliance report. "Stockholm is ~
|
|
292
|
+
compliance report. "Stockholm is ~28× cleaner than Sydney" is what you need to pick a region in
|
|
292
293
|
seconds — a certified figure is a different job for a different tool (Electricity Maps, WattTime).
|
|
293
294
|
GreenPick is for *decisions*, not *disclosures*, and says so in every tool's limitations.
|
|
294
295
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "greenpick"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.2"
|
|
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"
|
|
@@ -19,7 +19,7 @@ classifiers = [
|
|
|
19
19
|
"Topic :: Scientific/Engineering",
|
|
20
20
|
]
|
|
21
21
|
dependencies = [
|
|
22
|
-
"mcp>=1.
|
|
22
|
+
"mcp>=1.7,<2", # Model Context Protocol SDK (FastMCP lives here). >=1.7: tool annotations. Pinned <2: mcp 2.x
|
|
23
23
|
# renamed FastMCP -> MCPServer and changed APIs; this code targets the 1.x API.
|
|
24
24
|
"pydantic>=2.0",
|
|
25
25
|
"httpx>=0.27", # async HTTP for live data sources
|
|
@@ -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.4.
|
|
7
|
+
__version__ = "0.4.2"
|
|
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.
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"JP": { "country": "Japan", "g_co2_per_kwh": 477.3, "year": 2025,
|
|
9
9
|
"note": "National average; the Tokyo (TEPCO) area mix differs somewhat and varies by hour." },
|
|
10
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." }
|
|
11
|
+
"note": "National average; Maharashtra (Mumbai, Pune) runs about 6% above it on Ember's state-level data." },
|
|
12
|
+
"AU": { "country": "Australia", "g_co2_per_kwh": 525.2, "year": 2025,
|
|
13
|
+
"note": "National average; New South Wales (Sydney) is more coal-heavy and runs higher, so treat this as a lower bound for Sydney." }
|
|
12
14
|
}
|
|
13
15
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"us-west-2": { "name": "Oregon", "lat": 45.87, "lon": -119.69, "zone": "US-NW-PACW", "eia_ba": "BPAT", "jurisdiction": "US" },
|
|
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
|
-
"ap-southeast-2": { "name": "Sydney", "lat": -33.87, "lon": 151.21, "zone": "AU-NSW", "jurisdiction": "AU" },
|
|
9
|
+
"ap-southeast-2": { "name": "Sydney", "lat": -33.87, "lon": 151.21, "zone": "AU-NSW", "annual_zone": "AU", "jurisdiction": "AU" },
|
|
10
10
|
"ca-central-1": { "name": "Montreal", "lat": 45.50, "lon": -73.57, "zone": "CA-QC", "ca_source": "hq", "jurisdiction": "CA" },
|
|
11
11
|
"ap-southeast-1": { "name": "Singapore", "lat": 1.29, "lon": 103.85, "zone": "SG", "annual_zone": "SG", "jurisdiction": "SG" },
|
|
12
12
|
"ap-northeast-1": { "name": "Tokyo", "lat": 35.68, "lon": 139.69, "zone": "JP-TK", "annual_zone": "JP", "jurisdiction": "JP" },
|
|
@@ -8,6 +8,17 @@ from __future__ import annotations
|
|
|
8
8
|
from typing import Optional
|
|
9
9
|
|
|
10
10
|
from mcp.server.fastmcp import FastMCP
|
|
11
|
+
from mcp.types import ToolAnnotations
|
|
12
|
+
|
|
13
|
+
# Tool annotations let MCP hosts decide whether to ask the user before a call (and some
|
|
14
|
+
# directories require them). Every GreenPick tool only reads and computes: nothing is created,
|
|
15
|
+
# changed or deleted, so repeating a call has no extra effect. Most fetch live grid data from
|
|
16
|
+
# outside services (EIA, ENTSO-E, Hydro-Québec, IESO) -> openWorldHint; the methodology tool
|
|
17
|
+
# returns fixed text only.
|
|
18
|
+
LIVE_DATA_TOOL = ToolAnnotations(readOnlyHint=True, destructiveHint=False,
|
|
19
|
+
idempotentHint=True, openWorldHint=True)
|
|
20
|
+
LOCAL_TOOL = ToolAnnotations(readOnlyHint=True, destructiveHint=False,
|
|
21
|
+
idempotentHint=True, openWorldHint=False)
|
|
11
22
|
|
|
12
23
|
from .models import Workload, GeoPoint, Provider
|
|
13
24
|
from .data import get_repository
|
|
@@ -48,7 +59,7 @@ mcp = FastMCP("greenpick", instructions=INSTRUCTIONS)
|
|
|
48
59
|
_repo = get_repository()
|
|
49
60
|
|
|
50
61
|
|
|
51
|
-
@mcp.tool()
|
|
62
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
52
63
|
async def compare_regions_tri_axis(
|
|
53
64
|
user_lat: float,
|
|
54
65
|
user_lon: float,
|
|
@@ -80,7 +91,7 @@ async def compare_regions_tri_axis(
|
|
|
80
91
|
return with_hint(rec.model_dump())
|
|
81
92
|
|
|
82
93
|
|
|
83
|
-
@mcp.tool()
|
|
94
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
84
95
|
async def greener_alternative_to(
|
|
85
96
|
current_region: str,
|
|
86
97
|
user_lat: float,
|
|
@@ -110,7 +121,7 @@ async def greener_alternative_to(
|
|
|
110
121
|
return with_hint(rec.model_dump())
|
|
111
122
|
|
|
112
123
|
|
|
113
|
-
@mcp.tool()
|
|
124
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
114
125
|
async def estimate_workload_carbon(
|
|
115
126
|
provider: str,
|
|
116
127
|
region: str,
|
|
@@ -134,7 +145,7 @@ async def estimate_workload_carbon(
|
|
|
134
145
|
})
|
|
135
146
|
|
|
136
147
|
|
|
137
|
-
@mcp.tool()
|
|
148
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
138
149
|
async def region_carbon_now(provider: str, region: str) -> dict:
|
|
139
150
|
"""Current grid carbon intensity for a region, with source, timestamp, and data freshness.
|
|
140
151
|
When the reading is live, also says how it compares with what's typical for that hour this month
|
|
@@ -149,7 +160,7 @@ async def region_carbon_now(provider: str, region: str) -> dict:
|
|
|
149
160
|
return with_hint(out)
|
|
150
161
|
|
|
151
162
|
|
|
152
|
-
@mcp.tool()
|
|
163
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
153
164
|
async def cleanest_hours_to_run(
|
|
154
165
|
provider: str, region: str, top_n: int = 3, window_hours: Optional[int] = None,
|
|
155
166
|
) -> dict:
|
|
@@ -164,7 +175,7 @@ async def cleanest_hours_to_run(
|
|
|
164
175
|
return with_hint(advice.model_dump())
|
|
165
176
|
|
|
166
177
|
|
|
167
|
-
@mcp.tool()
|
|
178
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
168
179
|
async def greener_instance_family(
|
|
169
180
|
provider: str,
|
|
170
181
|
region: str,
|
|
@@ -192,7 +203,7 @@ async def greener_instance_family(
|
|
|
192
203
|
return with_hint(advice.model_dump())
|
|
193
204
|
|
|
194
205
|
|
|
195
|
-
@mcp.tool()
|
|
206
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
196
207
|
async def estimate_inference_carbon(
|
|
197
208
|
size_class: str,
|
|
198
209
|
monthly_requests: float,
|
|
@@ -214,7 +225,7 @@ async def estimate_inference_carbon(
|
|
|
214
225
|
return with_hint(advice.model_dump())
|
|
215
226
|
|
|
216
227
|
|
|
217
|
-
@mcp.tool()
|
|
228
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
218
229
|
async def rightsizing_check(
|
|
219
230
|
provider: str,
|
|
220
231
|
region: str,
|
|
@@ -237,7 +248,7 @@ async def rightsizing_check(
|
|
|
237
248
|
return with_hint(advice.model_dump())
|
|
238
249
|
|
|
239
250
|
|
|
240
|
-
@mcp.tool()
|
|
251
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
241
252
|
async def carbon_metric_scaffold(
|
|
242
253
|
language: str = "python",
|
|
243
254
|
region: Optional[str] = None,
|
|
@@ -260,7 +271,7 @@ async def carbon_metric_scaffold(
|
|
|
260
271
|
return with_hint(scaffold.model_dump())
|
|
261
272
|
|
|
262
273
|
|
|
263
|
-
@mcp.tool()
|
|
274
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
264
275
|
async def serving_pattern_advice(
|
|
265
276
|
hubs: list[dict],
|
|
266
277
|
vcpu: float = 1,
|
|
@@ -290,7 +301,7 @@ async def serving_pattern_advice(
|
|
|
290
301
|
return with_hint(advice.model_dump())
|
|
291
302
|
|
|
292
303
|
|
|
293
|
-
@mcp.tool()
|
|
304
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
294
305
|
async def data_locality_egress_check(
|
|
295
306
|
compute_provider: str,
|
|
296
307
|
compute_region: str,
|
|
@@ -312,7 +323,7 @@ async def data_locality_egress_check(
|
|
|
312
323
|
return with_hint(advice.model_dump())
|
|
313
324
|
|
|
314
325
|
|
|
315
|
-
@mcp.tool()
|
|
326
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
316
327
|
async def carbon_forecast(
|
|
317
328
|
provider: str, region: str, hours_ahead: int = 24, top_n: int = 5,
|
|
318
329
|
) -> dict:
|
|
@@ -327,7 +338,7 @@ async def carbon_forecast(
|
|
|
327
338
|
return with_hint(advice.model_dump())
|
|
328
339
|
|
|
329
340
|
|
|
330
|
-
@mcp.tool()
|
|
341
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
331
342
|
async def cleanest_time_of_day(provider: str, region: str) -> dict:
|
|
332
343
|
"""For a RECURRING job (e.g. a nightly batch you run every day), find the region's cleanest
|
|
333
344
|
time of day — each hour-of-day averaged across the recent days of data. Returns the cleanest/
|
|
@@ -337,7 +348,7 @@ async def cleanest_time_of_day(provider: str, region: str) -> dict:
|
|
|
337
348
|
return with_hint(advice.model_dump())
|
|
338
349
|
|
|
339
350
|
|
|
340
|
-
@mcp.tool()
|
|
351
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
341
352
|
async def estimate_training_carbon(
|
|
342
353
|
gpu: str,
|
|
343
354
|
gpu_count: int,
|
|
@@ -364,7 +375,7 @@ async def estimate_training_carbon(
|
|
|
364
375
|
return with_hint(advice.model_dump())
|
|
365
376
|
|
|
366
377
|
|
|
367
|
-
@mcp.tool()
|
|
378
|
+
@mcp.tool(annotations=LOCAL_TOOL)
|
|
368
379
|
def explain_methodology_and_limits() -> dict:
|
|
369
380
|
"""GreenPick's assumptions and what it explicitly does NOT claim. Read this before trusting a number."""
|
|
370
381
|
return {
|
|
@@ -381,6 +392,6 @@ def explain_methodology_and_limits() -> dict:
|
|
|
381
392
|
"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
393
|
"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
394
|
"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. '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,
|
|
395
|
+
"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, Sydney — national figures, so Sydney's is a lower bound for its coal-heavier state) — sound for comparing regions, no timing signal (no hourly profile). 'mock' = a hand-set illustrative value with a synthetic hourly pattern, a last resort no catalog region relies on; 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
396
|
"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
397
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: greenpick
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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
|
|
@@ -19,7 +19,7 @@ Classifier: Topic :: Scientific/Engineering
|
|
|
19
19
|
Requires-Python: >=3.11
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
|
-
Requires-Dist: mcp<2,>=1.
|
|
22
|
+
Requires-Dist: mcp<2,>=1.7
|
|
23
23
|
Requires-Dist: pydantic>=2.0
|
|
24
24
|
Requires-Dist: httpx>=0.27
|
|
25
25
|
Provides-Extra: dev
|
|
@@ -60,11 +60,12 @@ seconds. (It responds when you ask; auto-nudging on infra-code changes is on the
|
|
|
60
60
|
> this month's average for each grid, from a year of real data.
|
|
61
61
|
|
|
62
62
|
<p align="center">
|
|
63
|
-
<img src="https://raw.githubusercontent.com/bithug01/greenpick/master/docs/img/region-carbon-30x.svg" alt="Same H100 training run, ~
|
|
63
|
+
<img src="https://raw.githubusercontent.com/bithug01/greenpick/master/docs/img/region-carbon-30x.svg" alt="Same H100 training run, ~28× the carbon depending only on region (Stockholm vs Sydney)" width="560" />
|
|
64
64
|
</p>
|
|
65
65
|
|
|
66
|
-
*The same 40-GPU-hour training run emits ~0.
|
|
67
|
-
difference from region choice alone
|
|
66
|
+
*The same 40-GPU-hour training run emits ~0.6 kg CO₂ in Stockholm vs ~17.5 kg in Sydney — a ~28×
|
|
67
|
+
difference from region choice alone (typical October grids; Sydney on Ember's 2025 national figure,
|
|
68
|
+
a lower bound for its coal-heavier state grid). Ask GreenPick and it surfaces that (with the cost and latency
|
|
68
69
|
tradeoff) while you're still in the editor, before you commit.*
|
|
69
70
|
|
|
70
71
|
**▶ [See the 5-minute demo](docs/demo.md)** — three scenarios with real tool output (training placement, the egress backfire, the forecast).
|
|
@@ -195,12 +196,12 @@ See `docs/user-questions-backlog.md`.
|
|
|
195
196
|
intensity and average hour-of-day profile (UTC), computed from 12 months of real hourly EIA-930 /
|
|
196
197
|
ENTSO-E data (`data/typical.json`, refreshed by `scripts/refresh_typical.py`). Labelled
|
|
197
198
|
`quality: "typical"` — a realistic norm, never presented as a current reading.
|
|
198
|
-
- **Annual** — for Singapore, Tokyo
|
|
199
|
+
- **Annual** — for Singapore, Tokyo, Mumbai/Pune and Sydney, where no free hourly data exists: the country's
|
|
199
200
|
yearly lifecycle grid intensity from [Ember](https://ember-energy.org/) (via Our World in Data,
|
|
200
201
|
CC BY 4.0), with source and year (`data/annual.json`). Labelled `quality: "annual_average"`; fine for
|
|
201
202
|
comparing regions, but carries no timing signal.
|
|
202
|
-
- **Mock** — a hand-set illustrative value, labelled `quality: "mock"`,
|
|
203
|
-
|
|
203
|
+
- **Mock** — a hand-set illustrative value, labelled `quality: "mock"`, kept only as a last resort if
|
|
204
|
+
every other source is missing. No catalog region relies on it.
|
|
204
205
|
- Cost: a **dated static pricing table** (`data/pricing.json`), refreshed from live Vantage
|
|
205
206
|
pricing via `scripts/refresh_pricing.py` using one matched method across all three clouds.
|
|
206
207
|
- Instance-family efficiency: directional perf-per-watt multipliers (`data/instance_families.json`).
|
|
@@ -318,7 +319,7 @@ carbon, set two free keys (`EIA_API_KEY` for US, `ENTSOE_TOKEN` for EU) and
|
|
|
318
319
|
`GREENPICK_CARBON_SOURCE=auto` — see [Turn on live data](#turn-on-live-data).
|
|
319
320
|
|
|
320
321
|
**Why "directional" and not audited numbers?** Because it's for a *design-time decision*, not a
|
|
321
|
-
compliance report. "Stockholm is ~
|
|
322
|
+
compliance report. "Stockholm is ~28× cleaner than Sydney" is what you need to pick a region in
|
|
322
323
|
seconds — a certified figure is a different job for a different tool (Electricity Maps, WattTime).
|
|
323
324
|
GreenPick is for *decisions*, not *disclosures*, and says so in every tool's limitations.
|
|
324
325
|
|
|
@@ -348,10 +348,10 @@ async def test_auto_router_picks_source_by_region(monkeypatch):
|
|
|
348
348
|
repo = AutoRepository(eia_key="k", entsoe_token="t")
|
|
349
349
|
us = await repo.carbon_now("aws", "us-east-1") # -> EIA
|
|
350
350
|
eu = await repo.carbon_now("aws", "eu-north-1") # -> ENTSO-E
|
|
351
|
-
syd = await repo.carbon_now("aws", "ap-southeast-2") # ->
|
|
351
|
+
syd = await repo.carbon_now("aws", "ap-southeast-2") # -> no live source: sourced yearly figure
|
|
352
352
|
assert us.source == "eia"
|
|
353
353
|
assert eu.source == "entsoe"
|
|
354
|
-
assert syd.quality == "
|
|
354
|
+
assert syd.quality == "annual_average"
|
|
355
355
|
|
|
356
356
|
|
|
357
357
|
@pytest.mark.asyncio
|
|
@@ -1494,14 +1494,26 @@ async def test_typical_reading_is_this_months_real_average():
|
|
|
1494
1494
|
|
|
1495
1495
|
|
|
1496
1496
|
@pytest.mark.asyncio
|
|
1497
|
-
async def
|
|
1497
|
+
async def test_mock_is_only_the_last_resort(monkeypatch):
|
|
1498
|
+
from greenpick.data import typical_repository as tr
|
|
1498
1499
|
repo = TypicalCarbonRepository()
|
|
1500
|
+
# Sydney has no typical data but a sourced yearly figure -> annual, not mock.
|
|
1499
1501
|
r = await repo.carbon_now("aws", "ap-southeast-2")
|
|
1500
|
-
assert r.quality == "
|
|
1502
|
+
assert r.quality == "annual_average" and r.g_co2_per_kwh == 525.2
|
|
1503
|
+
# With no yearly figure either, mock still answers (labelled) rather than failing.
|
|
1504
|
+
monkeypatch.setattr(tr, "annual_entry", lambda p, reg: None)
|
|
1505
|
+
m = await repo.carbon_now("aws", "ap-southeast-2")
|
|
1506
|
+
assert m.quality == "mock"
|
|
1501
1507
|
hist = await repo.carbon_history("aws", "ap-southeast-2")
|
|
1502
1508
|
assert hist and hist[0].period.startswith("H") and not hist[0].period.endswith("Z")
|
|
1503
1509
|
|
|
1504
1510
|
|
|
1511
|
+
def test_no_catalog_region_relies_on_mock():
|
|
1512
|
+
from greenpick.data.static_data import iter_regions
|
|
1513
|
+
for prov, reg, meta in iter_regions():
|
|
1514
|
+
assert typical_entry(prov, reg) or annual_entry(prov, reg), f"{prov}/{reg} would be mock"
|
|
1515
|
+
|
|
1516
|
+
|
|
1505
1517
|
@pytest.mark.asyncio
|
|
1506
1518
|
async def test_typical_history_is_the_months_real_profile_in_utc():
|
|
1507
1519
|
from greenpick.services import scheduling_service as ss
|
|
@@ -1780,3 +1792,55 @@ async def test_asian_users_are_now_served_from_asia():
|
|
|
1780
1792
|
adv = await serving_service.serving_pattern(TypicalCarbonRepository(), hubs, Workload(vcpu=1, ram_gb=2))
|
|
1781
1793
|
assert adv.single_region.region in ("ap-southeast-1", "asia-southeast1", "southeastasia")
|
|
1782
1794
|
assert adv.single_region.weighted_latency_ms < 40 # was ~199 ms from Sydney
|
|
1795
|
+
|
|
1796
|
+
|
|
1797
|
+
# --- 0.4.2: tool annotations on every tool; every tool tested by name ---
|
|
1798
|
+
|
|
1799
|
+
def test_every_tool_declares_all_four_annotation_hints():
|
|
1800
|
+
import asyncio
|
|
1801
|
+
from greenpick import server
|
|
1802
|
+
tools = asyncio.run(server.mcp.list_tools())
|
|
1803
|
+
assert len(tools) == 15
|
|
1804
|
+
for t in tools:
|
|
1805
|
+
a = t.annotations
|
|
1806
|
+
assert a is not None, t.name
|
|
1807
|
+
for hint in (a.readOnlyHint, a.destructiveHint, a.idempotentHint, a.openWorldHint):
|
|
1808
|
+
assert isinstance(hint, bool), t.name # explicit booleans, none missing
|
|
1809
|
+
assert a.readOnlyHint and not a.destructiveHint and a.idempotentHint, t.name
|
|
1810
|
+
local = {t.name for t in tools if not t.annotations.openWorldHint}
|
|
1811
|
+
assert local == {"explain_methodology_and_limits"} # the only tool that fetches nothing
|
|
1812
|
+
|
|
1813
|
+
|
|
1814
|
+
def _tool(name):
|
|
1815
|
+
from greenpick import server
|
|
1816
|
+
f = getattr(server, name)
|
|
1817
|
+
return getattr(f, "fn", f)
|
|
1818
|
+
|
|
1819
|
+
|
|
1820
|
+
@pytest.mark.asyncio
|
|
1821
|
+
async def test_tool_greener_alternative_to(monkeypatch):
|
|
1822
|
+
from greenpick import server
|
|
1823
|
+
monkeypatch.setattr(server, "_repo", MockCarbonRepository())
|
|
1824
|
+
r = await _tool("greener_alternative_to")(current_region="us-east-1", user_lat=40.71, user_lon=-74.01,
|
|
1825
|
+
vcpu=4, ram_gb=16, max_added_latency_ms=50)
|
|
1826
|
+
assert r["recommended_region"] in ("aws/ca-central-1", "gcp/northamerica-northeast1", "azure/canadaeast")
|
|
1827
|
+
picked = next(c for c in r["candidates"]
|
|
1828
|
+
if f"{c['provider']}/{c['region']}" == r["recommended_region"])
|
|
1829
|
+
assert picked["added_latency_ms"] <= 50 + 15
|
|
1830
|
+
|
|
1831
|
+
|
|
1832
|
+
@pytest.mark.asyncio
|
|
1833
|
+
async def test_tool_cleanest_hours_to_run(monkeypatch):
|
|
1834
|
+
from greenpick import server
|
|
1835
|
+
monkeypatch.setattr(server, "_repo", TypicalCarbonRepository())
|
|
1836
|
+
r = await _tool("cleanest_hours_to_run")(provider="aws", region="us-west-2", window_hours=4)
|
|
1837
|
+
assert r["quality"] == "typical" and r["cleanest"]
|
|
1838
|
+
assert r["window"]["window_hours"] == 4 and r["window"]["start_hour_of_day"] is not None
|
|
1839
|
+
assert "live_data_hint" in r
|
|
1840
|
+
|
|
1841
|
+
|
|
1842
|
+
def test_tool_explain_methodology_and_limits():
|
|
1843
|
+
r = _tool("explain_methodology_and_limits")()
|
|
1844
|
+
for key in ("what_it_is", "what_it_is_not", "carbon", "cost", "fallback_data", "data_sources"):
|
|
1845
|
+
assert r.get(key), key
|
|
1846
|
+
assert "annual_average" in r["fallback_data"] and "Hydro-Québec" in r["data_sources"]
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|