diff --git a/.github/workflows/_tests.yml b/.github/workflows/_tests.yml index f7a9546d..d589ab99 100644 --- a/.github/workflows/_tests.yml +++ b/.github/workflows/_tests.yml @@ -9,7 +9,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11"] include: - os: macos-latest python-version: "3.11" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc4cac0a..db156b89 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -54,7 +54,7 @@ repos: rev: v1.17.0 hooks: - id: refurb - args: ["--python-version", "3.8"] + args: ["--python-version", "3.9"] language: python language_version: python3.10 stages: [manual] diff --git a/CHANGELOG.md b/CHANGELOG.md index f2ab4233..c7732069 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security +## [0.6.0] - 2023-11-02 + +### Changed + +- update code to use Python 3.9 syntax. + +### Removed + +- Support for Python 3.8. + ## [0.5.2] - 2023-10-29 ### Added @@ -196,7 +206,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Intersection Joiner - Geoparquet Loader -[unreleased]: https://github.com/srai-lab/srai/compare/0.5.2...HEAD +[unreleased]: https://github.com/srai-lab/srai/compare/0.6.0...HEAD +[0.6.0]: https://github.com/srai-lab/srai/compare/0.5.2...0.6.0 [0.5.2]: https://github.com/srai-lab/srai/compare/0.5.1...0.5.2 [0.5.1]: https://github.com/srai-lab/srai/compare/0.5.0...0.5.1 [0.5.0]: https://github.com/srai-lab/srai/compare/0.4.1...0.5.0 diff --git a/CITATION.cff b/CITATION.cff index e81606d7..c8650677 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -14,7 +14,7 @@ authors: given-names: "Szymon" orcid: "https://orcid.org/0000-0002-2047-1649" title: "SRAI: Spatial Representations for Artificial Intelligence" -version: 0.5.2 +version: 0.6.0 date-released: 2022-11-23 url: "https://kraina-ai.github.io/srai" repository-code: "https://github.com/kraina-ai/srai" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53323b66..e7f73752 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ To make changes to srai's code base, you need to fork and then clone the GitHub For first setup of the project locally, the following commands have to be executed. -0. Make sure you have installed at least version **3.8+** of Python. +0. Make sure you have installed at least version **3.9+** of Python. 1. Install [PDM](https://pdm.fming.dev/latest) (only if not already installed) @@ -24,7 +24,7 @@ For first setup of the project locally, the following commands have to be execut ```sh # Optional if you want to create venv in a specific version. More info: https://pdm.fming.dev/latest/usage/venv/#create-a-virtualenv-yourself - pdm venv create 3.8 # or any higher version of Python + pdm venv create 3.9 # or any higher version of Python pdm install -G:all ``` @@ -52,7 +52,7 @@ For testing, [tox](https://tox.wiki/en/latest/) is used to allow testing on mult To test code locally before committing, run: ```sh -tox -e python3.8 # put your python version here +tox -e python3.9 # put your python version here ``` diff --git a/pdm.lock b/pdm.lock index 9ec69de9..4865a03b 100644 --- a/pdm.lock +++ b/pdm.lock @@ -6,7 +6,7 @@ groups = ["default", "all", "dev", "docs", "gtfs", "license", "lint", "osm", "pe cross_platform = true static_urls = false lock_version = "4.3" -content_hash = "sha256:469b4e138ebe509b7151728c3f99534640e16be5716e5f8a5bf1726ea90a32a5" +content_hash = "sha256:bee382bb644b13d92a5bbdccdd61f7e4ca63165f18896b0c1c9b855679472c31" [[package]] name = "aiohttp" @@ -276,9 +276,6 @@ name = "babel" version = "2.12.1" requires_python = ">=3.7" summary = "Internationalization utilities" -dependencies = [ - "pytz>=2015.7; python_version < \"3.9\"", -] files = [ {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, @@ -1487,8 +1484,6 @@ requires_python = ">=3.7" summary = "An implementation of JSON Schema validation for Python" dependencies = [ "attrs>=17.4.0", - "importlib-resources>=1.4.0; python_version < \"3.9\"", - "pkgutil-resolve-name>=1.3.10; python_version < \"3.9\"", "pyrsistent!=0.17.0,!=0.17.1,!=0.17.2,>=0.14.0", ] files = [ @@ -1636,7 +1631,6 @@ summary = "JupyterLab computational environment" dependencies = [ "async-lru>=1.0.0", "importlib-metadata>=4.8.3; python_version < \"3.10\"", - "importlib-resources>=1.4; python_version < \"3.9\"", "ipykernel", "jinja2>=3.0.3", "jupyter-core", @@ -2533,7 +2527,6 @@ version = "7.0.0" requires_python = ">=3.8" summary = "Jupyter Notebook - A web-based notebook environment for interactive computing" dependencies = [ - "importlib-resources>=5.0; python_version < \"3.9\"", "jupyter-server<3,>=2.4.0", "jupyterlab-server<3,>=2.22.1", "jupyterlab<5,>=4.0.2", @@ -2564,7 +2557,6 @@ version = "0.57.1" requires_python = ">=3.8" summary = "compiling Python code using LLVM" dependencies = [ - "importlib-metadata; python_version < \"3.9\"", "llvmlite<0.41,>=0.40.0dev0", "numpy<1.25,>=1.21", ] @@ -3016,16 +3008,6 @@ files = [ {file = "Pillow-10.0.0.tar.gz", hash = "sha256:9c82b5b3e043c7af0d95792d0d20ccf68f61a1fec6b3530e718b688422727396"}, ] -[[package]] -name = "pkgutil-resolve-name" -version = "1.3.10" -requires_python = ">=3.6" -summary = "Resolve a name to an object." -files = [ - {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, - {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, -] - [[package]] name = "platformdirs" version = "3.9.1" @@ -3830,7 +3812,6 @@ summary = "Render rich text, tables, progress bars, syntax highlighting, markdow dependencies = [ "markdown-it-py>=2.2.0", "pygments<3.0.0,>=2.13.0", - "typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"", ] files = [ {file = "rich-13.4.2-py3-none-any.whl", hash = "sha256:8f87bc7ee54675732fa66a05ebfe489e27264caeeff3728c945d25971b6485ec"}, @@ -4329,7 +4310,6 @@ dependencies = [ "numpy>1.20.0", "packaging", "torch>=1.8.1", - "typing-extensions; python_version < \"3.9\"", ] files = [ {file = "torchmetrics-1.0.1-py3-none-any.whl", hash = "sha256:5278ebdf4ecc168d88d87d3f02045ceee6a4a4ae24d8bf09d616ab67441dde0a"}, diff --git a/pyproject.toml b/pyproject.toml index 420f0149..f18a91c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "srai" -version = "0.5.2" +version = "0.6.0" description = "A set of python modules for geospatial machine learning and data mining" authors = [ { name = "Piotr Gramacki", email = "pgramacki@kraina.ai" }, @@ -26,7 +26,7 @@ dependencies = [ "requests", "h3ronpy>=0.18.0", ] -requires-python = ">=3.8" +requires-python = ">=3.9" readme = "README.md" license = { text = "Apache-2.0" } classifiers = [ @@ -37,7 +37,6 @@ classifiers = [ "Intended Audience :: Science/Research", "Intended Audience :: Developers", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -184,7 +183,7 @@ close-quotes-on-newline = true wrap-one-line = true [tool.bumpver] -current_version = "0.5.2" +current_version = "0.6.0" version_pattern = "MAJOR.MINOR.PATCH[PYTAGNUM]" commit_message = "chore: bump version {old_version} -> {new_version}" commit = true diff --git a/srai/__init__.py b/srai/__init__.py index f8b5f152..587f085e 100644 --- a/srai/__init__.py +++ b/srai/__init__.py @@ -10,4 +10,4 @@ for complete documentation. """ -__version__ = "0.5.2" +__version__ = "0.6.0" diff --git a/srai/embedders/_base.py b/srai/embedders/_base.py index 0ac30abc..a5c4b5f6 100644 --- a/srai/embedders/_base.py +++ b/srai/embedders/_base.py @@ -2,7 +2,7 @@ import abc from pathlib import Path -from typing import Any, Dict, TypeVar, Union +from typing import Any, TypeVar, Union import geopandas as gpd import pandas as pd @@ -19,7 +19,7 @@ class Model(LightningModule): # type: ignore """Class for model based on LightningModule.""" - def get_config(self) -> Dict[str, Any]: + def get_config(self) -> dict[str, Any]: """Get model config.""" model_config = { k: v diff --git a/srai/embedders/contextual_count_embedder.py b/srai/embedders/contextual_count_embedder.py index cfad6358..d3f394e6 100644 --- a/srai/embedders/contextual_count_embedder.py +++ b/srai/embedders/contextual_count_embedder.py @@ -8,7 +8,8 @@ 1. https://arxiv.org/abs/2111.00990 """ -from typing import Iterator, List, Optional, Tuple, Union +from collections.abc import Iterator +from typing import Optional, Union import geopandas as gpd import numpy as np @@ -30,7 +31,7 @@ def __init__( neighbourhood_distance: int, concatenate_vectors: bool = False, expected_output_features: Optional[ - Union[List[str], OsmTagsFilter, GroupedOsmTagsFilter] + Union[list[str], OsmTagsFilter, GroupedOsmTagsFilter] ] = None, count_subcategories: bool = False, ) -> None: @@ -171,7 +172,7 @@ def _get_concatenated_embeddings(self, counts_df: pd.DataFrame) -> pd.DataFrame: def _get_averaged_values_for_distances( self, counts_df: pd.DataFrame - ) -> Iterator[Tuple[int, npt.NDArray[np.float32]]]: + ) -> Iterator[tuple[int, npt.NDArray[np.float32]]]: """ Generate averaged values for neighbours at given distances. diff --git a/srai/embedders/count_embedder.py b/srai/embedders/count_embedder.py index 4d246e14..c73310d2 100644 --- a/srai/embedders/count_embedder.py +++ b/srai/embedders/count_embedder.py @@ -3,7 +3,7 @@ This module contains count embedder implementation. """ -from typing import List, Optional, Set, Union, cast +from typing import Optional, Union, cast import geopandas as gpd import pandas as pd @@ -19,7 +19,7 @@ class CountEmbedder(Embedder): def __init__( self, expected_output_features: Optional[ - Union[List[str], OsmTagsFilter, GroupedOsmTagsFilter] + Union[list[str], OsmTagsFilter, GroupedOsmTagsFilter] ] = None, count_subcategories: bool = True, ) -> None: @@ -100,7 +100,7 @@ def transform( def _parse_expected_output_features( self, - expected_output_features: Optional[Union[List[str], OsmTagsFilter, GroupedOsmTagsFilter]], + expected_output_features: Optional[Union[list[str], OsmTagsFilter, GroupedOsmTagsFilter]], ) -> None: expected_output_features_list = [] @@ -127,8 +127,8 @@ def _parse_expected_output_features( def _parse_osm_tags_filter_to_expected_features( self, osm_filter: OsmTagsFilter, delimiter: str = "_" - ) -> List[str]: - expected_output_features: Set[str] = set() + ) -> list[str]: + expected_output_features: set[str] = set() if not self.count_subcategories: expected_output_features.update(osm_filter.keys()) @@ -150,8 +150,8 @@ def _parse_osm_tags_filter_to_expected_features( def _parse_grouped_osm_tags_filter_to_expected_features( self, grouped_osm_filter: GroupedOsmTagsFilter - ) -> List[str]: - expected_output_features: Set[str] = set() + ) -> list[str]: + expected_output_features: set[str] = set() if not self.count_subcategories: expected_output_features.update(grouped_osm_filter.keys()) diff --git a/srai/loaders/geoparquet_loader.py b/srai/loaders/geoparquet_loader.py index a7a6e343..342623b6 100644 --- a/srai/loaders/geoparquet_loader.py +++ b/srai/loaders/geoparquet_loader.py @@ -5,7 +5,7 @@ """ from pathlib import Path -from typing import List, Optional, Union +from typing import Optional, Union import geopandas as gpd @@ -28,7 +28,7 @@ def load( self, file_path: Union[Path, str], index_column: Optional[str] = None, - columns: Optional[List[str]] = None, + columns: Optional[list[str]] = None, area: Optional[gpd.GeoDataFrame] = None, ) -> gpd.GeoDataFrame: """ diff --git a/srai/neighbourhoods/_base.py b/srai/neighbourhoods/_base.py index 8c195e3e..a3de5dba 100644 --- a/srai/neighbourhoods/_base.py +++ b/srai/neighbourhoods/_base.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from queue import Queue -from typing import Dict, Generic, Optional, Set, Tuple, TypeVar +from typing import Generic, Optional, TypeVar from functional import seq @@ -39,7 +39,7 @@ def __init__(self, include_center: bool = False) -> None: @abstractmethod def get_neighbours( self, index: IndexType, include_center: Optional[bool] = None - ) -> Set[IndexType]: + ) -> set[IndexType]: """ Get the direct neighbours of a region using its index. @@ -55,7 +55,7 @@ def get_neighbours( def get_neighbours_up_to_distance( self, index: IndexType, distance: int, include_center: Optional[bool] = None - ) -> Set[IndexType]: + ) -> set[IndexType]: """ Get the neighbours of a region up to a certain distance. @@ -70,7 +70,7 @@ def get_neighbours_up_to_distance( Set[IndexType]: Indexes of the neighbours. """ neighbours_with_distances = self._get_neighbours_with_distances(index, distance) - neighbours: Set[IndexType] = seq(neighbours_with_distances).map(lambda x: x[0]).to_set() + neighbours: set[IndexType] = seq(neighbours_with_distances).map(lambda x: x[0]).to_set() neighbours = self._handle_center( index, distance, neighbours, at_distance=False, include_center_override=include_center ) @@ -78,7 +78,7 @@ def get_neighbours_up_to_distance( def get_neighbours_at_distance( self, index: IndexType, distance: int, include_center: Optional[bool] = None - ) -> Set[IndexType]: + ) -> set[IndexType]: """ Get the neighbours of a region at a certain distance. @@ -93,7 +93,7 @@ def get_neighbours_at_distance( Set[IndexType]: Indexes of the neighbours. """ neighbours_up_to_distance = self._get_neighbours_with_distances(index, distance) - neighbours_at_distance: Set[IndexType] = ( + neighbours_at_distance: set[IndexType] = ( seq(neighbours_up_to_distance) .filter(lambda x: x[1] == distance) .map(lambda x: x[0]) @@ -110,9 +110,9 @@ def get_neighbours_at_distance( def _get_neighbours_with_distances( self, index: IndexType, distance: int - ) -> Set[Tuple[IndexType, int]]: - visited_indexes: Dict[IndexType, int] = {} - to_visit: Queue[Tuple[IndexType, int]] = Queue() + ) -> set[tuple[IndexType, int]]: + visited_indexes: dict[IndexType, int] = {} + to_visit: Queue[tuple[IndexType, int]] = Queue() to_visit.put((index, 0)) while not to_visit.empty(): @@ -133,10 +133,10 @@ def _handle_center( self, index: IndexType, distance: int, - neighbours: Set[IndexType], + neighbours: set[IndexType], at_distance: bool, include_center_override: Optional[bool], - ) -> Set[IndexType]: + ) -> set[IndexType]: if include_center_override is None: include_center = self.include_center else: diff --git a/srai/neighbourhoods/adjacency_neighbourhood.py b/srai/neighbourhoods/adjacency_neighbourhood.py index 48fd0c7c..bb873921 100644 --- a/srai/neighbourhoods/adjacency_neighbourhood.py +++ b/srai/neighbourhoods/adjacency_neighbourhood.py @@ -4,7 +4,8 @@ This module contains the AdjacencyNeighbourhood class, that allows to get the neighbours of any region based on its borders. """ -from typing import Dict, Hashable, Optional, Set +from collections.abc import Hashable +from typing import Optional import geopandas as gpd @@ -40,7 +41,7 @@ def __init__(self, regions_gdf: gpd.GeoDataFrame, include_center: bool = False) if GEOMETRY_COLUMN not in regions_gdf.columns: raise ValueError("Regions must have a geometry column.") self.regions_gdf = regions_gdf - self.lookup: Dict[Hashable, Set[Hashable]] = {} + self.lookup: dict[Hashable, set[Hashable]] = {} def generate_neighbourhoods(self) -> None: """Generate the lookup table for all regions.""" @@ -50,7 +51,7 @@ def generate_neighbourhoods(self) -> None: def get_neighbours( self, index: Hashable, include_center: Optional[bool] = None - ) -> Set[Hashable]: + ) -> set[Hashable]: """ Get the direct neighbours of any region using its index. @@ -74,7 +75,7 @@ def get_neighbours( ) return neighbours - def _get_adjacent_neighbours(self, index: Hashable) -> Set[Hashable]: + def _get_adjacent_neighbours(self, index: Hashable) -> set[Hashable]: """ Get the direct neighbours of a region using `touches` [1] operator from the Shapely library. diff --git a/srai/neighbourhoods/h3_neighbourhood.py b/srai/neighbourhoods/h3_neighbourhood.py index 18e6b90d..0596fa5e 100644 --- a/srai/neighbourhoods/h3_neighbourhood.py +++ b/srai/neighbourhoods/h3_neighbourhood.py @@ -3,7 +3,7 @@ This module contains the H3Neighbourhood class, that allows to get the neighbours of an H3 region. """ -from typing import Optional, Set +from typing import Optional import geopandas as gpd import h3 @@ -40,11 +40,11 @@ def __init__( unless overridden in the function call. """ super().__init__(include_center) - self._available_indices: Optional[Set[str]] = None + self._available_indices: Optional[set[str]] = None if regions_gdf is not None: self._available_indices = set(regions_gdf.index) - def get_neighbours(self, index: str, include_center: Optional[bool] = None) -> Set[str]: + def get_neighbours(self, index: str, include_center: Optional[bool] = None) -> set[str]: """ Get the direct neighbours of an H3 region using its index. @@ -64,7 +64,7 @@ def get_neighbours_up_to_distance( distance: int, include_center: Optional[bool] = None, unchecked: bool = False, - ) -> Set[str]: + ) -> set[str]: """ Get the neighbours of an H3 region up to a certain distance. @@ -81,7 +81,7 @@ def get_neighbours_up_to_distance( if self._distance_incorrect(distance): return set() - neighbours: Set[str] = h3.grid_disk(index, distance) + neighbours: set[str] = h3.grid_disk(index, distance) neighbours = self._handle_center( index, distance, neighbours, at_distance=False, include_center_override=include_center ) @@ -91,7 +91,7 @@ def get_neighbours_up_to_distance( def get_neighbours_at_distance( self, index: str, distance: int, include_center: Optional[bool] = None - ) -> Set[str]: + ) -> set[str]: """ Get the neighbours of an H3 region at a certain distance. @@ -107,13 +107,13 @@ def get_neighbours_at_distance( if self._distance_incorrect(distance): return set() - neighbours: Set[str] = h3.grid_ring(index, distance) + neighbours: set[str] = h3.grid_ring(index, distance) neighbours = self._handle_center( index, distance, neighbours, at_distance=True, include_center_override=include_center ) return self._select_available(neighbours) - def _select_available(self, indices: Set[str]) -> Set[str]: + def _select_available(self, indices: set[str]) -> set[str]: if self._available_indices is None: return indices return indices.intersection(self._available_indices) diff --git a/srai/plotting/folium_wrapper.py b/srai/plotting/folium_wrapper.py index 54efda1c..15497136 100644 --- a/srai/plotting/folium_wrapper.py +++ b/srai/plotting/folium_wrapper.py @@ -5,7 +5,7 @@ function. """ from itertools import cycle, islice -from typing import List, Optional, Set, Union +from typing import Optional, Union import branca.colormap as cm import folium @@ -27,7 +27,7 @@ def plot_regions( tiles_style: str = "OpenStreetMap", height: Union[str, float] = "100%", width: Union[str, float] = "100%", - colormap: Union[str, List[str]] = px.colors.qualitative.Bold, + colormap: Union[str, list[str]] = px.colors.qualitative.Bold, map: Optional[folium.Map] = None, show_borders: bool = True, ) -> folium.Map: @@ -72,7 +72,7 @@ def plot_numeric_data( tiles_style: str = "CartoDB positron", height: Union[str, float] = "100%", width: Union[str, float] = "100%", - colormap: Union[str, List[str]] = px.colors.sequential.Sunsetdark, + colormap: Union[str, list[str]] = px.colors.sequential.Sunsetdark, map: Optional[folium.Map] = None, show_borders: bool = False, opacity: float = 0.8, @@ -135,7 +135,7 @@ def plot_numeric_data( def plot_neighbours( regions_gdf: gpd.GeoDataFrame, region_id: IndexType, - neighbours_ids: Set[IndexType], + neighbours_ids: set[IndexType], tiles_style: str = "OpenStreetMap", height: Union[str, float] = "100%", width: Union[str, float] = "100%", @@ -195,7 +195,7 @@ def plot_all_neighbourhood( tiles_style: str = "OpenStreetMap", height: Union[str, float] = "100%", width: Union[str, float] = "100%", - colormap: Union[str, List[str]] = px.colors.sequential.Agsunset_r, + colormap: Union[str, list[str]] = px.colors.sequential.Agsunset_r, map: Optional[folium.Map] = None, show_borders: bool = True, ) -> folium.Map: @@ -263,8 +263,8 @@ def plot_all_neighbourhood( ) -def _resample_plotly_colormap(colormap: List[str], steps: int) -> List[str]: - resampled_colormap: List[str] = px.colors.sample_colorscale( +def _resample_plotly_colormap(colormap: list[str], steps: int) -> list[str]: + resampled_colormap: list[str] = px.colors.sample_colorscale( colormap, np.linspace(0, 1, num=steps) ) return resampled_colormap @@ -272,15 +272,15 @@ def _resample_plotly_colormap(colormap: List[str], steps: int) -> List[str]: def _generate_colormap( distance: int, - colormap: List[str], + colormap: list[str], selected_color: str = "rgb(242, 242, 242)", other_color: str = "rgb(153, 153, 153)", -) -> List[str]: +) -> list[str]: return [selected_color, *islice(cycle(colormap), None, distance - 1), other_color] def _generate_linear_colormap( - colormap: List[str], min_value: float, max_value: float + colormap: list[str], min_value: float, max_value: float ) -> cm.LinearColormap: values, _ = px.colors.convert_colors_to_same_type(colormap, colortype="tuple") return cm.LinearColormap(values, vmin=min_value, vmax=max_value) diff --git a/srai/plotting/plotly_wrapper.py b/srai/plotting/plotly_wrapper.py index fe47d13d..4ea0aa03 100644 --- a/srai/plotting/plotly_wrapper.py +++ b/srai/plotting/plotly_wrapper.py @@ -3,7 +3,7 @@ This module contains functions for quick plotting of analysed gdfs using Plotly library. """ -from typing import Any, Dict, List, Optional, Set +from typing import Any, Optional import geopandas as gpd import numpy as np @@ -76,7 +76,7 @@ def plot_regions( def plot_neighbours( regions_gdf: gpd.GeoDataFrame, region_id: IndexType, - neighbours_ids: Set[IndexType], + neighbours_ids: set[IndexType], return_plot: bool = False, mapbox_style: str = "open-street-map", mapbox_accesstoken: Optional[str] = None, @@ -222,7 +222,7 @@ def plot_all_neighbourhood( def _plot_regions( regions_gdf: gpd.GeoDataFrame, hover_column_name: str, - hover_data: List[str], + hover_data: list[str], color_feature_column: Optional[str] = None, show_legend: bool = False, return_plot: bool = False, @@ -233,8 +233,8 @@ def _plot_regions( zoom: Optional[float] = None, height: Optional[float] = None, width: Optional[float] = None, - layout_kwargs: Optional[Dict[str, Any]] = None, - traces_kwargs: Optional[Dict[str, Any]] = None, + layout_kwargs: Optional[dict[str, Any]] = None, + traces_kwargs: Optional[dict[str, Any]] = None, **choropleth_mapbox_kwargs: Any, ) -> Optional[go.Figure]: """ diff --git a/srai/regionalizers/_spherical_voronoi.py b/srai/regionalizers/_spherical_voronoi.py index 8b0cf64b..07065d0f 100644 --- a/srai/regionalizers/_spherical_voronoi.py +++ b/srai/regionalizers/_spherical_voronoi.py @@ -7,11 +7,12 @@ import hashlib import warnings +from collections.abc import Hashable from contextlib import suppress from functools import partial from math import ceil from multiprocessing import cpu_count -from typing import Dict, Hashable, List, Optional, Set, Tuple, Union, cast +from typing import Optional, Union, cast import geopandas as gpd import numpy as np @@ -35,14 +36,14 @@ from srai.constants import WGS84_CRS -SPHERE_PARTS: List[SphericalPolygon] = [] -SPHERE_PARTS_BOUNDING_BOXES: List[Polygon] = [] +SPHERE_PARTS: list[SphericalPolygon] = [] +SPHERE_PARTS_BOUNDING_BOXES: list[Polygon] = [] SCIPY_THRESHOLD = 1e-8 VertexHash = bytes -EdgeHash = Tuple[VertexHash, VertexHash] +EdgeHash = tuple[VertexHash, VertexHash] def _generate_sphere_parts() -> None: @@ -71,11 +72,11 @@ def _generate_sphere_parts() -> None: def generate_voronoi_regions( - seeds: Union[gpd.GeoDataFrame, List[Point]], + seeds: Union[gpd.GeoDataFrame, list[Point]], max_meters_between_points: int = 10_000, num_of_multiprocessing_workers: int = -1, multiprocessing_activation_threshold: Optional[int] = None, -) -> List[MultiPolygon]: +) -> list[MultiPolygon]: """ Generate Thessien polygons for a given list of seeds. @@ -174,10 +175,10 @@ def generate_voronoi_regions( # identify all edges - hashed_vertices: Dict[VertexHash, npt.NDArray[np.float32]] = {} - hashed_edges: Set[EdgeHash] = set() + hashed_vertices: dict[VertexHash, npt.NDArray[np.float32]] = {} + hashed_edges: set[EdgeHash] = set() - regions_parts: Dict[int, List[Tuple[int, List[EdgeHash]]]] = {} + regions_parts: dict[int, list[tuple[int, list[EdgeHash]]]] = {} for region_id, sphere_part_id, spherical_polygon_points in spherical_polygons_parts: if region_id not in regions_parts: @@ -219,7 +220,7 @@ def generate_voronoi_regions( # interpolate unique ones - interpolated_edges: Dict[EdgeHash, List[Tuple[float, float]]] + interpolated_edges: dict[EdgeHash, list[tuple[float, float]]] interpolate_polygon_edge_func = partial( _interpolate_polygon_edge, @@ -250,14 +251,14 @@ def generate_voronoi_regions( # use interpolated edges to map spherical polygons into regions - generated_regions: List[MultiPolygon] = [] + generated_regions: list[MultiPolygon] = [] _generate_sphere_parts() for region_id in tqdm(region_ids, desc="Generating polygons"): - multi_polygon_parts: List[Polygon] = [] + multi_polygon_parts: list[Polygon] = [] for sphere_part_id, region_polygon_edges in regions_parts[region_id]: - polygon_points: List[Tuple[float, float]] = [] + polygon_points: list[tuple[float, float]] = [] for edge_start, edge_end in region_polygon_edges: if (edge_start, edge_end) in interpolated_edges: @@ -320,11 +321,11 @@ def _parse_multiprocessing_activation_threshold( return multiprocessing_activation_threshold -def _generate_seeds(gdf: gpd.GeoDataFrame) -> Tuple[List[Point], List[Hashable]]: +def _generate_seeds(gdf: gpd.GeoDataFrame) -> tuple[list[Point], list[Hashable]]: """Transform GeoDataFrame into list of Points with index.""" seeds_wgs84 = gdf.to_crs(crs=WGS84_CRS) - region_ids: List[Hashable] = [] - seeds: List[Point] = [] + region_ids: list[Hashable] = [] + seeds: list[Point] = [] for index, row in seeds_wgs84.iterrows(): candidate_point = row.geometry.centroid @@ -335,21 +336,21 @@ def _generate_seeds(gdf: gpd.GeoDataFrame) -> Tuple[List[Point], List[Hashable]] return seeds, region_ids -def _get_duplicated_seeds_ids(seeds: List[Point], region_ids: List[Hashable]) -> List[Hashable]: +def _get_duplicated_seeds_ids(seeds: list[Point], region_ids: list[Hashable]) -> list[Hashable]: """Return all seeds ids that overlap with another using quick sjoin operation.""" gdf = gpd.GeoDataFrame(data={"geometry": seeds}, index=region_ids, crs=WGS84_CRS) duplicated_seeds = gdf.sjoin(gdf).index.value_counts().loc[lambda x: x > 1] - duplicated_seeds_ids: List[Hashable] = duplicated_seeds.index.to_list() + duplicated_seeds_ids: list[Hashable] = duplicated_seeds.index.to_list() return duplicated_seeds_ids -def _check_if_in_bounds(seeds: List[Point]) -> bool: +def _check_if_in_bounds(seeds: list[Point]) -> bool: """Check if all seeds are within bounds.""" wgs84_earth_bbox = (box(minx=-180, miny=-90, maxx=180, maxy=90),) return all(point.covered_by(wgs84_earth_bbox) for point in seeds) -def _map_to_geocentric(lon: float, lat: float, ell: Ellipsoid) -> Tuple[float, float, float]: +def _map_to_geocentric(lon: float, lat: float, ell: Ellipsoid) -> tuple[float, float, float]: """ Wrapper for a geodetic2ecef function from pymap3d library. @@ -368,7 +369,7 @@ def _map_to_geocentric(lon: float, lat: float, ell: Ellipsoid) -> Tuple[float, f def _generate_spherical_polygons_parts( region_id: int, sv: SphericalVoronoi, -) -> List[Tuple[int, int, npt.NDArray[np.float32]]]: +) -> list[tuple[int, int, npt.NDArray[np.float32]]]: """ Generate spherical polygon intersections with sphere parts. @@ -423,10 +424,10 @@ def _generate_spherical_polygons_parts( def _interpolate_polygon_edge( hashed_edge: EdgeHash, - hashed_vertices: Dict[VertexHash, npt.NDArray[np.float32]], + hashed_vertices: dict[VertexHash, npt.NDArray[np.float32]], ell: Ellipsoid, max_meters_between_points: int, -) -> List[Tuple[float, float]]: +) -> list[tuple[float, float]]: """ Interpolates spherical polygon arc edge to the latitude and longitude. @@ -463,11 +464,11 @@ def _interpolate_polygon_edge( def _interpolate_edge( - start_point: Tuple[float, float, float], - end_point: Tuple[float, float, float], - step_ticks: List[float], + start_point: tuple[float, float, float], + end_point: tuple[float, float, float], + step_ticks: list[float], ell: Ellipsoid, -) -> List[Tuple[float, float]]: +) -> list[tuple[float, float]]: """ Generates latitude and longitude positions for an arc on the sphere. @@ -496,11 +497,11 @@ def _interpolate_edge( def _fix_edge( - edge_points: List[Tuple[float, float]], - bbox_bounds: Tuple[float, float, float, float], + edge_points: list[tuple[float, float]], + bbox_bounds: tuple[float, float, float, float], prev_lon: Optional[float] = None, prev_lat: Optional[float] = None, -) -> List[Tuple[float, float]]: +) -> list[tuple[float, float]]: """ Fixes points laying on the edge between sphere parts. @@ -515,7 +516,7 @@ def _fix_edge( Returns: List[Tuple[float, float]]: Fixed edge points. """ - fixed_edge_points: List[Tuple[float, float]] = [] + fixed_edge_points: list[tuple[float, float]] = [] if prev_lon is not None and prev_lat is not None: fixed_edge_points.append((prev_lon, prev_lat)) @@ -541,7 +542,7 @@ def _map_from_geocentric( y: npt.NDArray[np.float32], z: npt.NDArray[np.float32], ell: Ellipsoid, -) -> Tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]: +) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]: """ Wrapper for a ecef2geodetic function from pymap3d library. @@ -708,8 +709,8 @@ def ecef2geodetic_vectorized( def _fix_lat_lon( lon: float, lat: float, - bbox: Tuple[float, float, float, float], -) -> Tuple[float, float]: + bbox: tuple[float, float, float, float], +) -> tuple[float, float]: """ Fix point signs and rounding. diff --git a/srai/regionalizers/administrative_boundary_regionalizer.py b/srai/regionalizers/administrative_boundary_regionalizer.py index ebcabca7..0ec0ac06 100644 --- a/srai/regionalizers/administrative_boundary_regionalizer.py +++ b/srai/regionalizers/administrative_boundary_regionalizer.py @@ -7,7 +7,7 @@ import json import time from pathlib import Path -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional, Union import geopandas as gpd import topojson as tp @@ -194,10 +194,10 @@ def transform(self, gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame: def _generate_regions_from_all_geometries( self, gdf_wgs84: gpd.GeoDataFrame - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: """Query and optimize downloading data from Overpass.""" elements_ids = set() - generated_regions: List[Dict[str, Any]] = [] + generated_regions: list[dict[str, Any]] = [] unary_geometry = GeometryCollection() all_geometries = flatten_geometry_series(gdf_wgs84.geometry) @@ -218,7 +218,7 @@ def _generate_regions_from_all_geometries( return generated_regions - def _query_overpass(self, query: str) -> List[Dict[str, Any]]: + def _query_overpass(self, query: str) -> list[dict[str, Any]]: """ Query Overpass and catch exceptions. @@ -252,7 +252,7 @@ def _query_overpass(self, query: str) -> List[Dict[str, Any]]: else: query_result = json.loads(query_file_path.read_text()) - elements: List[Dict[str, Any]] = query_result["elements"] + elements: list[dict[str, Any]] = query_result["elements"] return elements except (MultipleRequestsError, ServerLoadError): time.sleep(60) @@ -283,7 +283,7 @@ def _generate_query_for_single_geometry(self, g: BaseGeometry) -> str: ) return query - def _parse_overpass_element(self, element: Dict[str, Any]) -> Dict[str, Any]: + def _parse_overpass_element(self, element: dict[str, Any]) -> dict[str, Any]: """Parse single Overpass Element and return a region.""" element_tags = element.get("tags", {}) region_id = None diff --git a/srai/regionalizers/geocode.py b/srai/regionalizers/geocode.py index 903a7b43..b31f219e 100644 --- a/srai/regionalizers/geocode.py +++ b/srai/regionalizers/geocode.py @@ -1,5 +1,5 @@ """Utility function for geocoding a name to `regions_gdf`.""" -from typing import Any, Dict, List, Union +from typing import Any, Union import geopandas as gpd @@ -8,7 +8,7 @@ def geocode_to_region_gdf( - query: Union[str, List[str], Dict[str, Any]], by_osmid: bool = False + query: Union[str, list[str], dict[str, Any]], by_osmid: bool = False ) -> gpd.GeoDataFrame: """ Geocode a query to the `regions_gdf` unified format. diff --git a/srai/regionalizers/s2_regionalizer.py b/srai/regionalizers/s2_regionalizer.py index 5a4f751c..45099124 100644 --- a/srai/regionalizers/s2_regionalizer.py +++ b/srai/regionalizers/s2_regionalizer.py @@ -10,7 +10,7 @@ """ import json -from typing import Any, Dict +from typing import Any import geopandas as gpd from functional import seq @@ -95,7 +95,7 @@ def _fill_with_s2_cells(self, gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame: return cells_gdf - def _geojson_to_cells(self, geo_json: Dict[str, Any], res: int) -> Sequence: + def _geojson_to_cells(self, geo_json: dict[str, Any], res: int) -> Sequence: raw_cells = s2.polyfill(geo_json, res, with_id=True, geo_json_conformant=True) cells: Sequence = seq(raw_cells).map(lambda c: (c["id"], Polygon(c[GEOMETRY_COLUMN]))) diff --git a/srai/regionalizers/slippy_map_regionalizer.py b/srai/regionalizers/slippy_map_regionalizer.py index 78aa9841..282453fc 100644 --- a/srai/regionalizers/slippy_map_regionalizer.py +++ b/srai/regionalizers/slippy_map_regionalizer.py @@ -7,7 +7,7 @@ 1. https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames """ from itertools import product -from typing import Any, Dict, List, Tuple +from typing import Any import geopandas as gpd import numpy as np @@ -55,13 +55,15 @@ def transform(self, gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame: values = ( seq(gdf_exploded[GEOMETRY_COLUMN]) .map(self._to_cells) - .flat_map(lambda x: x) + .flatten() .map( - lambda item: { - **item, - REGIONS_INDEX: f"{item['x']}_{item['y']}_{self.zoom}", - "z": self.zoom, - } + lambda item: ( + item + | { + REGIONS_INDEX: f"{item['x']}_{item['y']}_{self.zoom}", + "z": self.zoom, + } + ) ) .to_list() ) @@ -71,7 +73,7 @@ def transform(self, gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame: ) return gdf.drop_duplicates() - def _to_cells(self, polygon: shpg.Polygon) -> List[Dict[str, Any]]: + def _to_cells(self, polygon: shpg.Polygon) -> list[dict[str, Any]]: gdf_bounds = polygon.bounds x_start, y_start = self._coordinates_to_x_y(gdf_bounds[1], gdf_bounds[0]) x_end, y_end = self._coordinates_to_x_y(gdf_bounds[3], gdf_bounds[2]) @@ -99,7 +101,7 @@ def _should_skip_tile(self, area: shpg.Polygon, tile: shpg.Polygon) -> bool: intersects: bool = tile.intersects(area) return not intersects - def _coordinates_to_x_y(self, latitude: float, longitude: float) -> Tuple[int, int]: + def _coordinates_to_x_y(self, latitude: float, longitude: float) -> tuple[int, int]: """ Convert latitude and longitude into x and y values using `self.zoom`. @@ -111,7 +113,7 @@ def _coordinates_to_x_y(self, latitude: float, longitude: float) -> Tuple[int, i y_tile = int((1 - np.arcsinh(np.tan(lat_radian)) / np.pi) / 2 * n_rows) return x_tile, y_tile - def _x_y_to_coordinates(self, x: int, y: int) -> Tuple[float, float]: + def _x_y_to_coordinates(self, x: int, y: int) -> tuple[float, float]: """ Convert x and y values into latitude and longitude using `self.zoom`. diff --git a/srai/regionalizers/voronoi_regionalizer.py b/srai/regionalizers/voronoi_regionalizer.py index 48c8e614..0070ee2a 100644 --- a/srai/regionalizers/voronoi_regionalizer.py +++ b/srai/regionalizers/voronoi_regionalizer.py @@ -4,7 +4,8 @@ This module contains voronoi regionalizer implementation. """ -from typing import Hashable, List, Optional, Union +from collections.abc import Hashable +from typing import Optional, Union import geopandas as gpd from shapely.geometry import Point, box @@ -30,7 +31,7 @@ class VoronoiRegionalizer(Regionalizer): def __init__( self, - seeds: Union[gpd.GeoDataFrame, List[Point]], + seeds: Union[gpd.GeoDataFrame, list[Point]], max_meters_between_points: int = 10_000, num_of_multiprocessing_workers: int = -1, multiprocessing_activation_threshold: Optional[int] = None, @@ -65,8 +66,8 @@ def __init__( dependency_group="voronoi", modules=["haversine", "pymap3d", "scipy", "spherical_geometry"], ) - self.region_ids: List[Hashable] = [] - self.seeds: List[Point] = [] + self.region_ids: list[Hashable] = [] + self.seeds: list[Point] = [] if isinstance(seeds, gpd.GeoDataFrame): self._parse_geodataframe_seeds(seeds_gdf=seeds) @@ -135,11 +136,11 @@ def _parse_geodataframe_seeds(self, seeds_gdf: gpd.GeoDataFrame) -> None: self.region_ids.append(index) self.seeds.append(candidate_point) - def _get_duplicated_seeds_ids(self) -> List[Hashable]: + def _get_duplicated_seeds_ids(self) -> list[Hashable]: """Return all seeds ids that overlap with another using quick sjoin operation.""" gdf = gpd.GeoDataFrame( data={GEOMETRY_COLUMN: self.seeds}, index=self.region_ids, crs=WGS84_CRS ) duplicated_seeds = gdf.sjoin(gdf).index.value_counts().loc[lambda x: x > 1] - duplicated_seeds_ids: List[Hashable] = duplicated_seeds.index.to_list() + duplicated_seeds_ids: list[Hashable] = duplicated_seeds.index.to_list() return duplicated_seeds_ids diff --git a/tox.ini b/tox.ini index 5a206f46..32963528 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,5 @@ [tox] envlist = - python3.8 python3.9 python3.10 python3.11