From 21a3e67f79054958a388f31369ad5d9e513099df Mon Sep 17 00:00:00 2001 From: David Hinckley Date: Thu, 7 Aug 2025 10:08:58 -0400 Subject: [PATCH 01/21] FIX: addressed edge case where evaluating zarr source with neither interpolation nor a selector caused an error because the index_type was not respected. Solution was to apply a temporary nearest neighbor selector on the expanded coordinates. --- podpac/core/data/datasource.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/podpac/core/data/datasource.py b/podpac/core/data/datasource.py index c714adcf..e379f964 100644 --- a/podpac/core/data/datasource.py +++ b/podpac/core/data/datasource.py @@ -23,6 +23,8 @@ from podpac.core.node import Node from podpac.core.utils import common_doc, cached_property from podpac.core.node import COMMON_NODE_DOC +from podpac.core.interpolation.selector import Selector + log = logging.getLogger(__name__) @@ -418,6 +420,9 @@ def _eval(self, coordinates, output=None, _selector=None): else: # get source coordinates that are within the requested coordinates bounds (rsc, rsci) = self.coordinates.intersect(coordinates, outer=True, return_index=True) + # make a nearest neighbor source to improse index_type restrictions + temp_selector = Selector(method="nearest") + (rsc, rsci) = temp_selector.select(self.coordinates, rsc, index_type=self.coordinate_index_type) # if requested coordinates and coordinates do not intersect, shortcut with nan UnitsDataArary if rsc.size == 0: From 3c646049fc2f792fbb374b1f0ef7191254eab27a Mon Sep 17 00:00:00 2001 From: David Hinckley Date: Wed, 20 Aug 2025 09:31:19 -0400 Subject: [PATCH 02/21] FIX: applied the mssing selector fix from datasource to the rasterio_source._get_window_coords function --- podpac/core/data/rasterio_source.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/podpac/core/data/rasterio_source.py b/podpac/core/data/rasterio_source.py index 041df158..93f50c87 100644 --- a/podpac/core/data/rasterio_source.py +++ b/podpac/core/data/rasterio_source.py @@ -21,6 +21,7 @@ from podpac.core.data.datasource import COMMON_DATA_DOC, DATA_DOC from podpac.core.data.file_source import BaseFileSource from podpac.core.authentication import S3Mixin +from podpac.core.interpolation.selector import Selector _logger = logging.getLogger(__name__) @@ -172,10 +173,14 @@ def get_data(self, coordinates, coordinates_index): data.data.ravel()[:] = raster_data.ravel() return data - def _get_window_coords(self,coordinates,new_coords): - new_coords,slc = new_coords.intersect(coordinates,return_index=True,outer=True) - window = ((slc[0].start,slc[0].stop),(slc[1].start,slc[1].stop)) - return window,new_coords + def _get_window_coords(self, coordinates, new_coords): + # import ipdb + new_coords, slc = new_coords.intersect(coordinates, return_index=True, outer=True) + temp_selector = Selector(method="nearest") + new_coords, slc = temp_selector.select(coordinates, new_coords, index_type="slice") + new_coords = new_coords.transform(crs=self.coordinates.crs) + window = ((slc[0].start, slc[0].stop), (slc[1].start, slc[1].stop)) + return window, new_coords def get_data_overviews(self, coordinates): # Figure out how much coarser the request is than the actual data @@ -219,11 +224,13 @@ def get_data_overviews(self, coordinates): try: # read data within coordinates_index window at the resolution of the overview # Rasterio will then automatically pull from the overview + new_coords = Coordinates.from_geotransform( dataset.transform.to_gdal(), dataset.shape, crs=self.coordinates.crs ) - window,new_coords = self._get_window_coords(coordinates,new_coords) + window, new_coords = self._get_window_coords(coordinates, new_coords) missing_coords = self.coordinates.drop(["lat", "lon"]) + new_coords = merge_dims([new_coords, missing_coords]) new_coords = new_coords.transpose(*self.coordinates.dims) coordinates_shape = new_coords.shape[:2] @@ -322,4 +329,3 @@ def get_band_numbers(self, key, value): matches = np.nonzero(match)[0] + 1 return matches - From ebc815fae786311cd874150cbaa3967719d3f5d3 Mon Sep 17 00:00:00 2001 From: David Hinckley Date: Tue, 2 Sep 2025 13:02:36 -0400 Subject: [PATCH 03/21] FIX: removed the selector change in rasterio_source._get_window_coords --- podpac/core/data/rasterio_source.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/podpac/core/data/rasterio_source.py b/podpac/core/data/rasterio_source.py index 93f50c87..0a40ddc9 100644 --- a/podpac/core/data/rasterio_source.py +++ b/podpac/core/data/rasterio_source.py @@ -174,11 +174,7 @@ def get_data(self, coordinates, coordinates_index): return data def _get_window_coords(self, coordinates, new_coords): - # import ipdb new_coords, slc = new_coords.intersect(coordinates, return_index=True, outer=True) - temp_selector = Selector(method="nearest") - new_coords, slc = temp_selector.select(coordinates, new_coords, index_type="slice") - new_coords = new_coords.transform(crs=self.coordinates.crs) window = ((slc[0].start, slc[0].stop), (slc[1].start, slc[1].stop)) return window, new_coords From bd1327fe00d96e9c45b7c02b15708f631243c57b Mon Sep 17 00:00:00 2001 From: David Hinckley Date: Mon, 27 Oct 2025 14:46:50 -0400 Subject: [PATCH 04/21] FIX: adjusted datasource behavior when there is no coordinate intersection --- podpac/core/data/datasource.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/podpac/core/data/datasource.py b/podpac/core/data/datasource.py index e379f964..fa2c69dd 100644 --- a/podpac/core/data/datasource.py +++ b/podpac/core/data/datasource.py @@ -223,7 +223,6 @@ def coordinates(self): else: nc = self.get_coordinates() self.set_trait("_coordinates", nc) - print("get_coordinates", nc) if self.cache_coordinates: self.put_property_cache(nc, "coordinates") return nc @@ -415,14 +414,18 @@ def _eval(self, coordinates, output=None, _selector=None): log.debug("Evaluating {} data source".format(self.__class__.__name__)) # Use the selector + if _selector is not None: (rsc, rsci) = _selector(self.coordinates, coordinates, index_type=self.coordinate_index_type) else: # get source coordinates that are within the requested coordinates bounds (rsc, rsci) = self.coordinates.intersect(coordinates, outer=True, return_index=True) - # make a nearest neighbor source to improse index_type restrictions + # make a nearest neighbor source to impose index_type restrictions + # use the original coords if there was no intersection temp_selector = Selector(method="nearest") - (rsc, rsci) = temp_selector.select(self.coordinates, rsc, index_type=self.coordinate_index_type) + (rsc, rsci) = temp_selector.select( + coordinates, self.coordinates if rsc.size == 0 else rsc, index_type=self.coordinate_index_type + ) # if requested coordinates and coordinates do not intersect, shortcut with nan UnitsDataArary if rsc.size == 0: From 92a4acffc01563826807868fd631db4d96ae71a7 Mon Sep 17 00:00:00 2001 From: David Hinckley Date: Wed, 29 Oct 2025 15:24:41 -0400 Subject: [PATCH 05/21] FIX: previous attempt to fix not adhering to index_type in datasource._eval interfered with no-intersection behavior. This attempts to correct that --- podpac/core/data/datasource.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/podpac/core/data/datasource.py b/podpac/core/data/datasource.py index fa2c69dd..08c9f851 100644 --- a/podpac/core/data/datasource.py +++ b/podpac/core/data/datasource.py @@ -422,10 +422,9 @@ def _eval(self, coordinates, output=None, _selector=None): (rsc, rsci) = self.coordinates.intersect(coordinates, outer=True, return_index=True) # make a nearest neighbor source to impose index_type restrictions # use the original coords if there was no intersection - temp_selector = Selector(method="nearest") - (rsc, rsci) = temp_selector.select( - coordinates, self.coordinates if rsc.size == 0 else rsc, index_type=self.coordinate_index_type - ) + if rsc.size != 0: + temp_selector = Selector(method="nearest") + (rsc, rsci) = temp_selector.select(coordinates, rsc, index_type=self.coordinate_index_type) # if requested coordinates and coordinates do not intersect, shortcut with nan UnitsDataArary if rsc.size == 0: From e81e993134aafcb18ca2ac68b280971ec6bd35de Mon Sep 17 00:00:00 2001 From: David Hinckley Date: Wed, 12 Nov 2025 14:28:13 -0500 Subject: [PATCH 06/21] FIX: fixed issue where previous fix to resolve respect of index_type did returned coordinates/indices from the input and not the source. Added override for Datasource.create_output_array that adds in bounds and boundary_data to attrs. This already happened on successful evals but short-circuit cases did not contain these attrs. --- podpac/core/data/datasource.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/podpac/core/data/datasource.py b/podpac/core/data/datasource.py index 08c9f851..f897fdd2 100644 --- a/podpac/core/data/datasource.py +++ b/podpac/core/data/datasource.py @@ -424,7 +424,7 @@ def _eval(self, coordinates, output=None, _selector=None): # use the original coords if there was no intersection if rsc.size != 0: temp_selector = Selector(method="nearest") - (rsc, rsci) = temp_selector.select(coordinates, rsc, index_type=self.coordinate_index_type) + (rsc, rsci) = temp_selector.select(self.coordinates, rsc, index_type=self.coordinate_index_type) # if requested coordinates and coordinates do not intersect, shortcut with nan UnitsDataArary if rsc.size == 0: @@ -462,7 +462,6 @@ def _eval(self, coordinates, output=None, _selector=None): # get indexed boundary rsb = self._get_boundary(rsci) output.attrs["boundary_data"] = rsb - output.attrs["bounds"] = self.coordinates.bounds # save output to private for debugging if settings["DEBUG"]: @@ -475,6 +474,32 @@ def _eval(self, coordinates, output=None, _selector=None): return output + def create_output_array(self, coords, data=np.nan, attrs=None, outputs=None, **kwargs): + """ + Initialize an output data array. This adds bounds to the output attrs + + Parameters + ---------- + coords : podpac.Coordinates + {arr_coords} + data : None, number, or array-like (optional) + {arr_init_type} + attrs : dict + Attributes to add to output -- UnitsDataArray.create uses the 'crs' portion contained in here + outputs : list[string], optional + Default is self.outputs. List of strings listing the outputs + **kwargs + {arr_kwargs} + + Returns + ------- + {arr_return} + """ + output = super().create_output_array(coords, data=data, attrs=attrs, outputs=outputs, **kwargs) + output.attrs["bounds"], _ = self.get_bounds(crs=output.attrs["crs"]) + output.attrs["boundary_data"] = {} + return output + def find_coordinates(self): """ Get the available coordinates for the Node. For a DataSource, this is just the coordinates. From e7fa57bbfa5ae0cfd5234bf48150d12caeb14e47 Mon Sep 17 00:00:00 2001 From: David Hinckley Date: Fri, 14 Nov 2025 09:07:59 -0500 Subject: [PATCH 07/21] FIX: adjusted _format_value function to handle list returns when enumeration is applicable to account for podpac4's separation of nodes and interpolation --- podpac/core/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/podpac/core/utils.py b/podpac/core/utils.py index 3a0ecff0..0824013b 100644 --- a/podpac/core/utils.py +++ b/podpac/core/utils.py @@ -527,7 +527,7 @@ def _get_entry(key, out, definition): def _format_value(value, style, add_enumeration_labels): """Helper for probe_node().""" - if not add_enumeration_labels or style.enumeration_legend is None: + if not add_enumeration_labels or style.enumeration_legend is None or isinstance(value, list): return value if np.isnan(value): return str(value) + " (unknown)" From 2657f343d6b255a38c5ade458b59c46590f2c018 Mon Sep 17 00:00:00 2001 From: David Hinckley Date: Mon, 17 Nov 2025 13:16:46 -0500 Subject: [PATCH 08/21] ENH: added opt out for hash computation in probe_node. adjusted probe_node to separate value and units/label. expanded enumeration label behavior to handle array returns instead of just single values. --- podpac/core/utils.py | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/podpac/core/utils.py b/podpac/core/utils.py index 0824013b..299ebd28 100644 --- a/podpac/core/utils.py +++ b/podpac/core/utils.py @@ -515,10 +515,12 @@ def _get_entry(key, out, definition): entry = OrderedDict() entry["name"] = out[key]["name"] entry["value"] = str(out[key]["value"]) - if out[key]["units"] not in [None, ""]: - entry["value"] = entry["value"] + " " + str(out[key]["units"]) + # if out[key]["units"] not in [None, ""]: + # entry["value"] = entry["value"] + " " + str(out[key]["units"]) + entry['label'] = out[key]['label'] entry["active"] = out[key]["active"] - entry["node_id"] = out[key]["node_hash"] + if 'node_hash' in out[key]: + entry["node_id"] = out[key]["node_hash"] entry["params"] = {} entry["inputs"] = {"inputs": [_get_entry(inp, out, definition) for inp in out[key]["inputs"]]} if len(entry["inputs"]["inputs"]) == 0: @@ -527,7 +529,7 @@ def _get_entry(key, out, definition): def _format_value(value, style, add_enumeration_labels): """Helper for probe_node().""" - if not add_enumeration_labels or style.enumeration_legend is None or isinstance(value, list): + if not add_enumeration_labels or style.enumeration_legend is None: return value if np.isnan(value): return str(value) + " (unknown)" @@ -535,8 +537,30 @@ def _format_value(value, style, add_enumeration_labels): return str(int(value)) + " ({})".format(style.enumeration_legend[int(value)]) except ValueError: return str(value) + " (unknown)" - -def probe_node(node, lat=None, lon=None, time=None, alt=None, crs=None, nested=False, add_enumeration_labels=True): + +def _get_label(value, style, add_enumeration_labels): + if not add_enumeration_labels or style.enumeration_legend is None: + return style.units + if isinstance(value,list): # all list returns should be 2-D + ret = '' + for v in np.unique(value): + try: + new_label = style.enumeration_legend[int(v)] + except ValueError: + _log.warning('Enumeration label lookup failed for node of name {}, returning unknown'.format(style.name)) + new_label = 'unknown' + ret += '{}={}, '.format(v,new_label) + return ret[:-2] + else: + if np.isnan(value): + return 'unknown' + try: + return str(style.enumeration_legend[int(value)]) + except ValueError: + _log.warning('Enumeration label lookup failed for node of name {}, returning unknown'.format(style.name)) + return 'unknown' + +def probe_node(node, lat=None, lon=None, time=None, alt=None, crs=None, nested=False, add_enumeration_labels=True, compute_hash=True): """Evaluates every part of a node / pipeline at a point and records which nodes are actively being used. @@ -595,12 +619,13 @@ def probe_node(node, lat=None, lon=None, time=None, alt=None, crs=None, nested=F active = True out[item] = { "active": active, - "value": _format_value(value, n.style, add_enumeration_labels), - "units": n.style.units, + "value": value, + "label": _get_label(value, n.style, add_enumeration_labels), "inputs": inputs, "name": n.style.name if n.style.name else item, - "node_hash": n.hash, } + if compute_hash: + out[item]['node_hash'] = n.hash raw_values[item] = value # Fix sources for Compositors if isinstance(n, podpac.compositor.OrderedCompositor): From 8562f7deb44963300ad917f365bea252ce834edd Mon Sep 17 00:00:00 2001 From: David Hinckley Date: Thu, 20 Nov 2025 09:18:45 -0500 Subject: [PATCH 09/21] FIX: changes probe type return to node_class for clarity --- podpac/core/utils.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/podpac/core/utils.py b/podpac/core/utils.py index 299ebd28..86394d7a 100644 --- a/podpac/core/utils.py +++ b/podpac/core/utils.py @@ -519,6 +519,7 @@ def _get_entry(key, out, definition): # entry["value"] = entry["value"] + " " + str(out[key]["units"]) entry['label'] = out[key]['label'] entry["active"] = out[key]["active"] + entry['node_class'] = out[key]['node_class'] if 'node_hash' in out[key]: entry["node_id"] = out[key]["node_hash"] entry["params"] = {} @@ -526,22 +527,11 @@ def _get_entry(key, out, definition): if len(entry["inputs"]["inputs"]) == 0: entry["inputs"] = {} return entry - -def _format_value(value, style, add_enumeration_labels): - """Helper for probe_node().""" - if not add_enumeration_labels or style.enumeration_legend is None: - return value - if np.isnan(value): - return str(value) + " (unknown)" - try: - return str(int(value)) + " ({})".format(style.enumeration_legend[int(value)]) - except ValueError: - return str(value) + " (unknown)" def _get_label(value, style, add_enumeration_labels): if not add_enumeration_labels or style.enumeration_legend is None: return style.units - if isinstance(value,list): # all list returns should be 2-D + if isinstance(value, list): # all list returns should be 2-D ret = '' for v in np.unique(value): try: @@ -549,7 +539,7 @@ def _get_label(value, style, add_enumeration_labels): except ValueError: _log.warning('Enumeration label lookup failed for node of name {}, returning unknown'.format(style.name)) new_label = 'unknown' - ret += '{}={}, '.format(v,new_label) + ret += '{}={}, '.format(v, new_label) return ret[:-2] else: if np.isnan(value): @@ -623,6 +613,7 @@ def probe_node(node, lat=None, lon=None, time=None, alt=None, crs=None, nested=F "label": _get_label(value, n.style, add_enumeration_labels), "inputs": inputs, "name": n.style.name if n.style.name else item, + "node_class": type(n).__name__ } if compute_hash: out[item]['node_hash'] = n.hash From 4c9bf79cff484281f49cc39d796af4897c510ed8 Mon Sep 17 00:00:00 2001 From: John Walthour Date: Mon, 1 Dec 2025 14:25:19 -0500 Subject: [PATCH 10/21] Updated to get bounds from argument rather than from self --- podpac/core/data/datasource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/podpac/core/data/datasource.py b/podpac/core/data/datasource.py index f897fdd2..17f7726e 100644 --- a/podpac/core/data/datasource.py +++ b/podpac/core/data/datasource.py @@ -496,7 +496,7 @@ def create_output_array(self, coords, data=np.nan, attrs=None, outputs=None, **k {arr_return} """ output = super().create_output_array(coords, data=data, attrs=attrs, outputs=outputs, **kwargs) - output.attrs["bounds"], _ = self.get_bounds(crs=output.attrs["crs"]) + output.attrs["bounds"] = coords.transform(output.attrs["crs"]).bounds output.attrs["boundary_data"] = {} return output From ced229f1bfc4aaced89a7cb501565287eafa4c40 Mon Sep 17 00:00:00 2001 From: John Walthour Date: Mon, 8 Dec 2025 17:57:14 -0500 Subject: [PATCH 11/21] Make Selector objects callable so they can be passed in as selectors to datasources --- podpac/core/interpolation/selector.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/podpac/core/interpolation/selector.py b/podpac/core/interpolation/selector.py index 8c3f7f6a..4f29a042 100755 --- a/podpac/core/interpolation/selector.py +++ b/podpac/core/interpolation/selector.py @@ -76,6 +76,10 @@ def __init__(self, method=None): else: self.method = method + def __call__(self, source_coords, request_coords, index_type="numpy"): + """Pass-through to select()""" + return self.select(source_coords, request_coords, index_type) + def select(self, source_coords, request_coords, index_type="numpy"): """Sub-selects the source_coords based on the request_coords From 8405925add69b99dd60dcbc79d400f9003070fce Mon Sep 17 00:00:00 2001 From: John Walthour Date: Tue, 16 Dec 2025 10:42:24 -0500 Subject: [PATCH 12/21] Revert "Updated to get bounds from argument rather than from self" This reverts commit 4c9bf79cff484281f49cc39d796af4897c510ed8. --- podpac/core/data/datasource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/podpac/core/data/datasource.py b/podpac/core/data/datasource.py index 17f7726e..f897fdd2 100644 --- a/podpac/core/data/datasource.py +++ b/podpac/core/data/datasource.py @@ -496,7 +496,7 @@ def create_output_array(self, coords, data=np.nan, attrs=None, outputs=None, **k {arr_return} """ output = super().create_output_array(coords, data=data, attrs=attrs, outputs=outputs, **kwargs) - output.attrs["bounds"] = coords.transform(output.attrs["crs"]).bounds + output.attrs["bounds"], _ = self.get_bounds(crs=output.attrs["crs"]) output.attrs["boundary_data"] = {} return output From 8af9a57b703ac2e2c5267664df67772c42cc3c7d Mon Sep 17 00:00:00 2001 From: David Hinckley Date: Tue, 6 Jan 2026 13:30:26 -0500 Subject: [PATCH 13/21] FIX: for datasource.create_output_array, added common_doc decorator, fixed boundary_data value, added clarifying comments. --- podpac/core/data/datasource.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/podpac/core/data/datasource.py b/podpac/core/data/datasource.py index f897fdd2..99924807 100644 --- a/podpac/core/data/datasource.py +++ b/podpac/core/data/datasource.py @@ -474,10 +474,14 @@ def _eval(self, coordinates, output=None, _selector=None): return output + @common_doc(COMMON_DATA_DOC) def create_output_array(self, coords, data=np.nan, attrs=None, outputs=None, **kwargs): """ Initialize an output data array. This adds bounds to the output attrs + The boundary_data output.attrs is set to match this node's boundary. + For uniform grids, this expected to be an empty dictionary. + Parameters ---------- coords : podpac.Coordinates @@ -496,8 +500,8 @@ def create_output_array(self, coords, data=np.nan, attrs=None, outputs=None, **k {arr_return} """ output = super().create_output_array(coords, data=data, attrs=attrs, outputs=outputs, **kwargs) - output.attrs["bounds"], _ = self.get_bounds(crs=output.attrs["crs"]) - output.attrs["boundary_data"] = {} + output.attrs["bounds"], _ = self.get_bounds(crs=output.attrs["crs"]) # this is the bounds of the full dataset + output.attrs["boundary_data"] = self.boundary # this is the bounding polygon of the nonuniform dataset return output def find_coordinates(self): From d16a148ef80f9135efe6937dbd1e189dd64337e7 Mon Sep 17 00:00:00 2001 From: David Hinckley Date: Tue, 20 Jan 2026 09:29:54 -0500 Subject: [PATCH 14/21] FIX/DOC: removed selector callable, added mention of tag attr=True importance to nodes doc --- doc/source/nodes.md | 3 +++ podpac/core/interpolation/selector.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/source/nodes.md b/doc/source/nodes.md index 8d4ed5bc..56c2123c 100644 --- a/doc/source/nodes.md +++ b/doc/source/nodes.md @@ -141,6 +141,9 @@ output = node.execute(coords) You will also be able to set these tagged attrs in node definitions. + +The values of tagged attributes are preserved in node definitions, so it is important to tag all attributes that meaningfully contribute to a node's state as `attr=True`. + ## Serialization Any podpac Node can be saved, shared, and loaded using a JSON definition. This definition describes all of the nodes required to create and evaluate the final Node. diff --git a/podpac/core/interpolation/selector.py b/podpac/core/interpolation/selector.py index 4f29a042..3d9f0dce 100755 --- a/podpac/core/interpolation/selector.py +++ b/podpac/core/interpolation/selector.py @@ -76,9 +76,9 @@ def __init__(self, method=None): else: self.method = method - def __call__(self, source_coords, request_coords, index_type="numpy"): - """Pass-through to select()""" - return self.select(source_coords, request_coords, index_type) + # def __call__(self, source_coords, request_coords, index_type="numpy"): + # """Pass-through to select()""" + # return self.select(source_coords, request_coords, index_type) def select(self, source_coords, request_coords, index_type="numpy"): """Sub-selects the source_coords based on the request_coords From 89e13fee4eb3351b693c4174962cdfc9e1cb8641 Mon Sep 17 00:00:00 2001 From: dhinckley-creare Date: Tue, 20 Jan 2026 10:07:59 -0500 Subject: [PATCH 15/21] Update podpac/core/data/datasource.py Co-authored-by: mpu-creare --- podpac/core/data/datasource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/podpac/core/data/datasource.py b/podpac/core/data/datasource.py index 99924807..52455dbf 100644 --- a/podpac/core/data/datasource.py +++ b/podpac/core/data/datasource.py @@ -479,7 +479,7 @@ def create_output_array(self, coords, data=np.nan, attrs=None, outputs=None, **k """ Initialize an output data array. This adds bounds to the output attrs - The boundary_data output.attrs is set to match this node's boundary. + The `boundary_data` output.attrs is set to match this node's polygonal (i.e. non-rectangular) boundary. For uniform grids, this expected to be an empty dictionary. Parameters From 35c456a1862ff2d9c756dc245657c764c359a2b2 Mon Sep 17 00:00:00 2001 From: David Hinckley Date: Tue, 20 Jan 2026 10:20:58 -0500 Subject: [PATCH 16/21] FIX/DOC: removed commented out selector callable code. Added comment to explain utils._get_label --- podpac/core/interpolation/selector.py | 3 --- podpac/core/utils.py | 11 ++++++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/podpac/core/interpolation/selector.py b/podpac/core/interpolation/selector.py index 3d9f0dce..f39e0962 100755 --- a/podpac/core/interpolation/selector.py +++ b/podpac/core/interpolation/selector.py @@ -76,9 +76,6 @@ def __init__(self, method=None): else: self.method = method - # def __call__(self, source_coords, request_coords, index_type="numpy"): - # """Pass-through to select()""" - # return self.select(source_coords, request_coords, index_type) def select(self, source_coords, request_coords, index_type="numpy"): """Sub-selects the source_coords based on the request_coords diff --git a/podpac/core/utils.py b/podpac/core/utils.py index 86394d7a..07381433 100644 --- a/podpac/core/utils.py +++ b/podpac/core/utils.py @@ -528,7 +528,14 @@ def _get_entry(key, out, definition): entry["inputs"] = {} return entry + def _get_label(value, style, add_enumeration_labels): + """Helper for probe_node(). Handles both enumerations and units to be given back to the label field + + If no enumeration_legend is detected in style, or the user opts out of enumeration labels + with add_enumeration_labels = False, then units are returned. + Else, an enumeration label is determined, defaulting to "unknown" in error cases + """ if not add_enumeration_labels or style.enumeration_legend is None: return style.units if isinstance(value, list): # all list returns should be 2-D @@ -537,7 +544,9 @@ def _get_label(value, style, add_enumeration_labels): try: new_label = style.enumeration_legend[int(v)] except ValueError: - _log.warning('Enumeration label lookup failed for node of name {}, returning unknown'.format(style.name)) + _log.warning( + 'Enumeration label lookup failed for node of name {}, returning unknown'.format(style.name) + ) new_label = 'unknown' ret += '{}={}, '.format(v, new_label) return ret[:-2] From 9f143af4de2a21b96f7b00efd51fe8e39c4bc3e0 Mon Sep 17 00:00:00 2001 From: David Hinckley Date: Tue, 20 Jan 2026 10:24:06 -0500 Subject: [PATCH 17/21] DOC: removed old commented code --- podpac/core/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/podpac/core/utils.py b/podpac/core/utils.py index 07381433..8538a4bb 100644 --- a/podpac/core/utils.py +++ b/podpac/core/utils.py @@ -515,8 +515,6 @@ def _get_entry(key, out, definition): entry = OrderedDict() entry["name"] = out[key]["name"] entry["value"] = str(out[key]["value"]) - # if out[key]["units"] not in [None, ""]: - # entry["value"] = entry["value"] + " " + str(out[key]["units"]) entry['label'] = out[key]['label'] entry["active"] = out[key]["active"] entry['node_class'] = out[key]['node_class'] From 554fbc8a88c19a807508bae7df624f7b629a91f4 Mon Sep 17 00:00:00 2001 From: David Hinckley Date: Tue, 20 Jan 2026 14:41:47 -0500 Subject: [PATCH 18/21] ENH: improved unit test coverage of utils --- podpac/core/test/test_utils.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/podpac/core/test/test_utils.py b/podpac/core/test/test_utils.py index de462950..cb33e013 100644 --- a/podpac/core/test/test_utils.py +++ b/podpac/core/test/test_utils.py @@ -24,6 +24,7 @@ from podpac.core.utils import ind2slice from podpac.core.utils import probe_node from podpac.core.utils import align_xarray_dict +from podpac.core.utils import _get_param class TestCommonDocs(object): @@ -473,6 +474,9 @@ def test_nontuple(self): assert ind2slice([False, True, True, False, True, False]) == slice(1, 5) assert ind2slice([1, 3, 5]) == slice(1, 7, 2) + def test_empty_slice(self): + assert ind2slice([]) == slice(0, 0) + class AnotherOne(podpac.algorithm.Algorithm): def algorithm(self, inputs, coordinates): @@ -797,3 +801,25 @@ def test_align_xarray_dict(): assert(np.all(inputs['B'].data==data_b)) assert(np.all(inputs['C'].data==data_c)) assert(np.all((inputs['A'] + inputs['B'] + inputs['C']).shape == inputs['A'].shape)) + + +class TestGetParam: + def test_key_in_params_not_a_list(self): + params = {"test_key": 0} + ret = _get_param(params, "test_key") + assert ret == 0 + + def test_key_in_params_list(self): + params = {"test_key": [4, 5, 3, 0]} + ret = _get_param(params, "test_key") + assert ret == 4 + + def test_key_not_in_params_upper_in_params(self): + params = {"TEST_KEY": 0} + ret = _get_param(params, "test_key") + assert ret == 0 + + def test_key_not_in_params_upper_not_in_params(self): + params = {"test_key": 0} + ret = _get_param(params, "not_test_key") + assert ret is None From 57236579ddb81ea71386a47316aee8de4b78e67a Mon Sep 17 00:00:00 2001 From: David Hinckley Date: Wed, 21 Jan 2026 08:03:24 -0500 Subject: [PATCH 19/21] ENH: added unittests to improve code coverage --- podpac/core/test/test_utils.py | 56 ++++++++++++++++++++++++++++++++++ podpac/core/utils.py | 1 + setup.py | 1 + 3 files changed, 58 insertions(+) diff --git a/podpac/core/test/test_utils.py b/podpac/core/test/test_utils.py index cb33e013..46a653a8 100644 --- a/podpac/core/test/test_utils.py +++ b/podpac/core/test/test_utils.py @@ -13,6 +13,8 @@ import pandas as pd import xarray as xr import traitlets as tl +from requests import ConnectionError +from unittest.mock import MagicMock, patch import podpac from podpac.core.utils import common_doc @@ -25,6 +27,7 @@ from podpac.core.utils import probe_node from podpac.core.utils import align_xarray_dict from podpac.core.utils import _get_param +from podpac.core.utils import _get_from_url class TestCommonDocs(object): @@ -823,3 +826,56 @@ def test_key_not_in_params_upper_not_in_params(self): params = {"test_key": 0} ret = _get_param(params, "not_test_key") assert ret is None + + +class TestGetFromUrl: + def test_raise_requests_error(self): + mock_requests = MagicMock() + mock_requests.get.side_effect = ConnectionError("Test Connection Error") + + with patch("podpac.core.utils.requests", mock_requests): + ret = _get_from_url("TEST/URL", None) + assert ret is None + + def test_raise_runtime_error(self): + mock_requests = MagicMock() + mock_requests.get.side_effect = RuntimeError("Test Runtime Error") + + with patch("podpac.core.utils.requests", mock_requests): + ret = _get_from_url("TEST/URL", None) + assert ret is None + + def test_session_is_none(self): + mock_get_return = MagicMock() + mock_get_return.status_code = 200 + mock_get_return.validation_value = "Expected Return" + mock_requests = MagicMock() + mock_requests.get.return_value = mock_get_return + + with patch("podpac.core.utils.requests", mock_requests): + ret = _get_from_url("TEST/URL", None) + + assert ret.validation_value == "Expected Return" + + def test_session_is_not_none(self): + mock_get_return = MagicMock() + mock_get_return.status_code = 200 + mock_get_return.validation_value = "Expected Return" + mock_session = MagicMock() + mock_session.get.return_value = mock_get_return + + ret = _get_from_url("TEST/URL", mock_session) + + assert ret.validation_value == "Expected Return" + + def test_status_code_not_200(self): + mock_get_return = MagicMock() + mock_get_return.status_code = 000 + mock_get_return.validation_value = "Expected Return" + mock_requests = MagicMock() + mock_requests.get.return_value = mock_get_return + + with patch("podpac.core.utils.requests", mock_requests): + ret = _get_from_url("TEST/URL", None) + + assert ret.validation_value == "Expected Return" diff --git a/podpac/core/utils.py b/podpac/core/utils.py index 8538a4bb..c56d735c 100644 --- a/podpac/core/utils.py +++ b/podpac/core/utils.py @@ -347,6 +347,7 @@ def _get_from_url(url, session=None): r = None except RuntimeError as e: _log.warning("Cannot authenticate to {}. Check credentials. Error was as follows:".format(url) + str(e)) + r = None return r diff --git a/setup.py b/setup.py index e8bb9309..fa6f3590 100644 --- a/setup.py +++ b/setup.py @@ -72,6 +72,7 @@ "six>=1.0", "attrs>=17.4.0", "pre_commit>=1", + "unittest" ], } From abb00fc3814a890d5ad234f72aa004783f3c230a Mon Sep 17 00:00:00 2001 From: David Hinckley Date: Wed, 21 Jan 2026 08:05:59 -0500 Subject: [PATCH 20/21] FIX: removed unittest from setup --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index fa6f3590..78aaf6db 100644 --- a/setup.py +++ b/setup.py @@ -71,8 +71,7 @@ "coveralls>=1.3", "six>=1.0", "attrs>=17.4.0", - "pre_commit>=1", - "unittest" + "pre_commit>=1" ], } From 9f32f5f8aa796fc91005896518681acdb5661738 Mon Sep 17 00:00:00 2001 From: David Hinckley Date: Wed, 21 Jan 2026 10:37:34 -0500 Subject: [PATCH 21/21] DOC: updated datasource.create_output_array docstring --- podpac/core/data/datasource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/podpac/core/data/datasource.py b/podpac/core/data/datasource.py index 52455dbf..b28226aa 100644 --- a/podpac/core/data/datasource.py +++ b/podpac/core/data/datasource.py @@ -477,7 +477,7 @@ def _eval(self, coordinates, output=None, _selector=None): @common_doc(COMMON_DATA_DOC) def create_output_array(self, coords, data=np.nan, attrs=None, outputs=None, **kwargs): """ - Initialize an output data array. This adds bounds to the output attrs + Initialize an output data array. This adds `bounds` and `boundary_data` to the output attrs The `boundary_data` output.attrs is set to match this node's polygonal (i.e. non-rectangular) boundary. For uniform grids, this expected to be an empty dictionary.