greenpick 0.4.1__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.1/src/greenpick.egg-info → greenpick-0.4.2}/PKG-INFO +2 -2
- {greenpick-0.4.1 → greenpick-0.4.2}/pyproject.toml +2 -2
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/__init__.py +1 -1
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/server.py +26 -15
- {greenpick-0.4.1 → greenpick-0.4.2/src/greenpick.egg-info}/PKG-INFO +2 -2
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick.egg-info/requires.txt +1 -1
- {greenpick-0.4.1 → greenpick-0.4.2}/tests/test_services.py +52 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/LICENSE +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/README.md +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/setup.cfg +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/__main__.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/config.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/__init__.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/_net.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/annual.json +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/auto_repository.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/canada_repository.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/eia_repository.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/electricitymaps_repository.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/emission_factors.json +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/entsoe_repository.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/gpu_power.json +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/inference_energy.json +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/instance_families.json +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/mock_repository.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/network.json +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/power.json +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/pricing.json +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/regions.json +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/repository.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/static_data.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/typical.json +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/typical_repository.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/models.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/__init__.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/carbon_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/cost_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/data_hint.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/forecast_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/inference_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/instance_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/latency_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/locality_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/metric_scaffold_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/recommender.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/residency_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/rightsizing_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/scheduling_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/serving_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/training_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick.egg-info/SOURCES.txt +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick.egg-info/dependency_links.txt +0 -0
- {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick.egg-info/entry_points.txt +0 -0
- {greenpick-0.4.1 → 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
|
|
@@ -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,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 {
|
|
@@ -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
|
|
@@ -1792,3 +1792,55 @@ async def test_asian_users_are_now_served_from_asia():
|
|
|
1792
1792
|
adv = await serving_service.serving_pattern(TypicalCarbonRepository(), hubs, Workload(vcpu=1, ram_gb=2))
|
|
1793
1793
|
assert adv.single_region.region in ("ap-southeast-1", "asia-southeast1", "southeastasia")
|
|
1794
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|