Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement loggers #72

Merged
merged 14 commits into from
Mar 7, 2024
5 changes: 5 additions & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ colorama==0.4.6
# via
# build
# click
# loguru
# pytest
# tqdm
contourpy==1.2.0
Expand Down Expand Up @@ -116,6 +117,8 @@ lazy-loader==0.3
# via scikit-image
llvmlite==0.41.1
# via numba
loguru==0.7.2
# via swmmanywhere (pyproject.toml)
matplotlib==3.8.2
# via
# salib
Expand Down Expand Up @@ -300,6 +303,8 @@ virtualenv==20.24.5
# via pre-commit
wheel==0.41.3
# via pip-tools
win32-setctime==1.1.0
# via loguru
xarray==2023.12.0
# via
# rioxarray
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies = [ # TODO definitely don't need all of these
"geopandas",
"geopy",
"GitPython",
"loguru",
"matplotlib",
"netcdf4",
"networkx",
Expand Down
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ cligj==0.7.2
colorama==0.4.6
# via
# click
# loguru
# tqdm
contourpy==1.2.0
# via matplotlib
Expand Down Expand Up @@ -94,6 +95,8 @@ lazy-loader==0.3
# via scikit-image
llvmlite==0.41.1
# via numba
loguru==0.7.2
# via swmmanywhere (pyproject.toml)
matplotlib==3.8.2
# via
# salib
Expand Down Expand Up @@ -237,6 +240,8 @@ tzdata==2024.1
# via pandas
urllib3==2.1.0
# via requests
win32-setctime==1.1.0
# via loguru
xarray==2023.12.0
# via
# rioxarray
Expand Down
3 changes: 2 additions & 1 deletion swmmanywhere/graph_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from swmmanywhere import geospatial_utilities as go
from swmmanywhere import parameters
from swmmanywhere.utils import logger


def load_graph(fid: Path) -> nx.Graph:
Expand Down Expand Up @@ -940,7 +941,7 @@ def process_successors(G: nx.Graph,
edge_diams[(node,ds_node,0)] = diam
chamber_floor[ds_node] = surface_elevations[ds_node] - depth
if ix > 0:
print('''a node has multiple successors,
logger.warning('''a node has multiple successors,
not sure how that can happen if using shortest path
to derive topology''')

Expand Down
11 changes: 6 additions & 5 deletions swmmanywhere/prepare_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
import yaml
from geopy.geocoders import Nominatim

# Some minor comment (to remove)
from swmmanywhere.utils import logger


def get_country(x: float,
y: float) -> dict[int, str]:
Expand Down Expand Up @@ -85,9 +86,9 @@ def download_buildings(file_address: Path,
# Save data to the specified file address
with file_address.open("wb") as file:
file.write(response.content)
print(f"Data downloaded and saved to {file_address}")
logger.info(f"Data downloaded and saved to {file_address}")
else:
print(f"Error downloading data. Status code: {response.status_code}")
logger.error(f"Error downloading data. Status code: {response.status_code}")
return response.status_code

def download_street(bbox: tuple[float, float, float, float]) -> nx.MultiDiGraph:
Expand Down Expand Up @@ -176,10 +177,10 @@ def download_elevation(fid: Path,
with fid.open('wb') as rast_file:
shutil.copyfileobj(r.raw, rast_file)

print('Elevation data downloaded successfully.')
logger.info('Elevation data downloaded successfully.')

except requests.exceptions.RequestException as e:
print(f'Error downloading elevation data: {e}')
logger.error(f'Error downloading elevation data: {e}')

return r.status_code

Expand Down
13 changes: 7 additions & 6 deletions swmmanywhere/preprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from swmmanywhere import geospatial_utilities as go
from swmmanywhere import graph_utilities as gu
from swmmanywhere import parameters, prepare_data
from swmmanywhere.utils import logger


def next_directory(keyword: str, directory: Path) -> int:
Expand Down Expand Up @@ -161,7 +162,7 @@ def prepare_precipitation(bbox: tuple[float, float, float, float],
"""Download and reproject precipitation data."""
if addresses.precipitation.exists():
return
print(f'downloading precipitation to {addresses.precipitation}')
logger.info(f'downloading precipitation to {addresses.precipitation}')
precip = prepare_data.download_precipitation(bbox,
api_keys['cds_username'],
api_keys['cds_api_key'])
Expand All @@ -179,7 +180,7 @@ def prepare_elvation(bbox: tuple[float, float, float, float],
"""Download and reproject elevation data."""
if addresses.elevation.exists():
return
print(f'downloading elevation to {addresses.elevation}')
logger.info(f'downloading elevation to {addresses.elevation}')
with tempfile.TemporaryDirectory() as temp_dir:
fid = Path(temp_dir) / 'elevation.tif'
prepare_data.download_elevation(fid,
Expand All @@ -198,12 +199,12 @@ def prepare_building(bbox: tuple[float, float, float, float],
return

if not addresses.national_building.exists():
print(f'downloading buildings to {addresses.national_building}')
logger.info(f'downloading buildings to {addresses.national_building}')
prepare_data.download_buildings(addresses.national_building,
bbox[0],
bbox[1])

print(f'trimming buildings to {addresses.building}')
logger.info(f'trimming buildings to {addresses.building}')
national_buildings = gpd.read_parquet(addresses.national_building)
buildings = national_buildings.cx[bbox[0]:bbox[2], bbox[1]:bbox[3]] # type: ignore

Expand All @@ -217,7 +218,7 @@ def prepare_street(bbox: tuple[float, float, float, float],
"""Download and reproject street graph."""
if addresses.street.exists():
return
print(f'downloading street network to {addresses.street}')
logger.info(f'downloading street network to {addresses.street}')
street_network = prepare_data.download_street(bbox)
street_network = go.reproject_graph(street_network,
source_crs,
Expand All @@ -231,7 +232,7 @@ def prepare_river(bbox: tuple[float, float, float, float],
"""Download and reproject river graph."""
if addresses.river.exists():
return
print(f'downloading river network to {addresses.river}')
logger.info(f'downloading river network to {addresses.river}')
river_network = prepare_data.download_river(bbox)
river_network = go.reproject_graph(river_network,
source_crs,
Expand Down
35 changes: 35 additions & 0 deletions swmmanywhere/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
"""Created on 2024-03-04.

@author: Barney
"""
import os
import sys

import loguru


def get_logger(verbose: bool = False) -> loguru.logger:
"""Get a logger."""
logger = loguru.logger
logger.configure(
handlers=[
{
"sink": sys.stdout,
"colorize": True,
"format": " | ".join(
[
"<cyan>{time:YYYY/MM/DD HH:mm:ss}</>",
"{message}",
]
),
}
]
)
if os.getenv("PACKAGE_NAME_VERBOSE", str(verbose)).lower() == "true":
logger.enable("package_name")
else:
logger.disable("package_name")
barneydobson marked this conversation as resolved.
Show resolved Hide resolved
return logger

logger = get_logger()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this call to each function that requires logging, as it leads to unpredictable behavior when changing the verbosity using SWMMANYWHERE_VERBOSE env var. This happens because when you call it here in the utils module, you read the SWMMANYWHERE_VERBOSE variable only when the package is being loaded. So, if a user sets SWMMANYWHERE_VERBOSE after loading the package, it won't work.

Essentially, this works:

import os

os.environ["SWMMANYWHERE_VERBOSE"] = "true"

import swmmanywhere

but this doesn't work as expected:

import swmmanywhere
import os

os.environ["SWMMANYWHERE_VERBOSE"] = "true"

But if you call it within functions, it will always work correctly, regardless of the import order.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I tidied it all up a bit and added some tests (for these - see

def test_logger_disable():

and
def test_logger_reimport():

).

Simply importing logger and setting logger.enable('swmmanywhere') or logger.disable('swmmanywhere') seems to work fine globally regardless of when the import happens - if you can write a test that breaks it then I'll reconsider ;)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it easier and more flexible to use an env variable to control the verbosity rather than importing a function. You can set it from the command-line, or anywhere in code, without having to know the logger syntax.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK this is a good argument for global variables!

So I have added a filter that enables control of verbosity through globals, but also continues use of a global logger.

@cheginit and @dalonsoa - is this actually a good idea or am I opening up for a world of hurt?

Otherwise I will just go with @cheginit 's initial suggestion and import the logger locally each time it is used, still controlling it through globals.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks good to me. The dynamic filter seems to enable the best of both worlds.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, LGTM.

33 changes: 33 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
"""Created on 2024-01-26.

@author: Barney
"""
from pathlib import Path
from tempfile import NamedTemporaryFile

from swmmanywhere.utils import logger


def test_logger():
"""Test the get_logger function."""
assert logger is not None
logger.debug("This is a debug message.")
logger.info("This is an info message.")
logger.warning("This is a warning message.")
logger.error("This is an error message.")
logger.critical("This is a critical message.")

with NamedTemporaryFile(suffix='.log',
mode = 'w+b',
delete=False) as temp_file:
fid = Path(temp_file.name)
logger.add(fid)
logger.debug("This is a debug message.")
logger.info("This is an info message.")
logger.warning("This is a warning message.")
logger.error("This is an error message.")
logger.critical("This is a critical message.")
assert temp_file.read() != b""
logger.remove()
fid.unlink()