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

Improve logging and warnings #1125

Merged
merged 10 commits into from
Feb 5, 2024
Merged
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
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

## 2.0.0 (in development)

- add type annotations to all public and private functions throughout the package (#1107)
- improve docstrings throughout the package (#1116)
- add type annotations to all public and private functions throughout package (#1107)
- improve docstrings throughout package (#1116)
- remove functionality previously deprecated in v1 (#1113 #1122)
- drop Python 3.8 support (#1106)
- increase add_node_elevations_google default batch_size to 512 to match Google's limit (#1115)
- make which_result function parameter consistently able to accept a list throughout package (#1113)
- make utils_geo.bbox_from_point function return a tuple of floats for consistency with rest of package (#1113)
- improve logging and warnings throughout package (#1125)
- fix bug in \_downloader.\_save_to_cache function usage (#1107)
- fix bug in handling requests ConnectionError when querying Overpass status endpoint (#1113)
- minor fixes throughout to address inconsistencies revealed by type enforcement (#1107 #1114)
Expand Down
34 changes: 21 additions & 13 deletions osmnx/_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ def _save_to_cache(
"""
if settings.use_cache:
if not ok: # pragma: no cover
utils.log("Did not save to cache because HTTP status code is not OK.", level=lg.WARNING)
msg = "Did not save to cache because HTTP status code is not OK"
utils.log(msg, level=lg.WARNING)
else:
# create the folder on the disk if it doesn't already exist
cache_folder = Path(settings.cache_folder)
Expand All @@ -68,7 +69,8 @@ def _save_to_cache(

# dump to json, and save to file
cache_filepath.write_text(json.dumps(response_json), encoding="utf-8")
utils.log(f"Saved response to cache file {str(cache_filepath)!r}")
msg = f"Saved response to cache file {str(cache_filepath)!r}"
utils.log(msg, level=lg.INFO)


def _url_in_cache(url: str) -> Path | None:
Expand Down Expand Up @@ -129,14 +131,17 @@ def _retrieve_from_cache(
if (
check_remark and isinstance(response_json, dict) and "remark" in response_json
): # pragma: no cover
utils.log(
msg = (
f"Ignoring cache file {str(cache_filepath)!r} because "
f"it contains a remark: {response_json['remark']!r}"
)
utils.log(msg, lg.WARNING)
return None

utils.log(f"Retrieved response from cache file {str(cache_filepath)!r}")
msg = f"Retrieved response from cache file {str(cache_filepath)!r}"
utils.log(msg, lg.INFO)
return response_json

return None


Expand Down Expand Up @@ -197,7 +202,8 @@ def _resolve_host_via_doh(hostname: str) -> str:
"""
if settings.doh_url_template is None:
# if user has set the url template to None, return hostname itself
utils.log("User set `doh_url_template=None`, requesting host by name", level=lg.WARNING)
msg = "User set `doh_url_template=None`, requesting host by name"
utils.log(msg, level=lg.WARNING)
return hostname

err_msg = f"Failed to resolve {hostname!r} IP via DoH, requesting host by name"
Expand Down Expand Up @@ -255,16 +261,15 @@ def _config_dns(url: str) -> None:
ip = socket.gethostbyname(hostname)
except socket.gaierror: # pragma: no cover
# may occur when using a proxy, so instead resolve IP address via DoH
utils.log(
f"Encountered gaierror while trying to resolve {hostname!r}, trying again via DoH...",
level=lg.ERROR,
)
msg = f"Encountered gaierror while trying to resolve {hostname!r}, trying again via DoH..."
utils.log(msg, level=lg.ERROR)
ip = _resolve_host_via_doh(hostname)

# mutate socket.getaddrinfo to map hostname -> IP address
def _getaddrinfo(*args, **kwargs): # type: ignore[no-untyped-def]
if args[0] == hostname:
utils.log(f"Resolved {hostname!r} to {ip!r}")
msg = f"Resolved {hostname!r} to {ip!r}"
utils.log(msg, level=lg.INFO)
return _original_getaddrinfo(ip, *args[1:], **kwargs)

# otherwise
Expand Down Expand Up @@ -308,7 +313,8 @@ def _parse_response(response: requests.Response) -> dict[str, Any] | list[dict[s
# log the response size and domain
domain = _hostname_from_url(response.url)
size_kb = len(response.content) / 1000
utils.log(f"Downloaded {size_kb:,.1f}kB from {domain!r} with status {response.status_code}")
msg = f"Downloaded {size_kb:,.1f}kB from {domain!r} with status {response.status_code}"
utils.log(msg, level=lg.INFO)

# parse the response to JSON and log/raise exceptions
try:
Expand All @@ -322,10 +328,12 @@ def _parse_response(response: requests.Response) -> dict[str, Any] | list[dict[s

# log any remarks if they exist
if isinstance(response_json, dict) and "remark" in response_json: # pragma: no cover
utils.log(f'{domain!r} remarked: {response_json["remark"]!r}', level=lg.WARNING)
msg = f"{domain!r} remarked: {response_json['remark']!r}"
utils.log(msg, level=lg.WARNING)

# log if the response status_code is not OK
if not response.ok:
utils.log(f"{domain!r} returned HTTP status code {response.status_code}", level=lg.WARNING)
msg = f"{domain!r} returned HTTP status code {response.status_code}"
utils.log(msg, level=lg.WARNING)

return response_json
6 changes: 4 additions & 2 deletions osmnx/_nominatim.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,13 @@ def _nominatim_request(

# pause then request this URL
domain = _http._hostname_from_url(url)
utils.log(f"Pausing {pause} second(s) before making HTTP GET request to {domain!r}")
msg = f"Pausing {pause} second(s) before making HTTP GET request to {domain!r}"
utils.log(msg, level=lg.INFO)
time.sleep(pause)

# transmit the HTTP GET request
utils.log(f"Get {prepared_url} with timeout={settings.timeout}")
msg = f"Get {prepared_url} with timeout={settings.timeout}"
utils.log(msg, level=lg.INFO)
response = requests.get(
url,
params=params,
Expand Down
16 changes: 11 additions & 5 deletions osmnx/_osm_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
from __future__ import annotations

import bz2
import logging as lg
import xml.sax
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any
from typing import TextIO
from warnings import warn
from xml.etree import ElementTree as ET

import geopandas as gpd
import networkx as nx
import numpy as np
import pandas as pd
Expand All @@ -20,6 +21,9 @@
from . import utils_graph
from ._version import __version__

if TYPE_CHECKING:
import geopandas as gpd


class _OSMContentHandler(xml.sax.handler.ContentHandler):
"""
Expand Down Expand Up @@ -103,7 +107,7 @@ def _opener(filepath: Path, encoding: str) -> TextIO:
"in OSMnx, use the `io.save_graphml` and `io.load_graphml` "
"functions instead. Refer to the documentation for details."
)
warn(msg, stacklevel=2)
warn(msg, category=UserWarning, stacklevel=2)

# parse the XML to Overpass-like JSON
with _opener(Path(filepath), encoding) as f:
Expand Down Expand Up @@ -179,7 +183,7 @@ def _save_graph_xml(
"For the `save_graph_xml` function to behave properly, the graph "
"must have been created with `ox.settings.all_oneway=True`."
)
warn(msg, stacklevel=2)
warn(msg, category=UserWarning, stacklevel=2)

if isinstance(data, nx.MultiDiGraph):
gdf_nodes, gdf_edges = utils_graph.graph_to_gdfs(
Expand Down Expand Up @@ -229,7 +233,8 @@ def _save_graph_xml(

# write to disk
ET.ElementTree(root).write(filepath, encoding="utf-8", xml_declaration=True)
utils.log(f"Saved graph as .osm file at {filepath!r}")
msg = f"Saved graph as .osm file at {filepath!r}"
utils.log(msg, level=lg.INFO)


def _append_nodes_xml_tree(
Expand Down Expand Up @@ -496,6 +501,7 @@ def _get_unique_nodes_ordered_from_way(df_way_edges: pd.DataFrame) -> list[Any]:
num_unique_nodes = len(np.unique(all_nodes))

if len(unique_ordered_nodes) < num_unique_nodes:
utils.log(f"Recovered order for {len(unique_ordered_nodes)} of {num_unique_nodes} nodes")
msg = f"Recovered order for {len(unique_ordered_nodes)} of {num_unique_nodes} nodes"
utils.log(msg, level=lg.INFO)

return unique_ordered_nodes
41 changes: 27 additions & 14 deletions osmnx/_overpass.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@
import logging as lg
import time
from collections import OrderedDict
from collections.abc import Iterator
from typing import TYPE_CHECKING
from typing import Any

import numpy as np
import requests
from requests.exceptions import ConnectionError
from shapely.geometry import MultiPolygon
from shapely.geometry import Polygon

from . import _http
from . import projection
Expand All @@ -22,6 +20,12 @@
from . import utils_geo
from ._errors import InsufficientResponseError

if TYPE_CHECKING:
from collections.abc import Iterator

from shapely.geometry import MultiPolygon
from shapely.geometry import Polygon


def _get_network_filter(network_type: str) -> str:
"""
Expand Down Expand Up @@ -149,13 +153,15 @@ def _get_overpass_pause(
)
status = response.text.split("\n")[4]
status_first_token = status.split(" ")[0]
except ConnectionError as ce: # pragma: no cover
except ConnectionError as e: # pragma: no cover
# cannot reach status endpoint, log error and return default duration
utils.log(f"Unable to query {url}, {ce}", level=lg.ERROR)
msg = f"Unable to query {url}, {e}"
utils.log(msg, level=lg.ERROR)
return default_duration
except (AttributeError, IndexError, ValueError): # pragma: no cover
# cannot parse output, log error and return default duration
utils.log(f"Unable to parse {url} response: {response.text}", level=lg.ERROR)
msg = f"Unable to parse {url} response: {response.text}"
utils.log(msg, level=lg.ERROR)
return default_duration

try:
Expand All @@ -182,7 +188,8 @@ def _get_overpass_pause(

# any other status is unrecognized: log error, return default duration
else:
utils.log(f"Unrecognized server status: {status!r}", level=lg.ERROR)
msg = f"Unrecognized server status: {status!r}"
utils.log(msg, level=lg.ERROR)
return default_duration

return pause
Expand Down Expand Up @@ -232,13 +239,15 @@ def _make_overpass_polygon_coord_strs(polygon: Polygon | MultiPolygon) -> list[s
coord_strs = []
for geom in multi_poly.geoms:
x, y = geom.exterior.xy
coord_list = [f'{xy[1]:.6f}{" "}{xy[0]:.6f}' for xy in zip(x, y)]
coord_list = [f"{xy[1]:.6f}{' '}{xy[0]:.6f}" for xy in zip(x, y)]
coord_strs.append(" ".join(coord_list))

return coord_strs


def _create_overpass_query(polygon_coord_str: str, tags: dict[str, bool | str | list[str]]) -> str:
def _create_overpass_features_query(
polygon_coord_str: str, tags: dict[str, bool | str | list[str]]
) -> str:
"""
Create an Overpass features query string based on tags.

Expand Down Expand Up @@ -334,7 +343,8 @@ def _download_overpass_network(

# subdivide query polygon to get list of sub-divided polygon coord strings
polygon_coord_strs = _make_overpass_polygon_coord_strs(polygon)
utils.log(f"Requesting data from API in {len(polygon_coord_strs)} request(s)")
msg = f"Requesting data from API in {len(polygon_coord_strs)} request(s)"
utils.log(msg, level=lg.INFO)

# pass exterior coordinates of each polygon in list to API, one at a time
# the '>' makes it recurse so we get ways and the ways' nodes.
Expand Down Expand Up @@ -363,11 +373,12 @@ def _download_overpass_features(
"""
# subdivide query polygon to get list of sub-divided polygon coord strings
polygon_coord_strs = _make_overpass_polygon_coord_strs(polygon)
utils.log(f"Requesting data from API in {len(polygon_coord_strs)} request(s)")
msg = f"Requesting data from API in {len(polygon_coord_strs)} request(s)"
utils.log(msg, level=lg.INFO)

# pass exterior coordinates of each polygon in list to API, one at a time
for polygon_coord_str in polygon_coord_strs:
query_str = _create_overpass_query(polygon_coord_str, tags)
query_str = _create_overpass_features_query(polygon_coord_str, tags)
yield _overpass_request(OrderedDict(data=query_str))


Expand Down Expand Up @@ -406,11 +417,13 @@ def _overpass_request(
if pause is None:
this_pause = _get_overpass_pause(settings.overpass_endpoint)
domain = _http._hostname_from_url(url)
utils.log(f"Pausing {this_pause} second(s) before making HTTP POST request to {domain!r}")
msg = f"Pausing {this_pause} second(s) before making HTTP POST request to {domain!r}"
utils.log(msg, level=lg.INFO)
time.sleep(this_pause)

# transmit the HTTP POST request
utils.log(f"Post {prepared_url} with timeout={settings.timeout}")
msg = f"Post {prepared_url} with timeout={settings.timeout}"
utils.log(msg, level=lg.INFO)
response = requests.post(
url,
data=data,
Expand Down
27 changes: 13 additions & 14 deletions osmnx/bearing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import networkx as nx
import numpy as np
from numpy.typing import NDArray

from . import projection

Expand All @@ -31,20 +30,20 @@ def calculate_bearing(
# if coords are all arrays, return array
@overload # pragma: no cover
def calculate_bearing(
lat1: NDArray[np.float64],
lon1: NDArray[np.float64],
lat2: NDArray[np.float64],
lon2: NDArray[np.float64],
) -> NDArray[np.float64]:
lat1: np.typing.NDArray[np.float64],
lon1: np.typing.NDArray[np.float64],
lat2: np.typing.NDArray[np.float64],
lon2: np.typing.NDArray[np.float64],
) -> np.typing.NDArray[np.float64]:
...


def calculate_bearing(
lat1: float | NDArray[np.float64],
lon1: float | NDArray[np.float64],
lat2: float | NDArray[np.float64],
lon2: float | NDArray[np.float64],
) -> float | NDArray[np.float64]:
lat1: float | np.typing.NDArray[np.float64],
lon1: float | np.typing.NDArray[np.float64],
lat2: float | np.typing.NDArray[np.float64],
lon2: float | np.typing.NDArray[np.float64],
) -> float | np.typing.NDArray[np.float64]:
"""
Calculate the compass bearing(s) between pairs of lat-lon points.

Expand Down Expand Up @@ -80,7 +79,7 @@ def calculate_bearing(
initial_bearing = np.degrees(np.arctan2(y, x))

# normalize to 0-360 degrees to get compass bearing
bearing: float | NDArray[np.float64] = initial_bearing % 360
bearing: float | np.typing.NDArray[np.float64] = initial_bearing % 360
return bearing


Expand Down Expand Up @@ -168,7 +167,7 @@ def orientation_entropy(

def _extract_edge_bearings(
Gu: nx.MultiGraph, min_length: float = 0, weight: str | None = None
) -> NDArray[np.float64]:
) -> np.typing.NDArray[np.float64]:
"""
Extract undirected graph's bidirectional edge bearings.

Expand Down Expand Up @@ -216,7 +215,7 @@ def _extract_edge_bearings(

def _bearings_distribution(
Gu: nx.MultiGraph, num_bins: int, min_length: float = 0, weight: str | None = None
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
) -> tuple[np.typing.NDArray[np.float64], np.typing.NDArray[np.float64]]:
"""
Compute distribution of bearings across evenly spaced bins.

Expand Down
Loading
Loading