greenpick 0.6.1__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.
Files changed (58) hide show
  1. {greenpick-0.6.1/src/greenpick.egg-info → greenpick-0.6.2}/PKG-INFO +5 -1
  2. {greenpick-0.6.1 → greenpick-0.6.2}/README.md +4 -0
  3. {greenpick-0.6.1 → greenpick-0.6.2}/pyproject.toml +1 -1
  4. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/__init__.py +1 -1
  5. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/server.py +22 -1
  6. {greenpick-0.6.1 → greenpick-0.6.2/src/greenpick.egg-info}/PKG-INFO +5 -1
  7. {greenpick-0.6.1 → greenpick-0.6.2}/tests/test_services.py +43 -0
  8. {greenpick-0.6.1 → greenpick-0.6.2}/LICENSE +0 -0
  9. {greenpick-0.6.1 → greenpick-0.6.2}/setup.cfg +0 -0
  10. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/__main__.py +0 -0
  11. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/config.py +0 -0
  12. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/__init__.py +0 -0
  13. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/_net.py +0 -0
  14. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/annual.json +0 -0
  15. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/auto_repository.py +0 -0
  16. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/canada_repository.py +0 -0
  17. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/eia_repository.py +0 -0
  18. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/electricitymaps_repository.py +0 -0
  19. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/emission_factors.json +0 -0
  20. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/entsoe_repository.py +0 -0
  21. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/gpu_power.json +0 -0
  22. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/inference_energy.json +0 -0
  23. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/instance_families.json +0 -0
  24. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/mock_repository.py +0 -0
  25. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/network.json +0 -0
  26. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/power.json +0 -0
  27. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/pricing.json +0 -0
  28. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/regions.json +0 -0
  29. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/repository.py +0 -0
  30. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/static_data.py +0 -0
  31. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/typical.json +0 -0
  32. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/data/typical_repository.py +0 -0
  33. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/models.py +0 -0
  34. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/__init__.py +0 -0
  35. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/carbon_service.py +0 -0
  36. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/cost_service.py +0 -0
  37. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/data_hint.py +0 -0
  38. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/forecast_service.py +0 -0
  39. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/inference_service.py +0 -0
  40. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/instance_service.py +0 -0
  41. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/justification_service.py +0 -0
  42. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/latency_service.py +0 -0
  43. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/locality_service.py +0 -0
  44. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/metric_scaffold_service.py +0 -0
  45. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/migration_service.py +0 -0
  46. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/offhours_service.py +0 -0
  47. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/recommender.py +0 -0
  48. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/residency_service.py +0 -0
  49. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/rightsizing_service.py +0 -0
  50. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/scheduling_service.py +0 -0
  51. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/serving_service.py +0 -0
  52. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/training_service.py +0 -0
  53. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick/services/typical_ranking_service.py +0 -0
  54. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick.egg-info/SOURCES.txt +0 -0
  55. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick.egg-info/dependency_links.txt +0 -0
  56. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick.egg-info/entry_points.txt +0 -0
  57. {greenpick-0.6.1 → greenpick-0.6.2}/src/greenpick.egg-info/requires.txt +0 -0
  58. {greenpick-0.6.1 → 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.1
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.1"
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.1"
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
 
@@ -247,6 +259,11 @@ async def rightsizing_check(
247
259
  Returns the right-sized target (keeps ~40% headroom) with the monthly $ AND kg CO2 saving. Lead
248
260
  with the cost number, carbon is the twin. Directional, design-time — uses the utilisation you
249
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}
250
267
  advice = await rightsizing_service.rightsizing_check(
251
268
  _repo, provider, region, vcpu, ram_gb, cpu_utilization, # type: ignore[arg-type]
252
269
  ram_utilization=ram_utilization, hours_per_month=hours_per_month,
@@ -470,7 +487,11 @@ async def estimate_training_carbon(
470
487
  portable and deferrable, so this combines region choice with cleanest-hours timing. Directional
471
488
  (TDP-based energy); GPU availability per region is NOT checked - verify your card is offered there.
472
489
  Training data is often large: if your dataset lives in another region, run data_locality_egress_check
473
- 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}
474
495
  advice = await training_service.estimate_training_carbon(
475
496
  _repo, gpu, gpu_count, gpu_hours, # type: ignore[arg-type]
476
497
  region=region, provider=provider, utilization=utilization, # type: ignore[arg-type]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: greenpick
3
- Version: 0.6.1
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
@@ -1990,3 +1990,46 @@ def test_move_tools_point_to_the_justification_tool():
1990
1990
  assert "region_move_justification" in desc["compare_regions_tri_axis"]
1991
1991
  assert "region_move_justification" in desc["greener_alternative_to"]
1992
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