From b222a1beb81c4cd2edc9eef2c10e08839d4ad051 Mon Sep 17 00:00:00 2001 From: maleksandrov Date: Sat, 17 Sep 2022 18:09:28 +0200 Subject: [PATCH 01/24] Increased minimal version of sphinx_rtd_theme --- docs/environment.yml | 2 +- requirements-docs.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/environment.yml b/docs/environment.yml index afe579d51..6525543fa 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -5,7 +5,7 @@ dependencies: - rasterio - pip: - Sphinx~=4.0 - - sphinx-rtd-theme + - sphinx_rtd_theme>=1.0.0 - nbsphinx - jupyter - m2r2 diff --git a/requirements-docs.txt b/requirements-docs.txt index fb5cbb9b5..2de980e11 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,7 +1,7 @@ jupyter m2r2 nbsphinx -sphinx-rtd-theme +sphinx_rtd_theme>=1.0.0 sphinx jinja2==3.0.3 From caefa5124db1ac1be83484e5b36d4551b5a70832 Mon Sep 17 00:00:00 2001 From: maleksandrov Date: Sat, 17 Sep 2022 19:01:21 +0200 Subject: [PATCH 02/24] Implemented creation of custom github url links in sphinx docs build --- docs/source/conf.py | 92 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index dba0b1c2c..7a476aa3e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -16,6 +16,7 @@ import shutil import sys from collections import defaultdict +from typing import Any, Dict, Optional import sphinx.ext.autodoc @@ -63,11 +64,12 @@ "sphinx.ext.autosummary", "sphinx.ext.viewcode", "nbsphinx", + "sphinx_rtd_theme", "IPython.sphinxext.ipython_console_highlighting", "m2r2", ] -# Incude typehints in descriptions +# Include typehints in descriptions autodoc_typehints = "description" # Both the class’ and the __init__ method’s docstring are concatenated and inserted. @@ -309,7 +311,9 @@ def get_subclasses(cls): return list(set(direct_subclasses).union(nested_subclasses)) -with open("eotasks.rst", "w") as f: +EOTASKS_PATH = "eotasks" + +with open(f"{EOTASKS_PATH}.rst", "w") as f: f.write("EOTasks\n") f.write("=======\n") f.write("\n") @@ -349,6 +353,9 @@ def get_subclasses(cls): current_dir = os.path.abspath(os.path.dirname(__file__)) reference_dir = os.path.join(current_dir, "reference") custom_reference_dir = os.path.join(current_dir, "custom_reference") +custom_reference_files = {filename.rsplit(".", 1)[0] for filename in os.listdir(custom_reference_dir)} + +repository_dir = os.path.join(current_dir, "..", "..") modules = ["core", "coregistration", "features", "geometry", "io", "mask", "ml_tools", "visualization"] APIDOC_OPTIONS = ["--module-first", "--separate", "--no-toc", "--templatedir", os.path.join(current_dir, "_templates")] @@ -371,6 +378,87 @@ def run_apidoc(_): main(["-e", "-o", reference_dir, module_dir, *exclude, *APIDOC_OPTIONS]) +def configure_github_link(_app: Any, pagename: str, _templatename: Any, context: Dict[str, Any], _doctree: Any) -> None: + """Because some pages are auto-generated and some are copied from their original location the link "Edit on GitHub" + of a page is wrong. This function computes a custom link for such pages and saves it to a custom meta parameter + `github_url` which is then picked up by `sphinx_rtd_theme`. + + Resources to understand the implementation: + - https://www.sphinx-doc.org/en/master/extdev/appapi.html#event-html-page-context + - https://dev.readthedocs.io/en/latest/design/theme-context.html + - https://sphinx-rtd-theme.readthedocs.io/en/latest/configuring.html?highlight=github_url#file-wide-metadata + - https://github.com/readthedocs/sphinx_rtd_theme/blob/1.0.0/sphinx_rtd_theme/breadcrumbs.html#L35 + """ + # ReadTheDocs automatically sets the following parameters but for local testing we set them manually: + show_link = context.get("display_github") + context["display_github"] = True if show_link is None else show_link + context["github_user"] = context.get("github_user") or "sentinel-hub" + context["github_repo"] = context.get("github_repo") or "eo-learn" + context["github_version"] = context.get("github_version") or "develop" + context["conf_py_path"] = context.get("conf_py_path") or "/docs/source/" + + if pagename.startswith("examples/"): + github_url = create_github_url(context, conf_py_path="/") + + elif pagename.startswith("reference/"): + filename = pagename.split("/", 1)[1] + + if filename in custom_reference_files: + github_url = create_github_url(context, pagename=f"custom_reference/{filename}") + else: + subpackage = filename.split(".")[1] + filename = filename.replace(".", "/") + filename = f"{subpackage}/{filename}" + + full_path = os.path.join(repository_dir, f"{filename}.py") + is_module = os.path.exists(full_path) + + github_url = create_github_url( + context, + theme_vcs_pageview_mode="blob" if is_module else "tree", + conf_py_path="/", + pagename=filename, + page_source_suffix=".py" if is_module else "", + ) + + elif pagename == EOTASKS_PATH: + # This page is auto-generated in conf.py + github_url = create_github_url(context, pagename="conf", page_source_suffix=".py") + + else: + return + + context["meta"] = context.get("meta") or {} + context["meta"]["github_url"] = github_url + + +def create_github_url( + context: Dict[str, Any], + theme_vcs_pageview_mode: Optional[str] = None, + conf_py_path: Optional[str] = None, + pagename: Optional[str] = None, + page_source_suffix: Optional[str] = None, +) -> str: + """Creates a GitHub URL from context in exactly the same way as in + https://github.com/readthedocs/sphinx_rtd_theme/blob/1.0.0/sphinx_rtd_theme/breadcrumbs.html#L39 + + The function allows URL customization by overwriting certain parameters. + """ + github_host = context.get("github_host") or "github.com" + github_user = context.get("github_user", "") + github_repo = context.get("github_repo", "") + theme_vcs_pageview_mode = theme_vcs_pageview_mode or context.get("theme_vcs_pageview_mode") or "blob" + github_version = context.get("github_version", "") + conf_py_path = conf_py_path or context.get("conf_py_path", "") + pagename = pagename or context.get("pagename", "") + page_source_suffix = context.get("page_source_suffix", "") if page_source_suffix is None else page_source_suffix + return ( + f"https://{github_host}/{github_user}/{github_repo}/{theme_vcs_pageview_mode}/" + f"{github_version}{conf_py_path}{pagename}{page_source_suffix}" + ) + + def setup(app): app.connect("builder-inited", run_apidoc) + app.connect("html-page-context", configure_github_link) app.connect("autodoc-process-docstring", sphinx.ext.autodoc.between("Credits:", what=["module"], exclude=True)) From 806febb212cf0817671dce6fc09ed2decc27d8c1 Mon Sep 17 00:00:00 2001 From: jgersak <112631680+jgersak@users.noreply.github.com> Date: Tue, 20 Sep 2022 11:59:43 +0200 Subject: [PATCH 03/24] Add test for extract bands metod (#467) * Add test for extract bands metod Test checking deep copy of Map Feature Task * isclose replaced with approx added test for old value * rename variable --- core/eolearn/tests/test_core_tasks.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/eolearn/tests/test_core_tasks.py b/core/eolearn/tests/test_core_tasks.py index 8fb2f97b6..f8a9dec59 100644 --- a/core/eolearn/tests/test_core_tasks.py +++ b/core/eolearn/tests/test_core_tasks.py @@ -21,6 +21,7 @@ from fs.tempfs import TempFS from fs_s3fs import S3FS from numpy.testing import assert_equal +from pytest import approx from sentinelhub import CRS @@ -425,6 +426,11 @@ def test_extract_bands(test_eopatch): patch = move_bands(test_eopatch) assert np.array_equal(patch.data["MOVED_BANDS"], patch.data["REFERENCE_SCENES"][..., bands]) + old_value = patch.data["MOVED_BANDS"][0, 0, 0, 0] + patch.data["MOVED_BANDS"][0, 0, 0, 0] += 1.0 + assert patch.data["REFERENCE_SCENES"][0, 0, 0, bands[0]] == old_value + assert old_value + 1.0 == approx(patch.data["MOVED_BANDS"][0, 0, 0, 0]) + bands = [2, 4, 16] move_bands = ExtractBandsTask((FeatureType.DATA, "REFERENCE_SCENES"), (FeatureType.DATA, "MOVED_BANDS"), bands) with pytest.raises(ValueError): From b8aea41a6bf2dbc07cfdff0aa1f96619ce6f1ecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Tue, 27 Sep 2022 12:07:06 +0200 Subject: [PATCH 04/24] Feat/types (#469) * add core.utils.types module * add types to parser * Improve loop structure to avoid ignoring types too soon * switch default of `allowed_features` without breaking existing code * fix Literal import for 3.7 * Improve type structure and code flow * correct type of `_parse_sequence` --- core/eolearn/core/eotask.py | 7 +- core/eolearn/core/utils/parsing.py | 158 ++++++++++++++++------------- core/eolearn/core/utils/types.py | 20 ++++ 3 files changed, 115 insertions(+), 70 deletions(-) create mode 100644 core/eolearn/core/utils/types.py diff --git a/core/eolearn/core/eotask.py b/core/eolearn/core/eotask.py index afbea3f66..390c875b3 100644 --- a/core/eolearn/core/eotask.py +++ b/core/eolearn/core/eotask.py @@ -18,10 +18,11 @@ import logging from abc import ABCMeta, abstractmethod from dataclasses import dataclass -from typing import Dict, Iterable, Optional +from typing import Dict, Iterable, Union from .constants import FeatureType from .utils.parsing import FeatureParser, parse_feature, parse_features, parse_renamed_feature, parse_renamed_features +from .utils.types import EllipsisType LOGGER = logging.getLogger(__name__) @@ -67,7 +68,9 @@ def execute(self, *eopatches, **kwargs): """Override to specify action performed by task.""" @staticmethod - def get_feature_parser(features, allowed_feature_types: Optional[Iterable[FeatureType]] = None) -> FeatureParser: + def get_feature_parser( + features, allowed_feature_types: Union[Iterable[FeatureType], EllipsisType] = ... + ) -> FeatureParser: """See :class:`FeatureParser`.""" return FeatureParser(features, allowed_feature_types=allowed_feature_types) diff --git a/core/eolearn/core/utils/parsing.py b/core/eolearn/core/utils/parsing.py index 03cca2fca..2e4263677 100644 --- a/core/eolearn/core/utils/parsing.py +++ b/core/eolearn/core/utils/parsing.py @@ -13,15 +13,39 @@ """ from __future__ import annotations +import sys from itertools import repeat -from typing import TYPE_CHECKING, Iterable, List, Optional, Sequence, Tuple, Union, cast +from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Tuple, Union, cast from ..constants import FeatureType +from .types import EllipsisType + +if sys.version_info < (3, 8): + from typing_extensions import Literal +else: + from typing import Literal # pylint: disable=ungrouped-imports if TYPE_CHECKING: from ..eodata import EOPatch +FeatureSpec = Union[Tuple[Literal[FeatureType.BBOX, FeatureType.TIMESTAMP], None], Tuple[FeatureType, str]] +FeatureRenameSpec = Union[ + Tuple[Literal[FeatureType.BBOX, FeatureType.TIMESTAMP], None, None], Tuple[FeatureType, str, str] +] +SingleFeatureSpec = Union[FeatureSpec, FeatureRenameSpec] + +SequenceFeatureSpec = Sequence[Union[SingleFeatureSpec, FeatureType, Tuple[FeatureType, Optional[EllipsisType]]]] +DictFeatureSpec = Dict[FeatureType, Union[None, EllipsisType, Iterable[Union[str, Tuple[str, str]]]]] +MultiFeatureSpec = Union[ + EllipsisType, FeatureType, Tuple[FeatureType, EllipsisType], SequenceFeatureSpec, DictFeatureSpec +] + +FeaturesSpecification = Union[SingleFeatureSpec, MultiFeatureSpec] + +_ParserFeaturesSpec = Union[Tuple[FeatureType, None, None], Tuple[FeatureType, str, str]] + + class FeatureParser: """Class for parsing a variety of feature specifications into a streamlined format. @@ -63,7 +87,7 @@ class FeatureParser: FeatureType.BBOX: None } - 4. Sequences of elements, each describing a feature. For elements describing a feature type it is understood as + 4. Sequences of elements, each describing a feature. When describing all features of a given feature type use `(feature_type, ...)`. For specific features one can use `(feature_type, feature_name)` or even `(feature_type, old_name, new_name)` for renaming. @@ -81,25 +105,33 @@ class FeatureParser: - For `get_renamed_features` a list of triples `(feature_type, old_name, new_name)`. """ - def __init__(self, features: Union[dict, Sequence], allowed_feature_types: Optional[Iterable[FeatureType]] = None): + def __init__( + self, + features: FeaturesSpecification, + allowed_feature_types: Union[Iterable[FeatureType], EllipsisType] = ..., + ): """ :param features: A collection of features in one of the supported formats :param allowed_feature_types: Makes sure that only features of these feature types will be returned, otherwise an error is raised :raises: ValueError """ - self.allowed_feature_types = set(FeatureType) if allowed_feature_types is None else set(allowed_feature_types) + self.allowed_feature_types = ( + set(allowed_feature_types) if isinstance(allowed_feature_types, Iterable) else set(FeatureType) + ) self._feature_specs = self._parse_features(features) - def _parse_features( - self, features: Union[dict, Sequence] - ) -> List[Union[Tuple[FeatureType, str, str], Tuple[FeatureType, None, None]]]: + def _parse_features(self, features: FeaturesSpecification) -> List[_ParserFeaturesSpec]: """This method parses and validates input, returning a list of `(ftype, old_name, new_name)` triples. Due to typing issues the all-features requests are transformed from `(ftype, ...)` to `(ftype, None, None)`. This is a correct schema for BBOX and TIMESTAMP while for other features this is corrected when outputting, either by processing the request or by substituting ellipses back (case of `get_feature_specifications`). """ + + if isinstance(features, FeatureType): + return [(features, None, None)] + if isinstance(features, dict): return self._parse_dict(features) @@ -109,17 +141,17 @@ def _parse_features( if features is ...: return list(zip(self.allowed_feature_types, repeat(None), repeat(None))) - if features is FeatureType.BBOX or features is FeatureType.TIMESTAMP: - return [(features, None, None)] - raise ValueError( f"Unable to parse features {features}. Please see specifications of FeatureParser on viable inputs." ) - def _parse_dict(self, features: dict) -> List[Union[Tuple[FeatureType, str, str], Tuple[FeatureType, None, None]]]: + def _parse_dict( + self, + features: DictFeatureSpec, + ) -> List[_ParserFeaturesSpec]: """Implements parsing and validation in case the input is a dictionary.""" - feature_specs: List[Union[Tuple[FeatureType, str, str], Tuple[FeatureType, None, None]]] = [] + feature_specs: List[_ParserFeaturesSpec] = [] for feature_type, feature_names in features.items(): feature_type = self._parse_feature_type(feature_type, message_about_position="keys of the dictionary") @@ -139,11 +171,12 @@ def _parse_dict(self, features: dict) -> List[Union[Tuple[FeatureType, str, str] return feature_specs def _parse_sequence( - self, features: Sequence - ) -> List[Union[Tuple[FeatureType, str, str], Tuple[FeatureType, None, None]]]: - """Implements parsing and validation in case the input is a sequence.""" + self, + features: Union[SingleFeatureSpec, SequenceFeatureSpec], + ) -> List[_ParserFeaturesSpec]: + """Implements parsing and validation in case the input is a tuple describing a single feature or a sequence.""" - feature_specs: List[Union[Tuple[FeatureType, str, str], Tuple[FeatureType, None, None]]] = [] + feature_specs: List[_ParserFeaturesSpec] = [] # Check for possible singleton if 2 <= len(features) <= 3: @@ -168,9 +201,7 @@ def _parse_sequence( return feature_specs - def _parse_singleton( - self, feature: Sequence - ) -> Union[Tuple[FeatureType, str, str], Tuple[FeatureType, None, None]]: + def _parse_singleton(self, feature: Sequence) -> FeatureRenameSpec: """Parses a pair or triple specifying a single feature or a get-all request.""" feature_type, *feature_name = feature feature_type = self._parse_feature_type(feature_type, message_about_position="first elements of tuples") @@ -231,33 +262,14 @@ def _fail_for_noname_features(feature_type: FeatureType, specification: object) f" {specification} instead." ) - @staticmethod - def _validate_parsing_request(feature_type: FeatureType, name: Optional[str], eopatch: Optional[EOPatch]) -> None: - """Checks if the parsing request is viable with current arguments. - - This means checking that `eopatch` is provided if the request is an all-features request and in the case - where an EOPatch is provided, that the feature exists in the EOPatch. - """ - if not feature_type.has_dict(): - return - - if name is None and eopatch is None: - raise ValueError( - f"Input specifies that for feature type {feature_type} all existing features are parsed, but the " - "`eopatch` parameter was not provided." - ) - - if eopatch is not None and name is not None and (feature_type, name) not in eopatch: - raise ValueError(f"Requested feature {(feature_type, name)} not part of eopatch.") - - def get_feature_specifications(self) -> List[Tuple[FeatureType, object]]: + def get_feature_specifications(self) -> List[Tuple[FeatureType, Union[str, EllipsisType]]]: """Returns the feature specifications in a more streamlined fashion. Requests for all features, e.g. `(FeatureType.DATA, ...)`, are returned directly. """ return [(ftype, ... if fname is None else fname) for ftype, fname, _ in self._feature_specs] - def get_features(self, eopatch: Optional[EOPatch] = None) -> List[Tuple[FeatureType, Optional[str]]]: + def get_features(self, eopatch: Optional[EOPatch] = None) -> List[FeatureSpec]: """Returns a list of `(feature_type, feature_name)` pairs. For features that specify renaming, the new name of the feature is ignored. @@ -267,19 +279,10 @@ def get_features(self, eopatch: Optional[EOPatch] = None) -> List[Tuple[FeatureT If `eopatch` is not provided the method fails if an all-feature request is in the specification. """ - feature_names = [] - for feature_type, name, _ in self._feature_specs: - self._validate_parsing_request(feature_type, name, eopatch) - if name is None and feature_type.has_dict(): - feature_names.extend(list(zip(repeat(feature_type), eopatch[feature_type]))) # type: ignore - else: - feature_names.append((feature_type, name)) - return feature_names + renamed_features = self.get_renamed_features(eopatch) + return [feature[:2] for feature in renamed_features] # pattern unpacking messes with typechecking - def get_renamed_features( - self, - eopatch: Optional[EOPatch] = None, - ) -> List[Union[Tuple[FeatureType, str, str], Tuple[FeatureType, None, None]]]: + def get_renamed_features(self, eopatch: Optional[EOPatch] = None) -> List[FeatureRenameSpec]: """Returns a list of `(feature_type, old_name, new_name)` triples. For features without a specified renaming the new name is equal to the old one. @@ -290,20 +293,33 @@ def get_renamed_features( If `eopatch` is not provided the method fails if an all-feature request is in the specification. """ - feature_names = [] - for feature_type, old_name, new_name in self._feature_specs: - self._validate_parsing_request(feature_type, old_name, eopatch) - if old_name is None and feature_type.has_dict(): - feature_names.extend( - list(zip(repeat(feature_type), eopatch[feature_type], eopatch[feature_type])) # type: ignore - ) + parsed_features: List[FeatureRenameSpec] = [] + for feature_spec in self._feature_specs: + ftype, old_name, new_name = feature_spec + + if ftype is FeatureType.BBOX or ftype is FeatureType.TIMESTAMP: + parsed_features.append((ftype, None, None)) + + elif old_name is not None and new_name is not None: + # checking both is redundant, but typechecker has difficulties otherwise + if eopatch is not None and (ftype, old_name) not in eopatch: + raise ValueError(f"Requested feature {(ftype, old_name)} not part of eopatch.") + parsed_features.append((ftype, old_name, new_name)) + + elif eopatch is not None: + parsed_features.extend((ftype, name, name) for name in eopatch[ftype]) else: - feature_names.append((feature_type, old_name, new_name)) - return feature_names + raise ValueError( + f"Input of feature parser specifies that for feature type {ftype} all existing features are parsed," + " but the `eopatch` parameter was not provided." + ) + return parsed_features def parse_feature( - feature, eopatch: Optional[EOPatch] = None, allowed_feature_types: Optional[Iterable[FeatureType]] = None + feature: SingleFeatureSpec, + eopatch: Optional[EOPatch] = None, + allowed_feature_types: Union[Iterable[FeatureType], EllipsisType] = ..., ) -> Tuple[FeatureType, Optional[str]]: """Parses input describing a single feature into a `(feature_type, feature_name)` pair. @@ -317,8 +333,10 @@ def parse_feature( def parse_renamed_feature( - feature, eopatch: Optional[EOPatch] = None, allowed_feature_types: Optional[Iterable[FeatureType]] = None -) -> Union[Tuple[FeatureType, str, str], Tuple[FeatureType, None, None]]: + feature: SingleFeatureSpec, + eopatch: Optional[EOPatch] = None, + allowed_feature_types: Union[Iterable[FeatureType], EllipsisType] = ..., +) -> FeatureRenameSpec: """Parses input describing a single feature into a `(feature_type, old_name, new_name)` triple. See :class:`FeatureParser` for viable inputs. @@ -331,8 +349,10 @@ def parse_renamed_feature( def parse_features( - features, eopatch: Optional[EOPatch] = None, allowed_feature_types: Optional[Iterable[FeatureType]] = None -) -> List[Tuple[FeatureType, Optional[str]]]: + features: FeaturesSpecification, + eopatch: Optional[EOPatch] = None, + allowed_feature_types: Union[Iterable[FeatureType], EllipsisType] = ..., +) -> List[FeatureSpec]: """Parses input describing features into a list of `(feature_type, feature_name)` pairs. See :class:`FeatureParser` for viable inputs. @@ -341,8 +361,10 @@ def parse_features( def parse_renamed_features( - features, eopatch: Optional[EOPatch] = None, allowed_feature_types: Optional[Iterable[FeatureType]] = None -) -> List[Union[Tuple[FeatureType, str, str], Tuple[FeatureType, None, None]]]: + features: FeaturesSpecification, + eopatch: Optional[EOPatch] = None, + allowed_feature_types: Union[Iterable[FeatureType], EllipsisType] = ..., +) -> List[FeatureRenameSpec]: """Parses input describing features into a list of `(feature_type, old_name, new_name)` triples. See :class:`FeatureParser` for viable inputs. diff --git a/core/eolearn/core/utils/types.py b/core/eolearn/core/utils/types.py new file mode 100644 index 000000000..041eb73c9 --- /dev/null +++ b/core/eolearn/core/utils/types.py @@ -0,0 +1,20 @@ +""" +Types and type aliases used throughout the code. + +Credits: +Copyright (c) 2022 Žiga Lukšič (Sinergise) + +This source code is licensed under the MIT license found in the LICENSE +file in the root directory of this source tree. +""" +# pylint: disable=unused-import +import sys + +if sys.version_info >= (3, 10): + from types import EllipsisType # pylint: disable=ungrouped-imports +else: + import builtins # noqa: F401 + + from typing_extensions import TypeAlias + + EllipsisType: TypeAlias = "builtins.ellipsis" From d93fac1f6fcd97568bef5e4692b7f58c8d5d8331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Tue, 27 Sep 2022 13:48:52 +0200 Subject: [PATCH 05/24] remove the support for loading pickled files from old eolearn versions (#470) --- core/eolearn/core/eodata_io.py | 50 ++++++---------------------------- 1 file changed, 8 insertions(+), 42 deletions(-) diff --git a/core/eolearn/core/eodata_io.py b/core/eolearn/core/eodata_io.py index 66b5aa54d..378695d59 100644 --- a/core/eolearn/core/eodata_io.py +++ b/core/eolearn/core/eodata_io.py @@ -13,12 +13,10 @@ import datetime import gzip import json -import pickle import warnings from abc import ABCMeta, abstractmethod from collections import defaultdict -from functools import wraps -from typing import Any, BinaryIO, Callable, Generic, Iterable, Optional, TypeVar, Union +from typing import Any, BinaryIO, Generic, Iterable, Optional, TypeVar, Union import fs import fs.move @@ -28,14 +26,12 @@ from fs.base import FS from fs.osfs import OSFS from fs.tempfs import TempFS -from geopandas import GeoDataFrame, GeoSeries from sentinelhub import CRS, BBox, Geometry, MimeType from sentinelhub.exceptions import SHUserWarning from sentinelhub.os_utils import sys_is_windows from .constants import FeatureType, FeatureTypeSet, OverwritePermission -from .exceptions import EODeprecationWarning from .utils.parsing import FeatureParser from .utils.vector_io import infer_schema @@ -269,16 +265,15 @@ def load(self) -> _T: with self.filesystem.openbin(self.path, "r") as file_handle: if MimeType.GZIP.matches_extension(self.path): - path = fs.path.splitext(self.path)[0] with gzip.open(file_handle, "rb") as gzip_fp: - self.loaded_value = self._read_from_file(gzip_fp, path) + self.loaded_value = self._read_from_file(gzip_fp) else: - self.loaded_value = self._read_from_file(file_handle, self.path) + self.loaded_value = self._read_from_file(file_handle) return self.loaded_value @abstractmethod - def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile], path: str) -> _T: + def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> _T: """Loads from a file and decodes content.""" def save(self, data: _T, compress_level: int = 0) -> None: @@ -315,38 +310,12 @@ def _write_to_file(self, data: _T, file: Union[BinaryIO, gzip.GzipFile]) -> None """Writes data to a file in the appropriate way.""" -def _try_unpickling(read_method: Callable[[Any, Union[BinaryIO, gzip.GzipFile], str], _T]): - @wraps(read_method) - def unpickle_or_read_from_file(self, file: Union[BinaryIO, gzip.GzipFile], path: str) -> _T: - file_format = MimeType(fs.path.splitext(path)[1].strip(".")) - if file_format is MimeType.PICKLE: - warnings.warn( - f"File {self.path} is in pickle format which is deprecated since eo-learn version 1.0. Please re-save " - "this EOPatch with the new eo-learn version to update the format. In newer versions this backward " - "compatibility will be removed.", - EODeprecationWarning, - ) - - data = pickle.load(file) - - # There seems to be an issue in geopandas==0.8.1 where unpickling GeoDataFrames, which were saved with an - # old geopandas version, loads geometry column into a pandas.Series instead geopandas.GeoSeries. Because - # of that it is missing a crs attribute which is only attached to the entire GeoDataFrame - if isinstance(data, GeoDataFrame) and not isinstance(data.geometry, GeoSeries): - data = data.set_geometry("geometry") - - return data - return read_method(self, file, path) - - return unpickle_or_read_from_file - - class FeatureIONumpy(FeatureIO[np.ndarray]): """FeatureIO object specialized for Numpy arrays.""" file_format = MimeType.NPY - def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile], _: str) -> np.ndarray: + def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> np.ndarray: return np.load(file) def _write_to_file(self, data: np.ndarray, file: Union[BinaryIO, gzip.GzipFile]) -> None: @@ -358,8 +327,7 @@ class FeatureIOGeoDf(FeatureIO[gpd.GeoDataFrame]): file_format = MimeType.GPKG - @_try_unpickling - def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile], path: str) -> gpd.GeoDataFrame: + def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> gpd.GeoDataFrame: dataframe = gpd.read_file(file) if dataframe.crs is not None: @@ -399,8 +367,7 @@ class FeatureIOJson(FeatureIO[_T]): file_format = MimeType.JSON - @_try_unpickling - def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile], _: str) -> _T: + def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> _T: return json.load(file) def _write_to_file(self, data: _T, file: Union[BinaryIO, gzip.GzipFile]) -> None: @@ -420,8 +387,7 @@ class FeatureIOBBox(FeatureIO[BBox]): file_format = MimeType.GEOJSON - @_try_unpickling - def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile], _: str) -> BBox: + def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> BBox: json_data = json.load(file) return Geometry.from_geojson(json_data).bbox From b91fd086d1d4cd71820daf014eb47b1bccaffd4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Wed, 28 Sep 2022 15:19:11 +0200 Subject: [PATCH 06/24] Avoid inconsistency in paths of FeatureIO (#473) --- core/eolearn/core/eodata.py | 2 +- core/eolearn/core/eodata_io.py | 89 ++++++++++++++++++++++------------ 2 files changed, 59 insertions(+), 32 deletions(-) diff --git a/core/eolearn/core/eodata.py b/core/eolearn/core/eodata.py index 969de130b..2ae4af442 100644 --- a/core/eolearn/core/eodata.py +++ b/core/eolearn/core/eodata.py @@ -210,7 +210,7 @@ def _parse_feature_value(self, value: object, _: str) -> Any: def _create_feature_dict(feature_type: FeatureType, value: Dict[str, Any]) -> _FeatureDict: - """Creates the correct FeatureIO, corresponding to the FeatureType.""" + """Creates the correct FeatureDict, corresponding to the FeatureType.""" if feature_type.is_vector(): return _FeatureDictGeoDf(value, feature_type) if feature_type is FeatureType.META_INFO: diff --git a/core/eolearn/core/eodata_io.py b/core/eolearn/core/eodata_io.py index 378695d59..2183605cd 100644 --- a/core/eolearn/core/eodata_io.py +++ b/core/eolearn/core/eodata_io.py @@ -16,7 +16,7 @@ import warnings from abc import ABCMeta, abstractmethod from collections import defaultdict -from typing import Any, BinaryIO, Generic, Iterable, Optional, TypeVar, Union +from typing import Any, BinaryIO, Generic, Iterable, Optional, Type, TypeVar, Union import fs import fs.move @@ -36,6 +36,7 @@ from .utils.vector_io import infer_schema _T = TypeVar("_T") +Self = TypeVar("Self", bound="FeatureIO") def save_eopatch( @@ -67,10 +68,12 @@ def save_eopatch( features_to_save = [] for ftype, fname, path in eopatch_features: - feature_io = _create_feature_io(ftype, path, filesystem) + # the paths here do not have file extensions, so FeatureIO needs to be constructed differently + # pylint: disable=protected-access + feature_io = _get_feature_io_constructor(ftype)._from_eopatch_path(path, filesystem, compress_level) data = eopatch[(ftype, fname)] - features_to_save.append((feature_io, data, compress_level)) + features_to_save.append((feature_io, data)) # Cannot be done before due to lazy loading (this would delete the files before the data is loaded) if overwrite_permission is OverwritePermission.OVERWRITE_PATCH and patch_exists: @@ -108,7 +111,7 @@ def remove_redundant_files(filesystem, eopatch_features, filesystem_features, cu def load_eopatch(eopatch, filesystem, patch_location, features=..., lazy_loading=False): """A utility function used by `EOPatch.load` method.""" features = list(walk_filesystem(filesystem, patch_location, features)) - loading_data: Iterable[Any] = [_create_feature_io(ftype, path, filesystem) for ftype, _, path in features] + loading_data: Iterable[Any] = [_get_feature_io_constructor(ftype)(path, filesystem) for ftype, _, path in features] if not lazy_loading: with concurrent.futures.ThreadPoolExecutor() as executor: @@ -238,19 +241,38 @@ def _to_lowercase(ftype, fname, *_): class FeatureIO(Generic[_T], metaclass=ABCMeta): """A class that handles the saving and loading process of a single feature at a given location.""" - def __init__(self, path: str, filesystem: FS): + def __init__(self, path: str, filesystem: FS, compress_level: Optional[int] = None): """ :param path: A path in the filesystem :param filesystem: A filesystem object + :compress_level: The compression level to be used when saving, inferred from path if not provided """ + filename = fs.path.basename(path) + expected_extension = f".{self.get_file_format().extension}" + if compress_level is None: + # Infer compression level if not provided + compress_level = 1 if path.endswith(f".{MimeType.GZIP.extension}") else 0 + if compress_level: + expected_extension += f".{MimeType.GZIP.extension}" + if not filename.endswith(expected_extension): + raise ValueError(f"FeatureIO expects a filepath with the {expected_extension} file extension, got {path}") + self.path = path self.filesystem = filesystem + self.compress_level = compress_level self.loaded_value: Optional[_T] = None - @property + @classmethod + def _from_eopatch_path(cls: Type[Self], eopatch_path: str, filesystem: FS, compress_level: int = 0) -> Self: + """Constructor for creating FeatureIO objects directly from paths returned by `walk_eopatch`.""" + gz_extension = ("." + MimeType.GZIP.extension) if compress_level else "" + path = f"{eopatch_path}.{cls.get_file_format().extension}{gz_extension}" + return cls(path, filesystem, compress_level) + + @classmethod @abstractmethod - def file_format(self) -> MimeType: + def get_file_format(cls) -> MimeType: """The type of files handled by the FeatureIO.""" def __repr__(self) -> str: @@ -276,34 +298,31 @@ def load(self) -> _T: def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> _T: """Loads from a file and decodes content.""" - def save(self, data: _T, compress_level: int = 0) -> None: + def save(self, data: _T) -> None: """Method for saving a feature. To minimize the chance of corrupted files (in case of OSFS and TempFS) the file is first written and then moved to correct location. If any exceptions happen during the writing process the file is not moved (and old one not overwritten). """ - gz_extension = ("." + MimeType.GZIP.extension) if compress_level else "" - path = f"{self.path}.{self.file_format.extension}{gz_extension}" if isinstance(self.filesystem, (OSFS, TempFS)): with TempFS(temp_dir=self.filesystem.root_path) as tempfs: - self._save(tempfs, data, "tmp_feature", compress_level) - if fs.__version__ == "2.4.16" and self.filesystem.exists(path): # An issue in the fs version - self.filesystem.remove(path) - fs.move.move_file(tempfs, "tmp_feature", self.filesystem, path) + self._save(tempfs, data, "tmp_feature") + if fs.__version__ == "2.4.16" and self.filesystem.exists(self.path): # An issue in the fs version + self.filesystem.remove(self.path) + fs.move.move_file(tempfs, "tmp_feature", self.filesystem, self.path) return - self._save(self.filesystem, data, path, compress_level) + self._save(self.filesystem, data, self.path) - def _save(self, filesystem: FS, data: _T, path: str, compress_level: int = 0) -> None: + def _save(self, filesystem: FS, data: _T, path: str) -> None: """Given a filesystem it saves and compresses the data.""" - with filesystem.openbin(path, "w") as file_handle: - if compress_level == 0: - self._write_to_file(data, file_handle) - + with filesystem.openbin(path, "w") as file: + if self.compress_level == 0: + self._write_to_file(data, file) else: - with gzip.GzipFile(fileobj=file_handle, compresslevel=compress_level, mode="wb") as gzip_file_handle: - self._write_to_file(data, gzip_file_handle) + with gzip.GzipFile(fileobj=file, compresslevel=self.compress_level, mode="wb") as gzip_file: + self._write_to_file(data, gzip_file) @abstractmethod def _write_to_file(self, data: _T, file: Union[BinaryIO, gzip.GzipFile]) -> None: @@ -313,7 +332,9 @@ def _write_to_file(self, data: _T, file: Union[BinaryIO, gzip.GzipFile]) -> None class FeatureIONumpy(FeatureIO[np.ndarray]): """FeatureIO object specialized for Numpy arrays.""" - file_format = MimeType.NPY + @classmethod + def get_file_format(cls) -> MimeType: + return MimeType.NPY def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> np.ndarray: return np.load(file) @@ -325,7 +346,9 @@ def _write_to_file(self, data: np.ndarray, file: Union[BinaryIO, gzip.GzipFile]) class FeatureIOGeoDf(FeatureIO[gpd.GeoDataFrame]): """FeatureIO object specialized for GeoDataFrames.""" - file_format = MimeType.GPKG + @classmethod + def get_file_format(cls) -> MimeType: + return MimeType.GPKG def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> gpd.GeoDataFrame: dataframe = gpd.read_file(file) @@ -365,7 +388,9 @@ def _write_to_file(self, data: gpd.GeoDataFrame, file: Union[BinaryIO, gzip.Gzip class FeatureIOJson(FeatureIO[_T]): """FeatureIO object specialized for JSON-like objects.""" - file_format = MimeType.JSON + @classmethod + def get_file_format(cls) -> MimeType: + return MimeType.JSON def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> _T: return json.load(file) @@ -385,7 +410,9 @@ def _write_to_file(self, data: _T, file: Union[BinaryIO, gzip.GzipFile]) -> None class FeatureIOBBox(FeatureIO[BBox]): """FeatureIO object specialized for BBox objects.""" - file_format = MimeType.GEOJSON + @classmethod + def get_file_format(cls) -> MimeType: + return MimeType.GEOJSON def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> BBox: json_data = json.load(file) @@ -403,12 +430,12 @@ def _jsonify_timestamp(param: object) -> str: raise TypeError(f"Object of type {type(param)} is not yet supported in jsonify utility function") -def _create_feature_io(ftype: FeatureType, path: str, filesystem: FS) -> FeatureIO: +def _get_feature_io_constructor(ftype: FeatureType) -> Type[FeatureIO]: """Creates the correct FeatureIO, corresponding to the FeatureType.""" if ftype is FeatureType.BBOX: - return FeatureIOBBox(path, filesystem) + return FeatureIOBBox if ftype in (FeatureType.TIMESTAMP, FeatureType.META_INFO): - return FeatureIOJson(path, filesystem) + return FeatureIOJson if ftype in FeatureTypeSet.VECTOR_TYPES: - return FeatureIOGeoDf(path, filesystem) - return FeatureIONumpy(path, filesystem) + return FeatureIOGeoDf + return FeatureIONumpy From 4b2f18340e3b179cfb3805488e494d02ded30b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Thu, 29 Sep 2022 07:59:51 +0200 Subject: [PATCH 07/24] Add pre-commit hooks (#471) * adjust configuration of pre-commit * run formatters over everything * fix setup.py files * add rest of suggested changes * remove redundant line --- .flake8 | 3 +- .github/workflows/ci_action.yml | 39 +++++------- .pre-commit-config.yaml | 53 ++++++++++++++++ CHANGELOG.md | 10 +-- CONTRIBUTING.md | 2 +- README.md | 14 ++--- core/eolearn/core/eodata.py | 15 ++--- core/eolearn/core/eodata_io.py | 10 ++- core/eolearn/core/utils/parsing.py | 5 +- core/eolearn/core/utils/vector_io.py | 2 +- core/eolearn/tests/test_eodata_io.py | 5 +- core/eolearn/tests/test_eonode.py | 2 +- core/eolearn/tests/test_graph.py | 4 +- core/setup.py | 15 ++--- .../eolearn/coregistration/coregistration.py | 4 +- coregistration/setup.py | 15 ++--- docs/source/_templates/module.rst_t | 1 - docs/source/conf.py | 9 +-- example_data/TestEOPatch/bbox.geojson | 2 +- example_data/TestEOPatch/meta_info.json | 2 +- example_data/TestEOPatch/timestamp.json | 2 +- examples/README.md | 1 - examples/land-cover-map/README.md | 37 ++++++----- .../theewaterskloof_dam_nominal.wkt | 2 +- .../eolearn/features/feature_manipulation.py | 2 +- .../features/radiometric_normalization.py | 4 +- features/eolearn/tests/test_features_utils.py | 2 +- features/setup.py | 15 ++--- geometry/setup.py | 15 ++--- io/eolearn/io/extra/meteoblue.py | 4 +- io/eolearn/io/geometry_io.py | 17 +++-- io/eolearn/io/raster_io.py | 62 +++++++++---------- io/eolearn/io/sentinelhub_process.py | 4 +- io/requirements.txt | 2 +- io/setup.py | 15 ++--- mask/setup.py | 15 ++--- ml_tools/eolearn/ml_tools/sampling.py | 2 +- ml_tools/setup.py | 15 ++--- pyproject.toml | 3 + requirements-dev.txt | 6 +- requirements-docs.txt | 12 ++-- setup.py | 5 +- visualization/setup.py | 15 ++--- 43 files changed, 252 insertions(+), 217 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.flake8 b/.flake8 index 3b658fdaa..7a5fad5eb 100644 --- a/.flake8 +++ b/.flake8 @@ -1,8 +1,9 @@ [flake8] -ignore = E203, W503 +ignore = E203, W503, C408 exclude = .git, __pycache__, build, dist max-line-length= 120 max-complexity = 15 +min_python_version = 3.7.0 per-file-ignores = # imported but unused __init__.py: F401 diff --git a/.github/workflows/ci_action.yml b/.github/workflows/ci_action.yml index 4aa6383dc..f2e19f310 100644 --- a/.github/workflows/ci_action.yml +++ b/.github/workflows/ci_action.yml @@ -4,60 +4,49 @@ on: pull_request: push: branches: - - 'master' - - 'develop' + - "master" + - "develop" schedule: - - cron: '0 0 * * *' + - cron: "0 0 * * *" env: # The only way to simulate if-else statement CHECKOUT_BRANCH: ${{ github.event_name == 'schedule' && 'develop' || github.ref }} - jobs: - - check-code-black-isort-flake8: + check-pre-commit-hooks: runs-on: ubuntu-latest steps: - name: Checkout branch uses: actions/checkout@v2 with: ref: ${{ env.CHECKOUT_BRANCH }} - + - name: Setup Python uses: actions/setup-python@v2 with: python-version: "3.8" architecture: x64 - - name: Prepare linters - run: pip install black[jupyter] isort flake8 nbqa - - - name: Check code compliance with black - run: black . --check --diff - - - name: Check code compliance with isort + - name: Prepare pre-commit validators run: | - isort . --check --diff - nbqa isort . --nbqa-diff + pip install pre-commit - - name: Check code compliance with flake8 - run: | - flake8 . - nbqa flake8 . --nbqa-exclude=examples/core/CoreOverview.ipynb + - name: Check code compliance with pre-commit validators + run: pre-commit run --all-files test-on-github: runs-on: ubuntu-latest strategy: matrix: python-version: - - '3.7' - - '3.9' - - '3.10' - include: + - "3.7" + - "3.9" + - "3.10" + include: # A flag marks whether full or partial tests should be run # We don't run integration tests on pull requests from outside repos, because they don't have secrets - - python-version: '3.8' + - python-version: "3.8" full_test_suite: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} steps: - name: Checkout branch diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..1ac5c7516 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,53 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: trailing-whitespace + - id: debug-statements + - id: check-json + - id: check-toml + - id: check-yaml + - id: check-merge-conflict + - id: debug-statements + + - repo: https://github.com/psf/black + rev: 22.8.0 + hooks: + - id: black + language_version: python3 + + - repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + name: isort (python) + + - repo: https://github.com/PyCQA/autoflake + rev: v1.5.3 + hooks: + - id: autoflake + args: + [ + --remove-all-unused-imports, + --in-place, + --ignore-init-module-imports, + ] + + - repo: https://github.com/pycqa/flake8 + rev: 5.0.4 + hooks: + - id: flake8 + additional_dependencies: + - flake8-bugbear + - flake8-comprehensions + - flake8-simplify + - flake8-typing-imports + + - repo: https://github.com/nbQA-dev/nbQA + rev: 1.4.0 + hooks: + - id: nbqa-black + - id: nbqa-isort + - id: nbqa-flake8 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e847f0a8..f3e7603e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## [Version 1.2.1] - 2022-09-12 - Corrected the default for `no_data_value` in `ImportFromTiffTask` and `ExportToTiffTask` to `None`. The previous default of `0` was a poor choice in many scenarios. The switch might alter behavior in existing code. -- Changed the way `SpatialResizeTask` accepts parameters for the final image size. Now supports resizing by using resolution. +- Changed the way `SpatialResizeTask` accepts parameters for the final image size. Now supports resizing by using resolution. - Added `ExplodeBandsTask` that explodes a multi-band feature into multiple features. - Exposed resampling parameters in Sentinel Hub tasks and included a `geometry` execution parameter. - Reworked internal classes `FeatureIO` and `_FeatureDict` to improve types and maintainability. @@ -38,9 +38,9 @@ - Large improvements of parallelization in EOExecutor. Introduced the `eolearn.core.utils.parallelize` module, featuring tools for different parallelization modes. - Added support for session sharing in `SentinelHubInputTask`, `SentinelHubEvalscriptTask` and `SentinelHubDemTask` by adding a `session_loader` parameter. Session sharing of `sentinelhub-py` is explained [here](https://github.com/sentinel-hub/sentinelhub-py/blob/master/examples/session_sharing.ipynb). - Added `SpatialResizeTask` to `eolearn.features.feature_manipulation` for spatially resizing EOPatch features. -- Improved how `ImportFromTiffTask` reads from remote filesystems. +- Improved how `ImportFromTiffTask` reads from remote filesystems. - Switched to non-structural hashing of `EONode` class to avoid massive slowdowns in large workflows. -- Improved procedure for building documentation and displaying of type annotations. +- Improved procedure for building documentation and displaying of type annotations. - Various minor improvements. @@ -182,7 +182,7 @@ - Added `eolearn.features.DoublyLogisticApproximationTask`, contributed by @bsircelj. - Optional parameter `config` for `SaveTask` and `LoadTask` to enable defining custom AWS credentials. - Fixed a bug in `eolearn.features.ValueFilloutTask`. -- Started releasing `eo-learn` (sub)packages also as wheels. +- Started releasing `eo-learn` (sub)packages also as wheels. - Minor improvements and fixes. ## [Version 0.7.7] - 2020-08-03 @@ -228,4 +228,4 @@ - `eolearn.core.EOWorkflow`: fixed generating task dependencies. ### Added - Processing API docs generation. -- Introduced CHANGELOG.md. \ No newline at end of file +- Introduced CHANGELOG.md. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 14b423b52..aacaf5be2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -146,7 +146,7 @@ class FooTask(EOTask): When creating a new task, bear in mind the following: * Tasks should be as modular as possible, facilitating task re-use and sharing. -* An `EOTask` should perform a well-defined operation on the input eopatch(es). If the operation could be split into atomic sub-operations that could be used separately, then consider splitting the task into multiple tasks. Similarly, if tasks share the bulk of the implementation but differ in a minority of implementation, consider using Base classes and inheritance. The interpolation tasks represent a good example of this. +* An `EOTask` should perform a well-defined operation on the input eopatch(es). If the operation could be split into atomic sub-operations that could be used separately, then consider splitting the task into multiple tasks. Similarly, if tasks share the bulk of the implementation but differ in a minority of implementation, consider using Base classes and inheritance. The interpolation tasks represent a good example of this. * Tasks should be as generalizable as possible, therefore hard-coding of task parameters or `EOPatch` feature types should be avoided. Use the `EOTask._parse_features` method to parse input features in a task, and pass task parameters as arguments, either in the constructor, or at run-time. * If in doubt on whether a task is general enough to be of interest to the community, or you are not sure to which sub-package to contribute your task to, send us an email or open a [feature request](#feature-requests). diff --git a/README.md b/README.md index c19a7b7ef..c1c6b13b1 100644 --- a/README.md +++ b/README.md @@ -90,9 +90,9 @@ Some subpackages contain extension modules under `extra` subfolder. Those module ### Conda Forge distribution -The package requires a Python environment **>=3.7**. +The package requires a Python environment **>=3.7**. -Thanks to the maintainers of the conda forge feedstock (@benhuff, @dcunn, @mwilson8, @oblute, @rluria14), `eo-learn` can +Thanks to the maintainers of the conda forge feedstock (@benhuff, @dcunn, @mwilson8, @oblute, @rluria14), `eo-learn` can be installed using `conda-forge` as follows: ```bash @@ -123,7 +123,7 @@ docker pull sentinelhub/eolearn:latest docker run -p 8888:8888 sentinelhub/eolearn:latest ``` -An extended version of the `latest` image additionally contains all example notebooks and data to get you started with `eo-learn`. Run it with: +An extended version of the `latest` image additionally contains all example notebooks and data to get you started with `eo-learn`. Run it with: ```bash docker pull sentinelhub/eolearn:latest-examples @@ -164,8 +164,8 @@ If you would like to contribute to `eo-learn`, check out our [contribution guide * [Tracking a rapidly changing planet](https://medium.com/@developmentseed/tracking-a-rapidly-changing-planet-bc02efe3545d) (by Development Seed) * [Land Cover Monitoring System](https://medium.com/sentinel-hub/land-cover-monitoring-system-84406e3019ae) (by Jovan Visnjic and Matej Aleksandrov) * [eo-learn Webinar](https://www.youtube.com/watch?v=Rv-yK7Vbk4o) (by Anze Zupanc) - * [Cloud Masks at Your Service](https://medium.com/sentinel-hub/cloud-masks-at-your-service-6e5b2cb2ce8a) - * [ML examples for Common Agriculture Policy](https://medium.com/sentinel-hub/area-monitoring-concept-effc2c262583) + * [Cloud Masks at Your Service](https://medium.com/sentinel-hub/cloud-masks-at-your-service-6e5b2cb2ce8a) + * [ML examples for Common Agriculture Policy](https://medium.com/sentinel-hub/area-monitoring-concept-effc2c262583) * [High-Level Concept](https://medium.com/sentinel-hub/area-monitoring-concept-effc2c262583) * [Data Handling](https://medium.com/sentinel-hub/area-monitoring-data-handling-c255b215364f) * [Outlier detection](https://medium.com/sentinel-hub/area-monitoring-observation-outlier-detection-34f86b7cc63) @@ -183,8 +183,8 @@ If you would like to contribute to `eo-learn`, check out our [contribution guide * [The Challenge of Small Parcels](https://medium.com/sentinel-hub/area-monitoring-the-challenge-of-small-parcels-96121e169e5b) * [Traffic Light System](https://medium.com/sentinel-hub/area-monitoring-traffic-light-system-4a1348481c40) * [Expert Judgement Application](https://medium.com/sentinel-hub/expert-judgement-application-67a07f2feac4) - * [Scale-up your eo-learn workflow using Batch Processing API](https://medium.com/sentinel-hub/scale-up-your-eo-learn-workflow-using-batch-processing-api-d183b70ea237) (by Maxim Lamare) - + * [Scale-up your eo-learn workflow using Batch Processing API](https://medium.com/sentinel-hub/scale-up-your-eo-learn-workflow-using-batch-processing-api-d183b70ea237) (by Maxim Lamare) + ## Questions and Issues diff --git a/core/eolearn/core/eodata.py b/core/eolearn/core/eodata.py index 2ae4af442..b4b60cd98 100644 --- a/core/eolearn/core/eodata.py +++ b/core/eolearn/core/eodata.py @@ -286,12 +286,10 @@ def _parse_feature_type_value( if isinstance(value, (tuple, list)) and len(value) == 5: return BBox(value[:4], crs=value[4]) - if feature_type is FeatureType.TIMESTAMP: - if isinstance(value, (tuple, list)): - return [ - timestamp if isinstance(timestamp, dt.date) else dateutil.parser.parse(timestamp) - for timestamp in value - ] + if feature_type is FeatureType.TIMESTAMP and isinstance(value, (tuple, list)): + return [ + timestamp if isinstance(timestamp, dt.date) else dateutil.parser.parse(timestamp) for timestamp in value + ] raise TypeError( f"Attribute {feature_type} requires value of type {feature_type.type()} - " @@ -364,10 +362,7 @@ def __eq__(self, other): if not isinstance(self, type(other)): return False - for feature_type in FeatureType: - if not deep_eq(self[feature_type], other[feature_type]): - return False - return True + return all(deep_eq(self[feature_type], other[feature_type]) for feature_type in FeatureType) def __contains__(self, feature: Union[FeatureType, Tuple[FeatureType, str]]): if isinstance(feature, FeatureType): diff --git a/core/eolearn/core/eodata_io.py b/core/eolearn/core/eodata_io.py index 2183605cd..31861d4e0 100644 --- a/core/eolearn/core/eodata_io.py +++ b/core/eolearn/core/eodata_io.py @@ -10,6 +10,7 @@ file in the root directory of this source tree. """ import concurrent.futures +import contextlib import datetime import gzip import json @@ -355,12 +356,9 @@ def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> gpd.GeoDataFr if dataframe.crs is not None: # Trying to preserve a standard CRS and passing otherwise - try: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=SHUserWarning) - dataframe.crs = CRS(dataframe.crs).pyproj_crs() - except ValueError: - pass + with contextlib.suppress(ValueError), warnings.catch_warnings(): + warnings.simplefilter("ignore", category=SHUserWarning) + dataframe.crs = CRS(dataframe.crs).pyproj_crs() if "TIMESTAMP" in dataframe: dataframe.TIMESTAMP = pd.to_datetime(dataframe.TIMESTAMP) diff --git a/core/eolearn/core/utils/parsing.py b/core/eolearn/core/utils/parsing.py index 2e4263677..19bdef83c 100644 --- a/core/eolearn/core/utils/parsing.py +++ b/core/eolearn/core/utils/parsing.py @@ -13,6 +13,7 @@ """ from __future__ import annotations +import contextlib import sys from itertools import repeat from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Tuple, Union, cast @@ -180,10 +181,8 @@ def _parse_sequence( # Check for possible singleton if 2 <= len(features) <= 3: - try: + with contextlib.suppress(ValueError): return [(self._parse_singleton(features))] - except ValueError: - pass for feature in features: if isinstance(feature, (tuple, list)) and 2 <= len(feature) <= 3: diff --git a/core/eolearn/core/utils/vector_io.py b/core/eolearn/core/utils/vector_io.py index f24fd6c86..bf9cad396 100644 --- a/core/eolearn/core/utils/vector_io.py +++ b/core/eolearn/core/utils/vector_io.py @@ -29,7 +29,7 @@ def convert_type(column, in_type): if in_type.name.startswith("datetime64"): # numpy datetime type regardless of frequency return "datetime" - if str(in_type) in types: + if str(in_type) in types: # noqa out_type = types[str(in_type)] else: out_type = type(np.zeros(1, in_type).item()).__name__ diff --git a/core/eolearn/tests/test_eodata_io.py b/core/eolearn/tests/test_eodata_io.py index 354285553..304613a18 100644 --- a/core/eolearn/tests/test_eodata_io.py +++ b/core/eolearn/tests/test_eodata_io.py @@ -54,9 +54,8 @@ def eopatch_fixture(): def test_saving_to_a_file(eopatch): - with tempfile.NamedTemporaryFile() as fp: - with pytest.raises(CreateFailed): - eopatch.save(fp.name) + with tempfile.NamedTemporaryFile() as fp, pytest.raises(CreateFailed): + eopatch.save(fp.name) @mock_s3 diff --git a/core/eolearn/tests/test_eonode.py b/core/eolearn/tests/test_eonode.py index f4e523729..da70a833e 100644 --- a/core/eolearn/tests/test_eonode.py +++ b/core/eolearn/tests/test_eonode.py @@ -35,7 +35,7 @@ def test_nodes_different_uids(): def test_hashing(): - {EONode(Inc()): "Can be hashed!"} + _ = {EONode(Inc()): "Can be hashed!"} linear = EONode(Inc()) for _ in range(5000): diff --git a/core/eolearn/tests/test_graph.py b/core/eolearn/tests/test_graph.py index f00bbfe92..bd6e62214 100644 --- a/core/eolearn/tests/test_graph.py +++ b/core/eolearn/tests/test_graph.py @@ -81,13 +81,13 @@ def test_get_outdegree(test_graph): def test_vertices(test_graph): - assert test_graph.get_vertices() == set([1, 2, 3, 4]) + assert test_graph.get_vertices() == {1, 2, 3, 4} graph = DirectedGraph() graph.add_edge(1, 2) graph.add_edge(2, 3) graph.add_edge(3, 4) - assert graph.get_vertices() == set([1, 2, 3, 4]) + assert graph.get_vertices() == {1, 2, 3, 4} def test_add_vertex(): diff --git a/core/setup.py b/core/setup.py index ef46f5772..70e6266ab 100644 --- a/core/setup.py +++ b/core/setup.py @@ -13,16 +13,17 @@ def get_long_description(): def parse_requirements(file): - return sorted( - set(line.partition("#")[0].strip() for line in open(os.path.join(os.path.dirname(__file__), file))) - set("") - ) + with open(os.path.join(os.path.dirname(__file__), file)) as requirements_file: + return sorted({line.partition("#")[0].strip() for line in requirements_file} - set("")) def get_version(): - for line in open(os.path.join(os.path.dirname(__file__), "eolearn", "core", "__init__.py")): - if line.find("__version__") >= 0: - version = line.split("=")[1].strip() - version = version.strip('"').strip("'") + path = os.path.join(os.path.dirname(__file__), "eolearn", "core", "__init__.py") + with open(path) as version_file: + for line in version_file: + if line.find("__version__") >= 0: + version = line.split("=")[1].strip() + version = version.strip('"').strip("'") return version diff --git a/coregistration/eolearn/coregistration/coregistration.py b/coregistration/eolearn/coregistration/coregistration.py index a58b5f4d2..ec1f5e912 100644 --- a/coregistration/eolearn/coregistration/coregistration.py +++ b/coregistration/eolearn/coregistration/coregistration.py @@ -299,10 +299,10 @@ def get_params(self): LOGGER.info("\t\t\t\tRANSACThreshold: %.2f", self.params["RANSACThreshold"]) def check_params(self): - if not (self.params.get("Model") in ["Euler", "PartialAffine", "Homography"]): + if self.params.get("Model") not in ["Euler", "PartialAffine", "Homography"]: LOGGER.info("%s:Model set to Euler", self.__class__.__name__) self.params["Model"] = "Euler" - if not (self.params.get("Descriptor") in ["SIFT", "SURF"]): + if self.params.get("Descriptor") not in ["SIFT", "SURF"]: LOGGER.info("%s:Descriptor set to SIFT", self.__class__.__name__) self.params["Descriptor"] = "SIFT" if not isinstance(self.params.get("MaxIters"), int): diff --git a/coregistration/setup.py b/coregistration/setup.py index a6fe31c84..a6f3b61ad 100644 --- a/coregistration/setup.py +++ b/coregistration/setup.py @@ -13,16 +13,17 @@ def get_long_description(): def parse_requirements(file): - return sorted( - set(line.partition("#")[0].strip() for line in open(os.path.join(os.path.dirname(__file__), file))) - set("") - ) + with open(os.path.join(os.path.dirname(__file__), file)) as requirements_file: + return sorted({line.partition("#")[0].strip() for line in requirements_file} - set("")) def get_version(): - for line in open(os.path.join(os.path.dirname(__file__), "eolearn", "coregistration", "__init__.py")): - if line.find("__version__") >= 0: - version = line.split("=")[1].strip() - version = version.strip('"').strip("'") + path = os.path.join(os.path.dirname(__file__), "eolearn", "coregistration", "__init__.py") + with open(path) as version_file: + for line in version_file: + if line.find("__version__") >= 0: + version = line.split("=")[1].strip() + version = version.strip('"').strip("'") return version diff --git a/docs/source/_templates/module.rst_t b/docs/source/_templates/module.rst_t index d9a50e6b9..3878eba03 100644 --- a/docs/source/_templates/module.rst_t +++ b/docs/source/_templates/module.rst_t @@ -6,4 +6,3 @@ {%- for option in automodule_options %} :{{ option }}: {%- endfor %} - diff --git a/docs/source/conf.py b/docs/source/conf.py index 7a476aa3e..f889581a3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -43,10 +43,11 @@ # built documents. # # The release is read from __init__ file and version is shortened release string. -for line in open(os.path.join(os.path.dirname(__file__), "../../setup.py")): - if "version=" in line: - release = line.split("=")[1].strip('", \n').strip("'") - version = release.rsplit(".", 1)[0] +with open(os.path.join(os.path.dirname(__file__), "../../setup.py")) as setup_file: + for line in setup_file: + if "version=" in line: + release = line.split("=")[1].strip('", \n').strip("'") + version = release.rsplit(".", 1)[0] # -- General configuration --------------------------------------------------- diff --git a/example_data/TestEOPatch/bbox.geojson b/example_data/TestEOPatch/bbox.geojson index 81a40b648..1a58b648e 100644 --- a/example_data/TestEOPatch/bbox.geojson +++ b/example_data/TestEOPatch/bbox.geojson @@ -30,4 +30,4 @@ ] ] ] -} \ No newline at end of file +} diff --git a/example_data/TestEOPatch/meta_info.json b/example_data/TestEOPatch/meta_info.json index bf2bbc075..e615d22c7 100644 --- a/example_data/TestEOPatch/meta_info.json +++ b/example_data/TestEOPatch/meta_info.json @@ -3,4 +3,4 @@ "maxcc": 0.8, "service_type": "wcs", "size_x": "10m" -} \ No newline at end of file +} diff --git a/example_data/TestEOPatch/timestamp.json b/example_data/TestEOPatch/timestamp.json index 7935643c0..aa0b3b0c2 100644 --- a/example_data/TestEOPatch/timestamp.json +++ b/example_data/TestEOPatch/timestamp.json @@ -67,4 +67,4 @@ "2017-12-07T10:07:25", "2017-12-17T10:05:40", "2017-12-22T10:04:15" -] \ No newline at end of file +] diff --git a/examples/README.md b/examples/README.md index 7868a2189..e5fe876e4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -9,4 +9,3 @@ In order to run the example you'll need a Sentinel Hub account. You can get a tr Once you have the account set up, login to [Sentinel Hub Configurator](https://apps.sentinel-hub.com/configurator/). By default you will already have the default confoguration with an **instance ID** (alpha-numeric code of length 36). For these examples it is recommended that you create a new configuration (`"Add new configuration"`) and set the configuration to be based on **Python scripts template**. Such configuration will already contain all layers used in these examples. Otherwise you will have to define the layers for your configuration yourself. After you have decided which configuration to use, you have two options You can either put configuration's **instance ID** into `sentinelhub` package's configuration file following the [configuration instructions](http://sentinelhub-py.readthedocs.io/en/latest/configure.html) or you can write it down in the example notebooks. - diff --git a/examples/land-cover-map/README.md b/examples/land-cover-map/README.md index 678b20372..4afaa4d91 100644 --- a/examples/land-cover-map/README.md +++ b/examples/land-cover-map/README.md @@ -1,11 +1,11 @@ # Land Use and Land Cover (LULC) classification example -This example showcases how it's possible to perform an automatic LULC classification +This example showcases how it's possible to perform an automatic LULC classification on a country scale (Slovenia taken as an example) using the `eo-learn` package. The classification is performed on a time-series of all Sentinel-2 scenes from 2017. -The example is separated into multiple steps, where different aspects of the workflow are -explained. +The example is separated into multiple steps, where different aspects of the workflow are +explained. ##### Requirements @@ -23,8 +23,8 @@ check regularly for the updates.__ ## 1. Split area of interest into tiles The area of interest (AOI) at a country level is too large and needs to be tiled into smaller -pieces in order to be able to fit the one-year-long time series into memory. -Each smaller piece is called an `EOPatch` in the `eo-learn` package. In order to create an +pieces in order to be able to fit the one-year-long time series into memory. +Each smaller piece is called an `EOPatch` in the `eo-learn` package. In order to create an `EOPatch` we simply need the bounding box in a given coordinate reference system. We'll use `BBOXSplitter` from [`sentinelhub`](https://github.com/sentinel-hub/sentinelhub-py) python package. @@ -42,10 +42,10 @@ use `BBOXSplitter` from [`sentinelhub`](https://github.com/sentinel-hub/sentinel ## 2. Create EOPatches Now is time to create an `EOPatch` for each out of 293 tiles of the AOI. The `EOPatch` is created by filling it with Sentinel-2 data using Sentinel Hub services. We will add the following data to each `EOPatch`: -* L1C RGB (bands B04, B03, and B02) +* L1C RGB (bands B04, B03, and B02) * cloud probability and cloud mask from SentinelHub's `s2cloudless` cloud detector * in order to perform the cloud detection the 10 L1C bands needed by the `s2cloudless` cloud detector are going to be downloaded (and removed before saving) - + ### 2.1 Determine the number of valid observations We can count how many times in a time series a pixel is valid or not from the or SentinelHub's cloud mask. @@ -61,28 +61,28 @@ We can count how many times in a time series a pixel is valid or not from the or * `VALID_DATA` mask * map of number of valid frames per pixel * fraction of valid pixels per frame - * geo-referenced tiff files with number of valid observations + * geo-referenced tiff files with number of valid observations * Tasks (this example shows how to): * create an EOPatch and fill it with data (Sentinel-2 L1C) using SentinelHub services * run SentinelHub's cloud detector (`s2cloudless`) * remove features from an EOPatch * validate pixels using custom (user-specified) predicate - * count number of valid observations per pixel - * export a feature to geo-referenced tiff file + * count number of valid observations per pixel + * export a feature to geo-referenced tiff file * add custom (user-defined) feature to EOPatch * remove frames from an EOPatch using custom custom (user-specified) predicate * save EOPatch to disk * load EOPatch from disk - + If we take a look into the first `EOPatch` this is what we'll find: -A. Visualise a couple of frames +A. Visualise a couple of frames -![eopatch-0-frame-0](./readme_figs/patch_0.png) +![eopatch-0-frame-0](./readme_figs/patch_0.png) **Figure:** Frame with index 0 for an `EOPatch`. -![eopatch-0-frame-31](./readme_figs/patch_31.png) +![eopatch-0-frame-31](./readme_figs/patch_31.png) **Figure:** Frame with index 31 for the same `EOPatch`. @@ -98,7 +98,7 @@ B. Number of valid observations per EOPatch and entire AOI ![slovenia-valid-hist](./readme_figs/hist_number_of_valid_observations_slovenia.png) -**Figure:** Distribution of number of valid Senintel-2 observations for Slovenia in 2017. +**Figure:** Distribution of number of valid Senintel-2 observations for Slovenia in 2017. ![slovenia-fraction-valid-before](./readme_figs/fraction_valid_pixels_per_frame_eopatch-0.png) @@ -110,13 +110,12 @@ B. Number of valid observations per EOPatch and entire AOI **Notebook: 2_eopatch-L2A.ipynb** -This notebook does almost the same thing as the notebook `2_eopatch-L1C.ipynb`. The main difference that here the input collection is Sentinel-2 L2A (bottom of atmosphere or atmosphericaly corrected refelectances) produced with Sen2Cor. The cloud and cloud shadow masking is based on Sen2Cor's scene classification. +This notebook does almost the same thing as the notebook `2_eopatch-L1C.ipynb`. The main difference that here the input collection is Sentinel-2 L2A (bottom of atmosphere or atmosphericaly corrected refelectances) produced with Sen2Cor. The cloud and cloud shadow masking is based on Sen2Cor's scene classification. ![slovenia-valid-s2c](./readme_figs/number_of_valid_observations_slovenia_s2c.png) -**Figure:** Number of valid Sentinel-2 observations for Slovenia in 2017 as determined from Sen2Cor's scene classification. +**Figure:** Number of valid Sentinel-2 observations for Slovenia in 2017 as determined from Sen2Cor's scene classification. ![slovenia-valid-hist-comp](./readme_figs/hist_number_of_valid_observations_slovenia_s2c_vs_sh.png) -**Figure:** Distributions of number of valid Senintel-2 observations for Slovenia in 2017 as determined using Sen2Cor's scene classification in red and Sentinel Hub's cloud detector `s2cloudless` in blue. - +**Figure:** Distributions of number of valid Senintel-2 observations for Slovenia in 2017 as determined using Sen2Cor's scene classification in red and Sentinel Hub's cloud detector `s2cloudless` in blue. diff --git a/examples/water-monitor/theewaterskloof_dam_nominal.wkt b/examples/water-monitor/theewaterskloof_dam_nominal.wkt index d94ea1d9f..8cccf9ec1 100644 --- a/examples/water-monitor/theewaterskloof_dam_nominal.wkt +++ b/examples/water-monitor/theewaterskloof_dam_nominal.wkt @@ -1 +1 @@ -POLYGON ((19.1249753 -34.0522199, 19.1252975 -34.0501512, 19.1256813 -34.0486552, 19.1269945 -34.0488307, 19.1282511 -34.0490763, 19.1294231 -34.0491699, 19.1307786 -34.0492635, 19.131654 -34.0490997, 19.1328825 -34.048702, 19.1340827 -34.0481287, 19.1349863 -34.0475086, 19.1355229 -34.0470992, 19.1362289 -34.0462685, 19.1366765 -34.0458961, 19.1374453 -34.0461443, 19.1377248 -34.0461443, 19.1379944 -34.0460698, 19.1382041 -34.0458796, 19.1385835 -34.0452343, 19.139532 -34.0434969, 19.139542 -34.0432488, 19.1394022 -34.0430171, 19.1390827 -34.0428765, 19.1383139 -34.042711, 19.1368063 -34.0423552, 19.1366833 -34.0422585, 19.1374253 -34.0419995, 19.1383538 -34.0416272, 19.1395719 -34.0413625, 19.1403807 -34.0405765, 19.1409697 -34.0402456, 19.141449 -34.0407171, 19.1418683 -34.0411639, 19.1421978 -34.0414452, 19.1425073 -34.041652, 19.1427669 -34.0416768, 19.1430365 -34.0416272, 19.1441747 -34.0409488, 19.1448436 -34.0406261, 19.1453317 -34.0403761, 19.14559 -34.0402191, 19.1457797 -34.0400537, 19.1459994 -34.0405004, 19.146229 -34.0411871, 19.1467112 -34.0433933, 19.1472036 -34.0452841, 19.1479463 -34.0463661, 19.1484356 -34.0461675, 19.1482958 -34.0459193, 19.1479563 -34.0455388, 19.1477167 -34.0451831, 19.1475569 -34.0448025, 19.1473273 -34.043909, 19.1472474 -34.0433961, 19.1470777 -34.0426763, 19.146908 -34.0418407, 19.1467382 -34.0410878, 19.1465585 -34.0404756, 19.1461691 -34.0399047, 19.1450109 -34.038763, 19.1454503 -34.0388375, 19.1459095 -34.0387878, 19.1461991 -34.0384569, 19.1463089 -34.0382997, 19.1466783 -34.0382914, 19.1475263 -34.0381793, 19.148997 -34.037738, 19.1506702 -34.037585, 19.1515162 -34.037994, 19.1529484 -34.0377021, 19.1534094 -34.0367232, 19.1534283 -34.0356208, 19.1529932 -34.0339816, 19.1525861 -34.0331205, 19.1522096 -34.0326652, 19.1519102 -34.0317503, 19.1512112 -34.0310103, 19.1507419 -34.0307538, 19.1501029 -34.0306711, 19.1496237 -34.0305801, 19.1494739 -34.030216, 19.1494939 -34.0295209, 19.1493541 -34.0283294, 19.1493841 -34.0274192, 19.1492747 -34.026678, 19.1491644 -34.0260208, 19.1489248 -34.0254994, 19.1486652 -34.0254001, 19.1477067 -34.0251602, 19.1467482 -34.0249698, 19.1461392 -34.0244651, 19.1454203 -34.0239603, 19.1449211 -34.0233562, 19.1442621 -34.0228928, 19.1436431 -34.022388, 19.1434234 -34.0216846, 19.1427345 -34.0215936, 19.1418659 -34.0215771, 19.1412169 -34.0217674, 19.1410971 -34.0218584, 19.1409573 -34.0220074, 19.1413068 -34.0223798, 19.1415164 -34.0226942, 19.1412968 -34.0229176, 19.1406179 -34.0230004, 19.1401686 -34.0230087, 19.1395795 -34.0232073, 19.1393099 -34.0237038, 19.1389006 -34.0240265, 19.1385012 -34.023952, 19.1382716 -34.0236955, 19.1381617 -34.0235796, 19.1378522 -34.02353, 19.1371234 -34.0237452, 19.1361149 -34.0240513, 19.1355958 -34.0243327, 19.1353162 -34.0246554, 19.135416 -34.0250609, 19.1349668 -34.0254415, 19.1343078 -34.0258387, 19.1342079 -34.0262193, 19.1343577 -34.0266331, 19.1343877 -34.0270137, 19.1340782 -34.0272537, 19.1336388 -34.0271378, 19.1326504 -34.0266248, 19.1317518 -34.0262028, 19.1308432 -34.0261283, 19.1280876 -34.026178, 19.1266598 -34.026029, 19.1265999 -34.0247795, 19.1276483 -34.0235383, 19.1272689 -34.0227273, 19.1273388 -34.021635, 19.1286667 -34.0195827, 19.1293256 -34.0186145, 19.1326304 -34.015478, 19.1332295 -34.0144104, 19.1331296 -34.0126559, 19.1329899 -34.0113482, 19.1330797 -34.0096268, 19.1333593 -34.0075576, 19.1334898 -34.0064794, 19.1336488 -34.005894, 19.1340782 -34.0047932, 19.1348969 -34.0039076, 19.1359352 -34.0038414, 19.1369536 -34.003204, 19.1382216 -34.0025005, 19.1384382 -34.0016212, 19.1385652 -34.0015197, 19.1388606 -34.0012837, 19.1395595 -34.0008699, 19.1395196 -33.9998683, 19.1401686 -33.999231, 19.1413267 -33.9975258, 19.1427886 -33.9966579, 19.1441423 -33.9962179, 19.1440924 -33.9953736, 19.144342 -33.9950839, 19.1446914 -33.9955557, 19.14559 -33.995804, 19.146229 -33.9959696, 19.1464886 -33.9964663, 19.1473573 -33.9966318, 19.1476767 -33.9966153, 19.1478265 -33.9963669, 19.1475569 -33.9959696, 19.1471476 -33.9956633, 19.1465385 -33.9953984, 19.1462789 -33.9951253, 19.1454003 -33.9946865, 19.1453205 -33.994463, 19.1456252 -33.9938548, 19.1459595 -33.9939167, 19.1465984 -33.9940243, 19.1467083 -33.9939333, 19.1466084 -33.9937842, 19.1461691 -33.9935442, 19.1459495 -33.9932958, 19.1459794 -33.9929978, 19.1461691 -33.9926502, 19.1466983 -33.9923356, 19.1469479 -33.9921949, 19.1469379 -33.9917147, 19.1475816 -33.9889109, 19.1478165 -33.9880723, 19.1478964 -33.9875259, 19.1477067 -33.9869381, 19.1478165 -33.9866815, 19.1484156 -33.9870706, 19.1490845 -33.987203, 19.1498134 -33.9872279, 19.1509117 -33.987261, 19.15199 -33.9870374, 19.152589 -33.9867725, 19.1531681 -33.9867229, 19.1532979 -33.9869795, 19.153218 -33.9874679, 19.1530483 -33.9879564, 19.1529257 -33.9882924, 19.1531182 -33.9883786, 19.1541266 -33.9888008, 19.1547656 -33.9891154, 19.1557441 -33.9893058, 19.1568623 -33.9897031, 19.1577609 -33.9896286, 19.1584797 -33.9895293, 19.1588492 -33.9892395, 19.1592785 -33.989223, 19.1597677 -33.9893058, 19.1599674 -33.9895872, 19.1600672 -33.9897776, 19.1604466 -33.9900591, 19.1605265 -33.9904233, 19.160237 -33.9908786, 19.1602869 -33.9910442, 19.160886 -33.9907959, 19.1613452 -33.9905227, 19.1616547 -33.9902329, 19.1620841 -33.9902992, 19.162773 -33.9908538, 19.1632123 -33.9914498, 19.1641908 -33.9922114, 19.1647798 -33.9928406, 19.1651792 -33.994074, 19.1655886 -33.9948273, 19.1661377 -33.995713, 19.166557 -33.9964414, 19.167186 -33.9979314, 19.1680647 -33.9986184, 19.1687236 -33.9992641, 19.1693626 -33.9996779, 19.1693726 -34.0001166, 19.1688634 -34.0005885, 19.1695024 -34.0008368, 19.1701114 -34.0011844, 19.1708203 -34.0013169, 19.1717132 -34.0019868, 19.1728871 -34.0026081, 19.1742349 -34.0035931, 19.1753132 -34.0042966, 19.1763117 -34.0050498, 19.1775996 -34.0058195, 19.1791272 -34.0061754, 19.1804552 -34.006192, 19.1814935 -34.0068872, 19.1826417 -34.0069866, 19.1842192 -34.007119, 19.1855871 -34.0066638, 19.1869949 -34.0062251, 19.1889817 -34.0058361, 19.1904295 -34.0061589, 19.1918173 -34.0067631, 19.1931452 -34.007508, 19.1946728 -34.0077563, 19.1959607 -34.0080873, 19.1967295 -34.0084432, 19.1973785 -34.0091798, 19.1978378 -34.0096019, 19.1982072 -34.0097675, 19.1987963 -34.0097923, 19.1993354 -34.0097509, 19.1999944 -34.010024, 19.2005435 -34.0104875, 19.2010727 -34.0114227, 19.2014421 -34.0124407, 19.2019913 -34.0129042, 19.20281 -34.0136821, 19.2036287 -34.0144766, 19.2042178 -34.0150973, 19.2050764 -34.0162559, 19.2055457 -34.0175469, 19.2060748 -34.019202, 19.2069035 -34.0203275, 19.2073029 -34.0205096, 19.2079419 -34.0208157, 19.2080917 -34.0214612, 19.2082514 -34.0221894, 19.2082015 -34.0226942, 19.2083812 -34.0231907, 19.2087706 -34.0232652, 19.20917 -34.0246388, 19.2092299 -34.025425, 19.2093796 -34.0260539, 19.209759 -34.0262442, 19.2102563 -34.0264727, 19.2107674 -34.0267076, 19.211077 -34.0267241, 19.2117259 -34.0264924, 19.2120454 -34.0266745, 19.2126045 -34.0271958, 19.2125746 -34.0276426, 19.2131237 -34.0277253, 19.213573 -34.0277171, 19.2138426 -34.0277088, 19.2151041 -34.0272654, 19.2166282 -34.0269558, 19.2183056 -34.0267655, 19.2195536 -34.0268731, 19.2204822 -34.0263931, 19.2217601 -34.0266331, 19.2230082 -34.0265503, 19.2241281 -34.0261036, 19.2256105 -34.0250925, 19.2263165 -34.0250691, 19.2269237 -34.0248116, 19.2268248 -34.024168, 19.2272484 -34.024051, 19.2280815 -34.024285, 19.2291122 -34.025151, 19.2300724 -34.0261106, 19.2310043 -34.0271404, 19.2317244 -34.0278191, 19.2328637 -34.028387, 19.2339977 -34.0286383, 19.2350709 -34.0285898, 19.2374195 -34.0280819, 19.2392267 -34.0278108, 19.2403423 -34.0271575, 19.2412424 -34.0267311, 19.2422062 -34.0264553, 19.2428698 -34.0260691, 19.2437121 -34.0251068, 19.2442677 -34.0242904, 19.2444058 -34.0240654, 19.2444507 -34.0238503, 19.2443558 -34.0235979, 19.244261 -34.0231055, 19.244226 -34.022849, 19.2438417 -34.0228987, 19.2435571 -34.0227042, 19.2434622 -34.0224973, 19.2434023 -34.0221663, 19.2433724 -34.0219677, 19.2435321 -34.0219139, 19.2438067 -34.0219801, 19.2437168 -34.0218229, 19.2437119 -34.0215498, 19.2438616 -34.0210574, 19.2442111 -34.0206354, 19.2445955 -34.0201844, 19.2448251 -34.0196258, 19.2451146 -34.0194768, 19.245519 -34.0194686, 19.2460033 -34.0193486, 19.2463627 -34.0191086, 19.2465224 -34.0188562, 19.2468369 -34.018641, 19.2472163 -34.0184258, 19.2475209 -34.0180948, 19.2476956 -34.0178052, 19.2480251 -34.0176107, 19.2483995 -34.0176769, 19.2488837 -34.0177266, 19.2492432 -34.0180038, 19.2494179 -34.0181941, 19.249358 -34.0183679, 19.2489137 -34.0186162, 19.2488138 -34.0189927, 19.2485542 -34.0198823, 19.2481848 -34.0209292, 19.2481549 -34.0215333, 19.2481998 -34.0221663, 19.2484688 -34.0225766, 19.2491108 -34.0232722, 19.2496389 -34.0239992, 19.2502404 -34.0241031, 19.2508827 -34.0239879, 19.2513982 -34.0235999, 19.251648 -34.0231923, 19.2522759 -34.0226912, 19.2529655 -34.022453, 19.2536998 -34.0224062, 19.2546197 -34.0225241, 19.2552247 -34.0223711, 19.2563252 -34.0218431, 19.2573568 -34.0212827, 19.2578651 -34.0209434, 19.2581758 -34.0205455, 19.258233 -34.0200414, 19.2582774 -34.0180161, 19.2600337 -34.017519, 19.2606327 -34.0173742, 19.2612288 -34.0170745, 19.2614065 -34.0168983, 19.2621104 -34.0162818, 19.2626196 -34.015748, 19.2629391 -34.0151977, 19.2631987 -34.0146804, 19.2633684 -34.0142335, 19.2638177 -34.013857, 19.2646414 -34.0136873, 19.2652754 -34.013588, 19.2661241 -34.0129383, 19.2664985 -34.0126238, 19.2670127 -34.0130045, 19.2672922 -34.013439, 19.2677016 -34.0142501, 19.2678064 -34.0150611, 19.2677116 -34.016137, 19.2676667 -34.0170597, 19.2671275 -34.0175314, 19.2666083 -34.0182142, 19.2660242 -34.0191741, 19.2656798 -34.0201093, 19.2656398 -34.0206803, 19.2656898 -34.0211147, 19.2657946 -34.0215161, 19.2660784 -34.0217952, 19.2662788 -34.0217974, 19.266838 -34.0217767, 19.2670676 -34.0220912, 19.2671075 -34.0225629, 19.2666083 -34.0230718, 19.2660974 -34.0232751, 19.2656025 -34.0233942, 19.2646522 -34.0235212, 19.264092 -34.0237373, 19.2636812 -34.0240674, 19.2630942 -34.0246197, 19.2625885 -34.0254953, 19.2621556 -34.0261094, 19.2615332 -34.0267149, 19.2630014 -34.0277173, 19.2655497 -34.0278031, 19.2679265 -34.0274586, 19.2705924 -34.0263075, 19.2734105 -34.0262249, 19.275076 -34.0251286, 19.2764492 -34.0253959, 19.2769933 -34.02332, 19.2782073 -34.0224801, 19.278444 -34.0213299, 19.2805759 -34.0198308, 19.2826753 -34.0191536, 19.2856243 -34.0202793, 19.2872994 -34.0215368, 19.2880843 -34.0232801, 19.2879351 -34.0265412, 19.2880747 -34.0272675, 19.2879335 -34.0277824, 19.2876794 -34.0281218, 19.2875241 -34.0290462, 19.28751 -34.0301462, 19.2873123 -34.0314685, 19.2871428 -34.0326854, 19.2871428 -34.0344991, 19.2871005 -34.0357746, 19.2869549 -34.03631, 19.2866486 -34.0374361, 19.2863662 -34.0375648, 19.2860415 -34.0378339, 19.2858862 -34.0382785, 19.285985 -34.03877, 19.2862956 -34.0391561, 19.2859003 -34.0401623, 19.2852508 -34.0402559, 19.284573 -34.0406069, 19.2841353 -34.041426, 19.283514 -34.0419642, 19.2833305 -34.0425843, 19.2831045 -34.0433448, 19.2830117 -34.0436183, 19.2828504 -34.0440936, 19.2829351 -34.045556, 19.2830622 -34.0465973, 19.2833163 -34.0471706, 19.2831045 -34.0481884, 19.2830395 -34.0495123, 19.2831044 -34.0508028, 19.283324 -34.0513405, 19.2834838 -34.0518203, 19.2836935 -34.0525235, 19.2837234 -34.0532597, 19.2837833 -34.0537395, 19.2841527 -34.0542193, 19.2845621 -34.0550878, 19.284602 -34.0555676, 19.2843724 -34.0559398, 19.284013 -34.0564527, 19.2834039 -34.0568084, 19.2829946 -34.0569986, 19.2828448 -34.0573378, 19.282735 -34.0579333, 19.2828748 -34.0588515, 19.2832342 -34.0596538, 19.283334 -34.0603156, 19.2835637 -34.0608863, 19.2838432 -34.0621104, 19.2839331 -34.0624992, 19.2841827 -34.0626315, 19.2844323 -34.0626067, 19.2845421 -34.0624578, 19.2848616 -34.0623834, 19.2855206 -34.0623917, 19.2859837 -34.06248, 19.2863992 -34.0626398, 19.2868185 -34.0628052, 19.2864391 -34.063012, 19.2860198 -34.063194, 19.2856404 -34.0634256, 19.2854207 -34.0638391, 19.2851911 -34.064261, 19.2853009 -34.0648399, 19.2854807 -34.0655512, 19.2853409 -34.0657994, 19.2851112 -34.0662129, 19.2851811 -34.0666513, 19.2856404 -34.0671971, 19.2857439 -34.0676776, 19.2859799 -34.0681731, 19.2868685 -34.0691159, 19.2873078 -34.069455, 19.2881435 -34.0694929, 19.2895878 -34.0700898, 19.2912201 -34.0709553, 19.2920209 -34.0711003, 19.2924402 -34.0712988, 19.2926892 -34.0716011, 19.2928356 -34.0721622, 19.2926199 -34.0725393, 19.2922937 -34.0734127, 19.291961 -34.0741371, 19.2914435 -34.0748784, 19.2911659 -34.0756217, 19.2910577 -34.0756135, 19.2909823 -34.0756271, 19.2909446 -34.0756623, 19.2909302 -34.0757065, 19.2909122 -34.0759447, 19.2909373 -34.0763219, 19.2905677 -34.076685, 19.2900882 -34.0769505, 19.2890855 -34.077496, 19.2879897 -34.0781153, 19.2873968 -34.0783823, 19.2868376 -34.0785625, 19.2864278 -34.0786517, 19.2860655 -34.0786907, 19.2856502 -34.0786633, 19.2853741 -34.0785663, 19.284921 -34.0783679, 19.2842422 -34.0780512, 19.2837534 -34.0777831, 19.2826751 -34.077444, 19.2817665 -34.0774192, 19.2805684 -34.0776012, 19.2799494 -34.0778906, 19.27955 -34.0784199, 19.278921 -34.0786183, 19.2780923 -34.0787755, 19.2773934 -34.0788251, 19.2764249 -34.0787672, 19.2756761 -34.0786845, 19.2750071 -34.0786597, 19.274458 -34.0784695, 19.2738789 -34.0786431, 19.2732799 -34.0788085, 19.2728306 -34.0789739, 19.2725909 -34.0792137, 19.2724212 -34.0792551, 19.2724611 -34.0789657, 19.2728106 -34.0785853, 19.2734196 -34.0779981, 19.2745334 -34.0773618, 19.2749672 -34.0772538, 19.2754864 -34.0771132, 19.2758658 -34.0767576, 19.2764149 -34.0765178, 19.2767943 -34.0765261, 19.2770439 -34.0763607, 19.2761753 -34.0757239, 19.2761678 -34.0752703, 19.2756262 -34.0749713, 19.275097 -34.0743428, 19.2745778 -34.0735737, 19.2740786 -34.0731933, 19.2732399 -34.0728459, 19.2726409 -34.0726557, 19.2724711 -34.0724572, 19.2725588 -34.0721523, 19.2726409 -34.0719196, 19.2732898 -34.071655, 19.2737456 -34.0712809, 19.2740387 -34.0710264, 19.2741273 -34.0707619, 19.2740586 -34.0701001, 19.2740686 -34.0694385, 19.2739788 -34.0687272, 19.2738689 -34.0678009, 19.2736792 -34.0666347, 19.2734579 -34.0661955, 19.2732998 -34.0658986, 19.2729604 -34.0654685, 19.2726808 -34.0652535, 19.2720917 -34.0648647, 19.2718022 -34.0644925, 19.2719719 -34.0640624, 19.2721816 -34.0632684, 19.2723214 -34.0624661, 19.2722615 -34.061854, 19.2721117 -34.0610683, 19.2720019 -34.0604975, 19.2718621 -34.0597366, 19.2715549 -34.0591617, 19.2716824 -34.0587109, 19.2718022 -34.0583138, 19.2716025 -34.0577431, 19.271263 -34.0572385, 19.2709435 -34.05704, 19.2705042 -34.0569325, 19.2699351 -34.0569986, 19.2694559 -34.0572137, 19.2688269 -34.057437, 19.268098 -34.0576025, 19.2674191 -34.0575777, 19.2668799 -34.0576521, 19.2663308 -34.0577348, 19.2656618 -34.0576356, 19.2648431 -34.0576438, 19.2637049 -34.0579416, 19.2629262 -34.05838, 19.2622472 -34.0584627, 19.2614585 -34.0586778, 19.2607895 -34.0588929, 19.2602104 -34.0593809, 19.2593019 -34.0597862, 19.2583633 -34.0603652, 19.2579041 -34.0608367, 19.2577343 -34.0612502, 19.2577743 -34.0617713, 19.2578941 -34.0623669, 19.2581137 -34.0627391, 19.2581137 -34.0630037, 19.2575247 -34.0625736, 19.2572151 -34.0623751, 19.2568857 -34.061945, 19.2564963 -34.0615811, 19.256007 -34.061275, 19.2551783 -34.0610186, 19.2543397 -34.0607953, 19.2536308 -34.0606133, 19.2527721 -34.0605306, 19.2521831 -34.060663, 19.2512945 -34.060878, 19.2506655 -34.0610517, 19.2502162 -34.0611013, 19.2498567 -34.0611841, 19.2493575 -34.0613826, 19.2492677 -34.0611179, 19.2499166 -34.0606878, 19.2510548 -34.0601915, 19.2518436 -34.0596621, 19.2524444 -34.0594726, 19.2528121 -34.0591906, 19.2535509 -34.0591162, 19.2541999 -34.0592403, 19.2549687 -34.0591989, 19.2555578 -34.0590087, 19.2559471 -34.0587274, 19.2563365 -34.0582311, 19.2565961 -34.0576852, 19.2568457 -34.05704, 19.256686 -34.0567257, 19.2564563 -34.0565354, 19.2560769 -34.0561053, 19.2558972 -34.0558158, 19.2561568 -34.055609, 19.2564863 -34.0555841, 19.2569655 -34.055609, 19.2574548 -34.0555593, 19.257924 -34.0552946, 19.2582435 -34.0550713, 19.2589046 -34.0543125, 19.2597507 -34.0530957, 19.2606398 -34.0524077, 19.2615184 -34.052143, 19.2623471 -34.0520437, 19.2629561 -34.0520851, 19.2634254 -34.0522257, 19.2640943 -34.0522174, 19.2644438 -34.052052, 19.2651327 -34.0516383, 19.2659015 -34.0511172, 19.2664606 -34.050414, 19.2667002 -34.0500335, 19.2672593 -34.0499839, 19.267439 -34.0500914, 19.2678284 -34.0501328, 19.2682378 -34.0501659, 19.2685773 -34.0497853, 19.2688069 -34.0495123, 19.2683776 -34.049198, 19.2681579 -34.0491732, 19.2677685 -34.0493552, 19.2673093 -34.049744, 19.2665804 -34.0498267, 19.266231 -34.0495785, 19.2658715 -34.0495289, 19.265562 -34.0492145, 19.2652525 -34.0486437, 19.2650129 -34.0481887, 19.2643938 -34.0477089, 19.264294 -34.0472622, 19.2638547 -34.0466831, 19.2633155 -34.0461867, 19.2631857 -34.0457648, 19.2630759 -34.0450202, 19.2630959 -34.0445735, 19.2628263 -34.0443832, 19.262357 -34.0441598, 19.2620975 -34.0438124, 19.261728 -34.0435394, 19.2613387 -34.0435063, 19.2606497 -34.043589, 19.2600906 -34.0436386, 19.2596214 -34.0435642, 19.2589025 -34.043407, 19.2579839 -34.0434815, 19.2567459 -34.0437462, 19.2559272 -34.0439778, 19.2548089 -34.044557, 19.2538604 -34.0450699, 19.2530217 -34.0456573, 19.2520932 -34.0464515, 19.2513144 -34.0469809, 19.2507453 -34.0472456, 19.2503759 -34.0472208, 19.2504158 -34.0462943, 19.2500947 -34.0457366, 19.2494973 -34.0442674, 19.2491079 -34.043771, 19.2482293 -34.0428692, 19.2474605 -34.0423563, 19.2467616 -34.0419261, 19.2461725 -34.0418765, 19.2455835 -34.0418185, 19.2448147 -34.0418516, 19.2442456 -34.0417606, 19.2437963 -34.0412229, 19.2426792 -34.0402909, 19.2420091 -34.0394937, 19.2411504 -34.0389642, 19.2402518 -34.0385836, 19.2393033 -34.038352, 19.2383948 -34.0385174, 19.2377658 -34.0387077, 19.2374962 -34.03908, 19.2372665 -34.0395599, 19.2372166 -34.0398991, 19.2368172 -34.0403211, 19.2364378 -34.0405775, 19.2362581 -34.0403459, 19.235709 -34.0400646, 19.2349602 -34.0399819, 19.2346007 -34.0401225, 19.2337521 -34.0403376, 19.2333327 -34.0403293, 19.2330132 -34.040288, 19.2327437 -34.04047, 19.2324142 -34.0406437, 19.2318151 -34.0407099, 19.231286 -34.040834, 19.2309465 -34.041107, 19.2305172 -34.0413387, 19.2300579 -34.0416448, 19.2297084 -34.0422488, 19.2296785 -34.043043, 19.2296186 -34.0439778, 19.2296186 -34.0450037, 19.2294089 -34.0459468, 19.2291293 -34.0464928, 19.2288398 -34.0469892, 19.2285602 -34.0472622, 19.2285103 -34.0476841, 19.2285003 -34.0480315, 19.2283705 -34.0479902, 19.2282907 -34.0475765, 19.2280759 -34.047103, 19.227442 -34.046013, 19.2268743 -34.045271, 19.225576 -34.0439503, 19.2238788 -34.0430834, 19.2226594 -34.0431388, 19.2209423 -34.0442637, 19.2192748 -34.0462013, 19.2191047 -34.0446819, 19.219066 -34.0407609, 19.2181274 -34.0388553, 19.2157926 -34.0379702, 19.2139879 -34.0374727, 19.213122 -34.0380984, 19.2120463 -34.0389699, 19.2117563 -34.0408065, 19.2106607 -34.0409175, 19.209541 -34.0398983, 19.2076828 -34.0402841, 19.206598 -34.0408367, 19.2056066 -34.0415185, 19.2042101 -34.0426877, 19.2040174 -34.0419818, 19.2059287 -34.0392432, 19.2080949 -34.0357309, 19.2081945 -34.0343646, 19.2079447 -34.0328966, 19.2080578 -34.0315665, 19.2074543 -34.0300751, 19.2064335 -34.0289585, 19.2063462 -34.0279302, 19.2058989 -34.0279359, 19.20519 -34.02782, 19.2042615 -34.0281676, 19.2029635 -34.028391, 19.2014459 -34.0283331, 19.1992793 -34.0280765, 19.1982609 -34.0281758, 19.1972126 -34.0280269, 19.1959546 -34.0280103, 19.1959845 -34.0274973, 19.1965936 -34.0273898, 19.1965436 -34.0272656, 19.1960644 -34.0272491, 19.1958847 -34.0268684, 19.1948663 -34.0259168, 19.1945767 -34.0248659, 19.1942173 -34.0238316, 19.193768 -34.0229213, 19.1937281 -34.022491, 19.193748 -34.022069, 19.1931689 -34.0207698, 19.1927895 -34.0199091, 19.1922903 -34.0185188, 19.1915618 -34.0175299, 19.1907827 -34.0163672, 19.1904832 -34.0161023, 19.1899041 -34.0156306, 19.1891153 -34.0152582, 19.1881568 -34.015101, 19.1878473 -34.0152748, 19.1872383 -34.015283, 19.1865094 -34.0153658, 19.1856308 -34.0148858, 19.1852314 -34.0146375, 19.184253 -34.0145051, 19.1839035 -34.0144968, 19.183624 -34.0146292, 19.1835042 -34.0147699, 19.183604 -34.0148858, 19.1832346 -34.0148941, 19.1828652 -34.0150761, 19.1827254 -34.0154403, 19.1824957 -34.0157961, 19.1824458 -34.0161189, 19.182356 -34.0162761, 19.1819466 -34.0162927, 19.1814574 -34.0163258, 19.1809582 -34.0166072, 19.1805089 -34.0165741, 19.1796103 -34.0168803, 19.1794006 -34.0171947, 19.1786618 -34.0174016, 19.1777432 -34.0181382, 19.1772141 -34.0185685, 19.1769345 -34.0190236, 19.1769045 -34.0192554, 19.1769445 -34.0197022, 19.1769944 -34.019876, 19.1771342 -34.0199257, 19.1770044 -34.0201739, 19.1764053 -34.0203643, 19.175996 -34.0206787, 19.1758262 -34.0210677, 19.1757963 -34.0216056, 19.1757564 -34.0220276, 19.1757064 -34.0224248, 19.1752671 -34.0225986, 19.1739392 -34.0232689, 19.1731205 -34.0241295, 19.1724216 -34.0250314, 19.1723917 -34.0258093, 19.1724216 -34.026703, 19.1727012 -34.0275635, 19.1729608 -34.0292102, 19.1729608 -34.0302776, 19.172801 -34.0314111, 19.1727211 -34.0322055, 19.172801 -34.0330081, 19.172811 -34.0335542, 19.1724216 -34.0341664, 19.1716328 -34.0350849, 19.1705845 -34.0362763, 19.1698357 -34.0366652, 19.1689271 -34.0373602, 19.1679986 -34.0379228, 19.1665808 -34.0386177, 19.1653727 -34.0390231, 19.1648036 -34.0390397, 19.1645141 -34.0392796, 19.1643343 -34.0398174, 19.1636754 -34.0405206, 19.1623974 -34.0415879, 19.1618083 -34.0418444, 19.16072 -34.0429199, 19.1587931 -34.0449964, 19.1573154 -34.0462126, 19.1565266 -34.0465683, 19.1563469 -34.0469571, 19.1559675 -34.0473542, 19.1548892 -34.0473542, 19.1540705 -34.0470812, 19.1532618 -34.0466676, 19.1521835 -34.0461629, 19.1519339 -34.0461381, 19.1516044 -34.0463946, 19.1506359 -34.0463946, 19.1494478 -34.0464194, 19.1488127 -34.0463517, 19.1487109 -34.0463587, 19.1485838 -34.0465693, 19.1486332 -34.0468851, 19.1484003 -34.047201, 19.1480402 -34.0476456, 19.1476378 -34.0479615, 19.147433 -34.048213, 19.1472707 -34.048874, 19.147193 -34.0492952, 19.1471859 -34.0495877, 19.1473342 -34.0499854, 19.1475672 -34.0502955, 19.1477013 -34.050506, 19.1476731 -34.0509623, 19.1478708 -34.0516233, 19.1478496 -34.0521088, 19.1479131 -34.0525884, 19.1474966 -34.0528166, 19.1465294 -34.052799, 19.1456116 -34.0526879, 19.1450256 -34.0524773, 19.1443973 -34.0520737, 19.1436136 -34.0519216, 19.1427029 -34.0516759, 19.1417074 -34.0515063, 19.1410579 -34.0514595, 19.1405143 -34.0515005, 19.140006 -34.0514303, 19.1395894 -34.0512372, 19.1389046 -34.0511436, 19.1383186 -34.0512021, 19.1375985 -34.0514654, 19.1372455 -34.0517286, 19.1368784 -34.0521439, 19.1367372 -34.0525358, 19.1366384 -34.0528634, 19.1365042 -34.0530973, 19.136003 -34.0530739, 19.1351558 -34.0531149, 19.1343086 -34.0533547, 19.1334049 -34.0535126, 19.1328401 -34.0535009, 19.1324306 -34.0534249, 19.1309692 -34.0535945, 19.1295219 -34.0539513, 19.12871 -34.0535419, 19.1280323 -34.0531968, 19.1267544 -34.0529745, 19.1258931 -34.0527932, 19.125413 -34.0524539, 19.1249753 -34.0522199)) \ No newline at end of file +POLYGON ((19.1249753 -34.0522199, 19.1252975 -34.0501512, 19.1256813 -34.0486552, 19.1269945 -34.0488307, 19.1282511 -34.0490763, 19.1294231 -34.0491699, 19.1307786 -34.0492635, 19.131654 -34.0490997, 19.1328825 -34.048702, 19.1340827 -34.0481287, 19.1349863 -34.0475086, 19.1355229 -34.0470992, 19.1362289 -34.0462685, 19.1366765 -34.0458961, 19.1374453 -34.0461443, 19.1377248 -34.0461443, 19.1379944 -34.0460698, 19.1382041 -34.0458796, 19.1385835 -34.0452343, 19.139532 -34.0434969, 19.139542 -34.0432488, 19.1394022 -34.0430171, 19.1390827 -34.0428765, 19.1383139 -34.042711, 19.1368063 -34.0423552, 19.1366833 -34.0422585, 19.1374253 -34.0419995, 19.1383538 -34.0416272, 19.1395719 -34.0413625, 19.1403807 -34.0405765, 19.1409697 -34.0402456, 19.141449 -34.0407171, 19.1418683 -34.0411639, 19.1421978 -34.0414452, 19.1425073 -34.041652, 19.1427669 -34.0416768, 19.1430365 -34.0416272, 19.1441747 -34.0409488, 19.1448436 -34.0406261, 19.1453317 -34.0403761, 19.14559 -34.0402191, 19.1457797 -34.0400537, 19.1459994 -34.0405004, 19.146229 -34.0411871, 19.1467112 -34.0433933, 19.1472036 -34.0452841, 19.1479463 -34.0463661, 19.1484356 -34.0461675, 19.1482958 -34.0459193, 19.1479563 -34.0455388, 19.1477167 -34.0451831, 19.1475569 -34.0448025, 19.1473273 -34.043909, 19.1472474 -34.0433961, 19.1470777 -34.0426763, 19.146908 -34.0418407, 19.1467382 -34.0410878, 19.1465585 -34.0404756, 19.1461691 -34.0399047, 19.1450109 -34.038763, 19.1454503 -34.0388375, 19.1459095 -34.0387878, 19.1461991 -34.0384569, 19.1463089 -34.0382997, 19.1466783 -34.0382914, 19.1475263 -34.0381793, 19.148997 -34.037738, 19.1506702 -34.037585, 19.1515162 -34.037994, 19.1529484 -34.0377021, 19.1534094 -34.0367232, 19.1534283 -34.0356208, 19.1529932 -34.0339816, 19.1525861 -34.0331205, 19.1522096 -34.0326652, 19.1519102 -34.0317503, 19.1512112 -34.0310103, 19.1507419 -34.0307538, 19.1501029 -34.0306711, 19.1496237 -34.0305801, 19.1494739 -34.030216, 19.1494939 -34.0295209, 19.1493541 -34.0283294, 19.1493841 -34.0274192, 19.1492747 -34.026678, 19.1491644 -34.0260208, 19.1489248 -34.0254994, 19.1486652 -34.0254001, 19.1477067 -34.0251602, 19.1467482 -34.0249698, 19.1461392 -34.0244651, 19.1454203 -34.0239603, 19.1449211 -34.0233562, 19.1442621 -34.0228928, 19.1436431 -34.022388, 19.1434234 -34.0216846, 19.1427345 -34.0215936, 19.1418659 -34.0215771, 19.1412169 -34.0217674, 19.1410971 -34.0218584, 19.1409573 -34.0220074, 19.1413068 -34.0223798, 19.1415164 -34.0226942, 19.1412968 -34.0229176, 19.1406179 -34.0230004, 19.1401686 -34.0230087, 19.1395795 -34.0232073, 19.1393099 -34.0237038, 19.1389006 -34.0240265, 19.1385012 -34.023952, 19.1382716 -34.0236955, 19.1381617 -34.0235796, 19.1378522 -34.02353, 19.1371234 -34.0237452, 19.1361149 -34.0240513, 19.1355958 -34.0243327, 19.1353162 -34.0246554, 19.135416 -34.0250609, 19.1349668 -34.0254415, 19.1343078 -34.0258387, 19.1342079 -34.0262193, 19.1343577 -34.0266331, 19.1343877 -34.0270137, 19.1340782 -34.0272537, 19.1336388 -34.0271378, 19.1326504 -34.0266248, 19.1317518 -34.0262028, 19.1308432 -34.0261283, 19.1280876 -34.026178, 19.1266598 -34.026029, 19.1265999 -34.0247795, 19.1276483 -34.0235383, 19.1272689 -34.0227273, 19.1273388 -34.021635, 19.1286667 -34.0195827, 19.1293256 -34.0186145, 19.1326304 -34.015478, 19.1332295 -34.0144104, 19.1331296 -34.0126559, 19.1329899 -34.0113482, 19.1330797 -34.0096268, 19.1333593 -34.0075576, 19.1334898 -34.0064794, 19.1336488 -34.005894, 19.1340782 -34.0047932, 19.1348969 -34.0039076, 19.1359352 -34.0038414, 19.1369536 -34.003204, 19.1382216 -34.0025005, 19.1384382 -34.0016212, 19.1385652 -34.0015197, 19.1388606 -34.0012837, 19.1395595 -34.0008699, 19.1395196 -33.9998683, 19.1401686 -33.999231, 19.1413267 -33.9975258, 19.1427886 -33.9966579, 19.1441423 -33.9962179, 19.1440924 -33.9953736, 19.144342 -33.9950839, 19.1446914 -33.9955557, 19.14559 -33.995804, 19.146229 -33.9959696, 19.1464886 -33.9964663, 19.1473573 -33.9966318, 19.1476767 -33.9966153, 19.1478265 -33.9963669, 19.1475569 -33.9959696, 19.1471476 -33.9956633, 19.1465385 -33.9953984, 19.1462789 -33.9951253, 19.1454003 -33.9946865, 19.1453205 -33.994463, 19.1456252 -33.9938548, 19.1459595 -33.9939167, 19.1465984 -33.9940243, 19.1467083 -33.9939333, 19.1466084 -33.9937842, 19.1461691 -33.9935442, 19.1459495 -33.9932958, 19.1459794 -33.9929978, 19.1461691 -33.9926502, 19.1466983 -33.9923356, 19.1469479 -33.9921949, 19.1469379 -33.9917147, 19.1475816 -33.9889109, 19.1478165 -33.9880723, 19.1478964 -33.9875259, 19.1477067 -33.9869381, 19.1478165 -33.9866815, 19.1484156 -33.9870706, 19.1490845 -33.987203, 19.1498134 -33.9872279, 19.1509117 -33.987261, 19.15199 -33.9870374, 19.152589 -33.9867725, 19.1531681 -33.9867229, 19.1532979 -33.9869795, 19.153218 -33.9874679, 19.1530483 -33.9879564, 19.1529257 -33.9882924, 19.1531182 -33.9883786, 19.1541266 -33.9888008, 19.1547656 -33.9891154, 19.1557441 -33.9893058, 19.1568623 -33.9897031, 19.1577609 -33.9896286, 19.1584797 -33.9895293, 19.1588492 -33.9892395, 19.1592785 -33.989223, 19.1597677 -33.9893058, 19.1599674 -33.9895872, 19.1600672 -33.9897776, 19.1604466 -33.9900591, 19.1605265 -33.9904233, 19.160237 -33.9908786, 19.1602869 -33.9910442, 19.160886 -33.9907959, 19.1613452 -33.9905227, 19.1616547 -33.9902329, 19.1620841 -33.9902992, 19.162773 -33.9908538, 19.1632123 -33.9914498, 19.1641908 -33.9922114, 19.1647798 -33.9928406, 19.1651792 -33.994074, 19.1655886 -33.9948273, 19.1661377 -33.995713, 19.166557 -33.9964414, 19.167186 -33.9979314, 19.1680647 -33.9986184, 19.1687236 -33.9992641, 19.1693626 -33.9996779, 19.1693726 -34.0001166, 19.1688634 -34.0005885, 19.1695024 -34.0008368, 19.1701114 -34.0011844, 19.1708203 -34.0013169, 19.1717132 -34.0019868, 19.1728871 -34.0026081, 19.1742349 -34.0035931, 19.1753132 -34.0042966, 19.1763117 -34.0050498, 19.1775996 -34.0058195, 19.1791272 -34.0061754, 19.1804552 -34.006192, 19.1814935 -34.0068872, 19.1826417 -34.0069866, 19.1842192 -34.007119, 19.1855871 -34.0066638, 19.1869949 -34.0062251, 19.1889817 -34.0058361, 19.1904295 -34.0061589, 19.1918173 -34.0067631, 19.1931452 -34.007508, 19.1946728 -34.0077563, 19.1959607 -34.0080873, 19.1967295 -34.0084432, 19.1973785 -34.0091798, 19.1978378 -34.0096019, 19.1982072 -34.0097675, 19.1987963 -34.0097923, 19.1993354 -34.0097509, 19.1999944 -34.010024, 19.2005435 -34.0104875, 19.2010727 -34.0114227, 19.2014421 -34.0124407, 19.2019913 -34.0129042, 19.20281 -34.0136821, 19.2036287 -34.0144766, 19.2042178 -34.0150973, 19.2050764 -34.0162559, 19.2055457 -34.0175469, 19.2060748 -34.019202, 19.2069035 -34.0203275, 19.2073029 -34.0205096, 19.2079419 -34.0208157, 19.2080917 -34.0214612, 19.2082514 -34.0221894, 19.2082015 -34.0226942, 19.2083812 -34.0231907, 19.2087706 -34.0232652, 19.20917 -34.0246388, 19.2092299 -34.025425, 19.2093796 -34.0260539, 19.209759 -34.0262442, 19.2102563 -34.0264727, 19.2107674 -34.0267076, 19.211077 -34.0267241, 19.2117259 -34.0264924, 19.2120454 -34.0266745, 19.2126045 -34.0271958, 19.2125746 -34.0276426, 19.2131237 -34.0277253, 19.213573 -34.0277171, 19.2138426 -34.0277088, 19.2151041 -34.0272654, 19.2166282 -34.0269558, 19.2183056 -34.0267655, 19.2195536 -34.0268731, 19.2204822 -34.0263931, 19.2217601 -34.0266331, 19.2230082 -34.0265503, 19.2241281 -34.0261036, 19.2256105 -34.0250925, 19.2263165 -34.0250691, 19.2269237 -34.0248116, 19.2268248 -34.024168, 19.2272484 -34.024051, 19.2280815 -34.024285, 19.2291122 -34.025151, 19.2300724 -34.0261106, 19.2310043 -34.0271404, 19.2317244 -34.0278191, 19.2328637 -34.028387, 19.2339977 -34.0286383, 19.2350709 -34.0285898, 19.2374195 -34.0280819, 19.2392267 -34.0278108, 19.2403423 -34.0271575, 19.2412424 -34.0267311, 19.2422062 -34.0264553, 19.2428698 -34.0260691, 19.2437121 -34.0251068, 19.2442677 -34.0242904, 19.2444058 -34.0240654, 19.2444507 -34.0238503, 19.2443558 -34.0235979, 19.244261 -34.0231055, 19.244226 -34.022849, 19.2438417 -34.0228987, 19.2435571 -34.0227042, 19.2434622 -34.0224973, 19.2434023 -34.0221663, 19.2433724 -34.0219677, 19.2435321 -34.0219139, 19.2438067 -34.0219801, 19.2437168 -34.0218229, 19.2437119 -34.0215498, 19.2438616 -34.0210574, 19.2442111 -34.0206354, 19.2445955 -34.0201844, 19.2448251 -34.0196258, 19.2451146 -34.0194768, 19.245519 -34.0194686, 19.2460033 -34.0193486, 19.2463627 -34.0191086, 19.2465224 -34.0188562, 19.2468369 -34.018641, 19.2472163 -34.0184258, 19.2475209 -34.0180948, 19.2476956 -34.0178052, 19.2480251 -34.0176107, 19.2483995 -34.0176769, 19.2488837 -34.0177266, 19.2492432 -34.0180038, 19.2494179 -34.0181941, 19.249358 -34.0183679, 19.2489137 -34.0186162, 19.2488138 -34.0189927, 19.2485542 -34.0198823, 19.2481848 -34.0209292, 19.2481549 -34.0215333, 19.2481998 -34.0221663, 19.2484688 -34.0225766, 19.2491108 -34.0232722, 19.2496389 -34.0239992, 19.2502404 -34.0241031, 19.2508827 -34.0239879, 19.2513982 -34.0235999, 19.251648 -34.0231923, 19.2522759 -34.0226912, 19.2529655 -34.022453, 19.2536998 -34.0224062, 19.2546197 -34.0225241, 19.2552247 -34.0223711, 19.2563252 -34.0218431, 19.2573568 -34.0212827, 19.2578651 -34.0209434, 19.2581758 -34.0205455, 19.258233 -34.0200414, 19.2582774 -34.0180161, 19.2600337 -34.017519, 19.2606327 -34.0173742, 19.2612288 -34.0170745, 19.2614065 -34.0168983, 19.2621104 -34.0162818, 19.2626196 -34.015748, 19.2629391 -34.0151977, 19.2631987 -34.0146804, 19.2633684 -34.0142335, 19.2638177 -34.013857, 19.2646414 -34.0136873, 19.2652754 -34.013588, 19.2661241 -34.0129383, 19.2664985 -34.0126238, 19.2670127 -34.0130045, 19.2672922 -34.013439, 19.2677016 -34.0142501, 19.2678064 -34.0150611, 19.2677116 -34.016137, 19.2676667 -34.0170597, 19.2671275 -34.0175314, 19.2666083 -34.0182142, 19.2660242 -34.0191741, 19.2656798 -34.0201093, 19.2656398 -34.0206803, 19.2656898 -34.0211147, 19.2657946 -34.0215161, 19.2660784 -34.0217952, 19.2662788 -34.0217974, 19.266838 -34.0217767, 19.2670676 -34.0220912, 19.2671075 -34.0225629, 19.2666083 -34.0230718, 19.2660974 -34.0232751, 19.2656025 -34.0233942, 19.2646522 -34.0235212, 19.264092 -34.0237373, 19.2636812 -34.0240674, 19.2630942 -34.0246197, 19.2625885 -34.0254953, 19.2621556 -34.0261094, 19.2615332 -34.0267149, 19.2630014 -34.0277173, 19.2655497 -34.0278031, 19.2679265 -34.0274586, 19.2705924 -34.0263075, 19.2734105 -34.0262249, 19.275076 -34.0251286, 19.2764492 -34.0253959, 19.2769933 -34.02332, 19.2782073 -34.0224801, 19.278444 -34.0213299, 19.2805759 -34.0198308, 19.2826753 -34.0191536, 19.2856243 -34.0202793, 19.2872994 -34.0215368, 19.2880843 -34.0232801, 19.2879351 -34.0265412, 19.2880747 -34.0272675, 19.2879335 -34.0277824, 19.2876794 -34.0281218, 19.2875241 -34.0290462, 19.28751 -34.0301462, 19.2873123 -34.0314685, 19.2871428 -34.0326854, 19.2871428 -34.0344991, 19.2871005 -34.0357746, 19.2869549 -34.03631, 19.2866486 -34.0374361, 19.2863662 -34.0375648, 19.2860415 -34.0378339, 19.2858862 -34.0382785, 19.285985 -34.03877, 19.2862956 -34.0391561, 19.2859003 -34.0401623, 19.2852508 -34.0402559, 19.284573 -34.0406069, 19.2841353 -34.041426, 19.283514 -34.0419642, 19.2833305 -34.0425843, 19.2831045 -34.0433448, 19.2830117 -34.0436183, 19.2828504 -34.0440936, 19.2829351 -34.045556, 19.2830622 -34.0465973, 19.2833163 -34.0471706, 19.2831045 -34.0481884, 19.2830395 -34.0495123, 19.2831044 -34.0508028, 19.283324 -34.0513405, 19.2834838 -34.0518203, 19.2836935 -34.0525235, 19.2837234 -34.0532597, 19.2837833 -34.0537395, 19.2841527 -34.0542193, 19.2845621 -34.0550878, 19.284602 -34.0555676, 19.2843724 -34.0559398, 19.284013 -34.0564527, 19.2834039 -34.0568084, 19.2829946 -34.0569986, 19.2828448 -34.0573378, 19.282735 -34.0579333, 19.2828748 -34.0588515, 19.2832342 -34.0596538, 19.283334 -34.0603156, 19.2835637 -34.0608863, 19.2838432 -34.0621104, 19.2839331 -34.0624992, 19.2841827 -34.0626315, 19.2844323 -34.0626067, 19.2845421 -34.0624578, 19.2848616 -34.0623834, 19.2855206 -34.0623917, 19.2859837 -34.06248, 19.2863992 -34.0626398, 19.2868185 -34.0628052, 19.2864391 -34.063012, 19.2860198 -34.063194, 19.2856404 -34.0634256, 19.2854207 -34.0638391, 19.2851911 -34.064261, 19.2853009 -34.0648399, 19.2854807 -34.0655512, 19.2853409 -34.0657994, 19.2851112 -34.0662129, 19.2851811 -34.0666513, 19.2856404 -34.0671971, 19.2857439 -34.0676776, 19.2859799 -34.0681731, 19.2868685 -34.0691159, 19.2873078 -34.069455, 19.2881435 -34.0694929, 19.2895878 -34.0700898, 19.2912201 -34.0709553, 19.2920209 -34.0711003, 19.2924402 -34.0712988, 19.2926892 -34.0716011, 19.2928356 -34.0721622, 19.2926199 -34.0725393, 19.2922937 -34.0734127, 19.291961 -34.0741371, 19.2914435 -34.0748784, 19.2911659 -34.0756217, 19.2910577 -34.0756135, 19.2909823 -34.0756271, 19.2909446 -34.0756623, 19.2909302 -34.0757065, 19.2909122 -34.0759447, 19.2909373 -34.0763219, 19.2905677 -34.076685, 19.2900882 -34.0769505, 19.2890855 -34.077496, 19.2879897 -34.0781153, 19.2873968 -34.0783823, 19.2868376 -34.0785625, 19.2864278 -34.0786517, 19.2860655 -34.0786907, 19.2856502 -34.0786633, 19.2853741 -34.0785663, 19.284921 -34.0783679, 19.2842422 -34.0780512, 19.2837534 -34.0777831, 19.2826751 -34.077444, 19.2817665 -34.0774192, 19.2805684 -34.0776012, 19.2799494 -34.0778906, 19.27955 -34.0784199, 19.278921 -34.0786183, 19.2780923 -34.0787755, 19.2773934 -34.0788251, 19.2764249 -34.0787672, 19.2756761 -34.0786845, 19.2750071 -34.0786597, 19.274458 -34.0784695, 19.2738789 -34.0786431, 19.2732799 -34.0788085, 19.2728306 -34.0789739, 19.2725909 -34.0792137, 19.2724212 -34.0792551, 19.2724611 -34.0789657, 19.2728106 -34.0785853, 19.2734196 -34.0779981, 19.2745334 -34.0773618, 19.2749672 -34.0772538, 19.2754864 -34.0771132, 19.2758658 -34.0767576, 19.2764149 -34.0765178, 19.2767943 -34.0765261, 19.2770439 -34.0763607, 19.2761753 -34.0757239, 19.2761678 -34.0752703, 19.2756262 -34.0749713, 19.275097 -34.0743428, 19.2745778 -34.0735737, 19.2740786 -34.0731933, 19.2732399 -34.0728459, 19.2726409 -34.0726557, 19.2724711 -34.0724572, 19.2725588 -34.0721523, 19.2726409 -34.0719196, 19.2732898 -34.071655, 19.2737456 -34.0712809, 19.2740387 -34.0710264, 19.2741273 -34.0707619, 19.2740586 -34.0701001, 19.2740686 -34.0694385, 19.2739788 -34.0687272, 19.2738689 -34.0678009, 19.2736792 -34.0666347, 19.2734579 -34.0661955, 19.2732998 -34.0658986, 19.2729604 -34.0654685, 19.2726808 -34.0652535, 19.2720917 -34.0648647, 19.2718022 -34.0644925, 19.2719719 -34.0640624, 19.2721816 -34.0632684, 19.2723214 -34.0624661, 19.2722615 -34.061854, 19.2721117 -34.0610683, 19.2720019 -34.0604975, 19.2718621 -34.0597366, 19.2715549 -34.0591617, 19.2716824 -34.0587109, 19.2718022 -34.0583138, 19.2716025 -34.0577431, 19.271263 -34.0572385, 19.2709435 -34.05704, 19.2705042 -34.0569325, 19.2699351 -34.0569986, 19.2694559 -34.0572137, 19.2688269 -34.057437, 19.268098 -34.0576025, 19.2674191 -34.0575777, 19.2668799 -34.0576521, 19.2663308 -34.0577348, 19.2656618 -34.0576356, 19.2648431 -34.0576438, 19.2637049 -34.0579416, 19.2629262 -34.05838, 19.2622472 -34.0584627, 19.2614585 -34.0586778, 19.2607895 -34.0588929, 19.2602104 -34.0593809, 19.2593019 -34.0597862, 19.2583633 -34.0603652, 19.2579041 -34.0608367, 19.2577343 -34.0612502, 19.2577743 -34.0617713, 19.2578941 -34.0623669, 19.2581137 -34.0627391, 19.2581137 -34.0630037, 19.2575247 -34.0625736, 19.2572151 -34.0623751, 19.2568857 -34.061945, 19.2564963 -34.0615811, 19.256007 -34.061275, 19.2551783 -34.0610186, 19.2543397 -34.0607953, 19.2536308 -34.0606133, 19.2527721 -34.0605306, 19.2521831 -34.060663, 19.2512945 -34.060878, 19.2506655 -34.0610517, 19.2502162 -34.0611013, 19.2498567 -34.0611841, 19.2493575 -34.0613826, 19.2492677 -34.0611179, 19.2499166 -34.0606878, 19.2510548 -34.0601915, 19.2518436 -34.0596621, 19.2524444 -34.0594726, 19.2528121 -34.0591906, 19.2535509 -34.0591162, 19.2541999 -34.0592403, 19.2549687 -34.0591989, 19.2555578 -34.0590087, 19.2559471 -34.0587274, 19.2563365 -34.0582311, 19.2565961 -34.0576852, 19.2568457 -34.05704, 19.256686 -34.0567257, 19.2564563 -34.0565354, 19.2560769 -34.0561053, 19.2558972 -34.0558158, 19.2561568 -34.055609, 19.2564863 -34.0555841, 19.2569655 -34.055609, 19.2574548 -34.0555593, 19.257924 -34.0552946, 19.2582435 -34.0550713, 19.2589046 -34.0543125, 19.2597507 -34.0530957, 19.2606398 -34.0524077, 19.2615184 -34.052143, 19.2623471 -34.0520437, 19.2629561 -34.0520851, 19.2634254 -34.0522257, 19.2640943 -34.0522174, 19.2644438 -34.052052, 19.2651327 -34.0516383, 19.2659015 -34.0511172, 19.2664606 -34.050414, 19.2667002 -34.0500335, 19.2672593 -34.0499839, 19.267439 -34.0500914, 19.2678284 -34.0501328, 19.2682378 -34.0501659, 19.2685773 -34.0497853, 19.2688069 -34.0495123, 19.2683776 -34.049198, 19.2681579 -34.0491732, 19.2677685 -34.0493552, 19.2673093 -34.049744, 19.2665804 -34.0498267, 19.266231 -34.0495785, 19.2658715 -34.0495289, 19.265562 -34.0492145, 19.2652525 -34.0486437, 19.2650129 -34.0481887, 19.2643938 -34.0477089, 19.264294 -34.0472622, 19.2638547 -34.0466831, 19.2633155 -34.0461867, 19.2631857 -34.0457648, 19.2630759 -34.0450202, 19.2630959 -34.0445735, 19.2628263 -34.0443832, 19.262357 -34.0441598, 19.2620975 -34.0438124, 19.261728 -34.0435394, 19.2613387 -34.0435063, 19.2606497 -34.043589, 19.2600906 -34.0436386, 19.2596214 -34.0435642, 19.2589025 -34.043407, 19.2579839 -34.0434815, 19.2567459 -34.0437462, 19.2559272 -34.0439778, 19.2548089 -34.044557, 19.2538604 -34.0450699, 19.2530217 -34.0456573, 19.2520932 -34.0464515, 19.2513144 -34.0469809, 19.2507453 -34.0472456, 19.2503759 -34.0472208, 19.2504158 -34.0462943, 19.2500947 -34.0457366, 19.2494973 -34.0442674, 19.2491079 -34.043771, 19.2482293 -34.0428692, 19.2474605 -34.0423563, 19.2467616 -34.0419261, 19.2461725 -34.0418765, 19.2455835 -34.0418185, 19.2448147 -34.0418516, 19.2442456 -34.0417606, 19.2437963 -34.0412229, 19.2426792 -34.0402909, 19.2420091 -34.0394937, 19.2411504 -34.0389642, 19.2402518 -34.0385836, 19.2393033 -34.038352, 19.2383948 -34.0385174, 19.2377658 -34.0387077, 19.2374962 -34.03908, 19.2372665 -34.0395599, 19.2372166 -34.0398991, 19.2368172 -34.0403211, 19.2364378 -34.0405775, 19.2362581 -34.0403459, 19.235709 -34.0400646, 19.2349602 -34.0399819, 19.2346007 -34.0401225, 19.2337521 -34.0403376, 19.2333327 -34.0403293, 19.2330132 -34.040288, 19.2327437 -34.04047, 19.2324142 -34.0406437, 19.2318151 -34.0407099, 19.231286 -34.040834, 19.2309465 -34.041107, 19.2305172 -34.0413387, 19.2300579 -34.0416448, 19.2297084 -34.0422488, 19.2296785 -34.043043, 19.2296186 -34.0439778, 19.2296186 -34.0450037, 19.2294089 -34.0459468, 19.2291293 -34.0464928, 19.2288398 -34.0469892, 19.2285602 -34.0472622, 19.2285103 -34.0476841, 19.2285003 -34.0480315, 19.2283705 -34.0479902, 19.2282907 -34.0475765, 19.2280759 -34.047103, 19.227442 -34.046013, 19.2268743 -34.045271, 19.225576 -34.0439503, 19.2238788 -34.0430834, 19.2226594 -34.0431388, 19.2209423 -34.0442637, 19.2192748 -34.0462013, 19.2191047 -34.0446819, 19.219066 -34.0407609, 19.2181274 -34.0388553, 19.2157926 -34.0379702, 19.2139879 -34.0374727, 19.213122 -34.0380984, 19.2120463 -34.0389699, 19.2117563 -34.0408065, 19.2106607 -34.0409175, 19.209541 -34.0398983, 19.2076828 -34.0402841, 19.206598 -34.0408367, 19.2056066 -34.0415185, 19.2042101 -34.0426877, 19.2040174 -34.0419818, 19.2059287 -34.0392432, 19.2080949 -34.0357309, 19.2081945 -34.0343646, 19.2079447 -34.0328966, 19.2080578 -34.0315665, 19.2074543 -34.0300751, 19.2064335 -34.0289585, 19.2063462 -34.0279302, 19.2058989 -34.0279359, 19.20519 -34.02782, 19.2042615 -34.0281676, 19.2029635 -34.028391, 19.2014459 -34.0283331, 19.1992793 -34.0280765, 19.1982609 -34.0281758, 19.1972126 -34.0280269, 19.1959546 -34.0280103, 19.1959845 -34.0274973, 19.1965936 -34.0273898, 19.1965436 -34.0272656, 19.1960644 -34.0272491, 19.1958847 -34.0268684, 19.1948663 -34.0259168, 19.1945767 -34.0248659, 19.1942173 -34.0238316, 19.193768 -34.0229213, 19.1937281 -34.022491, 19.193748 -34.022069, 19.1931689 -34.0207698, 19.1927895 -34.0199091, 19.1922903 -34.0185188, 19.1915618 -34.0175299, 19.1907827 -34.0163672, 19.1904832 -34.0161023, 19.1899041 -34.0156306, 19.1891153 -34.0152582, 19.1881568 -34.015101, 19.1878473 -34.0152748, 19.1872383 -34.015283, 19.1865094 -34.0153658, 19.1856308 -34.0148858, 19.1852314 -34.0146375, 19.184253 -34.0145051, 19.1839035 -34.0144968, 19.183624 -34.0146292, 19.1835042 -34.0147699, 19.183604 -34.0148858, 19.1832346 -34.0148941, 19.1828652 -34.0150761, 19.1827254 -34.0154403, 19.1824957 -34.0157961, 19.1824458 -34.0161189, 19.182356 -34.0162761, 19.1819466 -34.0162927, 19.1814574 -34.0163258, 19.1809582 -34.0166072, 19.1805089 -34.0165741, 19.1796103 -34.0168803, 19.1794006 -34.0171947, 19.1786618 -34.0174016, 19.1777432 -34.0181382, 19.1772141 -34.0185685, 19.1769345 -34.0190236, 19.1769045 -34.0192554, 19.1769445 -34.0197022, 19.1769944 -34.019876, 19.1771342 -34.0199257, 19.1770044 -34.0201739, 19.1764053 -34.0203643, 19.175996 -34.0206787, 19.1758262 -34.0210677, 19.1757963 -34.0216056, 19.1757564 -34.0220276, 19.1757064 -34.0224248, 19.1752671 -34.0225986, 19.1739392 -34.0232689, 19.1731205 -34.0241295, 19.1724216 -34.0250314, 19.1723917 -34.0258093, 19.1724216 -34.026703, 19.1727012 -34.0275635, 19.1729608 -34.0292102, 19.1729608 -34.0302776, 19.172801 -34.0314111, 19.1727211 -34.0322055, 19.172801 -34.0330081, 19.172811 -34.0335542, 19.1724216 -34.0341664, 19.1716328 -34.0350849, 19.1705845 -34.0362763, 19.1698357 -34.0366652, 19.1689271 -34.0373602, 19.1679986 -34.0379228, 19.1665808 -34.0386177, 19.1653727 -34.0390231, 19.1648036 -34.0390397, 19.1645141 -34.0392796, 19.1643343 -34.0398174, 19.1636754 -34.0405206, 19.1623974 -34.0415879, 19.1618083 -34.0418444, 19.16072 -34.0429199, 19.1587931 -34.0449964, 19.1573154 -34.0462126, 19.1565266 -34.0465683, 19.1563469 -34.0469571, 19.1559675 -34.0473542, 19.1548892 -34.0473542, 19.1540705 -34.0470812, 19.1532618 -34.0466676, 19.1521835 -34.0461629, 19.1519339 -34.0461381, 19.1516044 -34.0463946, 19.1506359 -34.0463946, 19.1494478 -34.0464194, 19.1488127 -34.0463517, 19.1487109 -34.0463587, 19.1485838 -34.0465693, 19.1486332 -34.0468851, 19.1484003 -34.047201, 19.1480402 -34.0476456, 19.1476378 -34.0479615, 19.147433 -34.048213, 19.1472707 -34.048874, 19.147193 -34.0492952, 19.1471859 -34.0495877, 19.1473342 -34.0499854, 19.1475672 -34.0502955, 19.1477013 -34.050506, 19.1476731 -34.0509623, 19.1478708 -34.0516233, 19.1478496 -34.0521088, 19.1479131 -34.0525884, 19.1474966 -34.0528166, 19.1465294 -34.052799, 19.1456116 -34.0526879, 19.1450256 -34.0524773, 19.1443973 -34.0520737, 19.1436136 -34.0519216, 19.1427029 -34.0516759, 19.1417074 -34.0515063, 19.1410579 -34.0514595, 19.1405143 -34.0515005, 19.140006 -34.0514303, 19.1395894 -34.0512372, 19.1389046 -34.0511436, 19.1383186 -34.0512021, 19.1375985 -34.0514654, 19.1372455 -34.0517286, 19.1368784 -34.0521439, 19.1367372 -34.0525358, 19.1366384 -34.0528634, 19.1365042 -34.0530973, 19.136003 -34.0530739, 19.1351558 -34.0531149, 19.1343086 -34.0533547, 19.1334049 -34.0535126, 19.1328401 -34.0535009, 19.1324306 -34.0534249, 19.1309692 -34.0535945, 19.1295219 -34.0539513, 19.12871 -34.0535419, 19.1280323 -34.0531968, 19.1267544 -34.0529745, 19.1258931 -34.0527932, 19.125413 -34.0524539, 19.1249753 -34.0522199)) diff --git a/features/eolearn/features/feature_manipulation.py b/features/eolearn/features/feature_manipulation.py index 60744e4e6..872a411d3 100644 --- a/features/eolearn/features/feature_manipulation.py +++ b/features/eolearn/features/feature_manipulation.py @@ -61,7 +61,7 @@ def _get_filtered_indices(self, feature_data: Iterable) -> List[int]: @staticmethod def _filter_vector_feature(gdf: GeoDataFrame, good_idxs: List[int], timestamps: List[dt.datetime]) -> GeoDataFrame: """Filters rows that don't match with the timestamps that will be kept.""" - timestamps_to_keep = set(timestamps[idx] for idx in good_idxs) + timestamps_to_keep = {timestamps[idx] for idx in good_idxs} return gdf[gdf.TIMESTAMP.isin(timestamps_to_keep)] def execute(self, eopatch: EOPatch) -> EOPatch: diff --git a/features/eolearn/features/radiometric_normalization.py b/features/eolearn/features/radiometric_normalization.py index ac8d4152e..580d80482 100644 --- a/features/eolearn/features/radiometric_normalization.py +++ b/features/eolearn/features/radiometric_normalization.py @@ -8,7 +8,7 @@ This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree. """ -from abc import ABCMeta +from abc import ABCMeta, abstractmethod import numpy as np @@ -130,6 +130,7 @@ def _geoville_index_by_percentile(self, data, percentile): idx = np.where(valid_obs == 0, self.max_index, ind_tmp[ind, y_val, x_val]) return idx + @abstractmethod def _get_reference_band(self, data): """Extract reference band from input 4D data according to compositing method @@ -137,7 +138,6 @@ def _get_reference_band(self, data): :type data: numpy array :return: 3D array containing reference band according to compositing method """ - raise NotImplementedError def _get_indices(self, data): """Compute indices along temporal dimension corresponding to the sought percentile diff --git a/features/eolearn/tests/test_features_utils.py b/features/eolearn/tests/test_features_utils.py index 27db4de83..39e01f7a6 100644 --- a/features/eolearn/tests/test_features_utils.py +++ b/features/eolearn/tests/test_features_utils.py @@ -14,7 +14,7 @@ def test_spatially_resize_image_new_size( method: ResizeMethod, library: ResizeLib, dtype: Union[np.dtype, type], new_size: Tuple[int, int] ): """Test that all methods and backends are able to downscale and upscale images of various dtypes.""" - if library is ResizeLib.CV2: + if library is ResizeLib.CV2: # noqa: SIM102 if np.issubdtype(dtype, np.integer) and method is ResizeMethod.CUBIC or dtype == bool: return diff --git a/features/setup.py b/features/setup.py index 267dcf4a7..8e7b442d2 100644 --- a/features/setup.py +++ b/features/setup.py @@ -13,16 +13,17 @@ def get_long_description(): def parse_requirements(file): - return sorted( - set(line.partition("#")[0].strip() for line in open(os.path.join(os.path.dirname(__file__), file))) - set("") - ) + with open(os.path.join(os.path.dirname(__file__), file)) as requirements_file: + return sorted({line.partition("#")[0].strip() for line in requirements_file} - set("")) def get_version(): - for line in open(os.path.join(os.path.dirname(__file__), "eolearn", "features", "__init__.py")): - if line.find("__version__") >= 0: - version = line.split("=")[1].strip() - version = version.strip('"').strip("'") + path = os.path.join(os.path.dirname(__file__), "eolearn", "features", "__init__.py") + with open(path) as version_file: + for line in version_file: + if line.find("__version__") >= 0: + version = line.split("=")[1].strip() + version = version.strip('"').strip("'") return version diff --git a/geometry/setup.py b/geometry/setup.py index c2e4fbf96..6aa324636 100644 --- a/geometry/setup.py +++ b/geometry/setup.py @@ -13,16 +13,17 @@ def get_long_description(): def parse_requirements(file): - return sorted( - set(line.partition("#")[0].strip() for line in open(os.path.join(os.path.dirname(__file__), file))) - set("") - ) + with open(os.path.join(os.path.dirname(__file__), file)) as requirements_file: + return sorted({line.partition("#")[0].strip() for line in requirements_file} - set("")) def get_version(): - for line in open(os.path.join(os.path.dirname(__file__), "eolearn", "geometry", "__init__.py")): - if line.find("__version__") >= 0: - version = line.split("=")[1].strip() - version = version.strip('"').strip("'") + path = os.path.join(os.path.dirname(__file__), "eolearn", "geometry", "__init__.py") + with open(path) as version_file: + for line in version_file: + if line.find("__version__") >= 0: + version = line.split("=")[1].strip() + version = version.strip('"').strip("'") return version diff --git a/io/eolearn/io/extra/meteoblue.py b/io/eolearn/io/extra/meteoblue.py index 0c6de6db1..cfa576b69 100644 --- a/io/eolearn/io/extra/meteoblue.py +++ b/io/eolearn/io/extra/meteoblue.py @@ -42,7 +42,7 @@ def __init__( apikey: str, query: dict, units: dict = None, - time_difference: dt.timedelta = dt.timedelta(minutes=30), + time_difference: dt.timedelta = dt.timedelta(minutes=30), # noqa: B008 cache_folder: Optional[str] = None, cache_max_age: int = 604800, ): @@ -240,7 +240,7 @@ def meteoblue_to_numpy(result) -> np.ndarray: # Therefore we have to first transpose each code individually and then transpose everything again def map_code(code): """Transpose a single code""" - code_data = np.array(list(map(lambda t: t.data, code.timeIntervals))) + code_data = np.array([t.data for t in code.timeIntervals]) code_n_timesteps = code_data.size // n_locations // n_time_intervals code_data = code_data.reshape((n_time_intervals, geo_ny, geo_nx, code_n_timesteps)) diff --git a/io/eolearn/io/geometry_io.py b/io/eolearn/io/geometry_io.py index ae4558e5c..0c01bf030 100644 --- a/io/eolearn/io/geometry_io.py +++ b/io/eolearn/io/geometry_io.py @@ -169,15 +169,14 @@ def _load_vector_data(self, bbox): bbox_bounds = bbox.transform_bounds(self.dataset_crs).geometry.bounds if bbox else None if self.full_path.startswith("s3://"): - with fiona.Env(session=self.aws_session): - with fiona.open(self.full_path, **self.fiona_kwargs) as features: - feature_iter = features if bbox_bounds is None else features.filter(bbox=bbox_bounds) - - return gpd.GeoDataFrame.from_features( - feature_iter, - columns=list(features.schema["properties"]) + ["geometry"], - crs=self.dataset_crs.pyproj_crs(), - ) + with fiona.Env(session=self.aws_session), fiona.open(self.full_path, **self.fiona_kwargs) as features: + feature_iter = features if bbox_bounds is None else features.filter(bbox=bbox_bounds) + + return gpd.GeoDataFrame.from_features( + feature_iter, + columns=list(features.schema["properties"]) + ["geometry"], + crs=self.dataset_crs.pyproj_crs(), + ) return gpd.read_file(self.full_path, bbox=bbox_bounds, **self.fiona_kwargs) diff --git a/io/eolearn/io/raster_io.py b/io/eolearn/io/raster_io.py index ceef9917c..cc89a84c1 100644 --- a/io/eolearn/io/raster_io.py +++ b/io/eolearn/io/raster_io.py @@ -40,7 +40,7 @@ LOGGER = logging.getLogger(__name__) -class BaseRasterIoTask(IOTask, metaclass=ABCMeta): +class BaseRasterIoTask(IOTask, metaclass=ABCMeta): # noqa: B024 """Base abstract class for raster IO tasks""" def __init__( @@ -306,34 +306,33 @@ def _export_tiff( src_transform: Affine, ) -> None: """Export an EOPatch feature to tiff based on input channel range.""" - with rasterio.Env(): - with filesystem.openbin(path, "w") as file_handle: - with rasterio.open( - file_handle, - "w", - driver="GTiff", - width=dst_width, - height=dst_height, - count=channel_count, - dtype=image_array.dtype, - nodata=self.no_data_value, - transform=dst_transform, - crs=dst_crs, - compress=self.compress, - ) as dst: - if dst_crs == src_crs: - dst.write(image_array) - else: - for idx in range(channel_count): - rasterio.warp.reproject( - source=image_array[idx, ...], - destination=rasterio.band(dst, idx + 1), - src_transform=src_transform, - src_crs=src_crs, - dst_transform=dst_transform, - dst_crs=dst_crs, - resampling=rasterio.warp.Resampling.nearest, - ) + with rasterio.Env(), filesystem.openbin(path, "w") as file_handle: # noqa: SIM117 + with rasterio.open( + file_handle, + "w", + driver="GTiff", + width=dst_width, + height=dst_height, + count=channel_count, + dtype=image_array.dtype, + nodata=self.no_data_value, + transform=dst_transform, + crs=dst_crs, + compress=self.compress, + ) as dst: + if dst_crs == src_crs: + dst.write(image_array) + else: + for idx in range(channel_count): + rasterio.warp.reproject( + source=image_array[idx, ...], + destination=rasterio.band(dst, idx + 1), + src_transform=src_transform, + src_crs=src_crs, + dst_transform=dst_transform, + dst_crs=dst_crs, + resampling=rasterio.warp.Resampling.nearest, + ) def execute(self, eopatch: EOPatch, *, filename: Union[str, List[str], None] = "") -> EOPatch: """Execute method @@ -465,9 +464,8 @@ def _load_from_image(self, path: str, filesystem: FS, bbox: Optional[BBox]) -> T full_path = get_full_path(filesystem, path) return self._read_image(full_path, bbox) - with rasterio.Env(): - with filesystem.openbin(path, "r") as file_handle: - return self._read_image(file_handle, bbox) + with rasterio.Env(), filesystem.openbin(path, "r") as file_handle: + return self._read_image(file_handle, bbox) def _read_image(self, file_object: Union[str, BinaryIO], bbox: Optional[BBox]) -> Tuple[np.ndarray, Optional[BBox]]: """Reads data from the image.""" diff --git a/io/eolearn/io/sentinelhub_process.py b/io/eolearn/io/sentinelhub_process.py index ecb5b2dde..8e9285a2f 100644 --- a/io/eolearn/io/sentinelhub_process.py +++ b/io/eolearn/io/sentinelhub_process.py @@ -267,7 +267,7 @@ def _parse_and_validate_features(self, features): allowed_features = FeatureTypeSet.RASTER_TYPES.union({FeatureType.META_INFO}) _features = self.parse_renamed_features(features, allowed_feature_types=allowed_features) - ftr_data_types = set(ft for ft, _, _ in _features if not ft.is_meta()) + ftr_data_types = {ft for ft, _, _ in _features if not ft.is_meta()} if all(ft.is_timeless() for ft in ftr_data_types) or all(ft.is_temporal() for ft in ftr_data_types): return _features @@ -706,7 +706,7 @@ def get_available_timestamps( data_collection: DataCollection, *, time_interval: Optional[Tuple[dt.datetime, dt.datetime]] = None, - time_difference: dt.timedelta = dt.timedelta(seconds=-1), + time_difference: dt.timedelta = dt.timedelta(seconds=-1), # noqa: B008 maxcc: Optional[float] = None, config: Optional[SHConfig] = None, ) -> List[dt.datetime]: diff --git a/io/requirements.txt b/io/requirements.txt index 10cdf700b..3090aaac5 100644 --- a/io/requirements.txt +++ b/io/requirements.txt @@ -1,8 +1,8 @@ +affine boto3 eo-learn-core fiona>=1.8.18 geopandas>=0.8.1 -affine rasterio>=1.2.7 rtree sentinelhub>=3.5.1 diff --git a/io/setup.py b/io/setup.py index 763e24e28..ecaed890b 100644 --- a/io/setup.py +++ b/io/setup.py @@ -13,16 +13,17 @@ def get_long_description(): def parse_requirements(file): - return sorted( - set(line.partition("#")[0].strip() for line in open(os.path.join(os.path.dirname(__file__), file))) - set("") - ) + with open(os.path.join(os.path.dirname(__file__), file)) as requirements_file: + return sorted({line.partition("#")[0].strip() for line in requirements_file} - set("")) def get_version(): - for line in open(os.path.join(os.path.dirname(__file__), "eolearn", "io", "__init__.py")): - if line.find("__version__") >= 0: - version = line.split("=")[1].strip() - version = version.strip('"').strip("'") + path = os.path.join(os.path.dirname(__file__), "eolearn", "io", "__init__.py") + with open(path) as version_file: + for line in version_file: + if line.find("__version__") >= 0: + version = line.split("=")[1].strip() + version = version.strip('"').strip("'") return version diff --git a/mask/setup.py b/mask/setup.py index 61247654c..115f06380 100644 --- a/mask/setup.py +++ b/mask/setup.py @@ -13,16 +13,17 @@ def get_long_description(): def parse_requirements(file): - return sorted( - set(line.partition("#")[0].strip() for line in open(os.path.join(os.path.dirname(__file__), file))) - set("") - ) + with open(os.path.join(os.path.dirname(__file__), file)) as requirements_file: + return sorted({line.partition("#")[0].strip() for line in requirements_file} - set("")) def get_version(): - for line in open(os.path.join(os.path.dirname(__file__), "eolearn", "mask", "__init__.py")): - if line.find("__version__") >= 0: - version = line.split("=")[1].strip() - version = version.strip('"').strip("'") + path = os.path.join(os.path.dirname(__file__), "eolearn", "mask", "__init__.py") + with open(path) as version_file: + for line in version_file: + if line.find("__version__") >= 0: + version = line.split("=")[1].strip() + version = version.strip('"').strip("'") return version diff --git a/ml_tools/eolearn/ml_tools/sampling.py b/ml_tools/eolearn/ml_tools/sampling.py index 5427df16f..9553985f3 100644 --- a/ml_tools/eolearn/ml_tools/sampling.py +++ b/ml_tools/eolearn/ml_tools/sampling.py @@ -152,7 +152,7 @@ def get_mask_of_samples(image_shape: Tuple[int, int], row_grid: np.ndarray, colu return mask -class BaseSamplingTask(EOTask, metaclass=ABCMeta): +class BaseSamplingTask(EOTask, metaclass=ABCMeta): # noqa: B024 """A base class for sampling tasks""" def __init__(self, features_to_sample, *, mask_of_samples=None): diff --git a/ml_tools/setup.py b/ml_tools/setup.py index 7c1b8d85d..7c0cf3f8c 100644 --- a/ml_tools/setup.py +++ b/ml_tools/setup.py @@ -13,16 +13,17 @@ def get_long_description(): def parse_requirements(file): - return sorted( - set(line.partition("#")[0].strip() for line in open(os.path.join(os.path.dirname(__file__), file))) - set("") - ) + with open(os.path.join(os.path.dirname(__file__), file)) as requirements_file: + return sorted({line.partition("#")[0].strip() for line in requirements_file} - set("")) def get_version(): - for line in open(os.path.join(os.path.dirname(__file__), "eolearn", "ml_tools", "__init__.py")): - if line.find("__version__") >= 0: - version = line.split("=")[1].strip() - version = version.strip('"').strip("'") + path = os.path.join(os.path.dirname(__file__), "eolearn", "ml_tools", "__init__.py") + with open(path) as version_file: + for line in version_file: + if line.find("__version__") >= 0: + version = line.split("=")[1].strip() + version = version.strip('"').strip("'") return version diff --git a/pyproject.toml b/pyproject.toml index 963674ea7..b8685dab8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,9 @@ flake8 = [ "--extend-ignore=E402" ] +[tool.nbqa.exclude] +flake8 = "examples/core/CoreOverview.ipynb" + [tool.mypy] follow_imports = "normal" ignore_missing_imports = true diff --git a/requirements-dev.txt b/requirements-dev.txt index 250dd1e10..a8f99eefa 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,16 +1,12 @@ -black[jupyter]>=22.1.0 codecov -flake8 hypothesis -isort moto nbval -nbqa pylint>=2.14.0 pytest>=7.0.0 pytest-cov -pytest-mock pytest-lazy-fixture +pytest-mock ray[default] responses twine diff --git a/requirements-docs.txt b/requirements-docs.txt index 2de980e11..369b69a2b 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,9 +1,3 @@ -jupyter -m2r2 -nbsphinx -sphinx_rtd_theme>=1.0.0 -sphinx -jinja2==3.0.3 -e ./core -e ./coregistration @@ -13,3 +7,9 @@ jinja2==3.0.3 -e ./mask -e ./ml_tools -e ./visualization +jinja2==3.0.3 +jupyter +m2r2 +nbsphinx +sphinx +sphinx_rtd_theme>=1.0.0 diff --git a/setup.py b/setup.py index 879fed238..15019a362 100644 --- a/setup.py +++ b/setup.py @@ -14,9 +14,8 @@ def get_long_description(): def parse_requirements(file): - return sorted( - set(line.partition("#")[0].strip() for line in open(os.path.join(os.path.dirname(__file__), file))) - set("") - ) + with open(os.path.join(os.path.dirname(__file__), file)) as requirements_file: + return sorted({line.partition("#")[0].strip() for line in requirements_file} - set("")) setup( diff --git a/visualization/setup.py b/visualization/setup.py index 3300e9d06..48ce5427e 100644 --- a/visualization/setup.py +++ b/visualization/setup.py @@ -13,16 +13,17 @@ def get_long_description(): def parse_requirements(file): - return sorted( - set(line.partition("#")[0].strip() for line in open(os.path.join(os.path.dirname(__file__), file))) - set("") - ) + with open(os.path.join(os.path.dirname(__file__), file)) as requirements_file: + return sorted({line.partition("#")[0].strip() for line in requirements_file} - set("")) def get_version(): - for line in open(os.path.join(os.path.dirname(__file__), "eolearn", "visualization", "__init__.py")): - if line.find("__version__") >= 0: - version = line.split("=")[1].strip() - version = version.strip('"').strip("'") + path = os.path.join(os.path.dirname(__file__), "eolearn", "visualization", "__init__.py") + with open(path) as version_file: + for line in version_file: + if line.find("__version__") >= 0: + version = line.split("=")[1].strip() + version = version.strip('"').strip("'") return version From 55e2065accde5b17ec077e6365104e61589eff0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Thu, 29 Sep 2022 11:30:42 +0200 Subject: [PATCH 08/24] Make FeatureIO.save a class method instead (#474) --- core/eolearn/core/eodata_io.py | 86 +++++++++++++++++----------------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/core/eolearn/core/eodata_io.py b/core/eolearn/core/eodata_io.py index 31861d4e0..3818def3c 100644 --- a/core/eolearn/core/eodata_io.py +++ b/core/eolearn/core/eodata_io.py @@ -17,7 +17,7 @@ import warnings from abc import ABCMeta, abstractmethod from collections import defaultdict -from typing import Any, BinaryIO, Generic, Iterable, Optional, Type, TypeVar, Union +from typing import Any, BinaryIO, Generic, Iterable, List, Optional, Tuple, Type, TypeVar, Union import fs import fs.move @@ -67,14 +67,13 @@ def save_eopatch( else: _check_letter_case_collisions(eopatch_features, []) - features_to_save = [] - for ftype, fname, path in eopatch_features: - # the paths here do not have file extensions, so FeatureIO needs to be constructed differently - # pylint: disable=protected-access - feature_io = _get_feature_io_constructor(ftype)._from_eopatch_path(path, filesystem, compress_level) + features_to_save: List[Tuple[Type[FeatureIO], Any, FS, str, int]] = [] + for ftype, fname, feature_path in eopatch_features: + # the paths here do not have file extensions, but FeatureIO.save takes care of that + feature_io = _get_feature_io_constructor(ftype) data = eopatch[(ftype, fname)] - features_to_save.append((feature_io, data)) + features_to_save.append((feature_io, data, filesystem, feature_path, compress_level)) # Cannot be done before due to lazy loading (this would delete the files before the data is loaded) if overwrite_permission is OverwritePermission.OVERWRITE_PATCH and patch_exists: @@ -242,7 +241,7 @@ def _to_lowercase(ftype, fname, *_): class FeatureIO(Generic[_T], metaclass=ABCMeta): """A class that handles the saving and loading process of a single feature at a given location.""" - def __init__(self, path: str, filesystem: FS, compress_level: Optional[int] = None): + def __init__(self, path: str, filesystem: FS): """ :param path: A path in the filesystem :param filesystem: A filesystem object @@ -250,27 +249,15 @@ def __init__(self, path: str, filesystem: FS, compress_level: Optional[int] = No """ filename = fs.path.basename(path) expected_extension = f".{self.get_file_format().extension}" - if compress_level is None: - # Infer compression level if not provided - compress_level = 1 if path.endswith(f".{MimeType.GZIP.extension}") else 0 - if compress_level: - expected_extension += f".{MimeType.GZIP.extension}" - if not filename.endswith(expected_extension): + compressed_extension = expected_extension + f".{MimeType.GZIP.extension}" + if not filename.endswith((expected_extension, compressed_extension)): raise ValueError(f"FeatureIO expects a filepath with the {expected_extension} file extension, got {path}") self.path = path self.filesystem = filesystem - self.compress_level = compress_level self.loaded_value: Optional[_T] = None - @classmethod - def _from_eopatch_path(cls: Type[Self], eopatch_path: str, filesystem: FS, compress_level: int = 0) -> Self: - """Constructor for creating FeatureIO objects directly from paths returned by `walk_eopatch`.""" - gz_extension = ("." + MimeType.GZIP.extension) if compress_level else "" - path = f"{eopatch_path}.{cls.get_file_format().extension}{gz_extension}" - return cls(path, filesystem, compress_level) - @classmethod @abstractmethod def get_file_format(cls) -> MimeType: @@ -299,34 +286,41 @@ def load(self) -> _T: def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> _T: """Loads from a file and decodes content.""" - def save(self, data: _T) -> None: - """Method for saving a feature. + @classmethod + def save(cls, data: _T, filesystem: FS, feature_path: str, compress_level: int = 0) -> None: + """Method for saving a feature. The path is assumed to be filesystem path but without file extensions. + + Example of path is `eopatch/data/NDVI`, which is then used to save `eopatch/data/NDVI.npy.gz`. To minimize the chance of corrupted files (in case of OSFS and TempFS) the file is first written and then moved to correct location. If any exceptions happen during the writing process the file is not moved (and old one not overwritten). """ - - if isinstance(self.filesystem, (OSFS, TempFS)): - with TempFS(temp_dir=self.filesystem.root_path) as tempfs: - self._save(tempfs, data, "tmp_feature") - if fs.__version__ == "2.4.16" and self.filesystem.exists(self.path): # An issue in the fs version - self.filesystem.remove(self.path) - fs.move.move_file(tempfs, "tmp_feature", self.filesystem, self.path) + gz_extension = ("." + MimeType.GZIP.extension) if compress_level else "" + path = f"{feature_path}.{cls.get_file_format().extension}{gz_extension}" + + if isinstance(filesystem, (OSFS, TempFS)): + with TempFS(temp_dir=filesystem.root_path) as tempfs: + cls._save(data, tempfs, "tmp_feature", compress_level) + if fs.__version__ == "2.4.16" and filesystem.exists(path): # An issue in the fs version + filesystem.remove(path) + fs.move.move_file(tempfs, "tmp_feature", filesystem, path) return - self._save(self.filesystem, data, self.path) + cls._save(data, filesystem, path, compress_level) - def _save(self, filesystem: FS, data: _T, path: str) -> None: + @classmethod + def _save(cls, data: _T, filesystem: FS, path: str, compress_level: int) -> None: """Given a filesystem it saves and compresses the data.""" with filesystem.openbin(path, "w") as file: - if self.compress_level == 0: - self._write_to_file(data, file) + if compress_level == 0: + cls._write_to_file(data, file, path) else: - with gzip.GzipFile(fileobj=file, compresslevel=self.compress_level, mode="wb") as gzip_file: - self._write_to_file(data, gzip_file) + with gzip.GzipFile(fileobj=file, compresslevel=compress_level, mode="wb") as gzip_file: + cls._write_to_file(data, gzip_file, path) + @classmethod @abstractmethod - def _write_to_file(self, data: _T, file: Union[BinaryIO, gzip.GzipFile]) -> None: + def _write_to_file(cls, data: _T, file: Union[BinaryIO, gzip.GzipFile], path: str) -> None: """Writes data to a file in the appropriate way.""" @@ -340,7 +334,8 @@ def get_file_format(cls) -> MimeType: def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> np.ndarray: return np.load(file) - def _write_to_file(self, data: np.ndarray, file: Union[BinaryIO, gzip.GzipFile]) -> None: + @classmethod + def _write_to_file(cls, data: np.ndarray, file: Union[BinaryIO, gzip.GzipFile], _: str) -> None: return np.save(file, data) @@ -365,8 +360,9 @@ def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> gpd.GeoDataFr return dataframe - def _write_to_file(self, data: gpd.GeoDataFrame, file: Union[BinaryIO, gzip.GzipFile]) -> None: - layer = fs.path.basename(self.path) + @classmethod + def _write_to_file(cls, data: gpd.GeoDataFrame, file: Union[BinaryIO, gzip.GzipFile], path: str) -> None: + layer = fs.path.basename(path) try: with warnings.catch_warnings(): warnings.filterwarnings( @@ -393,12 +389,13 @@ def get_file_format(cls) -> MimeType: def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> _T: return json.load(file) - def _write_to_file(self, data: _T, file: Union[BinaryIO, gzip.GzipFile]) -> None: + @classmethod + def _write_to_file(cls, data: _T, file: Union[BinaryIO, gzip.GzipFile], path: str) -> None: try: json_data = json.dumps(data, indent=2, default=_jsonify_timestamp) except TypeError as exception: raise TypeError( - f"Failed to serialize when saving JSON file to {self.path}. Make sure that this feature type " + f"Failed to serialize when saving JSON file to {path}. Make sure that this feature type " "contains only JSON serializable Python types before attempting to serialize it." ) from exception @@ -416,7 +413,8 @@ def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> BBox: json_data = json.load(file) return Geometry.from_geojson(json_data).bbox - def _write_to_file(self, data: BBox, file: Union[BinaryIO, gzip.GzipFile]) -> None: + @classmethod + def _write_to_file(cls, data: BBox, file: Union[BinaryIO, gzip.GzipFile], _: str) -> None: json_data = json.dumps(data.geojson, indent=2) file.write(json_data.encode()) From 9da2d42db146f6c5b523dacc5afbb966aa9b5bae Mon Sep 17 00:00:00 2001 From: jgersak <112631680+jgersak@users.noreply.github.com> Date: Thu, 29 Sep 2022 13:52:04 +0200 Subject: [PATCH 09/24] Add Feature io test (#476) * Add Feature io test Tests verifying that FeatureIO subclasses correctly save, load, and lazy-load data. Test cases do not include subfolders, because subfolder management is currently done by the `save_eopatch` function. * Remove not needed import, extract repeated value as a variable, rename helper method and add type annotations. --- core/eolearn/tests/test_eodata_io.py | 57 ++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/core/eolearn/tests/test_eodata_io.py b/core/eolearn/tests/test_eodata_io.py index 304613a18..a51d118cc 100644 --- a/core/eolearn/tests/test_eodata_io.py +++ b/core/eolearn/tests/test_eodata_io.py @@ -9,18 +9,22 @@ import datetime import os import tempfile +from typing import Any, Type import fs +import geopandas as gpd import numpy as np import pytest from fs.errors import CreateFailed, ResourceNotFound from fs.tempfs import TempFS from geopandas import GeoDataFrame from moto import mock_s3 +from shapely.geometry import Point from sentinelhub import CRS, BBox from eolearn.core import EOPatch, FeatureType, LoadTask, OverwritePermission, SaveTask +from eolearn.core.eodata_io import FeatureIO, FeatureIOBBox, FeatureIOGeoDf, FeatureIOJson, FeatureIONumpy FS_LOADERS = [TempFS, pytest.lazy_fixture("create_mocked_s3fs")] @@ -277,3 +281,56 @@ def test_lazy_loading_plus_overwrite_patch(fs_loader, folder_name, eopatch): lazy_eopatch.save(folder_name, filesystem=temp_fs, overwrite_permission=OverwritePermission.OVERWRITE_PATCH) assert temp_fs.exists(fs.path.join(folder_name, "data", "whatever.npy")) assert not temp_fs.exists(fs.path.join(folder_name, "data_timeless", "mask.npy")) + + +def assert_data_equal(data1: Any, data2: Any) -> None: + if isinstance(data1, np.ndarray): + np.testing.assert_array_equal(data1, data2) + elif isinstance(data1, gpd.GeoDataFrame): + assert CRS(data1.crs) == CRS(data2.crs) + gpd.testing.assert_geodataframe_equal(data1, data2, check_crs=False, check_index_type=False, check_dtype=False) + else: + assert data1 == data2 + + +@pytest.mark.parametrize( + "constructor, data", + [ + (FeatureIONumpy, np.zeros(20)), + (FeatureIONumpy, np.zeros((2, 3, 3, 2), dtype=np.int16)), + (FeatureIOBBox, BBox((1, 2, 3, 4), CRS.WGS84)), + (FeatureIOGeoDf, gpd.GeoDataFrame({"col1": ["name1"], "geometry": [Point(1, 2)]}, crs="EPSG:4326")), + (FeatureIOGeoDf, gpd.GeoDataFrame({"col1": ["name1"], "geometry": [Point(1, 2)]}, crs="EPSG:32733")), + ( + FeatureIOGeoDf, + gpd.GeoDataFrame( + { + "values": [1, 2], + "TIMESTAMP": [datetime.datetime(2017, 1, 1, 10, 4, 7), datetime.datetime(2017, 1, 4, 10, 14, 5)], + "geometry": [Point(1, 2), Point(2, 1)], + }, + crs="EPSG:4326", + ), + ), + (FeatureIOJson, {"test": "test1", "test3": {"test": "test1"}}), + ], +) +@pytest.mark.parametrize("compress_level", [0, 1]) +def test_feature_io(constructor: Type[FeatureIO], data: Any, compress_level: int) -> None: + """ + Tests verifying that FeatureIO subclasses correctly save, load, and lazy-load data. + Test cases do not include subfolders, because subfolder management is currently done by the `save_eopatch` function. + """ + + file_extension = "." + str(constructor.get_file_format().extension) + file_extension = file_extension if compress_level == 0 else file_extension + ".gz" + file_name = "name" + with TempFS("testing_file_sistem") as temp_fs: + feat_io = constructor(file_name + file_extension, filesystem=temp_fs) + constructor.save(data, temp_fs, file_name, compress_level) + loaded_data = feat_io.load() + assert_data_equal(loaded_data, data) + + temp_fs.remove(file_name + file_extension) + cache_data = feat_io.load() + assert_data_equal(loaded_data, cache_data) From 529e0eb5acdf68164b290b16068ba0939642cbcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Thu, 29 Sep 2022 14:36:37 +0200 Subject: [PATCH 10/24] Improve tests coverage of eolearn.core (#475) * Improve tests coverage of eolearn.core * add types to raster test * correct type and add another large test --- core/eolearn/core/exceptions.py | 5 +- core/eolearn/tests/test_exceptions.py | 37 +++++++++++++++ core/eolearn/tests/test_utils/test_raster.py | 49 +++++++++++++++++++- 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 core/eolearn/tests/test_exceptions.py diff --git a/core/eolearn/core/exceptions.py b/core/eolearn/core/exceptions.py index 94d108de8..823b5aeeb 100644 --- a/core/eolearn/core/exceptions.py +++ b/core/eolearn/core/exceptions.py @@ -8,6 +8,7 @@ file in the root directory of this source tree. """ import warnings +from typing import Any, Type class EODeprecationWarning(DeprecationWarning): @@ -27,7 +28,7 @@ class EORuntimeWarning(RuntimeWarning): warnings.simplefilter("always", EORuntimeWarning) -def renamed_and_deprecated(deprecated_class): +def renamed_and_deprecated(deprecated_class: Type) -> Type: """A class decorator that signals that the class has been renamed when initialized. Example of use: @@ -41,7 +42,7 @@ class OldNameForClass(NewNameForClass): """ - def warn_and_init(self, *args, **kwargs): + def warn_and_init(self, *args: Any, **kwargs: Any) -> None: warnings.warn( f"The class {self.__class__.__name__} has been renamed to {self.__class__.__mro__[1].__name__}. " "The old name is deprecated and will be removed in version 1.0", diff --git a/core/eolearn/tests/test_exceptions.py b/core/eolearn/tests/test_exceptions.py new file mode 100644 index 000000000..390527a20 --- /dev/null +++ b/core/eolearn/tests/test_exceptions.py @@ -0,0 +1,37 @@ +""" +Credits: +Copyright (c) 2022 Matej Aleksandrov, Žiga Lukšič (Sinergise) + +This source code is licensed under the MIT license found in the LICENSE +file in the root directory of this source tree. +""" +import pytest + +from eolearn.core.exceptions import renamed_and_deprecated + + +def test_renamed_and_deprecated(): + """Ensures that the decorator works as intended, i.e. warns on every initialization without changing it.""" + + class TestClass: + def __init__(self, arg1, kwarg1=10): + self.arg = arg1 + self.kwarg = kwarg1 + self.secret = "exists" + + @renamed_and_deprecated + class DeprecatedClass(TestClass): + pass + + _ = TestClass(1) + _ = TestClass(2, kwarg1=20) + with pytest.warns(): + case1 = DeprecatedClass(1) + with pytest.warns(): + case2 = DeprecatedClass(2, kwarg1=20) + with pytest.warns(): + case3 = DeprecatedClass(3, 30) + + assert case1.arg == 1 and case1.kwarg == 10 and case1.secret == "exists" + assert case2.arg == 2 and case2.kwarg == 20 and case2.secret == "exists" + assert case3.arg == 3 and case3.kwarg == 30 and case3.secret == "exists" diff --git a/core/eolearn/tests/test_utils/test_raster.py b/core/eolearn/tests/test_utils/test_raster.py index cdd032f88..0f0fd2ca0 100644 --- a/core/eolearn/tests/test_utils/test_raster.py +++ b/core/eolearn/tests/test_utils/test_raster.py @@ -6,13 +6,20 @@ This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree. """ +import sys import warnings +from typing import Optional, Tuple import numpy as np import pytest from numpy.testing import assert_array_equal -from eolearn.core.utils.raster import fast_nanpercentile +from eolearn.core.utils.raster import constant_pad, fast_nanpercentile + +if sys.version_info < (3, 8): + from typing_extensions import Literal +else: + from typing import Literal # pylint: disable=ungrouped-imports @pytest.mark.parametrize("size", [0, 5]) @@ -37,3 +44,43 @@ def test_fast_nanpercentile(size: int, percentile: float, nan_ratio: float, dtyp result = fast_nanpercentile(data, percentile, method=method) assert_array_equal(result, expected_result) + + +@pytest.mark.parametrize( + argnames="array, multiple_of, up_down_rule, left_right_rule, pad_value, expected_result", + argvalues=[ + (np.arange(2).reshape((1, 2)), (3, 3), "even", "right", 5, np.array([[5, 5, 5], [0, 1, 5], [5, 5, 5]])), + (np.arange(2).reshape((1, 2)), (3, 3), "up", "even", 5, np.array([[5, 5, 5], [5, 5, 5], [0, 1, 5]])), + (np.arange(4).reshape((2, 2)), (3, 3), "down", "left", 7, np.array([[7, 0, 1], [7, 2, 3], [7, 7, 7]])), + (np.arange(20).reshape((4, 5)), (3, 3), "down", "left", 3, None), + (np.arange(60).reshape((6, 10)), (11, 11), "even", "even", 3, None), + (np.ones((167, 210)), (256, 256), "even", "even", 3, None), + (np.arange(6).reshape((2, 3)), (2, 2), "down", "even", 9, np.array([[0, 1, 2, 9], [3, 4, 5, 9]])), + ( + np.arange(6).reshape((3, 2)), + (4, 4), + "down", + "even", + 9, + np.array([[9, 0, 1, 9], [9, 2, 3, 9], [9, 4, 5, 9], [9, 9, 9, 9]]), + ), + ], +) +def test_constant_pad( + array: np.ndarray, + multiple_of: Tuple[int, int], + up_down_rule: Literal["even", "up", "down"], + left_right_rule: Literal["even", "left", "right"], + pad_value: float, + expected_result: Optional[np.ndarray], +): + """Checks that the function pads correctly and minimally. In larger cases only the shapes are checked.""" + padded = constant_pad(array, multiple_of, up_down_rule, left_right_rule, pad_value) + if expected_result is not None: + assert_array_equal(padded, expected_result) + + # correct amount of padding is present + assert np.sum(padded == pad_value) - np.sum(array == pad_value) == np.prod(padded.shape) - np.prod(array.shape) + for dim in (0, 1): + assert (padded.shape[dim] - array.shape[dim]) // multiple_of[dim] == 0 # least amount of padding + assert padded.shape[dim] % multiple_of[dim] == 0 # is divisible From 7b7f0d2faf9a7052ac62faa0d7c1ff21dd9e2fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Thu, 29 Sep 2022 15:33:08 +0200 Subject: [PATCH 11/24] add type annotations to core.eodata_io (#477) --- core/eolearn/core/eodata_io.py | 103 ++++++++++++++++++++++++--------- 1 file changed, 77 insertions(+), 26 deletions(-) diff --git a/core/eolearn/core/eodata_io.py b/core/eolearn/core/eodata_io.py index 3818def3c..3be389dc4 100644 --- a/core/eolearn/core/eodata_io.py +++ b/core/eolearn/core/eodata_io.py @@ -9,6 +9,8 @@ This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree. """ +from __future__ import annotations + import concurrent.futures import contextlib import datetime @@ -17,7 +19,24 @@ import warnings from abc import ABCMeta, abstractmethod from collections import defaultdict -from typing import Any, BinaryIO, Generic, Iterable, List, Optional, Tuple, Type, TypeVar, Union +from typing import ( + TYPE_CHECKING, + Any, + BinaryIO, + DefaultDict, + Dict, + Generic, + Iterable, + Iterator, + List, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + Union, + cast, +) import fs import fs.move @@ -33,21 +52,25 @@ from sentinelhub.os_utils import sys_is_windows from .constants import FeatureType, FeatureTypeSet, OverwritePermission -from .utils.parsing import FeatureParser +from .utils.parsing import FeatureParser, FeaturesSpecification +from .utils.types import EllipsisType from .utils.vector_io import infer_schema _T = TypeVar("_T") Self = TypeVar("Self", bound="FeatureIO") +if TYPE_CHECKING: + from .eodata import EOPatch + def save_eopatch( - eopatch, - filesystem, - patch_location, - features=..., - overwrite_permission=OverwritePermission.ADD_ONLY, - compress_level=0, -): + eopatch: EOPatch, + filesystem: FS, + patch_location: str, + features: FeaturesSpecification = ..., + overwrite_permission: OverwritePermission = OverwritePermission.ADD_ONLY, + compress_level: int = 0, +) -> None: """A utility function used by `EOPatch.save` method.""" patch_exists = filesystem.exists(patch_location) @@ -67,7 +90,7 @@ def save_eopatch( else: _check_letter_case_collisions(eopatch_features, []) - features_to_save: List[Tuple[Type[FeatureIO], Any, FS, str, int]] = [] + features_to_save: List[Tuple[Type[FeatureIO], Any, FS, Optional[str], int]] = [] for ftype, fname, feature_path in eopatch_features: # the paths here do not have file extensions, but FeatureIO.save takes care of that feature_io = _get_feature_io_constructor(ftype) @@ -94,7 +117,12 @@ def save_eopatch( remove_redundant_files(filesystem, eopatch_features, fs_features, compress_level) -def remove_redundant_files(filesystem, eopatch_features, filesystem_features, current_compress_level): +def remove_redundant_files( + filesystem: FS, + eopatch_features: Sequence[Tuple[FeatureType, Union[str, EllipsisType], str]], + filesystem_features: Sequence[Tuple[FeatureType, Union[str, EllipsisType], str]], + current_compress_level: int, +) -> None: """Removes files that should have been overwritten but were not due to different compression levels.""" files_to_remove = [] saved_features = {(ftype, fname) for ftype, fname, _ in eopatch_features} @@ -108,29 +136,40 @@ def remove_redundant_files(filesystem, eopatch_features, filesystem_features, cu list(executor.map(filesystem.remove, files_to_remove)) -def load_eopatch(eopatch, filesystem, patch_location, features=..., lazy_loading=False): +def load_eopatch( + eopatch: EOPatch, + filesystem: FS, + patch_location: str, + features: FeaturesSpecification = ..., + lazy_loading: bool = False, +) -> EOPatch: """A utility function used by `EOPatch.load` method.""" - features = list(walk_filesystem(filesystem, patch_location, features)) - loading_data: Iterable[Any] = [_get_feature_io_constructor(ftype)(path, filesystem) for ftype, _, path in features] + parsed_features = list(walk_filesystem(filesystem, patch_location, features)) + loading_data: Iterable[Any] = [ + _get_feature_io_constructor(ftype)(path, filesystem) for ftype, _, path in parsed_features + ] if not lazy_loading: with concurrent.futures.ThreadPoolExecutor() as executor: loading_data = executor.map(lambda loader: loader.load(), loading_data) - for (ftype, fname, _), value in zip(features, loading_data): + for (ftype, fname, _), value in zip(parsed_features, loading_data): eopatch[(ftype, fname)] = value return eopatch -def walk_filesystem(filesystem, patch_location, features=...): +def walk_filesystem( + filesystem: FS, patch_location: str, features: FeaturesSpecification = ... +) -> Iterator[Tuple[FeatureType, Union[str, EllipsisType], str]]: """Recursively reads a patch_location and yields tuples of (feature_type, feature_name, file_path).""" - existing_features = defaultdict(dict) + existing_features: DefaultDict[FeatureType, Dict[Union[str, EllipsisType], str]] = defaultdict(dict) for ftype, fname, path in walk_main_folder(filesystem, patch_location): existing_features[ftype][fname] = path returned_meta_features = set() queried_features = set() + feature_name: Union[str, EllipsisType] for ftype, fname in FeatureParser(features).get_feature_specifications(): if fname is ... and not existing_features[ftype]: continue @@ -163,13 +202,14 @@ def walk_filesystem(filesystem, patch_location, features=...): yield ftype, fname, existing_features[ftype][fname] -def walk_main_folder(filesystem, folder_path): +def walk_main_folder(filesystem: FS, folder_path: str) -> Iterator[Tuple[FeatureType, Union[str, EllipsisType], str]]: """Walks the main EOPatch folders and yields tuples (feature type, feature name, path in filesystem). The results depend on the implementation of `filesystem.listdir`. For each folder that coincides with a feature type it returns (feature type, ..., path). If files in subfolders are also listed by `listdir` it returns them as well, which allows `walk_filesystem` to skip such subfolders from further searches. """ + fname: Union[str, EllipsisType] for path in filesystem.listdir(folder_path): raw_path = path.split(".")[0].strip("/") @@ -182,7 +222,7 @@ def walk_main_folder(filesystem, folder_path): yield FeatureType(ftype_str), fname, fs.path.combine(folder_path, path) -def walk_feature_type_folder(filesystem, folder_path): +def walk_feature_type_folder(filesystem: FS, folder_path: str) -> Iterator[Tuple[str, str]]: """Walks a feature type subfolder of EOPatch and yields tuples (feature name, path in filesystem). Skips folders and files in subfolders. """ @@ -191,7 +231,9 @@ def walk_feature_type_folder(filesystem, folder_path): yield path.split(".")[0], fs.path.combine(folder_path, path) -def walk_eopatch(eopatch, patch_location, features): +def walk_eopatch( + eopatch: EOPatch, patch_location: str, features: FeaturesSpecification +) -> Iterator[Tuple[FeatureType, Union[str, EllipsisType], str]]: """Yields tuples of (feature_type, feature_name, file_path), with file_path being the expected file path.""" returned_meta_features = set() for ftype, fname in FeatureParser(features).get_features(eopatch): @@ -203,20 +245,27 @@ def walk_eopatch(eopatch, patch_location, features): yield ftype, ..., name_basis returned_meta_features.add(ftype) else: + fname = cast(str, fname) # name is not None for non-meta features yield ftype, fname, fs.path.combine(name_basis, fname) -def _check_add_only_permission(eopatch_features, filesystem_features): +def _check_add_only_permission( + eopatch_features: Sequence[Tuple[FeatureType, Union[str, EllipsisType], str]], + filesystem_features: Sequence[Tuple[FeatureType, Union[str, EllipsisType], str]], +) -> None: """Checks that no existing feature will be overwritten.""" - filesystem_features = {_to_lowercase(*feature) for feature in filesystem_features} - eopatch_features = {_to_lowercase(*feature) for feature in eopatch_features} + unique_filesystem_features = {_to_lowercase(*feature) for feature in filesystem_features} + unique_eopatch_features = {_to_lowercase(*feature) for feature in eopatch_features} - intersection = filesystem_features.intersection(eopatch_features) + intersection = unique_filesystem_features.intersection(unique_eopatch_features) if intersection: raise ValueError(f"Cannot save features {intersection} with overwrite_permission=OverwritePermission.ADD_ONLY") -def _check_letter_case_collisions(eopatch_features, filesystem_features): +def _check_letter_case_collisions( + eopatch_features: Sequence[Tuple[FeatureType, Union[str, EllipsisType], str]], + filesystem_features: Sequence[Tuple[FeatureType, Union[str, EllipsisType], str]], +) -> None: """Check that features have no name clashes (ignoring case) with other EOPatch features and saved features.""" lowercase_features = {_to_lowercase(*feature) for feature in eopatch_features} @@ -233,7 +282,9 @@ def _check_letter_case_collisions(eopatch_features, filesystem_features): ) -def _to_lowercase(ftype, fname, *_): +def _to_lowercase( + ftype: FeatureType, fname: Union[str, EllipsisType], *_: Any +) -> Tuple[FeatureType, Union[str, EllipsisType]]: """Transforms a feature to it's lowercase representation.""" return ftype, fname if fname is ... else fname.lower() From 9b8f95cd0aba8c5e7935a5aa3eda1532c890d69e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Fri, 30 Sep 2022 08:08:39 +0200 Subject: [PATCH 12/24] allow pickle in loading numpy arrays (#478) --- core/eolearn/core/eodata_io.py | 2 +- core/eolearn/tests/test_eodata_io.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/eolearn/core/eodata_io.py b/core/eolearn/core/eodata_io.py index 3be389dc4..12189adb4 100644 --- a/core/eolearn/core/eodata_io.py +++ b/core/eolearn/core/eodata_io.py @@ -383,7 +383,7 @@ def get_file_format(cls) -> MimeType: return MimeType.NPY def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> np.ndarray: - return np.load(file) + return np.load(file, allow_pickle=True) @classmethod def _write_to_file(cls, data: np.ndarray, file: Union[BinaryIO, gzip.GzipFile], _: str) -> None: diff --git a/core/eolearn/tests/test_eodata_io.py b/core/eolearn/tests/test_eodata_io.py index a51d118cc..647e8015d 100644 --- a/core/eolearn/tests/test_eodata_io.py +++ b/core/eolearn/tests/test_eodata_io.py @@ -298,8 +298,9 @@ def assert_data_equal(data1: Any, data2: Any) -> None: [ (FeatureIONumpy, np.zeros(20)), (FeatureIONumpy, np.zeros((2, 3, 3, 2), dtype=np.int16)), + (FeatureIONumpy, np.full((4, 5), fill_value=CRS.POP_WEB)), (FeatureIOBBox, BBox((1, 2, 3, 4), CRS.WGS84)), - (FeatureIOGeoDf, gpd.GeoDataFrame({"col1": ["name1"], "geometry": [Point(1, 2)]}, crs="EPSG:4326")), + (FeatureIOGeoDf, gpd.GeoDataFrame({"col1": ["name1"], "geometry": [Point(1, 2)]}, crs="EPSG:3857")), (FeatureIOGeoDf, gpd.GeoDataFrame({"col1": ["name1"], "geometry": [Point(1, 2)]}, crs="EPSG:32733")), ( FeatureIOGeoDf, @@ -309,7 +310,7 @@ def assert_data_equal(data1: Any, data2: Any) -> None: "TIMESTAMP": [datetime.datetime(2017, 1, 1, 10, 4, 7), datetime.datetime(2017, 1, 4, 10, 14, 5)], "geometry": [Point(1, 2), Point(2, 1)], }, - crs="EPSG:4326", + crs="EPSG:3857", ), ), (FeatureIOJson, {"test": "test1", "test3": {"test": "test1"}}), From deeffbc3a822752ed73c7abcab63ccd59cfacab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Fri, 30 Sep 2022 12:19:45 +0200 Subject: [PATCH 13/24] Add types to EOPatch (#479) * Add types to EOPatch * some light overloading for meta features --- core/eolearn/core/eodata.py | 136 +++++++++++++++--------------- core/eolearn/core/eodata_merge.py | 28 ++++-- 2 files changed, 90 insertions(+), 74 deletions(-) diff --git a/core/eolearn/core/eodata.py b/core/eolearn/core/eodata.py index b4b60cd98..63b47ba43 100644 --- a/core/eolearn/core/eodata.py +++ b/core/eolearn/core/eodata.py @@ -18,12 +18,13 @@ import logging import sys from abc import ABCMeta, abstractmethod -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar, Union, overload +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union, cast, overload import attr import dateutil.parser import geopandas as gpd import numpy as np +from fs.base import FS from sentinelhub import CRS, BBox @@ -32,7 +33,8 @@ from .eodata_merge import merge_eopatches from .utils.common import deep_eq, is_discrete_type from .utils.fs import get_filesystem -from .utils.parsing import parse_features +from .utils.parsing import FeatureSpec, FeaturesSpecification, parse_features +from .utils.types import EllipsisType if sys.version_info < (3, 8): from typing_extensions import Literal @@ -251,15 +253,15 @@ class EOPatch: vector_timeless: _FeatureDictGeoDf = attr.ib(factory=_FeatureDictGeoDf.empty_factory(FeatureType.VECTOR_TIMELESS)) meta_info: _FeatureDictJson = attr.ib(factory=_FeatureDictJson.empty_factory(FeatureType.META_INFO)) bbox: Optional[BBox] = attr.ib(default=None) - timestamp: List[dt.date] = attr.ib(factory=list) + timestamp: List[dt.datetime] = attr.ib(factory=list) - def __setattr__(self, key, value, feature_name=None): + def __setattr__(self, key: str, value: object, feature_name: Union[str, None, EllipsisType] = None) -> None: """Raises TypeError if feature type attributes are not of correct type. In case they are a dictionary they are cast to _FeatureDict class. """ if feature_name not in (None, Ellipsis) and FeatureType.has_value(key): - self[key][feature_name] = value + self.__getattribute__(key)[feature_name] = value return if FeatureType.has_value(key) and not isinstance(value, FeatureIO): @@ -296,7 +298,7 @@ def _parse_feature_type_value( f"failed to parse given value {value}" ) - def __getattribute__(self, key, load=True, feature_name=None): + def __getattribute__(self, key: str, load: bool = True, feature_name: Union[str, None, EllipsisType] = None) -> Any: """Handles lazy loading and can even provide a single feature from _FeatureDict.""" value = super().__getattribute__(key) @@ -306,33 +308,48 @@ def __getattribute__(self, key, load=True, feature_name=None): value = getattr(self, key) if feature_name not in (None, Ellipsis) and isinstance(value, _FeatureDict): + feature_name = cast(str, feature_name) # the above check deals with ... and None return value[feature_name] return value - def __getitem__(self, feature_type): + @overload + def __getitem__( + self, feature_type: Union[Literal[FeatureType.BBOX], Tuple[Literal[FeatureType.BBOX], Any]] + ) -> BBox: + ... + + @overload + def __getitem__( + self, feature_type: Union[Literal[FeatureType.TIMESTAMP], Tuple[Literal[FeatureType.TIMESTAMP], Any]] + ) -> List[dt.datetime]: + ... + + @overload + def __getitem__(self, feature_type: Union[FeatureType, Tuple[FeatureType, Union[str, None, EllipsisType]]]) -> Any: + ... + + def __getitem__(self, feature_type: Union[FeatureType, Tuple[FeatureType, Union[str, None, EllipsisType]]]) -> Any: """Provides features of requested feature type. It can also accept a tuple of (feature_type, feature_name). :param feature_type: Type of EOPatch feature - :type feature_type: FeatureType or str or (FeatureType, str) - :return: Dictionary of features """ feature_name = None if isinstance(feature_type, tuple): self._check_tuple_key(feature_type) feature_type, feature_name = feature_type - return self.__getattribute__(FeatureType(feature_type).value, feature_name=feature_name) + ftype = FeatureType(feature_type).value + return self.__getattribute__(ftype, feature_name=feature_name) # type: ignore[call-arg] - def __setitem__(self, feature_type, value): + def __setitem__( + self, feature_type: Union[FeatureType, Tuple[FeatureType, Union[str, None, EllipsisType]]], value: Any + ) -> None: """Sets a new dictionary / list to the given FeatureType. As a key it can also accept a tuple of (feature_type, feature_name). :param feature_type: Type of EOPatch feature - :type feature_type: FeatureType or str or (FeatureType, str) :param value: New dictionary or list - :type value: dict or list - :return: Dictionary of features """ feature_name = None if isinstance(feature_type, tuple): @@ -341,30 +358,29 @@ def __setitem__(self, feature_type, value): return self.__setattr__(FeatureType(feature_type).value, value, feature_name=feature_name) - def __delitem__(self, feature): + def __delitem__(self, feature: Tuple[FeatureType, str]) -> None: """Deletes the selected feature. :param feature: EOPatch feature - :type feature: (FeatureType, str) """ self._check_tuple_key(feature) feature_type, feature_name = feature del self[feature_type][feature_name] @staticmethod - def _check_tuple_key(key): + def _check_tuple_key(key: tuple) -> None: """A helper function that checks a tuple, which should hold (feature_type, feature_name).""" if len(key) != 2: raise ValueError(f"Given element should be a tuple of (feature_type, feature_name), but {key} found.") - def __eq__(self, other): + def __eq__(self, other: object) -> bool: """True if FeatureType attributes, bbox, and timestamps of both EOPatches are equal by value.""" - if not isinstance(self, type(other)): + if not isinstance(other, type(self)): return False return all(deep_eq(self[feature_type], other[feature_type]) for feature_type in FeatureType) - def __contains__(self, feature: Union[FeatureType, Tuple[FeatureType, str]]): + def __contains__(self, feature: Union[FeatureType, Tuple[FeatureType, str]]) -> bool: if isinstance(feature, FeatureType): return bool(self[feature]) if isinstance(feature, tuple) and len(feature) == 2: @@ -377,11 +393,11 @@ def __contains__(self, feature: Union[FeatureType, Tuple[FeatureType, str]]): "`(feature_type, feature_name)` tuples." ) - def __add__(self, other): + def __add__(self, other: EOPatch) -> EOPatch: """Merges two EOPatches into a new EOPatch.""" return self.merge(other) - def __repr__(self): + def __repr__(self) -> str: feature_repr_list = [] for feature_type in FeatureType: content = self[feature_type] @@ -404,12 +420,11 @@ def __repr__(self): return f"{self.__class__.__name__}({feature_repr})" @staticmethod - def _repr_value(value): + def _repr_value(value: object) -> str: """Creates a representation string for different types of data. :param value: data in any type :return: representation string - :rtype: str """ if isinstance(value, np.ndarray): return f"{EOPatch._repr_value_class(value)}(shape={value.shape}, dtype={value.dtype})" @@ -438,16 +453,15 @@ def _repr_value(value): return repr(value) @staticmethod - def _repr_value_class(value): + def _repr_value_class(value: object) -> str: """A representation of a class of a given value""" cls = value.__class__ return ".".join([cls.__module__.split(".")[0], cls.__name__]) - def __copy__(self, features=...): + def __copy__(self, features: FeaturesSpecification = ...) -> EOPatch: """Returns a new EOPatch with shallow copies of given features. :param features: A collection of features or feature types that will be copied into new EOPatch. - :type features: object supported by the :class:`FeatureParser` """ if not features: # For some reason deepcopy and copy pass {} by default features = ... @@ -460,13 +474,11 @@ def __copy__(self, features=...): new_eopatch[feature_type] = copy.copy(self[feature_type]) return new_eopatch - def __deepcopy__(self, memo=None, features=...): + def __deepcopy__(self, memo: Optional[dict] = None, features: FeaturesSpecification = ...) -> EOPatch: """Returns a new EOPatch with deep copies of given features. :param memo: built-in parameter for memoization - :type memo: dict :param features: A collection of features or feature types that will be copied into new EOPatch. - :type features: object supported by the :class:`FeatureParser` """ if not features: # For some reason deepcopy and copy pass {} by default features = ... @@ -489,26 +501,22 @@ def __deepcopy__(self, memo=None, features=...): return new_eopatch - def copy(self, features=..., deep=False): + def copy(self, features: FeaturesSpecification = ..., deep: bool = False) -> EOPatch: """Get a copy of the current `EOPatch`. :param features: Features to be copied into a new `EOPatch`. By default, all features will be copied. - :type features: object supported by the :class:`FeatureParser` :param deep: If `True` it will make a deep copy of all data inside the `EOPatch`. Otherwise, only a shallow copy of `EOPatch` will be made. Note that `BBOX` and `TIMESTAMP` will be copied even with a shallow copy. - :type deep: bool :return: An EOPatch copy. - :rtype: EOPatch """ if deep: return self.__deepcopy__(features=features) # pylint: disable=unnecessary-dunder-call return self.__copy__(features=features) # pylint: disable=unnecessary-dunder-call - def reset_feature_type(self, feature_type): + def reset_feature_type(self, feature_type: FeatureType) -> None: """Resets the values of the given feature type. :param feature_type: Type of feature - :type feature_type: FeatureType """ feature_type = FeatureType(feature_type) if feature_type.has_dict(): @@ -518,32 +526,29 @@ def reset_feature_type(self, feature_type): else: self[feature_type] = [] - def get_features(self): + def get_features(self) -> Dict[FeatureType, Union[Set[str], Literal[True]]]: """Returns a dictionary of all non-empty features of EOPatch. The elements are either sets of feature names or a boolean `True` in case feature type has no dictionary of feature names. :return: A dictionary of features - :rtype: dict(FeatureType: str or True) """ - feature_dict = {} + feature_dict: Dict[FeatureType, Union[Set[str], Literal[True]]] = {} for feature_type in FeatureType: if self[feature_type]: feature_dict[feature_type] = set(self[feature_type]) if feature_type.has_dict() else True return feature_dict - def get_spatial_dimension(self, feature_type, feature_name): + def get_spatial_dimension(self, feature_type: FeatureType, feature_name: str) -> Tuple[int, int]: """ Returns a tuple of spatial dimension (height, width) of a feature. The feature has to be spatial or time dependent. - :param feature_type: Enum of the attribute - :type feature_type: FeatureType + :param feature_type: Type of the feature :param feature_name: Name of the feature - :type feature_name: str """ if feature_type.is_temporal() or feature_type.is_spatial(): shape = self[feature_type][feature_name].shape @@ -570,8 +575,13 @@ def get_feature_list(self) -> List[Union[FeatureType, Tuple[FeatureType, str]]]: return feature_list def save( - self, path, features=..., overwrite_permission=OverwritePermission.ADD_ONLY, compress_level=0, filesystem=None - ): + self, + path: str, + features: FeaturesSpecification = ..., + overwrite_permission: OverwritePermission = OverwritePermission.ADD_ONLY, + compress_level: int = 0, + filesystem: Optional[FS] = None, + ) -> None: """Method to save an EOPatch from memory to a storage. :param path: A location where to save EOPatch. It can be either a local path or a remote URL path. @@ -602,20 +612,17 @@ def save( ) @staticmethod - def load(path, features=..., lazy_loading=False, filesystem=None): + def load( + path: str, features: FeaturesSpecification = ..., lazy_loading: bool = False, filesystem: Optional[FS] = None + ) -> EOPatch: """Method to load an EOPatch from a storage into memory. :param path: A location from where to load EOPatch. It can be either a local path or a remote URL path. - :type path: str :param features: A collection of features to be loaded. By default, all features will be loaded. - :type features: object :param lazy_loading: If `True` features will be lazy loaded. - :type lazy_loading: bool :param filesystem: An existing filesystem object. If not given it will be initialized according to the `path` parameter. - :type filesystem: fs.FS or None :return: Loaded EOPatch - :rtype: EOPatch """ if filesystem is None: filesystem = get_filesystem(path, create=False) @@ -623,13 +630,17 @@ def load(path, features=..., lazy_loading=False, filesystem=None): return load_eopatch(EOPatch(), filesystem, path, features=features, lazy_loading=lazy_loading) - def merge(self, *eopatches, features=..., time_dependent_op=None, timeless_op=None): + def merge( + self, + *eopatches: EOPatch, + features: FeaturesSpecification = ..., + time_dependent_op: Union[Literal[None, "concatenate", "min", "max", "mean", "median"], Callable] = None, + timeless_op: Union[Literal[None, "concatenate", "min", "max", "mean", "median"], Callable] = None, + ) -> EOPatch: """Merge features of given EOPatches into a new EOPatch. :param eopatches: Any number of EOPatches to be merged together with the current EOPatch - :type eopatches: EOPatch :param features: A collection of features to be merged together. By default, all features will be merged. - :type features: object :param time_dependent_op: An operation to be used to join data for any time-dependent raster feature. Before joining time slices of all arrays will be sorted. Supported options are: @@ -640,7 +651,6 @@ def merge(self, *eopatches, features=..., time_dependent_op=None, timeless_op=No - 'max': Join time slices with matching timestamps by taking maximum values. Ignore NaN values. - 'mean': Join time slices with matching timestamps by taking mean values. Ignore NaN values. - 'median': Join time slices with matching timestamps by taking median values. Ignore NaN values. - :type time_dependent_op: str or Callable or None :param timeless_op: An operation to be used to join data for any timeless raster feature. Supported options are: @@ -650,9 +660,7 @@ def merge(self, *eopatches, features=..., time_dependent_op=None, timeless_op=No - 'max': Join arrays by taking maximum values. Ignore NaN values. - 'mean': Join arrays by taking mean values. Ignore NaN values. - 'median': Join arrays by taking median values. Ignore NaN values. - :type timeless_op: str or Callable or None - :return: A dictionary with EOPatch features and values - :rtype: Dict[(FeatureType, str), object] + :return: A merged EOPatch """ eopatch_content = merge_eopatches( self, *eopatches, features=features, time_dependent_op=time_dependent_op, timeless_op=timeless_op @@ -664,7 +672,7 @@ def merge(self, *eopatches, features=..., time_dependent_op=None, timeless_op=No return merged_eopatch - def get_time_series(self, ref_date=None, scale_time=1): + def get_time_series(self, ref_date: Optional[dt.datetime] = None, scale_time: int = 1) -> np.ndarray: """Returns a numpy array with seconds passed between the reference date and the timestamp of each image. An array is constructed as time_series[i] = (timestamp[i] - ref_date).total_seconds(). @@ -672,9 +680,7 @@ def get_time_series(self, ref_date=None, scale_time=1): If EOPatch timestamp attribute is empty the method returns None. :param ref_date: reference date relative to which the time is measured - :type ref_date: datetime object :param scale_time: scale seconds by factor. If `60`, time will be in minutes, if `3600` hours - :type scale_time: int """ if not self.timestamp: @@ -687,13 +693,11 @@ def get_time_series(self, ref_date=None, scale_time=1): [round((timestamp - ref_date).total_seconds() / scale_time) for timestamp in self.timestamp], dtype=np.int64 ) - def consolidate_timestamps(self, timestamps): + def consolidate_timestamps(self, timestamps: List[dt.datetime]) -> Set[dt.datetime]: """Removes all frames from the EOPatch with a date not found in the provided timestamps list. :param timestamps: keep frames with date found in this list - :type timestamps: list of datetime objects :return: set of removed frames' dates - :rtype: set of datetime objects """ remove_from_patch = set(self.timestamp).difference(timestamps) remove_from_patch_idxs = [self.timestamp.index(rm_date) for rm_date in remove_from_patch] @@ -714,7 +718,7 @@ def consolidate_timestamps(self, timestamps): def plot( self, - feature, + feature: FeatureSpec, *, times: Union[List[int], slice, None] = None, channels: Union[List[int], slice, None] = None, @@ -722,7 +726,7 @@ def plot( rgb: Optional[Tuple[int, int, int]] = None, backend: Union[str, PlotBackend] = "matplotlib", config: Optional[BasePlotConfig] = None, - **kwargs, + **kwargs: Any, ) -> object: """Plots an `EOPatch` feature. diff --git a/core/eolearn/core/eodata_merge.py b/core/eolearn/core/eodata_merge.py index 091499676..bd7143e71 100644 --- a/core/eolearn/core/eodata_merge.py +++ b/core/eolearn/core/eodata_merge.py @@ -9,10 +9,14 @@ This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree. """ +from __future__ import annotations + import functools import itertools as it +import sys import warnings from collections.abc import Callable +from typing import TYPE_CHECKING, Any, Dict, Union import numpy as np import pandas as pd @@ -20,16 +24,27 @@ from .constants import FeatureType from .exceptions import EORuntimeWarning -from .utils.parsing import FeatureParser +from .utils.parsing import FeatureParser, FeatureSpec, FeaturesSpecification + +if sys.version_info < (3, 8): + from typing_extensions import Literal +else: + from typing import Literal # pylint: disable=ungrouped-imports + +if TYPE_CHECKING: + from .eodata import EOPatch -def merge_eopatches(*eopatches, features=..., time_dependent_op=None, timeless_op=None): +def merge_eopatches( + *eopatches: EOPatch, + features: FeaturesSpecification = ..., + time_dependent_op: Union[Literal[None, "concatenate", "min", "max", "mean", "median"], Callable] = None, + timeless_op: Union[Literal[None, "concatenate", "min", "max", "mean", "median"], Callable] = None, +) -> Dict[FeatureSpec, Any]: """Merge features of given EOPatches into a new EOPatch. :param eopatches: Any number of EOPatches to be merged together - :type eopatches: EOPatch :param features: A collection of features to be merged together. By default, all features will be merged. - :type features: object :param time_dependent_op: An operation to be used to join data for any time-dependent raster feature. Before joining time slices of all arrays will be sorted. Supported options are: @@ -41,7 +56,6 @@ def merge_eopatches(*eopatches, features=..., time_dependent_op=None, timeless_o - 'mean': Join time slices with matching timestamps by taking mean values. Ignore NaN values. - 'median': Join time slices with matching timestamps by taking median values. Ignore NaN values. - :type time_dependent_op: str or Callable or None :param timeless_op: An operation to be used to join data for any timeless raster feature. Supported options are: @@ -52,9 +66,7 @@ def merge_eopatches(*eopatches, features=..., time_dependent_op=None, timeless_o - 'mean': Join arrays by taking mean values. Ignore NaN values. - 'median': Join arrays by taking median values. Ignore NaN values. - :type timeless_op: str or Callable or None - :return: A dictionary with EOPatch features and values - :rtype: Dict[(FeatureType, str), object] + :return: Merged EOPatch """ reduce_timestamps = time_dependent_op != "concatenate" time_dependent_operation = _parse_operation(time_dependent_op, is_timeless=False) From a1eb79c4768889f5f041798e72c1f6c1aefb0f33 Mon Sep 17 00:00:00 2001 From: jgersak <112631680+jgersak@users.noreply.github.com> Date: Fri, 30 Sep 2022 12:44:33 +0200 Subject: [PATCH 14/24] FeatureIO for timestamp (#480) * FeatureIO for timestamp add sub class for handling timestamps add test * add type, add tests, reorder tests, rename class --- core/eolearn/core/eodata_io.py | 13 ++++++++++++- core/eolearn/tests/test_eodata_io.py | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/core/eolearn/core/eodata_io.py b/core/eolearn/core/eodata_io.py index 12189adb4..af1c2a5ff 100644 --- a/core/eolearn/core/eodata_io.py +++ b/core/eolearn/core/eodata_io.py @@ -38,6 +38,7 @@ cast, ) +import dateutil.parser import fs import fs.move import geopandas as gpd @@ -453,6 +454,14 @@ def _write_to_file(cls, data: _T, file: Union[BinaryIO, gzip.GzipFile], path: st file.write(json_data.encode()) +class FeatureIOTimestamp(FeatureIOJson[List[datetime.datetime]]): + """FeatureIOJson object specialized for List[dt.datetime].""" + + def _read_from_file(self, file: Union[BinaryIO, gzip.GzipFile]) -> List[datetime.datetime]: + data = json.load(file) + return [dateutil.parser.parse(timestamp) for timestamp in data] + + class FeatureIOBBox(FeatureIO[BBox]): """FeatureIO object specialized for BBox objects.""" @@ -481,8 +490,10 @@ def _get_feature_io_constructor(ftype: FeatureType) -> Type[FeatureIO]: """Creates the correct FeatureIO, corresponding to the FeatureType.""" if ftype is FeatureType.BBOX: return FeatureIOBBox - if ftype in (FeatureType.TIMESTAMP, FeatureType.META_INFO): + if ftype is FeatureType.META_INFO: return FeatureIOJson + if ftype is FeatureType.TIMESTAMP: + return FeatureIOTimestamp if ftype in FeatureTypeSet.VECTOR_TYPES: return FeatureIOGeoDf return FeatureIONumpy diff --git a/core/eolearn/tests/test_eodata_io.py b/core/eolearn/tests/test_eodata_io.py index 647e8015d..56cc6cc75 100644 --- a/core/eolearn/tests/test_eodata_io.py +++ b/core/eolearn/tests/test_eodata_io.py @@ -24,7 +24,14 @@ from sentinelhub import CRS, BBox from eolearn.core import EOPatch, FeatureType, LoadTask, OverwritePermission, SaveTask -from eolearn.core.eodata_io import FeatureIO, FeatureIOBBox, FeatureIOGeoDf, FeatureIOJson, FeatureIONumpy +from eolearn.core.eodata_io import ( + FeatureIO, + FeatureIOBBox, + FeatureIOGeoDf, + FeatureIOJson, + FeatureIONumpy, + FeatureIOTimestamp, +) FS_LOADERS = [TempFS, pytest.lazy_fixture("create_mocked_s3fs")] @@ -299,7 +306,6 @@ def assert_data_equal(data1: Any, data2: Any) -> None: (FeatureIONumpy, np.zeros(20)), (FeatureIONumpy, np.zeros((2, 3, 3, 2), dtype=np.int16)), (FeatureIONumpy, np.full((4, 5), fill_value=CRS.POP_WEB)), - (FeatureIOBBox, BBox((1, 2, 3, 4), CRS.WGS84)), (FeatureIOGeoDf, gpd.GeoDataFrame({"col1": ["name1"], "geometry": [Point(1, 2)]}, crs="EPSG:3857")), (FeatureIOGeoDf, gpd.GeoDataFrame({"col1": ["name1"], "geometry": [Point(1, 2)]}, crs="EPSG:32733")), ( @@ -313,7 +319,11 @@ def assert_data_equal(data1: Any, data2: Any) -> None: crs="EPSG:3857", ), ), + (FeatureIOJson, {}), (FeatureIOJson, {"test": "test1", "test3": {"test": "test1"}}), + (FeatureIOBBox, BBox((1, 2, 3, 4), CRS.WGS84)), + (FeatureIOTimestamp, []), + (FeatureIOTimestamp, [datetime.datetime(2017, 1, 1, 10, 4, 7), datetime.datetime(2017, 1, 4, 10, 14, 5)]), ], ) @pytest.mark.parametrize("compress_level", [0, 1]) From 35a6b8bf2f20f2fae792cc2268fe06b2d3e45caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Fri, 30 Sep 2022 15:23:30 +0200 Subject: [PATCH 15/24] Docs using type aliases for features (#481) * add documentation for two specs * try using type alias * add conf.py line * adjust documentation a bit --- core/eolearn/core/utils/parsing.py | 25 ++++++++++++++++++------- docs/source/conf.py | 1 + 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/core/eolearn/core/utils/parsing.py b/core/eolearn/core/utils/parsing.py index 19bdef83c..210969153 100644 --- a/core/eolearn/core/utils/parsing.py +++ b/core/eolearn/core/utils/parsing.py @@ -26,23 +26,34 @@ else: from typing import Literal # pylint: disable=ungrouped-imports +if sys.version_info < (3, 10): + from typing_extensions import TypeAlias +else: + from typing import TypeAlias # pylint: disable=ungrouped-imports + if TYPE_CHECKING: from ..eodata import EOPatch +# DEVELOPER NOTE: the #: comments are applied as docstrings -FeatureSpec = Union[Tuple[Literal[FeatureType.BBOX, FeatureType.TIMESTAMP], None], Tuple[FeatureType, str]] -FeatureRenameSpec = Union[ +#: Specification describing a single feature +FeatureSpec: TypeAlias = Union[Tuple[Literal[FeatureType.BBOX, FeatureType.TIMESTAMP], None], Tuple[FeatureType, str]] +#: Specification describing a feature with its current and desired new name +FeatureRenameSpec: TypeAlias = Union[ Tuple[Literal[FeatureType.BBOX, FeatureType.TIMESTAMP], None, None], Tuple[FeatureType, str, str] ] -SingleFeatureSpec = Union[FeatureSpec, FeatureRenameSpec] +SingleFeatureSpec: TypeAlias = Union[FeatureSpec, FeatureRenameSpec] -SequenceFeatureSpec = Sequence[Union[SingleFeatureSpec, FeatureType, Tuple[FeatureType, Optional[EllipsisType]]]] -DictFeatureSpec = Dict[FeatureType, Union[None, EllipsisType, Iterable[Union[str, Tuple[str, str]]]]] -MultiFeatureSpec = Union[ +SequenceFeatureSpec: TypeAlias = Sequence[ + Union[SingleFeatureSpec, FeatureType, Tuple[FeatureType, Optional[EllipsisType]]] +] +DictFeatureSpec: TypeAlias = Dict[FeatureType, Union[None, EllipsisType, Iterable[Union[str, Tuple[str, str]]]]] +MultiFeatureSpec: TypeAlias = Union[ EllipsisType, FeatureType, Tuple[FeatureType, EllipsisType], SequenceFeatureSpec, DictFeatureSpec ] -FeaturesSpecification = Union[SingleFeatureSpec, MultiFeatureSpec] +#: Specification of a single or multiple features. See :class:`FeatureParser`. +FeaturesSpecification: TypeAlias = Union[SingleFeatureSpec, MultiFeatureSpec] _ParserFeaturesSpec = Union[Tuple[FeatureType, None, None], Tuple[FeatureType, str, str]] diff --git a/docs/source/conf.py b/docs/source/conf.py index f889581a3..f31d61044 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -72,6 +72,7 @@ # Include typehints in descriptions autodoc_typehints = "description" +autodoc_type_aliases = {"FeaturesSpecification": "eolearn.core.parsing.FeaturesSpecification"} # Both the class’ and the __init__ method’s docstring are concatenated and inserted. autoclass_content = "both" From b5d699aac479a79232f8aab6597d20eb505a128a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Mon, 3 Oct 2022 10:52:03 +0200 Subject: [PATCH 16/24] Minor type fixes and exposing types in eolearn.core (#482) * contains should use `object` annotation * parallelize annotations were incorrect for multiple param iterables * remove comments that are currently not required * include Literal in core.utils.types * minor additions in core classes * expose types with py.typed * resolve merge conflict caught by pre-commit on remote --- core/eolearn/core/eodata.py | 10 ++-------- core/eolearn/core/eodata_merge.py | 7 +------ core/eolearn/core/eotask.py | 15 ++++++++------- core/eolearn/core/eoworkflow_tasks.py | 4 ++-- core/eolearn/core/exceptions.py | 2 +- core/eolearn/core/py.typed | 0 core/eolearn/core/utils/parallelize.py | 9 ++++----- core/eolearn/core/utils/parsing.py | 7 +------ core/eolearn/core/utils/raster.py | 6 +----- core/eolearn/core/utils/types.py | 5 +++++ core/eolearn/tests/test_utils/test_raster.py | 7 +------ 11 files changed, 26 insertions(+), 46 deletions(-) create mode 100644 core/eolearn/core/py.typed diff --git a/core/eolearn/core/eodata.py b/core/eolearn/core/eodata.py index 63b47ba43..07f51e847 100644 --- a/core/eolearn/core/eodata.py +++ b/core/eolearn/core/eodata.py @@ -16,7 +16,6 @@ import copy import datetime as dt import logging -import sys from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union, cast, overload @@ -34,12 +33,7 @@ from .utils.common import deep_eq, is_discrete_type from .utils.fs import get_filesystem from .utils.parsing import FeatureSpec, FeaturesSpecification, parse_features -from .utils.types import EllipsisType - -if sys.version_info < (3, 8): - from typing_extensions import Literal -else: - from typing import Literal # pylint: disable=ungrouped-imports +from .utils.types import EllipsisType, Literal _T = TypeVar("_T") _Self = TypeVar("_Self") @@ -380,7 +374,7 @@ def __eq__(self, other: object) -> bool: return all(deep_eq(self[feature_type], other[feature_type]) for feature_type in FeatureType) - def __contains__(self, feature: Union[FeatureType, Tuple[FeatureType, str]]) -> bool: + def __contains__(self, feature: object) -> bool: if isinstance(feature, FeatureType): return bool(self[feature]) if isinstance(feature, tuple) and len(feature) == 2: diff --git a/core/eolearn/core/eodata_merge.py b/core/eolearn/core/eodata_merge.py index bd7143e71..140125b2d 100644 --- a/core/eolearn/core/eodata_merge.py +++ b/core/eolearn/core/eodata_merge.py @@ -13,7 +13,6 @@ import functools import itertools as it -import sys import warnings from collections.abc import Callable from typing import TYPE_CHECKING, Any, Dict, Union @@ -25,11 +24,7 @@ from .constants import FeatureType from .exceptions import EORuntimeWarning from .utils.parsing import FeatureParser, FeatureSpec, FeaturesSpecification - -if sys.version_info < (3, 8): - from typing_extensions import Literal -else: - from typing import Literal # pylint: disable=ungrouped-imports +from .utils.types import Literal if TYPE_CHECKING: from .eodata import EOPatch diff --git a/core/eolearn/core/eotask.py b/core/eolearn/core/eotask.py index 390c875b3..df4260590 100644 --- a/core/eolearn/core/eotask.py +++ b/core/eolearn/core/eotask.py @@ -18,7 +18,7 @@ import logging from abc import ABCMeta, abstractmethod from dataclasses import dataclass -from typing import Dict, Iterable, Union +from typing import Any, Dict, Iterable, Type, TypeVar, Union from .constants import FeatureType from .utils.parsing import FeatureParser, parse_feature, parse_features, parse_renamed_feature, parse_renamed_features @@ -26,6 +26,8 @@ LOGGER = logging.getLogger(__name__) +Self = TypeVar("Self") + class EOTask(metaclass=ABCMeta): """Base class for EOTask.""" @@ -35,11 +37,11 @@ class EOTask(metaclass=ABCMeta): parse_features = staticmethod(parse_features) parse_renamed_features = staticmethod(parse_renamed_features) - def __new__(cls, *args, **kwargs): + def __new__(cls: Type[Self], *args: Any, **kwargs: Any) -> Self: """Stores initialization parameters and the order to the instance attribute `init_args`.""" - self = super().__new__(cls) + self = super().__new__(cls) # type: ignore[misc] - init_args = {} + init_args: Dict[str, object] = {} for arg, value in zip(inspect.getfullargspec(self.__init__).args[1 : len(args) + 1], args): init_args[arg] = repr(value) for arg in inspect.getfullargspec(self.__init__).args[len(args) + 1 :]: @@ -51,13 +53,12 @@ def __new__(cls, *args, **kwargs): return self @property - def private_task_config(self): + def private_task_config(self) -> "_PrivateTaskConfig": """Keeps track of the arguments for which the task was initialized for better logging. :return: The initial configuration arguments of the task - :rtype: _PrivateTaskConfig """ - return self._private_task_config + return self._private_task_config # type: ignore[attr-defined] def __call__(self, *eopatches, **kwargs): """Syntactic sugar for task execution""" diff --git a/core/eolearn/core/eoworkflow_tasks.py b/core/eolearn/core/eoworkflow_tasks.py index 028a20d9f..03ab09c59 100644 --- a/core/eolearn/core/eoworkflow_tasks.py +++ b/core/eolearn/core/eoworkflow_tasks.py @@ -23,7 +23,7 @@ def __init__(self, value: Optional[object] = None): """ self.value = value - def execute(self, *, value: Optional[object] = None) -> object: # type: ignore[override] + def execute(self, *, value: Optional[object] = None) -> object: """ :param value: A value that the task should provide as its result. If not set uses the value from initialization :return: Directly returns `value` @@ -51,7 +51,7 @@ def name(self) -> str: """ return self._name - def execute(self, data: object) -> object: # type: ignore[override] + def execute(self, data: object) -> object: """ :param data: input data :return: Same data, to be stored in results (for `EOPatch` returns shallow copy containing only `features`) diff --git a/core/eolearn/core/exceptions.py b/core/eolearn/core/exceptions.py index 823b5aeeb..64fd594f7 100644 --- a/core/eolearn/core/exceptions.py +++ b/core/eolearn/core/exceptions.py @@ -42,7 +42,7 @@ class OldNameForClass(NewNameForClass): """ - def warn_and_init(self, *args: Any, **kwargs: Any) -> None: + def warn_and_init(self: Any, *args: Any, **kwargs: Any) -> None: warnings.warn( f"The class {self.__class__.__name__} has been renamed to {self.__class__.__mro__[1].__name__}. " "The old name is deprecated and will be removed in version 1.0", diff --git a/core/eolearn/core/py.typed b/core/eolearn/core/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/core/eolearn/core/utils/parallelize.py b/core/eolearn/core/utils/parallelize.py index f60aefcca..6fae444d0 100644 --- a/core/eolearn/core/utils/parallelize.py +++ b/core/eolearn/core/utils/parallelize.py @@ -26,7 +26,6 @@ # pylint: disable=invalid-name _T = TypeVar("_T") _FutureType = TypeVar("_FutureType") -_InputType = TypeVar("_InputType") _OutputType = TypeVar("_OutputType") @@ -55,8 +54,8 @@ def _decide_processing_type(workers: Optional[int], multiprocess: bool) -> _Proc def parallelize( - function: Callable[[_InputType], _OutputType], - *params: Iterable[_InputType], + function: Callable[..., _OutputType], + *params: Iterable[Any], workers: Optional[int], multiprocess: bool = True, **tqdm_kwargs: Any, @@ -117,8 +116,8 @@ def execute_with_mp_lock(function: Callable[..., _OutputType], *args: Any, **kwa def submit_and_monitor_execution( executor: Executor, - function: Callable[[_InputType], _OutputType], - *params: Iterable[_InputType], + function: Callable[..., _OutputType], + *params: Iterable[Any], **tqdm_kwargs: Any, ) -> List[_OutputType]: """Performs the execution parallelization and monitors the process using a progress bar. diff --git a/core/eolearn/core/utils/parsing.py b/core/eolearn/core/utils/parsing.py index 210969153..c98253592 100644 --- a/core/eolearn/core/utils/parsing.py +++ b/core/eolearn/core/utils/parsing.py @@ -19,12 +19,7 @@ from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Tuple, Union, cast from ..constants import FeatureType -from .types import EllipsisType - -if sys.version_info < (3, 8): - from typing_extensions import Literal -else: - from typing import Literal # pylint: disable=ungrouped-imports +from .types import EllipsisType, Literal if sys.version_info < (3, 10): from typing_extensions import TypeAlias diff --git a/core/eolearn/core/utils/raster.py b/core/eolearn/core/utils/raster.py index 3945e61d8..fbabed264 100644 --- a/core/eolearn/core/utils/raster.py +++ b/core/eolearn/core/utils/raster.py @@ -9,15 +9,11 @@ This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree. """ -import sys from typing import Tuple import numpy as np -if sys.version_info < (3, 8): - from typing_extensions import Literal -else: - from typing import Literal # pylint: disable=ungrouped-imports +from .types import Literal def fast_nanpercentile(data: np.ndarray, percentile: float, *, method: str = "linear") -> np.ndarray: diff --git a/core/eolearn/core/utils/types.py b/core/eolearn/core/utils/types.py index 041eb73c9..785ef4c87 100644 --- a/core/eolearn/core/utils/types.py +++ b/core/eolearn/core/utils/types.py @@ -18,3 +18,8 @@ from typing_extensions import TypeAlias EllipsisType: TypeAlias = "builtins.ellipsis" + +if sys.version_info < (3, 8): + from typing_extensions import Literal +else: + from typing import Literal # pylint: disable=ungrouped-imports # noqa: F401 diff --git a/core/eolearn/tests/test_utils/test_raster.py b/core/eolearn/tests/test_utils/test_raster.py index 0f0fd2ca0..e60249338 100644 --- a/core/eolearn/tests/test_utils/test_raster.py +++ b/core/eolearn/tests/test_utils/test_raster.py @@ -6,7 +6,6 @@ This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree. """ -import sys import warnings from typing import Optional, Tuple @@ -15,11 +14,7 @@ from numpy.testing import assert_array_equal from eolearn.core.utils.raster import constant_pad, fast_nanpercentile - -if sys.version_info < (3, 8): - from typing_extensions import Literal -else: - from typing import Literal # pylint: disable=ungrouped-imports +from eolearn.core.utils.types import Literal @pytest.mark.parametrize("size", [0, 5]) From 8c9ca3363c26c9477fa24742c92af73e8d89e4dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Mon, 3 Oct 2022 15:51:24 +0200 Subject: [PATCH 17/24] Add mypy to ci (#483) * add mypy to ci * add mypy to dev * add dateutils typeshed to dev requirements * fix remaining mypy issue * remove branch exception in CI after testing done --- .github/workflows/ci_action.yml | 30 ++++++++++++++++++++++++++++-- core/eolearn/core/utils/raster.py | 2 +- requirements-dev.txt | 2 ++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci_action.yml b/.github/workflows/ci_action.yml index f2e19f310..cf72029a2 100644 --- a/.github/workflows/ci_action.yml +++ b/.github/workflows/ci_action.yml @@ -35,6 +35,33 @@ jobs: - name: Check code compliance with pre-commit validators run: pre-commit run --all-files + check-code-pylint-and-mypy: + runs-on: ubuntu-latest + steps: + - name: Checkout branch + uses: actions/checkout@v2 + with: + ref: ${{ env.CHECKOUT_BRANCH }} + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: "3.8" + architecture: x64 + + - name: Install packages + run: | + sudo apt-get update + sudo apt-get install -y build-essential libgdal-dev graphviz proj-bin gcc libproj-dev libspatialindex-dev + pip install -r requirements-dev.txt --upgrade + python install_all.py -e + + - name: Run pylint + run: make pylint + + - name: Run mypy + run: mypy core/eolearn/core + test-on-github: runs-on: ubuntu-latest strategy: @@ -75,10 +102,9 @@ jobs: --sh_client_secret "${{ secrets.SH_CLIENT_SECRET }}" pytest --cov --cov-report=term --cov-report=xml - - name: Run pylint and reduced tests + - name: Run reduced tests if: ${{ !matrix.full_test_suite }} run: | - make pylint pytest -m "not sh_integration" - name: Upload code coverage diff --git a/core/eolearn/core/utils/raster.py b/core/eolearn/core/utils/raster.py index fbabed264..4de9b0de4 100644 --- a/core/eolearn/core/utils/raster.py +++ b/core/eolearn/core/utils/raster.py @@ -54,7 +54,7 @@ def fast_nanpercentile(data: np.ndarray, percentile: float, *, method: str = "li chunk = chunk[~np.isnan(chunk)] chunk = chunk.reshape((time_size - no_data_num, sample_size), order="F") - result = np.percentile(chunk, q=percentile, axis=0, **method_kwargs) + result = np.percentile(chunk, q=percentile, axis=0, **method_kwargs) # type: ignore[call-overload] combined_data[mask] = result diff --git a/requirements-dev.txt b/requirements-dev.txt index a8f99eefa..f88e4b170 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,7 @@ codecov hypothesis moto +mypy nbval pylint>=2.14.0 pytest>=7.0.0 @@ -10,3 +11,4 @@ pytest-mock ray[default] responses twine +types-python-dateutil From 318e4d7e4eeb8fb9c7c30982296a8dea7d669c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Mon, 3 Oct 2022 15:51:35 +0200 Subject: [PATCH 18/24] Add type annotations to core tasks (#484) * add types to task init methods * add types to execute methods of core tasks --- core/eolearn/core/core_tasks.py | 168 +++++++++++--------------- core/eolearn/core/eoworkflow_tasks.py | 4 +- 2 files changed, 74 insertions(+), 98 deletions(-) diff --git a/core/eolearn/core/core_tasks.py b/core/eolearn/core/core_tasks.py index 04e0acac5..814f6daba 100644 --- a/core/eolearn/core/core_tasks.py +++ b/core/eolearn/core/core_tasks.py @@ -13,15 +13,19 @@ """ import copy from abc import ABCMeta, abstractmethod -from typing import Dict, Iterable, Tuple, Union +from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union, cast import fs import numpy as np +from fs.base import FS + +from sentinelhub import SHConfig from .constants import FeatureType from .eodata import EOPatch from .eotask import EOTask from .utils.fs import get_filesystem, pickle_fs, unpickle_fs +from .utils.parsing import FeatureSpec, FeaturesSpecification class CopyTask(EOTask): @@ -30,10 +34,9 @@ class CopyTask(EOTask): It copies feature type dictionaries but not the data itself. """ - def __init__(self, features=...): + def __init__(self, features: FeaturesSpecification = ...): """ :param features: A collection of features or feature types that will be copied into a new EOPatch. - :type features: an object supported by the :class:`FeatureParser` """ self.features = features @@ -51,18 +54,16 @@ def execute(self, eopatch): class IOTask(EOTask, metaclass=ABCMeta): """An abstract Input/Output task that can handle a path and a filesystem object.""" - def __init__(self, path, filesystem=None, create=False, config=None): + def __init__( + self, path: str, filesystem: Optional[FS] = None, create: bool = False, config: Optional[SHConfig] = None + ): """ :param path: root path where all EOPatches are saved - :type path: str :param filesystem: An existing filesystem object. If not given it will be initialized according to the EOPatch path. - :type filesystem: fs.base.FS or None :param create: If the filesystem path doesn't exist this flag indicates to either create it or raise an error - :type create: bool :param config: A configuration object with AWS credentials. By default, is set to None and in this case the default configuration will be taken. - :type config: SHConfig or None """ self.path = path self.filesystem_path = "/" if filesystem is None else self.path @@ -72,7 +73,7 @@ def __init__(self, path, filesystem=None, create=False, config=None): self.config = config @property - def filesystem(self): + def filesystem(self) -> FS: """A filesystem property that unpickles an existing filesystem definition or creates a new one.""" if self._pickled_filesystem is None: filesystem = get_filesystem(self.path, create=self._create_path, config=self.config) @@ -89,16 +90,13 @@ def execute(self, *eopatches, **kwargs): class SaveTask(IOTask): """Saves the given EOPatch to a filesystem.""" - def __init__(self, path, filesystem=None, config=None, **kwargs): + def __init__(self, path: str, filesystem: Optional[FS] = None, config: Optional[SHConfig] = None, **kwargs: Any): """ :param path: root path where all EOPatches are saved - :type path: str :param filesystem: An existing filesystem object. If not given it will be initialized according to the EOPatch path. - :type filesystem: fs.base.FS or None :param features: A collection of features types specifying features of which type will be saved. By default, all features will be saved. - :type features: an object supported by the :class:`FeatureParser` :param overwrite_permission: A level of permission for overwriting an existing EOPatch :type overwrite_permission: OverwritePermission or int :param compress_level: A level of data compression and can be specified with an integer from 0 (no compression) @@ -106,20 +104,16 @@ def __init__(self, path, filesystem=None, config=None, **kwargs): :type compress_level: int :param config: A configuration object with AWS credentials. By default, is set to None and in this case the default configuration will be taken. - :type config: SHConfig or None """ self.kwargs = kwargs super().__init__(path, filesystem=filesystem, create=True, config=config) - def execute(self, eopatch, *, eopatch_folder=""): + def execute(self, eopatch: EOPatch, *, eopatch_folder: Optional[str] = "") -> EOPatch: """Saves the EOPatch to disk: `folder/eopatch_folder`. :param eopatch: EOPatch which will be saved - :type eopatch: EOPatch :param eopatch_folder: Name of EOPatch folder containing data. If `None` is given it won't save anything. - :type eopatch_folder: str or None :return: The same EOPatch - :rtype: EOPatch """ if eopatch_folder is None: return eopatch @@ -132,34 +126,28 @@ def execute(self, eopatch, *, eopatch_folder=""): class LoadTask(IOTask): """Loads an EOPatch from a filesystem.""" - def __init__(self, path, filesystem=None, config=None, **kwargs): + def __init__(self, path: str, filesystem: Optional[FS] = None, config: Optional[SHConfig] = None, **kwargs: Any): """ :param path: root directory where all EOPatches are saved - :type path: str :param filesystem: An existing filesystem object. If not given it will be initialized according to the EOPatch path. If you intend to run this task in multiprocessing mode you shouldn't specify this parameter. - :type filesystem: fs.base.FS or None :param features: A collection of features to be loaded. By default, all features will be loaded. - :type features: an object supported by the :class:`FeatureParser` :param lazy_loading: If `True` features will be lazy loaded. Default is `False` :type lazy_loading: bool :param config: A configuration object with AWS credentials. By default, is set to None and in this case the default configuration will be taken. - :type config: SHConfig or None """ self.kwargs = kwargs super().__init__(path, filesystem=filesystem, create=False, config=config) - def execute(self, eopatch=None, *, eopatch_folder=""): + def execute(self, eopatch: Optional[EOPatch] = None, *, eopatch_folder: Optional[str] = "") -> EOPatch: """Loads the EOPatch from disk: `folder/eopatch_folder`. :param eopatch: Optional input EOPatch. If given the loaded features are merged onto it, otherwise a new EOPatch is created. :param eopatch_folder: Name of EOPatch folder containing data. If `None` is given it will return an empty or modified `EOPatch` (depending on the task input). - :type eopatch_folder: str or None :return: EOPatch loaded from disk - :rtype: EOPatch """ if eopatch_folder is None: return eopatch or EOPatch() @@ -174,22 +162,18 @@ def execute(self, eopatch=None, *, eopatch_folder=""): class AddFeatureTask(EOTask): """Adds a feature to the given EOPatch.""" - def __init__(self, feature): + def __init__(self, feature: FeatureSpec): """ :param feature: Feature to be added - :type feature: (FeatureType, feature_name) or FeatureType """ self.feature_type, self.feature_name = self.parse_feature(feature) - def execute(self, eopatch, data): + def execute(self, eopatch: EOPatch, data: object) -> EOPatch: """Returns the EOPatch with added features. :param eopatch: input EOPatch - :type eopatch: EOPatch :param data: data to be added to the feature - :type data: object :return: input EOPatch with the specified feature - :rtype: EOPatch """ if self.feature_name is None: eopatch[self.feature_type] = data @@ -202,23 +186,20 @@ def execute(self, eopatch, data): class RemoveFeatureTask(EOTask): """Removes one or multiple features from the given EOPatch.""" - def __init__(self, features): + def __init__(self, features: FeaturesSpecification): """ :param features: A collection of features to be removed. - :type features: an object supported by the :class:`FeatureParser` """ self.feature_parser = self.get_feature_parser(features) - def execute(self, eopatch): + def execute(self, eopatch: EOPatch) -> EOPatch: """Returns the EOPatch with removed features. :param eopatch: input EOPatch - :type eopatch: EOPatch :return: input EOPatch without the specified feature - :rtype: EOPatch """ for feature_type, feature_name in self.feature_parser.get_features(eopatch): - if feature_name is ...: + if feature_name is None: eopatch.reset_feature_type(feature_type) else: del eopatch[feature_type][feature_name] @@ -229,20 +210,17 @@ def execute(self, eopatch): class RenameFeatureTask(EOTask): """Renames one or multiple features from the given EOPatch.""" - def __init__(self, features): + def __init__(self, features: FeaturesSpecification): """ :param features: A collection of features to be renamed. - :type features: an object supported by the :class:`FeatureParser` """ self.feature_parser = self.get_feature_parser(features) - def execute(self, eopatch): + def execute(self, eopatch: EOPatch) -> EOPatch: """Returns the EOPatch with renamed features. :param eopatch: input EOPatch - :type eopatch: EOPatch :return: input EOPatch with the renamed features - :rtype: EOPatch """ for feature_type, feature_name, new_feature_name in self.feature_parser.get_renamed_features(eopatch): eopatch[feature_type][new_feature_name] = eopatch[feature_type][feature_name] @@ -254,23 +232,19 @@ def execute(self, eopatch): class DuplicateFeatureTask(EOTask): """Duplicates one or multiple features in an EOPatch.""" - def __init__(self, features, deep_copy=False): + def __init__(self, features: FeaturesSpecification, deep_copy: bool = False): """ :param features: A collection of features to be copied. - :type features: an object supported by the :class:`FeatureParser` :param deep_copy: Make a deep copy of feature's data if set to true, else just assign it. - :type deep_copy: bool """ self.feature_parser = self.get_feature_parser(features) self.deep = deep_copy - def execute(self, eopatch): + def execute(self, eopatch: EOPatch) -> EOPatch: """Returns the EOPatch with copied features. :param eopatch: Input EOPatch - :type eopatch: EOPatch :return: Input EOPatch with the duplicated features. - :rtype: EOPatch :raises ValueError: Raises an exception when trying to duplicate a feature with an already existing feature name. """ @@ -300,20 +274,24 @@ class InitializeFeatureTask(EOTask): InitializeFeature((FeatureType.MASK, 'mask1'), shape=(FeatureType.DATA, 'data1'), init_value=1) """ - def __init__(self, features, shape, init_value=0, dtype=np.uint8): + def __init__( + self, + features: FeaturesSpecification, + shape: Union[Tuple[int, ...], FeatureSpec], + init_value: int = 0, + dtype: Union[np.dtype, type] = np.uint8, + ): """ :param features: A collection of features to initialize. - :type features: an object supported by the :class:`FeatureParser` :param shape: A shape object (t, n, m, d) or a feature from which to read the shape. - :type shape: A tuple or an object supported by the :class:`FeatureParser` :param init_value: A value with which to initialize the array of the new feature. - :type init_value: int :param dtype: Type of array values. - :type dtype: NumPy dtype :raises ValueError: Raises an exception when passing the wrong shape argument. """ self.features = self.parse_features(features) + self.shape_feature: Optional[Tuple[FeatureType, Optional[str]]] + self.shape: Union[None, Tuple[int, int, int], Tuple[int, int, int, int]] try: self.shape_feature = self.parse_feature(shape) @@ -323,23 +301,21 @@ def __init__(self, features, shape, init_value=0, dtype=np.uint8): if self.shape_feature: self.shape = None elif isinstance(shape, tuple) and len(shape) in (3, 4) and all(isinstance(x, int) for x in shape): - self.shape = shape + self.shape = cast(Union[Tuple[int, int, int], Tuple[int, int, int, int]], shape) else: raise ValueError("shape argument is not a shape tuple or a feature containing one.") self.init_value = init_value self.dtype = dtype - def execute(self, eopatch): + def execute(self, eopatch: EOPatch) -> EOPatch: """ :param eopatch: Input EOPatch. - :type eopatch: EOPatch :return: Input EOPatch with the initialized additional features. - :rtype: EOPatch """ shape = eopatch[self.shape_feature].shape if self.shape_feature else self.shape - add_features = set(self.features) - set(eopatch.get_feature_list()) + add_features = set(self.features) - set(self.parse_features(eopatch.get_feature_list())) for feature in add_features: eopatch[feature] = np.ones(shape, dtype=self.dtype) * self.init_value @@ -350,24 +326,19 @@ def execute(self, eopatch): class MoveFeatureTask(EOTask): """Task to copy/deepcopy fields from one EOPatch to another.""" - def __init__(self, features, deep_copy=False): + def __init__(self, features: FeaturesSpecification, deep_copy: bool = False): """ :param features: A collection of features to be moved. - :type features: an object supported by the :class:`FeatureParser` :param deep_copy: Make a deep copy of feature's data if set to true, else just assign it. - :type deep_copy: bool """ self.feature_parser = self.get_feature_parser(features) self.deep = deep_copy - def execute(self, src_eopatch, dst_eopatch): + def execute(self, src_eopatch: EOPatch, dst_eopatch: EOPatch) -> EOPatch: """ :param src_eopatch: Source EOPatch from which to take features. - :type src_eopatch: EOPatch :param dst_eopatch: Destination EOPatch to which to move/copy features. - :type dst_eopatch: EOPatch :return: dst_eopatch with the additional features from src_eopatch. - :rtype: EOPatch """ for feature in self.feature_parser.get_features(src_eopatch): @@ -418,34 +389,39 @@ def map_method(self, f): result = maximum(patch) """ - def __init__(self, input_features, output_features, map_function=None, **kwargs): + def __init__( + self, + input_features: FeaturesSpecification, + output_features: FeaturesSpecification, + map_function: Optional[Callable] = None, + **kwargs: Any, + ): """ :param input_features: A collection of the input features to be mapped. - :type input_features: an object supported by the :class:`FeatureParser` :param output_features: A collection of the output features to which to assign the output data. - :type output_features: an object supported by the :class:`FeatureParser` :param map_function: A function or lambda to be applied to the input data. :raises ValueError: Raises an exception when passing feature collections with different lengths. :param kwargs: kwargs to be passed to the map function. """ self.input_features = self.parse_features(input_features) - self.output_feature = self.parse_features(output_features) + self.output_features = self.parse_features(output_features) self.kwargs = kwargs - if len(self.input_features) != len(self.output_feature): + if len(self.input_features) != len(self.output_features): raise ValueError("The number of input and output features must match.") - self.function = map_function if map_function else self.map_method + if map_function: # mypy 0.981 has issues with inlined conditional and functions + self.function: Callable = map_function + else: + self.function = self.map_method - def execute(self, eopatch): + def execute(self, eopatch: EOPatch) -> EOPatch: """ :param eopatch: Source EOPatch from which to read the data of input features. - :type eopatch: EOPatch :return: An eopatch with the additional mapped features. - :rtype: EOPatch """ - for input_features, output_feature in zip(self.input_features, self.output_feature): - eopatch[output_feature] = self.function(eopatch[input_features], **self.kwargs) + for input_feature, output_feature in zip(self.input_features, self.output_features): + eopatch[output_feature] = self.function(eopatch[input_feature], **self.kwargs) return eopatch @@ -495,26 +471,32 @@ def zip_method(self, *f): result = maximum(patch) """ - def __init__(self, input_features, output_feature, zip_function=None, **kwargs): + def __init__( + self, + input_features: FeaturesSpecification, + output_feature: FeaturesSpecification, + zip_function: Optional[Callable] = None, + **kwargs: Any, + ): """ :param input_features: A collection of the input features to be mapped. - :type input_features: an object supported by the :class:`FeatureParser` :param output_feature: An output feature object to which to assign the data. - :type output_feature: an object supported by the :class:`FeatureParser` :param zip_function: A function or lambda to be applied to the input data. :param kwargs: kwargs to be passed to the zip function. """ self.input_features = self.parse_features(input_features) self.output_feature = self.parse_feature(output_feature) - self.function = zip_function if zip_function else self.zip_method self.kwargs = kwargs - def execute(self, eopatch): + if zip_function: # mypy 0.981 has issues with inlined conditional and functions + self.function: Callable = zip_function + else: + self.function = self.zip_method + + def execute(self, eopatch: EOPatch) -> EOPatch: """ :param eopatch: Source EOPatch from which to read the data of input features. - :type eopatch: EOPatch :return: An eopatch with the additional zipped features. - :rtype: EOPatch """ data = [eopatch[feature] for feature in self.input_features] @@ -530,7 +512,7 @@ def zip_method(self, *f): class MergeFeatureTask(ZipFeatureTask): """Merges multiple features together by concatenating their data along the last axis.""" - def zip_method(self, *f, dtype=None): + def zip_method(self, *f: np.ndarray, dtype: Union[None, np.dtype, type] = None) -> np.ndarray: """Concatenates the data of features along the last axis.""" return np.concatenate(f, axis=-1, dtype=dtype) # pylint: disable=unexpected-keyword-arg @@ -538,14 +520,11 @@ def zip_method(self, *f, dtype=None): class ExtractBandsTask(MapFeatureTask): """Moves a subset of bands from one feature to a new one.""" - def __init__(self, input_feature, output_feature, bands): + def __init__(self, input_feature: FeaturesSpecification, output_feature: FeaturesSpecification, bands: List[int]): """ :param input_feature: A source feature from which to take the subset of bands. - :type input_feature: an object supported by the :class:`FeatureParser` :param output_feature: An output feature to which to write the bands. - :type output_feature: an object supported by the :class:`FeatureParser` :param bands: A list of bands to be moved. - :type bands: list """ super().__init__(input_feature, output_feature) self.bands = bands @@ -572,7 +551,7 @@ def __init__( self.input_feature = input_feature self.output_mapping = output_mapping - def execute(self, eopatch): + def execute(self, eopatch: EOPatch) -> EOPatch: for output_feature, bands in self.output_mapping.items(): new_bands = list(bands) if isinstance(bands, Iterable) else [bands] eopatch = ExtractBandsTask( @@ -584,12 +563,11 @@ def execute(self, eopatch): class CreateEOPatchTask(EOTask): """Creates an EOPatch.""" - def execute(self, **kwargs): + def execute(self, **kwargs: Any) -> EOPatch: """Returns a newly created EOPatch with the given kwargs. :param kwargs: Any valid kwargs accepted by :class:`EOPatch.__init__` :return: A new eopatch. - :rtype: EOPatch """ return EOPatch(**kwargs) @@ -600,18 +578,16 @@ class MergeEOPatchesTask(EOTask): Check :func:`EOPatch.merge` for more information about the merging process. """ - def __init__(self, **merge_kwargs): + def __init__(self, **merge_kwargs: Any): """ :param merge_kwargs: Keyword arguments defined for `EOPatch.merge` method. """ self.merge_kwargs = merge_kwargs - def execute(self, *eopatches): + def execute(self, *eopatches: EOPatch) -> EOPatch: """ :param eopatches: EOPatches to be merged - :type eopatches: EOPatch :return: A new EOPatch with merged content - :rtype: EOPatch """ if not eopatches: raise ValueError("At least one EOPatch should be given") diff --git a/core/eolearn/core/eoworkflow_tasks.py b/core/eolearn/core/eoworkflow_tasks.py index 03ab09c59..9d37d3406 100644 --- a/core/eolearn/core/eoworkflow_tasks.py +++ b/core/eolearn/core/eoworkflow_tasks.py @@ -12,6 +12,7 @@ from .eodata import EOPatch from .eotask import EOTask from .utils.common import generate_uid +from .utils.parsing import FeaturesSpecification class InputTask(EOTask): @@ -34,11 +35,10 @@ def execute(self, *, value: Optional[object] = None) -> object: class OutputTask(EOTask): """Stores data as an output of `EOWorkflow` results.""" - def __init__(self, name: Optional[str] = None, features=...): + def __init__(self, name: Optional[str] = None, features: FeaturesSpecification = ...): """ :param name: A name under which the data will be saved in `WorkflowResults`, auto-generated if `None` :param features: A collection of features to be kept if the data is an `EOPatch` - :type features: an object supported by the :class:`FeatureParser` """ self._name = name or generate_uid("output") self.features = features From 2548e4e290db45408cdc42c9e65cba2ab79f50ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Tue, 4 Oct 2022 13:44:40 +0200 Subject: [PATCH 19/24] add types to rest of easily typeable content (#485) --- core/eolearn/core/core_tasks.py | 12 ++-- core/eolearn/core/eodata_merge.py | 100 ++++++++++++++++++------------ core/eolearn/core/eotask.py | 11 +++- 3 files changed, 73 insertions(+), 50 deletions(-) diff --git a/core/eolearn/core/core_tasks.py b/core/eolearn/core/core_tasks.py index 814f6daba..e3daea1a0 100644 --- a/core/eolearn/core/core_tasks.py +++ b/core/eolearn/core/core_tasks.py @@ -12,7 +12,7 @@ file in the root directory of this source tree. """ import copy -from abc import ABCMeta, abstractmethod +from abc import ABCMeta from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union, cast import fs @@ -40,18 +40,18 @@ def __init__(self, features: FeaturesSpecification = ...): """ self.features = features - def execute(self, eopatch): + def execute(self, eopatch: EOPatch) -> EOPatch: return eopatch.copy(features=self.features) class DeepCopyTask(CopyTask): """Makes a deep copy of the given EOPatch.""" - def execute(self, eopatch): + def execute(self, eopatch: EOPatch) -> EOPatch: return eopatch.copy(features=self.features, deep=True) -class IOTask(EOTask, metaclass=ABCMeta): +class IOTask(EOTask, metaclass=ABCMeta): # noqa B024 """An abstract Input/Output task that can handle a path and a filesystem object.""" def __init__( @@ -82,10 +82,6 @@ def filesystem(self) -> FS: return unpickle_fs(self._pickled_filesystem) - @abstractmethod - def execute(self, *eopatches, **kwargs): - """Implement execute function""" - class SaveTask(IOTask): """Saves the given EOPatch to a filesystem.""" diff --git a/core/eolearn/core/eodata_merge.py b/core/eolearn/core/eodata_merge.py index 140125b2d..a511ac146 100644 --- a/core/eolearn/core/eodata_merge.py +++ b/core/eolearn/core/eodata_merge.py @@ -11,16 +11,18 @@ """ from __future__ import annotations +import datetime as dt import functools import itertools as it import warnings -from collections.abc import Callable -from typing import TYPE_CHECKING, Any, Dict, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Sequence, Tuple, Union, cast import numpy as np import pandas as pd from geopandas import GeoDataFrame +from sentinelhub import BBox + from .constants import FeatureType from .exceptions import EORuntimeWarning from .utils.parsing import FeatureParser, FeatureSpec, FeaturesSpecification @@ -29,12 +31,14 @@ if TYPE_CHECKING: from .eodata import EOPatch +OperationInputType = Union[Literal[None, "concatenate", "min", "max", "mean", "median"], Callable] + def merge_eopatches( *eopatches: EOPatch, features: FeaturesSpecification = ..., - time_dependent_op: Union[Literal[None, "concatenate", "min", "max", "mean", "median"], Callable] = None, - timeless_op: Union[Literal[None, "concatenate", "min", "max", "mean", "median"], Callable] = None, + time_dependent_op: OperationInputType = None, + timeless_op: OperationInputType = None, ) -> Dict[FeatureSpec, Any]: """Merge features of given EOPatches into a new EOPatch. @@ -69,7 +73,7 @@ def merge_eopatches( feature_parser = FeatureParser(features) all_features = {feature for eopatch in eopatches for feature in feature_parser.get_features(eopatch)} - eopatch_content = {} + eopatch_content: Dict[FeatureSpec, object] = {} timestamps, order_mask_per_eopatch = _merge_timestamps(eopatches, reduce_timestamps) optimize_raster_temporal = _check_if_optimize(eopatches, time_dependent_op) @@ -92,6 +96,7 @@ def merge_eopatches( eopatch_content[feature] = timestamps if feature_type is FeatureType.META_INFO: + feature_name = cast(str, feature_name) # parser makes sure of it eopatch_content[feature] = _select_meta_info_feature(eopatches, feature_name) if feature_type is FeatureType.BBOX: @@ -100,34 +105,36 @@ def merge_eopatches( return eopatch_content -def _parse_operation(operation_input, is_timeless): +def _parse_operation(operation_input: OperationInputType, is_timeless: bool) -> Callable: """Transforms operation's instruction (i.e. an input string) into a function that can be applied to a list of arrays. If the input already is a function it returns it. """ - if isinstance(operation_input, Callable): - return operation_input - - try: - return { - None: _return_if_equal_operation, - "concatenate": functools.partial(np.concatenate, axis=-1 if is_timeless else 0), - "mean": functools.partial(np.nanmean, axis=0), - "median": functools.partial(np.nanmedian, axis=0), - "min": functools.partial(np.nanmin, axis=0), - "max": functools.partial(np.nanmax, axis=0), - }[operation_input] - except KeyError as exception: - raise ValueError(f"Merge operation {operation_input} is not supported") from exception - - -def _return_if_equal_operation(arrays): + defaults: Dict[Optional[str], Callable] = { + None: _return_if_equal_operation, + "concatenate": functools.partial(np.concatenate, axis=-1 if is_timeless else 0), + "mean": functools.partial(np.nanmean, axis=0), + "median": functools.partial(np.nanmedian, axis=0), + "min": functools.partial(np.nanmin, axis=0), + "max": functools.partial(np.nanmax, axis=0), + } + if operation_input in defaults: + return defaults[operation_input] # type: ignore[index] + + if isinstance(operation_input, Callable): # type: ignore[arg-type] #mypy 0.981 has issues with callable + return cast(Callable, operation_input) + raise ValueError(f"Merge operation {operation_input} is not supported") + + +def _return_if_equal_operation(arrays: np.ndarray) -> bool: """Checks if arrays are all equal and returns first one of them. If they are not equal it raises an error.""" if _all_equal(arrays): return arrays[0] raise ValueError("Cannot merge given arrays because their values are not the same.") -def _merge_timestamps(eopatches, reduce_timestamps): +def _merge_timestamps( + eopatches: Sequence[EOPatch], reduce_timestamps: bool +) -> Tuple[List[dt.datetime], List[np.ndarray]]: """Merges together timestamps from EOPatches. It also prepares a list of masks, one for each EOPatch, how timestamps should be ordered and joined together. """ @@ -138,11 +145,11 @@ def _merge_timestamps(eopatches, reduce_timestamps): return [], [np.array([], dtype=np.int32) for _ in range(len(eopatches))] if reduce_timestamps: - all_timestamps, order_mask = np.unique(all_timestamps, return_inverse=True) - all_timestamps = all_timestamps.tolist() + unique_timestamps, order_mask = np.unique(all_timestamps, return_inverse=True) # type: ignore[call-overload] + ordered_timestamps = unique_timestamps.tolist() else: - order_mask = np.argsort(all_timestamps) - all_timestamps = sorted(all_timestamps) + order_mask = np.argsort(all_timestamps) # type: ignore[arg-type] + ordered_timestamps = sorted(all_timestamps) order_mask = order_mask.tolist() @@ -152,10 +159,10 @@ def _merge_timestamps(eopatches, reduce_timestamps): for eopatch_timestamps in timestamps_per_eopatch ] - return all_timestamps, order_mask_per_eopatch + return ordered_timestamps, order_mask_per_eopatch -def _check_if_optimize(eopatches, operation_input): +def _check_if_optimize(eopatches: Sequence[EOPatch], operation_input: OperationInputType) -> bool: """Checks whether optimisation of `_merge_time_dependent_raster_feature` is possible""" if operation_input not in [None, "mean", "median", "min", "max"]: return False @@ -163,7 +170,13 @@ def _check_if_optimize(eopatches, operation_input): return _all_equal(timestamp_list) -def _merge_time_dependent_raster_feature(eopatches, feature, operation, order_mask_per_eopatch, optimize): +def _merge_time_dependent_raster_feature( + eopatches: Sequence[EOPatch], + feature: FeatureSpec, + operation: Callable, + order_mask_per_eopatch: Sequence[np.ndarray], + optimize: bool, +) -> np.ndarray: """Merges numpy arrays of a time-dependent raster feature with a given operation and masks on how to order and join time raster's time slices. """ @@ -200,7 +213,12 @@ def _merge_time_dependent_raster_feature(eopatches, feature, operation, order_ma return np.array(split_arrays) -def _extract_and_join_time_dependent_feature_values(eopatches, feature, order_mask_per_eopatch, optimize): +def _extract_and_join_time_dependent_feature_values( + eopatches: Sequence[EOPatch], + feature: FeatureSpec, + order_mask_per_eopatch: Sequence[np.ndarray], + optimize: bool, +) -> Tuple[np.ndarray, np.ndarray]: """Collects feature arrays from EOPatches that have them and joins them together. It also joins together corresponding order masks. """ @@ -225,12 +243,14 @@ def _extract_and_join_time_dependent_feature_values(eopatches, feature, order_ma return np.concatenate(arrays, axis=0), np.concatenate(order_masks) -def _is_strictly_increasing(array): +def _is_strictly_increasing(array: np.ndarray) -> bool: """Checks if a 1D array of values is strictly increasing.""" - return (np.diff(array) > 0).all() + return (np.diff(array) > 0).all().astype(bool) -def _merge_timeless_raster_feature(eopatches, feature, operation): +def _merge_timeless_raster_feature( + eopatches: Sequence[EOPatch], feature: FeatureSpec, operation: Callable +) -> np.ndarray: """Merges numpy arrays of a timeless raster feature with a given operation.""" arrays = _extract_feature_values(eopatches, feature) @@ -246,7 +266,7 @@ def _merge_timeless_raster_feature(eopatches, feature, operation): ) from exception -def _merge_vector_feature(eopatches, feature): +def _merge_vector_feature(eopatches: Sequence[EOPatch], feature: FeatureSpec) -> GeoDataFrame: """Merges GeoDataFrames of a vector feature.""" dataframes = _extract_feature_values(eopatches, feature) @@ -266,7 +286,7 @@ def _merge_vector_feature(eopatches, feature): return merged_dataframe -def _select_meta_info_feature(eopatches, feature_name): +def _select_meta_info_feature(eopatches: Sequence[EOPatch], feature_name: str) -> Any: """Selects a value for a meta info feature of a merged EOPatch. By default, the value is the first one.""" values = _extract_feature_values(eopatches, (FeatureType.META_INFO, feature_name)) @@ -280,7 +300,7 @@ def _select_meta_info_feature(eopatches, feature_name): return values[0] -def _get_common_bbox(eopatches): +def _get_common_bbox(eopatches: Sequence[EOPatch]) -> Optional[BBox]: """Makes sure that all EOPatches, which define a bounding box and CRS, define the same ones.""" bboxes = [eopatch.bbox for eopatch in eopatches if eopatch.bbox is not None] @@ -292,13 +312,13 @@ def _get_common_bbox(eopatches): raise ValueError("Cannot merge EOPatches because they are defined for different bounding boxes.") -def _extract_feature_values(eopatches, feature): +def _extract_feature_values(eopatches: Sequence[EOPatch], feature: FeatureSpec) -> List[Any]: """A helper function that extracts a feature values from those EOPatches where a feature exists.""" feature_type, feature_name = feature return [eopatch[feature] for eopatch in eopatches if feature_name in eopatch[feature_type]] -def _all_equal(values): +def _all_equal(values: Union[Sequence[Any], np.ndarray]) -> bool: """A helper function that checks if all values in a given list are equal to each other.""" first_value = values[0] diff --git a/core/eolearn/core/eotask.py b/core/eolearn/core/eotask.py index df4260590..924314cf6 100644 --- a/core/eolearn/core/eotask.py +++ b/core/eolearn/core/eotask.py @@ -21,7 +21,14 @@ from typing import Any, Dict, Iterable, Type, TypeVar, Union from .constants import FeatureType -from .utils.parsing import FeatureParser, parse_feature, parse_features, parse_renamed_feature, parse_renamed_features +from .utils.parsing import ( + FeatureParser, + FeaturesSpecification, + parse_feature, + parse_features, + parse_renamed_feature, + parse_renamed_features, +) from .utils.types import EllipsisType LOGGER = logging.getLogger(__name__) @@ -70,7 +77,7 @@ def execute(self, *eopatches, **kwargs): @staticmethod def get_feature_parser( - features, allowed_feature_types: Union[Iterable[FeatureType], EllipsisType] = ... + features: FeaturesSpecification, allowed_feature_types: Union[Iterable[FeatureType], EllipsisType] = ... ) -> FeatureParser: """See :class:`FeatureParser`.""" return FeatureParser(features, allowed_feature_types=allowed_feature_types) From 598a2341da913f97410deecbf174a4ad6f573b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Wed, 5 Oct 2022 09:33:07 +0200 Subject: [PATCH 20/24] delay annotations in core_tasks for docbuilding purposes (#487) --- core/eolearn/core/core_tasks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/eolearn/core/core_tasks.py b/core/eolearn/core/core_tasks.py index e3daea1a0..6df8bb974 100644 --- a/core/eolearn/core/core_tasks.py +++ b/core/eolearn/core/core_tasks.py @@ -11,6 +11,8 @@ This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree. """ +from __future__ import annotations + import copy from abc import ABCMeta from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union, cast From 97512832510d8aefbe6c38f9ced9244ff53ffcb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Wed, 5 Oct 2022 09:50:21 +0200 Subject: [PATCH 21/24] remove redundant types in docstring of save method (#488) --- core/eolearn/core/eodata.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/core/eolearn/core/eodata.py b/core/eolearn/core/eodata.py index 07f51e847..d3c3868db 100644 --- a/core/eolearn/core/eodata.py +++ b/core/eolearn/core/eodata.py @@ -579,18 +579,13 @@ def save( """Method to save an EOPatch from memory to a storage. :param path: A location where to save EOPatch. It can be either a local path or a remote URL path. - :type path: str :param features: A collection of features types specifying features of which type will be saved. By default, all features will be saved. - :type features: list(FeatureType) or list((FeatureType, str)) or ... :param overwrite_permission: A level of permission for overwriting an existing EOPatch - :type overwrite_permission: OverwritePermission or int :param compress_level: A level of data compression and can be specified with an integer from 0 (no compression) to 9 (highest compression). - :type compress_level: int :param filesystem: An existing filesystem object. If not given it will be initialized according to the `path` parameter. - :type filesystem: fs.FS or None """ if filesystem is None: filesystem = get_filesystem(path, create=True) From f6defdbb05870dc51120efdab6a57fcc7681893e Mon Sep 17 00:00:00 2001 From: jgersak <112631680+jgersak@users.noreply.github.com> Date: Wed, 5 Oct 2022 15:33:02 +0200 Subject: [PATCH 22/24] Add tests for feature parser (#486) * Add tests for feature parser * renames, add tests and add a docstring * add test for allowed feature types * add feature type BBOX in tests, rename, docstrings * rename add TIMESTAMP test and shorten tests * add removed test case * add tests * inline test cases for first two tests * make parsing of ... deterministic * clean rest of test cases * remove "parsing" from test descriptions * add removed test Co-authored-by: Ziga Luksic --- core/eolearn/core/utils/parsing.py | 5 +- core/eolearn/tests/test_utils/test_parsing.py | 297 ++++++++++++++++++ 2 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 core/eolearn/tests/test_utils/test_parsing.py diff --git a/core/eolearn/core/utils/parsing.py b/core/eolearn/core/utils/parsing.py index c98253592..072571e97 100644 --- a/core/eolearn/core/utils/parsing.py +++ b/core/eolearn/core/utils/parsing.py @@ -15,7 +15,6 @@ import contextlib import sys -from itertools import repeat from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Tuple, Union, cast from ..constants import FeatureType @@ -146,7 +145,9 @@ def _parse_features(self, features: FeaturesSpecification) -> List[_ParserFeatur return self._parse_sequence(features) if features is ...: - return list(zip(self.allowed_feature_types, repeat(None), repeat(None))) + # we sort allowed_feature_types to keep behaviour deterministic + ftypes = sorted(self.allowed_feature_types, key=lambda ftype: ftype.value) + return [(ftype, None, None) for ftype in ftypes] raise ValueError( f"Unable to parse features {features}. Please see specifications of FeatureParser on viable inputs." diff --git a/core/eolearn/tests/test_utils/test_parsing.py b/core/eolearn/tests/test_utils/test_parsing.py new file mode 100644 index 000000000..664c23a16 --- /dev/null +++ b/core/eolearn/tests/test_utils/test_parsing.py @@ -0,0 +1,297 @@ +import datetime as dt +from dataclasses import dataclass +from typing import Iterable, List, Optional, Tuple, Union + +import numpy as np +import pytest + +from sentinelhub import CRS, BBox + +from eolearn.core import EOPatch, FeatureParser, FeatureType +from eolearn.core.utils.parsing import FeatureRenameSpec, FeatureSpec, FeaturesSpecification +from eolearn.core.utils.types import EllipsisType + + +@dataclass +class TestCase: + input: FeaturesSpecification + features: List[FeatureSpec] + renaming: List[FeatureRenameSpec] + specifications: Optional[List[Tuple[FeatureType, Union[str, EllipsisType]]]] = None + description: str = "" + + +def get_test_case_description(test_case: TestCase) -> str: + return test_case.description + + +@pytest.mark.parametrize( + "test_case", + [ + TestCase(input=[], features=[], renaming=[], specifications=[], description="Empty input"), + TestCase( + input=(FeatureType.DATA, "bands"), + features=[(FeatureType.DATA, "bands")], + renaming=[(FeatureType.DATA, "bands", "bands")], + specifications=[(FeatureType.DATA, "bands")], + description="Singleton feature", + ), + TestCase( + input=FeatureType.BBOX, + features=[(FeatureType.BBOX, None)], + renaming=[(FeatureType.BBOX, None, None)], + specifications=[(FeatureType.BBOX, ...)], + description="BBox feature", + ), + TestCase( + input=(FeatureType.MASK, "CLM", "new_CLM"), + features=[(FeatureType.MASK, "CLM")], + renaming=[(FeatureType.MASK, "CLM", "new_CLM")], + specifications=[(FeatureType.MASK, "CLM")], + description="Renamed feature", + ), + TestCase( + input=[FeatureType.BBOX, (FeatureType.DATA, "bands"), (FeatureType.VECTOR_TIMELESS, "geoms")], + features=[(FeatureType.BBOX, None), (FeatureType.DATA, "bands"), (FeatureType.VECTOR_TIMELESS, "geoms")], + renaming=[ + (FeatureType.BBOX, None, None), + (FeatureType.DATA, "bands", "bands"), + (FeatureType.VECTOR_TIMELESS, "geoms", "geoms"), + ], + specifications=[ + (FeatureType.BBOX, ...), + (FeatureType.DATA, "bands"), + (FeatureType.VECTOR_TIMELESS, "geoms"), + ], + description="List of inputs", + ), + TestCase( + input=((FeatureType.TIMESTAMP, ...), (FeatureType.MASK, "CLM"), (FeatureType.SCALAR, "a", "b")), + features=[(FeatureType.TIMESTAMP, None), (FeatureType.MASK, "CLM"), (FeatureType.SCALAR, "a")], + renaming=[ + (FeatureType.TIMESTAMP, None, None), + (FeatureType.MASK, "CLM", "CLM"), + (FeatureType.SCALAR, "a", "b"), + ], + specifications=[(FeatureType.TIMESTAMP, ...), (FeatureType.MASK, "CLM"), (FeatureType.SCALAR, "a")], + description="Tuple of inputs with rename", + ), + TestCase( + input={ + FeatureType.DATA: ["bands_S2", ("bands_l8", "BANDS_L8")], + FeatureType.MASK_TIMELESS: [], + FeatureType.BBOX: ..., + FeatureType.TIMESTAMP: None, + }, + features=[ + (FeatureType.DATA, "bands_S2"), + (FeatureType.DATA, "bands_l8"), + (FeatureType.BBOX, None), + (FeatureType.TIMESTAMP, None), + ], + renaming=[ + (FeatureType.DATA, "bands_S2", "bands_S2"), + (FeatureType.DATA, "bands_l8", "BANDS_L8"), + (FeatureType.BBOX, None, None), + (FeatureType.TIMESTAMP, None, None), + ], + specifications=[ + (FeatureType.DATA, "bands_S2"), + (FeatureType.DATA, "bands_l8"), + (FeatureType.BBOX, ...), + (FeatureType.TIMESTAMP, ...), + ], + description="Dictionary", + ), + ], + ids=get_test_case_description, +) +def test_feature_parser_no_eopatch(test_case: TestCase): + """Test that input is parsed according to our expectations. No EOPatch provided.""" + parser = FeatureParser(test_case.input) + assert parser.get_features() == test_case.features + assert parser.get_renamed_features() == test_case.renaming + assert parser.get_feature_specifications() == test_case.specifications + + +@pytest.mark.parametrize( + "test_input, specifications", + [ + [(FeatureType.DATA, ...), [(FeatureType.DATA, ...)]], + [ + [FeatureType.BBOX, (FeatureType.MASK, "CLM"), FeatureType.DATA], + [(FeatureType.BBOX, ...), (FeatureType.MASK, "CLM"), (FeatureType.DATA, ...)], + ], + [ + {FeatureType.BBOX: None, FeatureType.MASK: ["CLM"], FeatureType.DATA: ...}, + [(FeatureType.BBOX, ...), (FeatureType.MASK, "CLM"), (FeatureType.DATA, ...)], + ], + ], +) +def test_feature_parser_no_eopatch_failure( + test_input: FeaturesSpecification, specifications: List[Tuple[FeatureType, Union[str, EllipsisType]]] +): + """When a get-all request `...` without an eopatch the parser should fail unless parsing specifications.""" + parser = FeatureParser(test_input) + with pytest.raises(ValueError): + parser.get_features() + with pytest.raises(ValueError): + parser.get_renamed_features() + assert parser.get_feature_specifications() == specifications + + +@pytest.mark.parametrize( + "test_input, allowed_types", + [ + [ + ( + (FeatureType.DATA, "bands", "new_bands"), + (FeatureType.MASK, "IS_VALID", "new_IS_VALID"), + (FeatureType.MASK, "CLM", "new_CLM"), + ), + (FeatureType.MASK,), + ], + [ + { + FeatureType.MASK: ["CLM", "IS_VALID"], + FeatureType.DATA: [("bands", "new_bands")], + FeatureType.BBOX: None, + }, + ( + FeatureType.MASK, + FeatureType.DATA, + ), + ], + ], +) +def test_allowed_feature_types(test_input: FeaturesSpecification, allowed_types: Iterable[FeatureType]): + """Ensure that the parser raises an error if features don't comply with allowed feature types.""" + with pytest.raises(ValueError): + FeatureParser(features=test_input, allowed_feature_types=allowed_types) + + +@pytest.fixture(name="eopatch", scope="session") +def eopatch_fixture(): + return EOPatch( + data=dict(data=np.zeros((2, 2, 2, 2)), CLP=np.zeros((2, 2, 2, 2))), # name duplication intentional + bbox=BBox((1, 2, 3, 4), CRS.WGS84), + timestamp=[dt.datetime(2020, 5, 1), dt.datetime(2020, 5, 25)], + mask=dict(data=np.zeros((2, 2, 2, 2), dtype=int), IS_VALID=np.zeros((2, 2, 2, 2), dtype=int)), + mask_timeless=dict(LULC=np.zeros((2, 2, 2), dtype=int)), + meta_info={"something": "else"}, + ) + + +@pytest.mark.parametrize( + "test_case", + [ + TestCase( + input=..., + features=[ + (FeatureType.BBOX, None), + (FeatureType.DATA, "data"), + (FeatureType.DATA, "CLP"), + (FeatureType.MASK, "data"), + (FeatureType.MASK, "IS_VALID"), + (FeatureType.MASK_TIMELESS, "LULC"), + (FeatureType.META_INFO, "something"), + (FeatureType.TIMESTAMP, None), + ], + renaming=[ + (FeatureType.BBOX, None, None), + (FeatureType.DATA, "data", "data"), + (FeatureType.DATA, "CLP", "CLP"), + (FeatureType.MASK, "data", "data"), + (FeatureType.MASK, "IS_VALID", "IS_VALID"), + (FeatureType.MASK_TIMELESS, "LULC", "LULC"), + (FeatureType.META_INFO, "something", "something"), + (FeatureType.TIMESTAMP, None, None), + ], + description="Get-all", + ), + TestCase( + input=(FeatureType.DATA, ...), + features=[(FeatureType.DATA, "data"), (FeatureType.DATA, "CLP")], + renaming=[(FeatureType.DATA, "data", "data"), (FeatureType.DATA, "CLP", "CLP")], + description="Get-all for a feature type", + ), + TestCase( + input=[ + FeatureType.BBOX, + FeatureType.MASK, + (FeatureType.META_INFO, ...), + (FeatureType.MASK_TIMELESS, "LULC", "new_LULC"), + ], + features=[ + (FeatureType.BBOX, None), + (FeatureType.MASK, "data"), + (FeatureType.MASK, "IS_VALID"), + (FeatureType.META_INFO, "something"), + (FeatureType.MASK_TIMELESS, "LULC"), + ], + renaming=[ + (FeatureType.BBOX, None, None), + (FeatureType.MASK, "data", "data"), + (FeatureType.MASK, "IS_VALID", "IS_VALID"), + (FeatureType.META_INFO, "something", "something"), + (FeatureType.MASK_TIMELESS, "LULC", "new_LULC"), + ], + description="Sequence with ellipsis", + ), + TestCase( + input={ + FeatureType.DATA: ["data", ("CLP", "new_CLP")], + FeatureType.MASK_TIMELESS: ..., + }, + features=[(FeatureType.DATA, "data"), (FeatureType.DATA, "CLP"), (FeatureType.MASK_TIMELESS, "LULC")], + renaming=[ + (FeatureType.DATA, "data", "data"), + (FeatureType.DATA, "CLP", "new_CLP"), + (FeatureType.MASK_TIMELESS, "LULC", "LULC"), + ], + description="Dictionary with ellipsis", + ), + TestCase( + input={FeatureType.VECTOR: ...}, features=[], renaming=[], description="Request all of an empty feature" + ), + ], + ids=get_test_case_description, +) +def test_feature_parser_with_eopatch(test_case: TestCase, eopatch: EOPatch): + """Test that input is parsed according to our expectations. EOPatch provided.""" + parser = FeatureParser(test_case.input) + assert parser.get_features(eopatch) == test_case.features, f"{parser.get_features(eopatch)}" + assert parser.get_renamed_features(eopatch) == test_case.renaming + + +@pytest.mark.parametrize( + "test_input", + [ + (FeatureType.VECTOR, "geoms"), + {FeatureType.DATA: ["data"], FeatureType.MASK: ["bands_l8"]}, + (FeatureType.MASK, (FeatureType.SCALAR, "something", "else")), + ], +) +def test_feature_parser_with_eopatch_failure(test_input: FeaturesSpecification, eopatch: EOPatch): + """These cases should fail because the request feature is not part of the EOPatch.""" + parser = FeatureParser(test_input) + with pytest.raises(ValueError): + parser.get_features(eopatch) + with pytest.raises(ValueError): + parser.get_renamed_features(eopatch) + + +def test_all_features_allowed_feature_types(eopatch: EOPatch): + """Ensure that allowed_feature_types is respected when requesting all features.""" + parser = FeatureParser(..., allowed_feature_types=(FeatureType.DATA, FeatureType.BBOX)) + assert parser.get_feature_specifications() == [(FeatureType.BBOX, ...), (FeatureType.DATA, ...)] + assert parser.get_features(eopatch) == [ + (FeatureType.BBOX, None), + (FeatureType.DATA, "data"), + (FeatureType.DATA, "CLP"), + ] + assert parser.get_renamed_features(eopatch) == [ + (FeatureType.BBOX, None, None), + (FeatureType.DATA, "data", "data"), + (FeatureType.DATA, "CLP", "CLP"), + ] From a17056b25f87a5b8957d0c8838cf94b34b217c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Thu, 6 Oct 2022 12:47:42 +0200 Subject: [PATCH 23/24] adjust to catalog 1.0.0 (#468) --- io/eolearn/io/sentinelhub_process.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/io/eolearn/io/sentinelhub_process.py b/io/eolearn/io/sentinelhub_process.py index 8e9285a2f..00f3921ff 100644 --- a/io/eolearn/io/sentinelhub_process.py +++ b/io/eolearn/io/sentinelhub_process.py @@ -720,11 +720,11 @@ def get_available_timestamps( :param config: A configuration object. :return: A list of timestamps of available observations. """ - query = None + query_filter = None if maxcc is not None and data_collection.has_cloud_coverage: if isinstance(maxcc, (int, float)) and (maxcc < 0 or maxcc > 1): raise ValueError('Maximum cloud coverage "maxcc" parameter should be a float on an interval [0, 1]') - query = {"eo:cloud_cover": {"lte": int(maxcc * 100)}} + query_filter = f"eo:cloud_cover < {int(maxcc * 100)}" fields = {"include": ["properties.datetime"], "exclude": []} @@ -734,7 +734,7 @@ def get_available_timestamps( catalog = SentinelHubCatalog(config=config) search_iterator = catalog.search( - collection=data_collection, bbox=bbox, time=time_interval, query=query, fields=fields + collection=data_collection, bbox=bbox, time=time_interval, filter=query_filter, fields=fields ) all_timestamps = search_iterator.get_timestamps() From ecc8d80264a60867206e4fecff436b3f18b8971a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Luk=C5=A1i=C4=8D?= <31988337+zigaLuksic@users.noreply.github.com> Date: Thu, 6 Oct 2022 12:56:35 +0200 Subject: [PATCH 24/24] Increase version to 1.3.0 (#489) * increase versions * bump requirement for typing-extensions * Update changelog * add py.typed to package data --- CHANGELOG.md | 13 +++++++++++++ core/MANIFEST.in | 1 + core/eolearn/core/__init__.py | 2 +- core/requirements.txt | 2 +- core/setup.py | 1 + .../eolearn/coregistration/__init__.py | 2 +- features/eolearn/features/__init__.py | 2 +- geometry/eolearn/geometry/__init__.py | 2 +- io/eolearn/io/__init__.py | 2 +- io/requirements.txt | 2 +- mask/eolearn/mask/__init__.py | 2 +- ml_tools/eolearn/ml_tools/__init__.py | 2 +- setup.py | 18 +++++++++--------- .../eolearn/visualization/__init__.py | 2 +- 14 files changed, 34 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3e7603e0..b8a718ebd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## [Version 1.3.0] - 2022-10-06 + +- (**codebreaking**) Adapted Sentinel Hub tasks to `sentinelhub-py 3.8.0` which switched to Catalog 1.0.0. +- (**codebreaking**) Removed support for loading pickled objects in EOPatches (deprecated since version 1.0.0). +- (**codebreaking**) Various improvements of `FeatureIO` class. Only affects direct use of class. +- Added type annotations to majority of `eolearn.core`. The types are now exposed via `py.typed` file, which enables use of `mypy`. Added type-checking to CI for the `core` module. +- Numpy-array based features can now save and load `object` populated arrays. +- Improved documentation building, fixed links to GitHub. +- Improved test coverage. +- Added pre-commit hooks to repository for easier development. +- Various minor improvements. + + ## [Version 1.2.1] - 2022-09-12 - Corrected the default for `no_data_value` in `ImportFromTiffTask` and `ExportToTiffTask` to `None`. The previous default of `0` was a poor choice in many scenarios. The switch might alter behavior in existing code. diff --git a/core/MANIFEST.in b/core/MANIFEST.in index 2efa68caa..4b271965f 100644 --- a/core/MANIFEST.in +++ b/core/MANIFEST.in @@ -1,6 +1,7 @@ include requirements*.txt include LICENSE include README.md +include eolearn/core/py.typed exclude eolearn/tests/* exclude eolearn/tests/test_extra/* exclude eolearn/tests/test_utils/* diff --git a/core/eolearn/core/__init__.py b/core/eolearn/core/__init__.py index 73fc0577e..f9347af56 100644 --- a/core/eolearn/core/__init__.py +++ b/core/eolearn/core/__init__.py @@ -32,4 +32,4 @@ from .utils.parallelize import execute_with_mp_lock, join_futures, join_futures_iter, parallelize from .utils.parsing import FeatureParser -__version__ = "1.2.1" +__version__ = "1.3.0" diff --git a/core/requirements.txt b/core/requirements.txt index 3155af9f6..e42c2209a 100644 --- a/core/requirements.txt +++ b/core/requirements.txt @@ -7,4 +7,4 @@ numpy>=1.20.0 python-dateutil sentinelhub>=3.4.4 tqdm>=4.27 -typing-extensions;python_version<"3.8" +typing-extensions;python_version<"3.10" diff --git a/core/setup.py b/core/setup.py index 70e6266ab..f9908c8ff 100644 --- a/core/setup.py +++ b/core/setup.py @@ -45,6 +45,7 @@ def get_version(): author_email="eoresearch@sinergise.com", license="MIT", packages=find_packages(exclude=["eolearn.tests*"]), + package_data={"eolearn": ["core/py.typed"]}, include_package_data=True, install_requires=parse_requirements("requirements.txt"), extras_require={"RAY": parse_requirements("requirements-ray.txt")}, diff --git a/coregistration/eolearn/coregistration/__init__.py b/coregistration/eolearn/coregistration/__init__.py index b544eb081..a7dad3f22 100644 --- a/coregistration/eolearn/coregistration/__init__.py +++ b/coregistration/eolearn/coregistration/__init__.py @@ -4,4 +4,4 @@ from .coregistration import ECCRegistrationTask, InterpolationType, PointBasedRegistrationTask, RegistrationTask -__version__ = "1.2.1" +__version__ = "1.3.0" diff --git a/features/eolearn/features/__init__.py b/features/eolearn/features/__init__.py index ee524518a..5b0567c76 100644 --- a/features/eolearn/features/__init__.py +++ b/features/eolearn/features/__init__.py @@ -38,4 +38,4 @@ AddSpatioTemporalFeaturesTask, ) -__version__ = "1.2.1" +__version__ = "1.3.0" diff --git a/geometry/eolearn/geometry/__init__.py b/geometry/eolearn/geometry/__init__.py index 72d5964ef..66f733ee6 100644 --- a/geometry/eolearn/geometry/__init__.py +++ b/geometry/eolearn/geometry/__init__.py @@ -11,4 +11,4 @@ ) from .transformations import RasterToVectorTask, VectorToRasterTask -__version__ = "1.2.1" +__version__ = "1.3.0" diff --git a/io/eolearn/io/__init__.py b/io/eolearn/io/__init__.py index c0efed38b..589592fc9 100644 --- a/io/eolearn/io/__init__.py +++ b/io/eolearn/io/__init__.py @@ -13,4 +13,4 @@ get_available_timestamps, ) -__version__ = "1.2.1" +__version__ = "1.3.0" diff --git a/io/requirements.txt b/io/requirements.txt index 3090aaac5..76f2bd037 100644 --- a/io/requirements.txt +++ b/io/requirements.txt @@ -5,4 +5,4 @@ fiona>=1.8.18 geopandas>=0.8.1 rasterio>=1.2.7 rtree -sentinelhub>=3.5.1 +sentinelhub>=3.8.0 diff --git a/mask/eolearn/mask/__init__.py b/mask/eolearn/mask/__init__.py index 1780e77d0..fcba92689 100644 --- a/mask/eolearn/mask/__init__.py +++ b/mask/eolearn/mask/__init__.py @@ -8,4 +8,4 @@ from .snow_mask import SnowMaskTask, TheiaSnowMaskTask from .utils import resize_images -__version__ = "1.2.1" +__version__ = "1.3.0" diff --git a/ml_tools/eolearn/ml_tools/__init__.py b/ml_tools/eolearn/ml_tools/__init__.py index e29513d29..f7a309be5 100644 --- a/ml_tools/eolearn/ml_tools/__init__.py +++ b/ml_tools/eolearn/ml_tools/__init__.py @@ -5,4 +5,4 @@ from .sampling import BlockSamplingTask, FractionSamplingTask, GridSamplingTask, sample_by_values from .train_test_split import TrainTestSplitTask -__version__ = "1.2.1" +__version__ = "1.3.0" diff --git a/setup.py b/setup.py index 15019a362..60036dc2f 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def parse_requirements(file): setup( name="eo-learn", python_requires=">=3.7", - version="1.2.1", + version="1.3.0", description="Earth observation processing framework for machine learning in Python", long_description=get_long_description(), long_description_content_type="text/markdown", @@ -38,14 +38,14 @@ def parse_requirements(file): packages=[], include_package_data=True, install_requires=[ - "eo-learn-core==1.2.1", - "eo-learn-coregistration==1.2.1", - "eo-learn-features==1.2.1", - "eo-learn-geometry==1.2.1", - "eo-learn-io==1.2.1", - "eo-learn-mask==1.2.1", - "eo-learn-ml-tools==1.2.1", - "eo-learn-visualization==1.2.1", + "eo-learn-core==1.3.0", + "eo-learn-coregistration==1.3.0", + "eo-learn-features==1.3.0", + "eo-learn-geometry==1.3.0", + "eo-learn-io==1.3.0", + "eo-learn-mask==1.3.0", + "eo-learn-ml-tools==1.3.0", + "eo-learn-visualization==1.3.0", ], extras_require={"DEV": parse_requirements("requirements-dev.txt")}, zip_safe=False, diff --git a/visualization/eolearn/visualization/__init__.py b/visualization/eolearn/visualization/__init__.py index 5af9d6c63..d337c77d9 100644 --- a/visualization/eolearn/visualization/__init__.py +++ b/visualization/eolearn/visualization/__init__.py @@ -4,4 +4,4 @@ from .eopatch import PlotBackend, PlotConfig -__version__ = "1.2.1" +__version__ = "1.3.0"