greenpick 0.1.6__tar.gz → 0.1.7__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.1.6/src/greenpick.egg-info → greenpick-0.1.7}/PKG-INFO +4 -2
- {greenpick-0.1.6 → greenpick-0.1.7}/README.md +3 -1
- {greenpick-0.1.6 → greenpick-0.1.7}/pyproject.toml +1 -1
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/__init__.py +1 -1
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/models.py +5 -2
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/server.py +1 -1
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/services/rightsizing_service.py +11 -4
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/services/scheduling_service.py +26 -8
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/services/training_service.py +7 -5
- {greenpick-0.1.6 → greenpick-0.1.7/src/greenpick.egg-info}/PKG-INFO +4 -2
- {greenpick-0.1.6 → greenpick-0.1.7}/tests/test_services.py +64 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/LICENSE +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/setup.cfg +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/__main__.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/config.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/data/__init__.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/data/_net.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/data/auto_repository.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/data/eia_repository.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/data/electricitymaps_repository.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/data/emission_factors.json +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/data/entsoe_repository.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/data/gpu_power.json +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/data/inference_energy.json +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/data/instance_families.json +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/data/mock_repository.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/data/network.json +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/data/power.json +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/data/pricing.json +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/data/regions.json +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/data/repository.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/data/static_data.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/services/__init__.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/services/carbon_service.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/services/cost_service.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/services/forecast_service.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/services/inference_service.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/services/instance_service.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/services/latency_service.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/services/locality_service.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/services/metric_scaffold_service.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/services/recommender.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/services/residency_service.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick/services/serving_service.py +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick.egg-info/SOURCES.txt +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick.egg-info/dependency_links.txt +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick.egg-info/entry_points.txt +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/src/greenpick.egg-info/requires.txt +0 -0
- {greenpick-0.1.6 → greenpick-0.1.7}/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.1.
|
|
3
|
+
Version: 0.1.7
|
|
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
|
|
@@ -260,6 +260,7 @@ environment variables to it:
|
|
|
260
260
|
|
|
261
261
|
```python
|
|
262
262
|
import sys
|
|
263
|
+
# "mcp" is the MCP Python SDK (installed alongside greenpick), not a greenpick module.
|
|
263
264
|
from mcp import ClientSession, StdioServerParameters
|
|
264
265
|
from mcp.client.stdio import stdio_client
|
|
265
266
|
|
|
@@ -273,7 +274,8 @@ params = StdioServerParameters(
|
|
|
273
274
|
},
|
|
274
275
|
)
|
|
275
276
|
|
|
276
|
-
#
|
|
277
|
+
# Keep errlog=: in Jupyter it must be a real file, because the notebook's stderr has no fileno()
|
|
278
|
+
# (without it you get "UnsupportedOperation: fileno").
|
|
277
279
|
with open("greenpick-server.log", "w") as errlog:
|
|
278
280
|
async with stdio_client(params, errlog=errlog) as (read, write):
|
|
279
281
|
async with ClientSession(read, write) as session:
|
|
@@ -230,6 +230,7 @@ environment variables to it:
|
|
|
230
230
|
|
|
231
231
|
```python
|
|
232
232
|
import sys
|
|
233
|
+
# "mcp" is the MCP Python SDK (installed alongside greenpick), not a greenpick module.
|
|
233
234
|
from mcp import ClientSession, StdioServerParameters
|
|
234
235
|
from mcp.client.stdio import stdio_client
|
|
235
236
|
|
|
@@ -243,7 +244,8 @@ params = StdioServerParameters(
|
|
|
243
244
|
},
|
|
244
245
|
)
|
|
245
246
|
|
|
246
|
-
#
|
|
247
|
+
# Keep errlog=: in Jupyter it must be a real file, because the notebook's stderr has no fileno()
|
|
248
|
+
# (without it you get "UnsupportedOperation: fileno").
|
|
247
249
|
with open("greenpick-server.log", "w") as errlog:
|
|
248
250
|
async with stdio_client(params, errlog=errlog) as (read, write):
|
|
249
251
|
async with ClientSession(read, write) as session:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "greenpick"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.7"
|
|
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.1.
|
|
7
|
+
__version__ = "0.1.7"
|
|
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.
|
|
@@ -70,7 +70,8 @@ class ForecastAdvice(BaseModel):
|
|
|
70
70
|
class CleanestWindow(BaseModel):
|
|
71
71
|
"""The lowest-average contiguous run of N hours — for timing a job of known duration."""
|
|
72
72
|
window_hours: int
|
|
73
|
-
|
|
73
|
+
start_hour_of_day: Optional[int] = None # 0..23 — the advice: start around this hour each time
|
|
74
|
+
start_period: str # when that window began in the recent data (evidence, not a date to use)
|
|
74
75
|
end_period: str # the last hour in the window
|
|
75
76
|
avg_g_co2_per_kwh: float # mean intensity across the window
|
|
76
77
|
hours: list[HourlyCarbon] = [] # the per-hour breakdown of the chosen window
|
|
@@ -224,7 +225,9 @@ class RightsizingAdvice(BaseModel):
|
|
|
224
225
|
carbon_now_kg_per_month: Optional[float] = None
|
|
225
226
|
carbon_rightsized_kg_per_month: Optional[float] = None
|
|
226
227
|
monthly_kg_co2_saving: Optional[float] = None
|
|
227
|
-
|
|
228
|
+
cost_saving_pct: Optional[float] = None # % off the monthly bill
|
|
229
|
+
carbon_saving_pct: Optional[float] = None # % off monthly kg CO2
|
|
230
|
+
saving_pct: Optional[float] = None # = carbon_saving_pct; kept for compatibility
|
|
228
231
|
carbon_source: Optional[str] = None
|
|
229
232
|
carbon_quality: Optional[str] = None
|
|
230
233
|
rationale: str = ""
|
|
@@ -35,7 +35,7 @@ serving_pattern_advice; carbon_metric_scaffold for an app to track its own estim
|
|
|
35
35
|
Before recommending a region move, run data_locality_egress_check: egress can erase the win. Mention \
|
|
36
36
|
any residency_note.
|
|
37
37
|
Every carbon figure carries source and quality fields. Report 'mock', 'cached' or 'historical' values \
|
|
38
|
-
as such, never as live readings. If a call fails or output is missing, say so rather than describing \
|
|
38
|
+
as such, never as live readings — including figures derived from them, such as a kg CO2 saving. If a call fails or output is missing, say so rather than describing \
|
|
39
39
|
what the tool would return. GreenPick estimates; it does not measure running systems or report model \
|
|
40
40
|
accuracy. Use explain_methodology_and_limits when asked how a number was produced.\
|
|
41
41
|
"""
|
|
@@ -83,13 +83,19 @@ async def rightsizing_check(
|
|
|
83
83
|
|
|
84
84
|
is_oversized = saving_pct is not None and saving_pct >= _MIN_SAVING_PCT
|
|
85
85
|
|
|
86
|
+
# Separate, labelled percentages: cost and carbon shrink by different fractions (price and power
|
|
87
|
+
# scale differently with vCPU vs RAM), so one unlabelled % beside a dollar figure misleads.
|
|
88
|
+
cost_pct = (round(usd_saving / cost_now * 100, 1)
|
|
89
|
+
if usd_saving is not None and cost_now else None)
|
|
90
|
+
|
|
86
91
|
if is_oversized:
|
|
87
|
-
|
|
92
|
+
cost_txt = (f"~{cost_pct:.0f}% cheaper (~${usd_saving:,.0f}/mo saved)"
|
|
93
|
+
if cost_pct is not None else "a lower bill")
|
|
88
94
|
rationale = (
|
|
89
95
|
f"Over-provisioned: at ~{cpu_utilization*100:.0f}% CPU use, {vcpu:g} vCPU / {ram_gb:g} GB "
|
|
90
96
|
f"is bigger than you need. Right-size to {rec_vcpu:g} vCPU / {rec_ram:g} GB (the smallest "
|
|
91
|
-
f"standard size that keeps ~40% headroom)
|
|
92
|
-
f"~{kg_saving:.1f} kg CO2/mo. "
|
|
97
|
+
f"standard size that keeps ~40% headroom): {cost_txt} AND ~{saving_pct:.0f}% less carbon "
|
|
98
|
+
f"(~{kg_saving:.1f} kg CO2/mo, {reading.quality} grid data). "
|
|
93
99
|
f"Same work, less waste: the cost and carbon win here are the same action.")
|
|
94
100
|
else:
|
|
95
101
|
rationale = (
|
|
@@ -105,7 +111,8 @@ async def rightsizing_check(
|
|
|
105
111
|
cost_now_usd_per_month=cost_now, cost_rightsized_usd_per_month=cost_rs,
|
|
106
112
|
monthly_usd_saving=usd_saving,
|
|
107
113
|
carbon_now_kg_per_month=carbon_now, carbon_rightsized_kg_per_month=carbon_rs,
|
|
108
|
-
monthly_kg_co2_saving=kg_saving,
|
|
114
|
+
monthly_kg_co2_saving=kg_saving,
|
|
115
|
+
cost_saving_pct=cost_pct, carbon_saving_pct=saving_pct, saving_pct=saving_pct,
|
|
109
116
|
carbon_source=reading.source, carbon_quality=reading.quality,
|
|
110
117
|
rationale=rationale,
|
|
111
118
|
limitations=("Directional, design-time estimate from the utilisation YOU report — not a "
|
|
@@ -37,6 +37,22 @@ def _hour_index(period: str) -> Optional[int]:
|
|
|
37
37
|
return int(dt.timestamp() // 3600) # absolute hours since epoch
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
def hour_of_day(period: str) -> Optional[int]:
|
|
41
|
+
"""0..23 hour-of-day for a period label (UTC for live ISO timestamps; mock 'H15' -> 15)."""
|
|
42
|
+
idx = _hour_index(period)
|
|
43
|
+
return None if idx is None else idx % 24
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def clock_label(period: str) -> str:
|
|
47
|
+
"""Turn a past period into advice you can act on: '15:00 UTC' (live) or '15:00' (mock hours
|
|
48
|
+
carry no timezone). Recent history says WHEN IN THE DAY is cleanest — the date it happened on
|
|
49
|
+
is evidence, not a start date, so it's never the headline."""
|
|
50
|
+
hod = hour_of_day(period)
|
|
51
|
+
if hod is None:
|
|
52
|
+
return period
|
|
53
|
+
return f"{hod:02d}:00" if period.strip().startswith("H") else f"{hod:02d}:00 UTC"
|
|
54
|
+
|
|
55
|
+
|
|
40
56
|
def _best_and_worst_window(series: list[HourlyCarbon], window_hours: int
|
|
41
57
|
) -> tuple[Optional[list[HourlyCarbon]], Optional[float]]:
|
|
42
58
|
"""Slide a window of `window_hours` over the CHRONOLOGICALLY ordered series, considering only
|
|
@@ -114,20 +130,22 @@ async def cleanest_hours(
|
|
|
114
130
|
win_hours, win_reduction = _best_and_worst_window(history, window_hours)
|
|
115
131
|
if win_hours:
|
|
116
132
|
avg = round(sum(h.g_co2_per_kwh for h in win_hours) / len(win_hours), 1)
|
|
133
|
+
start = win_hours[0].period
|
|
117
134
|
advice.window = CleanestWindow(
|
|
118
|
-
window_hours=window_hours,
|
|
119
|
-
start_period=
|
|
135
|
+
window_hours=window_hours, start_hour_of_day=hour_of_day(start),
|
|
136
|
+
start_period=start, end_period=win_hours[-1].period,
|
|
120
137
|
avg_g_co2_per_kwh=avg, hours=win_hours,
|
|
121
138
|
)
|
|
122
139
|
advice.window_reduction_pct = win_reduction
|
|
140
|
+
seen = f"(the cleanest {window_hours}h window in the recent data began {start})"
|
|
123
141
|
if win_reduction is not None and win_reduction >= 10:
|
|
124
|
-
advice.note = (f"For a ~{window_hours}h job, start
|
|
125
|
-
f"
|
|
126
|
-
f"
|
|
142
|
+
advice.note = (f"For a ~{window_hours}h job, start around {clock_label(start)} "
|
|
143
|
+
f"{seen}: avg ~{avg:.0f} gCO2/kWh, ~{win_reduction:.0f}% cleaner than "
|
|
144
|
+
f"the worst {window_hours}h window. A recent pattern, not a forecast.")
|
|
127
145
|
else:
|
|
128
|
-
advice.note = (f"For a ~{window_hours}h job, the cleanest
|
|
129
|
-
f"{
|
|
130
|
-
f"fairly flat — timing won't help much here.")
|
|
146
|
+
advice.note = (f"For a ~{window_hours}h job, the cleanest start is around "
|
|
147
|
+
f"{clock_label(start)} {seen}, avg ~{avg:.0f} gCO2/kWh, but the grid "
|
|
148
|
+
f"is fairly flat — timing won't help much here.")
|
|
131
149
|
else:
|
|
132
150
|
advice.note = (f"Not enough contiguous history to find a {window_hours}h window; "
|
|
133
151
|
f"showing cleanest individual hours instead. " + advice.note)
|
|
@@ -170,12 +170,14 @@ async def estimate_training_carbon(
|
|
|
170
170
|
|
|
171
171
|
if advice.window and advice.window_reduction_pct and advice.window_reduction_pct >= 10:
|
|
172
172
|
w = advice.window
|
|
173
|
-
time_txt = (f" It runs ~{wall_clock_h}h wall-clock;
|
|
174
|
-
f"
|
|
175
|
-
f"
|
|
176
|
-
f"
|
|
173
|
+
time_txt = (f" It runs ~{wall_clock_h}h wall-clock; start it around "
|
|
174
|
+
f"{scheduling_service.clock_label(w.start_period)} (the cleanest {w.window_hours}h "
|
|
175
|
+
f"window in the recent data began {w.start_period}, avg ~{w.avg_g_co2_per_kwh:.0f} "
|
|
176
|
+
f"gCO2/kWh), ~{advice.window_reduction_pct:.0f}% cleaner than the worst same-length "
|
|
177
|
+
f"window - training is deferrable, so this stacks on the region choice. "
|
|
178
|
+
f"A recent pattern, not a forecast.")
|
|
177
179
|
elif advice.potential_reduction_pct and advice.potential_reduction_pct >= 10 and advice.cleanest:
|
|
178
|
-
best = advice.cleanest[0].period
|
|
180
|
+
best = scheduling_service.clock_label(advice.cleanest[0].period)
|
|
179
181
|
time_txt = (f" Timing it to the cleanest hours in that region (around {best}) cuts a further "
|
|
180
182
|
f"~{advice.potential_reduction_pct:.0f}% vs the dirtiest hour - training is "
|
|
181
183
|
f"deferrable, so this stacks on the region choice.")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: greenpick
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
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
|
|
@@ -260,6 +260,7 @@ environment variables to it:
|
|
|
260
260
|
|
|
261
261
|
```python
|
|
262
262
|
import sys
|
|
263
|
+
# "mcp" is the MCP Python SDK (installed alongside greenpick), not a greenpick module.
|
|
263
264
|
from mcp import ClientSession, StdioServerParameters
|
|
264
265
|
from mcp.client.stdio import stdio_client
|
|
265
266
|
|
|
@@ -273,7 +274,8 @@ params = StdioServerParameters(
|
|
|
273
274
|
},
|
|
274
275
|
)
|
|
275
276
|
|
|
276
|
-
#
|
|
277
|
+
# Keep errlog=: in Jupyter it must be a real file, because the notebook's stderr has no fileno()
|
|
278
|
+
# (without it you get "UnsupportedOperation: fileno").
|
|
277
279
|
with open("greenpick-server.log", "w") as errlog:
|
|
278
280
|
async with stdio_client(params, errlog=errlog) as (read, write):
|
|
279
281
|
async with ClientSession(read, write) as session:
|
|
@@ -1251,3 +1251,67 @@ def test_server_sends_instructions_that_lead_with_rightsizing():
|
|
|
1251
1251
|
named = set(re.findall(r"\b[a-z]+(?:_[a-z]+)+\b", text)) - {"wall_clock_hours", "gpu_hours", "residency_note"}
|
|
1252
1252
|
registered = {t.name for t in asyncio.run(server.mcp.list_tools())}
|
|
1253
1253
|
assert named <= registered, named - registered
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
# --- 0.1.7: labelled percentages; start advice as a time of day, not a past date ---
|
|
1257
|
+
|
|
1258
|
+
@pytest.mark.asyncio
|
|
1259
|
+
async def test_rightsizing_labels_cost_and_carbon_percentages_separately():
|
|
1260
|
+
from greenpick.services import rightsizing_service
|
|
1261
|
+
repo = MockCarbonRepository()
|
|
1262
|
+
adv = await rightsizing_service.rightsizing_check(
|
|
1263
|
+
repo, "aws", "us-east-1", vcpu=8, ram_gb=32, cpu_utilization=0.15,
|
|
1264
|
+
)
|
|
1265
|
+
# Cost % is the bill saving; carbon % is the kg saving. They differ, so both are reported.
|
|
1266
|
+
assert adv.cost_saving_pct == round(adv.monthly_usd_saving / adv.cost_now_usd_per_month * 100, 1)
|
|
1267
|
+
assert adv.carbon_saving_pct == round(adv.monthly_kg_co2_saving / adv.carbon_now_kg_per_month * 100, 1)
|
|
1268
|
+
assert adv.cost_saving_pct != adv.carbon_saving_pct
|
|
1269
|
+
assert adv.saving_pct == adv.carbon_saving_pct # compatibility field unchanged
|
|
1270
|
+
assert f"~{adv.cost_saving_pct:.0f}% cheaper" in adv.rationale
|
|
1271
|
+
assert f"~{adv.carbon_saving_pct:.0f}% less carbon" in adv.rationale
|
|
1272
|
+
assert "mock grid data" in adv.rationale # derived carbon carries its quality
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def test_clock_label_turns_past_periods_into_a_time_of_day():
|
|
1276
|
+
from greenpick.services import scheduling_service as ss
|
|
1277
|
+
assert ss.clock_label("2026-10-17T15") == "15:00 UTC"
|
|
1278
|
+
assert ss.clock_label("2026-10-17T15:00Z") == "15:00 UTC"
|
|
1279
|
+
assert ss.clock_label("H02") == "02:00" # mock hours have no timezone
|
|
1280
|
+
assert ss.hour_of_day("2026-10-17T15") == 15
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
@pytest.mark.asyncio
|
|
1284
|
+
async def test_window_advice_leads_with_time_of_day_not_a_date(monkeypatch):
|
|
1285
|
+
from greenpick.services import scheduling_service as ss
|
|
1286
|
+
from greenpick.models import HourlyCarbon
|
|
1287
|
+
|
|
1288
|
+
class Repo(MockCarbonRepository):
|
|
1289
|
+
async def carbon_history(self, provider, region):
|
|
1290
|
+
# Two past days; 15:00-19:00 is clean on the 17th.
|
|
1291
|
+
out = []
|
|
1292
|
+
for day in ("2026-10-17", "2026-10-18"):
|
|
1293
|
+
for h in range(24):
|
|
1294
|
+
g = 60.0 if (day == "2026-10-17" and 15 <= h <= 19) else 200.0
|
|
1295
|
+
out.append(HourlyCarbon(period=f"{day}T{h:02d}", g_co2_per_kwh=g))
|
|
1296
|
+
return out
|
|
1297
|
+
|
|
1298
|
+
adv = await ss.cleanest_hours(Repo(), "aws", "us-west-2", window_hours=5)
|
|
1299
|
+
assert adv.window.start_hour_of_day == 15
|
|
1300
|
+
assert adv.window.start_period == "2026-10-17T15" # kept, as evidence
|
|
1301
|
+
assert adv.note.startswith("For a ~5h job, start around 15:00 UTC")
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
@pytest.mark.asyncio
|
|
1305
|
+
async def test_training_rationale_says_start_around_a_time():
|
|
1306
|
+
from greenpick.services import training_service
|
|
1307
|
+
repo = MockCarbonRepository()
|
|
1308
|
+
adv = await training_service.estimate_training_carbon(
|
|
1309
|
+
repo, gpu="a100", gpu_count=8, gpu_hours=40, region="us-east-1", provider="aws",
|
|
1310
|
+
)
|
|
1311
|
+
assert adv.cleanest_window is not None and adv.cleanest_window.start_hour_of_day is not None
|
|
1312
|
+
assert f"start it around {adv.cleanest_window.start_hour_of_day:02d}:00" in adv.rationale
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
def test_instructions_cover_derived_mock_figures():
|
|
1316
|
+
from greenpick import server
|
|
1317
|
+
assert "derived from them" in server.mcp.instructions
|
|
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
|