Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/_static/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ API Reference
fetchers/slovenia
fetchers/southafrica
fetchers/spain
fetchers/sweden
fetchers/uk_ea
fetchers/uk_nrfa
fetchers/usa
5 changes: 5 additions & 0 deletions docs/fetchers/sweden.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Sweden Fetcher
==============

.. automodule:: rivretrieve.sweden
:members:
31 changes: 31 additions & 0 deletions examples/test_sweden_fetcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import logging

import matplotlib.pyplot as plt

from rivretrieve import SwedenFetcher, constants

logging.basicConfig(level=logging.INFO)

gauge_id = "2357"
variable = constants.DISCHARGE_DAILY_MEAN
start_date = "2026-03-01"
end_date = "2026-03-15"

fetcher = SwedenFetcher()
data = fetcher.get_data(gauge_id=gauge_id, variable=variable, start_date=start_date, end_date=end_date)

if data.empty:
print(f"No data found for {gauge_id} ({variable})")
else:
print(data.head())
plt.figure(figsize=(12, 6))
plt.plot(data.index, data[variable], label=f"{gauge_id} - {variable}")
plt.xlabel(constants.TIME_INDEX)
plt.ylabel("m³/s")
plt.title(f"Sweden River Data ({gauge_id})")
plt.legend()
plt.grid(True)
plt.tight_layout()
plot_path = "sweden_discharge_plot.png"
plt.savefig(plot_path)
print(f"Plot saved to {plot_path}")
1 change: 1 addition & 0 deletions rivretrieve/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .slovenia import SloveniaFetcher
from .southafrica import SouthAfricaFetcher
from .spain import SpainFetcher
from .sweden import SwedenFetcher
from .uk_ea import UKEAFetcher
from .uk_nrfa import UKNRFAFetcher
from .usa import USAFetcher
Expand Down
901 changes: 901 additions & 0 deletions rivretrieve/cached_site_data/sweden_sites.csv

Large diffs are not rendered by default.

251 changes: 251 additions & 0 deletions rivretrieve/sweden.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
"""Fetcher for Swedish hydrological data from SMHI HydroObs."""

import logging
from typing import Any, Optional

import numpy as np
import pandas as pd
import requests

from . import base, constants, utils

logger = logging.getLogger(__name__)


class SwedenFetcher(base.RiverDataFetcher):
"""Fetches Swedish hydrological time series and station metadata from SMHI HydroObs.

Data source:
https://opendata.smhi.se/hydroobs/

Supported variables:
- ``constants.DISCHARGE_DAILY_MEAN`` (m³/s)
- ``constants.DISCHARGE_INSTANT`` (m³/s)
- ``constants.STAGE_DAILY_MEAN`` (m)
- ``constants.WATER_TEMPERATURE_DAILY_MEAN`` (°C)
- ``constants.DISCHARGE_MONTHLY_MEAN`` (m³/s)

Data description and API:
- see https://opendata.smhi.se/hydroobs/resources/parameter

Terms of use:
- see https://www.smhi.se/data/om-smhis-data/smhis-datapolicy

Notes:
- SMHI documents ``Vattenstånd`` and ``Vattendragstemperatur`` as daily measurements.
RivRetrieve normalizes these Sweden series to the daily mean constants for a consistent
daily-resolution API.
"""

BASE_URL = "https://opendata-download-hydroobs.smhi.se/api/version/1.0"
COUNTRY = "Sweden"
SOURCE = "SMHI HydroObs"
TIMESTAMP_OFFSET = pd.Timedelta(hours=2)
VARIABLE_CONFIG = {
constants.DISCHARGE_DAILY_MEAN: {"parameter_id": 1, "scale": 1.0},
constants.DISCHARGE_INSTANT: {"parameter_id": 2, "scale": 1.0},
constants.STAGE_DAILY_MEAN: {"parameter_id": 3, "scale": 0.01},
constants.WATER_TEMPERATURE_DAILY_MEAN: {"parameter_id": 4, "scale": 1.0},
constants.DISCHARGE_MONTHLY_MEAN: {"parameter_id": 10, "scale": 1.0},
}

@staticmethod
def _empty_result(variable: str) -> pd.DataFrame:
"""Returns a standardized empty time series result."""
return pd.DataFrame(columns=[variable], index=pd.DatetimeIndex([], name=constants.TIME_INDEX))

@staticmethod
def _empty_metadata_frame() -> pd.DataFrame:
columns = [
constants.GAUGE_ID,
constants.STATION_NAME,
constants.RIVER,
constants.LATITUDE,
constants.LONGITUDE,
constants.ALTITUDE,
constants.AREA,
constants.COUNTRY,
constants.SOURCE,
]
return pd.DataFrame(columns=columns).set_index(constants.GAUGE_ID)

@staticmethod
def get_cached_metadata() -> pd.DataFrame:
"""Retrieves cached Swedish gauge metadata."""
return utils.load_cached_metadata_csv("sweden")

@staticmethod
def get_available_variables() -> tuple[str, ...]:
"""Returns a tuple of supported variables."""
return tuple(SwedenFetcher.VARIABLE_CONFIG.keys())

@classmethod
def _format_query_datetime(cls, date_str: str, end_of_day: bool = False) -> str:
suffix = "23:59Z" if end_of_day else "00:00Z"
return f"{date_str}T{suffix}"

@classmethod
def _to_local_timestamp(cls, value: Any) -> pd.Timestamp:
timestamp = pd.to_datetime(value, unit="ms", utc=True, errors="coerce")
if pd.isna(timestamp):
return pd.NaT
# SMHI labels archive timestamps using Swedish summer time (UTC+2) throughout the year.
return (timestamp + cls.TIMESTAMP_OFFSET).tz_localize(None)

@classmethod
def _to_local_series(cls, values: pd.Series) -> pd.Series:
timestamps = pd.to_datetime(values, unit="ms", utc=True, errors="coerce")
return (timestamps + cls.TIMESTAMP_OFFSET).dt.tz_localize(None)

@staticmethod
def _normalize_area(value: Any) -> float:
area = pd.to_numeric(value, errors="coerce")
if pd.notna(area) and area < 0:
return np.nan
return area

@classmethod
def _build_parameter_url(cls, parameter_id: int) -> str:
return f"{cls.BASE_URL}/parameter/{parameter_id}.json"

@classmethod
def _build_data_url(cls, gauge_id: str, parameter_id: int) -> str:
return f"{cls.BASE_URL}/parameter/{parameter_id}/station/{gauge_id}/period/corrected-archive/data.json"

def _download_json(self, url: str, params: Optional[dict[str, str]] = None) -> dict[str, Any]:
session = utils.requests_retry_session()
response = session.get(url, params=params, timeout=60)
response.raise_for_status()
return response.json()

def _fetch_parameter_metadata(self, parameter_id: int) -> pd.DataFrame:
"""Fetches and standardizes station metadata for a single SMHI parameter."""
url = self._build_parameter_url(parameter_id)

try:
payload = self._download_json(url)
except requests.exceptions.RequestException as exc:
logger.error(f"Failed to fetch Swedish metadata for parameter {parameter_id}: {exc}")
return self._empty_metadata_frame()
except ValueError as exc:
logger.error(f"Failed to decode Swedish metadata for parameter {parameter_id}: {exc}")
return self._empty_metadata_frame()

stations = payload.get("station", [])
if not isinstance(stations, list) or not stations:
return self._empty_metadata_frame()

rows = []
for station in stations:
row = dict(station)
gauge_id = str(station.get("id") or station.get("key") or "").strip()
if not gauge_id:
continue

row[constants.GAUGE_ID] = gauge_id
row[constants.STATION_NAME] = station.get("name")
row[constants.RIVER] = station.get("catchmentName")
row[constants.LATITUDE] = pd.to_numeric(station.get("latitude"), errors="coerce")
row[constants.LONGITUDE] = pd.to_numeric(station.get("longitude"), errors="coerce")
row[constants.ALTITUDE] = np.nan
row[constants.AREA] = self._normalize_area(station.get("catchmentSize"))
row[constants.COUNTRY] = self.COUNTRY
row[constants.SOURCE] = self.SOURCE
row["parameter_id"] = parameter_id
row["from"] = self._to_local_timestamp(station.get("from"))
row["to"] = self._to_local_timestamp(station.get("to"))
rows.append(row)

if not rows:
return self._empty_metadata_frame()

df = pd.DataFrame(rows)
return df.set_index(constants.GAUGE_ID)

def get_metadata(self) -> pd.DataFrame:
"""Fetches site metadata for all supported Swedish parameters."""
frames = []
for config in self.VARIABLE_CONFIG.values():
frame = self._fetch_parameter_metadata(config["parameter_id"])
if not frame.empty:
frames.append(frame)

if not frames:
return self._empty_metadata_frame()

df = pd.concat(frames, axis=0)
df = df[~df.index.duplicated(keep="first")]
df.index = df.index.astype(str)
return df.sort_index()

def _download_data(self, gauge_id: str, variable: str, start_date: str, end_date: str) -> dict[str, Any]:
"""Downloads raw JSON time-series data from SMHI."""
config = self.VARIABLE_CONFIG.get(variable)
if config is None:
raise ValueError(f"Unsupported variable: {variable}")

params = {
"from": self._format_query_datetime(start_date),
"to": self._format_query_datetime(end_date, end_of_day=True),
}
url = self._build_data_url(gauge_id, config["parameter_id"])
return self._download_json(url, params=params)

def _parse_data(self, gauge_id: str, raw_data: dict[str, Any], variable: str) -> pd.DataFrame:
"""Parses raw SMHI JSON into a standardized time-series DataFrame."""
values = raw_data.get("value", []) if isinstance(raw_data, dict) else []
if not isinstance(values, list) or not values:
return self._empty_result(variable)

df = pd.DataFrame(values)
required_columns = {"date", "value"}
if df.empty or not required_columns.issubset(df.columns):
return self._empty_result(variable)

result = pd.DataFrame(
{
constants.TIME_INDEX: self._to_local_series(df["date"]),
variable: pd.to_numeric(df["value"], errors="coerce"),
}
)

scale = self.VARIABLE_CONFIG[variable]["scale"]
result[variable] = result[variable] * scale
result = result.dropna(subset=[constants.TIME_INDEX, variable])
if result.empty:
return self._empty_result(variable)

result = result.sort_values(constants.TIME_INDEX)
result = result.drop_duplicates(subset=[constants.TIME_INDEX], keep="last")
return result.set_index(constants.TIME_INDEX)

def get_data(
self,
gauge_id: str,
variable: str,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
) -> pd.DataFrame:
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)
except Exception as exc:
logger.error(f"Failed to get data for site {gauge_id}, variable {variable}: {exc}")
return self._empty_result(variable)

if df.empty:
return self._empty_result(variable)

start_date_dt = pd.to_datetime(start_date)
end_date_dt = pd.to_datetime(end_date)
if constants.INSTANTANEOUS in variable or constants.HOURLY in variable:
end_date_dt = end_date_dt + pd.Timedelta(days=1)
return df[(df.index >= start_date_dt) & (df.index < end_date_dt)]

return df[(df.index >= start_date_dt) & (df.index <= end_date_dt)]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"updated":1774530000000,"parameter":{"key":"3","name":"Vattenstånd","unit":"cm"},"station":{"key":"1906","name":"BYRVIKSBANKEN","owner":"Extern","measuringStations":"CORE"},"period":{"key":"latest-day","from":1774389600000,"to":1774530000000,"summary":"Data från senaste dygnet"},"position":[{"from":1774389600000,"to":1774530000000,"latitude":60.7618,"longitude":14.9378}],"link":[{"href":"https://opendata-download-hydroobs.smhi.se/api/version/1.0/parameter/3/station/1906/period/latest-day/data.json","rel":"data","type":"application/json"},{"href":"https://opendata-download-hydroobs.smhi.se/api/version/1.0/parameter/3/station/1906/period/latest-day/data.xml","rel":"data","type":"application/xml"},{"href":"https://opendata-download-hydroobs.smhi.se/api/version/1.0/parameter/3/station/1906/period/latest-day/data.csv","rel":"data","type":"text/plain"},{"href":"https://opendata-download-hydroobs.smhi.se/api/version/1.0/parameter/3/station/1906/period/latest-day.atom","rel":"period","type":"application/atom+xml"},{"href":"https://opendata-catalog.smhi.se/md/ae7f2051-7693-4cc6-8528-de1b6a592cef","rel":"iso19139","type":"application/vnd.iso.19139+xml"},{"href":"https://opendata-catalog.smhi.se/md/2ef48b70-7d1f-11ed-a51b-432755aaeed3","rel":"iso19139","type":"application/vnd.iso.19139+xml"}],"value":[{"date":1774389600000,"value":16133.0,"quality":"O"},{"date":1774476000000,"value":16140.0,"quality":"O"}]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"updated":1774530000000,"parameter":{"key":"2","name":"Vattenföring (15 min)","unit":"m³/s"},"station":{"key":"2357","name":"ABISKO","owner":"SMHI","measuringStations":"CORE"},"period":{"key":"latest-day","from":1774389600000,"to":1774530000000,"summary":"Data från senaste dygnet"},"position":[{"from":1774389600000,"to":1774530000000,"latitude":68.1936,"longitude":19.9859}],"link":[{"href":"https://opendata-download-hydroobs.smhi.se/api/version/1.0/parameter/2/station/2357/period/latest-day/data.json","rel":"data","type":"application/json"},{"href":"https://opendata-download-hydroobs.smhi.se/api/version/1.0/parameter/2/station/2357/period/latest-day/data.xml","rel":"data","type":"application/xml"},{"href":"https://opendata-download-hydroobs.smhi.se/api/version/1.0/parameter/2/station/2357/period/latest-day/data.csv","rel":"data","type":"text/plain"},{"href":"https://opendata-download-hydroobs.smhi.se/api/version/1.0/parameter/2/station/2357/period/latest-day.atom","rel":"period","type":"application/atom+xml"},{"href":"https://opendata-catalog.smhi.se/md/ae7f2051-7693-4cc6-8528-de1b6a592cef","rel":"iso19139","type":"application/vnd.iso.19139+xml"},{"href":"https://opendata-catalog.smhi.se/md/545f7280-7d15-11ed-a51b-432755aaeed3","rel":"iso19139","type":"application/vnd.iso.19139+xml"}],"value":[{"date":1774389600000,"value":18.0,"quality":"O"},{"date":1774390500000,"value":18.0,"quality":"O"},{"date":1774391400000,"value":18.0,"quality":"O"},{"date":1774392300000,"value":18.0,"quality":"O"}]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"updated":744069600000,"parameter":{"key":"4","name":"Vattendragstemperatur","unit":"°C"},"station":{"key":"80107","name":"UNTRA KRV TEMP","owner":"-","measuringStations":"ADDITIONAL"},"period":{"key":"corrected-archive","from":743637600000,"to":744069600000,"summary":"Data som mätstationen har levererat fram till kl. 00 innevarande dygn"},"position":[{"from":743637600000,"to":744069600000,"latitude":60.4359,"longitude":17.3211}],"link":[{"href":"https://sid-hydroobs-portal.smhi.se:8080/api/version/latest/parameter/4/station/80107/period/corrected-archive/data.json","rel":"data","type":"application/json"},{"href":"https://sid-hydroobs-portal.smhi.se:8080/api/version/latest/parameter/4/station/80107/period/corrected-archive/data.xml","rel":"data","type":"application/xml"},{"href":"https://sid-hydroobs-portal.smhi.se:8080/api/version/latest/parameter/4/station/80107/period/corrected-archive/data.csv","rel":"data","type":"text/plain"},{"href":"https://sid-hydroobs-portal.smhi.se:8080/api/version/latest/parameter/4/station/80107/period/corrected-archive.atom","rel":"period","type":"application/atom+xml"},{"href":"https://opendata-catalog.smhi.se/md/ae7f2051-7693-4cc6-8528-de1b6a592cef","rel":"iso19139","type":"application/vnd.iso.19139+xml"},{"href":"https://opendata-catalog.smhi.se/md/9d61ab90-7d21-11ed-a51b-432755aaeed3","rel":"iso19139","type":"application/vnd.iso.19139+xml"}],"value":[{"date":743637600000,"value":20.0,"quality":"O"},{"date":743724000000,"value":19.0,"quality":"O"},{"date":743810400000,"value":19.0,"quality":"O"},{"date":743896800000,"value":18.0,"quality":"O"},{"date":743983200000,"value":19.0,"quality":"O"},{"date":744069600000,"value":19.0,"quality":"O"}]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"updated":1380578400000,"parameter":{"key":"10","name":"Vattenföring (Månad)","unit":"m³/s"},"station":{"key":"90039","name":"Avasundet VM","owner":"SMHI","measuringStations":"CORE"},"period":{"key":"corrected-archive","from":1285884000000,"to":1380578400000,"summary":"Data som mätstationen har levererat fram till kl. 00 innevarande dygn"},"position":[{"from":1285884000000,"to":1380578400000,"latitude":64.3195,"longitude":21.3401}],"link":[{"href":"https://sid-hydroobs-portal.smhi.se:8080/api/version/latest/parameter/10/station/90039/period/corrected-archive/data.json","rel":"data","type":"application/json"},{"href":"https://sid-hydroobs-portal.smhi.se:8080/api/version/latest/parameter/10/station/90039/period/corrected-archive/data.xml","rel":"data","type":"application/xml"},{"href":"https://sid-hydroobs-portal.smhi.se:8080/api/version/latest/parameter/10/station/90039/period/corrected-archive/data.csv","rel":"data","type":"text/plain"},{"href":"https://sid-hydroobs-portal.smhi.se:8080/api/version/latest/parameter/10/station/90039/period/corrected-archive.atom","rel":"period","type":"application/atom+xml"},{"href":"https://opendata-catalog.smhi.se/md/ae7f2051-7693-4cc6-8528-de1b6a592cef","rel":"iso19139","type":"application/vnd.iso.19139+xml"},{"href":"https://opendata-catalog.smhi.se/md/4a3b003b-f886-4225-b940-a6e498ac2187","rel":"iso19139","type":"application/vnd.iso.19139+xml"}],"value":[{"date":1288562400000,"value":2.08,"quality":"O"},{"date":1291154400000,"value":1.12,"quality":"O"},{"date":1293832800000,"value":0.838,"quality":"O"}]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"updated":1435615200000,"parameter":{"key":"1","name":"Vattenföring (Dygn)","unit":"m³/s"},"station":{"key":"90072","name":"Gamla Pershyttan VM","owner":"SMHI","measuringStations":"CORE"},"period":{"key":"corrected-archive","from":1383775200000,"to":1435615200000,"summary":"Data som mätstationen har levererat fram till kl. 00 innevarande dygn"},"position":[{"from":1383775200000,"to":1435615200000,"latitude":59.4867,"longitude":15.0022}],"link":[{"href":"https://sid-hydroobs-portal.smhi.se:8080/api/version/latest/parameter/1/station/90072/period/corrected-archive/data.json","rel":"data","type":"application/json"},{"href":"https://sid-hydroobs-portal.smhi.se:8080/api/version/latest/parameter/1/station/90072/period/corrected-archive/data.xml","rel":"data","type":"application/xml"},{"href":"https://sid-hydroobs-portal.smhi.se:8080/api/version/latest/parameter/1/station/90072/period/corrected-archive/data.csv","rel":"data","type":"text/plain"},{"href":"https://sid-hydroobs-portal.smhi.se:8080/api/version/latest/parameter/1/station/90072/period/corrected-archive.atom","rel":"period","type":"application/atom+xml"},{"href":"https://opendata-catalog.smhi.se/md/ae7f2051-7693-4cc6-8528-de1b6a592cef","rel":"iso19139","type":"application/vnd.iso.19139+xml"},{"href":"https://opendata-catalog.smhi.se/md/ba15aad9-1b42-4224-bec7-428e592bed14","rel":"iso19139","type":"application/vnd.iso.19139+xml"}],"value":[{"date":1383775200000,"value":0.157,"quality":"O"},{"date":1383861600000,"value":0.161,"quality":"O"},{"date":1383948000000,"value":0.17,"quality":"O"}]}
Loading
Loading