From 608c8dfeb9a64ad7910327d39ce62d2dcfb3c3eb Mon Sep 17 00:00:00 2001 From: Frederik Kratzert Date: Fri, 7 Nov 2025 13:23:20 +0000 Subject: [PATCH 1/2] Add Lithuania fetcher --- docs/api.rst | 1 + docs/fetchers/lithuania.rst | 5 + examples/test_lithuania_fetcher.py | 64 +++++ rivretrieve/__init__.py | 1 + .../cached_site_data/lithuania_sites.csv | 98 ++++++++ rivretrieve/lithuania.py | 227 ++++++++++++++++++ .../lithuania_aunuvenu-vms_2024-01.json | 168 +++++++++++++ tests/test_lithuania.py | 68 ++++++ 8 files changed, 632 insertions(+) create mode 100644 docs/fetchers/lithuania.rst create mode 100644 examples/test_lithuania_fetcher.py create mode 100644 rivretrieve/cached_site_data/lithuania_sites.csv create mode 100644 rivretrieve/lithuania.py create mode 100644 tests/test_data/lithuania_aunuvenu-vms_2024-01.json create mode 100644 tests/test_lithuania.py diff --git a/docs/api.rst b/docs/api.rst index 0c36d34..d9e7223 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -16,6 +16,7 @@ API Reference fetchers/france fetchers/germany_berlin fetchers/japan + fetchers/lithuania fetchers/norway fetchers/poland fetchers/portugal diff --git a/docs/fetchers/lithuania.rst b/docs/fetchers/lithuania.rst new file mode 100644 index 0000000..363ff0b --- /dev/null +++ b/docs/fetchers/lithuania.rst @@ -0,0 +1,5 @@ +Lithuania Fetcher +================= + +.. automodule:: rivretrieve.lithuania + :members: diff --git a/examples/test_lithuania_fetcher.py b/examples/test_lithuania_fetcher.py new file mode 100644 index 0000000..dce6c78 --- /dev/null +++ b/examples/test_lithuania_fetcher.py @@ -0,0 +1,64 @@ +import logging + +import matplotlib.pyplot as plt + +from rivretrieve import LithuaniaFetcher, constants + +logging.basicConfig(level=logging.INFO) + +# Example gauge ID from Meteo.lt +gauge_ids = ["aunuvenu-vms"] + +variables = [constants.DISCHARGE_DAILY_MEAN, constants.STAGE_DAILY_MEAN] + +# Period to fetch +start_date = "2024-01-01" +end_date = "2024-01-31" + +fetcher = LithuaniaFetcher() + +# metadata = fetcher.get_metadata() +# print(metadata.head()) + +for variable in variables: + plt.figure(figsize=(12, 6)) + print(f"\n--- Testing variable: {variable} ---") + has_data = False + for gauge_id in gauge_ids: + print(f"Fetching {variable} for {gauge_id} from {start_date} to {end_date}...") + data = fetcher.get_data( + gauge_id=gauge_id, + variable=variable, + start_date=start_date, + end_date=end_date, + ) + if not data.empty: + print(f"Data for {gauge_id}:") + print(data.head()) + print(f"Time series from {data.index.min()} to {data.index.max()}") + plt.plot( + data.index, + data[variable], + label=f"{gauge_id} - {variable}", + marker=".", + linestyle="-", + ) + has_data = True + else: + print(f"No {variable} data found for {gauge_id}") + + if has_data: + plt.xlabel(constants.TIME_INDEX) + unit = "m³/s" if variable == constants.DISCHARGE_DAILY_MEAN else "m" + plt.ylabel(f"{variable} ({unit})") + plt.title(f"Lithuania River Data ({gauge_ids[0]} - {start_date} to {end_date})") + plt.legend() + plt.grid(True) + plt.tight_layout() + plot_path = f"lithuania_{variable}_plot.png" + plt.savefig(plot_path) + print(f"Plot saved to {plot_path}") + else: + print(f"No data to plot for {variable}.") + +print("Test finished.") diff --git a/rivretrieve/__init__.py b/rivretrieve/__init__.py index 2625760..ae2e150 100644 --- a/rivretrieve/__init__.py +++ b/rivretrieve/__init__.py @@ -9,6 +9,7 @@ from .france import FranceFetcher from .germany_berlin import GermanyBerlinFetcher from .japan import JapanFetcher +from .lithuania import LithuaniaFetcher from .norway import NorwayFetcher from .poland import PolandFetcher from .portugal import PortugalFetcher diff --git a/rivretrieve/cached_site_data/lithuania_sites.csv b/rivretrieve/cached_site_data/lithuania_sites.csv new file mode 100644 index 0000000..541442b --- /dev/null +++ b/rivretrieve/cached_site_data/lithuania_sites.csv @@ -0,0 +1,98 @@ +gauge_id,station_name,river,latitude,longitude,altitude,area,country,source +anyksciu-vms,Anykščių VMS,Šventoji,55.520924,25.091461,,,Lithuania,Meteo.lt +aunuvenu-vms,Aunuvėnų VMS,Aunuva,55.850369,22.765429,,,Lithuania,Meteo.lt +babtu-vms,Babtų VMS,Nevėžis,55.105762,23.782552,,,Lithuania,Meteo.lt +bernatoniu-levuo-vms,Bernatonių VMS,Lėvuo,55.793849,24.281908,,,Lithuania,Meteo.lt +bernatoniu-sanziles-vms,Bernatonių VMS,Sanžilės kanalas,55.788833,24.279353,,,Lithuania,Meteo.lt +birstono-vms,Birštono VMS,Nemunas,54.61352,24.033594,,,Lithuania,Meteo.lt +buivydziu-vms,Buivydžių VMS,Neris,54.836266,25.741858,,,Lithuania,Meteo.lt +civoniu-vms,Čivonių VMS,Šlavantas,54.130368,23.635849,,,Lithuania,Meteo.lt +darsuniskio-vms,Darsūniškio VMS,Nemunas,54.73433,24.11369,,,Lithuania,Meteo.lt +drabuzninku-vms,Drabužninkų VMS,Spindžius,54.572739,24.669575,,,Lithuania,Meteo.lt +druskininku-vms,Druskininkų VMS,Nemunas,54.018664,23.983581,,,Lithuania,Meteo.lt +dubininko-vms,Dubininko VMS,Skroblus,54.090454,24.294633,,,Lithuania,Meteo.lt +eiduku-vms,Eidukų VMS,Upita,55.5907,21.5891,,,Lithuania,Meteo.lt +girutiskes-vms,Girutiškės VMS,Šventas,55.603315,26.318259,,,Lithuania,Meteo.lt +guntauninku-vms,Guntauninkų VMS,Svyla,55.251342,26.594287,,,Lithuania,Meteo.lt +jasiunu-vms,Jašiūnų VMS,Merkys,54.434877,25.327865,,,Lithuania,Meteo.lt +jonavos-vms,Jonavos VMS,Neris,55.075005,24.29199,,,Lithuania,Meteo.lt +josvainiu-vms,Josvainių VMS,Šušvė,55.242395,23.832754,,,Lithuania,Meteo.lt +juodkrantes-vms,Juodkrantės VMS,Kuršių marios,55.533293,21.121437,,,Lithuania,Meteo.lt +kacergiskes-vms,Kačergiškės VMS,Dysna,55.355261,26.42559,,,Lithuania,Meteo.lt +kamorunu-vms,Kamorūnų VMS,Vilkas,54.119327,23.813996,,,Lithuania,Meteo.lt +kartenos-vms,Kartenos VMS,Minija,55.909629,21.467874,,,Lithuania,Meteo.lt +kauno-vms,Kauno VMS,Nemunas,54.882091,23.926865,,,Lithuania,Meteo.lt +klaipedos-juru-uosto-vms,Klaipėdos jūrų uosto VMS,Baltijos jūra,55.713148,21.11915,,,Lithuania,Meteo.lt +klaipedos-vms,Klaipėdos VMS,Akmena-Danė,55.755884,21.135223,,,Lithuania,Meteo.lt +kretingos-vms,Kretingos VMS,Akmena-Danė,55.860562,21.220636,,,Lithuania,Meteo.lt +kudirkos-naumiescio-vms,Kudirkos Naumiesčio VMS,Šešupė,54.77723,22.86552,,,Lithuania,Meteo.lt +kulynu-vms,Kūlynų VMS,Leitė,55.247873,21.482476,,,Lithuania,Meteo.lt +kupiskio-vms,Kupiškio VMS,Lėvuo,55.833417,24.93249,,,Lithuania,Meteo.lt +kvetku-vms,Kvetkų VMS,Nemunėlis,56.151452,25.140653,,,Lithuania,Meteo.lt +kyburiu-vms,Kyburių VMS,Yslykis,56.246224,24.238581,,,Lithuania,Meteo.lt +lampedziu-vms,Lampėdžių VMS,Nemunas,54.906414,23.817574,,,Lithuania,Meteo.lt +lankupiu-klaipedos-vms,Lankupių VMS,Klaipėdos kanalas,55.492184,21.337453,,,Lithuania,Meteo.lt +lankupiu-minija-vms,Lankupių VMS,Minija,55.483301,21.36597,,,Lithuania,Meteo.lt +lazdenu-vms,Lazdėnų VMS,Nemunas,55.124307,21.759532,,,Lithuania,Meteo.lt +lazinku-vms,Lazinkų VMS,Birvėta,55.284716,26.784597,,,Lithuania,Meteo.lt +leckavos-vms,Leckavos VMS,Venta,56.390768,22.231794,,,Lithuania,Meteo.lt +likenu-vms,Likėnų VMS,Smardonė,56.19808,24.620299,,,Lithuania,Meteo.lt +liubavo-vms,Liubavo VMS,Šešupė,54.35698,23.063608,,,Lithuania,Meteo.lt +liukoniu-vms,Liukonių VMS,Širvinta,55.026816,24.675293,,,Lithuania,Meteo.lt +lyduvenu-vms,Lyduvėnų VMS,Dubysa,55.505109,23.089659,,,Lithuania,Meteo.lt +lynezerio-vms,Lynežerio VMS,Beržupis,54.062111,24.571774,,,Lithuania,Meteo.lt +miestaliu-vms,Miestalių VMS,Tenenys,55.433132,21.499001,,,Lithuania,Meteo.lt +mikuziu-vms,Mikužių VMS,Veiviržas,55.559223,21.522166,,,Lithuania,Meteo.lt +musteikos-vms,Musteikos VMS,Musteika,53.956795,24.366187,,,Lithuania,Meteo.lt +nemajunu-vms,Nemajūnų VMS,Nemunas,54.554227,24.072006,,,Lithuania,Meteo.lt +paakmenio-vms,Paakmenio VMS,Akmena,55.490125,22.294283,,,Lithuania,Meteo.lt +pabrades-vms,Pabradės VMS,Žeimena,54.983728,25.772459,,,Lithuania,Meteo.lt +padubysio-vms,Padubysio VMS,Dubysa,55.202228,23.503463,,,Lithuania,Meteo.lt +paelnes-vms,Paelnės VMS,Alnis,55.263074,25.70617,,,Lithuania,Meteo.lt +pajurio-vms,Pajūrio VMS,Jūra,55.450275,22.020419,,,Lithuania,Meteo.lt +palangos-vms,Palangos VMS,Baltijos jūra,55.920336,21.044301,,,Lithuania,Meteo.lt +panemunes-vms,Panemunės VMS,Nemunas,55.086526,21.902807,,,Lithuania,Meteo.lt +panevezio-vms,Panevėžio VMS,Nevėžis,55.738344,24.267973,,,Lithuania,Meteo.lt +papiles-vms,Papilės VMS,Venta,56.149886,22.785504,,,Lithuania,Meteo.lt +paramelio-vms,Paramėlio VMS,Katra,54.01109,24.635779,,,Lithuania,Meteo.lt +pazeimenes-vms,Pažeimenės VMS,Mera-Kūna,54.985137,25.908503,,,Lithuania,Meteo.lt +plaskiu-vms,Plaškių VMS,Gėgė,55.180952,21.725636,,,Lithuania,Meteo.lt +pluskiu-vms,Pluskių VMS,Kražantė,55.56874,22.839271,,,Lithuania,Meteo.lt +pozeres-vms,Požerės VMS,Paršežeris,55.640743,22.300626,,,Lithuania,Meteo.lt +priekules-vms,Priekulės VMS,Minija,55.549907,21.330332,,,Lithuania,Meteo.lt +puvociu-vms,Puvočių VMS,Merkys,54.116079,24.302952,,,Lithuania,Meteo.lt +rimsoniu-vms,Rimšonių VMS,Daugyvenė,56.00169,23.964458,,,Lithuania,Meteo.lt +rusiskiu-vms,Rušiškių VMS,Girutiškis,55.209629,25.857363,,,Lithuania,Meteo.lt +rusnes-atmata-vms,Rusnės VMS,Atmata,55.30066,21.3803,,,Lithuania,Meteo.lt +rusnes-skirvyte-vms,Rusnės VMS,Skirvytė,55.287031,21.371227,,,Lithuania,Meteo.lt +sarkiu-vms,Šarkių VMS,Sidabra,56.308952,23.621228,,,Lithuania,Meteo.lt +semeliskiu-vms,Semeliškių VMS,Strėva,54.668964,24.653065,,,Lithuania,Meteo.lt +siaulenu-vms,Šiaulėnų VMS,Šušvė,55.675663,23.446673,,,Lithuania,Meteo.lt +silininku-vms,Šilininkų VMS,Nemunas,55.198714,21.566672,,,Lithuania,Meteo.lt +silutes-vms,Šilutės VMS,Šyša,55.337168,21.475836,,,Lithuania,Meteo.lt +skirgailu-vms,Skirgailų VMS,Šešuvis,55.208908,22.305419,,,Lithuania,Meteo.lt +skuodo-vms,Skuodo VMS,Bartuva,56.280435,21.508347,,,Lithuania,Meteo.lt +smalininku-vms,Smalininkų VMS,Nemunas,55.072284,22.585918,,,Lithuania,Meteo.lt +sventosios-vms,Šventosios VMS,Šventoji,56.035485,21.119474,,,Lithuania,Meteo.lt +tabokines-vms,Tabokinės VMS,Nemunėlis,56.41415,24.855184,,,Lithuania,Meteo.lt +taurages-vms,Tauragės VMS,Jūra,55.250397,22.280468,,,Lithuania,Meteo.lt +tauragnu-vms,Tauragnų VMS,Tauragnas,55.442766,25.826318,,,Lithuania,Meteo.lt +traupio-vms,Traupio VMS,Nevėžis,55.510482,24.746812,,,Lithuania,Meteo.lt +trecioniu-vms,Trečionių VMS,Tatula,56.145003,24.518004,,,Lithuania,Meteo.lt +ukmerges-vms,Ukmergės VMS,Šventoji,55.247456,24.768688,,,Lithuania,Meteo.lt +uostadvario-vms,Uostadvario VMS,Atmata,55.344016,21.290822,,,Lithuania,Meteo.lt +ustukiu-vms,Ustukių VMS,Mūša,56.065853,24.370572,,,Lithuania,Meteo.lt +vaineikiu-vms,Vaineikių VMS,Platonis,56.316568,23.571321,,,Lithuania,Meteo.lt +valkininku-vms,Valkininkų VMS,Šalčia,54.339546,24.850475,,,Lithuania,Meteo.lt +varenos-vms,Varėnos VMS,Varėnė,54.249671,24.555675,,,Lithuania,Meteo.lt +varniu-vms,Varnių VMS,Stervas,55.770855,22.401059,,,Lithuania,Meteo.lt +verbyliskiu-vms,Verbyliškių VMS,Verknė,54.572081,24.182523,,,Lithuania,Meteo.lt +viasniunu-vms,Viašniūnų VMS,Baluošas,55.387729,26.061933,,,Lithuania,Meteo.lt +vilniaus-neris-vms,Vilniaus VMS,Neris,54.691858,25.276258,,,Lithuania,Meteo.lt +vilniaus-vilnia-vms,Vilniaus VMS,Vilnia,54.679215,25.294857,,,Lithuania,Meteo.lt +zadeikiu-vms,Žadeikių VMS,Pyvesa,56.033584,24.43964,,,Lithuania,Meteo.lt +zagares-vms,Žagarės VMS,Švėtė,56.360208,23.251806,,,Lithuania,Meteo.lt +zervynu-vms,Zervynų VMS,Ūla-Pelesa,54.109543,24.498255,,,Lithuania,Meteo.lt +zilpamusio-vms,Žilpamūšio VMS,Mūša,56.194006,24.423412,,,Lithuania,Meteo.lt +zindaiciu-vms,Žindaičių VMS,Mituva,55.172566,22.705305,,,Lithuania,Meteo.lt +zuvinto-vms,Žuvinto VMS,Žuvintas,54.45,23.583,,,Lithuania,Meteo.lt diff --git a/rivretrieve/lithuania.py b/rivretrieve/lithuania.py new file mode 100644 index 0000000..e4d24cc --- /dev/null +++ b/rivretrieve/lithuania.py @@ -0,0 +1,227 @@ +"""Fetcher for Lithuanian river gauge data from Meteo.lt.""" + +import logging +import time +from collections import deque +from typing import Any, Dict, List, Optional + +import pandas as pd +import requests + +from . import base, constants, utils + +logger = logging.getLogger(__name__) + + +class LithuaniaFetcher(base.RiverDataFetcher): + """Fetches river gauge data from Lithuania's meteorological service (Meteo.lt). + + Data Source: Meteo.lt API https://api.meteo.lt/v1/ + + Supported Variables: + - constants.DISCHARGE_DAILY_MEAN (m³/s) + - constants.STAGE_DAILY_MEAN (m) + + Terms of Use: + - For the license of the data and the terms of use, see https://api.meteo.lt/ + """ + + METADATA_URL = "https://api.meteo.lt/v1/hydro-stations" + DATA_URL_TEMPLATE = "https://api.meteo.lt/v1/hydro-stations/{}/observations/historical/{}" + + # To comply with the 180 requests/minute limit + request_times: deque[float] = deque(maxlen=180) + + @staticmethod + def get_cached_metadata() -> pd.DataFrame: + """Retrieves cached metadata (if available).""" + return utils.load_cached_metadata_csv("lithuania") + + def get_metadata(self) -> pd.DataFrame: + """Downloads and parses site metadata from Meteo.lt.""" + headers = { + "Accept": "application/json", + } + s = utils.requests_retry_session() + try: + logger.info(f"Fetching Lithuania metadata from {self.METADATA_URL}") + resp = s.get(self.METADATA_URL, headers=headers, timeout=20) + resp.raise_for_status() + stations = resp.json() + + df = pd.json_normalize(stations) + + rename_map = { + "code": constants.GAUGE_ID, + "name": constants.STATION_NAME, + "waterBody": constants.RIVER, + "coordinates.latitude": constants.LATITUDE, + "coordinates.longitude": constants.LONGITUDE, + } + df = df.rename(columns=rename_map) + + df[constants.ALTITUDE] = None + df[constants.AREA] = None + df[constants.COUNTRY] = "Lithuania" + df[constants.SOURCE] = "Meteo.lt" + + df = df[ + [ + constants.GAUGE_ID, + constants.STATION_NAME, + constants.RIVER, + constants.LATITUDE, + constants.LONGITUDE, + constants.ALTITUDE, + constants.AREA, + constants.COUNTRY, + constants.SOURCE, + ] + ] + + df[constants.GAUGE_ID] = df[constants.GAUGE_ID].astype(str).str.strip() + df[constants.LATITUDE] = pd.to_numeric(df[constants.LATITUDE], errors="coerce") + df[constants.LONGITUDE] = pd.to_numeric(df[constants.LONGITUDE], errors="coerce") + + logger.info(f"Fetched {len(df)} Lithuania gauge metadata records.") + return df.set_index(constants.GAUGE_ID) + + except Exception as e: + logger.error(f"Failed to fetch Meteo.lt metadata: {e}") + return pd.DataFrame(columns=[constants.GAUGE_ID]).set_index(constants.GAUGE_ID) + + @staticmethod + def get_available_variables() -> tuple[str, ...]: + return (constants.DISCHARGE_DAILY_MEAN, constants.STAGE_DAILY_MEAN) + + def _get_api_variable(self, variable: str) -> str: + if variable == constants.DISCHARGE_DAILY_MEAN: + return "waterDischarge" + elif variable == constants.STAGE_DAILY_MEAN: + return "waterLevel" + else: + raise ValueError(f"Unsupported variable: {variable}") + + def _throttle_requests(self): + """Ensures API rate limit is not exceeded.""" + now = time.time() + while self.request_times and now - self.request_times[0] > 60: + self.request_times.popleft() + + if len(self.request_times) >= 180: + wait_time = 60 - (now - self.request_times[0]) + 0.1 # Add a small buffer + logger.info(f"Rate limit reached. Waiting {wait_time:.1f} seconds...") + time.sleep(wait_time) + # Clear outdated timestamps after sleep + now = time.time() + while self.request_times and now - self.request_times[0] > 60: + self.request_times.popleft() + self.request_times.append(time.time()) + + def _download_data( + self, + gauge_id: str, + variable: str, + start_date: str, + end_date: str, + ) -> List[Dict[str, Any]]: + """Downloads raw data in monthly chunks from the Meteo.lt API.""" + s = utils.requests_retry_session() + headers = { + "Accept": "application/json", + } + all_data = [] + + date_range = pd.date_range(start=start_date, end=end_date, freq="MS") + + for dt in date_range: + date_str = dt.strftime("%Y-%m") + url = self.DATA_URL_TEMPLATE.format(gauge_id, date_str) + + self._throttle_requests() + + logger.debug(f"Fetching {gauge_id} ({variable}) - {date_str} from {url}") + try: + r = s.get(url, headers=headers, timeout=20) + + if r.status_code == 404: + logger.debug(f"No data for {gauge_id} for {date_str}") + continue + r.raise_for_status() + + js = r.json() + obs = js.get("observations", []) + if obs: + all_data.extend(obs) + except requests.exceptions.RequestException as e: + logger.error(f"Failed to download {gauge_id} for {date_str}: {e}") + except Exception as e: + logger.error(f"Error processing response for {gauge_id} {date_str}: {e}") + + return all_data + + def _parse_data(self, gauge_id: str, raw_data: List[Dict[str, Any]], variable: str) -> pd.DataFrame: + """Parses the raw JSON data.""" + if not raw_data: + return pd.DataFrame(columns=[constants.TIME_INDEX, variable]) + + api_variable = self._get_api_variable(variable) + try: + df = pd.DataFrame(raw_data) + if df.empty or "observationDateUtc" not in df.columns or api_variable not in df.columns: + logger.warning(f"Missing expected columns for site {gauge_id}") + return pd.DataFrame(columns=[constants.TIME_INDEX, variable]) + + df[constants.TIME_INDEX] = pd.to_datetime(df["observationDateUtc"], errors="coerce").dt.tz_localize("UTC") + df[variable] = pd.to_numeric(df[api_variable], errors="coerce") + + # Unit conversion + if variable == constants.STAGE_DAILY_MEAN: + df[variable] = df[variable] / 100.0 # cm to m + + df = df.dropna(subset=[constants.TIME_INDEX, variable]) + + if df.empty: + logger.warning(f"DataFrame empty after dropna for {gauge_id} {variable}") + return pd.DataFrame(columns=[constants.TIME_INDEX, variable]) + + return ( + df[[constants.TIME_INDEX, variable]] + .sort_values(by=constants.TIME_INDEX) + .set_index(constants.TIME_INDEX) + ) + except Exception as e: + logger.error(f"Error parsing JSON data for site {gauge_id}: {e}") + return pd.DataFrame(columns=[constants.TIME_INDEX, variable]) + + def get_data( + self, + gauge_id: str, + variable: str, + start_date: Optional[str] = None, + end_date: Optional[str] = None, + ) -> pd.DataFrame: + """Fetches and parses time series data for a specific gauge and variable.""" + start_date = utils.format_start_date(start_date) + end_date = utils.format_end_date(end_date) + + if variable not in self.get_available_variables(): + raise ValueError(f"Unsupported variable: {variable}") + + try: + raw_data = self._download_data(gauge_id, variable, start_date, end_date) + df = self._parse_data(gauge_id, raw_data, variable) + + if df.empty: + logger.debug(f"Parsed DataFrame is empty for {gauge_id} {variable}") + return df + + # Filter to the exact date range + start_date_dt = pd.to_datetime(start_date).tz_localize("UTC") + end_date_dt = pd.to_datetime(end_date).tz_localize("UTC") + pd.Timedelta(days=1) + + df_filtered = df[(df.index >= start_date_dt) & (df.index < end_date_dt)] + return df_filtered + except Exception as e: + logger.error(f"Failed to get data for site {gauge_id}, variable {variable}: {e}") + return pd.DataFrame(columns=[constants.TIME_INDEX, variable]) diff --git a/tests/test_data/lithuania_aunuvenu-vms_2024-01.json b/tests/test_data/lithuania_aunuvenu-vms_2024-01.json new file mode 100644 index 0000000..04b0713 --- /dev/null +++ b/tests/test_data/lithuania_aunuvenu-vms_2024-01.json @@ -0,0 +1,168 @@ +{ + "station": { + "code": "aunuvenu-vms", + "name": "Aunuvėnų VMS", + "waterBody": "Aunuva", + "coordinates": { + "latitude": 55.850369, + "longitude": 22.765429 + } + }, + "observations": [ + { + "observationDateUtc": "2024-01-01", + "waterLevel": 245, + "waterDischarge": 1.96 + }, + { + "observationDateUtc": "2024-01-02", + "waterLevel": 225, + "waterDischarge": 1.62 + }, + { + "observationDateUtc": "2024-01-03", + "waterLevel": 206, + "waterDischarge": 1.32 + }, + { + "observationDateUtc": "2024-01-04", + "waterLevel": 179, + "waterDischarge": 0.93 + }, + { + "observationDateUtc": "2024-01-05", + "waterLevel": 157, + "waterDischarge": 0.69 + }, + { + "observationDateUtc": "2024-01-06", + "waterLevel": 142, + "waterDischarge": 0.54 + }, + { + "observationDateUtc": "2024-01-07", + "waterLevel": 128, + "waterDischarge": 0.4 + }, + { + "observationDateUtc": "2024-01-08", + "waterLevel": 119, + "waterDischarge": 0.32 + }, + { + "observationDateUtc": "2024-01-09", + "waterLevel": 116, + "waterDischarge": 0.3 + }, + { + "observationDateUtc": "2024-01-10", + "waterLevel": 118, + "waterDischarge": 0.32 + }, + { + "observationDateUtc": "2024-01-11", + "waterLevel": 119, + "waterDischarge": 0.32 + }, + { + "observationDateUtc": "2024-01-12", + "waterLevel": 115, + "waterDischarge": 0.29 + }, + { + "observationDateUtc": "2024-01-13", + "waterLevel": 110, + "waterDischarge": 0.25 + }, + { + "observationDateUtc": "2024-01-14", + "waterLevel": 112, + "waterDischarge": 0.27 + }, + { + "observationDateUtc": "2024-01-15", + "waterLevel": 117, + "waterDischarge": 0.31 + }, + { + "observationDateUtc": "2024-01-16", + "waterLevel": 113, + "waterDischarge": 0.26 + }, + { + "observationDateUtc": "2024-01-17", + "waterLevel": 111, + "waterDischarge": 0.25 + }, + { + "observationDateUtc": "2024-01-18", + "waterLevel": 108, + "waterDischarge": 0.22 + }, + { + "observationDateUtc": "2024-01-19", + "waterLevel": 107, + "waterDischarge": 0.21 + }, + { + "observationDateUtc": "2024-01-20", + "waterLevel": 108, + "waterDischarge": 0.21 + }, + { + "observationDateUtc": "2024-01-21", + "waterLevel": 107, + "waterDischarge": 0.21 + }, + { + "observationDateUtc": "2024-01-22", + "waterLevel": 107, + "waterDischarge": 0.21 + }, + { + "observationDateUtc": "2024-01-23", + "waterLevel": 146, + "waterDischarge": 0.64 + }, + { + "observationDateUtc": "2024-01-24", + "waterLevel": 250, + "waterDischarge": 2.17 + }, + { + "observationDateUtc": "2024-01-25", + "waterLevel": 264, + "waterDischarge": 2.51 + }, + { + "observationDateUtc": "2024-01-26", + "waterLevel": 245, + "waterDischarge": 2.07 + }, + { + "observationDateUtc": "2024-01-27", + "waterLevel": 220, + "waterDischarge": 1.57 + }, + { + "observationDateUtc": "2024-01-28", + "waterLevel": 196, + "waterDischarge": 1.15 + }, + { + "observationDateUtc": "2024-01-29", + "waterLevel": 185, + "waterDischarge": 1 + }, + { + "observationDateUtc": "2024-01-30", + "waterLevel": 176, + "waterDischarge": 0.91 + }, + { + "observationDateUtc": "2024-01-31", + "waterLevel": 172, + "waterDischarge": 0.88 + } + ] +} \ No newline at end of file diff --git a/tests/test_lithuania.py b/tests/test_lithuania.py new file mode 100644 index 0000000..ce38be5 --- /dev/null +++ b/tests/test_lithuania.py @@ -0,0 +1,68 @@ +import json +import os +import unittest +from unittest.mock import patch + +import pandas as pd +from pandas.testing import assert_frame_equal + +from rivretrieve import LithuaniaFetcher, constants + + +class TestLithuaniaFetcher(unittest.TestCase): + def setUp(self): + self.fetcher = LithuaniaFetcher() + self.test_data_dir = os.path.join(os.path.dirname(__file__), "test_data") + self.gauge_id = "aunuvenu-vms" + + def load_sample_json(self, filename): + with open(os.path.join(self.test_data_dir, filename), "r") as f: + return json.load(f) + + def mocked_download_data(self, gauge_id, variable, start_date, end_date): + # This mock function will return the sample data regardless of the date range + # as we only have one sample file. + sample_data = self.load_sample_json("lithuania_aunuvenu-vms_2024-01.json") + return sample_data.get("observations", []) + + @patch("rivretrieve.lithuania.LithuaniaFetcher._download_data") + def test_get_data_discharge(self, mock_download): + mock_download.side_effect = self.mocked_download_data + + variable = constants.DISCHARGE_DAILY_MEAN + start_date = "2024-01-01" + end_date = "2024-01-03" + + result_df = self.fetcher.get_data(self.gauge_id, variable, start_date, end_date) + + expected_data = { + constants.TIME_INDEX: pd.to_datetime(["2024-01-01", "2024-01-02", "2024-01-03"], utc=True), + constants.DISCHARGE_DAILY_MEAN: [1.96, 1.62, 1.32], + } + expected_df = pd.DataFrame(expected_data).set_index(constants.TIME_INDEX) + + assert_frame_equal(result_df, expected_df) + mock_download.assert_called_once_with(self.gauge_id, variable, "2024-01-01", "2024-01-03") + + @patch("rivretrieve.lithuania.LithuaniaFetcher._download_data") + def test_get_data_stage(self, mock_download): + mock_download.side_effect = self.mocked_download_data + + variable = constants.STAGE_DAILY_MEAN + start_date = "2024-01-29" + end_date = "2024-01-31" + + result_df = self.fetcher.get_data(self.gauge_id, variable, start_date, end_date) + + expected_data = { + constants.TIME_INDEX: pd.to_datetime(["2024-01-29", "2024-01-30", "2024-01-31"], utc=True), + constants.STAGE_DAILY_MEAN: [1.85, 1.76, 1.72], # Divided by 100 + } + expected_df = pd.DataFrame(expected_data).set_index(constants.TIME_INDEX) + + assert_frame_equal(result_df, expected_df) + mock_download.assert_called_once_with(self.gauge_id, variable, "2024-01-29", "2024-01-31") + + +if __name__ == "__main__": + unittest.main() From f753d07d392ccc647ddc1449f48025abb5a773ca Mon Sep 17 00:00:00 2001 From: Frederik Kratzert Date: Fri, 7 Nov 2025 13:31:31 +0000 Subject: [PATCH 2/2] Increase version to 0.2.0 --- rivretrieve/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rivretrieve/__about__.py b/rivretrieve/__about__.py index 3dc1f76..d3ec452 100644 --- a/rivretrieve/__about__.py +++ b/rivretrieve/__about__.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.2.0"