greenpick 0.6.0__tar.gz → 0.6.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.6.0/src/greenpick.egg-info → greenpick-0.6.2}/PKG-INFO +5 -1
- {greenpick-0.6.0 → greenpick-0.6.2}/README.md +4 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/pyproject.toml +1 -1
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/__init__.py +1 -1
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/server.py +30 -5
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/migration_service.py +12 -1
- {greenpick-0.6.0 → greenpick-0.6.2/src/greenpick.egg-info}/PKG-INFO +5 -1
- {greenpick-0.6.0 → greenpick-0.6.2}/tests/test_services.py +65 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/LICENSE +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/setup.cfg +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/__main__.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/config.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/__init__.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/_net.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/annual.json +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/auto_repository.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/canada_repository.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/eia_repository.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/electricitymaps_repository.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/emission_factors.json +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/entsoe_repository.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/gpu_power.json +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/inference_energy.json +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/instance_families.json +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/mock_repository.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/network.json +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/power.json +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/pricing.json +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/regions.json +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/repository.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/static_data.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/typical.json +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/data/typical_repository.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/models.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/__init__.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/carbon_service.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/cost_service.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/data_hint.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/forecast_service.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/inference_service.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/instance_service.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/justification_service.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/latency_service.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/locality_service.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/metric_scaffold_service.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/offhours_service.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/recommender.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/residency_service.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/rightsizing_service.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/scheduling_service.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/serving_service.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/training_service.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick/services/typical_ranking_service.py +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick.egg-info/SOURCES.txt +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick.egg-info/dependency_links.txt +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick.egg-info/entry_points.txt +0 -0
- {greenpick-0.6.0 → greenpick-0.6.2}/src/greenpick.egg-info/requires.txt +0 -0
- {greenpick-0.6.0 → greenpick-0.6.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.6.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -308,6 +308,10 @@ with open("greenpick-server.log", "w") as errlog:
|
|
|
308
308
|
Top-level `await` works in Jupyter. In a plain script, put the `with` block inside an `async def`
|
|
309
309
|
and run it with `asyncio.run(...)`.
|
|
310
310
|
|
|
311
|
+
Using an AI assistant inside a notebook? Paste the
|
|
312
|
+
[starter prompt](https://github.com/bithug01/greenpick/blob/master/docs/using-from-a-notebook.md)
|
|
313
|
+
as your first message: it sets up the connection and points the assistant at the right tool for each question.
|
|
314
|
+
|
|
311
315
|
Every carbon figure includes a `quality` field: `live` means a current reading, `typical` means
|
|
312
316
|
this month's average from last year's real data, `annual_average` means a sourced yearly national
|
|
313
317
|
figure (regions with no free hourly data), and `mock` means a hand-set value. Check it to confirm
|
|
@@ -278,6 +278,10 @@ with open("greenpick-server.log", "w") as errlog:
|
|
|
278
278
|
Top-level `await` works in Jupyter. In a plain script, put the `with` block inside an `async def`
|
|
279
279
|
and run it with `asyncio.run(...)`.
|
|
280
280
|
|
|
281
|
+
Using an AI assistant inside a notebook? Paste the
|
|
282
|
+
[starter prompt](https://github.com/bithug01/greenpick/blob/master/docs/using-from-a-notebook.md)
|
|
283
|
+
as your first message: it sets up the connection and points the assistant at the right tool for each question.
|
|
284
|
+
|
|
281
285
|
Every carbon figure includes a `quality` field: `live` means a current reading, `typical` means
|
|
282
286
|
this month's average from last year's real data, `annual_average` means a sourced yearly national
|
|
283
287
|
figure (regions with no free hourly data), and `mock` means a hand-set value. Check it to confirm
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "greenpick"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.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"
|
|
@@ -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.6.
|
|
7
|
+
__version__ = "0.6.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.
|
|
@@ -60,6 +60,18 @@ accuracy. Use explain_methodology_and_limits when asked how a number was produce
|
|
|
60
60
|
"""
|
|
61
61
|
|
|
62
62
|
mcp = FastMCP("greenpick", instructions=INSTRUCTIONS)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _fraction_error(name: str, value: Optional[float], allow_zero: bool = False) -> Optional[str]:
|
|
66
|
+
"""Utilisation inputs are fractions (0.15 = 15%). A value like 15 is almost always a percentage;
|
|
67
|
+
say so rather than silently reading it as 1,500%."""
|
|
68
|
+
if value is None:
|
|
69
|
+
return None
|
|
70
|
+
low_ok = value >= 0 if allow_zero else value > 0
|
|
71
|
+
if low_ok and value <= 1:
|
|
72
|
+
return None
|
|
73
|
+
hint = f" Did you mean {value / 100:g}?" if 1 < value <= 100 else ""
|
|
74
|
+
return (f"{name} must be a fraction between 0 and 1 (0.15 means 15%); got {value:g}.{hint}")
|
|
63
75
|
_repo = get_repository()
|
|
64
76
|
|
|
65
77
|
|
|
@@ -81,7 +93,8 @@ async def compare_regions_tri_axis(
|
|
|
81
93
|
(e.g. ["us-east-1","us-west-2","eu-west-1"]) to rank ONLY those you'd actually consider,
|
|
82
94
|
instead of the whole catalog — your baseline is always kept in the comparison. If the pick is
|
|
83
95
|
in a different region from where your data lives, run data_locality_egress_check before moving:
|
|
84
|
-
cross-region/cross-cloud egress can erase the carbon/cost win.
|
|
96
|
+
cross-region/cross-cloud egress can erase the carbon/cost win. To write a chosen move up for a pull
|
|
97
|
+
request or decision record, use region_move_justification (do not draft the text from this result)."""
|
|
85
98
|
rec = await recommender.compare_regions(
|
|
86
99
|
_repo,
|
|
87
100
|
providers=[p for p in (providers or [])] or None, # type: ignore[list-item]
|
|
@@ -111,7 +124,8 @@ async def greener_alternative_to(
|
|
|
111
124
|
with the carbon/cost/latency delta vs. your current choice. The core 'nudge' tool. Pass
|
|
112
125
|
`regions` to limit the search to options you'd actually move to; your current region is
|
|
113
126
|
always included. Before accepting a move, run data_locality_egress_check if your data stays in
|
|
114
|
-
the current region — the egress to feed compute from afar can outweigh the greener-region win.
|
|
127
|
+
the current region — the egress to feed compute from afar can outweigh the greener-region win.
|
|
128
|
+
To write a chosen move up for a pull request or decision record, use region_move_justification."""
|
|
115
129
|
rec = await recommender.compare_regions(
|
|
116
130
|
_repo,
|
|
117
131
|
providers=[p for p in (providers or [])] or None, # type: ignore[list-item]
|
|
@@ -245,6 +259,11 @@ async def rightsizing_check(
|
|
|
245
259
|
Returns the right-sized target (keeps ~40% headroom) with the monthly $ AND kg CO2 saving. Lead
|
|
246
260
|
with the cost number, carbon is the twin. Directional, design-time — uses the utilisation you
|
|
247
261
|
report, not a reading of your fleet."""
|
|
262
|
+
for name, value, zero in (("cpu_utilization", cpu_utilization, False),
|
|
263
|
+
("ram_utilization", ram_utilization, True)):
|
|
264
|
+
err = _fraction_error(name, value, allow_zero=zero)
|
|
265
|
+
if err:
|
|
266
|
+
return {"provider": provider, "region": region, "error": err}
|
|
248
267
|
advice = await rightsizing_service.rightsizing_check(
|
|
249
268
|
_repo, provider, region, vcpu, ram_gb, cpu_utilization, # type: ignore[arg-type]
|
|
250
269
|
ram_utilization=ram_utilization, hours_per_month=hours_per_month,
|
|
@@ -385,8 +404,10 @@ async def migration_payoff(
|
|
|
385
404
|
data_locality_egress_check. Give where the data is now, where compute runs, how many GB you would
|
|
386
405
|
migrate once, and how many GB a month cross between them today. Returns the payback in months, the
|
|
387
406
|
one-off vs monthly egress cost and transfer carbon, and the net over the horizon (default 12 months).
|
|
388
|
-
Egress only: storage prices, request charges, downtime and effort are not included
|
|
389
|
-
the
|
|
407
|
+
Egress only: storage prices, request charges, downtime and effort are not included, and it does NOT
|
|
408
|
+
judge whether the compute should be in that region (use region_move_justification for that). Quote the
|
|
409
|
+
FULL dollar range (low and high), not just the high end; it can straddle zero. Ask the user for the GB
|
|
410
|
+
figures rather than assuming them."""
|
|
390
411
|
if data_gb_to_migrate <= 0 or monthly_cross_boundary_gb < 0 or not 0 < horizon_months <= 120:
|
|
391
412
|
return {"error": "data_gb_to_migrate must be > 0, monthly_cross_boundary_gb >= 0, and "
|
|
392
413
|
"horizon_months between 0 and 120."}
|
|
@@ -466,7 +487,11 @@ async def estimate_training_carbon(
|
|
|
466
487
|
portable and deferrable, so this combines region choice with cleanest-hours timing. Directional
|
|
467
488
|
(TDP-based energy); GPU availability per region is NOT checked - verify your card is offered there.
|
|
468
489
|
Training data is often large: if your dataset lives in another region, run data_locality_egress_check
|
|
469
|
-
before shipping the job to the greenest region - the data egress can outweigh the carbon saving.
|
|
490
|
+
before shipping the job to the greenest region - the data egress can outweigh the carbon saving.
|
|
491
|
+
`utilization` is an optional fraction of TDP (0..1, e.g. 0.6 for 60%)."""
|
|
492
|
+
err = _fraction_error("utilization", utilization)
|
|
493
|
+
if err:
|
|
494
|
+
return {"gpu": gpu, "error": err}
|
|
470
495
|
advice = await training_service.estimate_training_carbon(
|
|
471
496
|
_repo, gpu, gpu_count, gpu_hours, # type: ignore[arg-type]
|
|
472
497
|
region=region, provider=provider, utilization=utilization, # type: ignore[arg-type]
|
|
@@ -22,6 +22,10 @@ from ..data.static_data import network_model
|
|
|
22
22
|
from . import carbon_service, locality_service, residency_service
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def _usd(x: float) -> str:
|
|
26
|
+
return f"-${abs(x):,.0f}" if x < 0 else f"${x:,.0f}"
|
|
27
|
+
|
|
28
|
+
|
|
25
29
|
async def migration_payoff(
|
|
26
30
|
repo: CarbonRepository,
|
|
27
31
|
data_provider: str, data_region: str,
|
|
@@ -69,6 +73,8 @@ async def migration_payoff(
|
|
|
69
73
|
residency = residency_service.residency_flag(
|
|
70
74
|
data_provider, data_region, compute_provider, compute_region)
|
|
71
75
|
|
|
76
|
+
straddle = (net_usd is not None and net_usd[0] < 0 < net_usd[1])
|
|
77
|
+
egress_only = " Egress only: this does not judge whether the compute should be in that region."
|
|
72
78
|
if monthly_cross_boundary_gb <= 0:
|
|
73
79
|
verdict = "no_ongoing_traffic"
|
|
74
80
|
summary = (f"Moving the data buys no egress saving: no GB a month cross between "
|
|
@@ -79,12 +85,17 @@ async def migration_payoff(
|
|
|
79
85
|
summary = (f"Moving the data too pays back in about {payback_months:g} months "
|
|
80
86
|
f"(~{data_gb_to_migrate:,.0f} GB once vs ~{monthly_cross_boundary_gb:,.0f} GB a month "
|
|
81
87
|
f"no longer crossing), within your {horizon_months:g}-month horizon.")
|
|
88
|
+
if straddle:
|
|
89
|
+
summary += (f" In dollars it could land either side of zero over the horizon "
|
|
90
|
+
f"(net {_usd(net_usd[0])} to {_usd(net_usd[1])}); the carbon saving is "
|
|
91
|
+
f"{'positive' if net_kg and net_kg > 0 else 'not positive'} (~{net_kg:,.1f} kg).")
|
|
92
|
+
summary += egress_only
|
|
82
93
|
else:
|
|
83
94
|
verdict = "does_not_pay_back"
|
|
84
95
|
summary = (f"Moving the data too does NOT pay back within {horizon_months:g} months: it takes "
|
|
85
96
|
f"about {payback_months:g} months (~{data_gb_to_migrate:,.0f} GB once vs "
|
|
86
97
|
f"~{monthly_cross_boundary_gb:,.0f} GB a month no longer crossing). Leave the data "
|
|
87
|
-
"where it is, or re-check the volumes.")
|
|
98
|
+
"where it is, or re-check the volumes.") + egress_only
|
|
88
99
|
|
|
89
100
|
return {
|
|
90
101
|
"summary": summary,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: greenpick
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -308,6 +308,10 @@ with open("greenpick-server.log", "w") as errlog:
|
|
|
308
308
|
Top-level `await` works in Jupyter. In a plain script, put the `with` block inside an `async def`
|
|
309
309
|
and run it with `asyncio.run(...)`.
|
|
310
310
|
|
|
311
|
+
Using an AI assistant inside a notebook? Paste the
|
|
312
|
+
[starter prompt](https://github.com/bithug01/greenpick/blob/master/docs/using-from-a-notebook.md)
|
|
313
|
+
as your first message: it sets up the connection and points the assistant at the right tool for each question.
|
|
314
|
+
|
|
311
315
|
Every carbon figure includes a `quality` field: `live` means a current reading, `typical` means
|
|
312
316
|
this month's average from last year's real data, `annual_average` means a sourced yearly national
|
|
313
317
|
figure (regions with no free hourly data), and `mock` means a hand-set value. Check it to confirm
|
|
@@ -1968,3 +1968,68 @@ async def test_new_tools_validate_inputs():
|
|
|
1968
1968
|
assert "error" in bad
|
|
1969
1969
|
bad2 = await _tool("region_move_justification")("aws", "us-east-1", "aws", "eu-north-1", 4, 16, hours_per_month=0)
|
|
1970
1970
|
assert "error" in bad2
|
|
1971
|
+
|
|
1972
|
+
|
|
1973
|
+
# --- 0.6.1: migration summary is honest about a dollar range that straddles zero ---
|
|
1974
|
+
@pytest.mark.asyncio
|
|
1975
|
+
async def test_migration_summary_flags_dollar_range_straddling_zero():
|
|
1976
|
+
from greenpick.services import migration_service as ms
|
|
1977
|
+
args = ("aws", "us-east-1", "aws", "eu-north-1")
|
|
1978
|
+
r = await ms.migration_payoff(MockCarbonRepository(), *args, 2000, 300)
|
|
1979
|
+
assert r["verdict"] == "pays_back" and r["net_over_horizon_usd_low"] < 0 < r["net_over_horizon_usd_high"]
|
|
1980
|
+
assert "either side of zero" in r["summary"] and "Egress only" in r["summary"]
|
|
1981
|
+
clear = await ms.migration_payoff(MockCarbonRepository(), *args, 500, 300)
|
|
1982
|
+
assert "either side of zero" not in clear["summary"] and "Egress only" in clear["summary"]
|
|
1983
|
+
assert "Egress only" in (await ms.migration_payoff(MockCarbonRepository(), *args, 8000, 300))["summary"]
|
|
1984
|
+
|
|
1985
|
+
|
|
1986
|
+
def test_move_tools_point_to_the_justification_tool():
|
|
1987
|
+
import asyncio
|
|
1988
|
+
from greenpick import server
|
|
1989
|
+
desc = {t.name: t.description for t in asyncio.run(server.mcp.list_tools())}
|
|
1990
|
+
assert "region_move_justification" in desc["compare_regions_tri_axis"]
|
|
1991
|
+
assert "region_move_justification" in desc["greener_alternative_to"]
|
|
1992
|
+
assert "FULL dollar range" in desc["migration_payoff"]
|
|
1993
|
+
|
|
1994
|
+
|
|
1995
|
+
# --- docs/using-from-a-notebook.md stays in step with the real tool list ---
|
|
1996
|
+
def test_notebook_prompt_lists_every_tool_and_no_unknown_ones():
|
|
1997
|
+
import asyncio, re
|
|
1998
|
+
from pathlib import Path
|
|
1999
|
+
from greenpick import server
|
|
2000
|
+
names = {t.name for t in asyncio.run(server.mcp.list_tools())}
|
|
2001
|
+
text = (Path(__file__).resolve().parent.parent / "docs" / "using-from-a-notebook.md").read_text(encoding="utf-8")
|
|
2002
|
+
missing = sorted(n for n in names if n not in text)
|
|
2003
|
+
assert not missing, f"add these tools to docs/using-from-a-notebook.md: {missing}"
|
|
2004
|
+
routed = set()
|
|
2005
|
+
for line in text.splitlines():
|
|
2006
|
+
if line.startswith("- ") and " -> " in line:
|
|
2007
|
+
routed |= set(re.findall(r"[a-z][a-z0-9]*(?:_[a-z0-9]+)+", line.split(" -> ", 1)[1]))
|
|
2008
|
+
assert routed and routed <= names, f"unknown tool names in the routing list: {sorted(routed - names)}"
|
|
2009
|
+
|
|
2010
|
+
|
|
2011
|
+
# --- 0.6.2: a percentage passed as a utilisation fraction is rejected, not read as 1,500% ---
|
|
2012
|
+
@pytest.mark.asyncio
|
|
2013
|
+
@pytest.mark.parametrize("bad", [15, 1.5, 0, -0.1, 150])
|
|
2014
|
+
async def test_rightsizing_rejects_non_fraction_cpu_utilization(bad):
|
|
2015
|
+
r = await _tool("rightsizing_check")(provider="aws", region="us-east-1", vcpu=8, ram_gb=32, cpu_utilization=bad)
|
|
2016
|
+
assert "error" in r and "fraction" in r["error"]
|
|
2017
|
+
if bad == 15:
|
|
2018
|
+
assert "Did you mean 0.15?" in r["error"]
|
|
2019
|
+
|
|
2020
|
+
|
|
2021
|
+
@pytest.mark.asyncio
|
|
2022
|
+
async def test_rightsizing_fraction_still_works_and_ram_is_checked(monkeypatch):
|
|
2023
|
+
from greenpick import server
|
|
2024
|
+
monkeypatch.setattr(server, "_repo", MockCarbonRepository())
|
|
2025
|
+
ok = await _tool("rightsizing_check")(provider="aws", region="us-east-1", vcpu=8, ram_gb=32, cpu_utilization=0.15)
|
|
2026
|
+
assert "error" not in ok and ok["recommended_vcpu"] == 4
|
|
2027
|
+
bad = await _tool("rightsizing_check")(provider="aws", region="us-east-1", vcpu=8, ram_gb=32,
|
|
2028
|
+
cpu_utilization=0.15, ram_utilization=20)
|
|
2029
|
+
assert "ram_utilization" in bad["error"] and "0.2" in bad["error"]
|
|
2030
|
+
|
|
2031
|
+
|
|
2032
|
+
@pytest.mark.asyncio
|
|
2033
|
+
async def test_training_rejects_percentage_utilization():
|
|
2034
|
+
r = await _tool("estimate_training_carbon")(gpu="a100", gpu_count=8, gpu_hours=40, utilization=60)
|
|
2035
|
+
assert "error" in r and "Did you mean 0.6?" in r["error"]
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|