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.
Files changed (97) hide show
  1. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/CHANGELOG.md +28 -1
  2. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/PKG-INFO +38 -9
  3. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/README.md +37 -8
  4. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/SPEC.md +2 -2
  5. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/docs/DECISIONI.md +58 -0
  6. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/docs/DIARIO.md +44 -20
  7. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/docs/FONTI.md +16 -3
  8. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/docs/TAPPE.md +50 -6
  9. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/__init__.py +1 -1
  10. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/export/geotiff.py +1 -1
  11. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/alerts.py +3 -0
  12. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/_common.py +35 -0
  13. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/_text.py +3 -27
  14. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/alerts.py +4 -26
  15. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/bulletins.py +1 -1
  16. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/skill/__init__.py +26 -3
  17. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/skill/swpc-kit/SKILL.md +64 -18
  18. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/skill/swpc-kit/references/api.md +23 -8
  19. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/conftest.py +15 -2
  20. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_alerts.py +36 -0
  21. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_bulletins.py +6 -0
  22. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_grids.py +3 -0
  23. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_package.py +15 -0
  24. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_public_api.py +1 -1
  25. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_readme.py +4 -2
  26. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_skill.py +49 -1
  27. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tools/build_skill.py +113 -22
  28. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/.gitignore +0 -0
  29. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/AGENTS.md +0 -0
  30. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/LICENSE +0 -0
  31. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/pyproject.toml +0 -0
  32. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/client/__init__.py +0 -0
  33. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/client/_core.py +0 -0
  34. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/client/_httpx.py +0 -0
  35. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/client/_products.py +0 -0
  36. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/codes.py +0 -0
  37. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/errors.py +0 -0
  38. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/export/__init__.py +0 -0
  39. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/export/_tiff.py +0 -0
  40. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/export/data.py +0 -0
  41. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/export/geojson.py +0 -0
  42. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/__init__.py +0 -0
  43. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/_checks.py +0 -0
  44. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/bulletins.py +0 -0
  45. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/goes.py +0 -0
  46. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/grids.py +0 -0
  47. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/indices.py +0 -0
  48. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/records.py +0 -0
  49. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/models/solar_wind.py +0 -0
  50. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/__init__.py +0 -0
  51. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/goes.py +0 -0
  52. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/grids.py +0 -0
  53. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/indices.py +0 -0
  54. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/parse/solar_wind.py +0 -0
  55. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/py.typed +0 -0
  56. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/src/swpc_kit/skill/__main__.py +0 -0
  57. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/27-day-outlook/2026-10-15.txt +0 -0
  58. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/3-day-forecast/2022-09-22-sample.txt +0 -0
  59. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/3-day-forecast/2026-10-15.txt +0 -0
  60. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/README.md +0 -0
  61. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/alerts/2026-10-15.json +0 -0
  62. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/drap/2026-10-15.txt +0 -0
  63. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/dst/2026-10-15-built.json +0 -0
  64. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/dst/2026-10-15.json +0 -0
  65. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/f107-30-day/2026-10-15-built.json +0 -0
  66. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/f107-30-day/2026-10-15.json +0 -0
  67. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/geomag-forecast/2026-10-15.txt +0 -0
  68. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/goes-protons-1-day/2026-10-15.json +0 -0
  69. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/goes-xrays-1-day/2026-10-15.json +0 -0
  70. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/kp-1m/2026-10-15.json +0 -0
  71. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/kp-3h/2022-09-22-sample.json +0 -0
  72. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/kp-3h/2026-10-15-built.json +0 -0
  73. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/kp-3h/2026-10-15.json +0 -0
  74. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/kp-forecast/2022-09-22-sample.json +0 -0
  75. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/kp-forecast/2026-10-15.json +0 -0
  76. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/noaa-scales/2026-10-15.json +0 -0
  77. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/ovation-aurora/2026-10-15.json +0 -0
  78. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/rtsw-mag-1m/2026-10-15.json +0 -0
  79. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/fixtures/rtsw-wind-1m/2026-10-15.json +0 -0
  80. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_build_old_format.py +0 -0
  81. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_client.py +0 -0
  82. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_codes.py +0 -0
  83. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_errors.py +0 -0
  84. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_export_data.py +0 -0
  85. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_export_geojson.py +0 -0
  86. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_export_geotiff.py +0 -0
  87. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_fixtures.py +0 -0
  88. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_goes.py +0 -0
  89. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_indices.py +0 -0
  90. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_parse_common.py +0 -0
  91. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_record_fixtures.py +0 -0
  92. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_skill_domain.py +0 -0
  93. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_solar_wind.py +0 -0
  94. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tests/test_tolerance.py +0 -0
  95. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tools/build_old_format.py +0 -0
  96. {swpc_kit-0.3.0 → swpc_kit-0.3.1}/tools/fixtures.toml +0 -0
  97. {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.0...HEAD
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.0
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 most
76
- recent time its content declares, and `source_url`.
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: an unchanged file comes back as `304 Not Modified`
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 (`429`,
265
- `5xx`), the network failed, or the file was caught while SWPC was rewriting
266
- it. Wait, and wait longer each time; give up after a few attempts.
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 most
53
- recent time its content declares, and `source_url`.
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: an unchanged file comes back as `304 Not Modified`
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 (`429`,
242
- `5xx`), the network failed, or the file was caught while SWPC was rewriting
243
- it. Wait, and wait longer each time; give up after a few attempts.
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 most recent time in the
101
- content), and the client accepts `max_age`.
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**: T0–T11, T13–T16 e T18 fatte e in `main` (PR #2–#7). Il
8
- commit `Version 0.3.0` (ramo `version-0.3.0`) porta la versione, la
9
- sezione 0.3.0 del CHANGELOG, che presenta il pacchetto intero perché
10
- 0.1.0 e 0.2.0 non sono mai uscite, e il tag `v0.3.0`.
11
- - **Prossimo passo**: la prima pubblicazione (`AGENTS.md`, "Releases"). Il
12
- maintainer registra il pending publisher su pypi.org; poi il merge della
13
- PR di versione e il push del tag, che avvia `release.yml`, e il
14
- maintainer approva l'environment `pypi`. Subito prima del tag, ricontrollare
15
- che `swpc-kit` sia libero su PyPI. Dopo la 0.3.0: T12, e T17 (il
16
- trasporto educato su politefetch, la cui API è cambiata: rileggerla).
17
- - **Decisioni in sospeso**: quelle di T12 (parser dei tempi testuali, stub
18
- di rasterio, supporto di Windows). Il job `lowest` resta fuori dai
19
- controlli richiesti di `main` (maintainer, 2026-10-26): per chi usa la
20
- libreria il solo minimo è httpx2 2.13, e il job lo prova lo stesso su ogni
21
- PR.
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
- - Su Windows, nella suite intera, l'avviso dei test async può cadere su
212
- un test GeoTIFF o GeoJSON: è il guasto noto, e da soli passano.
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 last `time_tag` of any source; the freshness of the
147
- active source alone is `latest_active().time`.
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
- - [ ] Fatta
204
+ - [x] Fatta
205
205
 
206
206
  Rinvii scritti nel diario e mai chiusi:
207
207
 
208
- - [ ] Unire il parser dei tempi testuali (`2026 Oct 13 2058 UTC`) di
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
- - [ ] rasterio: tenere l'override mypy (`ignore_missing_imports`) o passare
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
- - [ ] Windows: la suite non passa tutta (async con la barriera sul socket del
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
- - [ ] Ricontrollare che `swpc-kit` sia libero su PyPI subito prima del tag
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
@@ -20,7 +20,7 @@ from swpc_kit.errors import (
20
20
  UnexpectedContent,
21
21
  )
22
22
 
23
- __version__ = "0.3.0"
23
+ __version__ = "0.3.1"
24
24
 
25
25
  __all__ = [
26
26
  "ParseError",
@@ -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 UTC, date, datetime
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 = datetime(
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()