swpc-kit 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- swpc_kit/__init__.py +32 -0
- swpc_kit/client/__init__.py +482 -0
- swpc_kit/client/_core.py +384 -0
- swpc_kit/client/_httpx.py +93 -0
- swpc_kit/client/_products.py +146 -0
- swpc_kit/codes.py +249 -0
- swpc_kit/errors.py +135 -0
- swpc_kit/export/__init__.py +12 -0
- swpc_kit/export/_tiff.py +64 -0
- swpc_kit/export/data.py +167 -0
- swpc_kit/export/geojson.py +111 -0
- swpc_kit/export/geotiff.py +134 -0
- swpc_kit/models/__init__.py +116 -0
- swpc_kit/models/_checks.py +16 -0
- swpc_kit/models/alerts.py +205 -0
- swpc_kit/models/bulletins.py +239 -0
- swpc_kit/models/goes.py +252 -0
- swpc_kit/models/grids.py +353 -0
- swpc_kit/models/indices.py +356 -0
- swpc_kit/models/records.py +29 -0
- swpc_kit/models/solar_wind.py +274 -0
- swpc_kit/parse/__init__.py +53 -0
- swpc_kit/parse/_common.py +353 -0
- swpc_kit/parse/_text.py +334 -0
- swpc_kit/parse/alerts.py +252 -0
- swpc_kit/parse/bulletins.py +532 -0
- swpc_kit/parse/goes.py +111 -0
- swpc_kit/parse/grids.py +379 -0
- swpc_kit/parse/indices.py +344 -0
- swpc_kit/parse/solar_wind.py +154 -0
- swpc_kit/py.typed +0 -0
- swpc_kit/skill/__init__.py +213 -0
- swpc_kit/skill/__main__.py +7 -0
- swpc_kit/skill/swpc-kit/SKILL.md +261 -0
- swpc_kit/skill/swpc-kit/references/api.md +1052 -0
- swpc_kit-0.3.0.dist-info/METADATA +375 -0
- swpc_kit-0.3.0.dist-info/RECORD +39 -0
- swpc_kit-0.3.0.dist-info/WHEEL +4 -0
- swpc_kit-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""What a product made of records says about the records it could not read.
|
|
2
|
+
|
|
3
|
+
The JSON products made of records (indices, solar wind, GOES, alerts) are
|
|
4
|
+
read record by record: one that cannot be read goes to the model's
|
|
5
|
+
``skipped`` instead of making the whole product fail.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
__all__ = ["SkippedRecord"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class SkippedRecord:
|
|
15
|
+
"""A record of the source that the parser could not read.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
index: 0-based position of the record in the JSON array.
|
|
19
|
+
field: The field that is wrong, when one is.
|
|
20
|
+
reason: What is wrong, for a person to read.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
index: int
|
|
24
|
+
field: str | None
|
|
25
|
+
reason: str
|
|
26
|
+
|
|
27
|
+
def __post_init__(self) -> None:
|
|
28
|
+
if self.index < 0:
|
|
29
|
+
raise ValueError(f"index must not be negative, got {self.index}")
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Models for the real-time solar wind (RTSW) at L1.
|
|
2
|
+
|
|
3
|
+
SWPC publishes one-minute averages of the interplanetary magnetic field
|
|
4
|
+
(``rtsw_mag_1m.json``) and of the solar wind plasma (``rtsw_wind_1m.json``)
|
|
5
|
+
from several spacecraft at once. Each record names its ``source`` and says
|
|
6
|
+
whether it is the ``active`` one, the source SWPC uses for its products. A
|
|
7
|
+
minute can have records from several sources, from inactive sources only,
|
|
8
|
+
or none at all.
|
|
9
|
+
|
|
10
|
+
Times are UTC. SWPC writes the ``time_tag`` values without a time zone; they
|
|
11
|
+
are read as UTC.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from enum import StrEnum
|
|
17
|
+
from typing import Protocol
|
|
18
|
+
|
|
19
|
+
from swpc_kit.models._checks import check_utc, check_values
|
|
20
|
+
from swpc_kit.models.records import SkippedRecord
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RtswSource(StrEnum):
|
|
24
|
+
"""Spacecraft names seen in the ``source`` field (October 2026).
|
|
25
|
+
|
|
26
|
+
Samples keep ``source`` as a plain string, so a name SWPC adds later does
|
|
27
|
+
not break parsing; these members compare equal to those strings.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
SOLAR1 = "SOLAR1"
|
|
31
|
+
"""SWFO-L1 (SOLAR-1), the primary source since 2026-06-30."""
|
|
32
|
+
ACE = "ACE"
|
|
33
|
+
"""ACE, the backup source."""
|
|
34
|
+
IMAP = "IMAP"
|
|
35
|
+
"""IMAP, published but inactive in October 2026."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class RtswMagSample:
|
|
40
|
+
"""One one-minute record of the interplanetary magnetic field.
|
|
41
|
+
|
|
42
|
+
Field names follow the source. Physical values are ``None`` when the
|
|
43
|
+
source writes ``null``.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
time: Time of the record. Not always on the minute (IMAP).
|
|
47
|
+
source: Spacecraft, such as ``"SOLAR1"`` (see :class:`RtswSource`).
|
|
48
|
+
active: Whether this source is the one SWPC uses at this time.
|
|
49
|
+
bt: Total field strength in nT.
|
|
50
|
+
bx_gse: X component in GSE coordinates, nT.
|
|
51
|
+
by_gse: Y component in GSE coordinates, nT.
|
|
52
|
+
bz_gse: Z component in GSE coordinates, nT.
|
|
53
|
+
theta_gse: Latitude of the field in GSE, degrees, -90 to 90.
|
|
54
|
+
phi_gse: Longitude of the field in GSE, degrees, 0 to 360.
|
|
55
|
+
bx_gsm: X component in GSM coordinates, nT.
|
|
56
|
+
by_gsm: Y component in GSM coordinates, nT.
|
|
57
|
+
bz_gsm: Z component in GSM coordinates, nT.
|
|
58
|
+
theta_gsm: Latitude of the field in GSM, degrees, -90 to 90.
|
|
59
|
+
phi_gsm: Longitude of the field in GSM, degrees, 0 to 360.
|
|
60
|
+
manual_mode: ``manual_mode`` flag of the source.
|
|
61
|
+
sample_size: Number of measurements averaged.
|
|
62
|
+
max_telemetry_flag: Worst telemetry flag in the minute.
|
|
63
|
+
max_data_flag: Worst data flag in the minute, as written; SOLAR1 and
|
|
64
|
+
ACE write ``-9999``, which looks like a fill value.
|
|
65
|
+
overall_quality: Overall quality flag (0 in every record seen).
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
time: datetime
|
|
69
|
+
source: str
|
|
70
|
+
active: bool
|
|
71
|
+
bt: float | None
|
|
72
|
+
bx_gse: float | None
|
|
73
|
+
by_gse: float | None
|
|
74
|
+
bz_gse: float | None
|
|
75
|
+
theta_gse: float | None
|
|
76
|
+
phi_gse: float | None
|
|
77
|
+
bx_gsm: float | None
|
|
78
|
+
by_gsm: float | None
|
|
79
|
+
bz_gsm: float | None
|
|
80
|
+
theta_gsm: float | None
|
|
81
|
+
phi_gsm: float | None
|
|
82
|
+
manual_mode: bool | None
|
|
83
|
+
sample_size: int | None
|
|
84
|
+
max_telemetry_flag: int | None
|
|
85
|
+
max_data_flag: int | None
|
|
86
|
+
overall_quality: int | None
|
|
87
|
+
|
|
88
|
+
def __post_init__(self) -> None:
|
|
89
|
+
check_utc("time", self.time)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True, slots=True)
|
|
93
|
+
class RtswWindSample:
|
|
94
|
+
"""One one-minute record of the solar wind plasma.
|
|
95
|
+
|
|
96
|
+
Field names follow the source. Physical values are ``None`` when the
|
|
97
|
+
source writes ``null``: in October 2026 ACE and IMAP give no velocity
|
|
98
|
+
components and no source gives alpha particle values.
|
|
99
|
+
|
|
100
|
+
Attributes:
|
|
101
|
+
time: Time of the record. Not always on the minute (IMAP).
|
|
102
|
+
source: Spacecraft, such as ``"SOLAR1"`` (see :class:`RtswSource`).
|
|
103
|
+
active: Whether this source is the one SWPC uses at this time.
|
|
104
|
+
proton_speed: Bulk proton speed in km/s.
|
|
105
|
+
proton_temperature: Proton temperature in K.
|
|
106
|
+
proton_density: Proton density in cm⁻³.
|
|
107
|
+
proton_vx_gse: Proton velocity, X in GSE, km/s.
|
|
108
|
+
proton_vy_gse: Proton velocity, Y in GSE, km/s.
|
|
109
|
+
proton_vz_gse: Proton velocity, Z in GSE, km/s.
|
|
110
|
+
proton_vx_gsm: Proton velocity, X in GSM, km/s.
|
|
111
|
+
proton_vy_gsm: Proton velocity, Y in GSM, km/s.
|
|
112
|
+
proton_vz_gsm: Proton velocity, Z in GSM, km/s.
|
|
113
|
+
proton_sample_size: Number of proton measurements averaged.
|
|
114
|
+
alpha_speed: Alpha particle speed in km/s.
|
|
115
|
+
alpha_temperature: Alpha particle temperature in K.
|
|
116
|
+
alpha_density: Alpha particle density in cm⁻³.
|
|
117
|
+
alpha_vx_gse: Alpha velocity, X in GSE, km/s.
|
|
118
|
+
alpha_vy_gse: Alpha velocity, Y in GSE, km/s.
|
|
119
|
+
alpha_vz_gse: Alpha velocity, Z in GSE, km/s.
|
|
120
|
+
alpha_vx_gsm: Alpha velocity, X in GSM, km/s.
|
|
121
|
+
alpha_vy_gsm: Alpha velocity, Y in GSM, km/s.
|
|
122
|
+
alpha_vz_gsm: Alpha velocity, Z in GSM, km/s.
|
|
123
|
+
alpha_sample_size: Number of alpha measurements averaged.
|
|
124
|
+
max_convergence_flag: Worst convergence flag in the minute.
|
|
125
|
+
max_data_flag: Worst data flag in the minute.
|
|
126
|
+
max_error_count_flag: Worst error count flag in the minute.
|
|
127
|
+
max_processing_flag: Worst processing flag in the minute.
|
|
128
|
+
max_range_flag: Worst range flag in the minute.
|
|
129
|
+
max_sample_count_flag: Worst sample count flag in the minute.
|
|
130
|
+
max_telemetry_flag: Worst telemetry flag in the minute.
|
|
131
|
+
overall_quality: Overall quality flag (0 in every record seen).
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
time: datetime
|
|
135
|
+
source: str
|
|
136
|
+
active: bool
|
|
137
|
+
proton_speed: float | None
|
|
138
|
+
proton_temperature: float | None
|
|
139
|
+
proton_density: float | None
|
|
140
|
+
proton_vx_gse: float | None
|
|
141
|
+
proton_vy_gse: float | None
|
|
142
|
+
proton_vz_gse: float | None
|
|
143
|
+
proton_vx_gsm: float | None
|
|
144
|
+
proton_vy_gsm: float | None
|
|
145
|
+
proton_vz_gsm: float | None
|
|
146
|
+
proton_sample_size: int | None
|
|
147
|
+
alpha_speed: float | None
|
|
148
|
+
alpha_temperature: float | None
|
|
149
|
+
alpha_density: float | None
|
|
150
|
+
alpha_vx_gse: float | None
|
|
151
|
+
alpha_vy_gse: float | None
|
|
152
|
+
alpha_vz_gse: float | None
|
|
153
|
+
alpha_vx_gsm: float | None
|
|
154
|
+
alpha_vy_gsm: float | None
|
|
155
|
+
alpha_vz_gsm: float | None
|
|
156
|
+
alpha_sample_size: int | None
|
|
157
|
+
max_convergence_flag: int | None
|
|
158
|
+
max_data_flag: int | None
|
|
159
|
+
max_error_count_flag: int | None
|
|
160
|
+
max_processing_flag: int | None
|
|
161
|
+
max_range_flag: int | None
|
|
162
|
+
max_sample_count_flag: int | None
|
|
163
|
+
max_telemetry_flag: int | None
|
|
164
|
+
overall_quality: int | None
|
|
165
|
+
|
|
166
|
+
def __post_init__(self) -> None:
|
|
167
|
+
check_utc("time", self.time)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class _Sample(Protocol):
|
|
171
|
+
@property
|
|
172
|
+
def time(self) -> datetime: ...
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def source(self) -> str: ...
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def active(self) -> bool: ...
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class _RtswSeries[S: _Sample]:
|
|
182
|
+
"""Access by source and by active record, shared by the RTSW products."""
|
|
183
|
+
|
|
184
|
+
__slots__ = ()
|
|
185
|
+
|
|
186
|
+
values: tuple[S, ...]
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def data_time(self) -> datetime:
|
|
190
|
+
"""Time of the most recent active record.
|
|
191
|
+
|
|
192
|
+
Freshness follows the active source, the only one to read as "the
|
|
193
|
+
solar wind now": a backup that keeps running after the active source
|
|
194
|
+
stopped does not make the data look new. With no active record at
|
|
195
|
+
all, the time of the oldest record, since no time of the file has
|
|
196
|
+
active data.
|
|
197
|
+
"""
|
|
198
|
+
active = [v.time for v in self.values if v.active]
|
|
199
|
+
return max(active) if active else min(v.time for v in self.values)
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def sources(self) -> tuple[str, ...]:
|
|
203
|
+
"""Names of the sources in the content, sorted."""
|
|
204
|
+
return tuple(sorted({v.source for v in self.values}))
|
|
205
|
+
|
|
206
|
+
def series(self, source: str) -> tuple[S, ...]:
|
|
207
|
+
"""Return the records of ``source``, oldest first (empty if none)."""
|
|
208
|
+
return tuple(v for v in self.values if v.source == source)
|
|
209
|
+
|
|
210
|
+
def active_series(self) -> tuple[S, ...]:
|
|
211
|
+
"""Return the active record of each time that has one, oldest first.
|
|
212
|
+
|
|
213
|
+
Times with inactive records only are left out. If a time had more
|
|
214
|
+
than one active record (not seen so far), the first one in the
|
|
215
|
+
source file is taken.
|
|
216
|
+
"""
|
|
217
|
+
chosen: dict[datetime, S] = {}
|
|
218
|
+
for v in self.values:
|
|
219
|
+
if v.active and v.time not in chosen:
|
|
220
|
+
chosen[v.time] = v
|
|
221
|
+
return tuple(chosen.values())
|
|
222
|
+
|
|
223
|
+
def active_at(self, time: datetime) -> S | None:
|
|
224
|
+
"""Return the active record at exactly ``time``, or ``None``.
|
|
225
|
+
|
|
226
|
+
``None`` means there is no record at that time or only inactive ones.
|
|
227
|
+
"""
|
|
228
|
+
return next((v for v in self.values if v.time == time and v.active), None)
|
|
229
|
+
|
|
230
|
+
def latest_active(self) -> S | None:
|
|
231
|
+
"""Return the most recent active record, or ``None`` if there is none."""
|
|
232
|
+
active = [v for v in self.values if v.active]
|
|
233
|
+
if not active:
|
|
234
|
+
return None
|
|
235
|
+
latest = max(v.time for v in active)
|
|
236
|
+
return next(v for v in active if v.time == latest)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@dataclass(frozen=True, slots=True)
|
|
240
|
+
class RtswMag1m(_RtswSeries[RtswMagSample]):
|
|
241
|
+
"""One-minute interplanetary magnetic field of the last 24 hours.
|
|
242
|
+
|
|
243
|
+
Attributes:
|
|
244
|
+
values: The records of every source, oldest first; records with the
|
|
245
|
+
same time keep the order of the source file.
|
|
246
|
+
source_url: Where the content came from.
|
|
247
|
+
skipped: The records that could not be read, in source order.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
values: tuple[RtswMagSample, ...]
|
|
251
|
+
source_url: str
|
|
252
|
+
skipped: tuple[SkippedRecord, ...] = ()
|
|
253
|
+
|
|
254
|
+
def __post_init__(self) -> None:
|
|
255
|
+
check_values(self.values)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@dataclass(frozen=True, slots=True)
|
|
259
|
+
class RtswWind1m(_RtswSeries[RtswWindSample]):
|
|
260
|
+
"""One-minute solar wind plasma of the last 24 hours.
|
|
261
|
+
|
|
262
|
+
Attributes:
|
|
263
|
+
values: The records of every source, oldest first; records with the
|
|
264
|
+
same time keep the order of the source file.
|
|
265
|
+
source_url: Where the content came from.
|
|
266
|
+
skipped: The records that could not be read, in source order.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
values: tuple[RtswWindSample, ...]
|
|
270
|
+
source_url: str
|
|
271
|
+
skipped: tuple[SkippedRecord, ...] = ()
|
|
272
|
+
|
|
273
|
+
def __post_init__(self) -> None:
|
|
274
|
+
check_values(self.values)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Parsers: pure functions from raw SWPC content to models.
|
|
2
|
+
|
|
3
|
+
Each parser takes ``str`` or ``bytes`` and returns a model from
|
|
4
|
+
:mod:`swpc_kit.models`. Parsers do no I/O and use only the standard library.
|
|
5
|
+
They accept every documented format of a product, old and new, and raise the
|
|
6
|
+
errors of :mod:`swpc_kit.errors` on bad input.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from swpc_kit.parse.alerts import parse_alerts
|
|
10
|
+
from swpc_kit.parse.bulletins import (
|
|
11
|
+
parse_27_day_outlook,
|
|
12
|
+
parse_geomagnetic_forecast,
|
|
13
|
+
parse_three_day_forecast,
|
|
14
|
+
)
|
|
15
|
+
from swpc_kit.parse.goes import (
|
|
16
|
+
parse_goes_integral_protons,
|
|
17
|
+
parse_goes_xrays,
|
|
18
|
+
)
|
|
19
|
+
from swpc_kit.parse.grids import (
|
|
20
|
+
parse_drap_global_frequencies,
|
|
21
|
+
parse_ovation_aurora,
|
|
22
|
+
)
|
|
23
|
+
from swpc_kit.parse.indices import (
|
|
24
|
+
parse_f107_flux_30_day,
|
|
25
|
+
parse_kyoto_dst,
|
|
26
|
+
parse_noaa_scales,
|
|
27
|
+
parse_planetary_k_index,
|
|
28
|
+
parse_planetary_k_index_1m,
|
|
29
|
+
parse_planetary_k_index_forecast,
|
|
30
|
+
)
|
|
31
|
+
from swpc_kit.parse.solar_wind import (
|
|
32
|
+
parse_rtsw_mag_1m,
|
|
33
|
+
parse_rtsw_wind_1m,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"parse_27_day_outlook",
|
|
38
|
+
"parse_alerts",
|
|
39
|
+
"parse_drap_global_frequencies",
|
|
40
|
+
"parse_f107_flux_30_day",
|
|
41
|
+
"parse_geomagnetic_forecast",
|
|
42
|
+
"parse_goes_integral_protons",
|
|
43
|
+
"parse_goes_xrays",
|
|
44
|
+
"parse_kyoto_dst",
|
|
45
|
+
"parse_noaa_scales",
|
|
46
|
+
"parse_ovation_aurora",
|
|
47
|
+
"parse_planetary_k_index",
|
|
48
|
+
"parse_planetary_k_index_1m",
|
|
49
|
+
"parse_planetary_k_index_forecast",
|
|
50
|
+
"parse_rtsw_mag_1m",
|
|
51
|
+
"parse_rtsw_wind_1m",
|
|
52
|
+
"parse_three_day_forecast",
|
|
53
|
+
]
|