swpc-kit 0.3.0__tar.gz → 0.3.1__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.
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/CHANGELOG.md +28 -1
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/PKG-INFO +38 -9
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/README.md +37 -8
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/SPEC.md +2 -2
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/docs/DECISIONI.md +58 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/docs/DIARIO.md +44 -20
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/docs/FONTI.md +16 -3
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/docs/TAPPE.md +50 -6
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/__init__.py +1 -1
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/export/geotiff.py +1 -1
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/alerts.py +3 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/_common.py +35 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/_text.py +3 -27
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/alerts.py +4 -26
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/bulletins.py +1 -1
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/skill/__init__.py +26 -3
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/skill/swpc-kit/SKILL.md +64 -18
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/skill/swpc-kit/references/api.md +23 -8
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/conftest.py +15 -2
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_alerts.py +36 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_bulletins.py +6 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_grids.py +3 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_package.py +15 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_public_api.py +1 -1
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_readme.py +4 -2
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_skill.py +49 -1
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tools/build_skill.py +113 -22
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/.gitignore +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/AGENTS.md +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/LICENSE +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/pyproject.toml +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/client/__init__.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/client/_core.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/client/_httpx.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/client/_products.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/codes.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/errors.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/export/__init__.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/export/_tiff.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/export/data.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/export/geojson.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/__init__.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/_checks.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/bulletins.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/goes.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/grids.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/indices.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/records.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/solar_wind.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/__init__.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/goes.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/grids.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/indices.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/solar_wind.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/py.typed +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/skill/__main__.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/27-day-outlook/2026-10-15.txt +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/3-day-forecast/2022-09-22-sample.txt +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/3-day-forecast/2026-10-15.txt +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/README.md +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/alerts/2026-10-15.json +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/drap/2026-10-15.txt +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/dst/2026-10-15-built.json +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/dst/2026-10-15.json +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/f107-30-day/2026-10-15-built.json +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/f107-30-day/2026-10-15.json +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/geomag-forecast/2026-10-15.txt +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/goes-protons-1-day/2026-10-15.json +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/goes-xrays-1-day/2026-10-15.json +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/kp-1m/2026-10-15.json +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/kp-3h/2022-09-22-sample.json +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/kp-3h/2026-10-15-built.json +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/kp-3h/2026-10-15.json +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/kp-forecast/2022-09-22-sample.json +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/kp-forecast/2026-10-15.json +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/noaa-scales/2026-10-15.json +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/ovation-aurora/2026-10-15.json +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/rtsw-mag-1m/2026-10-15.json +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/rtsw-wind-1m/2026-10-15.json +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_build_old_format.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_client.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_codes.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_errors.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_export_data.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_export_geojson.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_export_geotiff.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_fixtures.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_goes.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_indices.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_parse_common.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_record_fixtures.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_skill_domain.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_solar_wind.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_tolerance.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tools/build_old_format.py +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tools/fixtures.toml +0 -0
- {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tools/record_fixtures.py +0 -0
|
@@ -6,6 +6,32 @@ All notable changes to this project are listed here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.1] - 2026-10-26
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `MessageKind.CANCEL_WARNING`.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Windows is supported: the CI runs the whole test suite on Windows too.
|
|
18
|
+
- The docs describe `data_time` the same way everywhere, present the `304`
|
|
19
|
+
as a saving rather than a promise, and the skill lists the plausibility
|
|
20
|
+
limits, the fields each record must have and the strict products.
|
|
21
|
+
- The docs treat a `429` as a refusal (slow down, stop until the next run)
|
|
22
|
+
rather than a reason to retry soon, and explain how to use the parsers
|
|
23
|
+
behind your own download code. The client still raises `SourceNotReady`.
|
|
24
|
+
- `python -m swpc_kit.skill check` without `--target` looks for the copy
|
|
25
|
+
from the current folder up to the root of the repository, so it works
|
|
26
|
+
from a subfolder too.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- A `CANCEL WARNING` message had `kind=None`.
|
|
31
|
+
- The skill's API reference left out the methods the solar wind models
|
|
32
|
+
inherit (`latest_active`, `active_series` and the others), and wrote
|
|
33
|
+
some types as `Optional[...]`.
|
|
34
|
+
|
|
9
35
|
## [0.3.0] - 2026-10-26
|
|
10
36
|
|
|
11
37
|
The first release on PyPI. 0.1.0 and 0.2.0 were never published; their
|
|
@@ -172,5 +198,6 @@ First version. Never published or tagged.
|
|
|
172
198
|
declares.
|
|
173
199
|
- Recorded fixtures and the tools that record them (`tools/`).
|
|
174
200
|
|
|
175
|
-
[Unreleased]: https://github.com/Tabrigos/swpc-kit/compare/v0.3.
|
|
201
|
+
[Unreleased]: https://github.com/Tabrigos/swpc-kit/compare/v0.3.1...HEAD
|
|
202
|
+
[0.3.1]: https://github.com/Tabrigos/swpc-kit/releases/tag/v0.3.1
|
|
176
203
|
[0.3.0]: https://github.com/Tabrigos/swpc-kit/releases/tag/v0.3.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: swpc-kit
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Typed readers for NOAA SWPC space weather data products, with GeoJSON and GeoTIFF export.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Tabrigos/swpc-kit
|
|
6
6
|
Project-URL: Issues, https://github.com/Tabrigos/swpc-kit/issues
|
|
@@ -34,7 +34,7 @@ for what is known about each SWPC format.
|
|
|
34
34
|
|
|
35
35
|
## Install
|
|
36
36
|
|
|
37
|
-
Requires Python 3.12 or later.
|
|
37
|
+
Requires Python 3.12 or later; the tests run on Linux and Windows.
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
pip install swpc-kit # parsers, models, JSON, GeoJSON, GeoTIFF
|
|
@@ -72,8 +72,10 @@ Parsers live in `swpc_kit.parse`, models in `swpc_kit.models`, the clients in
|
|
|
72
72
|
## Parse content you already have
|
|
73
73
|
|
|
74
74
|
Parsers are pure functions: `str` or `bytes` in, a frozen dataclass out, with
|
|
75
|
-
every time an aware UTC `datetime`. Each model has `data_time`, the
|
|
76
|
-
|
|
75
|
+
every time an aware UTC `datetime`. Each model has `data_time`, the time its
|
|
76
|
+
data describes and never a forecast time (the last observed value, the
|
|
77
|
+
observation time of a grid, the issue time of a bulletin; the docstring of
|
|
78
|
+
each model says which), and `source_url`.
|
|
77
79
|
|
|
78
80
|
```pycon
|
|
79
81
|
>>> from pathlib import Path
|
|
@@ -212,8 +214,10 @@ write_geotiff(drap, "drap.tif") # EPSG:4326, times in the tags
|
|
|
212
214
|
|
|
213
215
|
The client (`client` extra) downloads, classifies the errors of the source,
|
|
214
216
|
parses, and checks the age of the data on the time the content declares.
|
|
215
|
-
Requests are conditional:
|
|
216
|
-
and the previous model is reused.
|
|
217
|
+
Requests are conditional: when the `ETag` of a file has not changed, SWPC
|
|
218
|
+
answers `304 Not Modified` and the previous model is reused. The `ETag` may
|
|
219
|
+
also change when the content does not, so a `304` is a saving, not a
|
|
220
|
+
promise.
|
|
217
221
|
|
|
218
222
|
```python
|
|
219
223
|
from datetime import timedelta
|
|
@@ -261,9 +265,14 @@ wants.
|
|
|
261
265
|
|
|
262
266
|
### Trying again
|
|
263
267
|
|
|
264
|
-
`SourceNotReady` is the only error worth retrying: SWPC is busy (`
|
|
265
|
-
|
|
266
|
-
|
|
268
|
+
`SourceNotReady` is the only error worth retrying: SWPC is busy (`5xx`), the
|
|
269
|
+
network failed, or the file was caught while SWPC was rewriting it. Wait, and
|
|
270
|
+
wait longer each time; give up after a few attempts.
|
|
271
|
+
|
|
272
|
+
A `429` is a `SourceNotReady` too, but it is a refusal: SWPC asks you to slow
|
|
273
|
+
down. If the attempts below do not get past it, stop and try at the next run
|
|
274
|
+
of your job, and make the runs less frequent. A later version will give the
|
|
275
|
+
`429` an error of its own.
|
|
267
276
|
|
|
268
277
|
```python
|
|
269
278
|
import time
|
|
@@ -294,6 +303,26 @@ updated the product yet: try at the next run of your job, not within
|
|
|
294
303
|
seconds. The client does not read `Retry-After` and keeps its `ETag`s in
|
|
295
304
|
memory only, so keep one `Client` for the life of your program.
|
|
296
305
|
|
|
306
|
+
### Downloading with your own code
|
|
307
|
+
|
|
308
|
+
If your program already downloads through a layer of its own (a scheduler,
|
|
309
|
+
a rate limiter, a cache), keep it and use only the parsers. Read the answers
|
|
310
|
+
the way the client does:
|
|
311
|
+
|
|
312
|
+
- **200**: parse the body. A `SourceNotReady` from the parser is a file
|
|
313
|
+
caught while SWPC was rewriting it: try again later, by the rules of your
|
|
314
|
+
layer.
|
|
315
|
+
- **408, 425, 5xx**, a timeout or a network failure: try again later, by the
|
|
316
|
+
rules of your layer. If that layer records failures and pauses the host,
|
|
317
|
+
"later" is its next run, never a loop in the same process.
|
|
318
|
+
- **429**: a refusal. Slow down, honour `Retry-After` if your layer reads
|
|
319
|
+
it, and do not retry in the same run.
|
|
320
|
+
- **A redirect, a 404 or any other status**, or an HTML page: stop. SWPC
|
|
321
|
+
moved or changed the product, or the URL is wrong.
|
|
322
|
+
|
|
323
|
+
Then measure the age on `data_time`, not on the HTTP headers: a `304` or a
|
|
324
|
+
recent `Last-Modified` does not say that the data is new.
|
|
325
|
+
|
|
297
326
|
### When SWPC moves a product or changes a format
|
|
298
327
|
|
|
299
328
|
- **A product that moved** answers `UnexpectedContent`, with `status` (301,
|
|
@@ -11,7 +11,7 @@ for what is known about each SWPC format.
|
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
14
|
-
Requires Python 3.12 or later.
|
|
14
|
+
Requires Python 3.12 or later; the tests run on Linux and Windows.
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
pip install swpc-kit # parsers, models, JSON, GeoJSON, GeoTIFF
|
|
@@ -49,8 +49,10 @@ Parsers live in `swpc_kit.parse`, models in `swpc_kit.models`, the clients in
|
|
|
49
49
|
## Parse content you already have
|
|
50
50
|
|
|
51
51
|
Parsers are pure functions: `str` or `bytes` in, a frozen dataclass out, with
|
|
52
|
-
every time an aware UTC `datetime`. Each model has `data_time`, the
|
|
53
|
-
|
|
52
|
+
every time an aware UTC `datetime`. Each model has `data_time`, the time its
|
|
53
|
+
data describes and never a forecast time (the last observed value, the
|
|
54
|
+
observation time of a grid, the issue time of a bulletin; the docstring of
|
|
55
|
+
each model says which), and `source_url`.
|
|
54
56
|
|
|
55
57
|
```pycon
|
|
56
58
|
>>> from pathlib import Path
|
|
@@ -189,8 +191,10 @@ write_geotiff(drap, "drap.tif") # EPSG:4326, times in the tags
|
|
|
189
191
|
|
|
190
192
|
The client (`client` extra) downloads, classifies the errors of the source,
|
|
191
193
|
parses, and checks the age of the data on the time the content declares.
|
|
192
|
-
Requests are conditional:
|
|
193
|
-
and the previous model is reused.
|
|
194
|
+
Requests are conditional: when the `ETag` of a file has not changed, SWPC
|
|
195
|
+
answers `304 Not Modified` and the previous model is reused. The `ETag` may
|
|
196
|
+
also change when the content does not, so a `304` is a saving, not a
|
|
197
|
+
promise.
|
|
194
198
|
|
|
195
199
|
```python
|
|
196
200
|
from datetime import timedelta
|
|
@@ -238,9 +242,14 @@ wants.
|
|
|
238
242
|
|
|
239
243
|
### Trying again
|
|
240
244
|
|
|
241
|
-
`SourceNotReady` is the only error worth retrying: SWPC is busy (`
|
|
242
|
-
|
|
243
|
-
|
|
245
|
+
`SourceNotReady` is the only error worth retrying: SWPC is busy (`5xx`), the
|
|
246
|
+
network failed, or the file was caught while SWPC was rewriting it. Wait, and
|
|
247
|
+
wait longer each time; give up after a few attempts.
|
|
248
|
+
|
|
249
|
+
A `429` is a `SourceNotReady` too, but it is a refusal: SWPC asks you to slow
|
|
250
|
+
down. If the attempts below do not get past it, stop and try at the next run
|
|
251
|
+
of your job, and make the runs less frequent. A later version will give the
|
|
252
|
+
`429` an error of its own.
|
|
244
253
|
|
|
245
254
|
```python
|
|
246
255
|
import time
|
|
@@ -271,6 +280,26 @@ updated the product yet: try at the next run of your job, not within
|
|
|
271
280
|
seconds. The client does not read `Retry-After` and keeps its `ETag`s in
|
|
272
281
|
memory only, so keep one `Client` for the life of your program.
|
|
273
282
|
|
|
283
|
+
### Downloading with your own code
|
|
284
|
+
|
|
285
|
+
If your program already downloads through a layer of its own (a scheduler,
|
|
286
|
+
a rate limiter, a cache), keep it and use only the parsers. Read the answers
|
|
287
|
+
the way the client does:
|
|
288
|
+
|
|
289
|
+
- **200**: parse the body. A `SourceNotReady` from the parser is a file
|
|
290
|
+
caught while SWPC was rewriting it: try again later, by the rules of your
|
|
291
|
+
layer.
|
|
292
|
+
- **408, 425, 5xx**, a timeout or a network failure: try again later, by the
|
|
293
|
+
rules of your layer. If that layer records failures and pauses the host,
|
|
294
|
+
"later" is its next run, never a loop in the same process.
|
|
295
|
+
- **429**: a refusal. Slow down, honour `Retry-After` if your layer reads
|
|
296
|
+
it, and do not retry in the same run.
|
|
297
|
+
- **A redirect, a 404 or any other status**, or an HTML page: stop. SWPC
|
|
298
|
+
moved or changed the product, or the URL is wrong.
|
|
299
|
+
|
|
300
|
+
Then measure the age on `data_time`, not on the HTTP headers: a `304` or a
|
|
301
|
+
recent `Last-Modified` does not say that the data is new.
|
|
302
|
+
|
|
274
303
|
### When SWPC moves a product or changes a format
|
|
275
304
|
|
|
276
305
|
- **A product that moved** answers `UnexpectedContent`, with `status` (301,
|
|
@@ -97,8 +97,8 @@ A hierarchy under `SwpcError`:
|
|
|
97
97
|
The age of the data is measured **on the time the content declares**
|
|
98
98
|
(`Observation Time`, `:Issued:`, the last `time_tag`), never on the HTTP
|
|
99
99
|
outcome: a `304 Not Modified` or a recent modification date does not say that
|
|
100
|
-
the data is new. Every model exposes `data_time` (the
|
|
101
|
-
|
|
100
|
+
the data is new. Every model exposes `data_time` (the time its data
|
|
101
|
+
describes, never a forecast time), and the client accepts `max_age`.
|
|
102
102
|
|
|
103
103
|
## Changing formats
|
|
104
104
|
|
|
@@ -414,3 +414,61 @@ scelte già prese e sparse fra `SPEC.md`, `docs/FONTI.md` e il diario; la data
|
|
|
414
414
|
minimi di numpy e rasterio provati dal job `lowest` diventano quelli dei
|
|
415
415
|
test.
|
|
416
416
|
|
|
417
|
+
## D-022 — Windows supportato; tempi testuali con una parte comune; niente stub di rasterio
|
|
418
|
+
|
|
419
|
+
- **Data e tappa**: 2026-10-26, T12 (scelte del maintainer)
|
|
420
|
+
- **Contesto**: su Windows la suite non passava tutta. Le cause stavano
|
|
421
|
+
tutte nei test:
|
|
422
|
+
- la barriera di rete bloccava la coppia di socket con cui l'event loop
|
|
423
|
+
di asyncio si risveglia, che su Windows si connette a 127.0.0.1;
|
|
424
|
+
- gli id di `test_drap_inconsistent_lines` contenevano un file D-RAP
|
|
425
|
+
intero, oltre il limite di una variabile d'ambiente di Windows (pytest
|
|
426
|
+
ci mette l'id);
|
|
427
|
+
- `test_readme` creava un symlink, che su Windows richiede un privilegio.
|
|
428
|
+
|
|
429
|
+
Gli avvisi lasciati dai test async cadevano poi su test innocenti.
|
|
430
|
+
- **Decisione**:
|
|
431
|
+
- Windows è supportato. Un job `windows` nella CI fa girare la suite
|
|
432
|
+
intera con la wheel. È un job a sé, non una voce della matrice di
|
|
433
|
+
`checks`, così i nomi dei controlli richiesti di `main` non cambiano;
|
|
434
|
+
il maintainer lo aggiunge fra i richiesti dopo il primo giro verde.
|
|
435
|
+
- La barriera lascia aperto solo `connect` verso l'indirizzo locale
|
|
436
|
+
(`127.0.0.1`, `::1`); `connect_ex`, `create_connection` e
|
|
437
|
+
`getaddrinfo` restano chiusi. Un test lo prova.
|
|
438
|
+
- Tempi testuali: la tabella dei mesi e la costruzione dell'ora UTC
|
|
439
|
+
(`text_time`) stanno in `parse/_common.py`. Ogni formato tiene il suo
|
|
440
|
+
pattern e il suo modo di leggere il mese: i bollettini lo leggono senza
|
|
441
|
+
badare alle maiuscole, gli avvisi solo come `Oct`. Due test fissano
|
|
442
|
+
questa differenza.
|
|
443
|
+
- rasterio resta con l'override di mypy (`ignore_missing_imports`).
|
|
444
|
+
- **Alternative scartate**:
|
|
445
|
+
- saltare su Windows i test che falliscono (la suite di chi sviluppa qui
|
|
446
|
+
restava rossa, e un guasto vero si sarebbe confuso con quelli noti);
|
|
447
|
+
- un solo parser per i due formati (gli avvisi avrebbero accettato
|
|
448
|
+
`OCT`, un cambio di comportamento);
|
|
449
|
+
- `types-rasterio` (una dipendenza in più, legata alla versione di
|
|
450
|
+
rasterio, per un solo file di test dopo D-021).
|
|
451
|
+
- **Conseguenze**: la suite è verde anche su Windows, e un guasto solo di
|
|
452
|
+
Windows si vede nella CI.
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
## D-023 — Il 429 è un rifiuto: per ora lo dice la documentazione
|
|
456
|
+
|
|
457
|
+
- **Data e tappa**: 2026-10-26, T19 (scelta del maintainer, concordata con
|
|
458
|
+
l'agente di politefetch)
|
|
459
|
+
- **Contesto**: README e SKILL mettevano il 429 fra gli errori da ritentare
|
|
460
|
+
presto, con `SourceNotReady`. Per chi interroga SWPC da un job, e per
|
|
461
|
+
politefetch, un 429 è un rifiuto: si rallenta, non si ritenta subito.
|
|
462
|
+
Mancava anche una guida per chi scarica con un livello suo e usa solo i
|
|
463
|
+
parser.
|
|
464
|
+
- **Decisione**: il comportamento del client non cambia fino alla T17. README
|
|
465
|
+
e SKILL dicono che il 429 è un rifiuto: dopo pochi tentativi distanziati ci
|
|
466
|
+
si ferma fino al giro successivo del job. Una sezione nuova spiega come
|
|
467
|
+
leggere le risposte HTTP a chi scarica da sé; con un livello che registra i
|
|
468
|
+
fallimenti, "più tardi" vuol dire il suo giro successivo, mai un ciclo nello
|
|
469
|
+
stesso processo.
|
|
470
|
+
- **Alternative scartate**: una classe d'errore per il 429 già nella prossima
|
|
471
|
+
patch (cambia il contratto prima che la T17 lo progetti insieme
|
|
472
|
+
all'errore per l'host congelato, D-016).
|
|
473
|
+
- **Conseguenze**: fino alla T17 il 429 si distingue solo dal messaggio, che
|
|
474
|
+
la SKILL sconsiglia di leggere; la T17 gli dà un errore suo.
|
|
@@ -4,21 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
Riscritto da capo a ogni fine sessione, mai accodato.
|
|
6
6
|
|
|
7
|
-
- **Stato**:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
0.1
|
|
11
|
-
- **Prossimo passo**:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
7
|
+
- **Stato**: tutte le tappe fino alla T18 fatte, più la T12; **0.3.0
|
|
8
|
+
pubblicata su PyPI** il 2026-10-26 (tag `v0.3.0`, release su GitHub). Lo
|
|
9
|
+
stesso giorno la T19 (PR #10) e la ricopia del modello delle skill
|
|
10
|
+
(PR #11), poi la **0.3.1** (tag `v0.3.1`) con T12, T19 e la ricopia.
|
|
11
|
+
- **Prossimo passo**: T17, il trasporto educato su politefetch, che è su
|
|
12
|
+
PyPI dal 2026-10-26 (0.1.0). La sua API è cambiata prima della
|
|
13
|
+
pubblicazione (client con retry propri rifiutato, `NoPolicy`,
|
|
14
|
+
`Transport.send` con un context manager): rileggerla, e decidere con
|
|
15
|
+
quella sessione la semantica del 429 e il client HTTP.
|
|
16
|
+
- **Decisioni in sospeso**: niente. Il maintainer ha scelto (2026-10-26): il
|
|
17
|
+
429 resta `SourceNotReady` fino alla T17 e la documentazione lo dice un
|
|
18
|
+
rifiuto (D-023). Il job `windows` è fra i controlli richiesti di `main`
|
|
19
|
+
dal 2026-10-26, dopo il primo giro verde (D-022). Il
|
|
20
|
+
job `lowest` resta fuori dai richiesti (maintainer, 2026-10-26): per chi
|
|
21
|
+
usa la libreria il solo minimo è httpx2 2.13, e il job lo prova lo stesso
|
|
22
|
+
su ogni PR.
|
|
22
23
|
- **Da sapere** (per chi scrive i prossimi parser):
|
|
23
24
|
- `swpc_kit/parse/_common.py` è il kit comune: `load_json` classifica gli
|
|
24
25
|
errori (vuoto o troncato → `SourceNotReady`; HTML o non JSON →
|
|
@@ -195,9 +196,6 @@ Riscritto da capo a ogni fine sessione, mai accodato.
|
|
|
195
196
|
`remember` (log e cache): l'async fa il parse fra i due, in
|
|
196
197
|
`asyncio.to_thread`. Gli argomenti dei client li controlla
|
|
197
198
|
`_check_arguments`; il trasporto `check_transport`.
|
|
198
|
-
- Su Windows i test async si provano con
|
|
199
|
-
`uv run pytest --noconftest tests/test_client.py -k async`: senza la
|
|
200
|
-
barriera di rete, dato che usano trasporti finti.
|
|
201
199
|
- **T16** (rilascio):
|
|
202
200
|
- Job `lowest` in `.github/workflows/ci.yml` (D-020). In locale si prova
|
|
203
201
|
con `UV_PROJECT_ENVIRONMENT=<cartella a parte>` e `uv sync --all-extras
|
|
@@ -208,8 +206,21 @@ Riscritto da capo a ogni fine sessione, mai accodato.
|
|
|
208
206
|
- `swpc_kit/export/_tiff.py` è il contenitore TIFF; i numeri dei tag
|
|
209
207
|
GeoTIFF e GDAL sono commentati in `geotiff.py`. rasterio serve solo ai
|
|
210
208
|
test, per rileggere.
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
- **T12** (Windows e debiti):
|
|
210
|
+
- Su Windows la suite intera è verde, anche in locale (D-022). La
|
|
211
|
+
barriera di `tests/conftest.py` lascia aperto solo `connect` verso
|
|
212
|
+
l'indirizzo locale; un test lungo con id costruiti dai dati deve dare
|
|
213
|
+
id corti (`ids=`), perché pytest mette l'id in una variabile
|
|
214
|
+
d'ambiente.
|
|
215
|
+
- `MONTHS` e `text_time` stanno in `parse/_common.py`; i pattern e le
|
|
216
|
+
maiuscole del mese restano di ciascun formato.
|
|
217
|
+
- **T19** (riscontri):
|
|
218
|
+
- `MessageKind.CANCEL_WARNING`; il caso sta come testo in
|
|
219
|
+
`tests/test_alerts.py`, perché la fixture registrata non ha un warning
|
|
220
|
+
annullato.
|
|
221
|
+
- Il generatore di `api.md` prende solo la prima riga delle docstring
|
|
222
|
+
(con la regola D205 il primo paragrafo è una riga): i dettagli vanno
|
|
223
|
+
nella SKILL, non nelle docstring.
|
|
213
224
|
- **Dubbi aperti**: le ipotesi rimaste in `docs/FONTI.md` (UTC dei
|
|
214
225
|
`time_tag` senza `Z`, millisecondi del vecchio Dst e flusso, significato
|
|
215
226
|
delle chiavi di `noaa-scales.json`, unità dei valori OVATION).
|
|
@@ -292,3 +303,16 @@ Una riga per sessione: data, tappa, cosa è entrato, cosa resta.
|
|
|
292
303
|
CHANGELOG che presenta il pacchetto, i link in fondo e il tag `v0.3.0`.
|
|
293
304
|
Note della release provate con lo stesso `awk` del workflow. Il nome
|
|
294
305
|
`swpc-kit` su PyPI era ancora libero.
|
|
306
|
+
- 2026-10-26 — 0.3.0 pubblicata. T12: Windows supportato, con la suite
|
|
307
|
+
verde e un job `windows` nella CI (barriera aperta solo verso l'indirizzo
|
|
308
|
+
locale, id corti, fixture copiate invece del symlink); tabella dei mesi e
|
|
309
|
+
`text_time` in `_common.py`; override di mypy per rasterio tenuto
|
|
310
|
+
(D-022). Resta: T17.
|
|
311
|
+
- 2026-10-26 — T19 (in locale, ramo `t19-riscontri`): riscontri
|
|
312
|
+
della prima prova d'uso vagliati sul codice; `MessageKind.CANCEL_WARNING`;
|
|
313
|
+
`data_time`, `304` ed ETag, limiti, campi obbligatori e prodotti rigidi
|
|
314
|
+
descritti allo stesso modo in README, SPEC, FONTI e SKILL; il 429 è un
|
|
315
|
+
rifiuto e c'è una sezione per chi scarica da sé (D-023). Prossima: 0.3.1.
|
|
316
|
+
- 2026-10-26 — Ricopia del modello delle skill (PR #11: `check` dalla radice
|
|
317
|
+
del repo, metodi ereditati e tipi concreti in `api.md`) e versione 0.3.1
|
|
318
|
+
(Windows, `CANCEL_WARNING`, documentazione, 429 come rifiuto). Resta: T17.
|
|
@@ -143,8 +143,9 @@ texts end with `\n`. The server sends `Last-Modified`, `ETag` and
|
|
|
143
143
|
instant is the one with `active: true` (if there were two, the first in
|
|
144
144
|
the file wins); an instant with no active record stays without one, with
|
|
145
145
|
no fallback to another source. Unknown sources stay strings.
|
|
146
|
-
`data_time` is the
|
|
147
|
-
|
|
146
|
+
Since T13 (D-012) `data_time` is the `time_tag` of the last active
|
|
147
|
+
record, not of any source: a backup that keeps running after the active
|
|
148
|
+
source stopped does not make the data look new.
|
|
148
149
|
|
|
149
150
|
## GOES
|
|
150
151
|
|
|
@@ -216,7 +217,7 @@ texts end with `\n`. The server sends `Last-Modified`, `ETag` and
|
|
|
216
217
|
- First three lines always present: `Space Weather Message Code: WARK04`,
|
|
217
218
|
`Serial Number: 5435`, `Issue Time: 2026 Oct 13 2058 UTC`. Then the title
|
|
218
219
|
(`ALERT:`, `WARNING:`, `EXTENDED WARNING:`, `WATCH:`, `CONTINUED ALERT:`,
|
|
219
|
-
`CANCEL WATCH:`, `SUMMARY:`) and fields such as `Valid From:`,
|
|
220
|
+
`CANCEL WATCH:`, `SUMMARY:`; `CANCEL WARNING:` too, below) and fields such as `Valid From:`,
|
|
220
221
|
`Valid To:`, `Now Valid Until:`, `Threshold Reached:`,
|
|
221
222
|
`Synoptic Period:`, `Active Warning:`, `Begin Time:`, `End Time:`,
|
|
222
223
|
`Maximum Time:`, `Extension to Serial Number:`,
|
|
@@ -281,6 +282,13 @@ Verified in T4 on the same fixture:
|
|
|
281
282
|
are lines without `Name:` (`THIS SUPERSEDES ANY/ALL PRIOR WATCHES IN EFFECT`,
|
|
282
283
|
`CORRECTED`, free sentences, the continuation of `Potential Impacts:`),
|
|
283
284
|
and `Comment:` can contain long runs of spaces.
|
|
285
|
+
- **`CANCEL WARNING`**, **verified** on one record of `alerts.json` issued
|
|
286
|
+
2026-07-12 21:01 UTC, outside the recorded fixture (the test keeps its
|
|
287
|
+
text): `product_id` `K05W`, code `WARK05`, the same as the warning it
|
|
288
|
+
cancels, then `Cancel Serial Number:` and `Original Issue Time:`. Its
|
|
289
|
+
scale line is glued to a sentence
|
|
290
|
+
(`... until 12/2100 UTC.NOAA Scale: G1 - Minor`), so it is not read as a
|
|
291
|
+
field.
|
|
284
292
|
- **Hypothesis**: an empty array `[]` has never been seen; the parser reads
|
|
285
293
|
it as "no messages" (not as `SourceNotReady`), because a period without
|
|
286
294
|
alerts is a legitimate state.
|
|
@@ -476,6 +484,11 @@ comes):
|
|
|
476
484
|
downloads and parses all 16 products of the day; SWPC sends **weak**
|
|
477
485
|
`ETag`s (`W/"12c4-…"`) and `Last-Modified`, and answers
|
|
478
486
|
`304 Not Modified` to the conditional request with `If-None-Match`.
|
|
487
|
+
- **Hypothesis** (reported by a user of the library, measured on
|
|
488
|
+
2026-09-13, not verified here): the `ETag` of a product can change about
|
|
489
|
+
every 165 seconds while the content stays byte for byte the same, so the
|
|
490
|
+
`304` saves less than it seems. The docs present the `304` as a saving,
|
|
491
|
+
not a promise.
|
|
479
492
|
|
|
480
493
|
## Plausibility limits (T14, 2026-10-25)
|
|
481
494
|
|
|
@@ -201,18 +201,30 @@ Non fa parte della tappa (da concordare con politefetch): la semantica del
|
|
|
201
201
|
|
|
202
202
|
## T12 — Debiti rimasti dalle tappe nel cloud
|
|
203
203
|
|
|
204
|
-
- [
|
|
204
|
+
- [x] Fatta
|
|
205
205
|
|
|
206
206
|
Rinvii scritti nel diario e mai chiusi:
|
|
207
207
|
|
|
208
|
-
- [
|
|
208
|
+
- [x] Unire il parser dei tempi testuali (`2026 Oct 13 2058 UTC`) di
|
|
209
209
|
`parse/alerts.py` con quello di `parse/_text.py` in `parse/_common.py`
|
|
210
210
|
(rinviato da T4 e T5 a T8, poi rimasto).
|
|
211
|
-
- [
|
|
211
|
+
- [x] rasterio: tenere l'override mypy (`ignore_missing_imports`) o passare
|
|
212
212
|
agli stub `types-rasterio` (nuova dipendenza di sviluppo, da giustificare).
|
|
213
|
-
- [
|
|
213
|
+
- [x] Windows: la suite non passa tutta (async con la barriera sul socket del
|
|
214
214
|
loop Proactor, griglie, esempi del README). Decidere se Windows è
|
|
215
215
|
supportato; se sì, CI anche su Windows.
|
|
216
|
+
- Esito (D-022), con le scelte del maintainer:
|
|
217
|
+
- Windows supportato, job `windows` nella CI;
|
|
218
|
+
- in `_common.py` la tabella dei mesi e `text_time`, mentre pattern e
|
|
219
|
+
maiuscole restano di ciascun formato: un solo parser avrebbe cambiato
|
|
220
|
+
il comportamento degli avvisi;
|
|
221
|
+
- l'override di mypy per rasterio resta.
|
|
222
|
+
|
|
223
|
+
In più, in un commit a sé, la parte comune di `tests/test_skill.py`
|
|
224
|
+
ricopiata dal modello `c3371e9`: i percorsi citati in SKILL.md si devono
|
|
225
|
+
aprire dalla copia di un progetto. La `829d005` non passava `ruff format`.
|
|
226
|
+
La SKILL.md di swpc-kit cita solo `references/api.md`; il test si è visto
|
|
227
|
+
fallire con un percorso del repo aggiunto per prova.
|
|
216
228
|
|
|
217
229
|
## Revisione prima della pubblicazione (2026-10-25)
|
|
218
230
|
|
|
@@ -340,8 +352,9 @@ tolleranza per record (D-015).
|
|
|
340
352
|
`numpy>=2.0`, `rasterio>=1.4`.
|
|
341
353
|
- [x] Valutare un writer GeoTIFF senza rasterio (il raster è piccolo):
|
|
342
354
|
solo valutazione, con la scelta in `docs/DECISIONI.md`.
|
|
343
|
-
- [
|
|
344
|
-
(libero il 2026-10-25: `https://pypi.org/pypi/swpc-kit/json` dà 404
|
|
355
|
+
- [x] Ricontrollare che `swpc-kit` sia libero su PyPI subito prima del tag
|
|
356
|
+
(libero il 2026-10-25: `https://pypi.org/pypi/swpc-kit/json` dà 404; di
|
|
357
|
+
nuovo libero il 2026-10-26, subito prima di spingere `v0.3.0`).
|
|
345
358
|
- Esito (D-019, D-020):
|
|
346
359
|
- classifier "4 - Beta";
|
|
347
360
|
- job `lowest` nella CI;
|
|
@@ -374,6 +387,37 @@ Quando: prima della 0.3.0, deciso dal maintainer il 2026-10-25 (D-019).
|
|
|
374
387
|
in più ora controllano la compressione, l'elenco completo dei tag,
|
|
375
388
|
l'intero raster di OVATION completa e le regole TIFF che GDAL perdona.
|
|
376
389
|
|
|
390
|
+
## T19 — Riscontri della prima prova d'uso
|
|
391
|
+
|
|
392
|
+
- [x] Fatta
|
|
393
|
+
|
|
394
|
+
Quando: dopo la 0.3.0, prima della T17. Il 2026-10-26 un progetto che userebbe
|
|
395
|
+
solo i parser ha provato la 0.3.0 sulle sue risposte registrate. Ogni voce
|
|
396
|
+
del suo riscontro è stata ricontrollata sul codice prima di tenerla buona.
|
|
397
|
+
|
|
398
|
+
- [x] `MessageKind.CANCEL_WARNING`: un `CANCEL WARNING` aveva `kind=None`.
|
|
399
|
+
Ha lo stesso codice del warning che annulla, quindi `info` va letto
|
|
400
|
+
dopo `kind` (docstring e SKILL).
|
|
401
|
+
- [x] `data_time` descritto allo stesso modo in README, SPEC, FONTI e SKILL:
|
|
402
|
+
il tempo che i dati descrivono, mai un tempo di previsione. In FONTI la
|
|
403
|
+
riga del vento solare era rimasta a prima di D-012.
|
|
404
|
+
- [x] Il `304` è un risparmio, non una promessa: l'ETag può cambiare a
|
|
405
|
+
contenuto identico (ipotesi riportata, in FONTI).
|
|
406
|
+
- [x] SKILL: installazione senza extra, limiti di plausibilità, campi
|
|
407
|
+
obbligatori in ogni record, `noaa-scales.json` fra i prodotti rigidi,
|
|
408
|
+
veicolo attivo fermo, dettagli del GeoTIFF, `partial` di OVATION.
|
|
409
|
+
- Restano come sono, per scelta già presa (D-015, D-017): colonna assente
|
|
410
|
+
da tutti i record = `UnexpectedContent`; `noaa-scales`, bollettini e
|
|
411
|
+
griglie rigidi; `a_running` e `station_count` obbligatori (nessun null
|
|
412
|
+
visto nelle risposte registrate); classe dei flare troncata, come la
|
|
413
|
+
`max_class` di SWPC nell'elenco dei flare della prova; nessun ripiego
|
|
414
|
+
automatico sul veicolo di riserva (D-012).
|
|
415
|
+
- [x] Il 429 è un rifiuto (D-023): README e SKILL lo dicono, il
|
|
416
|
+
comportamento cambia in T17. Sezione per chi scarica con codice suo,
|
|
417
|
+
concordata con l'agente di politefetch (scelte del maintainer).
|
|
418
|
+
- Fuori da questa tappa: i metodi ereditati in `api.md` sono un difetto del
|
|
419
|
+
modello comune, corretto con la sua ricopia.
|
|
420
|
+
|
|
377
421
|
## T17 — Trasporto educato su politefetch
|
|
378
422
|
|
|
379
423
|
- [ ] Fatta
|
|
@@ -46,7 +46,7 @@ def write_geotiff(
|
|
|
46
46
|
*,
|
|
47
47
|
nodata: float = math.nan,
|
|
48
48
|
) -> None:
|
|
49
|
-
"""Write a gridded product to a GeoTIFF file.
|
|
49
|
+
"""Write a gridded product to a GeoTIFF file, in place; nodata is declared.
|
|
50
50
|
|
|
51
51
|
Cells the grid does not have (the missing longitudes of a reduced
|
|
52
52
|
OVATION file) are set to ``nodata``.
|
|
@@ -40,6 +40,7 @@ class MessageKind(StrEnum):
|
|
|
40
40
|
CONTINUED_ALERT = "CONTINUED ALERT"
|
|
41
41
|
WARNING = "WARNING"
|
|
42
42
|
EXTENDED_WARNING = "EXTENDED WARNING"
|
|
43
|
+
CANCEL_WARNING = "CANCEL WARNING"
|
|
43
44
|
WATCH = "WATCH"
|
|
44
45
|
CANCEL_WATCH = "CANCEL WATCH"
|
|
45
46
|
SUMMARY = "SUMMARY"
|
|
@@ -179,6 +180,8 @@ class AlertMessage:
|
|
|
179
180
|
def info(self) -> MessageCode | None:
|
|
180
181
|
"""Catalogue entry of the message, by short code then long code.
|
|
181
182
|
|
|
183
|
+
A cancel has the code of the message it cancels, so the meaning of
|
|
184
|
+
the entry holds only when ``kind`` is not a ``CANCEL_*`` kind.
|
|
182
185
|
``None`` when neither code is in :mod:`swpc_kit.codes`.
|
|
183
186
|
"""
|
|
184
187
|
return by_short(self.product_id) or by_long(self.code)
|
|
@@ -109,6 +109,41 @@ def parse_time(text: str) -> datetime:
|
|
|
109
109
|
return value.astimezone(UTC)
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
MONTHS = {
|
|
113
|
+
name: number
|
|
114
|
+
for number, name in enumerate(
|
|
115
|
+
(
|
|
116
|
+
"Jan",
|
|
117
|
+
"Feb",
|
|
118
|
+
"Mar",
|
|
119
|
+
"Apr",
|
|
120
|
+
"May",
|
|
121
|
+
"Jun",
|
|
122
|
+
"Jul",
|
|
123
|
+
"Aug",
|
|
124
|
+
"Sep",
|
|
125
|
+
"Oct",
|
|
126
|
+
"Nov",
|
|
127
|
+
"Dec",
|
|
128
|
+
),
|
|
129
|
+
start=1,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
"""English month abbreviations as SWPC writes them, to month numbers."""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def text_time(year: str, month: int, day: str, hour: str, minute: str) -> datetime:
|
|
136
|
+
"""Return the UTC time of the parts of a text time (``2026 Oct 13 2058 UTC``).
|
|
137
|
+
|
|
138
|
+
The text products and the alerts each match their own pattern and look up
|
|
139
|
+
the month in :data:`MONTHS`; this builds the time from the parts.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
ValueError: If the parts are not a valid date and time.
|
|
143
|
+
"""
|
|
144
|
+
return datetime(int(year), month, int(day), int(hour), int(minute), tzinfo=UTC)
|
|
145
|
+
|
|
146
|
+
|
|
112
147
|
class Record:
|
|
113
148
|
"""One record of a JSON product, with typed accessors.
|
|
114
149
|
|
|
@@ -22,32 +22,10 @@ import re
|
|
|
22
22
|
from bisect import bisect_right
|
|
23
23
|
from collections.abc import Sequence
|
|
24
24
|
from dataclasses import dataclass
|
|
25
|
-
from datetime import
|
|
25
|
+
from datetime import date, datetime
|
|
26
26
|
|
|
27
27
|
from swpc_kit.errors import ParseError, SourceNotReady, SwpcError, UnexpectedContent
|
|
28
|
-
from swpc_kit.parse._common import decode
|
|
29
|
-
|
|
30
|
-
MONTHS = {
|
|
31
|
-
name: number
|
|
32
|
-
for number, name in enumerate(
|
|
33
|
-
(
|
|
34
|
-
"Jan",
|
|
35
|
-
"Feb",
|
|
36
|
-
"Mar",
|
|
37
|
-
"Apr",
|
|
38
|
-
"May",
|
|
39
|
-
"Jun",
|
|
40
|
-
"Jul",
|
|
41
|
-
"Aug",
|
|
42
|
-
"Sep",
|
|
43
|
-
"Oct",
|
|
44
|
-
"Nov",
|
|
45
|
-
"Dec",
|
|
46
|
-
),
|
|
47
|
-
start=1,
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
"""English month abbreviations as SWPC writes them, to month numbers."""
|
|
28
|
+
from swpc_kit.parse._common import MONTHS, decode, text_time
|
|
51
29
|
|
|
52
30
|
_PRODUCT = re.compile(r":Product:\s*(.*)")
|
|
53
31
|
_ISSUED = re.compile(
|
|
@@ -325,9 +303,7 @@ def open_text_product(
|
|
|
325
303
|
if number is None:
|
|
326
304
|
raise reader.error(f"unknown month {month!r}", line.number, "Issued")
|
|
327
305
|
try:
|
|
328
|
-
issued =
|
|
329
|
-
int(year), number, int(day), int(hour), int(minute), tzinfo=UTC
|
|
330
|
-
)
|
|
306
|
+
issued = text_time(year, number, day, hour, minute)
|
|
331
307
|
except ValueError as exc:
|
|
332
308
|
raise reader.error(f"not a valid time: {exc}", line.number, "Issued") from None
|
|
333
309
|
reader.skip_comments()
|