greenpick 0.1.7__tar.gz → 0.2.0__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.1.7/src/greenpick.egg-info → greenpick-0.2.0}/PKG-INFO +27 -18
  2. {greenpick-0.1.7 → greenpick-0.2.0}/README.md +26 -17
  3. {greenpick-0.1.7 → greenpick-0.2.0}/pyproject.toml +1 -1
  4. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/__init__.py +1 -1
  5. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/data/__init__.py +4 -2
  6. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/data/auto_repository.py +2 -2
  7. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/data/eia_repository.py +2 -2
  8. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/data/electricitymaps_repository.py +2 -2
  9. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/data/entsoe_repository.py +2 -2
  10. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/data/static_data.py +10 -0
  11. greenpick-0.2.0/src/greenpick/data/typical.json +3692 -0
  12. greenpick-0.2.0/src/greenpick/data/typical_repository.py +56 -0
  13. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/models.py +5 -1
  14. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/server.py +21 -17
  15. greenpick-0.2.0/src/greenpick/services/data_hint.py +170 -0
  16. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/services/forecast_service.py +28 -7
  17. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/services/locality_service.py +1 -0
  18. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/services/scheduling_service.py +54 -12
  19. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/services/serving_service.py +2 -0
  20. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/services/training_service.py +11 -6
  21. {greenpick-0.1.7 → greenpick-0.2.0/src/greenpick.egg-info}/PKG-INFO +27 -18
  22. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick.egg-info/SOURCES.txt +3 -0
  23. {greenpick-0.1.7 → greenpick-0.2.0}/tests/test_services.py +1546 -1317
  24. {greenpick-0.1.7 → greenpick-0.2.0}/LICENSE +0 -0
  25. {greenpick-0.1.7 → greenpick-0.2.0}/setup.cfg +0 -0
  26. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/__main__.py +0 -0
  27. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/config.py +0 -0
  28. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/data/_net.py +0 -0
  29. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/data/emission_factors.json +0 -0
  30. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/data/gpu_power.json +0 -0
  31. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/data/inference_energy.json +0 -0
  32. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/data/instance_families.json +0 -0
  33. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/data/mock_repository.py +0 -0
  34. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/data/network.json +0 -0
  35. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/data/power.json +0 -0
  36. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/data/pricing.json +0 -0
  37. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/data/regions.json +0 -0
  38. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/data/repository.py +0 -0
  39. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/services/__init__.py +0 -0
  40. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/services/carbon_service.py +0 -0
  41. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/services/cost_service.py +0 -0
  42. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/services/inference_service.py +0 -0
  43. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/services/instance_service.py +0 -0
  44. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/services/latency_service.py +0 -0
  45. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/services/metric_scaffold_service.py +0 -0
  46. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/services/recommender.py +0 -0
  47. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/services/residency_service.py +0 -0
  48. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick/services/rightsizing_service.py +0 -0
  49. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick.egg-info/dependency_links.txt +0 -0
  50. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick.egg-info/entry_points.txt +0 -0
  51. {greenpick-0.1.7 → greenpick-0.2.0}/src/greenpick.egg-info/requires.txt +0 -0
  52. {greenpick-0.1.7 → greenpick-0.2.0}/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.7
3
+ Version: 0.2.0
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
@@ -55,7 +55,8 @@ seconds. (It responds when you ask; auto-nudging on infra-code changes is on the
55
55
  > **Status: working — 15 tools.** Live carbon for US (EIA-930) and EU (ENTSO-E), cross-cloud cost
56
56
  > parity (Vantage-refreshed), per-axis R/Y/G ratings, carbon-aware scheduling + day-ahead forecast,
57
57
  > GPU-training **and LLM-inference** carbon, right-sizing, serving-pattern and data-locality/residency
58
- > checks — all shipped. Falls back to clearly-labelled mock data offline or without API keys.
58
+ > checks — all shipped. Offline or without API keys, falls back to clearly-labelled *typical* values:
59
+ > this month's average for each grid, from a year of real data.
59
60
 
60
61
  <p align="center">
61
62
  <img src="https://raw.githubusercontent.com/bithug01/greenpick/master/docs/img/region-carbon-30x.svg" alt="Same H100 training run, ~30× the carbon depending only on region (Stockholm vs Sydney)" width="560" />
@@ -74,7 +75,7 @@ tradeoff) while you're still in the editor, before you commit.*
74
75
  - ⚡ **GPU training & LLM inference carbon** — estimate a run's footprint and the greener *where* + *when*; the size/placement choices that actually move the number.
75
76
  - 🌍 **Greener region picks** — ranked by carbon, with the cost + latency tradeoff shown, not hidden.
76
77
  - 🛡 **Backfire guards** — flags when a greener move loses to egress, or crosses a data-residency line.
77
- - 🔌 **Live US + EU grid data**, honest mock fallback — **no API keys required to start**.
78
+ - 🔌 **Live US + EU grid data**, with a realistic fallback built from a year of real data — **no API keys required to start**.
78
79
 
79
80
  ## Contents
80
81
 
@@ -186,8 +187,12 @@ See `docs/user-questions-backlog.md`.
186
187
 
187
188
  - **EIA-930** (US balancing authorities) and **ENTSO-E** (EU zones) — live grid carbon,
188
189
  auto-routed by region. Both also drive `cleanest_hours_to_run` from recent hourly history.
189
- - **Mock** — deterministic, offline default; clearly labelled `quality: "mock"` so nothing is
190
- mistaken for a real reading. Used wherever a key is missing or a region isn't covered.
190
+ - **Typical** — the offline default and fallback: for each region, this calendar month's average
191
+ intensity and average hour-of-day profile (UTC), computed from 12 months of real hourly EIA-930 /
192
+ ENTSO-E data (`data/typical.json`, refreshed by `scripts/refresh_typical.py`). Labelled
193
+ `quality: "typical"` — a realistic norm, never presented as a current reading.
194
+ - **Mock** — a hand-set illustrative value, labelled `quality: "mock"`, used only for regions no free
195
+ source covers (e.g. Sydney).
191
196
  - Cost: a **dated static pricing table** (`data/pricing.json`), refreshed from live Vantage
192
197
  pricing via `scripts/refresh_pricing.py` using one matched method across all three clouds.
193
198
  - Instance-family efficiency: directional perf-per-watt multipliers (`data/instance_families.json`).
@@ -195,10 +200,10 @@ See `docs/user-questions-backlog.md`.
195
200
 
196
201
  Set `EIA_API_KEY` and `ENTSOE_TOKEN` (both free) for live carbon — see
197
202
  [Turn on live data](#turn-on-live-data); `GREENPICK_CARBON_SOURCE` selects the source (`auto` routes
198
- US→EIA, EU→ENTSO-E, else mock).
203
+ US→EIA, EU→ENTSO-E, falling back to typical).
199
204
 
200
- The data layer is a swappable repository (`MOCK` by default, works offline). Nothing here invents
201
- numbers: the LLM writes the *rationale*, the code supplies the *figures*.
205
+ The data layer is a swappable repository (typical values by default, works offline). Nothing here
206
+ invents numbers: the LLM writes the *rationale*, the code supplies the *figures*.
202
207
 
203
208
  ## Honest limitations (shipped in the tool itself)
204
209
 
@@ -220,7 +225,7 @@ Or from source, for development (editable install + tests):
220
225
  git clone https://github.com/bithug01/greenpick.git
221
226
  cd greenpick
222
227
  pip install -e ".[dev]"
223
- cp .env.example .env # defaults to mock data — works offline
228
+ cp .env.example .env # defaults to typical values — works offline
224
229
  python -m greenpick # run the MCP server (stdio)
225
230
  pytest # run tests
226
231
  ```
@@ -230,7 +235,7 @@ Connect it to an MCP client (e.g. Kiro / Claude Code / Cursor) by pointing the c
230
235
 
231
236
  ### Turn on live data
232
237
 
233
- GreenPick runs on labelled mock data until you give it two free keys:
238
+ GreenPick runs on labelled typical values until you give it two free keys:
234
239
  [EIA](https://www.eia.gov/opendata/register.php) (US grid) and
235
240
  [ENTSO-E](https://transparency.entsoe.eu/) (EU grid). Pass them as environment variables.
236
241
  (A `.env` file works for a source checkout; if you installed with `pip install greenpick`, use one
@@ -289,15 +294,18 @@ with open("greenpick-server.log", "w") as errlog:
289
294
  Top-level `await` works in Jupyter. In a plain script, put the `with` block inside an `async def`
290
295
  and run it with `asyncio.run(...)`.
291
296
 
292
- Every carbon figure includes a `quality` field: `live` means real grid data, `mock` means a
293
- fallback value. Check it to confirm live data is on.
297
+ Every carbon figure includes a `quality` field: `live` means a current reading, `typical` means
298
+ this month's average from last year's real data, and `mock` means a hand-set value for a region
299
+ with no real data. Check it to confirm live data is on. A result that isn't live also includes a
300
+ `live_data_hint` saying why (no key set, the source didn't respond, or no free source covers that
301
+ region) and what to do about it.
294
302
 
295
303
  ## FAQ
296
304
 
297
- **Do I need API keys?** No — GreenPick runs on clearly-labelled mock data out of the box. For *live*
305
+ **Do I need API keys?** No — out of the box GreenPick uses *typical* values: each grid's average for
306
+ the current month, from a year of real hourly data, labelled `quality: "typical"`. For *live*
298
307
  carbon, set two free keys (`EIA_API_KEY` for US, `ENTSOE_TOKEN` for EU) and
299
- `GREENPICK_CARBON_SOURCE=auto` — see [Turn on live data](#turn-on-live-data). Without them it still
300
- works; it just says `quality: "mock"`.
308
+ `GREENPICK_CARBON_SOURCE=auto` — see [Turn on live data](#turn-on-live-data).
301
309
 
302
310
  **Why "directional" and not audited numbers?** Because it's for a *design-time decision*, not a
303
311
  compliance report. "Stockholm is ~30× cleaner than Sydney" is what you need to pick a region in
@@ -313,9 +321,10 @@ your workload leans on right now. GreenPick is deliberately location-based becau
313
321
  *placement/timing* decision actually affects — you can't change the provider's annual REC portfolio,
314
322
  but you *can* choose a cleaner grid or a cleaner hour. We ignore RECs by design, not oversight.
315
323
 
316
- **What happens for a region with no live data?** It falls back to the recent historical pattern or
317
- to mock, and labels which — it never presents a fallback as a live reading. Forecast likewise degrades
318
- to historical (labelled) where no day-ahead feed exists (e.g. US).
324
+ **What happens for a region with no live data?** It falls back to that region's typical value for
325
+ the month (or, for a region no free source covers, a hand-set mock value), and labels which — it
326
+ never presents a fallback as a live reading. Forecast likewise degrades to the recent history, or
327
+ the typical day, where no day-ahead feed exists (e.g. US), labelled as such.
319
328
 
320
329
  **Can it rank Claude vs GPT vs Llama for carbon?** No, by design. `estimate_inference_carbon` works by
321
330
  model *size class* (self-hosted-friendly), not a named-model leaderboard — vendors don't disclose the
@@ -25,7 +25,8 @@ seconds. (It responds when you ask; auto-nudging on infra-code changes is on the
25
25
  > **Status: working — 15 tools.** Live carbon for US (EIA-930) and EU (ENTSO-E), cross-cloud cost
26
26
  > parity (Vantage-refreshed), per-axis R/Y/G ratings, carbon-aware scheduling + day-ahead forecast,
27
27
  > GPU-training **and LLM-inference** carbon, right-sizing, serving-pattern and data-locality/residency
28
- > checks — all shipped. Falls back to clearly-labelled mock data offline or without API keys.
28
+ > checks — all shipped. Offline or without API keys, falls back to clearly-labelled *typical* values:
29
+ > this month's average for each grid, from a year of real data.
29
30
 
30
31
  <p align="center">
31
32
  <img src="https://raw.githubusercontent.com/bithug01/greenpick/master/docs/img/region-carbon-30x.svg" alt="Same H100 training run, ~30× the carbon depending only on region (Stockholm vs Sydney)" width="560" />
@@ -44,7 +45,7 @@ tradeoff) while you're still in the editor, before you commit.*
44
45
  - ⚡ **GPU training & LLM inference carbon** — estimate a run's footprint and the greener *where* + *when*; the size/placement choices that actually move the number.
45
46
  - 🌍 **Greener region picks** — ranked by carbon, with the cost + latency tradeoff shown, not hidden.
46
47
  - 🛡 **Backfire guards** — flags when a greener move loses to egress, or crosses a data-residency line.
47
- - 🔌 **Live US + EU grid data**, honest mock fallback — **no API keys required to start**.
48
+ - 🔌 **Live US + EU grid data**, with a realistic fallback built from a year of real data — **no API keys required to start**.
48
49
 
49
50
  ## Contents
50
51
 
@@ -156,8 +157,12 @@ See `docs/user-questions-backlog.md`.
156
157
 
157
158
  - **EIA-930** (US balancing authorities) and **ENTSO-E** (EU zones) — live grid carbon,
158
159
  auto-routed by region. Both also drive `cleanest_hours_to_run` from recent hourly history.
159
- - **Mock** — deterministic, offline default; clearly labelled `quality: "mock"` so nothing is
160
- mistaken for a real reading. Used wherever a key is missing or a region isn't covered.
160
+ - **Typical** — the offline default and fallback: for each region, this calendar month's average
161
+ intensity and average hour-of-day profile (UTC), computed from 12 months of real hourly EIA-930 /
162
+ ENTSO-E data (`data/typical.json`, refreshed by `scripts/refresh_typical.py`). Labelled
163
+ `quality: "typical"` — a realistic norm, never presented as a current reading.
164
+ - **Mock** — a hand-set illustrative value, labelled `quality: "mock"`, used only for regions no free
165
+ source covers (e.g. Sydney).
161
166
  - Cost: a **dated static pricing table** (`data/pricing.json`), refreshed from live Vantage
162
167
  pricing via `scripts/refresh_pricing.py` using one matched method across all three clouds.
163
168
  - Instance-family efficiency: directional perf-per-watt multipliers (`data/instance_families.json`).
@@ -165,10 +170,10 @@ See `docs/user-questions-backlog.md`.
165
170
 
166
171
  Set `EIA_API_KEY` and `ENTSOE_TOKEN` (both free) for live carbon — see
167
172
  [Turn on live data](#turn-on-live-data); `GREENPICK_CARBON_SOURCE` selects the source (`auto` routes
168
- US→EIA, EU→ENTSO-E, else mock).
173
+ US→EIA, EU→ENTSO-E, falling back to typical).
169
174
 
170
- The data layer is a swappable repository (`MOCK` by default, works offline). Nothing here invents
171
- numbers: the LLM writes the *rationale*, the code supplies the *figures*.
175
+ The data layer is a swappable repository (typical values by default, works offline). Nothing here
176
+ invents numbers: the LLM writes the *rationale*, the code supplies the *figures*.
172
177
 
173
178
  ## Honest limitations (shipped in the tool itself)
174
179
 
@@ -190,7 +195,7 @@ Or from source, for development (editable install + tests):
190
195
  git clone https://github.com/bithug01/greenpick.git
191
196
  cd greenpick
192
197
  pip install -e ".[dev]"
193
- cp .env.example .env # defaults to mock data — works offline
198
+ cp .env.example .env # defaults to typical values — works offline
194
199
  python -m greenpick # run the MCP server (stdio)
195
200
  pytest # run tests
196
201
  ```
@@ -200,7 +205,7 @@ Connect it to an MCP client (e.g. Kiro / Claude Code / Cursor) by pointing the c
200
205
 
201
206
  ### Turn on live data
202
207
 
203
- GreenPick runs on labelled mock data until you give it two free keys:
208
+ GreenPick runs on labelled typical values until you give it two free keys:
204
209
  [EIA](https://www.eia.gov/opendata/register.php) (US grid) and
205
210
  [ENTSO-E](https://transparency.entsoe.eu/) (EU grid). Pass them as environment variables.
206
211
  (A `.env` file works for a source checkout; if you installed with `pip install greenpick`, use one
@@ -259,15 +264,18 @@ with open("greenpick-server.log", "w") as errlog:
259
264
  Top-level `await` works in Jupyter. In a plain script, put the `with` block inside an `async def`
260
265
  and run it with `asyncio.run(...)`.
261
266
 
262
- Every carbon figure includes a `quality` field: `live` means real grid data, `mock` means a
263
- fallback value. Check it to confirm live data is on.
267
+ Every carbon figure includes a `quality` field: `live` means a current reading, `typical` means
268
+ this month's average from last year's real data, and `mock` means a hand-set value for a region
269
+ with no real data. Check it to confirm live data is on. A result that isn't live also includes a
270
+ `live_data_hint` saying why (no key set, the source didn't respond, or no free source covers that
271
+ region) and what to do about it.
264
272
 
265
273
  ## FAQ
266
274
 
267
- **Do I need API keys?** No — GreenPick runs on clearly-labelled mock data out of the box. For *live*
275
+ **Do I need API keys?** No — out of the box GreenPick uses *typical* values: each grid's average for
276
+ the current month, from a year of real hourly data, labelled `quality: "typical"`. For *live*
268
277
  carbon, set two free keys (`EIA_API_KEY` for US, `ENTSOE_TOKEN` for EU) and
269
- `GREENPICK_CARBON_SOURCE=auto` — see [Turn on live data](#turn-on-live-data). Without them it still
270
- works; it just says `quality: "mock"`.
278
+ `GREENPICK_CARBON_SOURCE=auto` — see [Turn on live data](#turn-on-live-data).
271
279
 
272
280
  **Why "directional" and not audited numbers?** Because it's for a *design-time decision*, not a
273
281
  compliance report. "Stockholm is ~30× cleaner than Sydney" is what you need to pick a region in
@@ -283,9 +291,10 @@ your workload leans on right now. GreenPick is deliberately location-based becau
283
291
  *placement/timing* decision actually affects — you can't change the provider's annual REC portfolio,
284
292
  but you *can* choose a cleaner grid or a cleaner hour. We ignore RECs by design, not oversight.
285
293
 
286
- **What happens for a region with no live data?** It falls back to the recent historical pattern or
287
- to mock, and labels which — it never presents a fallback as a live reading. Forecast likewise degrades
288
- to historical (labelled) where no day-ahead feed exists (e.g. US).
294
+ **What happens for a region with no live data?** It falls back to that region's typical value for
295
+ the month (or, for a region no free source covers, a hand-set mock value), and labels which — it
296
+ never presents a fallback as a live reading. Forecast likewise degrades to the recent history, or
297
+ the typical day, where no day-ahead feed exists (e.g. US), labelled as such.
289
298
 
290
299
  **Can it rank Claude vs GPT vs Llama for carbon?** No, by design. `estimate_inference_carbon` works by
291
300
  model *size class* (self-hosted-friendly), not a named-model leaderboard — vendors don't disclose the
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "greenpick"
3
- version = "0.1.7"
3
+ version = "0.2.0"
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"
7
+ __version__ = "0.2.0"
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.
@@ -7,7 +7,8 @@ from __future__ import annotations
7
7
 
8
8
  from ..config import SETTINGS
9
9
  from .repository import CarbonRepository
10
- from .mock_repository import MockCarbonRepository
10
+ from .mock_repository import MockCarbonRepository # noqa: F401 (re-exported; tests use it)
11
+ from .typical_repository import TypicalCarbonRepository
11
12
 
12
13
 
13
14
  def get_repository() -> CarbonRepository:
@@ -25,4 +26,5 @@ def get_repository() -> CarbonRepository:
25
26
  # Route per region: EIA for US (eia_ba), ENTSO-E for EU (entsoe_eic), else mock.
26
27
  from .auto_repository import AutoRepository
27
28
  return AutoRepository(SETTINGS.eia_api_key, SETTINGS.entsoe_token, SETTINGS.cache_ttl_seconds)
28
- return MockCarbonRepository()
29
+ # Default and 'mock' setting: typical monthly values from real data, mock only where uncovered.
30
+ return TypicalCarbonRepository()
@@ -9,7 +9,7 @@ Lets a single GREENPICK_CARBON_SOURCE=auto give live data on both continents.
9
9
  from __future__ import annotations
10
10
 
11
11
  from ..models import CarbonReading, Provider, HourlyCarbon, ForecastHour
12
- from .mock_repository import MockCarbonRepository
12
+ from .typical_repository import TypicalCarbonRepository
13
13
  from .eia_repository import EIARepository
14
14
  from .entsoe_repository import ENTSOERepository
15
15
  from .static_data import region_meta
@@ -19,7 +19,7 @@ class AutoRepository:
19
19
  def __init__(self, eia_key: str, entsoe_token: str, cache_ttl_seconds: int = 1800) -> None:
20
20
  self._eia = EIARepository(eia_key, cache_ttl_seconds)
21
21
  self._entsoe = ENTSOERepository(entsoe_token, cache_ttl_seconds)
22
- self._mock = MockCarbonRepository()
22
+ self._mock = TypicalCarbonRepository()
23
23
 
24
24
  async def carbon_now(self, provider: Provider, region: str) -> CarbonReading:
25
25
  meta = region_meta(provider, region) or {}
@@ -22,7 +22,7 @@ from datetime import datetime, timezone
22
22
  import httpx
23
23
 
24
24
  from ..models import CarbonReading, Provider, HourlyCarbon
25
- from .mock_repository import MockCarbonRepository
25
+ from .typical_repository import TypicalCarbonRepository
26
26
  from .static_data import region_meta, emission_factors
27
27
  from ._net import REQUEST_TIMEOUT, REQUEST_DEADLINE_S, describe_error
28
28
 
@@ -36,7 +36,7 @@ class EIARepository:
36
36
  self._key = api_key
37
37
  self._ttl = cache_ttl_seconds
38
38
  self._cache: dict[str, tuple[float, CarbonReading]] = {} # BA -> (fetched_at, reading)
39
- self._fallback = MockCarbonRepository()
39
+ self._fallback = TypicalCarbonRepository()
40
40
  self._warned_no_key = False
41
41
 
42
42
  async def carbon_now(self, provider: Provider, region: str) -> CarbonReading:
@@ -21,7 +21,7 @@ from datetime import datetime, timezone
21
21
  import httpx
22
22
 
23
23
  from ..models import CarbonReading, Provider, HourlyCarbon
24
- from .mock_repository import MockCarbonRepository
24
+ from .typical_repository import TypicalCarbonRepository
25
25
  from .static_data import region_meta
26
26
  from ._net import REQUEST_TIMEOUT, REQUEST_DEADLINE_S, describe_error
27
27
 
@@ -35,7 +35,7 @@ class ElectricityMapsRepository:
35
35
  self._token = token
36
36
  self._ttl = cache_ttl_seconds
37
37
  self._cache: dict[str, tuple[float, CarbonReading]] = {} # zone -> (fetched_at, reading)
38
- self._fallback = MockCarbonRepository()
38
+ self._fallback = TypicalCarbonRepository()
39
39
  self._token_rejected = False # once a 401/403 is seen, stop calling with a bad key
40
40
  self._warned_no_token = False
41
41
 
@@ -24,7 +24,7 @@ from xml.etree import ElementTree as ET
24
24
  import httpx
25
25
 
26
26
  from ..models import CarbonReading, Provider, HourlyCarbon, ForecastHour
27
- from .mock_repository import MockCarbonRepository
27
+ from .typical_repository import TypicalCarbonRepository
28
28
  from .static_data import region_meta, emission_factors
29
29
  from ._net import REQUEST_TIMEOUT, REQUEST_DEADLINE_S, describe_error
30
30
 
@@ -75,7 +75,7 @@ class ENTSOERepository:
75
75
  self._token = token
76
76
  self._ttl = cache_ttl_seconds
77
77
  self._cache: dict[str, tuple[float, CarbonReading]] = {} # EIC -> (fetched_at, reading)
78
- self._fallback = MockCarbonRepository()
78
+ self._fallback = TypicalCarbonRepository()
79
79
  self._warned_no_token = False
80
80
  self._down_until: dict[str, float] = {} # EIC -> epoch secs; skip the zone until then
81
81
 
@@ -66,6 +66,16 @@ def resolve_size_class(name: str) -> tuple[str, dict] | None:
66
66
  return None
67
67
 
68
68
 
69
+ @lru_cache(maxsize=1)
70
+ def typical() -> dict:
71
+ """Per-region, per-calendar-month typical carbon from a year of real data (the fallback).
72
+ Empty if the file is absent, so GreenPick still runs (falling back to mock)."""
73
+ path = _DATA_DIR / "typical.json"
74
+ if not path.exists():
75
+ return {}
76
+ return json.loads(path.read_text(encoding="utf-8"))
77
+
78
+
69
79
  @lru_cache(maxsize=1)
70
80
  def gpu_power() -> dict:
71
81
  """Directional GPU/accelerator power model (TDP + utilization + host overhead) for training."""