greenpick 0.2.0__tar.gz → 0.2.1__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.2.0/src/greenpick.egg-info → greenpick-0.2.1}/PKG-INFO +2 -2
- {greenpick-0.2.0 → greenpick-0.2.1}/README.md +1 -1
- {greenpick-0.2.0 → greenpick-0.2.1}/pyproject.toml +1 -1
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/__init__.py +1 -1
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/typical_repository.py +50 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/server.py +11 -2
- {greenpick-0.2.0 → greenpick-0.2.1/src/greenpick.egg-info}/PKG-INFO +2 -2
- {greenpick-0.2.0 → greenpick-0.2.1}/tests/test_services.py +51 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/LICENSE +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/setup.cfg +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/__main__.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/config.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/__init__.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/_net.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/auto_repository.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/eia_repository.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/electricitymaps_repository.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/emission_factors.json +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/entsoe_repository.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/gpu_power.json +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/inference_energy.json +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/instance_families.json +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/mock_repository.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/network.json +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/power.json +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/pricing.json +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/regions.json +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/repository.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/static_data.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/typical.json +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/models.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/__init__.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/carbon_service.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/cost_service.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/data_hint.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/forecast_service.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/inference_service.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/instance_service.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/latency_service.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/locality_service.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/metric_scaffold_service.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/recommender.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/residency_service.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/rightsizing_service.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/scheduling_service.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/serving_service.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/training_service.py +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick.egg-info/SOURCES.txt +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick.egg-info/dependency_links.txt +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick.egg-info/entry_points.txt +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick.egg-info/requires.txt +0 -0
- {greenpick-0.2.0 → greenpick-0.2.1}/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.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -149,7 +149,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
149
149
|
|------|--------------|
|
|
150
150
|
| `compare_regions_tri_axis` | Rank candidate regions by carbon with cost + latency tradeoffs; recommend a pick, each axis rated R/Y/G. Pass `regions` to rank only the ones you'd actually consider. |
|
|
151
151
|
| `greener_alternative_to` | "You picked X — the greenest region within +N ms latency is Y (−Z% carbon, Δcost)." The core nudge. |
|
|
152
|
-
| `region_carbon_now` | Current gCO₂/kWh for a region, with source + timestamp + freshness. |
|
|
152
|
+
| `region_carbon_now` | Current gCO₂/kWh for a region, with source + timestamp + freshness — and, when live, whether that's cleaner, dirtier or about usual for that hour this month. |
|
|
153
153
|
|
|
154
154
|
### Time a job (scheduling)
|
|
155
155
|
|
|
@@ -119,7 +119,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
119
119
|
|------|--------------|
|
|
120
120
|
| `compare_regions_tri_axis` | Rank candidate regions by carbon with cost + latency tradeoffs; recommend a pick, each axis rated R/Y/G. Pass `regions` to rank only the ones you'd actually consider. |
|
|
121
121
|
| `greener_alternative_to` | "You picked X — the greenest region within +N ms latency is Y (−Z% carbon, Δcost)." The core nudge. |
|
|
122
|
-
| `region_carbon_now` | Current gCO₂/kWh for a region, with source + timestamp + freshness. |
|
|
122
|
+
| `region_carbon_now` | Current gCO₂/kWh for a region, with source + timestamp + freshness — and, when live, whether that's cleaner, dirtier or about usual for that hour this month. |
|
|
123
123
|
|
|
124
124
|
### Time a job (scheduling)
|
|
125
125
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "greenpick"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.1"
|
|
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.2.
|
|
7
|
+
__version__ = "0.2.1"
|
|
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.
|
|
@@ -27,6 +27,56 @@ def _month(entry: dict) -> Optional[dict]:
|
|
|
27
27
|
return entry.get("months", {}).get(str(datetime.now(timezone.utc).month))
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
# Within this % of the typical value, a reading is "about usual" rather than cleaner/dirtier.
|
|
31
|
+
USUAL_BAND_PCT = 10.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _when(timestamp: str) -> datetime:
|
|
35
|
+
"""The UTC moment a reading refers to (EIA '2026-10-23T06', ISO with Z/offset); now if unparseable."""
|
|
36
|
+
t = (timestamp or "").strip().replace("Z", "+00:00")
|
|
37
|
+
for candidate in (t, t[:13]):
|
|
38
|
+
try:
|
|
39
|
+
dt = datetime.fromisoformat(candidate) if len(candidate) > 13 else \
|
|
40
|
+
datetime.strptime(candidate, "%Y-%m-%dT%H")
|
|
41
|
+
return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
|
|
42
|
+
except ValueError:
|
|
43
|
+
continue
|
|
44
|
+
return datetime.now(timezone.utc)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def typical_context(provider: str, region: str, g_co2_per_kwh: float, timestamp: str) -> Optional[dict]:
|
|
48
|
+
"""Compare a live reading with what's typical for that region, month and hour of day.
|
|
49
|
+
None if the region has no typical data."""
|
|
50
|
+
entry = typical_entry(provider, region)
|
|
51
|
+
if not entry:
|
|
52
|
+
return None
|
|
53
|
+
when = _when(timestamp).astimezone(timezone.utc)
|
|
54
|
+
month = entry.get("months", {}).get(str(when.month))
|
|
55
|
+
if not month:
|
|
56
|
+
return None
|
|
57
|
+
hourly = month["hourly_utc"][when.hour]
|
|
58
|
+
typical_now = hourly if hourly is not None else month["avg_g_co2_per_kwh"]
|
|
59
|
+
pct = round((g_co2_per_kwh - typical_now) / typical_now * 100, 1) if typical_now else None
|
|
60
|
+
if pct is None or abs(pct) < USUAL_BAND_PCT:
|
|
61
|
+
verdict = "about_usual"
|
|
62
|
+
words = "about usual"
|
|
63
|
+
elif pct < 0:
|
|
64
|
+
verdict, words = "cleaner_than_usual", f"~{abs(pct):.0f}% cleaner than usual"
|
|
65
|
+
else:
|
|
66
|
+
verdict, words = "dirtier_than_usual", f"~{pct:.0f}% dirtier than usual"
|
|
67
|
+
month_name = when.strftime("%B")
|
|
68
|
+
return {
|
|
69
|
+
"typical_this_hour_g_co2_per_kwh": typical_now,
|
|
70
|
+
"typical_month_avg_g_co2_per_kwh": month["avg_g_co2_per_kwh"],
|
|
71
|
+
"vs_typical_pct": pct,
|
|
72
|
+
"vs_typical": verdict,
|
|
73
|
+
"typical_note": (f"Live {g_co2_per_kwh:.0f} gCO2/kWh is {words} for {when.hour:02d}:00 UTC in "
|
|
74
|
+
f"{month_name} (typical ~{typical_now:.0f}; {month_name} average "
|
|
75
|
+
f"~{month['avg_g_co2_per_kwh']:.0f}, from a year of {entry['source'].upper()} "
|
|
76
|
+
f"data). Directional context, not a forecast."),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
30
80
|
class TypicalCarbonRepository:
|
|
31
81
|
def __init__(self) -> None:
|
|
32
82
|
self._mock = MockCarbonRepository()
|
|
@@ -18,6 +18,7 @@ from .services import (
|
|
|
18
18
|
)
|
|
19
19
|
from .models import ServingHub
|
|
20
20
|
from .services.data_hint import with_hint
|
|
21
|
+
from .data.typical_repository import typical_context
|
|
21
22
|
|
|
22
23
|
# Sent to every MCP client at connect. Agents learn a server from its tool list, not its README, and
|
|
23
24
|
# the tool list starts with region-picking — so without this they lead with the rarest decision and
|
|
@@ -135,9 +136,17 @@ async def estimate_workload_carbon(
|
|
|
135
136
|
|
|
136
137
|
@mcp.tool()
|
|
137
138
|
async def region_carbon_now(provider: str, region: str) -> dict:
|
|
138
|
-
"""Current grid carbon intensity for a region, with source, timestamp, and data freshness.
|
|
139
|
+
"""Current grid carbon intensity for a region, with source, timestamp, and data freshness.
|
|
140
|
+
When the reading is live, also says how it compares with what's typical for that hour this month
|
|
141
|
+
(vs_typical: cleaner_than_usual / about_usual / dirtier_than_usual) — useful for "is now a good
|
|
142
|
+
moment to run this?"."""
|
|
139
143
|
reading = await carbon_service.carbon_now(_repo, provider, region) # type: ignore[arg-type]
|
|
140
|
-
|
|
144
|
+
out = reading.model_dump()
|
|
145
|
+
if reading.quality in ("live", "live_estimated", "cached"):
|
|
146
|
+
ctx = typical_context(provider, region, reading.g_co2_per_kwh, reading.timestamp)
|
|
147
|
+
if ctx:
|
|
148
|
+
out.update(ctx)
|
|
149
|
+
return with_hint(out)
|
|
141
150
|
|
|
142
151
|
|
|
143
152
|
@mcp.tool()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: greenpick
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -149,7 +149,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
149
149
|
|------|--------------|
|
|
150
150
|
| `compare_regions_tri_axis` | Rank candidate regions by carbon with cost + latency tradeoffs; recommend a pick, each axis rated R/Y/G. Pass `regions` to rank only the ones you'd actually consider. |
|
|
151
151
|
| `greener_alternative_to` | "You picked X — the greenest region within +N ms latency is Y (−Z% carbon, Δcost)." The core nudge. |
|
|
152
|
-
| `region_carbon_now` | Current gCO₂/kWh for a region, with source + timestamp + freshness. |
|
|
152
|
+
| `region_carbon_now` | Current gCO₂/kWh for a region, with source + timestamp + freshness — and, when live, whether that's cleaner, dirtier or about usual for that hour this month. |
|
|
153
153
|
|
|
154
154
|
### Time a job (scheduling)
|
|
155
155
|
|
|
@@ -1544,3 +1544,54 @@ def test_default_repository_is_typical(monkeypatch):
|
|
|
1544
1544
|
from greenpick import data as data_pkg
|
|
1545
1545
|
monkeypatch.setattr(data_pkg, "SETTINGS", _settings(source="mock"))
|
|
1546
1546
|
assert isinstance(data_pkg.get_repository(), TypicalCarbonRepository)
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
# --- 0.2.1: a live reading says how it compares with what's typical for that hour ---
|
|
1550
|
+
|
|
1551
|
+
from greenpick.data.typical_repository import typical_context
|
|
1552
|
+
|
|
1553
|
+
|
|
1554
|
+
def _typical_at(prov, reg, month, hour):
|
|
1555
|
+
return typical_entry(prov, reg)["months"][str(month)]["hourly_utc"][hour]
|
|
1556
|
+
|
|
1557
|
+
|
|
1558
|
+
def test_typical_context_compares_with_same_hour_this_month():
|
|
1559
|
+
base = _typical_at("aws", "us-east-1", 10, 6)
|
|
1560
|
+
clean = typical_context("aws", "us-east-1", base * 0.7, "2026-10-23T06")
|
|
1561
|
+
assert clean["typical_this_hour_g_co2_per_kwh"] == base
|
|
1562
|
+
assert clean["vs_typical"] == "cleaner_than_usual" and clean["vs_typical_pct"] == -30.0
|
|
1563
|
+
assert "06:00 UTC in October" in clean["typical_note"]
|
|
1564
|
+
dirty = typical_context("aws", "us-east-1", base * 1.25, "2026-10-23T06")
|
|
1565
|
+
assert dirty["vs_typical"] == "dirtier_than_usual"
|
|
1566
|
+
usual = typical_context("aws", "us-east-1", base * 1.05, "2026-10-23T06")
|
|
1567
|
+
assert usual["vs_typical"] == "about_usual"
|
|
1568
|
+
|
|
1569
|
+
|
|
1570
|
+
def test_typical_context_reads_entsoe_timestamps():
|
|
1571
|
+
base = _typical_at("aws", "eu-north-1", 10, 18)
|
|
1572
|
+
ctx = typical_context("aws", "eu-north-1", base, "2026-10-23T18:00Z")
|
|
1573
|
+
assert ctx["typical_this_hour_g_co2_per_kwh"] == base and ctx["vs_typical"] == "about_usual"
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
def test_no_typical_context_for_uncovered_region():
|
|
1577
|
+
assert typical_context("aws", "ap-southeast-2", 500.0, "2026-10-23T06") is None
|
|
1578
|
+
|
|
1579
|
+
|
|
1580
|
+
@pytest.mark.asyncio
|
|
1581
|
+
async def test_region_carbon_now_adds_context_only_to_live_readings(monkeypatch):
|
|
1582
|
+
from greenpick import server
|
|
1583
|
+
from greenpick.models import CarbonReading
|
|
1584
|
+
|
|
1585
|
+
class LiveRepo(MockCarbonRepository):
|
|
1586
|
+
async def carbon_now(self, provider, region):
|
|
1587
|
+
return CarbonReading(region=region, provider=provider, g_co2_per_kwh=200.0,
|
|
1588
|
+
source="eia", timestamp="2026-10-23T06", quality="live")
|
|
1589
|
+
|
|
1590
|
+
f = getattr(server.region_carbon_now, "fn", server.region_carbon_now)
|
|
1591
|
+
monkeypatch.setattr(server, "_repo", LiveRepo())
|
|
1592
|
+
live = await f(provider="aws", region="us-east-1")
|
|
1593
|
+
assert live["vs_typical"] == "cleaner_than_usual" and "live_data_hint" not in live
|
|
1594
|
+
|
|
1595
|
+
monkeypatch.setattr(server, "_repo", TypicalCarbonRepository())
|
|
1596
|
+
fallback = await f(provider="aws", region="us-east-1")
|
|
1597
|
+
assert fallback["quality"] == "typical" and "vs_typical" not in fallback # no self-comparison
|
|
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
|