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
swpc_kit/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Typed readers for the public data products of NOAA SWPC.
|
|
2
|
+
|
|
3
|
+
The package has three layers, each usable on its own:
|
|
4
|
+
|
|
5
|
+
- :mod:`swpc_kit.parse`: pure functions that turn raw ``str``/``bytes`` into
|
|
6
|
+
models (standard library only);
|
|
7
|
+
- :mod:`swpc_kit.models`: frozen dataclasses with UTC ``datetime`` fields;
|
|
8
|
+
- outputs: :mod:`swpc_kit.export` (GeoJSON, GeoTIFF) and
|
|
9
|
+
:mod:`swpc_kit.client` (HTTP access, ``client`` extra).
|
|
10
|
+
|
|
11
|
+
Data comes from NOAA SWPC (https://www.swpc.noaa.gov). This project is not
|
|
12
|
+
affiliated with or endorsed by NOAA.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from swpc_kit.errors import (
|
|
16
|
+
ParseError,
|
|
17
|
+
SourceNotReady,
|
|
18
|
+
StaleData,
|
|
19
|
+
SwpcError,
|
|
20
|
+
UnexpectedContent,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__version__ = "0.3.0"
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"ParseError",
|
|
27
|
+
"SourceNotReady",
|
|
28
|
+
"StaleData",
|
|
29
|
+
"SwpcError",
|
|
30
|
+
"UnexpectedContent",
|
|
31
|
+
"__version__",
|
|
32
|
+
]
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"""HTTP client for SWPC products (``client`` extra).
|
|
2
|
+
|
|
3
|
+
:class:`Client` and :class:`AsyncClient` have one method per product. Each
|
|
4
|
+
downloads the product, recognises the errors of the source, hands the content
|
|
5
|
+
to the matching parser and, if asked, checks that the data is recent enough::
|
|
6
|
+
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from swpc_kit.client import Client
|
|
9
|
+
|
|
10
|
+
with Client() as client:
|
|
11
|
+
kp = client.planetary_k_index(max_age=timedelta(hours=6))
|
|
12
|
+
|
|
13
|
+
Errors (all :class:`~swpc_kit.errors.SwpcError`):
|
|
14
|
+
|
|
15
|
+
- :class:`~swpc_kit.errors.SourceNotReady`: timeout, network failure,
|
|
16
|
+
``408``/``425``/``429``/``5xx``, or truncated content; try again later;
|
|
17
|
+
- :class:`~swpc_kit.errors.UnexpectedContent`: redirect (never followed),
|
|
18
|
+
other status, HTML, or content no parser knows;
|
|
19
|
+
- :class:`~swpc_kit.errors.ParseError`: known format, inconsistent value;
|
|
20
|
+
- :class:`~swpc_kit.errors.StaleData`: the content declares a time older
|
|
21
|
+
than ``max_age``.
|
|
22
|
+
|
|
23
|
+
Requests are conditional: once a URL answered with ``ETag`` or
|
|
24
|
+
``Last-Modified``, the next request sends them back, and a ``304 Not
|
|
25
|
+
Modified`` reuses the previous model (``max_age`` is still checked).
|
|
26
|
+
|
|
27
|
+
If SWPC moves a product before a new version of swpc-kit knows the new
|
|
28
|
+
address, ``urls`` points the client there::
|
|
29
|
+
|
|
30
|
+
Client(urls={"kyoto_dst": "https://services.swpc.noaa.gov/new/dst.json"})
|
|
31
|
+
|
|
32
|
+
Every error that goes through the client says which product and URL it comes
|
|
33
|
+
from (``product`` and ``url`` attributes, and in the message).
|
|
34
|
+
|
|
35
|
+
The HTTP library (httpx2) is used only by the default transports
|
|
36
|
+
(:class:`HttpxTransport`, :class:`AsyncHttpxTransport`) and imported only
|
|
37
|
+
when one is needed; any object with the :class:`Transport` or
|
|
38
|
+
:class:`AsyncTransport` interface can replace them.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
import asyncio
|
|
42
|
+
from collections.abc import Callable, Mapping
|
|
43
|
+
from datetime import datetime, timedelta
|
|
44
|
+
from types import TracebackType
|
|
45
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
46
|
+
|
|
47
|
+
from swpc_kit.client import _products as p
|
|
48
|
+
from swpc_kit.client._core import (
|
|
49
|
+
DEFAULT_TIMEOUT,
|
|
50
|
+
DEFAULT_USER_AGENT,
|
|
51
|
+
AsyncTransport,
|
|
52
|
+
BaseClient,
|
|
53
|
+
HttpResponse,
|
|
54
|
+
Product,
|
|
55
|
+
Transport,
|
|
56
|
+
_Timed,
|
|
57
|
+
_utc_now,
|
|
58
|
+
check_transport,
|
|
59
|
+
)
|
|
60
|
+
from swpc_kit.client._products import PRODUCTS
|
|
61
|
+
from swpc_kit.errors import SwpcError
|
|
62
|
+
from swpc_kit.models.alerts import (
|
|
63
|
+
Alerts,
|
|
64
|
+
)
|
|
65
|
+
from swpc_kit.models.bulletins import (
|
|
66
|
+
GeomagneticForecast,
|
|
67
|
+
ThreeDayForecast,
|
|
68
|
+
TwentySevenDayOutlook,
|
|
69
|
+
)
|
|
70
|
+
from swpc_kit.models.goes import (
|
|
71
|
+
GoesIntegralProtons,
|
|
72
|
+
GoesXrays,
|
|
73
|
+
)
|
|
74
|
+
from swpc_kit.models.grids import (
|
|
75
|
+
DrapFrequencies,
|
|
76
|
+
OvationAurora,
|
|
77
|
+
)
|
|
78
|
+
from swpc_kit.models.indices import (
|
|
79
|
+
F107Flux,
|
|
80
|
+
KyotoDst,
|
|
81
|
+
NoaaScales,
|
|
82
|
+
PlanetaryKIndex,
|
|
83
|
+
PlanetaryKIndex1m,
|
|
84
|
+
PlanetaryKIndexForecast,
|
|
85
|
+
)
|
|
86
|
+
from swpc_kit.models.solar_wind import (
|
|
87
|
+
RtswMag1m,
|
|
88
|
+
RtswWind1m,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if TYPE_CHECKING:
|
|
92
|
+
from swpc_kit.client._httpx import AsyncHttpxTransport, HttpxTransport
|
|
93
|
+
|
|
94
|
+
__all__ = [
|
|
95
|
+
"DEFAULT_TIMEOUT",
|
|
96
|
+
"DEFAULT_USER_AGENT",
|
|
97
|
+
"PRODUCTS",
|
|
98
|
+
"AsyncClient",
|
|
99
|
+
"AsyncHttpxTransport",
|
|
100
|
+
"AsyncTransport",
|
|
101
|
+
"Client",
|
|
102
|
+
"HttpResponse",
|
|
103
|
+
"HttpxTransport",
|
|
104
|
+
"Product",
|
|
105
|
+
"Transport",
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def __getattr__(name: str) -> Any:
|
|
110
|
+
# The httpx2 transports load on first use: a custom transport needs no
|
|
111
|
+
# httpx2, and without the extra the import error says what to install.
|
|
112
|
+
if name in ("HttpxTransport", "AsyncHttpxTransport"):
|
|
113
|
+
from swpc_kit.client import _httpx
|
|
114
|
+
|
|
115
|
+
return getattr(_httpx, name)
|
|
116
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class Client(BaseClient):
|
|
120
|
+
"""Synchronous client for SWPC products.
|
|
121
|
+
|
|
122
|
+
Use it with ``with`` or call :meth:`close`.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
transport: How requests are sent; by default an
|
|
126
|
+
:class:`HttpxTransport` (closed with the client).
|
|
127
|
+
user_agent: ``User-Agent`` header sent with every request.
|
|
128
|
+
timeout: Timeout in seconds for each phase of a request: connecting,
|
|
129
|
+
sending, waiting for a free connection, and each wait for the
|
|
130
|
+
next piece of the reply, as httpx2 applies it. It does not
|
|
131
|
+
bound the whole request: a server that keeps sending slowly can
|
|
132
|
+
make one last longer.
|
|
133
|
+
max_age: Default maximum age of the data; ``None`` means no check.
|
|
134
|
+
Each method also takes ``max_age``, which wins over this one.
|
|
135
|
+
conditional: Use ``ETag``/``Last-Modified`` conditional requests.
|
|
136
|
+
clock: Returns the current time as an aware UTC datetime (for
|
|
137
|
+
``max_age``).
|
|
138
|
+
urls: URLs that replace the canonical ones, by method name, for a
|
|
139
|
+
product that SWPC moved: ``{"kyoto_dst": "https://..."}``.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
TypeError: If an argument has the wrong type, or ``transport`` lacks
|
|
143
|
+
the methods it needs.
|
|
144
|
+
ValueError: If ``timeout`` or ``max_age`` is not positive, the user
|
|
145
|
+
agent is empty or not one line, or ``urls`` names a method that
|
|
146
|
+
does not exist or gives a URL that is not HTTP.
|
|
147
|
+
ImportError: If no ``transport`` is given and httpx2 (the
|
|
148
|
+
``client`` extra) is not installed.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def __init__(
|
|
152
|
+
self,
|
|
153
|
+
*,
|
|
154
|
+
transport: Transport | None = None,
|
|
155
|
+
user_agent: str = DEFAULT_USER_AGENT,
|
|
156
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
157
|
+
max_age: timedelta | None = None,
|
|
158
|
+
conditional: bool = True,
|
|
159
|
+
clock: Callable[[], datetime] = _utc_now,
|
|
160
|
+
urls: Mapping[str, str] | None = None,
|
|
161
|
+
) -> None:
|
|
162
|
+
super().__init__(
|
|
163
|
+
user_agent=user_agent,
|
|
164
|
+
timeout=timeout,
|
|
165
|
+
max_age=max_age,
|
|
166
|
+
conditional=conditional,
|
|
167
|
+
clock=clock,
|
|
168
|
+
urls=urls,
|
|
169
|
+
)
|
|
170
|
+
if transport is None:
|
|
171
|
+
from swpc_kit.client._httpx import HttpxTransport
|
|
172
|
+
|
|
173
|
+
transport = HttpxTransport()
|
|
174
|
+
check_transport("Client", transport, "get", "close")
|
|
175
|
+
self.transport: Transport = transport
|
|
176
|
+
|
|
177
|
+
def fetch[M: _Timed](
|
|
178
|
+
self, product: Product[M], *, max_age: timedelta | None = None
|
|
179
|
+
) -> M:
|
|
180
|
+
"""Fetch and parse ``product`` (one of :data:`PRODUCTS`).
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
product: What to fetch.
|
|
184
|
+
max_age: Maximum age of the data; ``None`` uses the client's.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
The parsed model.
|
|
188
|
+
"""
|
|
189
|
+
product = self.resolve(product)
|
|
190
|
+
try:
|
|
191
|
+
response = self.transport.get(
|
|
192
|
+
product.url,
|
|
193
|
+
headers=self.request_headers(product),
|
|
194
|
+
timeout=self.timeout,
|
|
195
|
+
)
|
|
196
|
+
return self.handle(product, response, max_age)
|
|
197
|
+
except SwpcError as error:
|
|
198
|
+
self.locate(error, product)
|
|
199
|
+
raise
|
|
200
|
+
|
|
201
|
+
def close(self) -> None:
|
|
202
|
+
"""Close the transport."""
|
|
203
|
+
self.transport.close()
|
|
204
|
+
|
|
205
|
+
def __enter__(self) -> Self:
|
|
206
|
+
return self
|
|
207
|
+
|
|
208
|
+
def __exit__(
|
|
209
|
+
self,
|
|
210
|
+
exc_type: type[BaseException] | None,
|
|
211
|
+
exc: BaseException | None,
|
|
212
|
+
tb: TracebackType | None,
|
|
213
|
+
) -> None:
|
|
214
|
+
self.close()
|
|
215
|
+
|
|
216
|
+
def planetary_k_index(self, *, max_age: timedelta | None = None) -> PlanetaryKIndex:
|
|
217
|
+
"""Fetch the 3-hour planetary K index of the last days."""
|
|
218
|
+
return self.fetch(p.PLANETARY_K_INDEX, max_age=max_age)
|
|
219
|
+
|
|
220
|
+
def planetary_k_index_forecast(
|
|
221
|
+
self, *, max_age: timedelta | None = None
|
|
222
|
+
) -> PlanetaryKIndexForecast:
|
|
223
|
+
"""Fetch the observed and forecast 3-hour Kp."""
|
|
224
|
+
return self.fetch(p.PLANETARY_K_INDEX_FORECAST, max_age=max_age)
|
|
225
|
+
|
|
226
|
+
def planetary_k_index_1m(
|
|
227
|
+
self, *, max_age: timedelta | None = None
|
|
228
|
+
) -> PlanetaryKIndex1m:
|
|
229
|
+
"""Fetch the 1-minute Kp estimates."""
|
|
230
|
+
return self.fetch(p.PLANETARY_K_INDEX_1M, max_age=max_age)
|
|
231
|
+
|
|
232
|
+
def kyoto_dst(self, *, max_age: timedelta | None = None) -> KyotoDst:
|
|
233
|
+
"""Fetch the hourly Kyoto Dst of the last week."""
|
|
234
|
+
return self.fetch(p.KYOTO_DST, max_age=max_age)
|
|
235
|
+
|
|
236
|
+
def f107_flux_30_day(self, *, max_age: timedelta | None = None) -> F107Flux:
|
|
237
|
+
"""Fetch the daily F10.7 of the last 30 days."""
|
|
238
|
+
return self.fetch(p.F107_FLUX_30_DAY, max_age=max_age)
|
|
239
|
+
|
|
240
|
+
def noaa_scales(self, *, max_age: timedelta | None = None) -> NoaaScales:
|
|
241
|
+
"""Fetch the NOAA space weather scales."""
|
|
242
|
+
return self.fetch(p.NOAA_SCALES, max_age=max_age)
|
|
243
|
+
|
|
244
|
+
def rtsw_mag_1m(self, *, max_age: timedelta | None = None) -> RtswMag1m:
|
|
245
|
+
"""Fetch the real-time solar wind magnetic field."""
|
|
246
|
+
return self.fetch(p.RTSW_MAG_1M, max_age=max_age)
|
|
247
|
+
|
|
248
|
+
def rtsw_wind_1m(self, *, max_age: timedelta | None = None) -> RtswWind1m:
|
|
249
|
+
"""Fetch the real-time solar wind plasma."""
|
|
250
|
+
return self.fetch(p.RTSW_WIND_1M, max_age=max_age)
|
|
251
|
+
|
|
252
|
+
def goes_xrays(self, *, max_age: timedelta | None = None) -> GoesXrays:
|
|
253
|
+
"""Fetch the GOES X-ray flux of the last day (primary satellite)."""
|
|
254
|
+
return self.fetch(p.GOES_XRAYS, max_age=max_age)
|
|
255
|
+
|
|
256
|
+
def goes_integral_protons(
|
|
257
|
+
self, *, max_age: timedelta | None = None
|
|
258
|
+
) -> GoesIntegralProtons:
|
|
259
|
+
"""Fetch the GOES integral proton flux of the last day (primary satellite)."""
|
|
260
|
+
return self.fetch(p.GOES_INTEGRAL_PROTONS, max_age=max_age)
|
|
261
|
+
|
|
262
|
+
def alerts(self, *, max_age: timedelta | None = None) -> Alerts:
|
|
263
|
+
"""Fetch the alerts, warnings, watches and summaries."""
|
|
264
|
+
return self.fetch(p.ALERTS, max_age=max_age)
|
|
265
|
+
|
|
266
|
+
def three_day_forecast(
|
|
267
|
+
self, *, max_age: timedelta | None = None
|
|
268
|
+
) -> ThreeDayForecast:
|
|
269
|
+
"""Fetch the 3-day forecast."""
|
|
270
|
+
return self.fetch(p.THREE_DAY_FORECAST, max_age=max_age)
|
|
271
|
+
|
|
272
|
+
def geomagnetic_forecast(
|
|
273
|
+
self, *, max_age: timedelta | None = None
|
|
274
|
+
) -> GeomagneticForecast:
|
|
275
|
+
"""Fetch the 3-day geomagnetic forecast."""
|
|
276
|
+
return self.fetch(p.GEOMAGNETIC_FORECAST, max_age=max_age)
|
|
277
|
+
|
|
278
|
+
def twenty_seven_day_outlook(
|
|
279
|
+
self, *, max_age: timedelta | None = None
|
|
280
|
+
) -> TwentySevenDayOutlook:
|
|
281
|
+
"""Fetch the 27-day outlook."""
|
|
282
|
+
return self.fetch(p.TWENTY_SEVEN_DAY_OUTLOOK, max_age=max_age)
|
|
283
|
+
|
|
284
|
+
def ovation_aurora(self, *, max_age: timedelta | None = None) -> OvationAurora:
|
|
285
|
+
"""Fetch the OVATION aurora forecast grid."""
|
|
286
|
+
return self.fetch(p.OVATION_AURORA, max_age=max_age)
|
|
287
|
+
|
|
288
|
+
def drap_global_frequencies(
|
|
289
|
+
self, *, max_age: timedelta | None = None
|
|
290
|
+
) -> DrapFrequencies:
|
|
291
|
+
"""Fetch the D-RAP global frequency grid."""
|
|
292
|
+
return self.fetch(p.DRAP_GLOBAL_FREQUENCIES, max_age=max_age)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class AsyncClient(BaseClient):
|
|
296
|
+
"""Asynchronous client for SWPC products.
|
|
297
|
+
|
|
298
|
+
Use it with ``async with`` or call :meth:`aclose`. The content is parsed
|
|
299
|
+
in a thread (:func:`asyncio.to_thread`), so a large file such as the
|
|
300
|
+
OVATION grid does not block the event loop. For a limit on the whole
|
|
301
|
+
call, wrap it in :func:`asyncio.timeout`.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
transport: How requests are sent; by default an
|
|
305
|
+
:class:`AsyncHttpxTransport` (closed with the client).
|
|
306
|
+
user_agent: ``User-Agent`` header sent with every request.
|
|
307
|
+
timeout: Timeout in seconds for each phase of a request: connecting,
|
|
308
|
+
sending, waiting for a free connection, and each wait for the
|
|
309
|
+
next piece of the reply, as httpx2 applies it. It does not
|
|
310
|
+
bound the whole request: a server that keeps sending slowly can
|
|
311
|
+
make one last longer.
|
|
312
|
+
max_age: Default maximum age of the data; ``None`` means no check.
|
|
313
|
+
Each method also takes ``max_age``, which wins over this one.
|
|
314
|
+
conditional: Use ``ETag``/``Last-Modified`` conditional requests.
|
|
315
|
+
clock: Returns the current time as an aware UTC datetime (for
|
|
316
|
+
``max_age``).
|
|
317
|
+
urls: URLs that replace the canonical ones, by method name, for a
|
|
318
|
+
product that SWPC moved: ``{"kyoto_dst": "https://..."}``.
|
|
319
|
+
|
|
320
|
+
Raises:
|
|
321
|
+
TypeError: If an argument has the wrong type, or ``transport`` lacks
|
|
322
|
+
the methods it needs.
|
|
323
|
+
ValueError: If ``timeout`` or ``max_age`` is not positive, the user
|
|
324
|
+
agent is empty or not one line, or ``urls`` names a method that
|
|
325
|
+
does not exist or gives a URL that is not HTTP.
|
|
326
|
+
ImportError: If no ``transport`` is given and httpx2 (the
|
|
327
|
+
``client`` extra) is not installed.
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
def __init__(
|
|
331
|
+
self,
|
|
332
|
+
*,
|
|
333
|
+
transport: AsyncTransport | None = None,
|
|
334
|
+
user_agent: str = DEFAULT_USER_AGENT,
|
|
335
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
336
|
+
max_age: timedelta | None = None,
|
|
337
|
+
conditional: bool = True,
|
|
338
|
+
clock: Callable[[], datetime] = _utc_now,
|
|
339
|
+
urls: Mapping[str, str] | None = None,
|
|
340
|
+
) -> None:
|
|
341
|
+
super().__init__(
|
|
342
|
+
user_agent=user_agent,
|
|
343
|
+
timeout=timeout,
|
|
344
|
+
max_age=max_age,
|
|
345
|
+
conditional=conditional,
|
|
346
|
+
clock=clock,
|
|
347
|
+
urls=urls,
|
|
348
|
+
)
|
|
349
|
+
if transport is None:
|
|
350
|
+
from swpc_kit.client._httpx import AsyncHttpxTransport
|
|
351
|
+
|
|
352
|
+
transport = AsyncHttpxTransport()
|
|
353
|
+
check_transport("AsyncClient", transport, "get", "aclose")
|
|
354
|
+
self.transport: AsyncTransport = transport
|
|
355
|
+
|
|
356
|
+
async def fetch[M: _Timed](
|
|
357
|
+
self, product: Product[M], *, max_age: timedelta | None = None
|
|
358
|
+
) -> M:
|
|
359
|
+
"""Fetch and parse ``product`` (one of :data:`PRODUCTS`).
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
product: What to fetch.
|
|
363
|
+
max_age: Maximum age of the data; ``None`` uses the client's.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
The parsed model.
|
|
367
|
+
"""
|
|
368
|
+
product = self.resolve(product)
|
|
369
|
+
try:
|
|
370
|
+
response = await self.transport.get(
|
|
371
|
+
product.url,
|
|
372
|
+
headers=self.request_headers(product),
|
|
373
|
+
timeout=self.timeout,
|
|
374
|
+
)
|
|
375
|
+
model = self.reuse(product, response)
|
|
376
|
+
if model is None:
|
|
377
|
+
model = await asyncio.to_thread(
|
|
378
|
+
product.parse, response.content, product.url
|
|
379
|
+
)
|
|
380
|
+
self.remember(product, response, model)
|
|
381
|
+
self.check_age(model, max_age)
|
|
382
|
+
return model
|
|
383
|
+
except SwpcError as error:
|
|
384
|
+
self.locate(error, product)
|
|
385
|
+
raise
|
|
386
|
+
|
|
387
|
+
async def aclose(self) -> None:
|
|
388
|
+
"""Close the transport."""
|
|
389
|
+
await self.transport.aclose()
|
|
390
|
+
|
|
391
|
+
async def __aenter__(self) -> Self:
|
|
392
|
+
return self
|
|
393
|
+
|
|
394
|
+
async def __aexit__(
|
|
395
|
+
self,
|
|
396
|
+
exc_type: type[BaseException] | None,
|
|
397
|
+
exc: BaseException | None,
|
|
398
|
+
tb: TracebackType | None,
|
|
399
|
+
) -> None:
|
|
400
|
+
await self.aclose()
|
|
401
|
+
|
|
402
|
+
async def planetary_k_index(
|
|
403
|
+
self, *, max_age: timedelta | None = None
|
|
404
|
+
) -> PlanetaryKIndex:
|
|
405
|
+
"""Fetch the 3-hour planetary K index of the last days."""
|
|
406
|
+
return await self.fetch(p.PLANETARY_K_INDEX, max_age=max_age)
|
|
407
|
+
|
|
408
|
+
async def planetary_k_index_forecast(
|
|
409
|
+
self, *, max_age: timedelta | None = None
|
|
410
|
+
) -> PlanetaryKIndexForecast:
|
|
411
|
+
"""Fetch the observed and forecast 3-hour Kp."""
|
|
412
|
+
return await self.fetch(p.PLANETARY_K_INDEX_FORECAST, max_age=max_age)
|
|
413
|
+
|
|
414
|
+
async def planetary_k_index_1m(
|
|
415
|
+
self, *, max_age: timedelta | None = None
|
|
416
|
+
) -> PlanetaryKIndex1m:
|
|
417
|
+
"""Fetch the 1-minute Kp estimates."""
|
|
418
|
+
return await self.fetch(p.PLANETARY_K_INDEX_1M, max_age=max_age)
|
|
419
|
+
|
|
420
|
+
async def kyoto_dst(self, *, max_age: timedelta | None = None) -> KyotoDst:
|
|
421
|
+
"""Fetch the hourly Kyoto Dst of the last week."""
|
|
422
|
+
return await self.fetch(p.KYOTO_DST, max_age=max_age)
|
|
423
|
+
|
|
424
|
+
async def f107_flux_30_day(self, *, max_age: timedelta | None = None) -> F107Flux:
|
|
425
|
+
"""Fetch the daily F10.7 of the last 30 days."""
|
|
426
|
+
return await self.fetch(p.F107_FLUX_30_DAY, max_age=max_age)
|
|
427
|
+
|
|
428
|
+
async def noaa_scales(self, *, max_age: timedelta | None = None) -> NoaaScales:
|
|
429
|
+
"""Fetch the NOAA space weather scales."""
|
|
430
|
+
return await self.fetch(p.NOAA_SCALES, max_age=max_age)
|
|
431
|
+
|
|
432
|
+
async def rtsw_mag_1m(self, *, max_age: timedelta | None = None) -> RtswMag1m:
|
|
433
|
+
"""Fetch the real-time solar wind magnetic field."""
|
|
434
|
+
return await self.fetch(p.RTSW_MAG_1M, max_age=max_age)
|
|
435
|
+
|
|
436
|
+
async def rtsw_wind_1m(self, *, max_age: timedelta | None = None) -> RtswWind1m:
|
|
437
|
+
"""Fetch the real-time solar wind plasma."""
|
|
438
|
+
return await self.fetch(p.RTSW_WIND_1M, max_age=max_age)
|
|
439
|
+
|
|
440
|
+
async def goes_xrays(self, *, max_age: timedelta | None = None) -> GoesXrays:
|
|
441
|
+
"""Fetch the GOES X-ray flux of the last day (primary satellite)."""
|
|
442
|
+
return await self.fetch(p.GOES_XRAYS, max_age=max_age)
|
|
443
|
+
|
|
444
|
+
async def goes_integral_protons(
|
|
445
|
+
self, *, max_age: timedelta | None = None
|
|
446
|
+
) -> GoesIntegralProtons:
|
|
447
|
+
"""Fetch the GOES integral proton flux of the last day (primary satellite)."""
|
|
448
|
+
return await self.fetch(p.GOES_INTEGRAL_PROTONS, max_age=max_age)
|
|
449
|
+
|
|
450
|
+
async def alerts(self, *, max_age: timedelta | None = None) -> Alerts:
|
|
451
|
+
"""Fetch the alerts, warnings, watches and summaries."""
|
|
452
|
+
return await self.fetch(p.ALERTS, max_age=max_age)
|
|
453
|
+
|
|
454
|
+
async def three_day_forecast(
|
|
455
|
+
self, *, max_age: timedelta | None = None
|
|
456
|
+
) -> ThreeDayForecast:
|
|
457
|
+
"""Fetch the 3-day forecast."""
|
|
458
|
+
return await self.fetch(p.THREE_DAY_FORECAST, max_age=max_age)
|
|
459
|
+
|
|
460
|
+
async def geomagnetic_forecast(
|
|
461
|
+
self, *, max_age: timedelta | None = None
|
|
462
|
+
) -> GeomagneticForecast:
|
|
463
|
+
"""Fetch the 3-day geomagnetic forecast."""
|
|
464
|
+
return await self.fetch(p.GEOMAGNETIC_FORECAST, max_age=max_age)
|
|
465
|
+
|
|
466
|
+
async def twenty_seven_day_outlook(
|
|
467
|
+
self, *, max_age: timedelta | None = None
|
|
468
|
+
) -> TwentySevenDayOutlook:
|
|
469
|
+
"""Fetch the 27-day outlook."""
|
|
470
|
+
return await self.fetch(p.TWENTY_SEVEN_DAY_OUTLOOK, max_age=max_age)
|
|
471
|
+
|
|
472
|
+
async def ovation_aurora(
|
|
473
|
+
self, *, max_age: timedelta | None = None
|
|
474
|
+
) -> OvationAurora:
|
|
475
|
+
"""Fetch the OVATION aurora forecast grid."""
|
|
476
|
+
return await self.fetch(p.OVATION_AURORA, max_age=max_age)
|
|
477
|
+
|
|
478
|
+
async def drap_global_frequencies(
|
|
479
|
+
self, *, max_age: timedelta | None = None
|
|
480
|
+
) -> DrapFrequencies:
|
|
481
|
+
"""Fetch the D-RAP global frequency grid."""
|
|
482
|
+
return await self.fetch(p.DRAP_GLOBAL_FREQUENCIES, max_age=max_age)
|