gridhour 0.1.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 (39) hide show
  1. gridhour-0.1.0/CHANGELOG.md +22 -0
  2. gridhour-0.1.0/LICENSE +21 -0
  3. gridhour-0.1.0/MANIFEST.in +4 -0
  4. gridhour-0.1.0/PKG-INFO +189 -0
  5. gridhour-0.1.0/README.md +163 -0
  6. gridhour-0.1.0/SECURITY.md +65 -0
  7. gridhour-0.1.0/gridhour/__init__.py +3 -0
  8. gridhour-0.1.0/gridhour/__main__.py +3 -0
  9. gridhour-0.1.0/gridhour/app.py +224 -0
  10. gridhour-0.1.0/gridhour/canvas.py +108 -0
  11. gridhour-0.1.0/gridhour/cli.py +126 -0
  12. gridhour-0.1.0/gridhour/compose.py +561 -0
  13. gridhour-0.1.0/gridhour/grid.py +417 -0
  14. gridhour-0.1.0/gridhour/keys.py +143 -0
  15. gridhour-0.1.0/gridhour/output.py +131 -0
  16. gridhour-0.1.0/gridhour/plan.py +184 -0
  17. gridhour-0.1.0/gridhour/safe.py +67 -0
  18. gridhour-0.1.0/gridhour/state.py +104 -0
  19. gridhour-0.1.0/gridhour/themes.py +111 -0
  20. gridhour-0.1.0/gridhour.egg-info/PKG-INFO +189 -0
  21. gridhour-0.1.0/gridhour.egg-info/SOURCES.txt +37 -0
  22. gridhour-0.1.0/gridhour.egg-info/dependency_links.txt +1 -0
  23. gridhour-0.1.0/gridhour.egg-info/entry_points.txt +2 -0
  24. gridhour-0.1.0/gridhour.egg-info/top_level.txt +1 -0
  25. gridhour-0.1.0/pyproject.toml +71 -0
  26. gridhour-0.1.0/setup.cfg +4 -0
  27. gridhour-0.1.0/tests/conftest.py +61 -0
  28. gridhour-0.1.0/tests/fixtures/agile_c.json +1 -0
  29. gridhour-0.1.0/tests/fixtures/carbon_sw1a.json +1 -0
  30. gridhour-0.1.0/tests/fixtures/products.json +1 -0
  31. gridhour-0.1.0/tests/test_app.py +67 -0
  32. gridhour-0.1.0/tests/test_canvas.py +58 -0
  33. gridhour-0.1.0/tests/test_cli.py +52 -0
  34. gridhour-0.1.0/tests/test_compose.py +89 -0
  35. gridhour-0.1.0/tests/test_grid.py +107 -0
  36. gridhour-0.1.0/tests/test_keys.py +105 -0
  37. gridhour-0.1.0/tests/test_output.py +48 -0
  38. gridhour-0.1.0/tests/test_plan.py +104 -0
  39. gridhour-0.1.0/tests/test_security.py +325 -0
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes are listed here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
4
+ and the project uses [semantic versioning](https://semver.org/).
5
+
6
+ ## [Unreleased]
7
+
8
+ ## [0.1.0] - 2026-10-04
9
+
10
+ First release.
11
+
12
+ * 48 hour timeline of regional carbon intensity and Octopus Agile prices for a UK postcode.
13
+ * Best start time for each job (washing, dishwasher, EV charge, batch job, or your own), ranked by
14
+ carbon, price or both.
15
+ * Generation mix rows (wind, solar, gas, nuclear, ...) on the same time axis.
16
+ * `--line`, `--tmux`, `--watch` and `--json` for status bars and scripts.
17
+ * Six themes (carbon, daylight, slate, ember, mono, colorblind), 12/24 hour clock, compact layout
18
+ for small panes.
19
+ * Works offline from cache. Talks only to the two APIs over HTTPS.
20
+
21
+ [Unreleased]: https://github.com/777dimas/gridhour/compare/v0.1.0...HEAD
22
+ [0.1.0]: https://github.com/777dimas/gridhour/releases/tag/v0.1.0
gridhour-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 the gridhour authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ # The sdist carries what it takes to run the test suite (packagers do), not the screenshots or CI.
2
+ graft tests
3
+ include CHANGELOG.md SECURITY.md
4
+ global-exclude *.py[cod] __pycache__
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: gridhour
3
+ Version: 0.1.0
4
+ Summary: When British electricity is green and cheap: a 48 hour carbon intensity and Agile price timeline for the terminal
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/777dimas/gridhour
7
+ Project-URL: Issues, https://github.com/777dimas/gridhour/issues
8
+ Project-URL: Security, https://github.com/777dimas/gridhour/security/policy
9
+ Project-URL: Changelog, https://github.com/777dimas/gridhour/blob/main/CHANGELOG.md
10
+ Keywords: carbon intensity,octopus agile,electricity,national grid,tui,terminal,tmux
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Utilities
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Dynamic: license-file
26
+
27
+ # gridhour
28
+
29
+ [![CI](https://github.com/777dimas/gridhour/actions/workflows/ci.yml/badge.svg)](https://github.com/777dimas/gridhour/actions/workflows/ci.yml)
30
+ [![CodeQL](https://github.com/777dimas/gridhour/actions/workflows/codeql.yml/badge.svg)](https://github.com/777dimas/gridhour/actions/workflows/codeql.yml)
31
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/777dimas/gridhour/badge)](https://scorecard.dev/viewer/?uri=github.com/777dimas/gridhour)
32
+
33
+ When is British electricity green and cheap? gridhour shows the next 48 hours of carbon
34
+ intensity for your postcode and Octopus Agile prices on one timeline in the terminal, then
35
+ picks the best time to start the washing, charge the car or kick off a batch job.
36
+
37
+ ```console
38
+ $ gridhour --line
39
+ ⚡ 142g · 14p · green in 2h
40
+ ```
41
+
42
+ ![gridhour main view](https://raw.githubusercontent.com/777dimas/gridhour/main/docs/main.png)
43
+
44
+ ## What you see
45
+
46
+ * **The chart.** Carbon intensity (gCO₂/kWh) rises above the time axis, the Agile unit price
47
+ hangs below it. Green is clean or cheap, red is dirty or dear, blue is a plunge price where
48
+ Octopus pays you to use power. The hours already gone are dimmed.
49
+ * **Generation mix.** Wind, solar, gas, nuclear and the rest, as shares of your region's
50
+ supply, on the same time axis. Handy for seeing *why* tomorrow afternoon is clean.
51
+ * **Best time to run.** One row per job. Each row is shaded by how good every start time is,
52
+ and the bright block is the best window, with its average carbon and price next to it. The
53
+ status line tells you how much that saves against starting right now.
54
+ * **A cursor** runs straight down through all of it. Move it with the arrow keys to read any
55
+ half hour; `Enter` jumps to the selected job's best window.
56
+
57
+ Jobs are ranked by carbon, by price, or by both (each normalised over the 48 hours and
58
+ averaged). `w` switches between them.
59
+
60
+ | Scrubbed to tomorrow morning | Compact layout for a tmux split |
61
+ | --- | --- |
62
+ | ![scrubbing](https://raw.githubusercontent.com/777dimas/gridhour/main/docs/scrub.png) | ![compact](https://raw.githubusercontent.com/777dimas/gridhour/main/docs/compact.png) |
63
+
64
+ ## Install
65
+
66
+ You need Python 3.11 or newer, a terminal with truecolor and a UTF-8 locale. Linux and macOS;
67
+ on Windows use WSL.
68
+
69
+ ```sh
70
+ pipx install git+https://github.com/777dimas/gridhour
71
+ gridhour SW1A 1AA
72
+ ```
73
+
74
+ Once a release is on PyPI this becomes `pipx install gridhour`. From a clone there is nothing to
75
+ build: `python3 -m gridhour SW1A`.
76
+
77
+ The postcode you give is remembered, so after the first run plain `gridhour` is enough. Only
78
+ the first half (`SW1A`) is ever used or sent anywhere. Northern Ireland is not on the GB grid,
79
+ so BT postcodes have no forecast.
80
+
81
+ ## Usage
82
+
83
+ ```sh
84
+ gridhour # your saved postcode
85
+ gridhour M1 1AE # a new postcode (saved)
86
+ gridhour --region scotland # a region instead, for this run only
87
+ gridhour --mode green # rank by carbon only (also: cheap, both)
88
+ gridhour --no-prices # skip Agile, for people on a flat tariff
89
+ gridhour --at 2026-10-04T18:00Z # start with the cursor at a given time
90
+ gridhour --theme slate --12h
91
+ gridhour --reset # delete the saved postcode, jobs, settings and the cache
92
+ ```
93
+
94
+ Keys inside the app (`?` lists them all):
95
+
96
+ | Key | Action |
97
+ | --- | --- |
98
+ | `←` `→` | move the cursor 30 minutes, Shift or `H` `L` for 2 hours |
99
+ | `Home` `End` `r` | start and end of the forecast, back to now |
100
+ | `↑` `↓` | select a job |
101
+ | `+` `-` | make the selected job 30 minutes longer or shorter |
102
+ | `a` `d` | add a job (`Dryer 1h30`), delete the selected one |
103
+ | `Enter` | jump to the selected job's best window |
104
+ | `w` | rank by carbon, price, or both |
105
+ | `p` | change postcode |
106
+ | `m` `c` `T` `t` | mix rows, compact layout, theme, 12/24h |
107
+ | `R` | refetch now |
108
+ | `q` | quit |
109
+
110
+ Settings and jobs live in `~/.config/gridhour/config.json`.
111
+
112
+ ## Status bars and scripts
113
+
114
+ ```console
115
+ $ gridhour --line
116
+ ⚡ 142g · 14p · green in 2h
117
+
118
+ $ gridhour --line --best 3h
119
+ ⚡ 142g · 14p · 3h best 13:00 (in 4h30)
120
+
121
+ $ gridhour --tmux # same, with tmux colour codes for status-right
122
+ $ gridhour --watch # same, updating in place
123
+ $ gridhour --json | jq '.jobs[] | {name, start: .best.from}'
124
+ ```
125
+
126
+ "Green" means NESO's forecast grades the half hour as *low* or *very low* for your region.
127
+ If nothing in the next 48 hours makes that grade, the line says `greenest in 9h` instead.
128
+
129
+ `--json` gives the current half hour, the next green slot, the best window for every job (with
130
+ the carbon and price you would get starting now, for comparison) and the full 48 hour series.
131
+
132
+ A tmux example:
133
+
134
+ ```tmux
135
+ set -g status-right '#(gridhour --tmux) '
136
+ set -g status-interval 300
137
+ ```
138
+
139
+ ## Where the numbers come from
140
+
141
+ * **Carbon intensity** and the **generation mix**: the [Carbon Intensity API](https://carbonintensity.org.uk/)
142
+ from the National Energy System Operator (NESO), regional forecast by postcode. Free, no key,
143
+ CC BY 4.0.
144
+ * **Prices**: the public [Octopus Energy API](https://developer.octopus.energy/), the Agile
145
+ import tariff for your region, including VAT. Octopus publishes the next day's prices at about
146
+ 4pm, so before that the price half of the chart stops around 11pm tonight and the "cheap"
147
+ ranking only looks that far ahead.
148
+
149
+ These are forecasts. The carbon forecast for tomorrow afternoon can be off by a fair
150
+ margin; Agile prices, once published, are what you pay. gridhour is not affiliated with Octopus
151
+ Energy or NESO.
152
+
153
+ Responses are cached in `~/.cache/gridhour` for 30 minutes. If the network is down the app
154
+ keeps showing the cached data and says how old it is. `GRIDHOUR_OFFLINE=1` stops all network
155
+ access.
156
+
157
+ ## Security and privacy
158
+
159
+ gridhour talks to those two APIs over HTTPS and nothing else (redirects are refused), sends only
160
+ the first half of your postcode and your region letter, and has no runtime dependencies.
161
+ Responses are checked field by field before they are used or cached, and text from them never
162
+ reaches your terminal or tmux with escape sequences or format codes in it. Details and how to
163
+ report a problem: [SECURITY.md](https://github.com/777dimas/gridhour/blob/main/SECURITY.md).
164
+
165
+ ## Development
166
+
167
+ ```sh
168
+ python3 -m venv .venv && . .venv/bin/activate
169
+ pip install --require-hashes -r requirements/dev.txt && pip install --no-deps -e .
170
+ pytest
171
+ ```
172
+
173
+ The tests replay real API responses from `tests/fixtures/`; sockets are blocked while they run.
174
+ More in [CONTRIBUTING.md](https://github.com/777dimas/gridhour/blob/main/CONTRIBUTING.md).
175
+
176
+ | Module | What it does |
177
+ | --- | --- |
178
+ | `grid.py` | the two APIs, regions and postcodes, the cache |
179
+ | `plan.py` | scoring half hours, best windows, "green in 2h" |
180
+ | `compose.py` | turns the state into a frame |
181
+ | `canvas.py` | character grid with colours, rendered to ANSI |
182
+ | `safe.py` | cleaning outside text, private atomic file writes |
183
+ | `app.py`, `keys.py` | the terminal loop and every key |
184
+ | `output.py` | `--line`, `--tmux`, `--json`, `--watch` |
185
+ | `state.py`, `themes.py`, `cli.py` | settings, colours, arguments |
186
+
187
+ ## Licence
188
+
189
+ MIT, see [LICENSE](https://github.com/777dimas/gridhour/blob/main/LICENSE).
@@ -0,0 +1,163 @@
1
+ # gridhour
2
+
3
+ [![CI](https://github.com/777dimas/gridhour/actions/workflows/ci.yml/badge.svg)](https://github.com/777dimas/gridhour/actions/workflows/ci.yml)
4
+ [![CodeQL](https://github.com/777dimas/gridhour/actions/workflows/codeql.yml/badge.svg)](https://github.com/777dimas/gridhour/actions/workflows/codeql.yml)
5
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/777dimas/gridhour/badge)](https://scorecard.dev/viewer/?uri=github.com/777dimas/gridhour)
6
+
7
+ When is British electricity green and cheap? gridhour shows the next 48 hours of carbon
8
+ intensity for your postcode and Octopus Agile prices on one timeline in the terminal, then
9
+ picks the best time to start the washing, charge the car or kick off a batch job.
10
+
11
+ ```console
12
+ $ gridhour --line
13
+ ⚡ 142g · 14p · green in 2h
14
+ ```
15
+
16
+ ![gridhour main view](https://raw.githubusercontent.com/777dimas/gridhour/main/docs/main.png)
17
+
18
+ ## What you see
19
+
20
+ * **The chart.** Carbon intensity (gCO₂/kWh) rises above the time axis, the Agile unit price
21
+ hangs below it. Green is clean or cheap, red is dirty or dear, blue is a plunge price where
22
+ Octopus pays you to use power. The hours already gone are dimmed.
23
+ * **Generation mix.** Wind, solar, gas, nuclear and the rest, as shares of your region's
24
+ supply, on the same time axis. Handy for seeing *why* tomorrow afternoon is clean.
25
+ * **Best time to run.** One row per job. Each row is shaded by how good every start time is,
26
+ and the bright block is the best window, with its average carbon and price next to it. The
27
+ status line tells you how much that saves against starting right now.
28
+ * **A cursor** runs straight down through all of it. Move it with the arrow keys to read any
29
+ half hour; `Enter` jumps to the selected job's best window.
30
+
31
+ Jobs are ranked by carbon, by price, or by both (each normalised over the 48 hours and
32
+ averaged). `w` switches between them.
33
+
34
+ | Scrubbed to tomorrow morning | Compact layout for a tmux split |
35
+ | --- | --- |
36
+ | ![scrubbing](https://raw.githubusercontent.com/777dimas/gridhour/main/docs/scrub.png) | ![compact](https://raw.githubusercontent.com/777dimas/gridhour/main/docs/compact.png) |
37
+
38
+ ## Install
39
+
40
+ You need Python 3.11 or newer, a terminal with truecolor and a UTF-8 locale. Linux and macOS;
41
+ on Windows use WSL.
42
+
43
+ ```sh
44
+ pipx install git+https://github.com/777dimas/gridhour
45
+ gridhour SW1A 1AA
46
+ ```
47
+
48
+ Once a release is on PyPI this becomes `pipx install gridhour`. From a clone there is nothing to
49
+ build: `python3 -m gridhour SW1A`.
50
+
51
+ The postcode you give is remembered, so after the first run plain `gridhour` is enough. Only
52
+ the first half (`SW1A`) is ever used or sent anywhere. Northern Ireland is not on the GB grid,
53
+ so BT postcodes have no forecast.
54
+
55
+ ## Usage
56
+
57
+ ```sh
58
+ gridhour # your saved postcode
59
+ gridhour M1 1AE # a new postcode (saved)
60
+ gridhour --region scotland # a region instead, for this run only
61
+ gridhour --mode green # rank by carbon only (also: cheap, both)
62
+ gridhour --no-prices # skip Agile, for people on a flat tariff
63
+ gridhour --at 2026-10-04T18:00Z # start with the cursor at a given time
64
+ gridhour --theme slate --12h
65
+ gridhour --reset # delete the saved postcode, jobs, settings and the cache
66
+ ```
67
+
68
+ Keys inside the app (`?` lists them all):
69
+
70
+ | Key | Action |
71
+ | --- | --- |
72
+ | `←` `→` | move the cursor 30 minutes, Shift or `H` `L` for 2 hours |
73
+ | `Home` `End` `r` | start and end of the forecast, back to now |
74
+ | `↑` `↓` | select a job |
75
+ | `+` `-` | make the selected job 30 minutes longer or shorter |
76
+ | `a` `d` | add a job (`Dryer 1h30`), delete the selected one |
77
+ | `Enter` | jump to the selected job's best window |
78
+ | `w` | rank by carbon, price, or both |
79
+ | `p` | change postcode |
80
+ | `m` `c` `T` `t` | mix rows, compact layout, theme, 12/24h |
81
+ | `R` | refetch now |
82
+ | `q` | quit |
83
+
84
+ Settings and jobs live in `~/.config/gridhour/config.json`.
85
+
86
+ ## Status bars and scripts
87
+
88
+ ```console
89
+ $ gridhour --line
90
+ ⚡ 142g · 14p · green in 2h
91
+
92
+ $ gridhour --line --best 3h
93
+ ⚡ 142g · 14p · 3h best 13:00 (in 4h30)
94
+
95
+ $ gridhour --tmux # same, with tmux colour codes for status-right
96
+ $ gridhour --watch # same, updating in place
97
+ $ gridhour --json | jq '.jobs[] | {name, start: .best.from}'
98
+ ```
99
+
100
+ "Green" means NESO's forecast grades the half hour as *low* or *very low* for your region.
101
+ If nothing in the next 48 hours makes that grade, the line says `greenest in 9h` instead.
102
+
103
+ `--json` gives the current half hour, the next green slot, the best window for every job (with
104
+ the carbon and price you would get starting now, for comparison) and the full 48 hour series.
105
+
106
+ A tmux example:
107
+
108
+ ```tmux
109
+ set -g status-right '#(gridhour --tmux) '
110
+ set -g status-interval 300
111
+ ```
112
+
113
+ ## Where the numbers come from
114
+
115
+ * **Carbon intensity** and the **generation mix**: the [Carbon Intensity API](https://carbonintensity.org.uk/)
116
+ from the National Energy System Operator (NESO), regional forecast by postcode. Free, no key,
117
+ CC BY 4.0.
118
+ * **Prices**: the public [Octopus Energy API](https://developer.octopus.energy/), the Agile
119
+ import tariff for your region, including VAT. Octopus publishes the next day's prices at about
120
+ 4pm, so before that the price half of the chart stops around 11pm tonight and the "cheap"
121
+ ranking only looks that far ahead.
122
+
123
+ These are forecasts. The carbon forecast for tomorrow afternoon can be off by a fair
124
+ margin; Agile prices, once published, are what you pay. gridhour is not affiliated with Octopus
125
+ Energy or NESO.
126
+
127
+ Responses are cached in `~/.cache/gridhour` for 30 minutes. If the network is down the app
128
+ keeps showing the cached data and says how old it is. `GRIDHOUR_OFFLINE=1` stops all network
129
+ access.
130
+
131
+ ## Security and privacy
132
+
133
+ gridhour talks to those two APIs over HTTPS and nothing else (redirects are refused), sends only
134
+ the first half of your postcode and your region letter, and has no runtime dependencies.
135
+ Responses are checked field by field before they are used or cached, and text from them never
136
+ reaches your terminal or tmux with escape sequences or format codes in it. Details and how to
137
+ report a problem: [SECURITY.md](https://github.com/777dimas/gridhour/blob/main/SECURITY.md).
138
+
139
+ ## Development
140
+
141
+ ```sh
142
+ python3 -m venv .venv && . .venv/bin/activate
143
+ pip install --require-hashes -r requirements/dev.txt && pip install --no-deps -e .
144
+ pytest
145
+ ```
146
+
147
+ The tests replay real API responses from `tests/fixtures/`; sockets are blocked while they run.
148
+ More in [CONTRIBUTING.md](https://github.com/777dimas/gridhour/blob/main/CONTRIBUTING.md).
149
+
150
+ | Module | What it does |
151
+ | --- | --- |
152
+ | `grid.py` | the two APIs, regions and postcodes, the cache |
153
+ | `plan.py` | scoring half hours, best windows, "green in 2h" |
154
+ | `compose.py` | turns the state into a frame |
155
+ | `canvas.py` | character grid with colours, rendered to ANSI |
156
+ | `safe.py` | cleaning outside text, private atomic file writes |
157
+ | `app.py`, `keys.py` | the terminal loop and every key |
158
+ | `output.py` | `--line`, `--tmux`, `--json`, `--watch` |
159
+ | `state.py`, `themes.py`, `cli.py` | settings, colours, arguments |
160
+
161
+ ## Licence
162
+
163
+ MIT, see [LICENSE](https://github.com/777dimas/gridhour/blob/main/LICENSE).
@@ -0,0 +1,65 @@
1
+ # Security policy
2
+
3
+ ## Reporting a problem
4
+
5
+ Please report security problems privately through
6
+ [GitHub's private vulnerability reporting](https://github.com/777dimas/gridhour/security/advisories/new),
7
+ not in a public issue. You should get a reply within 5 working days. Once a fix is out I credit the
8
+ reporter in the advisory and the changelog, unless you ask me not to.
9
+
10
+ ## Supported versions
11
+
12
+ Only the latest release gets fixes.
13
+
14
+ ## What gridhour does that matters for security
15
+
16
+ **Network.** HTTPS GET requests to two hosts and nothing else: `api.carbonintensity.org.uk` (with the
17
+ first half of your postcode, for example `SW1A`) and `api.octopus.energy` (with your region letter).
18
+
19
+ * `grid.http_json` refuses any other scheme or host, refuses redirects, and checks the final URL.
20
+ * A response may be 4 MB at most. Once connected, the whole body must arrive within 25 seconds,
21
+ however slowly it trickles in (connecting and DNS have their own 12 second timeout).
22
+ * No API keys, no accounts, no telemetry. `GRIDHOUR_OFFLINE=1` turns the network off.
23
+
24
+ **Untrusted input.** API responses, the cache and the config file are all treated as hostile.
25
+
26
+ * Every field is type- and range-checked before use: numbers must be finite and plausible
27
+ (booleans and strings are refused), fuel and index names must be known values, times must be
28
+ ISO 8601. A bad row costs that half hour, not the whole forecast.
29
+ * A response is checked before it is cached, so a broken one is never stored. A cache entry that
30
+ no longer passes the checks, or carries a timestamp from the future, is ignored.
31
+ * Text that is shown (the region name, job names) loses control characters, bidi and zero-width
32
+ characters, combining marks and double-width characters, so it can neither send escape
33
+ sequences to your terminal nor shift or reorder the screen. In `--tmux` output every `#` is
34
+ doubled, so no text can become a tmux format or a `#(command)`. `--json` output is pure ASCII.
35
+ * Error messages never repeat response content.
36
+ * The Agile product code from the API must match `AGILE-[A-Z0-9-]+` exactly, and the region letter
37
+ must be one of the fourteen known ones, before either goes into a URL.
38
+ * Malformed responses, config files and arguments produce an error message, not a crash. The
39
+ test suite in `tests/test_security.py` holds the cases.
40
+
41
+ **Files.** `~/.config/gridhour/config.json` (postcode outward code, jobs, theme) and
42
+ `~/.cache/gridhour/*.json` (checked API responses). Both directories are kept at `0700` (tightened
43
+ if they already exist with looser modes) and the files at `0600`. Writes go to a fresh temporary
44
+ file created with `O_EXCL` and are renamed into place, so a planted symlink is never followed.
45
+ `gridhour --reset` deletes the config and the cache.
46
+
47
+ **Dependencies.** None at runtime, only the Python standard library.
48
+
49
+ ## Supply chain
50
+
51
+ * Every GitHub Action is pinned to a full commit SHA. Workflows run with read-only tokens unless a
52
+ job needs more, and never keep the checkout credentials.
53
+ * CI installs its tools from hash-locked files (`requirements/*.txt`, `pip --require-hashes`).
54
+ * CodeQL (Python and the workflows), OpenSSF Scorecard and dependency review run on the repository.
55
+ `zizmor --persona=pedantic` and `actionlint` are clean.
56
+ * A release runs only for a `vX.Y.Z` tag on a commit that is already on `main`, needs approval in
57
+ the `pypi` environment, and goes to PyPI through trusted publishing (OIDC, no stored token). The
58
+ jobs holding an OIDC token install nothing and can reach only an allowlist of hosts.
59
+ * Every release file carries signed build provenance. To check a file you downloaded:
60
+
61
+ ```sh
62
+ pip download gridhour==0.1.0 --no-deps -d .
63
+ gh attestation verify gridhour-0.1.0-py3-none-any.whl --repo 777dimas/gridhour \
64
+ --signer-workflow 777dimas/gridhour/.github/workflows/release.yml --source-ref refs/tags/v0.1.0
65
+ ```
@@ -0,0 +1,3 @@
1
+ """gridhour: when British electricity is green and cheap, in the terminal."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()