"""The ERA5 lake: grid math, layout keys, and the read/slice client (no network — the local-dir source stands in for the bucket).""" import datetime import polars as pl import pytest from data import era5lake def _daily_frame(start="1940-01-01", days=32000): d0 = datetime.date.fromisoformat(start) dates = [d0 + datetime.timedelta(days=k) for k in range(days)] n = len(dates) return pl.DataFrame({ "date": dates, "tmax": [70.0] * n, "tmin": [50.0] * n, "precip": [0.1] * n, "wind": [8.0] * n, "gust": [14.0] * n, "humid": [55.0] * n, "fmax": [71.0] * n, "fmin": [48.0] * n, }) def test_grid_roundtrip_and_bounds(): # Nearest-index snap, poles clamped, longitude wrapped to [0, 360). assert era5lake.to_idx(90.0, 0.0) == (0, 0) assert era5lake.to_idx(-90.0, 0.0) == (720, 0) assert era5lake.to_idx(51.5074, -0.1278) == (154, 1439) i, j = era5lake.to_idx(35.6762, 139.6503) lat, lon = era5lake.to_coords(i, j) assert abs(lat - 35.6762) <= era5lake.STEP / 2 + 1e-9 assert abs(lon - 139.6503) <= era5lake.STEP / 2 + 1e-9 def test_tile_and_keys_align_with_hive_layout(): assert era5lake.tile_of(154, 1439) == (12, 119) assert era5lake.point_key(154, 1439) == "era5/points/lat=154/lon=1439.parquet" assert era5lake.daily_part_key(12, 119, 1994, 3) == \ "era5/daily/tile=12_119/year=1994/month=03/part.parquet" def test_s3_config_requires_both_keys(monkeypatch): monkeypatch.delenv("THERMOGRAPH_LAKE_S3_ACCESS_KEY", raising=False) monkeypatch.delenv("THERMOGRAPH_LAKE_S3_SECRET_KEY", raising=False) assert era5lake.s3_config() is None monkeypatch.setenv("THERMOGRAPH_LAKE_S3_ACCESS_KEY", "k") assert era5lake.s3_config() is None monkeypatch.setenv("THERMOGRAPH_LAKE_S3_SECRET_KEY", "s") cfg = era5lake.s3_config() assert cfg["bucket"] == "era5-thermograph" opts = era5lake.storage_options(cfg) assert opts["aws_virtual_hosted_style_request"] == "false" def _write_point(tmp_path, cell, df): i, j = era5lake.to_idx(cell["center_lat"], cell["center_lon"]) path = tmp_path / era5lake.point_key(i, j) path.parent.mkdir(parents=True, exist_ok=True) df.write_parquet(path) CELL = {"center_lat": 51.5, "center_lon": -0.125} def test_fetch_slices_to_start_and_keeps_schema(monkeypatch, tmp_path): monkeypatch.setenv("THERMOGRAPH_LAKE_LOCAL_DIR", str(tmp_path)) _write_point(tmp_path, CELL, _daily_frame()) df = era5lake.fetch_history_lake(CELL, start="1980-01-01") assert df["date"].min() == datetime.date(1980, 1, 1) assert set(df.columns) == {"date", "tmax", "tmin", "precip", "wind", "gust", "humid", "fmax", "fmin"} # Without a start, the whole 1940+ record comes back. assert era5lake.fetch_history_lake(CELL).height == 32000 def test_fetch_rejects_short_and_missing(monkeypatch, tmp_path): monkeypatch.setenv("THERMOGRAPH_LAKE_LOCAL_DIR", str(tmp_path)) with pytest.raises(era5lake.LakeUnavailable): era5lake.fetch_history_lake(CELL) # nothing written _write_point(tmp_path, CELL, _daily_frame(days=500)) with pytest.raises(era5lake.LakeUnavailable): # implausibly short record era5lake.fetch_history_lake(CELL) def test_fetch_unconfigured_raises(monkeypatch): monkeypatch.delenv("THERMOGRAPH_LAKE_LOCAL_DIR", raising=False) monkeypatch.delenv("THERMOGRAPH_LAKE_URL", raising=False) monkeypatch.delenv("THERMOGRAPH_LAKE_S3_ACCESS_KEY", raising=False) monkeypatch.delenv("THERMOGRAPH_LAKE_S3_SECRET_KEY", raising=False) with pytest.raises(era5lake.LakeUnavailable): era5lake.fetch_history_lake(CELL)