dclimate-client-py 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dclimate_client_py/.pytest_cache/.gitignore +2 -0
- dclimate_client_py/.pytest_cache/CACHEDIR.TAG +4 -0
- dclimate_client_py/.pytest_cache/README.md +8 -0
- dclimate_client_py/.pytest_cache/v/cache/nodeids +1 -0
- dclimate_client_py/.pytest_cache/v/cache/stepwise +1 -0
- dclimate_client_py/__init__.py +32 -0
- dclimate_client_py/cids.json +1 -0
- dclimate_client_py/client.py +203 -0
- dclimate_client_py/concatenate.py +207 -0
- dclimate_client_py/datasets.py +667 -0
- dclimate_client_py/dclimate_client.py +204 -0
- dclimate_client_py/dclimate_zarr_errors.py +73 -0
- dclimate_client_py/encryption_codec.py +109 -0
- dclimate_client_py/geotemporal_data.py +722 -0
- dclimate_client_py/ipfs_retrieval.py +144 -0
- dclimate_client_py/s3_retrieval.py +105 -0
- dclimate_client_py/zarr_metadata.py +107 -0
- dclimate_client_py-0.1.1.dist-info/METADATA +183 -0
- dclimate_client_py-0.1.1.dist-info/RECORD +22 -0
- dclimate_client_py-0.1.1.dist-info/WHEEL +4 -0
- dclimate_client_py-0.1.1.dist-info/entry_points.txt +4 -0
- dclimate_client_py-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# pytest cache directory #
|
|
2
|
+
|
|
3
|
+
This directory contains data from the pytest's cache plugin,
|
|
4
|
+
which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
|
|
5
|
+
|
|
6
|
+
**Do not** commit this to version control.
|
|
7
|
+
|
|
8
|
+
See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# public API
|
|
2
|
+
from .client import (
|
|
3
|
+
load_s3,
|
|
4
|
+
geo_temporal_query,
|
|
5
|
+
)
|
|
6
|
+
from .dclimate_client import dClimateClient
|
|
7
|
+
from .geotemporal_data import GeotemporalData
|
|
8
|
+
from .encryption_codec import (
|
|
9
|
+
EncryptionCodec,
|
|
10
|
+
)
|
|
11
|
+
from .datasets import (
|
|
12
|
+
list_dataset_catalog,
|
|
13
|
+
fetch_cid_from_url,
|
|
14
|
+
DatasetCatalog,
|
|
15
|
+
CatalogCollection,
|
|
16
|
+
CatalogDataset,
|
|
17
|
+
DatasetVariantConfig,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"dClimateClient",
|
|
22
|
+
"load_s3",
|
|
23
|
+
"geo_temporal_query",
|
|
24
|
+
"list_dataset_catalog",
|
|
25
|
+
"fetch_cid_from_url",
|
|
26
|
+
"GeotemporalData",
|
|
27
|
+
"EncryptionCodec",
|
|
28
|
+
"DatasetCatalog",
|
|
29
|
+
"CatalogCollection",
|
|
30
|
+
"CatalogDataset",
|
|
31
|
+
"DatasetVariantConfig",
|
|
32
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"cpc-precip-conus": "bafyr4iff6vuvpkb4zexkx7exoafsjvfnno4ofidyc37gsto2aaxixg5zia", "cpc-precip-global": "bafyr4ihe35o55qz47y2dbau6ikvfkbrthqn57pdhem5jdxhvqf5u7qlrbm", "chirps-final-p05": "bafyr4ic2n63fkxdvwgh2uzealcrbeubncivcqaptttfrr4vp5ecbe5e65a"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functions that will map to endpoints in the flask app
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import typing
|
|
7
|
+
import xarray as xr
|
|
8
|
+
|
|
9
|
+
from .dclimate_zarr_errors import (
|
|
10
|
+
ConflictingGeoRequestError,
|
|
11
|
+
ConflictingAggregationRequestError,
|
|
12
|
+
InvalidExportFormatError,
|
|
13
|
+
InvalidSelectionError,
|
|
14
|
+
)
|
|
15
|
+
from .geotemporal_data import GeotemporalData, DEFAULT_POINT_LIMIT
|
|
16
|
+
from .s3_retrieval import get_dataset_from_s3
|
|
17
|
+
from .ipfs_retrieval import (
|
|
18
|
+
_get_dataset_by_ipfs_cid,
|
|
19
|
+
)
|
|
20
|
+
from .datasets import (
|
|
21
|
+
resolve_dataset_source,
|
|
22
|
+
get_concatenable_variants,
|
|
23
|
+
find_dataset_by_name,
|
|
24
|
+
find_collection_by_name,
|
|
25
|
+
fetch_cid_from_url,
|
|
26
|
+
DATASET_CATALOG_INTERNAL,
|
|
27
|
+
DatasetCatalog,
|
|
28
|
+
)
|
|
29
|
+
from .concatenate import concatenate_datasets
|
|
30
|
+
|
|
31
|
+
def load_s3(
|
|
32
|
+
dataset_name: str,
|
|
33
|
+
bucket_name: str,
|
|
34
|
+
) -> GeotemporalData:
|
|
35
|
+
"""
|
|
36
|
+
Load a Geotemporal dataset from an S3 bucket.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
|
|
41
|
+
dataset_name: str
|
|
42
|
+
The name of the dataset in the bucket.
|
|
43
|
+
bucket_name: str
|
|
44
|
+
S3 bucket name where the dataset is going to be fetched
|
|
45
|
+
"""
|
|
46
|
+
ds = get_dataset_from_s3(dataset_name, bucket_name)
|
|
47
|
+
return GeotemporalData(ds, dataset_name=dataset_name)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def geo_temporal_query(
|
|
51
|
+
dataset_name: str,
|
|
52
|
+
source: typing.Literal["s3"] = "s3",
|
|
53
|
+
bucket_name: str = None,
|
|
54
|
+
var_name: str = None,
|
|
55
|
+
gateway_uri_stem: str | None = None,
|
|
56
|
+
rpc_uri_stem: str | None = None,
|
|
57
|
+
forecast_reference_time: str = None,
|
|
58
|
+
point_kwargs: dict = None,
|
|
59
|
+
circle_kwargs: dict = None,
|
|
60
|
+
rectangle_kwargs: dict = None,
|
|
61
|
+
polygon_kwargs: dict = None,
|
|
62
|
+
multiple_points_kwargs: dict = None,
|
|
63
|
+
spatial_agg_kwargs: dict = None,
|
|
64
|
+
temporal_agg_kwargs: dict = None,
|
|
65
|
+
rolling_agg_kwargs: dict = None,
|
|
66
|
+
time_range: typing.Optional[typing.List[datetime.datetime]] = None,
|
|
67
|
+
# as_of: typing.Optional[datetime.datetime] = None, # Removed as_of
|
|
68
|
+
point_limit: int = DEFAULT_POINT_LIMIT,
|
|
69
|
+
output_format: str = "array",
|
|
70
|
+
) -> typing.Union[dict, bytes]:
|
|
71
|
+
"""Filter an XArray dataset
|
|
72
|
+
|
|
73
|
+
Filter an XArray dataset by specified spatial and/or temporal bounds and aggregate
|
|
74
|
+
according to spatial and/or temporal logic, if desired. Before aggregating check
|
|
75
|
+
that the filtered data fits within specified point and area maximums to avoid
|
|
76
|
+
computationally expensive retrieval and processing operations. When bounds or
|
|
77
|
+
aggregation logic are not provided, pass the dataset along untouched.
|
|
78
|
+
|
|
79
|
+
Return either a numpy array of data values or a NetCDF file.
|
|
80
|
+
|
|
81
|
+
Only one of point, circle, rectangle, or polygon kwargs may be provided. Only one of
|
|
82
|
+
temporal or rolling aggregation kwargs may be provided, although they can be chained
|
|
83
|
+
with spatial aggregations if desired.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
dataset_name (str): Name used to identify the dataset within the STAC catalog (for IPFS)
|
|
87
|
+
or the dataset name in the bucket (for S3).
|
|
88
|
+
source: (typing.Literal["ipfs", "s3"]): how to pull data. Defaults to "ipfs".
|
|
89
|
+
bucket_name (str): S3 bucket name where the datasets are going to be fetched (required if source="s3").
|
|
90
|
+
var_name (str, optional): Specific data variable to use within the dataset.
|
|
91
|
+
gateway_uri_stem (str | None, optional): Custom IPFS HTTP Gateway URI stem for IPFS source.
|
|
92
|
+
rpc_uri_stem (str | None, optional): Custom IPFS RPC API URI stem for IPFS source.
|
|
93
|
+
forecast_reference_time (str): Isoformatted string representing the desire date
|
|
94
|
+
to return all available forecasts for
|
|
95
|
+
circle_kwargs (dict, optional): a dictionary of parameters relevant to a
|
|
96
|
+
circular query
|
|
97
|
+
rectangle_kwargs (dict, optional): a dictionary of parameters relevant to a
|
|
98
|
+
rectangular query
|
|
99
|
+
polygon_kwargs (dict, optional): a dictionary of parameters relevant to a
|
|
100
|
+
polygonal query
|
|
101
|
+
multiple_points_kwargs (dict, optional): Parameters for querying multiple specific points.
|
|
102
|
+
point_kwargs (dict, optional): Parameters for querying a single point.
|
|
103
|
+
spatial_agg_kwargs (dict, optional): a dictionary of parameters relevant to a
|
|
104
|
+
spatial aggregation operation
|
|
105
|
+
temporal_agg_kwargs (dict, optional): a dictionary of parameters relevant to a
|
|
106
|
+
temporal aggregation operation
|
|
107
|
+
rolling_agg_kwargs (dict, optional): a dictionary of parameters relevant to a
|
|
108
|
+
rolling aggregation operation
|
|
109
|
+
time_range (typing.Optional[typing.List[datetime.datetime]], optional):
|
|
110
|
+
time range in which to subset data.
|
|
111
|
+
Defaults to None.
|
|
112
|
+
# REMOVED as_of (typing.Optional[datetime.datetime], optional):
|
|
113
|
+
# pull in most recent data created before this time. If None, just get most
|
|
114
|
+
# recent. Defaults to None.
|
|
115
|
+
point_limit (int, optional): maximum number of data points user can request.
|
|
116
|
+
Defaults to DEFAULT_POINT_LIMIT.
|
|
117
|
+
output_format (str, optional): Current supported formats are `array` and
|
|
118
|
+
`netcdf`. Defaults to "array", which provides a dict of data
|
|
119
|
+
values and coordinates.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
typing.Union[dict, bytes]: Output data as dict (default) or NetCDF bytes.
|
|
123
|
+
"""
|
|
124
|
+
# Check for incompatible request parameters
|
|
125
|
+
if (
|
|
126
|
+
len(
|
|
127
|
+
[
|
|
128
|
+
kwarg_dict
|
|
129
|
+
for kwarg_dict in [
|
|
130
|
+
circle_kwargs,
|
|
131
|
+
rectangle_kwargs,
|
|
132
|
+
polygon_kwargs,
|
|
133
|
+
multiple_points_kwargs,
|
|
134
|
+
point_kwargs,
|
|
135
|
+
]
|
|
136
|
+
if kwarg_dict is not None
|
|
137
|
+
]
|
|
138
|
+
)
|
|
139
|
+
> 1
|
|
140
|
+
):
|
|
141
|
+
raise ConflictingGeoRequestError(
|
|
142
|
+
"User requested more than one type of geographic query, but only one can "
|
|
143
|
+
"be submitted at a time"
|
|
144
|
+
)
|
|
145
|
+
if spatial_agg_kwargs and point_kwargs:
|
|
146
|
+
raise ConflictingGeoRequestError(
|
|
147
|
+
"User requested spatial aggregation methods on a single point, "
|
|
148
|
+
"but these are mutually exclusive parameters. Only one may be requested at "
|
|
149
|
+
"a time."
|
|
150
|
+
)
|
|
151
|
+
if temporal_agg_kwargs and rolling_agg_kwargs:
|
|
152
|
+
raise ConflictingAggregationRequestError(
|
|
153
|
+
"User requested both rolling and temporal aggregation, but these are "
|
|
154
|
+
"mutually exclusive operations. Only one may be requested at a time."
|
|
155
|
+
)
|
|
156
|
+
if output_format not in ["array", "netcdf"]:
|
|
157
|
+
raise InvalidExportFormatError(
|
|
158
|
+
"User requested an invalid export format. Only 'array' or 'netcdf' "
|
|
159
|
+
"permitted."
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Set defaults to avoid Nones accidentally passed by users causing a TypeError
|
|
163
|
+
if not point_limit:
|
|
164
|
+
point_limit = DEFAULT_POINT_LIMIT
|
|
165
|
+
|
|
166
|
+
# Load the dataset based on the source
|
|
167
|
+
if source == "s3":
|
|
168
|
+
if not bucket_name:
|
|
169
|
+
raise ValueError("bucket_name is required when source is 's3'")
|
|
170
|
+
data = load_s3(dataset_name, bucket_name)
|
|
171
|
+
else:
|
|
172
|
+
raise ValueError(
|
|
173
|
+
"Invalid source specified. Must be 's3'. "
|
|
174
|
+
"IPFS source is deprecated - use dClimateClient instead."
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# If specific variable is requested, use that
|
|
178
|
+
if var_name is not None:
|
|
179
|
+
data = data.use(var_name)
|
|
180
|
+
|
|
181
|
+
# Filter data down temporally, then spatially, and check that the size of resulting
|
|
182
|
+
# dataset fits within the limit. While a user can get the entire DS by providing no
|
|
183
|
+
# filters, this will almost certainly cause the size checks to fail
|
|
184
|
+
|
|
185
|
+
data = data.query(
|
|
186
|
+
forecast_reference_time=forecast_reference_time,
|
|
187
|
+
point_kwargs=point_kwargs,
|
|
188
|
+
circle_kwargs=circle_kwargs,
|
|
189
|
+
rectangle_kwargs=rectangle_kwargs,
|
|
190
|
+
polygon_kwargs=polygon_kwargs,
|
|
191
|
+
multiple_points_kwargs=multiple_points_kwargs,
|
|
192
|
+
spatial_agg_kwargs=spatial_agg_kwargs,
|
|
193
|
+
temporal_agg_kwargs=temporal_agg_kwargs,
|
|
194
|
+
rolling_agg_kwargs=rolling_agg_kwargs,
|
|
195
|
+
time_range=time_range,
|
|
196
|
+
point_limit=point_limit,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Export
|
|
200
|
+
if output_format == "netcdf":
|
|
201
|
+
return data.to_netcdf()
|
|
202
|
+
else: # "array"
|
|
203
|
+
return data.as_dict()
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Concatenation utilities for combining dataset variants.
|
|
3
|
+
|
|
4
|
+
Implements smart concatenation logic similar to dclimate-client-js,
|
|
5
|
+
avoiding duplicate data by finding split indices where new data begins.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import typing
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
import numpy as np
|
|
12
|
+
import xarray as xr
|
|
13
|
+
|
|
14
|
+
from .dclimate_zarr_errors import NoDataFoundError
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def find_split_index(
|
|
20
|
+
combined_coords: typing.Any,
|
|
21
|
+
next_coords: typing.Any,
|
|
22
|
+
last_coord_value: typing.Any,
|
|
23
|
+
) -> int:
|
|
24
|
+
"""
|
|
25
|
+
Find the index in next_coords where values are strictly greater than last_coord_value.
|
|
26
|
+
|
|
27
|
+
This prevents duplicate data when concatenating datasets.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
combined_coords: Coordinates from the combined dataset (for reference)
|
|
31
|
+
next_coords: Coordinates from the next variant to concatenate
|
|
32
|
+
last_coord_value: The last coordinate value from the combined dataset
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Index in next_coords where to start slicing (first value > last_coord_value)
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
NoDataFoundError: If no new data found in next variant
|
|
39
|
+
"""
|
|
40
|
+
# Convert coordinates to numpy array for easier comparison
|
|
41
|
+
next_array = np.array(next_coords)
|
|
42
|
+
|
|
43
|
+
# Handle different data types
|
|
44
|
+
if isinstance(last_coord_value, (datetime, np.datetime64)):
|
|
45
|
+
# Convert to numpy datetime64 for comparison
|
|
46
|
+
if isinstance(last_coord_value, datetime):
|
|
47
|
+
last_coord_value = np.datetime64(last_coord_value)
|
|
48
|
+
|
|
49
|
+
# Find where next coords are strictly after last coord
|
|
50
|
+
mask = next_array > last_coord_value
|
|
51
|
+
indices = np.where(mask)[0]
|
|
52
|
+
|
|
53
|
+
if len(indices) == 0:
|
|
54
|
+
raise NoDataFoundError(
|
|
55
|
+
f"No new data found in next variant after {last_coord_value}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return int(indices[0])
|
|
59
|
+
|
|
60
|
+
elif isinstance(last_coord_value, (int, float, np.number)):
|
|
61
|
+
# Numeric comparison
|
|
62
|
+
mask = next_array > last_coord_value
|
|
63
|
+
indices = np.where(mask)[0]
|
|
64
|
+
|
|
65
|
+
if len(indices) == 0:
|
|
66
|
+
raise NoDataFoundError(
|
|
67
|
+
f"No new data found in next variant after {last_coord_value}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return int(indices[0])
|
|
71
|
+
|
|
72
|
+
elif isinstance(last_coord_value, str):
|
|
73
|
+
# String comparison (lexicographic)
|
|
74
|
+
mask = next_array > last_coord_value
|
|
75
|
+
indices = np.where(mask)[0]
|
|
76
|
+
|
|
77
|
+
if len(indices) == 0:
|
|
78
|
+
raise NoDataFoundError(
|
|
79
|
+
f"No new data found in next variant after '{last_coord_value}'"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return int(indices[0])
|
|
83
|
+
|
|
84
|
+
else:
|
|
85
|
+
# For other types, try direct comparison
|
|
86
|
+
try:
|
|
87
|
+
mask = next_array > last_coord_value
|
|
88
|
+
indices = np.where(mask)[0]
|
|
89
|
+
|
|
90
|
+
if len(indices) == 0:
|
|
91
|
+
raise NoDataFoundError(
|
|
92
|
+
f"No new data found in next variant after {last_coord_value}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return int(indices[0])
|
|
96
|
+
except Exception as e:
|
|
97
|
+
raise TypeError(
|
|
98
|
+
f"Cannot compare coordinate type {type(last_coord_value).__name__}: {e}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def concatenate_datasets(
|
|
103
|
+
datasets: typing.List[xr.Dataset],
|
|
104
|
+
dimension: str = "time",
|
|
105
|
+
) -> xr.Dataset:
|
|
106
|
+
"""
|
|
107
|
+
Concatenate multiple xarray datasets along a dimension, avoiding duplicate data.
|
|
108
|
+
|
|
109
|
+
Implements smart concatenation logic:
|
|
110
|
+
1. Start with first dataset (highest priority)
|
|
111
|
+
2. For each subsequent dataset:
|
|
112
|
+
- Find the last coordinate value in combined dataset
|
|
113
|
+
- Find split index in next dataset where coords > last coord
|
|
114
|
+
- Slice next dataset to only include new data
|
|
115
|
+
- Concatenate sliced dataset
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
datasets: List of xarray datasets to concatenate (in priority order)
|
|
119
|
+
dimension: Dimension to concatenate along (default: "time")
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Concatenated xarray dataset
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
ValueError: If datasets list is empty or dimension not found
|
|
126
|
+
NoDataFoundError: If a variant contains no new data
|
|
127
|
+
"""
|
|
128
|
+
if not datasets:
|
|
129
|
+
raise ValueError("Cannot concatenate empty list of datasets")
|
|
130
|
+
|
|
131
|
+
if len(datasets) == 1:
|
|
132
|
+
logger.info("Only one dataset provided, returning it without concatenation")
|
|
133
|
+
return datasets[0]
|
|
134
|
+
|
|
135
|
+
# Check that all datasets have the dimension
|
|
136
|
+
for i, ds in enumerate(datasets):
|
|
137
|
+
if dimension not in ds.dims:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"Dataset {i} does not have dimension '{dimension}'. "
|
|
140
|
+
f"Available dimensions: {list(ds.dims.keys())}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
logger.info(f"Concatenating {len(datasets)} datasets along '{dimension}' dimension")
|
|
144
|
+
|
|
145
|
+
# Start with the first dataset (highest priority)
|
|
146
|
+
combined = datasets[0]
|
|
147
|
+
logger.debug(
|
|
148
|
+
f"Starting with dataset 1/{len(datasets)}, "
|
|
149
|
+
f"{dimension} range: {combined[dimension].values[0]} to {combined[dimension].values[-1]}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Concatenate each subsequent dataset
|
|
153
|
+
for i, next_ds in enumerate(datasets[1:], start=2):
|
|
154
|
+
# Get the last coordinate value from combined dataset
|
|
155
|
+
last_coord_value = combined[dimension].values[-1]
|
|
156
|
+
|
|
157
|
+
# Get coordinates from next dataset
|
|
158
|
+
next_coords = next_ds[dimension].values
|
|
159
|
+
|
|
160
|
+
logger.debug(
|
|
161
|
+
f"Processing dataset {i}/{len(datasets)}, "
|
|
162
|
+
f"{dimension} range: {next_coords[0]} to {next_coords[-1]}"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Find where to split the next dataset
|
|
166
|
+
try:
|
|
167
|
+
split_index = find_split_index(
|
|
168
|
+
combined[dimension].values,
|
|
169
|
+
next_coords,
|
|
170
|
+
last_coord_value,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
logger.debug(
|
|
174
|
+
f"Found split index {split_index} (out of {len(next_coords)} coords) "
|
|
175
|
+
f"for dataset {i}, new data starts at: {next_coords[split_index]}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Slice the next dataset to only include new data
|
|
179
|
+
sliced_next = next_ds.isel({dimension: slice(split_index, None)})
|
|
180
|
+
|
|
181
|
+
logger.debug(
|
|
182
|
+
f"Sliced dataset {i} to {len(sliced_next[dimension])} new coords"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Concatenate with combined dataset
|
|
186
|
+
new_combined = xr.concat(
|
|
187
|
+
[combined, sliced_next],
|
|
188
|
+
dim=dimension,
|
|
189
|
+
)
|
|
190
|
+
logger.debug(
|
|
191
|
+
f"After concatenating dataset {i}, total {dimension} coords: {len(combined[dimension])}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
except NoDataFoundError as e:
|
|
195
|
+
logger.warning(
|
|
196
|
+
f"Skipping dataset {i} as it contains no new data: {e}"
|
|
197
|
+
)
|
|
198
|
+
# Continue to next dataset
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
logger.info(
|
|
202
|
+
f"Concatenation complete. Final dataset has {len(combined[dimension])} "
|
|
203
|
+
f"{dimension} coordinates ranging from {combined[dimension].values[0]} "
|
|
204
|
+
f"to {combined[dimension].values[-1]}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return combined
|