diff --git a/copernicusmarine/catalogue_parser/models.py b/copernicusmarine/catalogue_parser/models.py index fc4dd33e..ad10f1cc 100644 --- a/copernicusmarine/catalogue_parser/models.py +++ b/copernicusmarine/catalogue_parser/models.py @@ -100,7 +100,7 @@ class WebApi(Enum): class ServiceNotHandled(Exception): - ... + pass # service formats @@ -175,11 +175,11 @@ def _format_admp_valid_start_date( if to_timestamp: return int( datetime_parser( - arco_data_metadata_producer_valid_start_date.split(".")[0] + arco_data_metadata_producer_valid_start_date ).timestamp() * 1000 ) - return arco_data_metadata_producer_valid_start_date.split(".")[0] + return arco_data_metadata_producer_valid_start_date def _convert_elevation_to_depth(self): self.coordinates_id = "depth" @@ -507,11 +507,11 @@ def filter_only_official_versions_and_parts(self): # Errors class DatasetVersionPartNotFound(Exception): - ... + pass class DatasetVersionNotFound(Exception): - ... + pass def dataset_version_part_not_found_exception( diff --git a/copernicusmarine/catalogue_parser/request_structure.py b/copernicusmarine/catalogue_parser/request_structure.py index 63e98b7c..b28ab788 100644 --- a/copernicusmarine/catalogue_parser/request_structure.py +++ b/copernicusmarine/catalogue_parser/request_structure.py @@ -3,10 +3,11 @@ import pathlib import re from dataclasses import dataclass, field -from datetime import datetime from json import load from typing import Any, Dict, List, Optional +from pendulum import DateTime + from copernicusmarine.core_functions.deprecated_options import ( DEPRECATED_OPTIONS, ) @@ -41,8 +42,8 @@ class DatasetTimeAndSpaceSubset: maximum_latitude: Optional[float] = None minimum_depth: Optional[float] = None maximum_depth: Optional[float] = None - start_datetime: Optional[datetime] = None - end_datetime: Optional[datetime] = None + start_datetime: Optional[DateTime] = None + end_datetime: Optional[DateTime] = None @dataclass @@ -59,8 +60,8 @@ class SubsetRequest: minimum_depth: Optional[float] = None maximum_depth: Optional[float] = None vertical_dimension_as_originally_produced: bool = True - start_datetime: Optional[datetime] = None - end_datetime: Optional[datetime] = None + start_datetime: Optional[DateTime] = None + end_datetime: Optional[DateTime] = None subset_method: SubsetMethod = DEFAULT_SUBSET_METHOD output_filename: Optional[str] = None file_format: FileFormat = DEFAULT_FILE_FORMAT diff --git a/copernicusmarine/command_line_interface/group_subset.py b/copernicusmarine/command_line_interface/group_subset.py index f53a4d94..267ed421 100644 --- a/copernicusmarine/command_line_interface/group_subset.py +++ b/copernicusmarine/command_line_interface/group_subset.py @@ -4,6 +4,7 @@ from typing import List, Optional import click +import pendulum from copernicusmarine.command_line_interface.exception_handler import ( log_exception_and_exit, @@ -400,8 +401,12 @@ def subset( minimum_depth, maximum_depth, vertical_dimension_as_originally_produced, - start_datetime, - end_datetime, + ( + start_datetime + if not start_datetime + else pendulum.instance(start_datetime) + ), + end_datetime if not end_datetime else pendulum.instance(end_datetime), subset_method, output_filename, file_format, diff --git a/copernicusmarine/core_functions/credentials_utils.py b/copernicusmarine/core_functions/credentials_utils.py index 2d23304f..9373ac34 100644 --- a/copernicusmarine/core_functions/credentials_utils.py +++ b/copernicusmarine/core_functions/credentials_utils.py @@ -36,12 +36,10 @@ # TODO: handle cache of the credentials without cachier -class CredentialCannotBeNone(Exception): - ... +class CredentialCannotBeNone(Exception): ... -class InvalidUsernameOrPassword(Exception): - ... +class InvalidUsernameOrPassword(Exception): ... def _load_credential_from_copernicus_marine_configuration_file( diff --git a/copernicusmarine/core_functions/services_utils.py b/copernicusmarine/core_functions/services_utils.py index dc0ccb8c..7fbf0cc8 100644 --- a/copernicusmarine/core_functions/services_utils.py +++ b/copernicusmarine/core_functions/services_utils.py @@ -181,8 +181,16 @@ def _get_best_arco_service_type( time_size = get_size_of_coordinate_subset( dataset, "time", - dataset_subset.start_datetime, - dataset_subset.end_datetime, + ( + dataset_subset.start_datetime.in_tz("UTC").naive() + if dataset_subset.start_datetime + else dataset_subset.start_datetime + ), + ( + dataset_subset.end_datetime.in_tz("UTC").naive() + if dataset_subset.end_datetime + else dataset_subset.end_datetime + ), ) dataset_coordinates = dataset.coords @@ -444,7 +452,7 @@ def _get_dataset_start_date_from_service( class ServiceNotAvailable(Exception): - ... + pass def _warning_dataset_will_be_deprecated( @@ -498,7 +506,7 @@ def _service_not_available_error( class NoServiceAvailable(Exception): - ... + pass def _no_service_available_for_command( diff --git a/copernicusmarine/core_functions/subset.py b/copernicusmarine/core_functions/subset.py index e325b54f..3da85af4 100644 --- a/copernicusmarine/core_functions/subset.py +++ b/copernicusmarine/core_functions/subset.py @@ -1,9 +1,10 @@ import json import logging import pathlib -from datetime import datetime from typing import List, Optional +from pendulum import DateTime + from copernicusmarine.catalogue_parser.models import ( CopernicusMarineDatasetServiceType, CopernicusMarineServiceFormat, @@ -51,8 +52,8 @@ def subset_function( minimum_depth: Optional[float], maximum_depth: Optional[float], vertical_dimension_as_originally_produced: bool, - start_datetime: Optional[datetime], - end_datetime: Optional[datetime], + start_datetime: Optional[DateTime], + end_datetime: Optional[DateTime], subset_method: SubsetMethod, output_filename: Optional[str], file_format: FileFormat, diff --git a/copernicusmarine/core_functions/utils.py b/copernicusmarine/core_functions/utils.py index 7cc94b08..57a3e70a 100644 --- a/copernicusmarine/core_functions/utils.py +++ b/copernicusmarine/core_functions/utils.py @@ -2,7 +2,6 @@ import logging import pathlib import re -from datetime import datetime, timezone from importlib.metadata import version from typing import ( Any, @@ -12,6 +11,7 @@ Iterable, Iterator, List, + Literal, Optional, Tuple, TypeVar, @@ -21,7 +21,10 @@ import cftime import numpy import pandas as pd +import pendulum +import pendulum.exceptions import xarray +from pendulum import DateTime from requests import PreparedRequest from copernicusmarine import __version__ as copernicusmarine_version @@ -48,11 +51,6 @@ "%Y-%m-%d %H:%M:%S.%f%Z", ] -DATETIME_NON_ISO_FORMATS = [ - "%Y", - "%Y-%m-%dT%H:%M:%S.%fZ", -] - def get_unique_filename( filepath: pathlib.Path, overwrite_option: bool @@ -126,27 +124,44 @@ def construct_query_params_for_marine_data_store_monitoring( class WrongDatetimeFormat(Exception): - ... + pass -def datetime_parser(string: str) -> datetime: - if string == "now": - return datetime.now(tz=timezone.utc).replace(tzinfo=None) +def datetime_parser(date: Union[str, numpy.datetime64]) -> DateTime: + if date == "now": + return pendulum.now(tz="UTC") try: - parsed_datetime = datetime.fromisoformat(string) - if parsed_datetime.tzinfo is None: - return parsed_datetime - else: - return parsed_datetime.astimezone(timezone.utc).replace( - tzinfo=None - ) - except ValueError: - for datetime_format in DATETIME_NON_ISO_FORMATS: - try: - return datetime.strptime(string, datetime_format) - except ValueError: - pass - raise WrongDatetimeFormat(string) + if isinstance(date, numpy.datetime64): + date = str(date) + parsed_datetime = pendulum.parse(date) + # ignoring types because one needs to pass + # `exact=True` to `parse` method to get + # something else than `pendulum.DateTime` + return parsed_datetime # type: ignore + except pendulum.exceptions.ParserError: + pass + raise WrongDatetimeFormat(date) + + +def timestamp_parser( + timestamp: Union[int, float], unit: Literal["s", "ms"] = "ms" +) -> DateTime: + """ + Convert a timestamp in milliseconds to a pendulum DateTime object + by default. The unit can be changed to seconds by passing "s" as + the unit. + """ + conversion_factor = 1 if unit == "s" else 10e3 + return pendulum.from_timestamp(timestamp / conversion_factor, tz="UTC") + + +def timestamp_or_datestring_to_datetime( + date: Union[str, int, numpy.datetime64] +) -> DateTime: + if isinstance(date, int): + return timestamp_parser(date) + else: + return datetime_parser(date) def convert_datetime64_to_netcdf_timestamp( diff --git a/copernicusmarine/download_functions/download_arco_series.py b/copernicusmarine/download_functions/download_arco_series.py index 8ecac34e..09de01bf 100644 --- a/copernicusmarine/download_functions/download_arco_series.py +++ b/copernicusmarine/download_functions/download_arco_series.py @@ -24,14 +24,12 @@ LongitudeParameters, TemporalParameters, ) -from copernicusmarine.download_functions.subset_xarray import ( - date_to_datetime, - subset, -) +from copernicusmarine.download_functions.subset_xarray import subset from copernicusmarine.download_functions.utils import ( FileFormat, get_filename, get_formatted_dataset_size_estimation, + timestamp_or_datestring_to_datetime, ) logger = logging.getLogger("copernicusmarine") @@ -153,7 +151,9 @@ def download_zarr( ) start_datetime = subset_request.start_datetime if dataset_valid_start_date: - minimum_start_date = date_to_datetime(dataset_valid_start_date) + minimum_start_date = timestamp_or_datestring_to_datetime( + dataset_valid_start_date + ) if ( not subset_request.start_datetime or subset_request.start_datetime < minimum_start_date diff --git a/copernicusmarine/download_functions/download_original_files.py b/copernicusmarine/download_functions/download_original_files.py index 1720cc6b..49b3c632 100644 --- a/copernicusmarine/download_functions/download_original_files.py +++ b/copernicusmarine/download_functions/download_original_files.py @@ -1,4 +1,3 @@ -import datetime import logging import os import pathlib @@ -9,8 +8,10 @@ from typing import Iterator, List, Optional, Tuple import click +import pendulum from botocore.client import ClientError from numpy import append, arange +from pendulum import DateTime from tqdm import tqdm from copernicusmarine.catalogue_parser.request_structure import ( @@ -28,6 +29,7 @@ flatten, get_unique_filename, parse_access_dataset_url, + timestamp_parser, ) logger = logging.getLogger("copernicusmarine") @@ -51,7 +53,7 @@ def download_original_files( filenames_in_sync_ignored: list[str] = [] total_size: float = 0.0 sizes: list[float] = [] - last_modified_datetimes: list[datetime.datetime] = [] + last_modified_datetimes: list[DateTime] = [] filenames_in: list[str] = [] if get_request.direct_download: ( @@ -269,7 +271,7 @@ def _download_header( Tuple[str, str], list[str], List[float], - List[datetime.datetime], + List[DateTime], float, list[str], ] @@ -281,7 +283,7 @@ def _download_header( filenames: list[str] = [] sizes: list[float] = [] total_size = 0.0 - last_modified_datetimes: list[datetime.datetime] = [] + last_modified_datetimes: list[DateTime] = [] etags: list[str] = [] raw_filenames = _list_files_on_marine_data_lake_s3( username, endpoint_url, bucket, path, not only_list_root_path @@ -290,6 +292,7 @@ def _download_header( for filename, size, last_modified_datetime, etag in raw_filenames: if not regex or re.search(regex, filename): filenames_without_sync.append(filename) + last_modified_datetime = pendulum.instance(last_modified_datetime) if not sync or _check_needs_to_be_synced( filename, size, last_modified_datetime, directory_out ): @@ -345,7 +348,7 @@ def _download_header_for_direct_download( Tuple[str, str], List[str], List[float], - List[datetime.datetime], + List[DateTime], float, list[str], list[str], @@ -410,7 +413,7 @@ def _download_header_for_direct_download( def _check_needs_to_be_synced( filename: str, size: int, - last_modified_datetime: datetime.datetime, + last_modified_datetime: DateTime, directory_out: pathlib.Path, ) -> bool: filename_out = _local_path_from_s3_url(filename, directory_out) @@ -421,16 +424,19 @@ def _check_needs_to_be_synced( if file_stats.st_size != size: return True else: - last_created_datetime_out = datetime.datetime.fromtimestamp( - file_stats.st_ctime, tz=datetime.timezone.utc + last_created_datetime_out = timestamp_parser( + file_stats.st_mtime, unit="s" ) + # boto3.s3_resource.Object.last_modified is without microsecond + # boto3.paginate s3_object["LastModified"] is with microsecond + last_modified_datetime = last_modified_datetime.set(microsecond=0) return last_modified_datetime > last_created_datetime_out def _create_information_message_before_download( filenames: list[str], sizes: list[float], - last_modified_datetimes: list[datetime.datetime], + last_modified_datetimes: list[DateTime], total_size: float, ) -> str: message = "You requested the download of the following files:\n" @@ -438,13 +444,7 @@ def _create_information_message_before_download( filenames[:20], sizes[:20], last_modified_datetimes[:20] ): message += str(filename) - datetime_iso = re.sub( - r"\+00:00$", - "Z", - last_modified_datetime.astimezone(datetime.timezone.utc).isoformat( - timespec="seconds" - ), - ) + datetime_iso = last_modified_datetime.in_tz("UTC").to_iso8601_string() message += f" - {format_file_size(float(size))} - {datetime_iso}\n" if len(filenames) > 20: message += f"Printed 20 out of {len(filenames)} files\n" @@ -466,7 +466,7 @@ def _list_files_on_marine_data_lake_s3( bucket: str, prefix: str, recursive: bool, -) -> list[tuple[str, int, datetime.datetime, str]]: +) -> list[tuple[str, int, DateTime, str]]: s3_client, _ = get_configured_boto3_session( endpoint_url, ["ListObjects"], username @@ -483,7 +483,7 @@ def _list_files_on_marine_data_lake_s3( *map(lambda page: page.get("Contents", []), page_iterator) ) - files_already_found: list[tuple[str, int, datetime.datetime, str]] = [] + files_already_found: list[tuple[str, int, DateTime, str]] = [] for s3_object in s3_objects: files_already_found.append( ( @@ -498,7 +498,7 @@ def _list_files_on_marine_data_lake_s3( def _get_file_size_and_last_modified( endpoint_url: str, bucket: str, file_in: str, username: str -) -> Optional[Tuple[int, datetime.datetime]]: +) -> Optional[Tuple[int, DateTime]]: s3_client, _ = get_configured_boto3_session( endpoint_url, ["HeadObject"], username ) @@ -508,7 +508,9 @@ def _get_file_size_and_last_modified( Bucket=bucket, Key=file_in.replace(f"s3://{bucket}/", ""), ) - return s3_object["ContentLength"], s3_object["LastModified"] + return s3_object["ContentLength"], pendulum.instance( + s3_object["LastModified"] + ) except ClientError as e: if "404" in str(e): logger.warning( diff --git a/copernicusmarine/download_functions/subset_parameters.py b/copernicusmarine/download_functions/subset_parameters.py index 343e81a8..65128c71 100644 --- a/copernicusmarine/download_functions/subset_parameters.py +++ b/copernicusmarine/download_functions/subset_parameters.py @@ -1,7 +1,8 @@ from dataclasses import dataclass, field -from datetime import datetime from typing import Optional +from pendulum import DateTime + @dataclass class LatitudeParameters: @@ -27,8 +28,8 @@ class GeographicalParameters: @dataclass class TemporalParameters: - start_datetime: Optional[datetime] = None - end_datetime: Optional[datetime] = None + start_datetime: Optional[DateTime] = None + end_datetime: Optional[DateTime] = None @dataclass diff --git a/copernicusmarine/download_functions/subset_xarray.py b/copernicusmarine/download_functions/subset_xarray.py index 39d9afb3..97a13c91 100644 --- a/copernicusmarine/download_functions/subset_xarray.py +++ b/copernicusmarine/download_functions/subset_xarray.py @@ -1,12 +1,11 @@ import logging import typing -from datetime import datetime from decimal import Decimal from typing import List, Literal, Optional, Union import numpy import xarray -from pandas import Timestamp +from pendulum import DateTime from copernicusmarine.catalogue_parser.models import ( CopernicusMarineDatasetServiceType, @@ -21,7 +20,10 @@ VariableDoesNotExistInTheDataset, ) from copernicusmarine.core_functions.models import SubsetMethod -from copernicusmarine.core_functions.utils import ServiceNotSupported +from copernicusmarine.core_functions.utils import ( + ServiceNotSupported, + timestamp_or_datestring_to_datetime, +) from copernicusmarine.download_functions.subset_parameters import ( DepthParameters, GeographicalParameters, @@ -71,7 +73,7 @@ def _dataset_custom_sel( dataset: xarray.Dataset, coord_type: Literal["latitude", "longitude", "depth", "time"], - coord_selection: Union[float, slice, datetime, None], + coord_selection: Union[float, slice, DateTime, None], method: Union[str, None] = None, ) -> xarray.Dataset: for coord_label in COORDINATES_LABEL[coord_type]: @@ -105,8 +107,8 @@ def _dataset_custom_sel( def get_size_of_coordinate_subset( dataset: xarray.Dataset, coordinate: str, - minimum: Optional[Union[float, datetime]], - maximum: Optional[Union[float, datetime]], + minimum: Optional[Union[float, DateTime]], + maximum: Optional[Union[float, DateTime]], ) -> int: for label in COORDINATES_LABEL[coordinate]: if label in dataset.sizes: @@ -220,8 +222,16 @@ def _temporal_subset( dataset: xarray.Dataset, temporal_parameters: TemporalParameters, ) -> xarray.Dataset: - start_datetime = temporal_parameters.start_datetime - end_datetime = temporal_parameters.end_datetime + start_datetime = ( + temporal_parameters.start_datetime.in_tz("UTC").naive() + if temporal_parameters.start_datetime + else temporal_parameters.start_datetime + ) + end_datetime = ( + temporal_parameters.end_datetime.in_tz("UTC").naive() + if temporal_parameters.end_datetime + else temporal_parameters.end_datetime + ) if start_datetime is not None or end_datetime is not None: temporal_selection = ( start_datetime @@ -490,8 +500,12 @@ def check_dataset_subset_bounds( times_min = dataset_valid_date else: times_min = times.min() - dataset_minimum_coordinate_value = date_to_datetime(times_min) - dataset_maximum_coordinate_value = date_to_datetime(times.max()) + dataset_minimum_coordinate_value = ( + timestamp_or_datestring_to_datetime(times_min) + ) + dataset_maximum_coordinate_value = ( + timestamp_or_datestring_to_datetime(times.max()) + ) user_minimum_coordinate_value = ( dataset_subset.start_datetime if dataset_subset.start_datetime is not None @@ -531,20 +545,13 @@ def check_dataset_subset_bounds( ) -def date_to_datetime(date: Union[str, int]) -> datetime: - if isinstance(date, int): - return Timestamp(date * 1e6).to_pydatetime() - else: - return Timestamp(date).to_pydatetime().replace(tzinfo=None) - - @typing.no_type_check def _check_coordinate_overlap( dimension: str, - user_minimum_coordinate_value: Union[float, datetime], - user_maximum_coordinate_value: Union[float, datetime], - dataset_minimum_coordinate_value: Union[float, datetime], - dataset_maximum_coordinate_value: Union[float, datetime], + user_minimum_coordinate_value: Union[float, DateTime], + user_maximum_coordinate_value: Union[float, DateTime], + dataset_minimum_coordinate_value: Union[float, DateTime], + dataset_maximum_coordinate_value: Union[float, DateTime], is_strict: bool, ) -> None: message = ( diff --git a/copernicusmarine/download_functions/utils.py b/copernicusmarine/download_functions/utils.py index 762377e8..e6514fb9 100644 --- a/copernicusmarine/download_functions/utils.py +++ b/copernicusmarine/download_functions/utils.py @@ -1,15 +1,17 @@ import logging -from datetime import datetime from pathlib import Path from typing import Optional import xarray -from pandas import Timestamp +from pendulum import DateTime from copernicusmarine.core_functions.models import ( DEFAULT_FILE_EXTENSIONS, FileFormat, ) +from copernicusmarine.core_functions.utils import ( + timestamp_or_datestring_to_datetime, +) from copernicusmarine.download_functions.subset_xarray import COORDINATES_LABEL logger = logging.getLogger("copernicusmarine") @@ -63,15 +65,14 @@ def _build_filename_from_dataset( min_time_coordinate = _get_min_coordinate(dataset, "time") max_time_coordinate = _get_max_coordinate(dataset, "time") - datetimes = _format_datetimes( ( - Timestamp(min_time_coordinate).to_pydatetime() + timestamp_or_datestring_to_datetime(min_time_coordinate) if min_time_coordinate is not None else None ), ( - Timestamp(max_time_coordinate).to_pydatetime() + timestamp_or_datestring_to_datetime(max_time_coordinate) if max_time_coordinate is not None else None ), @@ -154,17 +155,17 @@ def _format_depths( def _format_datetimes( - minimum_datetime: Optional[datetime], maximum_datetime: Optional[datetime] + minimum_datetime: Optional[DateTime], maximum_datetime: Optional[DateTime] ) -> str: if minimum_datetime is None or maximum_datetime is None: return "" else: if minimum_datetime == maximum_datetime: - formatted_datetime = f"{minimum_datetime.strftime('%Y-%m-%d')}" + formatted_datetime = f"{minimum_datetime.format('YYYY-MM-DD')}" else: formatted_datetime = ( - f"{minimum_datetime.strftime('%Y-%m-%d')}-" - f"{maximum_datetime.strftime('%Y-%m-%d')}" + f"{minimum_datetime.format('YYYY-MM-DD')}-" + f"{maximum_datetime.format('YYYY-MM-DD')}" ) return formatted_datetime diff --git a/copernicusmarine/python_interface/load_utils.py b/copernicusmarine/python_interface/load_utils.py index 34a1837e..16a66f3b 100644 --- a/copernicusmarine/python_interface/load_utils.py +++ b/copernicusmarine/python_interface/load_utils.py @@ -18,7 +18,7 @@ from copernicusmarine.core_functions.utils import ServiceNotSupported from copernicusmarine.download_functions.subset_xarray import ( check_dataset_subset_bounds, - date_to_datetime, + timestamp_or_datestring_to_datetime, ) @@ -56,7 +56,7 @@ def load_data_object_from_load_request( CopernicusMarineDatasetServiceType.STATIC_ARCO, ]: if retrieval_service.dataset_valid_start_date: - parsed_start_datetime = date_to_datetime( + parsed_start_datetime = timestamp_or_datestring_to_datetime( retrieval_service.dataset_valid_start_date ) if ( diff --git a/copernicusmarine/python_interface/utils.py b/copernicusmarine/python_interface/utils.py index ab549b87..697b9cab 100644 --- a/copernicusmarine/python_interface/utils.py +++ b/copernicusmarine/python_interface/utils.py @@ -1,12 +1,17 @@ from datetime import datetime from typing import Optional, Union +import pendulum +from pendulum import DateTime + from copernicusmarine.core_functions.utils import datetime_parser def homogenize_datetime( input_datetime: Optional[Union[datetime, str]] -) -> Optional[datetime]: +) -> Optional[DateTime]: + if input_datetime is None: + return None if isinstance(input_datetime, str): return datetime_parser(input_datetime) - return input_datetime + return pendulum.instance(input_datetime) diff --git a/poetry.lock b/poetry.lock index 0cd3c121..5dc173d7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "aiohttp" @@ -1298,6 +1298,105 @@ toolz = "*" [package.extras] complete = ["blosc", "numpy (>=1.20.0)", "pandas (>=1.3)", "pyzmq"] +[[package]] +name = "pendulum" +version = "3.0.0" +description = "Python datetimes made easy" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pendulum-3.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cf9e53ef11668e07f73190c805dbdf07a1939c3298b78d5a9203a86775d1bfd"}, + {file = "pendulum-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb551b9b5e6059377889d2d878d940fd0bbb80ae4810543db18e6f77b02c5ef6"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c58227ac260d5b01fc1025176d7b31858c9f62595737f350d22124a9a3ad82d"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60fb6f415fea93a11c52578eaa10594568a6716602be8430b167eb0d730f3332"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b69f6b4dbcb86f2c2fe696ba991e67347bcf87fe601362a1aba6431454b46bde"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138afa9c373ee450ede206db5a5e9004fd3011b3c6bbe1e57015395cd076a09f"}, + {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83d9031f39c6da9677164241fd0d37fbfc9dc8ade7043b5d6d62f56e81af8ad2"}, + {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c2308af4033fa534f089595bcd40a95a39988ce4059ccd3dc6acb9ef14ca44a"}, + {file = "pendulum-3.0.0-cp310-none-win_amd64.whl", hash = "sha256:9a59637cdb8462bdf2dbcb9d389518c0263799189d773ad5c11db6b13064fa79"}, + {file = "pendulum-3.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3725245c0352c95d6ca297193192020d1b0c0f83d5ee6bb09964edc2b5a2d508"}, + {file = "pendulum-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c035f03a3e565ed132927e2c1b691de0dbf4eb53b02a5a3c5a97e1a64e17bec"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597e66e63cbd68dd6d58ac46cb7a92363d2088d37ccde2dae4332ef23e95cd00"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99a0f8172e19f3f0c0e4ace0ad1595134d5243cf75985dc2233e8f9e8de263ca"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77d8839e20f54706aed425bec82a83b4aec74db07f26acd039905d1237a5e1d4"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde30e8146292b059020fbc8b6f8fd4a60ae7c5e6f0afef937bbb24880bdf01"}, + {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:660434a6fcf6303c4efd36713ca9212c753140107ee169a3fc6c49c4711c2a05"}, + {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dee9e5a48c6999dc1106eb7eea3e3a50e98a50651b72c08a87ee2154e544b33e"}, + {file = "pendulum-3.0.0-cp311-none-win_amd64.whl", hash = "sha256:d4cdecde90aec2d67cebe4042fd2a87a4441cc02152ed7ed8fb3ebb110b94ec4"}, + {file = "pendulum-3.0.0-cp311-none-win_arm64.whl", hash = "sha256:773c3bc4ddda2dda9f1b9d51fe06762f9200f3293d75c4660c19b2614b991d83"}, + {file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"}, + {file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"}, + {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"}, + {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"}, + {file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"}, + {file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"}, + {file = "pendulum-3.0.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d4e2512f4e1a4670284a153b214db9719eb5d14ac55ada5b76cbdb8c5c00399d"}, + {file = "pendulum-3.0.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:3d897eb50883cc58d9b92f6405245f84b9286cd2de6e8694cb9ea5cb15195a32"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e169cc2ca419517f397811bbe4589cf3cd13fca6dc38bb352ba15ea90739ebb"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17c3084a4524ebefd9255513692f7e7360e23c8853dc6f10c64cc184e1217ab"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:826d6e258052715f64d05ae0fc9040c0151e6a87aae7c109ba9a0ed930ce4000"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2aae97087872ef152a0c40e06100b3665d8cb86b59bc8471ca7c26132fccd0f"}, + {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ac65eeec2250d03106b5e81284ad47f0d417ca299a45e89ccc69e36130ca8bc7"}, + {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a5346d08f3f4a6e9e672187faa179c7bf9227897081d7121866358af369f44f9"}, + {file = "pendulum-3.0.0-cp37-none-win_amd64.whl", hash = "sha256:235d64e87946d8f95c796af34818c76e0f88c94d624c268693c85b723b698aa9"}, + {file = "pendulum-3.0.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6a881d9c2a7f85bc9adafcfe671df5207f51f5715ae61f5d838b77a1356e8b7b"}, + {file = "pendulum-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d7762d2076b9b1cb718a6631ad6c16c23fc3fac76cbb8c454e81e80be98daa34"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e8e36a8130819d97a479a0e7bf379b66b3b1b520e5dc46bd7eb14634338df8c"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7dc843253ac373358ffc0711960e2dd5b94ab67530a3e204d85c6e8cb2c5fa10"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a78ad3635d609ceb1e97d6aedef6a6a6f93433ddb2312888e668365908c7120"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a137e9e0d1f751e60e67d11fc67781a572db76b2296f7b4d44554761049d6"}, + {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c95984037987f4a457bb760455d9ca80467be792236b69d0084f228a8ada0162"}, + {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d29c6e578fe0f893766c0d286adbf0b3c726a4e2341eba0917ec79c50274ec16"}, + {file = "pendulum-3.0.0-cp38-none-win_amd64.whl", hash = "sha256:deaba8e16dbfcb3d7a6b5fabdd5a38b7c982809567479987b9c89572df62e027"}, + {file = "pendulum-3.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b11aceea5b20b4b5382962b321dbc354af0defe35daa84e9ff3aae3c230df694"}, + {file = "pendulum-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a90d4d504e82ad236afac9adca4d6a19e4865f717034fc69bafb112c320dcc8f"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:825799c6b66e3734227756fa746cc34b3549c48693325b8b9f823cb7d21b19ac"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad769e98dc07972e24afe0cff8d365cb6f0ebc7e65620aa1976fcfbcadc4c6f3"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6fc26907eb5fb8cc6188cc620bc2075a6c534d981a2f045daa5f79dfe50d512"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c717eab1b6d898c00a3e0fa7781d615b5c5136bbd40abe82be100bb06df7a56"}, + {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3ddd1d66d1a714ce43acfe337190be055cdc221d911fc886d5a3aae28e14b76d"}, + {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:822172853d7a9cf6da95d7b66a16c7160cb99ae6df55d44373888181d7a06edc"}, + {file = "pendulum-3.0.0-cp39-none-win_amd64.whl", hash = "sha256:840de1b49cf1ec54c225a2a6f4f0784d50bd47f68e41dc005b7f67c7d5b5f3ae"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b1f74d1e6ffe5d01d6023870e2ce5c2191486928823196f8575dcc786e107b1"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:729e9f93756a2cdfa77d0fc82068346e9731c7e884097160603872686e570f07"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e586acc0b450cd21cbf0db6bae386237011b75260a3adceddc4be15334689a9a"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7944ffc1f0099a79ff468ee9630c73f8c7835cd76fdb57ef7320e6a409df4"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa30af36bd8e50686846bdace37cf6707bdd044e5cb6e1109acbad3277232e04"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:440215347b11914ae707981b9a57ab9c7b6983ab0babde07063c6ee75c0dc6e7"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:314c4038dc5e6a52991570f50edb2f08c339debdf8cea68ac355b32c4174e820"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5acb1d386337415f74f4d1955c4ce8d0201978c162927d07df8eb0692b2d8533"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a789e12fbdefaffb7b8ac67f9d8f22ba17a3050ceaaa635cd1cc4645773a4b1e"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:860aa9b8a888e5913bd70d819306749e5eb488e6b99cd6c47beb701b22bdecf5"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5ebc65ea033ef0281368217fbf59f5cb05b338ac4dd23d60959c7afcd79a60a0"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d9fef18ab0386ef6a9ac7bad7e43ded42c83ff7ad412f950633854f90d59afa8"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c134ba2f0571d0b68b83f6972e2307a55a5a849e7dac8505c715c531d2a8795"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:385680812e7e18af200bb9b4a49777418c32422d05ad5a8eb85144c4a285907b"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eec91cd87c59fb32ec49eb722f375bd58f4be790cae11c1b70fac3ee4f00da0"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4386bffeca23c4b69ad50a36211f75b35a4deb6210bdca112ac3043deb7e494a"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dfbcf1661d7146d7698da4b86e7f04814221081e9fe154183e34f4c5f5fa3bf8"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:04a1094a5aa1daa34a6b57c865b25f691848c61583fb22722a4df5699f6bf74c"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5b0ec85b9045bd49dd3a3493a5e7ddfd31c36a2a60da387c419fa04abcaecb23"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0a15b90129765b705eb2039062a6daf4d22c4e28d1a54fa260892e8c3ae6e157"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:bb8f6d7acd67a67d6fedd361ad2958ff0539445ef51cbe8cd288db4306503cd0"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd69b15374bef7e4b4440612915315cc42e8575fcda2a3d7586a0d88192d0c88"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc00f8110db6898360c53c812872662e077eaf9c75515d53ecc65d886eec209a"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:83a44e8b40655d0ba565a5c3d1365d27e3e6778ae2a05b69124db9e471255c4a"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1a3604e9fbc06b788041b2a8b78f75c243021e0f512447806a6d37ee5214905d"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:92c307ae7accebd06cbae4729f0ba9fa724df5f7d91a0964b1b972a22baa482b"}, + {file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"}, +] + +[package.dependencies] +python-dateutil = ">=2.6" +tzdata = ">=2020.1" + +[package.extras] +test = ["time-machine (>=2.6.0)"] + [[package]] name = "pexpect" version = "4.9.0" @@ -1907,4 +2006,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "10f7e27b97411bfe5cf27642ad7b4ae7c5a65b0634daa48c1551eb67dbe941f4" +content-hash = "2248793fb390933099ce694951481e01fc3bacfb49ec217e037ae5f43b29d715" diff --git a/pyproject.toml b/pyproject.toml index eae54043..f26d0ba4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ nest-asyncio = ">=1.5.8" pystac = ">=1.8.3" lxml = ">=4.9.0" numpy = ">=1.23.0,<2.0.0" +pendulum = "^3.0.0" [tool.poetry.dev-dependencies] pre-commit = "^2.20.0" diff --git a/tests/test_utility_functions.py b/tests/test_utility_functions.py index 6f867292..f975d37c 100644 --- a/tests/test_utility_functions.py +++ b/tests/test_utility_functions.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from freezegun import freeze_time @@ -9,23 +9,29 @@ class TestUtilityFunctions: @freeze_time("2012-01-14 03:21:34", tz_offset=-2) def test_datetime_parser(self): # all parsed dates are in UTC - assert datetime_parser("now") == datetime(2012, 1, 14, 1, 21, 34) + assert datetime_parser("now") == datetime( + 2012, 1, 14, 1, 21, 34, tzinfo=timezone.utc + ) assert datetime_parser("2012-01-14T03:21:34.000000+02:00") == datetime( - 2012, 1, 14, 1, 21, 34 + 2012, 1, 14, 1, 21, 34, tzinfo=timezone.utc ) # All format are supported - assert datetime_parser("2012") == datetime(2012, 1, 1, 0, 0, 0) - assert datetime_parser("2012-01-14") == datetime(2012, 1, 14, 0, 0, 0) + assert datetime_parser("2012") == datetime( + 2012, 1, 1, 0, 0, 0, tzinfo=timezone.utc + ) + assert datetime_parser("2012-01-14") == datetime( + 2012, 1, 14, 0, 0, 0, tzinfo=timezone.utc + ) assert datetime_parser("2012-01-14T03:21:34") == datetime( - 2012, 1, 14, 3, 21, 34 + 2012, 1, 14, 3, 21, 34, tzinfo=timezone.utc ) assert datetime_parser("2012-01-14 03:21:34") == datetime( - 2012, 1, 14, 3, 21, 34 + 2012, 1, 14, 3, 21, 34, tzinfo=timezone.utc ) assert datetime_parser("2012-01-14T03:21:34.000000") == datetime( - 2012, 1, 14, 3, 21, 34 + 2012, 1, 14, 3, 21, 34, tzinfo=timezone.utc ) assert datetime_parser("2012-01-14T03:21:34.000000Z") == datetime( - 2012, 1, 14, 3, 21, 34 + 2012, 1, 14, 3, 21, 34, tzinfo=timezone.utc )