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.
Files changed (52) hide show
  1. {greenpick-0.2.0/src/greenpick.egg-info → greenpick-0.2.1}/PKG-INFO +2 -2
  2. {greenpick-0.2.0 → greenpick-0.2.1}/README.md +1 -1
  3. {greenpick-0.2.0 → greenpick-0.2.1}/pyproject.toml +1 -1
  4. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/__init__.py +1 -1
  5. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/typical_repository.py +50 -0
  6. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/server.py +11 -2
  7. {greenpick-0.2.0 → greenpick-0.2.1/src/greenpick.egg-info}/PKG-INFO +2 -2
  8. {greenpick-0.2.0 → greenpick-0.2.1}/tests/test_services.py +51 -0
  9. {greenpick-0.2.0 → greenpick-0.2.1}/LICENSE +0 -0
  10. {greenpick-0.2.0 → greenpick-0.2.1}/setup.cfg +0 -0
  11. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/__main__.py +0 -0
  12. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/config.py +0 -0
  13. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/__init__.py +0 -0
  14. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/_net.py +0 -0
  15. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/auto_repository.py +0 -0
  16. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/eia_repository.py +0 -0
  17. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/electricitymaps_repository.py +0 -0
  18. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/emission_factors.json +0 -0
  19. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/entsoe_repository.py +0 -0
  20. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/gpu_power.json +0 -0
  21. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/inference_energy.json +0 -0
  22. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/instance_families.json +0 -0
  23. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/mock_repository.py +0 -0
  24. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/network.json +0 -0
  25. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/power.json +0 -0
  26. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/pricing.json +0 -0
  27. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/regions.json +0 -0
  28. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/repository.py +0 -0
  29. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/static_data.py +0 -0
  30. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/data/typical.json +0 -0
  31. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/models.py +0 -0
  32. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/__init__.py +0 -0
  33. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/carbon_service.py +0 -0
  34. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/cost_service.py +0 -0
  35. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/data_hint.py +0 -0
  36. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/forecast_service.py +0 -0
  37. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/inference_service.py +0 -0
  38. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/instance_service.py +0 -0
  39. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/latency_service.py +0 -0
  40. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/locality_service.py +0 -0
  41. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/metric_scaffold_service.py +0 -0
  42. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/recommender.py +0 -0
  43. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/residency_service.py +0 -0
  44. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/rightsizing_service.py +0 -0
  45. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/scheduling_service.py +0 -0
  46. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/serving_service.py +0 -0
  47. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick/services/training_service.py +0 -0
  48. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick.egg-info/SOURCES.txt +0 -0
  49. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick.egg-info/dependency_links.txt +0 -0
  50. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick.egg-info/entry_points.txt +0 -0
  51. {greenpick-0.2.0 → greenpick-0.2.1}/src/greenpick.egg-info/requires.txt +0 -0
  52. {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.0
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.0"
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.0"
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
- return with_hint(reading.model_dump())
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.0
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