greenpick 0.1.0__py3-none-any.whl

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 (42) hide show
  1. greenpick/__init__.py +7 -0
  2. greenpick/__main__.py +18 -0
  3. greenpick/config.py +58 -0
  4. greenpick/data/__init__.py +28 -0
  5. greenpick/data/auto_repository.py +47 -0
  6. greenpick/data/eia_repository.py +156 -0
  7. greenpick/data/electricitymaps_repository.py +104 -0
  8. greenpick/data/emission_factors.json +35 -0
  9. greenpick/data/entsoe_repository.py +401 -0
  10. greenpick/data/gpu_power.json +26 -0
  11. greenpick/data/inference_energy.json +50 -0
  12. greenpick/data/instance_families.json +32 -0
  13. greenpick/data/mock_repository.py +53 -0
  14. greenpick/data/network.json +7 -0
  15. greenpick/data/power.json +7 -0
  16. greenpick/data/pricing.json +31 -0
  17. greenpick/data/regions.json +22 -0
  18. greenpick/data/repository.py +27 -0
  19. greenpick/data/static_data.py +105 -0
  20. greenpick/models.py +342 -0
  21. greenpick/server.py +349 -0
  22. greenpick/services/__init__.py +5 -0
  23. greenpick/services/carbon_service.py +28 -0
  24. greenpick/services/cost_service.py +21 -0
  25. greenpick/services/forecast_service.py +72 -0
  26. greenpick/services/inference_service.py +100 -0
  27. greenpick/services/instance_service.py +132 -0
  28. greenpick/services/latency_service.py +33 -0
  29. greenpick/services/locality_service.py +160 -0
  30. greenpick/services/metric_scaffold_service.py +149 -0
  31. greenpick/services/recommender.py +203 -0
  32. greenpick/services/residency_service.py +53 -0
  33. greenpick/services/rightsizing_service.py +93 -0
  34. greenpick/services/scheduling_service.py +211 -0
  35. greenpick/services/serving_service.py +202 -0
  36. greenpick/services/training_service.py +209 -0
  37. greenpick-0.1.0.dist-info/METADATA +271 -0
  38. greenpick-0.1.0.dist-info/RECORD +42 -0
  39. greenpick-0.1.0.dist-info/WHEEL +5 -0
  40. greenpick-0.1.0.dist-info/entry_points.txt +2 -0
  41. greenpick-0.1.0.dist-info/licenses/LICENSE +21 -0
  42. greenpick-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,132 @@
1
+ """Greener instance-family suggestion.
2
+
3
+ The honest carbon mechanism here is performance-per-watt: a newer-generation or ARM
4
+ (Graviton-class) family does the SAME work on less energy, so for a fixed grid intensity it
5
+ emits less CO2. We take the user's current general-purpose family, look up directional
6
+ energy multipliers for the same-size families in that cloud, and rank the greener options —
7
+ reporting the energy saving, the CO2 estimate (energy x the region's live grid intensity),
8
+ and a relative R/Y/G rating.
9
+
10
+ Deliberately NOT a pricing engine: we don't claim per-family prices we don't have. Cost is
11
+ left to the region/cost tools; this tool's axis is energy -> carbon.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from typing import Optional
16
+
17
+ from ..models import (
18
+ Workload, Provider, CarbonReading,
19
+ GreenerComputeAdvice, FamilyOption,
20
+ )
21
+ from ..data.repository import CarbonRepository
22
+ from ..data.static_data import power_model, instance_families
23
+ from . import carbon_service
24
+
25
+
26
+ SUPPORTED_PROVIDERS = ("aws", "gcp", "azure")
27
+
28
+
29
+ def is_supported_provider(provider: str) -> bool:
30
+ return provider in SUPPORTED_PROVIDERS
31
+
32
+
33
+ def _families_for(provider: str) -> list[dict]:
34
+ data = instance_families().get(provider, {})
35
+ return list(data.get("_general_purpose_generations", []))
36
+
37
+
38
+ def _base_kwh_per_month(workload: Workload) -> float:
39
+ """Energy for the baseline (relative_energy = 1.00) family — same power model the
40
+ carbon estimate uses. Each family then scales this by its relative_energy."""
41
+ pm = power_model()
42
+ watts = (workload.vcpu * pm["watts_per_vcpu"] + workload.ram_gb * pm["watts_per_gb_ram"])
43
+ watts *= pm["pue_default"]
44
+ return watts / 1000.0 * workload.hours_per_month
45
+
46
+
47
+ def _rate(options: list[FamilyOption]) -> None:
48
+ """Assign relative R/Y/G by energy terciles across the option set (greenest = green)."""
49
+ if not options:
50
+ return
51
+ ordered = sorted(options, key=lambda o: o.relative_energy)
52
+ n = len(ordered)
53
+ for i, opt in enumerate(ordered):
54
+ if n <= 2:
55
+ opt.rating = "green" if i == 0 else ("red" if i == n - 1 else "yellow")
56
+ else:
57
+ third = n / 3.0
58
+ opt.rating = "green" if i < third else ("red" if i >= 2 * third else "yellow")
59
+
60
+
61
+ async def greener_instance_family(
62
+ repo: CarbonRepository,
63
+ provider: Provider,
64
+ region: str,
65
+ vcpu: float,
66
+ ram_gb: float,
67
+ current_family: Optional[str] = None,
68
+ hours_per_month: float = 730,
69
+ ) -> GreenerComputeAdvice:
70
+ families = _families_for(provider)
71
+ if not families:
72
+ return GreenerComputeAdvice(
73
+ provider=provider, region=region, current_family=current_family,
74
+ rationale=f"No instance-family efficiency data for provider '{provider}'.",
75
+ limitations="Supported providers: aws, gcp, azure (general-purpose families only).",
76
+ )
77
+
78
+ reading: CarbonReading = await carbon_service.carbon_now(repo, provider, region)
79
+ wl = Workload(vcpu=vcpu, ram_gb=ram_gb, hours_per_month=hours_per_month)
80
+ base_kwh = _base_kwh_per_month(wl)
81
+
82
+ # Resolve the current family (case-insensitive); if unknown/omitted, compare against the
83
+ # LEAST efficient family so savings are framed conservatively ("at least this much").
84
+ by_name = {f["family"].lower(): f for f in families}
85
+ current = by_name.get((current_family or "").lower())
86
+ current_rel = current["relative_energy"] if current else max(f["relative_energy"] for f in families)
87
+ current_resolved = current["family"] if current else None
88
+
89
+ options: list[FamilyOption] = []
90
+ for f in families:
91
+ rel = f["relative_energy"]
92
+ kwh = base_kwh * rel
93
+ kg = round(kwh * reading.g_co2_per_kwh / 1000.0, 2)
94
+ energy_saving = round((current_rel - rel) / current_rel * 100, 1) if current_rel > 0 else None
95
+ co2_saving = energy_saving # same grid intensity, so CO2 scales with energy
96
+ options.append(FamilyOption(
97
+ family=f["family"], arch=f["arch"], cpu=f["cpu"],
98
+ relative_energy=rel,
99
+ energy_saving_vs_current_pct=energy_saving,
100
+ est_kg_co2_per_month=kg,
101
+ co2_saving_vs_current_pct=co2_saving,
102
+ note=f.get("notes", ""),
103
+ ))
104
+
105
+ options.sort(key=lambda o: o.relative_energy) # greenest first
106
+ _rate(options)
107
+ greenest = options[0]
108
+
109
+ # Rationale: honest about the mechanism and the ARM caveat.
110
+ if current_resolved and greenest.family.lower() == current_resolved.lower():
111
+ rationale = (f"You're already on {current_resolved}, the greenest general-purpose family "
112
+ f"available here. No change needed.")
113
+ else:
114
+ saving = greenest.energy_saving_vs_current_pct
115
+ arm_note = (" It's an ARM (Graviton-class) family, so this assumes your workload can run "
116
+ "on ARM - a recompile/container rebuild may be needed."
117
+ if greenest.arch == "arm" else "")
118
+ base = (f"vs {current_resolved}" if current_resolved
119
+ else "vs the least-efficient family here")
120
+ rationale = (f"{greenest.family} ({greenest.cpu}) does the same work on "
121
+ f"~{saving:.0f}% less energy {base}, so ~{saving:.0f}% less CO2 at this "
122
+ f"region's grid intensity.{arm_note}")
123
+
124
+ return GreenerComputeAdvice(
125
+ provider=provider, region=region, current_family=current_resolved,
126
+ grid_intensity_g_co2_per_kwh=reading.g_co2_per_kwh,
127
+ carbon_source=reading.source, carbon_quality=reading.quality,
128
+ options=options, recommended_family=greenest.family, rationale=rationale,
129
+ limitations=("Energy multipliers are DIRECTIONAL (perf-per-watt by generation/arch), "
130
+ "not measured for your workload; general-purpose families only; embodied "
131
+ "hardware carbon ignored. ARM options require ARM-compatible builds."),
132
+ )
@@ -0,0 +1,33 @@
1
+ """Geographic latency estimate: great-circle distance -> rough round-trip ms.
2
+
3
+ A deliberate approximation (speed of light in fiber + routing overhead), good enough
4
+ for "is this region far from my users?" Not a measured ping.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import math
9
+ from ..models import GeoPoint, Provider
10
+ from ..data.static_data import region_meta
11
+
12
+ # Rough rule of thumb: ~fiber latency plus routing/queuing overhead.
13
+ # ~0.0107 ms per km one-way for light in fiber; double for round trip; add a fixed overhead.
14
+ _MS_PER_KM_RTT = 0.0107 * 2 * 1.4 # 1.4 fudge for non-great-circle routing
15
+ _FIXED_OVERHEAD_MS = 10.0
16
+
17
+
18
+ def _haversine_km(a: GeoPoint, b: GeoPoint) -> float:
19
+ r = 6371.0
20
+ dlat = math.radians(b.lat - a.lat)
21
+ dlon = math.radians(b.lon - a.lon)
22
+ h = (math.sin(dlat / 2) ** 2
23
+ + math.cos(math.radians(a.lat)) * math.cos(math.radians(b.lat)) * math.sin(dlon / 2) ** 2)
24
+ return 2 * r * math.asin(math.sqrt(h))
25
+
26
+
27
+ def estimate_latency_ms(provider: Provider, region: str, user: GeoPoint) -> float | None:
28
+ meta = region_meta(provider, region)
29
+ if not meta:
30
+ return None
31
+ region_pt = GeoPoint(lat=meta["lat"], lon=meta["lon"])
32
+ km = _haversine_km(user, region_pt)
33
+ return round(_FIXED_OVERHEAD_MS + km * _MS_PER_KM_RTT, 1)
@@ -0,0 +1,160 @@
1
+ """Data-locality / egress warning.
2
+
3
+ The trap this guards against: you move COMPUTE to a greener (often farther) region, but your
4
+ DATA stays put — a database, object store, or another service. Every byte crossing that
5
+ boundary is billed as egress and burns extra network energy. A naive "greener region" pick can
6
+ be eaten alive by egress cost, and sometimes by the transfer carbon too.
7
+
8
+ This tool is explicit by design: the caller tells us where compute runs, where the data lives,
9
+ and how much crosses per month. We classify the separation, give a DIRECTIONAL egress-cost
10
+ ballpark (the decision-relevant number), a deliberately-coarse transfer-carbon figure, and — if
11
+ the caller supplies the carbon saving the move buys — a net verdict on whether the move still
12
+ pays off.
13
+
14
+ Honesty posture: egress COST leads (it's concrete, if approximate). Transfer CARBON is secondary
15
+ and loudly flagged as order-of-magnitude, because the science on kWh/GB is unsettled.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import Optional
20
+
21
+ from ..models import DataLocalityWarning, Provider, CarbonReading
22
+ from ..data.repository import CarbonRepository
23
+ from ..data.static_data import pricing, network_model
24
+ from . import carbon_service, residency_service
25
+
26
+ LIMITATIONS = (
27
+ "Egress cost is DIRECTIONAL: a LOW-HIGH range for the locality tier (same-region / cross-region / "
28
+ "cross-cloud) from dated list prices — it ignores free-tier allowances, committed-use/negotiated "
29
+ "discounts, exact region-pair rates, and NAT/load-balancer fees, so treat it as a ballpark, not a "
30
+ "quote. Transfer carbon uses a single kWh/GB coefficient whose published estimates vary by orders "
31
+ "of magnitude; it's order-of-magnitude only. You supply where data lives and how much crosses — "
32
+ "GreenPick can't infer your architecture."
33
+ )
34
+
35
+
36
+ def _classify(compute_provider: str, compute_region: str,
37
+ data_provider: str, data_region: str) -> tuple[str, str]:
38
+ """Return (locality, severity). Severity is the structural separation, before volume."""
39
+ if compute_provider == data_provider and compute_region == data_region:
40
+ return "same_region", "none"
41
+ if compute_provider == data_provider:
42
+ return "cross_region_same_cloud", "medium"
43
+ return "cross_cloud", "high"
44
+
45
+
46
+ def _bump_severity_for_volume(severity: str, gb: float) -> str:
47
+ """A tiny cross-boundary trickle barely matters; a firehose matters a lot. Nudge severity
48
+ by monthly volume so the warning tracks real exposure, not just topology."""
49
+ if severity == "none":
50
+ return "none"
51
+ order = ["low", "medium", "high"]
52
+ base = {"low": 0, "medium": 1, "high": 2}[severity]
53
+ if gb >= 1000:
54
+ base = min(2, base + 1)
55
+ elif gb < 50:
56
+ base = max(0, base - 1)
57
+ return order[base]
58
+
59
+
60
+ async def data_locality_check(
61
+ repo: CarbonRepository,
62
+ compute_provider: Provider,
63
+ compute_region: str,
64
+ data_provider: Provider,
65
+ data_region: str,
66
+ monthly_cross_boundary_gb: float,
67
+ carbon_saving_kg_co2_from_move: Optional[float] = None,
68
+ ) -> DataLocalityWarning:
69
+ compute_loc = f"{compute_provider}/{compute_region}"
70
+ data_loc = f"{data_provider}/{data_region}"
71
+ locality, severity = _classify(compute_provider, compute_region, data_provider, data_region)
72
+
73
+ if locality == "same_region":
74
+ return DataLocalityWarning(
75
+ compute_location=compute_loc, data_location=data_loc,
76
+ monthly_cross_boundary_gb=monthly_cross_boundary_gb,
77
+ locality=locality, severity="none",
78
+ egress_rate_usd_per_gb_low=0.0, egress_rate_usd_per_gb_high=0.0,
79
+ est_monthly_egress_usd_low=0.0, est_monthly_egress_usd_high=0.0,
80
+ est_monthly_transfer_kg_co2=0.0,
81
+ warning="Compute and data are co-located — no cross-boundary egress for this flow.",
82
+ recommendation="No locality penalty. Choose the region on carbon/latency/cost as usual.",
83
+ limitations=LIMITATIONS,
84
+ )
85
+
86
+ severity = _bump_severity_for_volume(severity, monthly_cross_boundary_gb)
87
+
88
+ # Egress is a TIER LADDER by where the bytes cross — not one flat rate. Price the tier we just
89
+ # classified, as a LOW-HIGH range (region-pair rates vary). For cross-cloud/internet the rate
90
+ # is provider-specific, so anchor the high end to the data provider's own internet rate.
91
+ tiers = pricing().get("_egress_tiers_usd_per_gb", {})
92
+ tier = tiers.get(locality, {})
93
+ rate_low = tier.get("low")
94
+ rate_high = tier.get("high")
95
+ if locality == "cross_cloud":
96
+ prov_internet = (pricing().get(data_provider) or {}).get("egress_usd_per_gb")
97
+ if prov_internet is not None and rate_high is not None:
98
+ rate_high = max(rate_high, prov_internet) # don't under-quote this provider's internet rate
99
+ cost_low = round(monthly_cross_boundary_gb * rate_low, 2) if rate_low is not None else None
100
+ cost_high = round(monthly_cross_boundary_gb * rate_high, 2) if rate_high is not None else None
101
+
102
+ # Transfer carbon: energy to move the bytes x the grid intensity where that energy is drawn.
103
+ # Use the data region's grid as the representative intensity (coarse — energy is spread along
104
+ # the path, but this keeps it tied to a real, live number).
105
+ kwh_per_gb = network_model()["kwh_per_gb_transfer"]
106
+ reading: CarbonReading = await carbon_service.carbon_now(repo, data_provider, data_region)
107
+ transfer_kwh = monthly_cross_boundary_gb * kwh_per_gb
108
+ transfer_kg = round(transfer_kwh * reading.g_co2_per_kwh / 1000.0, 2)
109
+
110
+ # Net verdict only if the caller told us what the move saves.
111
+ verdict = None
112
+ if carbon_saving_kg_co2_from_move is not None:
113
+ net = carbon_saving_kg_co2_from_move - transfer_kg
114
+ if net > 0:
115
+ verdict = (f"Still a net win: the move saves ~{carbon_saving_kg_co2_from_move:.1f} kg "
116
+ f"CO2/mo, the extra transfer costs ~{transfer_kg:.1f} kg, net "
117
+ f"~{net:.1f} kg saved. (Egress COST may still argue against it.)")
118
+ else:
119
+ verdict = (f"Backfires on carbon: the move saves ~{carbon_saving_kg_co2_from_move:.1f} kg "
120
+ f"CO2/mo but the extra transfer adds ~{transfer_kg:.1f} kg - net "
121
+ f"~{abs(net):.1f} kg WORSE. Keep compute near the data, or move the data too.")
122
+
123
+ sep = ("across clouds" if locality == "cross_cloud" else "across regions in the same cloud")
124
+ if cost_low is not None and cost_high is not None:
125
+ cost_txt = (f"roughly ${cost_low:,.0f}-${cost_high:,.0f}/mo in egress "
126
+ f"(~${rate_low:.2f}-${rate_high:.2f}/GB for this tier)")
127
+ else:
128
+ cost_txt = "an egress cost (rate unknown for this tier)"
129
+ warning = (f"Compute and data are separated {sep}: ~{monthly_cross_boundary_gb:,.0f} GB/mo would "
130
+ f"cross the boundary, costing {cost_txt}, plus ~{transfer_kg:.1f} kg CO2/mo of extra "
131
+ f"transfer. A greener-region move can be undone by this.")
132
+
133
+ if severity == "high":
134
+ rec = ("High exposure. Co-locate compute with the data, or move the data to the greener "
135
+ "region too. If you can't, confirm the carbon/cost saving beats this egress penalty "
136
+ "before moving compute alone.")
137
+ elif severity == "medium":
138
+ rec = ("Moderate exposure. Weigh the egress cost against the region's carbon/cost saving; "
139
+ "co-locating is usually cleaner and cheaper.")
140
+ else:
141
+ rec = ("Low exposure at this volume. The locality penalty is small — the region's "
142
+ "carbon/latency/cost tradeoff probably dominates.")
143
+
144
+ # Residency heads-up: compute processing data that lives in another jurisdiction is exactly the
145
+ # residency-sensitive case. Flag the data-region -> compute-region crossing (neutral, not legal).
146
+ residency_note = residency_service.residency_flag(
147
+ data_provider, data_region, compute_provider, compute_region,
148
+ )
149
+
150
+ return DataLocalityWarning(
151
+ compute_location=compute_loc, data_location=data_loc,
152
+ monthly_cross_boundary_gb=monthly_cross_boundary_gb,
153
+ locality=locality, severity=severity,
154
+ egress_rate_usd_per_gb_low=rate_low, egress_rate_usd_per_gb_high=rate_high,
155
+ est_monthly_egress_usd_low=cost_low, est_monthly_egress_usd_high=cost_high,
156
+ est_monthly_transfer_kg_co2=transfer_kg,
157
+ carbon_saving_kg_co2_from_move=carbon_saving_kg_co2_from_move,
158
+ net_carbon_verdict=verdict, residency_note=residency_note,
159
+ warning=warning, recommendation=rec, limitations=LIMITATIONS,
160
+ )
@@ -0,0 +1,149 @@
1
+ """carbon_metric_scaffold: emit paste-in instrumentation so an app measures its OWN estimated carbon.
2
+
3
+ GreenPick does not run or measure anything here. Software can't read its own wattage, so the app
4
+ measures the cheap proxies it CAN observe (CPU utilisation, provisioned vCPU/RAM, uptime) and
5
+ applies GreenPick's power model x the region's grid intensity to emit an ESTIMATED gCO2 metric.
6
+ GreenPick's job is to supply (a) the code and (b) the coefficients that make the number meaningful.
7
+
8
+ Hard guardrails baked into the output:
9
+ - The metric is self-labelled `estimated_gco2*` with a comment stating it's directional, model-based,
10
+ NOT metered energy — because it will live in production dashboards and get quoted.
11
+ - v1 is SELF-tracking (watch your own app over time/version/region/load), NOT cross-app ranking,
12
+ which would need a shared spec to be anything but noise.
13
+ - Deliverable is a snippet + coefficients + methodology note, NOT a framework/SDK.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from typing import Optional
18
+
19
+ from ..models import CarbonMetricScaffold, Provider
20
+ from ..data.repository import CarbonRepository
21
+ from ..data.static_data import power_model
22
+ from . import carbon_service
23
+
24
+ SUPPORTED_LANGUAGES = ("python",)
25
+
26
+ METHODOLOGY = (
27
+ "estimated_gco2_per_hour = ((vcpu * watts_per_vcpu * cpu_utilisation) + (ram_gb * watts_per_gb_ram)) "
28
+ "* pue / 1000 * grid_g_co2_per_kwh. Proxies (CPU util, vCPU, RAM, uptime) are what an app can "
29
+ "actually observe; the watts/PUE come from GreenPick's documented power model; grid intensity from "
30
+ "the region. It is an ESTIMATE, not a measurement of real energy drawn."
31
+ )
32
+
33
+ LIMITATIONS = (
34
+ "DIRECTIONAL estimate, not metered energy — software cannot read its true wattage, so this models "
35
+ "it from CPU utilisation and a published power model. Ignores GPU/accelerator, disk/network, "
36
+ "embodied carbon, and real hardware variance. If you hardcode a grid intensity it goes stale — "
37
+ "refresh it (or call region_carbon_now) periodically. Use for SELF-tracking over time, NOT to rank "
38
+ "against other apps (different boundaries/coefficients make that meaningless)."
39
+ )
40
+
41
+
42
+ def _python_snippet(vcpu_default: float, ram_default: float, pm: dict,
43
+ grid: Optional[float], region: Optional[str]) -> str:
44
+ watts_vcpu = pm["watts_per_vcpu"]
45
+ watts_ram = pm["watts_per_gb_ram"]
46
+ pue = pm["pue_default"]
47
+ grid_literal = repr(grid) if grid is not None else "None"
48
+ region_comment = f" # region: {region}" if region else ""
49
+ return f'''# --- GreenPick carbon metric (PASTE INTO YOUR APP) ---------------------------------
50
+ # estimated_gco2 is a DIRECTIONAL ESTIMATE from CPU utilisation x a documented power model
51
+ # x grid carbon intensity. It is NOT a measurement of real energy drawn. Use it to watch THIS
52
+ # app's footprint over time/versions/regions - not to compare against other apps.
53
+ # Model + coefficients: GreenPick (power.json). Methodology in the accompanying note.
54
+ import time
55
+
56
+ # --- Coefficients from GreenPick's power model (safe to keep; refresh grid intensity periodically)
57
+ _WATTS_PER_VCPU = {watts_vcpu}
58
+ _WATTS_PER_GB_RAM = {watts_ram}
59
+ _PUE = {pue}
60
+ _GRID_G_CO2_PER_KWH = {grid_literal}{region_comment} # set/refresh for YOUR region; see region_carbon_now
61
+
62
+
63
+ class CarbonEstimator:
64
+ """Estimates this process's grid carbon from observable proxies. Directional, not metered."""
65
+
66
+ def __init__(self, vcpu: float = {vcpu_default}, ram_gb: float = {ram_default},
67
+ grid_g_co2_per_kwh: float | None = _GRID_G_CO2_PER_KWH):
68
+ self.vcpu = vcpu
69
+ self.ram_gb = ram_gb
70
+ self.grid = grid_g_co2_per_kwh
71
+ self._start = time.time()
72
+
73
+ def estimated_watts(self, cpu_utilisation: float) -> float:
74
+ """cpu_utilisation in 0..1 (e.g. psutil.cpu_percent()/100)."""
75
+ u = max(0.0, min(1.0, cpu_utilisation))
76
+ return (self.vcpu * _WATTS_PER_VCPU * u + self.ram_gb * _WATTS_PER_GB_RAM) * _PUE
77
+
78
+ def estimated_gco2_per_hour(self, cpu_utilisation: float) -> float | None:
79
+ if self.grid is None:
80
+ return None # no grid intensity set -> can't estimate carbon honestly
81
+ return self.estimated_watts(cpu_utilisation) / 1000.0 * self.grid
82
+
83
+ def estimated_gco2_since_start(self, avg_cpu_utilisation: float) -> float | None:
84
+ rate = self.estimated_gco2_per_hour(avg_cpu_utilisation)
85
+ if rate is None:
86
+ return None
87
+ hours = (time.time() - self._start) / 3600.0
88
+ return rate * hours
89
+
90
+
91
+ # Example: expose as a metric (Prometheus-style gauge, a log line, or a /metrics field).
92
+ # import psutil
93
+ # est = CarbonEstimator(vcpu=2, ram_gb=4)
94
+ # gco2_per_hour = est.estimated_gco2_per_hour(psutil.cpu_percent() / 100.0)
95
+ # # -> publish `estimated_gco2_per_hour` to your dashboard
96
+ # ---------------------------------------------------------------------------------'''
97
+
98
+
99
+ async def carbon_metric_scaffold(
100
+ repo: CarbonRepository,
101
+ language: str = "python",
102
+ region: Optional[str] = None,
103
+ provider: Optional[Provider] = None,
104
+ vcpu: float = 1,
105
+ ram_gb: float = 2,
106
+ ) -> CarbonMetricScaffold:
107
+ lang = language.strip().lower()
108
+ if lang not in SUPPORTED_LANGUAGES:
109
+ return CarbonMetricScaffold(
110
+ language=language,
111
+ how_to_use=f"Language '{language}' not supported yet. v1 emits Python; "
112
+ f"the math (see methodology) ports easily to any language.",
113
+ methodology=METHODOLOGY, limitations=LIMITATIONS,
114
+ )
115
+
116
+ pm = power_model()
117
+ grid = None
118
+ source = quality = None
119
+ if region is not None:
120
+ prov = provider or _guess_provider(region)
121
+ if prov is not None:
122
+ reading = await carbon_service.carbon_now(repo, prov, region) # type: ignore[arg-type]
123
+ grid = reading.g_co2_per_kwh
124
+ source, quality = reading.source, reading.quality
125
+
126
+ code = _python_snippet(vcpu, ram_gb, pm, grid, region)
127
+ how = ("Paste the snippet into your app. Instantiate CarbonEstimator with your instance's vcpu/"
128
+ "ram_gb, feed it CPU utilisation (e.g. psutil.cpu_percent()/100), and publish "
129
+ "estimated_gco2_per_hour to your dashboard/metrics. Set the grid intensity for your "
130
+ "region — if you gave GreenPick a region it's baked in; otherwise call region_carbon_now "
131
+ "and refresh it periodically so it doesn't go stale.")
132
+ return CarbonMetricScaffold(
133
+ language="python", region=region,
134
+ grid_intensity_g_co2_per_kwh=grid, carbon_source=source, carbon_quality=quality,
135
+ coefficients={
136
+ "watts_per_vcpu": pm["watts_per_vcpu"],
137
+ "watts_per_gb_ram": pm["watts_per_gb_ram"],
138
+ "pue_default": pm["pue_default"],
139
+ },
140
+ code=code, how_to_use=how, methodology=METHODOLOGY, limitations=LIMITATIONS,
141
+ )
142
+
143
+
144
+ def _guess_provider(region: str) -> Optional[str]:
145
+ from ..data.static_data import iter_regions
146
+ for prov, reg, _meta in iter_regions():
147
+ if reg == region:
148
+ return prov
149
+ return None