aemetxfb 0.1.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.
- aemetxfb/__init__.py +14 -0
- aemetxfb/cache.py +222 -0
- aemetxfb/clim/__init__.py +29 -0
- aemetxfb/clim/_masts.py +703 -0
- aemetxfb/clim/clim_ephem.py +293 -0
- aemetxfb/clim/clim_extremes.py +478 -0
- aemetxfb/clim/clim_normals.py +313 -0
- aemetxfb/clim/clim_thresholds.py +365 -0
- aemetxfb/obs/__init__.py +75 -0
- aemetxfb/obs/chem.py +415 -0
- aemetxfb/obs/lightning.py +128 -0
- aemetxfb/obs/masts.py +1152 -0
- aemetxfb/obs/radar.py +972 -0
- aemetxfb/obs/radiation.py +844 -0
- aemetxfb/obs/satellite.py +715 -0
- aemetxfb/pred/__init__.py +18 -0
- aemetxfb/pred/_stations.py +210 -0
- aemetxfb/pred/pred.py +522 -0
- aemetxfb/pred/stations.json +1 -0
- aemetxfb/utils.py +357 -0
- aemetxfb-0.1.0.dist-info/METADATA +275 -0
- aemetxfb-0.1.0.dist-info/RECORD +24 -0
- aemetxfb-0.1.0.dist-info/WHEEL +4 -0
- aemetxfb-0.1.0.dist-info/licenses/LICENSE +232 -0
aemetxfb/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Main module for aemetxfb package."""
|
|
2
|
+
|
|
3
|
+
from aemetxfb import cache, clim, obs, pred
|
|
4
|
+
from aemetxfb.utils import AEMetConfig, CONFIG, set_config
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"AEMetConfig",
|
|
8
|
+
"CONFIG",
|
|
9
|
+
"cache",
|
|
10
|
+
"clim",
|
|
11
|
+
"obs",
|
|
12
|
+
"pred",
|
|
13
|
+
"set_config",
|
|
14
|
+
]
|
aemetxfb/cache.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Optional disk-based response cache for AEMet API calls.
|
|
2
|
+
|
|
3
|
+
This module provides a ``@cached`` decorator backed by a single SQLite
|
|
4
|
+
database file. Cached entries expire after the TTL defined in
|
|
5
|
+
:data:`aemetxfb.utils.CONFIG` (``cache_ttl`` seconds).
|
|
6
|
+
|
|
7
|
+
The cache is **disabled by default**; enable it via
|
|
8
|
+
|
|
9
|
+
.. code-block:: python
|
|
10
|
+
|
|
11
|
+
from aemetxfb import AEMetConfig, set_config
|
|
12
|
+
|
|
13
|
+
set_config(AEMetConfig(cache_enabled=True, cache_ttl=86400))
|
|
14
|
+
|
|
15
|
+
Only pure data-returning functions should be decorated. Functions that
|
|
16
|
+
perform side-effects (downloading images, writing files) must **not**
|
|
17
|
+
use ``@cached`` because their return values are typically file paths
|
|
18
|
+
that are meaningless when replayed from cache.
|
|
19
|
+
|
|
20
|
+
Typical use case — climate data that changes rarely:
|
|
21
|
+
|
|
22
|
+
.. code-block:: python
|
|
23
|
+
|
|
24
|
+
from aemetxfb.cache import cached
|
|
25
|
+
from aemetxfb.clim import get_normals
|
|
26
|
+
|
|
27
|
+
@cached
|
|
28
|
+
def my_get_normals(station: str) -> pd.DataFrame:
|
|
29
|
+
return get_normals(station)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import hashlib
|
|
35
|
+
import pickle
|
|
36
|
+
import sqlite3
|
|
37
|
+
import time
|
|
38
|
+
from functools import wraps
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Any
|
|
41
|
+
|
|
42
|
+
from aemetxfb import utils as _utils
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# SQLite helpers
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
_DB_SCHEMA = """
|
|
50
|
+
CREATE TABLE IF NOT EXISTS cache (
|
|
51
|
+
key TEXT PRIMARY KEY,
|
|
52
|
+
value BLOB NOT NULL,
|
|
53
|
+
created REAL NOT NULL
|
|
54
|
+
);
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _get_db_path() -> Path:
|
|
59
|
+
"""Return the path to the SQLite cache database file."""
|
|
60
|
+
cache_dir = Path(_utils.CONFIG.cache_dir)
|
|
61
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
return cache_dir / "cache.db"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _get_connection() -> sqlite3.Connection:
|
|
66
|
+
"""Open (and initialise) the SQLite connection."""
|
|
67
|
+
conn = sqlite3.connect(str(_get_db_path()))
|
|
68
|
+
conn.execute(_DB_SCHEMA)
|
|
69
|
+
conn.commit()
|
|
70
|
+
return conn
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _make_key(func_name: str, args: tuple, kwargs: tuple) -> str:
|
|
74
|
+
"""Create a deterministic cache key from function name and arguments.
|
|
75
|
+
|
|
76
|
+
The key is the SHA-256 hex digest of a pickled tuple containing the
|
|
77
|
+
function name, positional arguments, and sorted keyword arguments.
|
|
78
|
+
"""
|
|
79
|
+
raw = (func_name, args, kwargs)
|
|
80
|
+
digest = hashlib.sha256(pickle.dumps(raw)).hexdigest()
|
|
81
|
+
return digest
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Low-level cache API
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def cache_get(key: str) -> Any | None:
|
|
90
|
+
"""Retrieve a value from the cache by key.
|
|
91
|
+
|
|
92
|
+
Returns ``None`` if the key is missing or has expired.
|
|
93
|
+
|
|
94
|
+
Examples
|
|
95
|
+
--------
|
|
96
|
+
>>> cache_get("nonexistent_key") is None # cache disabled by default
|
|
97
|
+
True
|
|
98
|
+
"""
|
|
99
|
+
if not _utils.CONFIG.cache_enabled:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
conn = _get_connection()
|
|
103
|
+
try:
|
|
104
|
+
row = conn.execute(
|
|
105
|
+
"SELECT value, created FROM cache WHERE key = ?", (key,)
|
|
106
|
+
).fetchone()
|
|
107
|
+
if row is None:
|
|
108
|
+
return None
|
|
109
|
+
value, created = row
|
|
110
|
+
age = time.time() - created
|
|
111
|
+
if age > _utils.CONFIG.cache_ttl:
|
|
112
|
+
conn.execute("DELETE FROM cache WHERE key = ?", (key,))
|
|
113
|
+
conn.commit()
|
|
114
|
+
return None
|
|
115
|
+
return pickle.loads(value)
|
|
116
|
+
finally:
|
|
117
|
+
conn.close()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def cache_set(key: str, value: Any) -> None:
|
|
121
|
+
"""Store a value in the cache.
|
|
122
|
+
|
|
123
|
+
If the key already exists it is overwritten.
|
|
124
|
+
"""
|
|
125
|
+
if not _utils.CONFIG.cache_enabled:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
conn = _get_connection()
|
|
129
|
+
try:
|
|
130
|
+
conn.execute(
|
|
131
|
+
"INSERT OR REPLACE INTO cache (key, value, created) VALUES (?, ?, ?)",
|
|
132
|
+
(key, pickle.dumps(value), time.time()),
|
|
133
|
+
)
|
|
134
|
+
conn.commit()
|
|
135
|
+
finally:
|
|
136
|
+
conn.close()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def clear_cache() -> None:
|
|
140
|
+
"""Delete all entries from the cache database.
|
|
141
|
+
|
|
142
|
+
The database file itself is kept so that subsequent inserts do not
|
|
143
|
+
need to re-create the schema.
|
|
144
|
+
|
|
145
|
+
Examples
|
|
146
|
+
--------
|
|
147
|
+
>>> clear_cache() # safe to call even when cache is disabled
|
|
148
|
+
"""
|
|
149
|
+
conn = _get_connection()
|
|
150
|
+
try:
|
|
151
|
+
conn.execute("DELETE FROM cache")
|
|
152
|
+
conn.commit()
|
|
153
|
+
finally:
|
|
154
|
+
conn.close()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_cache_stats() -> dict[str, int]:
|
|
158
|
+
"""Return basic statistics about the current cache.
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
dict
|
|
163
|
+
``{"size": ..., "entries": ...}`` where *size* is the file size
|
|
164
|
+
in bytes and *entries* is the number of rows in the cache table.
|
|
165
|
+
If the database does not exist yet both values are ``0``.
|
|
166
|
+
|
|
167
|
+
Examples
|
|
168
|
+
--------
|
|
169
|
+
>>> stats = get_cache_stats()
|
|
170
|
+
>>> "size" in stats and "entries" in stats
|
|
171
|
+
True
|
|
172
|
+
"""
|
|
173
|
+
db_path = _get_db_path()
|
|
174
|
+
if not db_path.exists():
|
|
175
|
+
return {"size": 0, "entries": 0}
|
|
176
|
+
size = db_path.stat().st_size
|
|
177
|
+
conn = _get_connection()
|
|
178
|
+
try:
|
|
179
|
+
count = conn.execute("SELECT COUNT(*) FROM cache").fetchone()[0]
|
|
180
|
+
finally:
|
|
181
|
+
conn.close()
|
|
182
|
+
return {"size": size, "entries": count}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
# Decorator
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def cached(func: Any) -> Any:
|
|
191
|
+
"""Decorator that caches the return value of a function.
|
|
192
|
+
|
|
193
|
+
The cache key is derived from the function name, positional arguments,
|
|
194
|
+
and keyword arguments. Entries expire after ``CONFIG.cache_ttl``
|
|
195
|
+
seconds. If ``CONFIG.cache_enabled`` is ``False`` the decorator is a
|
|
196
|
+
no-op and the underlying function is called every time.
|
|
197
|
+
|
|
198
|
+
Parameters that are not hashable (e.g. mutable lists) will cause a
|
|
199
|
+
``TypeError`` when pickling the cache key.
|
|
200
|
+
|
|
201
|
+
Examples
|
|
202
|
+
--------
|
|
203
|
+
>>> from aemetxfb import AEMetConfig, set_config # doctest: +SKIP
|
|
204
|
+
>>> from aemetxfb.cache import cached # doctest: +SKIP
|
|
205
|
+
>>> set_config(AEMetConfig(cache_enabled=True, cache_ttl=3600)) # doctest: +SKIP
|
|
206
|
+
>>> @cached # doctest: +SKIP
|
|
207
|
+
... def fetch_normals(station: str): # doctest: +SKIP
|
|
208
|
+
... return get_normals(station) # doctest: +SKIP
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
@wraps(func)
|
|
212
|
+
def wrapper(*args, **kwargs):
|
|
213
|
+
sorted_kwargs = tuple(sorted(kwargs.items()))
|
|
214
|
+
key = _make_key(func.__qualname__, args, sorted_kwargs)
|
|
215
|
+
cached_value = cache_get(key)
|
|
216
|
+
if cached_value is not None:
|
|
217
|
+
return cached_value
|
|
218
|
+
result = func(*args, **kwargs)
|
|
219
|
+
cache_set(key, result)
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
return wrapper
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Climate module for AEMet data.
|
|
2
|
+
|
|
3
|
+
This module provides access to climate-related data including
|
|
4
|
+
meteorological station information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from aemetxfb.clim._masts import STATIONS
|
|
10
|
+
from aemetxfb.clim.clim_ephem import get_clim_ephem
|
|
11
|
+
from aemetxfb.clim.clim_extremes import CLIM_EXTREMES_STATIONS, get_clim_extremes
|
|
12
|
+
from aemetxfb.clim.clim_normals import get_normals, get_normals_map
|
|
13
|
+
from aemetxfb.clim.clim_thresholds import (
|
|
14
|
+
get_clim_threshold_day,
|
|
15
|
+
get_clim_threshold_month,
|
|
16
|
+
VALID_EXTREME_PARAMS,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"STATIONS",
|
|
21
|
+
"CLIM_EXTREMES_STATIONS",
|
|
22
|
+
"VALID_EXTREME_PARAMS",
|
|
23
|
+
"get_normals",
|
|
24
|
+
"get_normals_map",
|
|
25
|
+
"get_clim_extremes",
|
|
26
|
+
"get_clim_threshold_day",
|
|
27
|
+
"get_clim_threshold_month",
|
|
28
|
+
"get_clim_ephem",
|
|
29
|
+
]
|