Skip to content

Commit

Permalink
Develop/release 2.43 (#109) (#111)
Browse files Browse the repository at this point in the history
* Develop/release 2.43 (#109)

* [2023-09-14 07:44] Release version of Waarnemingen Register model
  • Loading branch information
rflinnenbank authored Sep 18, 2023
1 parent 5e8b2d7 commit 8e73033
Show file tree
Hide file tree
Showing 17 changed files with 2,035 additions and 1,268 deletions.
2,265 changes: 1,241 additions & 1,024 deletions poetry.lock

Large diffs are not rendered by default.

18 changes: 10 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "weather_provider_api"
version = "2.41.0"
version = "2.43"
description = "Weather Provider Libraries and API"
authors = ["Verbindingsteam", "Raoul Linnenbank <58594297+rflinnenbank@users.noreply.github.com>"]
license = "MPL-2.0"
Expand All @@ -11,27 +11,27 @@ include = [
]

[tool.poetry.dependencies]
python = ">=3.8,<3.11"
fastapi = "^0.95.1"
python = ">=3.8,<3.12"
fastapi = "^0.103.1"
requests = "^2.28.1"
geopy = "^2.3.0"
numpy = "^1.23.5"
numpy = "^1.24.4"
structlog = "^23.1.0"
gunicorn = "^20.1.0"
gunicorn = "^21.2.0"
eccodes = "^1.5.0"
accept-types = "^0.4.1"
lxml = "^4.9.1"
starlette-prometheus = "^0.9.0"
beautifulsoup4 = "^4.11.1"
netcdf4 = "^1.6.2"
tomli = "^2.0.1"
pandas = "1.5.3"
xarray = "^2022.12.0"
pandas = "2.0.3"
xarray = "^2022.8.0"
cfgrib = "^0.9.10.3"
uvicorn = "^0.21.1"
slowapi = "^0.1.7"
ecmwflibs = "0.5.1"
starlette = "^0.26.1"
starlette = "^0.27.0"

[tool.poetry.group.dev.dependencies]
pytest = "^7.2.0"
Expand All @@ -47,9 +47,11 @@ pylint = "^2.15.10"
wpla_update_era5sl = "weather_provider_api.scripts.update_era5sl_repository:main"
wpla_update_era5land = "weather_provider_api.scripts.update_era5land_repository:main"
wpla_update_arome = "weather_provider_api.scripts.update_arome_repository:main"
wpla_update_waarnemingen = "weather_provider_api.scripts.update_waarnemingen_register:main"
wpla_clear_era5sl = "weather_provider_api.scripts.erase_era5sl_repository:main"
wpla_clear_era5land = "weather_provider_api.scripts.erase_era5land_repository:main"
wpla_clear_arome = "weather_provider_api.scripts.erase_arome_repository:main"
wpla_clear_waarnemingen = "weather_provider_api.scripts.erase_waarnemingen_register:main"
wpla_run_api = "weather_provider_api.main:main"

[tool.pylint]
Expand Down
10 changes: 4 additions & 6 deletions tests/test_knmi_actuele_waarnemingen.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import xarray as xr

from weather_provider_api.routers.weather.sources.knmi.models.actuele_waarnemingen import ActueleWaarnemingenModel
from weather_provider_api.routers.weather.sources.knmi.utils import download_actuele_waarnemingen_weather, \
_retrieve_observation_moment
from weather_provider_api.routers.weather.utils.geo_position import GeoPosition


Expand Down Expand Up @@ -42,20 +44,16 @@ def test_get_weather(mock_coordinates, start, end):
@pytest.mark.skip(reason="Test currently not working via Tox on GitHub Actions")
def test__retrieve_observation_date():
# Test to verify error handling
aw_model = ActueleWaarnemingenModel()

current_locale = locale.getlocale(locale.LC_TIME)
locale.setlocale(locale.LC_TIME, "dutch")
assert aw_model.retrieve_observation_moment(None).date() == datetime.now().date() # System now
assert _retrieve_observation_moment(None).date() == datetime.now().date() # System now
locale.setlocale(locale.LC_TIME, current_locale)


def test__download_weather(monkeypatch):
# Test to verify error handling
aw_model = ActueleWaarnemingenModel()

def mock_request_get(_, *args, **kwargs):
raise requests.exceptions.BaseHTTPError("Fake BaseHTTP Error!")

monkeypatch.setattr(requests, "get", mock_request_get)
assert aw_model._download_weather() is None
assert download_actuele_waarnemingen_weather() is None
12 changes: 9 additions & 3 deletions weather_provider_api/app_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ class BaseConfig(object):
"""

APP_NAME = os.environ.get("APP_NAME", __name__)
APP_DESCRIPTION = os.environ.get("APP_DESCRIPTION", """Alliander Weather Provider API""")
APP_DESCRIPTION = os.environ.get(
"APP_DESCRIPTION", """Alliander Weather Provider API"""
)
APP_MAINTAINER = os.environ.get("APP_MAINTAINER", "DNB/ST Innovatieteam")
APP_MAINTAINER_EMAIL = os.environ.get("APP_MAINTAINER_EMAIL", "weather.provider@alliander.com")
APP_MAINTAINER_EMAIL = os.environ.get(
"APP_MAINTAINER_EMAIL", "weather.provider@alliander.com"
)
SHOW_MAINTAINER = os.environ.get("SHOW_MAINTAINER", False)

default_version = None
Expand Down Expand Up @@ -51,7 +55,9 @@ class BaseConfig(object):
DEPLOYED = os.environ.get("DEPLOYED", False)
DEBUG = os.environ.get("DEBUG", True)

REPO_FOLDER = os.environ.get("REPO_FOLDER", f"{tempfile.gettempdir()}/Weather_Repository")
REPO_FOLDER = os.environ.get(
"REPO_FOLDER", f"{tempfile.gettempdir()}/Weather_Repository"
)


class LocalConfig(BaseConfig):
Expand Down
6 changes: 3 additions & 3 deletions weather_provider_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ def redirect_to_docs():
return RedirectResponse(url=redirect_url)


logger.info(f"--------------------------------------", datetime=datetime.utcnow())
logger.info(f"Finished booting; starting uvicorn...", datetime=datetime.utcnow())
logger.info(f"--------------------------------------", datetime=datetime.utcnow())
logger.info("--------------------------------------", datetime=datetime.utcnow())
logger.info("Finished booting; starting uvicorn...", datetime=datetime.utcnow())
logger.info("--------------------------------------", datetime=datetime.utcnow())


def main():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# SPDX-FileCopyrightText: 2019-2023 Alliander N.V.
# SPDX-License-Identifier: MPL-2.0

""" This module houses the repository class for the Actuele Waarnemingen Register. """
from datetime import datetime
from typing import List

import structlog
import xarray as xr
from dateutil import tz
from dateutil.relativedelta import relativedelta

from weather_provider_api.routers.weather.repository.repository import (
WeatherRepositoryBase,
)
from weather_provider_api.routers.weather.sources.knmi.utils import (
download_actuele_waarnemingen_weather,
)
from weather_provider_api.routers.weather.utils.geo_position import GeoPosition
from weather_provider_api.routers.weather.utils.grid_helpers import (
round_coordinates_to_wgs84_grid,
)


class ActueleWaarnemingenRegisterRepository(WeatherRepositoryBase):
""" """

def _delete_files_outside_of_scope(self):
if self.filename.exists():
current_data = xr.load_dataset(self.filename, engine="netcdf4")
current_data = current_data.sel(
time=slice(self.first_day_of_repo, self.last_day_of_repo)
)
current_data.to_netcdf(self.filename, format="NETCDF4")

def _get_file_list_for_period(self, start: datetime, end: datetime):
return self.filename

def __init__(self):
# Pre-work
super().__init__()

self.logger = structlog.get_logger(__name__)
self.repository_name = "KNMI Actuele Waarnemingen - 48 uur register"

# Repository settings
self.file_prefix = "ACTUEEL48"
self.runtime_limit = 3 * 60 # Three minutes maximum runtime
self.permanent_suffixes = []
self.filename = self.repository_folder / f"{self.file_prefix}_register.nc"
self.file_identifier_length = 0

self.logger.debug(
f"Initialized the [{self.repository_name}] repository",
datetime=datetime.utcnow(),
)

@property
def repository_sub_folder(self):
return "ACTUEEL48" # not using self.file_prefix, because that may not exist the first time using this

def _get_repo_sub_folder(self):
return self.repository_sub_folder

@property
def first_day_of_repo(self) -> datetime:
# Property to get the first moment of the repository as translated to the Dutch timezone.
from_zone = tz.gettz("UTC")
to_zone = tz.gettz("Europe/Amsterdam")

first_day_of_repo = datetime.utcnow() - relativedelta(hours=48) # 48 hours
first_day_of_repo = first_day_of_repo.replace(tzinfo=from_zone)
first_day_of_repo = first_day_of_repo.astimezone(to_zone)
return first_day_of_repo.replace(tzinfo=None)

@property
def last_day_of_repo(self) -> datetime:
# Property to get the last moment of the repository as translated to the Dutch timezone.
from_zone = tz.gettz("UTC")
to_zone = tz.gettz("Europe/Amsterdam")

last_day_of_repo = datetime.utcnow() # Right now
last_day_of_repo = last_day_of_repo.replace(tzinfo=from_zone)
last_day_of_repo = last_day_of_repo.astimezone(to_zone)

return last_day_of_repo.replace(tzinfo=None)

def update(self):
"""Implementation of the WeatherRepository update method
Attempts to update the repository with the current data. For this repository, that means getting the current
Actuele Waarnemingen output and storing it, while removing any data not within the scope of the repository.
Returns:
Nothing.
"""
raw_weather_ds = download_actuele_waarnemingen_weather()
time = datetime.utcnow().replace(second=0, microsecond=0)

# Cleanup any old data
self.cleanup()

# Update the file
self._update_file_with_new_data(new_data_ds=raw_weather_ds, update_moment=time)

def _update_file_with_new_data(
self, new_data_ds: xr.Dataset, update_moment: datetime
):
"""This method updates any existing data file with new data or creates a new from scratch if needed, using the
given dataset.
Args:
new_data_ds (xr.Dataset): A Xarray Dataset holding the data to append / create the data file with.
update_moment (datetime): A datetime holding the moment of update. Note that this doesn't need to be the
moment that the data is from.
Returns:
Nothing. The file is just updated.
"""
# Opening the file:
if self.filename.exists():
stored_data_ds = xr.load_dataset(self.filename, engine="netcdf4")
else:
stored_data_ds = None

self.logger.info(
f"Storing data for: {update_moment.strftime('%m-%d-%Y %H:%M:%S')} "
f"[{new_data_ds.isel(STN=0, time=0)['time'].values}]"
)
if stored_data_ds is None:
new_data_ds.to_netcdf(self.filename, format="NETCDF4")
else:
# Check if time not already in system
try:
new_stored_data_ds = xr.merge([new_data_ds, stored_data_ds])

new_stored_data_ds.to_netcdf(self.filename, format="NETCDF4")
except ValueError as value_error:
self.logger.warning(f"Could not update file: {value_error}")

def get_24_hour_registry_for_station(self, station: int) -> xr.Dataset:
"""This method obtains the last 24 hours of data of Actuele Waarnemingen and returns it for single station.
Args:
station (int): An integer representing the station to gather data for
Returns:
xr.Dataset: A Xarray Dataset holding last 24 hours of data for the requested station
"""
stored_data_ds = xr.load_dataset(self.filename, engine="netcdf4")
return stored_data_ds.sel(
STN=station,
time=slice(
self.first_day_of_repo + relativedelta(days=1), self.last_day_of_repo
),
)

def get_48_hour_registry_for_station(self, station: int) -> xr.Dataset:
"""This method obtains the last 48 hours of data of Actuele Waarnemingen and returns it for single station.
Args:
station (int): An integer representing the station to gather data for
Returns:
xr.Dataset: A Xarray Dataset holding last 48 hours of data for the requested station
"""
stored_data_ds = xr.load_dataset(self.filename, engine="netcdf4")
stored_data_ds = stored_data_ds.sel(
STN=station,
time=slice(self.first_day_of_repo, self.last_day_of_repo),
)
return stored_data_ds

def get_grid_coordinates(self, coordinates: List[GeoPosition]) -> List[GeoPosition]:
"""Rounds a list of GeoPositions to the resolution set through grid_resolution"""
return round_coordinates_to_wgs84_grid(coordinates, (0.023, 0.037), (49, 0))
Loading

0 comments on commit 8e73033

Please sign in to comment.