From 8245088e0c18548e7cf21508fe7a85e727fb5bd6 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Thu, 29 Jul 2021 14:45:32 -0700 Subject: [PATCH 001/102] add input_fname var to cache_file function --- pangeo_forge_recipes/storage.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pangeo_forge_recipes/storage.py b/pangeo_forge_recipes/storage.py index efe71603..e85dec13 100644 --- a/pangeo_forge_recipes/storage.py +++ b/pangeo_forge_recipes/storage.py @@ -147,7 +147,7 @@ def _full_path(self, path: str) -> str: class CacheFSSpecTarget(FlatFSSpecTarget): """Alias for FlatFSSpecTarget""" - def cache_file(self, fname: str, **open_kwargs) -> None: + def cache_file(self, fname: str, secrets: Optional[str] = None, **open_kwargs) -> None: # check and see if the file already exists in the cache logger.info(f"Caching file '{fname}'") if self.exists(fname): @@ -158,7 +158,8 @@ def cache_file(self, fname: str, **open_kwargs) -> None: logger.info(f"File '{fname}' is already cached") return - input_opener = fsspec.open(fname, mode="rb", **open_kwargs) + input_fname = fname if not secrets else _add_query_string_secrets(fname, secrets) + input_opener = fsspec.open(input_fname, mode="rb", **open_kwargs) target_opener = self.open(fname, mode="wb") logger.info(f"Coping remote file '{fname}' to cache") _copy_btw_filesystems(input_opener, target_opener) @@ -240,3 +241,7 @@ def _slugify(value: str) -> str: value = unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii") value = re.sub(r"[^.\w\s-]+", "_", value.lower()) return re.sub(r"[-\s]+", "-", value).strip("-_") + + +def _add_query_string_secrets(fname: str, secrets: str) -> str: + return fname + secrets From 1175af4ccb5fd90723aef376e3243df3c7e50765 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Thu, 29 Jul 2021 14:54:21 -0700 Subject: [PATCH 002/102] add query_string_secrets param to XarrayZarrRecipe, pass through to cache_input function --- pangeo_forge_recipes/recipes/xarray_zarr.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pangeo_forge_recipes/recipes/xarray_zarr.py b/pangeo_forge_recipes/recipes/xarray_zarr.py index 70cf5cdc..985b474e 100644 --- a/pangeo_forge_recipes/recipes/xarray_zarr.py +++ b/pangeo_forge_recipes/recipes/xarray_zarr.py @@ -156,6 +156,7 @@ def cache_input( delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], metadata_cache: Optional[MetadataTarget], + query_string_secrets: Optional[str], is_opendap=bool, ) -> None: if cache_inputs: @@ -165,7 +166,7 @@ def cache_input( raise ValueError("input_cache is not set.") logger.info(f"Caching input '{input_key!s}'") fname = file_pattern[input_key] - input_cache.cache_file(fname, **fsspec_open_kwargs) + input_cache.cache_file(fname, query_string_secrets, **fsspec_open_kwargs) if cache_metadata: return cache_input_metadata( @@ -703,6 +704,7 @@ class XarrayZarrRecipe(BaseRecipe): lock_timeout: Optional[int] = None subset_inputs: SubsetSpec = field(default_factory=dict) is_opendap: bool = False + query_string_secrets: Optional[str] = None, # internal attributes not meant to be seen or accessed by user _concat_dim: str = field(default_factory=str, repr=False, init=False) @@ -846,6 +848,7 @@ def cache_input(self) -> Callable[[Hashable], None]: process_input=self.process_input, metadata_cache=self.metadata_cache, is_opendap=self.is_opendap, + query_string_secrets=self.query_string_secrets, ) @property From ee77eba90d68011964b7af69c07ad2f3b9f6559c Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Thu, 29 Jul 2021 14:59:06 -0700 Subject: [PATCH 003/102] lint --- pangeo_forge_recipes/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pangeo_forge_recipes/storage.py b/pangeo_forge_recipes/storage.py index e85dec13..d1eac2ca 100644 --- a/pangeo_forge_recipes/storage.py +++ b/pangeo_forge_recipes/storage.py @@ -147,7 +147,7 @@ def _full_path(self, path: str) -> str: class CacheFSSpecTarget(FlatFSSpecTarget): """Alias for FlatFSSpecTarget""" - def cache_file(self, fname: str, secrets: Optional[str] = None, **open_kwargs) -> None: + def cache_file(self, fname: str, secrets: Optional[str] = None, **open_kwargs) -> None: # check and see if the file already exists in the cache logger.info(f"Caching file '{fname}'") if self.exists(fname): From 45e661d3496e7caaf9d6b7fa6839fca22f42611a Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Thu, 29 Jul 2021 15:01:55 -0700 Subject: [PATCH 004/102] don't need default arg in storage.py bc it's enforced by XarrayZarrRecipe defaults --- pangeo_forge_recipes/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pangeo_forge_recipes/storage.py b/pangeo_forge_recipes/storage.py index d1eac2ca..14c09f8a 100644 --- a/pangeo_forge_recipes/storage.py +++ b/pangeo_forge_recipes/storage.py @@ -147,7 +147,7 @@ def _full_path(self, path: str) -> str: class CacheFSSpecTarget(FlatFSSpecTarget): """Alias for FlatFSSpecTarget""" - def cache_file(self, fname: str, secrets: Optional[str] = None, **open_kwargs) -> None: + def cache_file(self, fname: str, secrets: Optional[str], **open_kwargs) -> None: # check and see if the file already exists in the cache logger.info(f"Caching file '{fname}'") if self.exists(fname): From 62904b6fb3a8778a8016b867ba2ef0e3e1543c3f Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Thu, 29 Jul 2021 15:25:58 -0700 Subject: [PATCH 005/102] dataclass param lines don't terminate in commas --- pangeo_forge_recipes/recipes/xarray_zarr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pangeo_forge_recipes/recipes/xarray_zarr.py b/pangeo_forge_recipes/recipes/xarray_zarr.py index 985b474e..76f02836 100644 --- a/pangeo_forge_recipes/recipes/xarray_zarr.py +++ b/pangeo_forge_recipes/recipes/xarray_zarr.py @@ -704,7 +704,7 @@ class XarrayZarrRecipe(BaseRecipe): lock_timeout: Optional[int] = None subset_inputs: SubsetSpec = field(default_factory=dict) is_opendap: bool = False - query_string_secrets: Optional[str] = None, + query_string_secrets: Optional[str] = None # internal attributes not meant to be seen or accessed by user _concat_dim: str = field(default_factory=str, repr=False, init=False) From 5d496e335e3bb871a93b2dc4c580622757ac229a Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Sat, 31 Jul 2021 15:04:55 -0700 Subject: [PATCH 006/102] update tests for query string secrets --- tests/conftest.py | 12 ++++++++++++ tests/test_storage.py | 15 +++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5e65c71f..ed171a98 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -160,6 +160,13 @@ def teardown(): return all_urls, items_per_file +@pytest.fixture(scope="session") +def netcdf_http_paths_with_secrets(netcdf_http_paths, fake_secrets): + all_urls, items_per_file = netcdf_http_paths + all_urls = [url + fake_secrets for url in all_urls] + return all_urls, items_per_file + + @pytest.fixture() def tmp_target(tmpdir_factory): fs = fsspec.get_filesystem_class("file")() @@ -175,6 +182,11 @@ def tmp_cache(tmpdir_factory): return cache +@pytest.fixture(scope="session") +def fake_secrets(): + return "?a-pretend-api-token" + + @pytest.fixture() def tmp_metadata_target(tmpdir_factory): path = str(tmpdir_factory.mktemp("cache")) diff --git a/tests/test_storage.py b/tests/test_storage.py index 1a65521c..d912127a 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -40,25 +40,32 @@ def test_metadata_target(tmp_metadata_target): @pytest.mark.parametrize( - "file_paths", [lazy_fixture("netcdf_local_paths"), lazy_fixture("netcdf_http_paths")] + "file_paths", + [ + lazy_fixture("netcdf_local_paths"), lazy_fixture("netcdf_http_paths"), + lazy_fixture("netcdf_http_paths_with_secrets"), + ] ) @pytest.mark.parametrize("copy_to_local", [False, True]) @pytest.mark.parametrize("use_cache, cache_first", [(False, False), (True, False), (True, True)]) @pytest.mark.parametrize("use_dask", [True, False]) @pytest.mark.parametrize("use_xarray", [True, False]) +@pytest.mark.parametrize("use_query_string_secrets", [True, False]) def test_file_opener( - file_paths, tmp_cache, copy_to_local, use_cache, cache_first, dask_cluster, use_dask, use_xarray + file_paths, tmp_cache, fake_secrets, copy_to_local, use_cache, cache_first, dask_cluster, + use_dask, use_xarray, use_query_string_secrets, ): all_paths, _ = file_paths path = str(all_paths[0]) cache = tmp_cache if use_cache else None + secrets = fake_secrets if use_query_string_secrets and file_paths[0][-1] == "?" else None def do_actual_test(): if cache_first: - cache.cache_file(path) + cache.cache_file(path, secrets) assert cache.exists(path) details = cache.fs.ls(cache.root_path, detail=True) - cache.cache_file(path) + cache.cache_file(path, secrets) # check that nothing happened assert cache.fs.ls(cache.root_path, detail=True) == details From 688135c36d98ab8adb0667db2524177d37f4ddfb Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Sat, 31 Jul 2021 15:08:24 -0700 Subject: [PATCH 007/102] lint tests --- tests/test_storage.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/test_storage.py b/tests/test_storage.py index d912127a..c9676079 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -42,9 +42,10 @@ def test_metadata_target(tmp_metadata_target): @pytest.mark.parametrize( "file_paths", [ - lazy_fixture("netcdf_local_paths"), lazy_fixture("netcdf_http_paths"), + lazy_fixture("netcdf_local_paths"), + lazy_fixture("netcdf_http_paths"), lazy_fixture("netcdf_http_paths_with_secrets"), - ] + ], ) @pytest.mark.parametrize("copy_to_local", [False, True]) @pytest.mark.parametrize("use_cache, cache_first", [(False, False), (True, False), (True, True)]) @@ -52,8 +53,16 @@ def test_metadata_target(tmp_metadata_target): @pytest.mark.parametrize("use_xarray", [True, False]) @pytest.mark.parametrize("use_query_string_secrets", [True, False]) def test_file_opener( - file_paths, tmp_cache, fake_secrets, copy_to_local, use_cache, cache_first, dask_cluster, - use_dask, use_xarray, use_query_string_secrets, + file_paths, + tmp_cache, + fake_secrets, + copy_to_local, + use_cache, + cache_first, + dask_cluster, + use_dask, + use_xarray, + use_query_string_secrets, ): all_paths, _ = file_paths path = str(all_paths[0]) From 3680a34f8a729851fe9927ca3af906ae856d2658 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 3 Aug 2021 18:02:41 -0700 Subject: [PATCH 008/102] parse + unparse url in _add_query_string_secrets --- pangeo_forge_recipes/storage.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pangeo_forge_recipes/storage.py b/pangeo_forge_recipes/storage.py index 14c09f8a..d49ad2e0 100644 --- a/pangeo_forge_recipes/storage.py +++ b/pangeo_forge_recipes/storage.py @@ -6,6 +6,7 @@ import tempfile import time import unicodedata +from urllib.parse import urlparse, urlunparse from abc import ABC, abstractmethod from contextlib import contextmanager from dataclasses import dataclass @@ -244,4 +245,7 @@ def _slugify(value: str) -> str: def _add_query_string_secrets(fname: str, secrets: str) -> str: - return fname + secrets + parsed = urlparse(fname) + query = secrets if len(parsed.query) == 0 else f"{parsed.query}&{secrets}" + parsed = parsed._replace(query=query) + return urlunparse(parsed) From 78e2af3ef2d84c3bc60c22415b56531f84d4f7a8 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 3 Aug 2021 18:10:11 -0700 Subject: [PATCH 009/102] wrap fsspec.open with _get_opener --- pangeo_forge_recipes/storage.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pangeo_forge_recipes/storage.py b/pangeo_forge_recipes/storage.py index d49ad2e0..3503f78f 100644 --- a/pangeo_forge_recipes/storage.py +++ b/pangeo_forge_recipes/storage.py @@ -21,8 +21,8 @@ OpenFileType = Any -def _get_url_size(fname, **open_kwargs): - with fsspec.open(fname, mode="rb", **open_kwargs) as of: +def _get_url_size(fname, secrets, **open_kwargs): + with _get_opener(fname, secrets, mode="rb", **open_kwargs) as of: size = of.size return size @@ -153,14 +153,13 @@ def cache_file(self, fname: str, secrets: Optional[str], **open_kwargs) -> None: logger.info(f"Caching file '{fname}'") if self.exists(fname): cached_size = self.size(fname) - remote_size = _get_url_size(fname, **open_kwargs) + remote_size = _get_url_size(fname, secrets, **open_kwargs) if cached_size == remote_size: # TODO: add checksumming here logger.info(f"File '{fname}' is already cached") return - input_fname = fname if not secrets else _add_query_string_secrets(fname, secrets) - input_opener = fsspec.open(input_fname, mode="rb", **open_kwargs) + input_opener = _get_opener(fname, secrets, mode="rb", **open_kwargs) target_opener = self.open(fname, mode="wb") logger.info(f"Coping remote file '{fname}' to cache") _copy_btw_filesystems(input_opener, target_opener) @@ -188,6 +187,7 @@ def file_opener( cache: Optional[CacheFSSpecTarget] = None, copy_to_local: bool = False, bypass_open: bool = False, + secrets: Optional[str] = None, **open_kwargs, ) -> Iterator[Union[OpenFileType, str]]: """ @@ -215,7 +215,7 @@ def file_opener( opener = cache.open(fname, mode="rb") else: logger.info(f"Opening '{fname}' directly.") - opener = fsspec.open(fname, mode="rb", **open_kwargs) + opener = _get_opener(fname, secrets, mode="rb", **open_kwargs) if copy_to_local: _, suffix = os.path.splitext(fname) ntf = tempfile.NamedTemporaryFile(suffix=suffix) @@ -249,3 +249,8 @@ def _add_query_string_secrets(fname: str, secrets: str) -> str: query = secrets if len(parsed.query) == 0 else f"{parsed.query}&{secrets}" parsed = parsed._replace(query=query) return urlunparse(parsed) + + +def _get_opener(fname: str, secrets: Optional[str], **open_kwargs): + fname = fname if not secrets else _add_query_string_secrets(fname, secrets) + return fsspec.open(fname, mode="rb", **open_kwargs) From ea858a037e19c5e2e4526669a32826d8807d1653 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 3 Aug 2021 18:16:50 -0700 Subject: [PATCH 010/102] remove fake_secrets fixture --- tests/conftest.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ed171a98..4ad91cf4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -161,9 +161,9 @@ def teardown(): @pytest.fixture(scope="session") -def netcdf_http_paths_with_secrets(netcdf_http_paths, fake_secrets): +def netcdf_http_paths_with_secrets(netcdf_http_paths): all_urls, items_per_file = netcdf_http_paths - all_urls = [url + fake_secrets for url in all_urls] + all_urls = [url + "?a-pretend-api-token" for url in all_urls] return all_urls, items_per_file @@ -182,11 +182,6 @@ def tmp_cache(tmpdir_factory): return cache -@pytest.fixture(scope="session") -def fake_secrets(): - return "?a-pretend-api-token" - - @pytest.fixture() def tmp_metadata_target(tmpdir_factory): path = str(tmpdir_factory.mktemp("cache")) From c42151e6ddce7123e4349748ee54b059f9fb8fb0 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 3 Aug 2021 18:27:35 -0700 Subject: [PATCH 011/102] correct & simplify additions to test_storage.py --- tests/test_storage.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_storage.py b/tests/test_storage.py index c9676079..25e61096 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -51,23 +51,20 @@ def test_metadata_target(tmp_metadata_target): @pytest.mark.parametrize("use_cache, cache_first", [(False, False), (True, False), (True, True)]) @pytest.mark.parametrize("use_dask", [True, False]) @pytest.mark.parametrize("use_xarray", [True, False]) -@pytest.mark.parametrize("use_query_string_secrets", [True, False]) def test_file_opener( file_paths, tmp_cache, - fake_secrets, copy_to_local, use_cache, cache_first, dask_cluster, use_dask, use_xarray, - use_query_string_secrets, ): all_paths, _ = file_paths path = str(all_paths[0]) cache = tmp_cache if use_cache else None - secrets = fake_secrets if use_query_string_secrets and file_paths[0][-1] == "?" else None + secrets = "?a-pretend-api-token" if "?a-pretend-api-token" in file_paths[0] else None def do_actual_test(): if cache_first: From d276d1ad06625fb2f092f80c65c8e79ea2e2d767 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 3 Aug 2021 18:31:23 -0700 Subject: [PATCH 012/102] _get_opener always takes mode='rb' --- pangeo_forge_recipes/storage.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pangeo_forge_recipes/storage.py b/pangeo_forge_recipes/storage.py index 3503f78f..6ef4785d 100644 --- a/pangeo_forge_recipes/storage.py +++ b/pangeo_forge_recipes/storage.py @@ -22,7 +22,7 @@ def _get_url_size(fname, secrets, **open_kwargs): - with _get_opener(fname, secrets, mode="rb", **open_kwargs) as of: + with _get_opener(fname, secrets, **open_kwargs) as of: size = of.size return size @@ -159,7 +159,7 @@ def cache_file(self, fname: str, secrets: Optional[str], **open_kwargs) -> None: logger.info(f"File '{fname}' is already cached") return - input_opener = _get_opener(fname, secrets, mode="rb", **open_kwargs) + input_opener = _get_opener(fname, secrets, **open_kwargs) target_opener = self.open(fname, mode="wb") logger.info(f"Coping remote file '{fname}' to cache") _copy_btw_filesystems(input_opener, target_opener) @@ -215,7 +215,7 @@ def file_opener( opener = cache.open(fname, mode="rb") else: logger.info(f"Opening '{fname}' directly.") - opener = _get_opener(fname, secrets, mode="rb", **open_kwargs) + opener = _get_opener(fname, secrets, **open_kwargs) if copy_to_local: _, suffix = os.path.splitext(fname) ntf = tempfile.NamedTemporaryFile(suffix=suffix) From 6874ec6ce7a6f6e3035fd42e00f161110ed92966 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 3 Aug 2021 18:35:20 -0700 Subject: [PATCH 013/102] lint --- pangeo_forge_recipes/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pangeo_forge_recipes/storage.py b/pangeo_forge_recipes/storage.py index 6ef4785d..fb68d41e 100644 --- a/pangeo_forge_recipes/storage.py +++ b/pangeo_forge_recipes/storage.py @@ -6,11 +6,11 @@ import tempfile import time import unicodedata -from urllib.parse import urlparse, urlunparse from abc import ABC, abstractmethod from contextlib import contextmanager from dataclasses import dataclass from typing import Any, Iterator, Optional, Sequence, Union +from urllib.parse import urlparse, urlunparse import fsspec from fsspec.implementations.http import BlockSizeError From 4ca70faff17d445a8bbd52949e97cbd97bbd9a07 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 3 Aug 2021 19:12:05 -0700 Subject: [PATCH 014/102] simplify token string --- tests/conftest.py | 2 +- tests/test_storage.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 4ad91cf4..663e0e2b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -163,7 +163,7 @@ def teardown(): @pytest.fixture(scope="session") def netcdf_http_paths_with_secrets(netcdf_http_paths): all_urls, items_per_file = netcdf_http_paths - all_urls = [url + "?a-pretend-api-token" for url in all_urls] + all_urls = [url + "?token=bar" for url in all_urls] return all_urls, items_per_file diff --git a/tests/test_storage.py b/tests/test_storage.py index 25e61096..29fadb9e 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -64,7 +64,7 @@ def test_file_opener( all_paths, _ = file_paths path = str(all_paths[0]) cache = tmp_cache if use_cache else None - secrets = "?a-pretend-api-token" if "?a-pretend-api-token" in file_paths[0] else None + secrets = "token=bar" if "?" in file_paths[0] else None def do_actual_test(): if cache_first: From 91a16ce770b8121424b6452e29fd988141ae231a Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 3 Aug 2021 19:17:05 -0700 Subject: [PATCH 015/102] add test for multiple params in query string --- tests/conftest.py | 7 +++++++ tests/test_storage.py | 1 + 2 files changed, 8 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 663e0e2b..7eebc025 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -167,6 +167,13 @@ def netcdf_http_paths_with_secrets(netcdf_http_paths): return all_urls, items_per_file +@pytest.fixture(scope="session") +def netcdf_http_paths_with_multiparam_secrets(netcdf_http_paths): + all_urls, items_per_file = netcdf_http_paths + all_urls = [url + "?filename=foo.nc&token=bar" for url in all_urls] + return all_urls, items_per_file + + @pytest.fixture() def tmp_target(tmpdir_factory): fs = fsspec.get_filesystem_class("file")() diff --git a/tests/test_storage.py b/tests/test_storage.py index 29fadb9e..c509ee5f 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -45,6 +45,7 @@ def test_metadata_target(tmp_metadata_target): lazy_fixture("netcdf_local_paths"), lazy_fixture("netcdf_http_paths"), lazy_fixture("netcdf_http_paths_with_secrets"), + lazy_fixture("netcdf_http_paths_with_multiparam_secrets"), ], ) @pytest.mark.parametrize("copy_to_local", [False, True]) From 76c7ced1883e39cc0efc2b4439217ffac3d5fddc Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 3 Aug 2021 19:22:17 -0700 Subject: [PATCH 016/102] cleaner to make secrets conditional on 'path' rather than 'file_paths' --- tests/test_storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_storage.py b/tests/test_storage.py index c509ee5f..1f4ad6b6 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -65,7 +65,7 @@ def test_file_opener( all_paths, _ = file_paths path = str(all_paths[0]) cache = tmp_cache if use_cache else None - secrets = "token=bar" if "?" in file_paths[0] else None + secrets = "token=bar" if "?" in path else None def do_actual_test(): if cache_first: From de679dc65bebdbd8bb932d0170dd379b66c72550 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 3 Aug 2021 20:17:50 -0700 Subject: [PATCH 017/102] remove fake token from fixtured path if present --- tests/test_storage.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_storage.py b/tests/test_storage.py index 1f4ad6b6..e375d37b 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -63,9 +63,16 @@ def test_file_opener( use_xarray, ): all_paths, _ = file_paths - path = str(all_paths[0]) + + if "?" not in str(all_paths[0]): + path = str(all_paths[0]) + elif "&" not in str(all_paths[0]): + path = str(all_paths[0]).split("?")[0] + else: + path = str(all_paths[0]).split("&")[0] + cache = tmp_cache if use_cache else None - secrets = "token=bar" if "?" in path else None + secrets = "token=bar" if "?" in str(all_paths[0]) else None def do_actual_test(): if cache_first: From a158c044f1cf600c37f06aef1bb4824ebae93119 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 13:16:49 -0700 Subject: [PATCH 018/102] use parse_qs + urlencode to allow secrets to be a dict --- pangeo_forge_recipes/storage.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pangeo_forge_recipes/storage.py b/pangeo_forge_recipes/storage.py index fb68d41e..25b105c8 100644 --- a/pangeo_forge_recipes/storage.py +++ b/pangeo_forge_recipes/storage.py @@ -10,7 +10,7 @@ from contextlib import contextmanager from dataclasses import dataclass from typing import Any, Iterator, Optional, Sequence, Union -from urllib.parse import urlparse, urlunparse +from urllib.parse import urlparse, urlunparse, parse_qs, urlencode import fsspec from fsspec.implementations.http import BlockSizeError @@ -148,7 +148,7 @@ def _full_path(self, path: str) -> str: class CacheFSSpecTarget(FlatFSSpecTarget): """Alias for FlatFSSpecTarget""" - def cache_file(self, fname: str, secrets: Optional[str], **open_kwargs) -> None: + def cache_file(self, fname: str, secrets: Optional[dict], **open_kwargs) -> None: # check and see if the file already exists in the cache logger.info(f"Caching file '{fname}'") if self.exists(fname): @@ -187,7 +187,7 @@ def file_opener( cache: Optional[CacheFSSpecTarget] = None, copy_to_local: bool = False, bypass_open: bool = False, - secrets: Optional[str] = None, + secrets: Optional[dict] = None, **open_kwargs, ) -> Iterator[Union[OpenFileType, str]]: """ @@ -244,13 +244,15 @@ def _slugify(value: str) -> str: return re.sub(r"[-\s]+", "-", value).strip("-_") -def _add_query_string_secrets(fname: str, secrets: str) -> str: +def _add_query_string_secrets(fname: str, secrets: dict) -> str: parsed = urlparse(fname) - query = secrets if len(parsed.query) == 0 else f"{parsed.query}&{secrets}" - parsed = parsed._replace(query=query) + query = parse_qs(parsed.query) + for k, v in secrets.items(): + query.update({k: v}) + parsed = parsed._replace(query=urlencode(query, doseq=True)) return urlunparse(parsed) -def _get_opener(fname: str, secrets: Optional[str], **open_kwargs): +def _get_opener(fname: str, secrets: Optional[dict], **open_kwargs): fname = fname if not secrets else _add_query_string_secrets(fname, secrets) return fsspec.open(fname, mode="rb", **open_kwargs) From a4c32ab8bc668ceb6726676b1fc9334bed013fd7 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 13:17:22 -0700 Subject: [PATCH 019/102] update type hint and docstring in xarray_zarr to reflect new secrets type --- pangeo_forge_recipes/recipes/xarray_zarr.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pangeo_forge_recipes/recipes/xarray_zarr.py b/pangeo_forge_recipes/recipes/xarray_zarr.py index 76f02836..5a37724e 100644 --- a/pangeo_forge_recipes/recipes/xarray_zarr.py +++ b/pangeo_forge_recipes/recipes/xarray_zarr.py @@ -156,7 +156,7 @@ def cache_input( delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], metadata_cache: Optional[MetadataTarget], - query_string_secrets: Optional[str], + query_string_secrets: Optional[dict], is_opendap=bool, ) -> None: if cache_inputs: @@ -684,6 +684,8 @@ class XarrayZarrRecipe(BaseRecipe): time dimension. Multiple dimensions are allowed. :param is_opednap: If True, assume all input fnames represent opendap endpoints. Cannot be used with caching. + :param query_string_secrets: If provided, these key/value pairs are appended to + the query string of each ``file_pattern`` url at runtime. """ file_pattern: FilePattern @@ -704,7 +706,7 @@ class XarrayZarrRecipe(BaseRecipe): lock_timeout: Optional[int] = None subset_inputs: SubsetSpec = field(default_factory=dict) is_opendap: bool = False - query_string_secrets: Optional[str] = None + query_string_secrets: Optional[dict] = None # internal attributes not meant to be seen or accessed by user _concat_dim: str = field(default_factory=str, repr=False, init=False) From 25a67a2a6525539a1271b9e947816d852dd9c24b Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 14:14:02 -0700 Subject: [PATCH 020/102] update secrets to dict in test_storage.py --- tests/test_storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_storage.py b/tests/test_storage.py index e375d37b..7ea40c09 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -72,7 +72,7 @@ def test_file_opener( path = str(all_paths[0]).split("&")[0] cache = tmp_cache if use_cache else None - secrets = "token=bar" if "?" in str(all_paths[0]) else None + secrets = {"token": "bar"} if "?" in str(all_paths[0]) else None def do_actual_test(): if cache_first: From a34b31ea0cbfef52327e74a96f08a0dad48fc213 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 15:08:43 -0700 Subject: [PATCH 021/102] refactor existing tests for click cli --- tests/conftest.py | 8 ++--- tests/http_auth_server.py | 67 +++++++++++++++++++++------------------ 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7eebc025..8654eb22 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -140,13 +140,11 @@ def netcdf_http_paths(netcdf_local_paths, request): command_list = [ "python", os.path.join(this_dir, "http_auth_server.py"), - port, - "127.0.0.1", - username, - password, + f"--port={port}", + "--address=127.0.0.1", ] if username: - command_list += [username, password] + command_list += [f"--username={username}", f"--password={password}"] p = subprocess.Popen(command_list, cwd=basedir) url = f"http://127.0.0.1:{port}" time.sleep(2) # let the server start up diff --git a/tests/http_auth_server.py b/tests/http_auth_server.py index 0618ed39..0bc72166 100644 --- a/tests/http_auth_server.py +++ b/tests/http_auth_server.py @@ -1,33 +1,40 @@ import base64 import http.server import socketserver -import sys - -port_str, ADDRESS = sys.argv[1:3] -PORT = int(port_str) -if len(sys.argv) > 3: - username, password = sys.argv[3:5] -else: - username, password = "", "" - - -class Handler(http.server.SimpleHTTPRequestHandler): - def do_GET(self): - if username: - auth = self.headers.get("Authorization") - if ( - auth is None - or not auth.startswith("Basic") - or auth[6:] - != str(base64.b64encode((username + ":" + password).encode("utf-8")), "utf-8") - ): - self.send_response(401) - self.send_header("WWW-Authenticate", "Basic") - self.end_headers() - return - return http.server.SimpleHTTPRequestHandler.do_GET(self) - - -socketserver.TCPServer.allow_reuse_address = True -with socketserver.TCPServer((ADDRESS, PORT), Handler) as httpd: - httpd.serve_forever() + +import click + + +@click.command() +@click.option("--address") +@click.option("--port") +@click.option("--username") +@click.option("--password") +def serve_forever(address, port, username, password): + + port = int(port) + + class Handler(http.server.SimpleHTTPRequestHandler): + + def do_GET(self): + if username: + auth = self.headers.get("Authorization") + if ( + auth is None + or not auth.startswith("Basic") + or auth[6:] + != str(base64.b64encode((username + ":" + password).encode("utf-8")), "utf-8") + ): + self.send_response(401) + self.send_header("WWW-Authenticate", "Basic") + self.end_headers() + return + return http.server.SimpleHTTPRequestHandler.do_GET(self) + + socketserver.TCPServer.allow_reuse_address = True + with socketserver.TCPServer((address, port), Handler) as httpd: + httpd.serve_forever() + + +if __name__ == '__main__': + serve_forever() From 85d317dc67469a6f474f83ba75ae3afd8402d6a8 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 15:11:50 -0700 Subject: [PATCH 022/102] add click as known_third_party --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index c856d560..86918c4a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,7 +56,7 @@ max-line-length = 100 [isort] known_first_party=pangeo_forge_recipes -known_third_party=dask,fsspec,numpy,pandas,prefect,pytest,pytest_lazyfixture,rechunker,setuptools,xarray,zarr +known_third_party=click,dask,fsspec,numpy,pandas,prefect,pytest,pytest_lazyfixture,rechunker,setuptools,xarray,zarr multi_line_output=3 include_trailing_comma=True force_grid_wrap=0 From c530e8e91df3a875e50a7cce9debd249aaf3a6f0 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 15:11:59 -0700 Subject: [PATCH 023/102] lint --- pangeo_forge_recipes/storage.py | 2 +- tests/http_auth_server.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pangeo_forge_recipes/storage.py b/pangeo_forge_recipes/storage.py index 25b105c8..22725284 100644 --- a/pangeo_forge_recipes/storage.py +++ b/pangeo_forge_recipes/storage.py @@ -10,7 +10,7 @@ from contextlib import contextmanager from dataclasses import dataclass from typing import Any, Iterator, Optional, Sequence, Union -from urllib.parse import urlparse, urlunparse, parse_qs, urlencode +from urllib.parse import parse_qs, urlencode, urlparse, urlunparse import fsspec from fsspec.implementations.http import BlockSizeError diff --git a/tests/http_auth_server.py b/tests/http_auth_server.py index 0bc72166..ef28acab 100644 --- a/tests/http_auth_server.py +++ b/tests/http_auth_server.py @@ -15,7 +15,6 @@ def serve_forever(address, port, username, password): port = int(port) class Handler(http.server.SimpleHTTPRequestHandler): - def do_GET(self): if username: auth = self.headers.get("Authorization") @@ -36,5 +35,5 @@ def do_GET(self): httpd.serve_forever() -if __name__ == '__main__': +if __name__ == "__main__": serve_forever() From afe9ed0d5cddad8e096eba3b7c760ed285d31ad7 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 15:38:23 -0700 Subject: [PATCH 024/102] make items_per_file a fixture --- tests/conftest.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8654eb22..8b96e8dc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -95,28 +95,33 @@ def _split_up_files_by_variable_and_day(ds, day_param): @pytest.fixture(scope="session", params=["D", "2D"]) -def netcdf_local_paths(daily_xarray_dataset, tmpdir_factory, request): +def items_per_file(request): + return request.param + + +@pytest.fixture(scope="session") +def netcdf_local_paths(daily_xarray_dataset, tmpdir_factory, items_per_file): """Return a list of paths pointing to netcdf files.""" tmp_path = tmpdir_factory.mktemp("netcdf_data") # copy needed to avoid polluting metadata across multiple tests - datasets, fnames = _split_up_files_by_day(daily_xarray_dataset.copy(), request.param) + datasets, fnames = _split_up_files_by_day(daily_xarray_dataset.copy(), items_per_file) full_paths = [tmp_path.join(fname) for fname in fnames] xr.save_mfdataset(datasets, [str(path) for path in full_paths]) - items_per_file = {"D": 1, "2D": 2}[request.param] + items_per_file = {"D": 1, "2D": 2}[items_per_file] return full_paths, items_per_file # TODO: this is quite repetetive of the fixture above. Replace with parametrization. -@pytest.fixture(scope="session", params=["D", "2D"]) -def netcdf_local_paths_by_variable(daily_xarray_dataset, tmpdir_factory, request): +@pytest.fixture(scope="session") +def netcdf_local_paths_by_variable(daily_xarray_dataset, tmpdir_factory, items_per_file): """Return a list of paths pointing to netcdf files.""" tmp_path = tmpdir_factory.mktemp("netcdf_data") datasets, fnames, fnames_by_variable = _split_up_files_by_variable_and_day( - daily_xarray_dataset.copy(), request.param + daily_xarray_dataset.copy(), items_per_file ) full_paths = [tmp_path.join(fname) for fname in fnames] xr.save_mfdataset(datasets, [str(path) for path in full_paths]) - items_per_file = {"D": 1, "2D": 2}[request.param] + items_per_file = {"D": 1, "2D": 2}[items_per_file] path_format = str(tmp_path) + "/{variable}_{time:03d}.nc" return full_paths, items_per_file, fnames_by_variable, path_format From ba798e0e8295f6b65f84cb82b1611cf61a496152 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 15:50:36 -0700 Subject: [PATCH 025/102] refactor 'local_paths' and 'by_variable' into single fixture --- tests/conftest.py | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8b96e8dc..697a6e8f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -99,31 +99,33 @@ def items_per_file(request): return request.param -@pytest.fixture(scope="session") -def netcdf_local_paths(daily_xarray_dataset, tmpdir_factory, items_per_file): - """Return a list of paths pointing to netcdf files.""" - tmp_path = tmpdir_factory.mktemp("netcdf_data") - # copy needed to avoid polluting metadata across multiple tests - datasets, fnames = _split_up_files_by_day(daily_xarray_dataset.copy(), items_per_file) - full_paths = [tmp_path.join(fname) for fname in fnames] - xr.save_mfdataset(datasets, [str(path) for path in full_paths]) - items_per_file = {"D": 1, "2D": 2}[items_per_file] - return full_paths, items_per_file +@pytest.fixture( + scope="session", + params=[_split_up_files_by_day, _split_up_files_by_variable_and_day] +) +def file_splitter(request): + return request.param -# TODO: this is quite repetetive of the fixture above. Replace with parametrization. @pytest.fixture(scope="session") -def netcdf_local_paths_by_variable(daily_xarray_dataset, tmpdir_factory, items_per_file): - """Return a list of paths pointing to netcdf files.""" +def netcdf_paths(daily_xarray_dataset, tmpdir_factory, items_per_file, file_splitter): tmp_path = tmpdir_factory.mktemp("netcdf_data") - datasets, fnames, fnames_by_variable = _split_up_files_by_variable_and_day( - daily_xarray_dataset.copy(), items_per_file - ) + file_splitter_tuple = file_splitter(daily_xarray_dataset.copy(), items_per_file) + + if len(file_splitter_tuple) == 2: + datasets, fnames = file_splitter_tuple + else: + datasets, fnames, fnames_by_variable = file_splitter_tuple + full_paths = [tmp_path.join(fname) for fname in fnames] xr.save_mfdataset(datasets, [str(path) for path in full_paths]) items_per_file = {"D": 1, "2D": 2}[items_per_file] - path_format = str(tmp_path) + "/{variable}_{time:03d}.nc" - return full_paths, items_per_file, fnames_by_variable, path_format + + if not fnames_by_variable: + return full_paths, items_per_file + else: + path_format = str(tmp_path) + "/{variable}_{time:03d}.nc" + return full_paths, items_per_file, fnames_by_variable, path_format # TODO: refactor to allow netcdf_local_paths_by_variable to be passed without From 56049c99e1eb3a39f3a0d3fc198e45b5b9af10d1 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 16:05:20 -0700 Subject: [PATCH 026/102] simplify conditional tuple unpacking in conftest.py --- tests/conftest.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 697a6e8f..5d35c8cd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -112,16 +112,15 @@ def netcdf_paths(daily_xarray_dataset, tmpdir_factory, items_per_file, file_spli tmp_path = tmpdir_factory.mktemp("netcdf_data") file_splitter_tuple = file_splitter(daily_xarray_dataset.copy(), items_per_file) - if len(file_splitter_tuple) == 2: - datasets, fnames = file_splitter_tuple - else: - datasets, fnames, fnames_by_variable = file_splitter_tuple + datasets, fnames = file_splitter_tuple[:2] + if len(file_splitter_tuple) == 3: + fnames_by_variable = file_splitter_tuple[2] full_paths = [tmp_path.join(fname) for fname in fnames] xr.save_mfdataset(datasets, [str(path) for path in full_paths]) items_per_file = {"D": 1, "2D": 2}[items_per_file] - if not fnames_by_variable: + if len(file_splitter_tuple) == 2: return full_paths, items_per_file else: path_format = str(tmp_path) + "/{variable}_{time:03d}.nc" From 9348eee070d461acc517819f53900e2b84daab99 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 16:06:35 -0700 Subject: [PATCH 027/102] remove http from test_file_opener, to try new fixture --- tests/test_storage.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/test_storage.py b/tests/test_storage.py index 7ea40c09..0ef340b6 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -40,13 +40,7 @@ def test_metadata_target(tmp_metadata_target): @pytest.mark.parametrize( - "file_paths", - [ - lazy_fixture("netcdf_local_paths"), - lazy_fixture("netcdf_http_paths"), - lazy_fixture("netcdf_http_paths_with_secrets"), - lazy_fixture("netcdf_http_paths_with_multiparam_secrets"), - ], + "file_paths", [lazy_fixture("netcdf_paths")], ) @pytest.mark.parametrize("copy_to_local", [False, True]) @pytest.mark.parametrize("use_cache, cache_first", [(False, False), (True, False), (True, True)]) @@ -62,7 +56,7 @@ def test_file_opener( use_dask, use_xarray, ): - all_paths, _ = file_paths + all_paths = file_paths[0] if "?" not in str(all_paths[0]): path = str(all_paths[0]) From 7095f3414211ccec74c2c925060ab1ede0532e60 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 16:16:56 -0700 Subject: [PATCH 028/102] update netcdf_http_paths for new fixture --- tests/conftest.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5d35c8cd..46083a1f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -128,10 +128,12 @@ def netcdf_paths(daily_xarray_dataset, tmpdir_factory, items_per_file, file_spli # TODO: refactor to allow netcdf_local_paths_by_variable to be passed without -# duplicating the whole test. +# duplicating the whole test. --> COMPLETE? @pytest.fixture(scope="session") -def netcdf_http_paths(netcdf_local_paths, request): - paths, items_per_file = netcdf_local_paths +def netcdf_http_paths(netcdf_paths, request): + paths, items_per_file = netcdf_paths[:2] + if len(netcdf_paths) == 4: + fnames_by_variable, path_format = netcdf_paths[2:] username = "" password = "" @@ -161,7 +163,7 @@ def teardown(): request.addfinalizer(teardown) all_urls = ["/".join([url, str(fname)]) for fname in fnames] - return all_urls, items_per_file + return all_urls, items_per_file, fnames_by_variable, path_format @pytest.fixture(scope="session") From 31beca63c0493e681cfe45f5bb9847ab9fe28f2d Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 16:21:10 -0700 Subject: [PATCH 029/102] conditional return for netcdf_http_paths --- tests/conftest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 46083a1f..c12eda7d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -163,7 +163,11 @@ def teardown(): request.addfinalizer(teardown) all_urls = ["/".join([url, str(fname)]) for fname in fnames] - return all_urls, items_per_file, fnames_by_variable, path_format + + if len(netcdf_paths) == 2: + return all_urls, items_per_file + else: + return all_urls, items_per_file, fnames_by_variable, path_format @pytest.fixture(scope="session") From 48ef594f867c667ee7ef8301d234e67a3c3612c5 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 16:21:46 -0700 Subject: [PATCH 030/102] add http paths back into test_file_opener params --- tests/test_storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_storage.py b/tests/test_storage.py index 0ef340b6..a866d299 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -40,7 +40,7 @@ def test_metadata_target(tmp_metadata_target): @pytest.mark.parametrize( - "file_paths", [lazy_fixture("netcdf_paths")], + "file_paths", [lazy_fixture("netcdf_paths"), lazy_fixture("netcdf_http_paths")], ) @pytest.mark.parametrize("copy_to_local", [False, True]) @pytest.mark.parametrize("use_cache, cache_first", [(False, False), (True, False), (True, True)]) From f85c15a560ed0b4b50579088b338d165591665f0 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 16:42:51 -0700 Subject: [PATCH 031/102] refactor test_fixtures.py for new fixture --- tests/test_fixtures.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 396a1946..4d77c222 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -4,26 +4,24 @@ from pangeo_forge_recipes.utils import fix_scalar_attr_encoding -def test_fixture_local_files(daily_xarray_dataset, netcdf_local_paths): - paths, items_per_file = netcdf_local_paths +def test_fixture_local_files(daily_xarray_dataset, netcdf_paths): + paths, items_per_file = netcdf_paths[:2] + if len(netcdf_paths) == 4: + fnames_by_variable, path_format = netcdf_paths[2:] paths = [str(path) for path in paths] - ds = xr.open_mfdataset(paths, combine="nested", concat_dim="time", engine="h5netcdf") - assert ds.identical(daily_xarray_dataset) - - -# TODO: this is quite repetetive of the test above. Replace with parametrization. -def test_fixture_local_files_by_variable(daily_xarray_dataset, netcdf_local_paths_by_variable): - paths, items_per_file, fnames_by_variable, path_format = netcdf_local_paths_by_variable - paths = [str(path) for path in paths] - ds = xr.open_mfdataset(paths, combine="by_coords", concat_dim="time", engine="h5netcdf") + combine = "nested" if len(netcdf_paths) == 2 else "by_coords" + ds = xr.open_mfdataset(paths, combine=combine, concat_dim="time", engine="h5netcdf") assert ds.identical(daily_xarray_dataset) def test_fixture_http_files(daily_xarray_dataset, netcdf_http_paths): - urls, items_per_file = netcdf_http_paths + urls, items_per_file = netcdf_http_paths[:2] + if len(netcdf_http_paths) == 4: + fnames_by_variable, path_format = netcdf_http_paths[2:] open_files = [fsspec.open(url).open() for url in urls] + combine = "nested" if len(netcdf_http_paths) == 2 else "by_coords" ds = xr.open_mfdataset( - open_files, combine="nested", concat_dim="time", engine="h5netcdf" + open_files, combine=combine, concat_dim="time", engine="h5netcdf" ).load() - ds = fix_scalar_attr_encoding(ds) + ds = fix_scalar_attr_encoding(ds) # necessary? assert ds.identical(daily_xarray_dataset) From f3e2b49d25990e18b12ab0ad24f3a7b0179f2cab Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 16:45:43 -0700 Subject: [PATCH 032/102] remove TODO comment re: http fixture b/c complete --- tests/conftest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c12eda7d..afbce5d1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -127,8 +127,6 @@ def netcdf_paths(daily_xarray_dataset, tmpdir_factory, items_per_file, file_spli return full_paths, items_per_file, fnames_by_variable, path_format -# TODO: refactor to allow netcdf_local_paths_by_variable to be passed without -# duplicating the whole test. --> COMPLETE? @pytest.fixture(scope="session") def netcdf_http_paths(netcdf_paths, request): paths, items_per_file = netcdf_paths[:2] From 61e439677a72452d4fd9285adcbe9e2857966702 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 17:24:46 -0700 Subject: [PATCH 033/102] refactor sequential_recipe fixture --- tests/conftest.py | 51 ++++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index afbce5d1..199538bf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -207,10 +207,27 @@ def tmp_metadata_target(tmpdir_factory): @pytest.fixture def netCDFtoZarr_sequential_recipe( - daily_xarray_dataset, netcdf_local_paths, tmp_target, tmp_cache, tmp_metadata_target + daily_xarray_dataset, netcdf_paths, tmp_target, tmp_cache, tmp_metadata_target ): - paths, items_per_file = netcdf_local_paths - file_pattern = pattern_from_file_sequence([str(path) for path in paths], "time", items_per_file) + paths, items_per_file = netcdf_paths[:2] + + if len(netcdf_paths) == 2: + file_pattern = pattern_from_file_sequence( + [str(path) for path in paths], "time", items_per_file + ) + else: + _, path_format = netcdf_paths[2:] + time_index = list(range(len(paths) // 2)) + + def format_function(variable, time): + return path_format.format(variable=variable, time=time) + + file_pattern = FilePattern( + format_function, + ConcatDim("time", time_index, items_per_file), + MergeDim("variable", ["foo", "bar"]), + ) + kwargs = dict( inputs_per_chunk=1, target=tmp_target, @@ -222,9 +239,9 @@ def netCDFtoZarr_sequential_recipe( @pytest.fixture def netCDFtoZarr_sequential_subset_recipe( - daily_xarray_dataset, netcdf_local_paths, tmp_target, tmp_cache, tmp_metadata_target + daily_xarray_dataset, netcdf_paths, tmp_target, tmp_cache, tmp_metadata_target ): - paths, items_per_file = netcdf_local_paths + paths, items_per_file = netcdf_paths[:2] if items_per_file != 2: pytest.skip("This recipe only makes sense with items_per_file == 2.") file_pattern = pattern_from_file_sequence([str(path) for path in paths], "time", items_per_file) @@ -238,30 +255,6 @@ def netCDFtoZarr_sequential_subset_recipe( return recipes.XarrayZarrRecipe, file_pattern, kwargs, daily_xarray_dataset, tmp_target -@pytest.fixture -def netCDFtoZarr_sequential_multi_variable_recipe( - daily_xarray_dataset, netcdf_local_paths_by_variable, tmp_target, tmp_cache, tmp_metadata_target -): - paths, items_per_file, fnames_by_variable, path_format = netcdf_local_paths_by_variable - time_index = list(range(len(paths) // 2)) - - def format_function(variable, time): - return path_format.format(variable=variable, time=time) - - file_pattern = FilePattern( - format_function, - ConcatDim("time", time_index, items_per_file), - MergeDim("variable", ["foo", "bar"]), - ) - kwargs = dict( - inputs_per_chunk=1, - target=tmp_target, - input_cache=tmp_cache, - metadata_cache=tmp_metadata_target, - ) - return recipes.XarrayZarrRecipe, file_pattern, kwargs, daily_xarray_dataset, tmp_target - - @pytest.fixture(scope="session") def dask_cluster(request): cluster = LocalCluster(n_workers=2, threads_per_worker=1, silence_logs=False) From 7f4370a9e4fe622fcc02166c3a77f9b31f6607ff Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 17:26:03 -0700 Subject: [PATCH 034/102] remove now nonexistent multi_variable recipe from tests --- tests/test_recipes.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 4efaeecb..41734892 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -13,13 +13,11 @@ all_recipes = [ lazy_fixture("netCDFtoZarr_sequential_recipe"), - lazy_fixture("netCDFtoZarr_sequential_multi_variable_recipe"), lazy_fixture("netCDFtoZarr_sequential_subset_recipe"), ] recipes_no_subset = [ lazy_fixture("netCDFtoZarr_sequential_recipe"), - lazy_fixture("netCDFtoZarr_sequential_multi_variable_recipe"), ] From 92078e923f7da55bf613162912735226205897c5 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 4 Aug 2021 17:28:54 -0700 Subject: [PATCH 035/102] lint --- tests/conftest.py | 3 +-- tests/test_fixtures.py | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 199538bf..e47acbae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -100,8 +100,7 @@ def items_per_file(request): @pytest.fixture( - scope="session", - params=[_split_up_files_by_day, _split_up_files_by_variable_and_day] + scope="session", params=[_split_up_files_by_day, _split_up_files_by_variable_and_day] ) def file_splitter(request): return request.param diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 4d77c222..414b1113 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -20,8 +20,6 @@ def test_fixture_http_files(daily_xarray_dataset, netcdf_http_paths): fnames_by_variable, path_format = netcdf_http_paths[2:] open_files = [fsspec.open(url).open() for url in urls] combine = "nested" if len(netcdf_http_paths) == 2 else "by_coords" - ds = xr.open_mfdataset( - open_files, combine=combine, concat_dim="time", engine="h5netcdf" - ).load() + ds = xr.open_mfdataset(open_files, combine=combine, concat_dim="time", engine="h5netcdf").load() ds = fix_scalar_attr_encoding(ds) # necessary? assert ds.identical(daily_xarray_dataset) From 315c2921d978a4a93911a9fbcf9a046dfa870972 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 24 Aug 2021 16:26:35 -0700 Subject: [PATCH 036/102] conditional file_pattern in subset test --- tests/conftest.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index e47acbae..bd38797c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -243,7 +243,23 @@ def netCDFtoZarr_sequential_subset_recipe( paths, items_per_file = netcdf_paths[:2] if items_per_file != 2: pytest.skip("This recipe only makes sense with items_per_file == 2.") - file_pattern = pattern_from_file_sequence([str(path) for path in paths], "time", items_per_file) + if len(netcdf_paths) == 2: + file_pattern = pattern_from_file_sequence( + [str(path) for path in paths], "time", items_per_file + ) + else: + _, path_format = netcdf_paths[2:] + time_index = list(range(len(paths) // 2)) + + def format_function(variable, time): + return path_format.format(variable=variable, time=time) + + file_pattern = FilePattern( + format_function, + ConcatDim("time", time_index, items_per_file), + MergeDim("variable", ["foo", "bar"]), + ) + kwargs = dict( subset_inputs={"time": 2}, inputs_per_chunk=1, From 116cac7e93b33916a17607ba3abc7f770f91518c Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 24 Aug 2021 16:53:45 -0700 Subject: [PATCH 037/102] refactor conditional file_pattern logic into helper func --- tests/conftest.py | 60 ++++++++++++++++++++--------------------------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index bd38797c..f57b41e7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -94,6 +94,29 @@ def _split_up_files_by_variable_and_day(ds, day_param): return all_dsets, all_fnames, fnames_by_variable +def _make_file_pattern(netcdf_paths): + paths, items_per_file = netcdf_paths[:2] + + if len(netcdf_paths) == 2: + file_pattern = pattern_from_file_sequence( + [str(path) for path in paths], "time", items_per_file + ) + else: + _, path_format = netcdf_paths[2:] + time_index = list(range(len(paths) // 2)) + + def format_function(variable, time): + return path_format.format(variable=variable, time=time) + + file_pattern = FilePattern( + format_function, + ConcatDim("time", time_index, items_per_file), + MergeDim("variable", ["foo", "bar"]), + ) + + return file_pattern + + @pytest.fixture(scope="session", params=["D", "2D"]) def items_per_file(request): return request.param @@ -208,24 +231,7 @@ def tmp_metadata_target(tmpdir_factory): def netCDFtoZarr_sequential_recipe( daily_xarray_dataset, netcdf_paths, tmp_target, tmp_cache, tmp_metadata_target ): - paths, items_per_file = netcdf_paths[:2] - - if len(netcdf_paths) == 2: - file_pattern = pattern_from_file_sequence( - [str(path) for path in paths], "time", items_per_file - ) - else: - _, path_format = netcdf_paths[2:] - time_index = list(range(len(paths) // 2)) - - def format_function(variable, time): - return path_format.format(variable=variable, time=time) - - file_pattern = FilePattern( - format_function, - ConcatDim("time", time_index, items_per_file), - MergeDim("variable", ["foo", "bar"]), - ) + file_pattern = _make_file_pattern(netcdf_paths) kwargs = dict( inputs_per_chunk=1, @@ -240,25 +246,11 @@ def format_function(variable, time): def netCDFtoZarr_sequential_subset_recipe( daily_xarray_dataset, netcdf_paths, tmp_target, tmp_cache, tmp_metadata_target ): - paths, items_per_file = netcdf_paths[:2] + items_per_file = netcdf_paths[1] if items_per_file != 2: pytest.skip("This recipe only makes sense with items_per_file == 2.") - if len(netcdf_paths) == 2: - file_pattern = pattern_from_file_sequence( - [str(path) for path in paths], "time", items_per_file - ) - else: - _, path_format = netcdf_paths[2:] - time_index = list(range(len(paths) // 2)) - def format_function(variable, time): - return path_format.format(variable=variable, time=time) - - file_pattern = FilePattern( - format_function, - ConcatDim("time", time_index, items_per_file), - MergeDim("variable", ["foo", "bar"]), - ) + file_pattern = _make_file_pattern(netcdf_paths) kwargs = dict( subset_inputs={"time": 2}, From 1161c9e838964d7f05f4407864c5b217739a53f3 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 24 Aug 2021 18:12:48 -0700 Subject: [PATCH 038/102] remove 'sequential' from recipe fixture name --- tests/conftest.py | 4 ++-- tests/test_recipes.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f57b41e7..795e6d66 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -228,7 +228,7 @@ def tmp_metadata_target(tmpdir_factory): @pytest.fixture -def netCDFtoZarr_sequential_recipe( +def netCDFtoZarr_recipe( daily_xarray_dataset, netcdf_paths, tmp_target, tmp_cache, tmp_metadata_target ): file_pattern = _make_file_pattern(netcdf_paths) @@ -243,7 +243,7 @@ def netCDFtoZarr_sequential_recipe( @pytest.fixture -def netCDFtoZarr_sequential_subset_recipe( +def netCDFtoZarr_subset_recipe( daily_xarray_dataset, netcdf_paths, tmp_target, tmp_cache, tmp_metadata_target ): items_per_file = netcdf_paths[1] diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 41734892..05e492ce 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -12,17 +12,17 @@ from pangeo_forge_recipes.recipes.base import BaseRecipe all_recipes = [ - lazy_fixture("netCDFtoZarr_sequential_recipe"), - lazy_fixture("netCDFtoZarr_sequential_subset_recipe"), + lazy_fixture("netCDFtoZarr_recipe"), + lazy_fixture("netCDFtoZarr_subset_recipe"), ] recipes_no_subset = [ - lazy_fixture("netCDFtoZarr_sequential_recipe"), + lazy_fixture("netCDFtoZarr_recipe"), ] -def test_to_pipelines_warns(netCDFtoZarr_sequential_recipe): - RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_sequential_recipe +def test_to_pipelines_warns(netCDFtoZarr_recipe): + RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_recipe rec = RecipeClass(file_pattern, **kwargs) with pytest.warns(FutureWarning): rec.to_pipelines() @@ -63,11 +63,11 @@ def test_prune_recipe(recipe_fixture, execute_recipe, nkeep): @pytest.mark.parametrize("cache_inputs", [True, False]) @pytest.mark.parametrize("copy_input_to_local_file", [True, False]) def test_recipe_caching_copying( - netCDFtoZarr_sequential_recipe, execute_recipe, cache_inputs, copy_input_to_local_file + netCDFtoZarr_recipe, execute_recipe, cache_inputs, copy_input_to_local_file ): """The basic recipe test. Use this as a template for other tests.""" - RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_sequential_recipe + RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_recipe if not cache_inputs: kwargs.pop("input_cache") # make sure recipe doesn't require input_cache rec = RecipeClass( @@ -203,8 +203,8 @@ def test_chunks( xr.testing.assert_identical(ds_actual, ds_expected) -def test_lock_timeout(netCDFtoZarr_sequential_recipe, execute_recipe): - RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_sequential_recipe +def test_lock_timeout(netCDFtoZarr_recipe, execute_recipe): + RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_recipe recipe = RecipeClass(file_pattern=file_pattern, lock_timeout=1, **kwargs) with patch("pangeo_forge_recipes.recipes.xarray_zarr.lock_for_conflicts") as p: From 635d0b71fc4eeaf57901c607dd28de76bf4924e1 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 24 Aug 2021 18:39:42 -0700 Subject: [PATCH 039/102] remove test_recipes redundancy --- tests/test_recipes.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 05e492ce..b41ee22a 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -23,6 +23,11 @@ def test_to_pipelines_warns(netCDFtoZarr_recipe): RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_recipe + + # `netCDFtoZarr_recipe` fixture is parametrized. We don't need to run this test more than once. + if len(file_pattern.merge_dims) != 0: + pytest.skip("It's redundant to run this test more than once.") + rec = RecipeClass(file_pattern, **kwargs) with pytest.warns(FutureWarning): rec.to_pipelines() @@ -66,8 +71,12 @@ def test_recipe_caching_copying( netCDFtoZarr_recipe, execute_recipe, cache_inputs, copy_input_to_local_file ): """The basic recipe test. Use this as a template for other tests.""" - RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_recipe + + # `netCDFtoZarr_recipe` fixture is parametrized. We don't need to run this test more than once. + if len(file_pattern.merge_dims) != 0: + pytest.skip("It's redundant to run this test more than once.") + if not cache_inputs: kwargs.pop("input_cache") # make sure recipe doesn't require input_cache rec = RecipeClass( @@ -205,6 +214,11 @@ def test_chunks( def test_lock_timeout(netCDFtoZarr_recipe, execute_recipe): RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_recipe + + # `netCDFtoZarr_recipe` fixture is parametrized. We don't need to run this test more than once. + if len(file_pattern.merge_dims) != 0: + pytest.skip("It's redundant to run this test more than once.") + recipe = RecipeClass(file_pattern=file_pattern, lock_timeout=1, **kwargs) with patch("pangeo_forge_recipes.recipes.xarray_zarr.lock_for_conflicts") as p: From 829692b92b12941b6c4177d29a1c1e21bc9c2510 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 24 Aug 2021 21:53:38 -0700 Subject: [PATCH 040/102] paths/urls are all that's needed in text_fixtures --- tests/test_fixtures.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 414b1113..663b724a 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -5,9 +5,7 @@ def test_fixture_local_files(daily_xarray_dataset, netcdf_paths): - paths, items_per_file = netcdf_paths[:2] - if len(netcdf_paths) == 4: - fnames_by_variable, path_format = netcdf_paths[2:] + paths = netcdf_paths[0] paths = [str(path) for path in paths] combine = "nested" if len(netcdf_paths) == 2 else "by_coords" ds = xr.open_mfdataset(paths, combine=combine, concat_dim="time", engine="h5netcdf") @@ -15,9 +13,7 @@ def test_fixture_local_files(daily_xarray_dataset, netcdf_paths): def test_fixture_http_files(daily_xarray_dataset, netcdf_http_paths): - urls, items_per_file = netcdf_http_paths[:2] - if len(netcdf_http_paths) == 4: - fnames_by_variable, path_format = netcdf_http_paths[2:] + urls = netcdf_http_paths[0] open_files = [fsspec.open(url).open() for url in urls] combine = "nested" if len(netcdf_http_paths) == 2 else "by_coords" ds = xr.open_mfdataset(open_files, combine=combine, concat_dim="time", engine="h5netcdf").load() From 60713c5b6c2a8d15248778440316cb39ba29fb9d Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 24 Aug 2021 22:09:27 -0700 Subject: [PATCH 041/102] netcdf_paths should always return 4-tuple --- tests/conftest.py | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 795e6d66..bae347bb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -95,14 +95,13 @@ def _split_up_files_by_variable_and_day(ds, day_param): def _make_file_pattern(netcdf_paths): - paths, items_per_file = netcdf_paths[:2] + paths, items_per_file, fnames_by_variable, path_format = netcdf_paths - if len(netcdf_paths) == 2: + if not fnames_by_variable: file_pattern = pattern_from_file_sequence( [str(path) for path in paths], "time", items_per_file ) else: - _, path_format = netcdf_paths[2:] time_index = list(range(len(paths) // 2)) def format_function(variable, time): @@ -135,25 +134,19 @@ def netcdf_paths(daily_xarray_dataset, tmpdir_factory, items_per_file, file_spli file_splitter_tuple = file_splitter(daily_xarray_dataset.copy(), items_per_file) datasets, fnames = file_splitter_tuple[:2] - if len(file_splitter_tuple) == 3: - fnames_by_variable = file_splitter_tuple[2] - full_paths = [tmp_path.join(fname) for fname in fnames] xr.save_mfdataset(datasets, [str(path) for path in full_paths]) items_per_file = {"D": 1, "2D": 2}[items_per_file] - if len(file_splitter_tuple) == 2: - return full_paths, items_per_file - else: - path_format = str(tmp_path) + "/{variable}_{time:03d}.nc" - return full_paths, items_per_file, fnames_by_variable, path_format + fnames_by_variable = file_splitter_tuple[-1] if len(file_splitter_tuple) == 3 else None + path_format = str(tmp_path) + "/{variable}_{time:03d}.nc" if fnames_by_variable else None + + return full_paths, items_per_file, fnames_by_variable, path_format @pytest.fixture(scope="session") def netcdf_http_paths(netcdf_paths, request): - paths, items_per_file = netcdf_paths[:2] - if len(netcdf_paths) == 4: - fnames_by_variable, path_format = netcdf_paths[2:] + paths, items_per_file, fnames_by_variable, path_format = netcdf_paths username = "" password = "" @@ -184,10 +177,7 @@ def teardown(): all_urls = ["/".join([url, str(fname)]) for fname in fnames] - if len(netcdf_paths) == 2: - return all_urls, items_per_file - else: - return all_urls, items_per_file, fnames_by_variable, path_format + return all_urls, items_per_file, fnames_by_variable, path_format @pytest.fixture(scope="session") From 056b6a2fd868fd26b2a59d15f4d57e166e0e1da6 Mon Sep 17 00:00:00 2001 From: Ryan Abernathey Date: Wed, 25 Aug 2021 21:48:33 -0400 Subject: [PATCH 042/102] try to refactor tests a bit --- tests/conftest.py | 53 ++++++++----------------------------------- tests/test_recipes.py | 46 +++++++++++++++++++++++++++++-------- 2 files changed, 46 insertions(+), 53 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index bae347bb..411d0e4a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,6 @@ from dask.distributed import Client, LocalCluster from prefect.executors import DaskExecutor -from pangeo_forge_recipes import recipes from pangeo_forge_recipes.executors import ( DaskPipelineExecutor, PrefectPipelineExecutor, @@ -74,19 +73,19 @@ def daily_xarray_dataset(): return ds -def _split_up_files_by_day(ds, day_param): +def split_up_files_by_day(ds, day_param): gb = ds.resample(time=day_param) _, datasets = zip(*gb) fnames = [f"{n:03d}.nc" for n in range(len(datasets))] return datasets, fnames -def _split_up_files_by_variable_and_day(ds, day_param): +def split_up_files_by_variable_and_day(ds, day_param): all_dsets = [] all_fnames = [] fnames_by_variable = {} for varname in ds.data_vars: - var_dsets, fnames = _split_up_files_by_day(ds[[varname]], day_param) + var_dsets, fnames = split_up_files_by_day(ds[[varname]], day_param) fnames = [f"{varname}_{fname}" for fname in fnames] all_dsets += var_dsets all_fnames += fnames @@ -94,7 +93,7 @@ def _split_up_files_by_variable_and_day(ds, day_param): return all_dsets, all_fnames, fnames_by_variable -def _make_file_pattern(netcdf_paths): +def make_file_pattern(netcdf_paths): paths, items_per_file, fnames_by_variable, path_format = netcdf_paths if not fnames_by_variable: @@ -121,9 +120,7 @@ def items_per_file(request): return request.param -@pytest.fixture( - scope="session", params=[_split_up_files_by_day, _split_up_files_by_variable_and_day] -) +@pytest.fixture(scope="session", params=[split_up_files_by_day, split_up_files_by_variable_and_day]) def file_splitter(request): return request.param @@ -144,6 +141,11 @@ def netcdf_paths(daily_xarray_dataset, tmpdir_factory, items_per_file, file_spli return full_paths, items_per_file, fnames_by_variable, path_format +@pytest.fixture(scope="session") +def netcdf_local_file_pattern(netcdf_paths): + return make_file_pattern(netcdf_paths) + + @pytest.fixture(scope="session") def netcdf_http_paths(netcdf_paths, request): paths, items_per_file, fnames_by_variable, path_format = netcdf_paths @@ -217,41 +219,6 @@ def tmp_metadata_target(tmpdir_factory): return cache -@pytest.fixture -def netCDFtoZarr_recipe( - daily_xarray_dataset, netcdf_paths, tmp_target, tmp_cache, tmp_metadata_target -): - file_pattern = _make_file_pattern(netcdf_paths) - - kwargs = dict( - inputs_per_chunk=1, - target=tmp_target, - input_cache=tmp_cache, - metadata_cache=tmp_metadata_target, - ) - return recipes.XarrayZarrRecipe, file_pattern, kwargs, daily_xarray_dataset, tmp_target - - -@pytest.fixture -def netCDFtoZarr_subset_recipe( - daily_xarray_dataset, netcdf_paths, tmp_target, tmp_cache, tmp_metadata_target -): - items_per_file = netcdf_paths[1] - if items_per_file != 2: - pytest.skip("This recipe only makes sense with items_per_file == 2.") - - file_pattern = _make_file_pattern(netcdf_paths) - - kwargs = dict( - subset_inputs={"time": 2}, - inputs_per_chunk=1, - target=tmp_target, - input_cache=tmp_cache, - metadata_cache=tmp_metadata_target, - ) - return recipes.XarrayZarrRecipe, file_pattern, kwargs, daily_xarray_dataset, tmp_target - - @pytest.fixture(scope="session") def dask_cluster(request): cluster = LocalCluster(n_workers=2, threads_per_worker=1, silence_logs=False) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index b41ee22a..4eb268c3 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -10,6 +10,39 @@ from pangeo_forge_recipes.patterns import FilePattern from pangeo_forge_recipes.recipes.base import BaseRecipe +from pangeo_forge_recipes.recipes.xarray_zarr import XarrayZarrRecipe + + +@pytest.fixture +def netCDFtoZarr_recipe( + daily_xarray_dataset, netcdf_local_file_pattern, tmp_target, tmp_cache, tmp_metadata_target +): + kwargs = dict( + inputs_per_chunk=1, + target=tmp_target, + input_cache=tmp_cache, + metadata_cache=tmp_metadata_target, + ) + return XarrayZarrRecipe, netcdf_local_file_pattern, kwargs, daily_xarray_dataset, tmp_target + + +@pytest.fixture +def netCDFtoZarr_subset_recipe( + daily_xarray_dataset, netcdf_local_file_pattern, tmp_target, tmp_cache, tmp_metadata_target +): + items_per_file = netcdf_local_file_pattern.nitems_per_input.get("time", None) + if items_per_file != 2: + pytest.skip("This recipe only makes sense with items_per_file == 2.") + + kwargs = dict( + subset_inputs={"time": 2}, + inputs_per_chunk=1, + target=tmp_target, + input_cache=tmp_cache, + metadata_cache=tmp_metadata_target, + ) + return XarrayZarrRecipe, netcdf_local_file_pattern, kwargs, daily_xarray_dataset, tmp_target + all_recipes = [ lazy_fixture("netCDFtoZarr_recipe"), @@ -24,10 +57,6 @@ def test_to_pipelines_warns(netCDFtoZarr_recipe): RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_recipe - # `netCDFtoZarr_recipe` fixture is parametrized. We don't need to run this test more than once. - if len(file_pattern.merge_dims) != 0: - pytest.skip("It's redundant to run this test more than once.") - rec = RecipeClass(file_pattern, **kwargs) with pytest.warns(FutureWarning): rec.to_pipelines() @@ -53,7 +82,7 @@ def test_recipe(recipe_fixture, execute_recipe): @pytest.mark.parametrize("recipe_fixture", all_recipes) @pytest.mark.parametrize("nkeep", [1, 2]) def test_prune_recipe(recipe_fixture, execute_recipe, nkeep): - """The basic recipe test. Use this as a template for other tests.""" + """Check that recipe.copy_pruned works as expected.""" RecipeClass, file_pattern, kwargs, ds_expected, target = recipe_fixture rec = RecipeClass(file_pattern, **kwargs) @@ -70,12 +99,9 @@ def test_prune_recipe(recipe_fixture, execute_recipe, nkeep): def test_recipe_caching_copying( netCDFtoZarr_recipe, execute_recipe, cache_inputs, copy_input_to_local_file ): - """The basic recipe test. Use this as a template for other tests.""" - RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_recipe + """Test that caching and copying to local file work.""" - # `netCDFtoZarr_recipe` fixture is parametrized. We don't need to run this test more than once. - if len(file_pattern.merge_dims) != 0: - pytest.skip("It's redundant to run this test more than once.") + RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_recipe if not cache_inputs: kwargs.pop("input_cache") # make sure recipe doesn't require input_cache From 25eb0c6f90a5b670523362cc8635820002c9fb2c Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Thu, 26 Aug 2021 16:44:09 -0700 Subject: [PATCH 043/102] make start server func, add http basic auth test --- tests/conftest.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 411d0e4a..31aded22 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -146,17 +146,11 @@ def netcdf_local_file_pattern(netcdf_paths): return make_file_pattern(netcdf_paths) -@pytest.fixture(scope="session") -def netcdf_http_paths(netcdf_paths, request): - paths, items_per_file, fnames_by_variable, path_format = netcdf_paths - - username = "" - password = "" +def start_http_server(paths, request, username=None, password=None): first_path = paths[0] # assume that all files are in the same directory basedir = first_path.dirpath() - fnames = [path.basename for path in paths] this_dir = os.path.dirname(os.path.abspath(__file__)) port = get_open_port() @@ -177,6 +171,28 @@ def teardown(): request.addfinalizer(teardown) + return url + + +@pytest.fixture(scope="session") +def netcdf_http_paths(netcdf_paths, request): + paths, items_per_file, fnames_by_variable, path_format = netcdf_paths + + url = start_http_server(paths, request) + + fnames = [path.basename for path in paths] + all_urls = ["/".join([url, str(fname)]) for fname in fnames] + + return all_urls, items_per_file, fnames_by_variable, path_format + + +@pytest.fixture(scope="session") +def netcdf_http_paths_basic_auth(netcdf_paths, request): + paths, items_per_file, fnames_by_variable, path_format = netcdf_paths + + url = start_http_server(paths, request, username="foo", password="bar") + + fnames = [path.basename for path in paths] all_urls = ["/".join([url, str(fname)]) for fname in fnames] return all_urls, items_per_file, fnames_by_variable, path_format From 636f03fee7dc83b96497e4162a775a164c77a910 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Thu, 26 Aug 2021 16:45:03 -0700 Subject: [PATCH 044/102] add basic auth test to test_file_opener params --- tests/test_storage.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/test_storage.py b/tests/test_storage.py index a866d299..28b5b5ea 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -1,4 +1,6 @@ +import aiohttp import pytest +import requests import xarray as xr from dask import delayed from dask.distributed import Client @@ -40,7 +42,11 @@ def test_metadata_target(tmp_metadata_target): @pytest.mark.parametrize( - "file_paths", [lazy_fixture("netcdf_paths"), lazy_fixture("netcdf_http_paths")], + "file_paths", [ + lazy_fixture("netcdf_paths"), + lazy_fixture("netcdf_http_paths"), + lazy_fixture("netcdf_http_paths_basic_auth"), + ], ) @pytest.mark.parametrize("copy_to_local", [False, True]) @pytest.mark.parametrize("use_cache, cache_first", [(False, False), (True, False), (True, True)]) @@ -55,6 +61,7 @@ def test_file_opener( dask_cluster, use_dask, use_xarray, + open_kwargs={}, ): all_paths = file_paths[0] @@ -68,16 +75,21 @@ def test_file_opener( cache = tmp_cache if use_cache else None secrets = {"token": "bar"} if "?" in str(all_paths[0]) else None + if str(all_paths[0]).split(":")[0] == "http": + r = requests.get(all_paths[0]) + # only pass username and password to fsspec if the server requires it + open_kwargs = dict(auth=aiohttp.BasicAuth("foo", "bar")) if r.status_code == 401 else {} + def do_actual_test(): if cache_first: - cache.cache_file(path, secrets) + cache.cache_file(path, secrets, **open_kwargs) assert cache.exists(path) details = cache.fs.ls(cache.root_path, detail=True) - cache.cache_file(path, secrets) + cache.cache_file(path, secrets, **open_kwargs) # check that nothing happened assert cache.fs.ls(cache.root_path, detail=True) == details - opener = file_opener(path, cache, copy_to_local=copy_to_local) + opener = file_opener(path, cache, copy_to_local=copy_to_local, **open_kwargs) if use_cache and not cache_first: with pytest.raises(FileNotFoundError): with opener as fp: From 3999d5617548826bf4b7fb4c3cdf02dac200e86e Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Fri, 27 Aug 2021 08:19:13 -0700 Subject: [PATCH 045/102] add query string fixture --- tests/conftest.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 31aded22..b329d02a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -146,7 +146,7 @@ def netcdf_local_file_pattern(netcdf_paths): return make_file_pattern(netcdf_paths) -def start_http_server(paths, request, username=None, password=None): +def start_http_server(paths, request, username=None, password=None, required_query_string=None): first_path = paths[0] # assume that all files are in the same directory @@ -162,6 +162,8 @@ def start_http_server(paths, request, username=None, password=None): ] if username: command_list += [f"--username={username}", f"--password={password}"] + if required_query_string: + command_list += [f"--required-query-string={required_query_string}"] p = subprocess.Popen(command_list, cwd=basedir) url = f"http://127.0.0.1:{port}" time.sleep(2) # let the server start up @@ -199,17 +201,15 @@ def netcdf_http_paths_basic_auth(netcdf_paths, request): @pytest.fixture(scope="session") -def netcdf_http_paths_with_secrets(netcdf_http_paths): - all_urls, items_per_file = netcdf_http_paths - all_urls = [url + "?token=bar" for url in all_urls] - return all_urls, items_per_file +def netcdf_http_paths_query_string(netcdf_paths, request): + paths, items_per_file, fnames_by_variable, path_format = netcdf_paths + url = start_http_server(paths, request, required_query_string="foo=foo&bar=bar") -@pytest.fixture(scope="session") -def netcdf_http_paths_with_multiparam_secrets(netcdf_http_paths): - all_urls, items_per_file = netcdf_http_paths - all_urls = [url + "?filename=foo.nc&token=bar" for url in all_urls] - return all_urls, items_per_file + fnames = [path.basename for path in paths] + all_urls = ["/".join([url, str(fname)]) for fname in fnames] + + return all_urls, items_per_file, fnames_by_variable, path_format @pytest.fixture() From dddaf8f5393b75ede3e9cac694db459d6c92054a Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Fri, 27 Aug 2021 08:19:39 -0700 Subject: [PATCH 046/102] check for query string on server side --- tests/http_auth_server.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/http_auth_server.py b/tests/http_auth_server.py index ef28acab..c82fc650 100644 --- a/tests/http_auth_server.py +++ b/tests/http_auth_server.py @@ -1,6 +1,7 @@ import base64 import http.server import socketserver +from urllib.parse import urlparse import click @@ -10,7 +11,8 @@ @click.option("--port") @click.option("--username") @click.option("--password") -def serve_forever(address, port, username, password): +@click.option("--required-query-string") +def serve_forever(address, port, username, password, required_query_string): port = int(port) @@ -28,6 +30,12 @@ def do_GET(self): self.send_header("WWW-Authenticate", "Basic") self.end_headers() return + if required_query_string: + query = urlparse(self.path).query + if query != required_query_string: + self.send_response(400) + self.end_headers() + return return http.server.SimpleHTTPRequestHandler.do_GET(self) socketserver.TCPServer.allow_reuse_address = True From b3dd635da8e83e98cfbdfe61380f74f3efd1eae1 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Fri, 27 Aug 2021 08:20:04 -0700 Subject: [PATCH 047/102] add query string test --- tests/test_storage.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/tests/test_storage.py b/tests/test_storage.py index 28b5b5ea..f1442551 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -46,6 +46,7 @@ def test_metadata_target(tmp_metadata_target): lazy_fixture("netcdf_paths"), lazy_fixture("netcdf_http_paths"), lazy_fixture("netcdf_http_paths_basic_auth"), + lazy_fixture("netcdf_http_paths_query_string"), ], ) @pytest.mark.parametrize("copy_to_local", [False, True]) @@ -62,23 +63,16 @@ def test_file_opener( use_dask, use_xarray, open_kwargs={}, + secrets={}, ): all_paths = file_paths[0] - - if "?" not in str(all_paths[0]): - path = str(all_paths[0]) - elif "&" not in str(all_paths[0]): - path = str(all_paths[0]).split("?")[0] - else: - path = str(all_paths[0]).split("&")[0] - + path = str(all_paths[0]) cache = tmp_cache if use_cache else None - secrets = {"token": "bar"} if "?" in str(all_paths[0]) else None - if str(all_paths[0]).split(":")[0] == "http": - r = requests.get(all_paths[0]) - # only pass username and password to fsspec if the server requires it + if path.split(":")[0] == "http": + r = requests.get(path) open_kwargs = dict(auth=aiohttp.BasicAuth("foo", "bar")) if r.status_code == 401 else {} + secrets = {"foo": "foo", "bar": "bar"} if r.status_code == 400 else {} def do_actual_test(): if cache_first: From 2a6f3ebb958d7a1a14f06297cc962cc3f31d9a3f Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Fri, 27 Aug 2021 09:30:30 -0700 Subject: [PATCH 048/102] secrets was missing from file_opener in test --- tests/test_storage.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_storage.py b/tests/test_storage.py index f1442551..992fc7ca 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -83,7 +83,9 @@ def do_actual_test(): # check that nothing happened assert cache.fs.ls(cache.root_path, detail=True) == details - opener = file_opener(path, cache, copy_to_local=copy_to_local, **open_kwargs) + opener = file_opener( + path, cache, copy_to_local=copy_to_local, secrets=secrets, **open_kwargs + ) if use_cache and not cache_first: with pytest.raises(FileNotFoundError): with opener as fp: From 4732062177ba030e8da673e8e9ad905fa420d2c2 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Fri, 27 Aug 2021 09:45:19 -0700 Subject: [PATCH 049/102] refactor 3 http fixtures into single fixture --- tests/conftest.py | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b329d02a..e76c798e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -176,35 +176,17 @@ def teardown(): return url -@pytest.fixture(scope="session") +@pytest.fixture( + scope="session", + params=[ + {}, + dict(username="foo", password="bar"), + dict(required_query_string="foo=foo&bar=bar")], +) def netcdf_http_paths(netcdf_paths, request): paths, items_per_file, fnames_by_variable, path_format = netcdf_paths - url = start_http_server(paths, request) - - fnames = [path.basename for path in paths] - all_urls = ["/".join([url, str(fname)]) for fname in fnames] - - return all_urls, items_per_file, fnames_by_variable, path_format - - -@pytest.fixture(scope="session") -def netcdf_http_paths_basic_auth(netcdf_paths, request): - paths, items_per_file, fnames_by_variable, path_format = netcdf_paths - - url = start_http_server(paths, request, username="foo", password="bar") - - fnames = [path.basename for path in paths] - all_urls = ["/".join([url, str(fname)]) for fname in fnames] - - return all_urls, items_per_file, fnames_by_variable, path_format - - -@pytest.fixture(scope="session") -def netcdf_http_paths_query_string(netcdf_paths, request): - paths, items_per_file, fnames_by_variable, path_format = netcdf_paths - - url = start_http_server(paths, request, required_query_string="foo=foo&bar=bar") + url = start_http_server(paths, request, **request.param) fnames = [path.basename for path in paths] all_urls = ["/".join([url, str(fname)]) for fname in fnames] From b485ba8ec891addd8dc557b7eec0098523e477eb Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Fri, 27 Aug 2021 09:46:28 -0700 Subject: [PATCH 050/102] change test params to reflect fixture refactor --- tests/test_storage.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/test_storage.py b/tests/test_storage.py index 992fc7ca..b5aa8f7b 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -42,12 +42,7 @@ def test_metadata_target(tmp_metadata_target): @pytest.mark.parametrize( - "file_paths", [ - lazy_fixture("netcdf_paths"), - lazy_fixture("netcdf_http_paths"), - lazy_fixture("netcdf_http_paths_basic_auth"), - lazy_fixture("netcdf_http_paths_query_string"), - ], + "file_paths", [lazy_fixture("netcdf_paths"), lazy_fixture("netcdf_http_paths")], ) @pytest.mark.parametrize("copy_to_local", [False, True]) @pytest.mark.parametrize("use_cache, cache_first", [(False, False), (True, False), (True, True)]) From d2b2f4702094f58ac57db79b55dbc58fecd2f8dc Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Fri, 27 Aug 2021 09:50:26 -0700 Subject: [PATCH 051/102] add aiohttp as known_third_party (used in auth test) --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 86918c4a..bba7e9e4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,7 +56,7 @@ max-line-length = 100 [isort] known_first_party=pangeo_forge_recipes -known_third_party=click,dask,fsspec,numpy,pandas,prefect,pytest,pytest_lazyfixture,rechunker,setuptools,xarray,zarr +known_third_party=aiohttp,click,dask,fsspec,numpy,pandas,prefect,pytest,pytest_lazyfixture,rechunker,setuptools,xarray,zarr multi_line_output=3 include_trailing_comma=True force_grid_wrap=0 From 3646d517d7637a86dbb3b65ab8e2652d4a7eab55 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Fri, 27 Aug 2021 09:51:55 -0700 Subject: [PATCH 052/102] lint conftest.py --- tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index e76c798e..e5c94df9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -181,7 +181,8 @@ def teardown(): params=[ {}, dict(username="foo", password="bar"), - dict(required_query_string="foo=foo&bar=bar")], + dict(required_query_string="foo=foo&bar=bar"), + ], ) def netcdf_http_paths(netcdf_paths, request): paths, items_per_file, fnames_by_variable, path_format = netcdf_paths From c9164fc94ba6005359933cc1cc7ffd6a26194405 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Fri, 27 Aug 2021 09:53:54 -0700 Subject: [PATCH 053/102] add requests as known_third_party (used in auth test) --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index bba7e9e4..1701c28f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,7 +56,7 @@ max-line-length = 100 [isort] known_first_party=pangeo_forge_recipes -known_third_party=aiohttp,click,dask,fsspec,numpy,pandas,prefect,pytest,pytest_lazyfixture,rechunker,setuptools,xarray,zarr +known_third_party=aiohttp,click,dask,fsspec,numpy,pandas,prefect,pytest,pytest_lazyfixture,rechunker,requests,setuptools,xarray,zarr multi_line_output=3 include_trailing_comma=True force_grid_wrap=0 From 1236adab6a5def2ae1a283ed3729adca1d3c7c1e Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Fri, 27 Aug 2021 10:35:36 -0700 Subject: [PATCH 054/102] Skip auth and query string tests in test_fixtures.py --- tests/test_fixtures.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 663b724a..eb6951b3 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -1,4 +1,6 @@ import fsspec +import pytest +import requests import xarray as xr from pangeo_forge_recipes.utils import fix_scalar_attr_encoding @@ -14,6 +16,9 @@ def test_fixture_local_files(daily_xarray_dataset, netcdf_paths): def test_fixture_http_files(daily_xarray_dataset, netcdf_http_paths): urls = netcdf_http_paths[0] + r = requests.get(urls[0]) + if r.status_code == 400 or r.status_code == 401: + pytest.skip("Authentication and required query strings are tested in test_storage.py") open_files = [fsspec.open(url).open() for url in urls] combine = "nested" if len(netcdf_http_paths) == 2 else "by_coords" ds = xr.open_mfdataset(open_files, combine=combine, concat_dim="time", engine="h5netcdf").load() From 08e4ae1cb69916b132a5be11b9682f7861f6870c Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Sat, 28 Aug 2021 14:43:14 -0700 Subject: [PATCH 055/102] add netcdf_http_file_pattern fixture --- tests/conftest.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e5c94df9..f47ccd9f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -93,8 +93,15 @@ def split_up_files_by_variable_and_day(ds, day_param): return all_dsets, all_fnames, fnames_by_variable -def make_file_pattern(netcdf_paths): - paths, items_per_file, fnames_by_variable, path_format = netcdf_paths +def make_file_pattern(path_fixture): + """Creates a filepattern from a `path_fixture` + + Parameters + ---------- + path_fixture : callable + One of `netcdf_paths` or `netcdf_http_paths` + """ + paths, items_per_file, fnames_by_variable, path_format = path_fixture if not fnames_by_variable: file_pattern = pattern_from_file_sequence( @@ -146,6 +153,11 @@ def netcdf_local_file_pattern(netcdf_paths): return make_file_pattern(netcdf_paths) +@pytest.fixture(scope="session") +def netcdf_http_file_pattern(netcdf_http_paths): + return make_file_pattern(netcdf_http_paths) + + def start_http_server(paths, request, username=None, password=None, required_query_string=None): first_path = paths[0] From 79f7d2574dff20bf3a93bf642c737cffdfb33fc6 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Sat, 28 Aug 2021 14:45:27 -0700 Subject: [PATCH 056/102] add test_recipe_http_caching_copying test --- tests/test_recipes.py | 53 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index fb322277..842a0fe6 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -2,7 +2,9 @@ from dataclasses import replace from unittest.mock import patch +import aiohttp import pytest +import requests import xarray as xr # need to import this way (rather than use pytest.lazy_fixture) to make it work with dask @@ -26,6 +28,19 @@ def netCDFtoZarr_recipe( return XarrayZarrRecipe, netcdf_local_file_pattern, kwargs, daily_xarray_dataset, tmp_target +@pytest.fixture +def netCDFtoZarr_http_recipe( + daily_xarray_dataset, netcdf_http_file_pattern, tmp_target, tmp_cache, tmp_metadata_target +): + kwargs = dict( + inputs_per_chunk=1, + target=tmp_target, + input_cache=tmp_cache, + metadata_cache=tmp_metadata_target, + ) + return XarrayZarrRecipe, netcdf_http_file_pattern, kwargs, daily_xarray_dataset, tmp_target + + @pytest.fixture def netCDFtoZarr_subset_recipe( daily_xarray_dataset, netcdf_local_file_pattern, tmp_target, tmp_cache, tmp_metadata_target @@ -116,6 +131,44 @@ def test_recipe_caching_copying( xr.testing.assert_identical(ds_actual, ds_expected) +@pytest.mark.parametrize("cache_inputs", [True, False]) +@pytest.mark.parametrize("copy_input_to_local_file", [True, False]) +def test_recipe_http_caching_copying( + netCDFtoZarr_http_recipe, execute_recipe, cache_inputs, copy_input_to_local_file +): + """Test that caching and copying from http to local file work.""" + + RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_http_recipe + + if len(file_pattern.merge_dims) != 0: + pytest.skip( + "This test ensures auth kwargs are correctly passed to the recipe class." + "`netcdf_http_file_pattern` fixture currently does not return a properly" + " formatted `path_format` for recipes with a `MergeDim.`" + ) + + first_url = list(file_pattern.items())[0][1] + r = requests.get(first_url) + if r.status_code == 401: + fsspec_open_kwargs = dict(auth=aiohttp.BasicAuth("foo", "bar")) + kwargs.update({"fsspec_open_kwargs": fsspec_open_kwargs}) + elif r.status_code == 400: + query_string_secrets = {"foo": "foo", "bar": "bar"} + kwargs.update({"query_string_secrets": query_string_secrets}) + + if not cache_inputs: + kwargs.pop("input_cache") # make sure recipe doesn't require input_cache + rec = RecipeClass( + file_pattern, + **kwargs, + cache_inputs=cache_inputs, + copy_input_to_local_file=copy_input_to_local_file + ) + execute_recipe(rec) + ds_actual = xr.open_zarr(target.get_mapper()).load() + xr.testing.assert_identical(ds_actual, ds_expected) + + # function passed to preprocessing def incr_date(ds, filename=""): # add one day From 2e6422f5aa49aa5cd92c4c00c79a5696518f17fa Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Sat, 28 Aug 2021 14:46:41 -0700 Subject: [PATCH 057/102] add auth kwargs where missing in xarray_zarr.py --- pangeo_forge_recipes/recipes/xarray_zarr.py | 33 ++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/pangeo_forge_recipes/recipes/xarray_zarr.py b/pangeo_forge_recipes/recipes/xarray_zarr.py index 69870765..90b5f1d1 100644 --- a/pangeo_forge_recipes/recipes/xarray_zarr.py +++ b/pangeo_forge_recipes/recipes/xarray_zarr.py @@ -146,6 +146,8 @@ def cache_input_metadata( delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], is_opendap: bool, + fsspec_open_kwargs: dict, + query_string_secrets: dict, ) -> None: if metadata_cache is None: raise ValueError("metadata_cache is not set.") @@ -160,6 +162,8 @@ def cache_input_metadata( delete_input_encoding=delete_input_encoding, process_input=process_input, is_opendap=is_opendap, + fsspec_open_kwargs=fsspec_open_kwargs, + query_string_secrets=query_string_secrets, ) as ds: input_metadata = ds.to_dict(data=False) metadata_cache[_input_metadata_fname(input_key)] = input_metadata @@ -262,6 +266,8 @@ def open_input( delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], is_opendap: bool, + fsspec_open_kwargs: dict, + query_string_secrets: dict, ) -> xr.Dataset: fname = file_pattern[input_key] logger.info(f"Opening input with Xarray {input_key!s}: '{fname}'") @@ -275,7 +281,12 @@ def open_input( cache = input_cache if cache_inputs else None with file_opener( - fname, cache=cache, copy_to_local=copy_input_to_local_file, bypass_open=is_opendap + fname, + cache=cache, + copy_to_local=copy_input_to_local_file, + bypass_open=is_opendap, + secrets=query_string_secrets, + **fsspec_open_kwargs, ) as f: with dask.config.set(scheduler="single-threaded"): # make sure we don't use a scheduler kw = xarray_open_kwargs.copy() @@ -326,6 +337,8 @@ def open_chunk( delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], is_opendap: bool, + fsspec_open_kwargs: dict, + query_string_secrets: dict, ) -> xr.Dataset: logger.info(f"Opening inputs for chunk {chunk_key!s}") ninputs = file_pattern.dims[file_pattern.concat_dims[0]] @@ -345,6 +358,8 @@ def open_chunk( delete_input_encoding=delete_input_encoding, process_input=process_input, is_opendap=is_opendap, + fsspec_open_kwargs=fsspec_open_kwargs, + query_string_secrets=query_string_secrets, ) ) for i in inputs @@ -441,6 +456,8 @@ def prepare_target( process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], metadata_cache: Optional[MetadataTarget], is_opendap: bool, + fsspec_open_kwargs: Optional[dict], + query_string_secrets: Optional[dict], ) -> None: try: ds = open_target(target) @@ -471,6 +488,8 @@ def prepare_target( delete_input_encoding=delete_input_encoding, process_input=process_input, is_opendap=is_opendap, + fsspec_open_kwargs=fsspec_open_kwargs, + query_string_secrets=query_string_secrets, ) as ds: # ds is already chunked @@ -547,6 +566,8 @@ def store_chunk( process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], metadata_cache: Optional[MetadataTarget], is_opendap: bool, + fsspec_open_kwargs: Optional[dict], + query_string_secrets: Optional[dict], ) -> None: if target is None: raise ValueError("target has not been set.") @@ -566,6 +587,8 @@ def store_chunk( delete_input_encoding=delete_input_encoding, process_input=process_input, is_opendap=is_opendap, + fsspec_open_kwargs=fsspec_open_kwargs, + query_string_secrets=query_string_secrets, ) as ds_chunk: # writing a region means that all the variables MUST have concat_dim to_drop = [v for v in ds_chunk.variables if concat_dim not in ds_chunk[v].dims] @@ -833,6 +856,8 @@ def prepare_target(self) -> Callable[[], None]: process_input=self.process_input, metadata_cache=self.metadata_cache, is_opendap=self.is_opendap, + fsspec_open_kwargs=self.fsspec_open_kwargs, + query_string_secrets=self.query_string_secrets, ) @property @@ -876,6 +901,8 @@ def store_chunk(self) -> Callable[[Hashable], None]: process_input=self.process_input, metadata_cache=self.metadata_cache, is_opendap=self.is_opendap, + fsspec_open_kwargs=self.fsspec_open_kwargs, + query_string_secrets=self.query_string_secrets, ) @property @@ -939,6 +966,8 @@ def open_input(self, input_key): delete_input_encoding=self.delete_input_encoding, process_input=self.process_input, is_opendap=self.is_opendap, + fsspec_open_kwargs=self.fsspec_open_kwargs, + query_string_secrets=self.query_string_secrets, ) as ds: yield ds @@ -959,5 +988,7 @@ def open_chunk(self, chunk_key): delete_input_encoding=self.delete_input_encoding, process_input=self.process_input, is_opendap=self.is_opendap, + fsspec_open_kwargs=self.fsspec_open_kwargs, + query_string_secrets=self.query_string_secrets, ) as ds: yield ds From 81b7c2a33280330dfc37393499d011f9ed68eabd Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Sat, 28 Aug 2021 15:08:52 -0700 Subject: [PATCH 058/102] mypy lint --- pangeo_forge_recipes/recipes/xarray_zarr.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pangeo_forge_recipes/recipes/xarray_zarr.py b/pangeo_forge_recipes/recipes/xarray_zarr.py index 90b5f1d1..dd5d362e 100644 --- a/pangeo_forge_recipes/recipes/xarray_zarr.py +++ b/pangeo_forge_recipes/recipes/xarray_zarr.py @@ -181,7 +181,7 @@ def cache_input( delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], metadata_cache: Optional[MetadataTarget], - query_string_secrets: Optional[dict], + query_string_secrets: dict, is_opendap=bool, ) -> None: if cache_inputs: @@ -456,8 +456,8 @@ def prepare_target( process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], metadata_cache: Optional[MetadataTarget], is_opendap: bool, - fsspec_open_kwargs: Optional[dict], - query_string_secrets: Optional[dict], + fsspec_open_kwargs: dict, + query_string_secrets: dict, ) -> None: try: ds = open_target(target) @@ -566,8 +566,8 @@ def store_chunk( process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], metadata_cache: Optional[MetadataTarget], is_opendap: bool, - fsspec_open_kwargs: Optional[dict], - query_string_secrets: Optional[dict], + fsspec_open_kwargs: dict, + query_string_secrets: dict, ) -> None: if target is None: raise ValueError("target has not been set.") @@ -729,7 +729,7 @@ class XarrayZarrRecipe(BaseRecipe): lock_timeout: Optional[int] = None subset_inputs: SubsetSpec = field(default_factory=dict) is_opendap: bool = False - query_string_secrets: Optional[dict] = None + query_string_secrets: dict = field(default_factory=dict) # internal attributes not meant to be seen or accessed by user _concat_dim: str = field(default_factory=str, repr=False, init=False) From f647ba21c4ef0ecd0eaf69841370f1e84c31c993 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Sat, 28 Aug 2021 15:35:51 -0700 Subject: [PATCH 059/102] pass auth kwargs to cache_input_metadata --- pangeo_forge_recipes/recipes/xarray_zarr.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pangeo_forge_recipes/recipes/xarray_zarr.py b/pangeo_forge_recipes/recipes/xarray_zarr.py index dd5d362e..51942f74 100644 --- a/pangeo_forge_recipes/recipes/xarray_zarr.py +++ b/pangeo_forge_recipes/recipes/xarray_zarr.py @@ -205,6 +205,8 @@ def cache_input( process_input=process_input, metadata_cache=metadata_cache, is_opendap=is_opendap, + fsspec_open_kwargs=fsspec_open_kwargs, + query_string_secrets=query_string_secrets, ) From ba9f7904fd79fd74dcf8f0bf8e62c094efe3af3c Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Mon, 30 Aug 2021 09:50:25 -0700 Subject: [PATCH 060/102] fix path_format for http multivariate patterns --- tests/conftest.py | 3 ++- tests/test_recipes.py | 7 ------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f47ccd9f..0854bb20 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -197,9 +197,10 @@ def teardown(): ], ) def netcdf_http_paths(netcdf_paths, request): - paths, items_per_file, fnames_by_variable, path_format = netcdf_paths + paths, items_per_file, fnames_by_variable, _ = netcdf_paths url = start_http_server(paths, request, **request.param) + path_format = url + "/{variable}_{time:03d}.nc" if fnames_by_variable else None fnames = [path.basename for path in paths] all_urls = ["/".join([url, str(fname)]) for fname in fnames] diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 842a0fe6..2a2443a3 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -140,13 +140,6 @@ def test_recipe_http_caching_copying( RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_http_recipe - if len(file_pattern.merge_dims) != 0: - pytest.skip( - "This test ensures auth kwargs are correctly passed to the recipe class." - "`netcdf_http_file_pattern` fixture currently does not return a properly" - " formatted `path_format` for recipes with a `MergeDim.`" - ) - first_url = list(file_pattern.items())[0][1] r = requests.get(first_url) if r.status_code == 401: From 48a96ff708ed92b9c3b9bbdd7b621b1b86cb4937 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Mon, 30 Aug 2021 16:18:41 -0700 Subject: [PATCH 061/102] add attribures to FilePattern --- pangeo_forge_recipes/patterns.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pangeo_forge_recipes/patterns.py b/pangeo_forge_recipes/patterns.py index 4aab2164..30bd3b81 100644 --- a/pangeo_forge_recipes/patterns.py +++ b/pangeo_forge_recipes/patterns.py @@ -127,12 +127,24 @@ class FilePattern: combine_op and returns a string representing the filename / url paths. Each argument name should correspond to a ``name`` in the ``combine_dims`` list. + :param fsspec_open_kwargs: Extra options for opening the inputs with fsspec. + May include ``block_size``, ``username``, ``password``, etc. + :param query_string_secrets: If provided, these key/value pairs are appended to + the query string of each ``file_pattern`` url at runtime. :param combine_dims: A sequence of either concat or merge dimensions. The outer product of the keys is used to generate the full list of file paths. """ - def __init__(self, format_function: Callable, *combine_dims: CombineDim): + def __init__( + self, + format_function: Callable, + fsspec_open_kwargs: dict = {}, + query_string_secrets: dict = {}, + *combine_dims: CombineDim, + ): self.format_function = format_function + self.fsspec_open_kwargs = fsspec_open_kwargs + self.query_string_secrets = query_string_secrets self.combine_dims = combine_dims def __repr__(self): From a24c9bdca92da423d471f18719287e2042e0318a Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Mon, 30 Aug 2021 16:28:12 -0700 Subject: [PATCH 062/102] remove fsspec_open_kwargs as XarrayZarrRecipe kwarg --- pangeo_forge_recipes/recipes/xarray_zarr.py | 22 ++------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/pangeo_forge_recipes/recipes/xarray_zarr.py b/pangeo_forge_recipes/recipes/xarray_zarr.py index 51942f74..9e1cd06f 100644 --- a/pangeo_forge_recipes/recipes/xarray_zarr.py +++ b/pangeo_forge_recipes/recipes/xarray_zarr.py @@ -146,7 +146,6 @@ def cache_input_metadata( delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], is_opendap: bool, - fsspec_open_kwargs: dict, query_string_secrets: dict, ) -> None: if metadata_cache is None: @@ -162,7 +161,6 @@ def cache_input_metadata( delete_input_encoding=delete_input_encoding, process_input=process_input, is_opendap=is_opendap, - fsspec_open_kwargs=fsspec_open_kwargs, query_string_secrets=query_string_secrets, ) as ds: input_metadata = ds.to_dict(data=False) @@ -174,7 +172,6 @@ def cache_input( cache_inputs: bool, input_cache: Optional[CacheFSSpecTarget], file_pattern: FilePattern, - fsspec_open_kwargs: dict, cache_metadata: bool, copy_input_to_local_file: bool, xarray_open_kwargs: dict, @@ -191,7 +188,7 @@ def cache_input( raise ValueError("input_cache is not set.") logger.info(f"Caching input '{input_key!s}'") fname = file_pattern[input_key] - input_cache.cache_file(fname, query_string_secrets, **fsspec_open_kwargs) + input_cache.cache_file(fname, query_string_secrets, **file_pattern.fsspec_open_kwargs) if cache_metadata: return cache_input_metadata( @@ -205,7 +202,6 @@ def cache_input( process_input=process_input, metadata_cache=metadata_cache, is_opendap=is_opendap, - fsspec_open_kwargs=fsspec_open_kwargs, query_string_secrets=query_string_secrets, ) @@ -268,7 +264,6 @@ def open_input( delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], is_opendap: bool, - fsspec_open_kwargs: dict, query_string_secrets: dict, ) -> xr.Dataset: fname = file_pattern[input_key] @@ -288,7 +283,7 @@ def open_input( copy_to_local=copy_input_to_local_file, bypass_open=is_opendap, secrets=query_string_secrets, - **fsspec_open_kwargs, + **file_pattern.fsspec_open_kwargs, ) as f: with dask.config.set(scheduler="single-threaded"): # make sure we don't use a scheduler kw = xarray_open_kwargs.copy() @@ -339,7 +334,6 @@ def open_chunk( delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], is_opendap: bool, - fsspec_open_kwargs: dict, query_string_secrets: dict, ) -> xr.Dataset: logger.info(f"Opening inputs for chunk {chunk_key!s}") @@ -360,7 +354,6 @@ def open_chunk( delete_input_encoding=delete_input_encoding, process_input=process_input, is_opendap=is_opendap, - fsspec_open_kwargs=fsspec_open_kwargs, query_string_secrets=query_string_secrets, ) ) @@ -458,7 +451,6 @@ def prepare_target( process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], metadata_cache: Optional[MetadataTarget], is_opendap: bool, - fsspec_open_kwargs: dict, query_string_secrets: dict, ) -> None: try: @@ -490,7 +482,6 @@ def prepare_target( delete_input_encoding=delete_input_encoding, process_input=process_input, is_opendap=is_opendap, - fsspec_open_kwargs=fsspec_open_kwargs, query_string_secrets=query_string_secrets, ) as ds: # ds is already chunked @@ -568,7 +559,6 @@ def store_chunk( process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], metadata_cache: Optional[MetadataTarget], is_opendap: bool, - fsspec_open_kwargs: dict, query_string_secrets: dict, ) -> None: if target is None: @@ -589,7 +579,6 @@ def store_chunk( delete_input_encoding=delete_input_encoding, process_input=process_input, is_opendap=is_opendap, - fsspec_open_kwargs=fsspec_open_kwargs, query_string_secrets=query_string_secrets, ) as ds_chunk: # writing a region means that all the variables MUST have concat_dim @@ -697,7 +686,6 @@ class XarrayZarrRecipe(BaseRecipe): the inputs to form a chunk. :param delete_input_encoding: Whether to remove Xarray encoding from variables in the input dataset - :param fsspec_open_kwargs: Extra options for opening the inputs with fsspec. :param process_input: Function to call on each opened input, with signature `(ds: xr.Dataset, filename: str) -> ds: xr.Dataset`. :param process_chunk: Function to call on each concatenated chunk, with signature @@ -725,7 +713,6 @@ class XarrayZarrRecipe(BaseRecipe): xarray_open_kwargs: dict = field(default_factory=dict) xarray_concat_kwargs: dict = field(default_factory=dict) delete_input_encoding: bool = True - fsspec_open_kwargs: dict = field(default_factory=dict) process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]] = None process_chunk: Optional[Callable[[xr.Dataset], xr.Dataset]] = None lock_timeout: Optional[int] = None @@ -858,7 +845,6 @@ def prepare_target(self) -> Callable[[], None]: process_input=self.process_input, metadata_cache=self.metadata_cache, is_opendap=self.is_opendap, - fsspec_open_kwargs=self.fsspec_open_kwargs, query_string_secrets=self.query_string_secrets, ) @@ -869,7 +855,6 @@ def cache_input(self) -> Callable[[Hashable], None]: cache_inputs=self.cache_inputs, input_cache=self.input_cache, file_pattern=self.file_pattern, - fsspec_open_kwargs=self.fsspec_open_kwargs, cache_metadata=self._cache_metadata, copy_input_to_local_file=self.copy_input_to_local_file, xarray_open_kwargs=self.xarray_open_kwargs, @@ -903,7 +888,6 @@ def store_chunk(self) -> Callable[[Hashable], None]: process_input=self.process_input, metadata_cache=self.metadata_cache, is_opendap=self.is_opendap, - fsspec_open_kwargs=self.fsspec_open_kwargs, query_string_secrets=self.query_string_secrets, ) @@ -968,7 +952,6 @@ def open_input(self, input_key): delete_input_encoding=self.delete_input_encoding, process_input=self.process_input, is_opendap=self.is_opendap, - fsspec_open_kwargs=self.fsspec_open_kwargs, query_string_secrets=self.query_string_secrets, ) as ds: yield ds @@ -990,7 +973,6 @@ def open_chunk(self, chunk_key): delete_input_encoding=self.delete_input_encoding, process_input=self.process_input, is_opendap=self.is_opendap, - fsspec_open_kwargs=self.fsspec_open_kwargs, query_string_secrets=self.query_string_secrets, ) as ds: yield ds From 8501c2c69d00c35fa88a717631585bea031a87d3 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Mon, 30 Aug 2021 16:32:12 -0700 Subject: [PATCH 063/102] remove query_string_secrets as XarrayZarrRecipe kwarg --- pangeo_forge_recipes/recipes/xarray_zarr.py | 25 ++++----------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/pangeo_forge_recipes/recipes/xarray_zarr.py b/pangeo_forge_recipes/recipes/xarray_zarr.py index 9e1cd06f..8a5cf668 100644 --- a/pangeo_forge_recipes/recipes/xarray_zarr.py +++ b/pangeo_forge_recipes/recipes/xarray_zarr.py @@ -146,7 +146,6 @@ def cache_input_metadata( delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], is_opendap: bool, - query_string_secrets: dict, ) -> None: if metadata_cache is None: raise ValueError("metadata_cache is not set.") @@ -161,7 +160,6 @@ def cache_input_metadata( delete_input_encoding=delete_input_encoding, process_input=process_input, is_opendap=is_opendap, - query_string_secrets=query_string_secrets, ) as ds: input_metadata = ds.to_dict(data=False) metadata_cache[_input_metadata_fname(input_key)] = input_metadata @@ -178,7 +176,6 @@ def cache_input( delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], metadata_cache: Optional[MetadataTarget], - query_string_secrets: dict, is_opendap=bool, ) -> None: if cache_inputs: @@ -188,7 +185,9 @@ def cache_input( raise ValueError("input_cache is not set.") logger.info(f"Caching input '{input_key!s}'") fname = file_pattern[input_key] - input_cache.cache_file(fname, query_string_secrets, **file_pattern.fsspec_open_kwargs) + input_cache.cache_file( + fname, file_pattern.query_string_secrets, **file_pattern.fsspec_open_kwargs + ) if cache_metadata: return cache_input_metadata( @@ -202,7 +201,6 @@ def cache_input( process_input=process_input, metadata_cache=metadata_cache, is_opendap=is_opendap, - query_string_secrets=query_string_secrets, ) @@ -264,7 +262,6 @@ def open_input( delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], is_opendap: bool, - query_string_secrets: dict, ) -> xr.Dataset: fname = file_pattern[input_key] logger.info(f"Opening input with Xarray {input_key!s}: '{fname}'") @@ -282,7 +279,7 @@ def open_input( cache=cache, copy_to_local=copy_input_to_local_file, bypass_open=is_opendap, - secrets=query_string_secrets, + secrets=file_pattern.query_string_secrets, **file_pattern.fsspec_open_kwargs, ) as f: with dask.config.set(scheduler="single-threaded"): # make sure we don't use a scheduler @@ -334,7 +331,6 @@ def open_chunk( delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], is_opendap: bool, - query_string_secrets: dict, ) -> xr.Dataset: logger.info(f"Opening inputs for chunk {chunk_key!s}") ninputs = file_pattern.dims[file_pattern.concat_dims[0]] @@ -354,7 +350,6 @@ def open_chunk( delete_input_encoding=delete_input_encoding, process_input=process_input, is_opendap=is_opendap, - query_string_secrets=query_string_secrets, ) ) for i in inputs @@ -451,7 +446,6 @@ def prepare_target( process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], metadata_cache: Optional[MetadataTarget], is_opendap: bool, - query_string_secrets: dict, ) -> None: try: ds = open_target(target) @@ -482,7 +476,6 @@ def prepare_target( delete_input_encoding=delete_input_encoding, process_input=process_input, is_opendap=is_opendap, - query_string_secrets=query_string_secrets, ) as ds: # ds is already chunked @@ -559,7 +552,6 @@ def store_chunk( process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], metadata_cache: Optional[MetadataTarget], is_opendap: bool, - query_string_secrets: dict, ) -> None: if target is None: raise ValueError("target has not been set.") @@ -579,7 +571,6 @@ def store_chunk( delete_input_encoding=delete_input_encoding, process_input=process_input, is_opendap=is_opendap, - query_string_secrets=query_string_secrets, ) as ds_chunk: # writing a region means that all the variables MUST have concat_dim to_drop = [v for v in ds_chunk.variables if concat_dim not in ds_chunk[v].dims] @@ -697,8 +688,6 @@ class XarrayZarrRecipe(BaseRecipe): time dimension. Multiple dimensions are allowed. :param is_opednap: If True, assume all input fnames represent opendap endpoints. Cannot be used with caching. - :param query_string_secrets: If provided, these key/value pairs are appended to - the query string of each ``file_pattern`` url at runtime. """ file_pattern: FilePattern @@ -718,7 +707,6 @@ class XarrayZarrRecipe(BaseRecipe): lock_timeout: Optional[int] = None subset_inputs: SubsetSpec = field(default_factory=dict) is_opendap: bool = False - query_string_secrets: dict = field(default_factory=dict) # internal attributes not meant to be seen or accessed by user _concat_dim: str = field(default_factory=str, repr=False, init=False) @@ -845,7 +833,6 @@ def prepare_target(self) -> Callable[[], None]: process_input=self.process_input, metadata_cache=self.metadata_cache, is_opendap=self.is_opendap, - query_string_secrets=self.query_string_secrets, ) @property @@ -862,7 +849,6 @@ def cache_input(self) -> Callable[[Hashable], None]: process_input=self.process_input, metadata_cache=self.metadata_cache, is_opendap=self.is_opendap, - query_string_secrets=self.query_string_secrets, ) @property @@ -888,7 +874,6 @@ def store_chunk(self) -> Callable[[Hashable], None]: process_input=self.process_input, metadata_cache=self.metadata_cache, is_opendap=self.is_opendap, - query_string_secrets=self.query_string_secrets, ) @property @@ -952,7 +937,6 @@ def open_input(self, input_key): delete_input_encoding=self.delete_input_encoding, process_input=self.process_input, is_opendap=self.is_opendap, - query_string_secrets=self.query_string_secrets, ) as ds: yield ds @@ -973,6 +957,5 @@ def open_chunk(self, chunk_key): delete_input_encoding=self.delete_input_encoding, process_input=self.process_input, is_opendap=self.is_opendap, - query_string_secrets=self.query_string_secrets, ) as ds: yield ds From 39bc994aa45561346f2ac5e509b19690d0fb6bd1 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Mon, 30 Aug 2021 18:21:45 -0700 Subject: [PATCH 064/102] assign & pass auth kwargs from path fixtures --- tests/conftest.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0854bb20..bb889ea2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ import subprocess import time +import aiohttp import fsspec import numpy as np import pandas as pd @@ -145,7 +146,9 @@ def netcdf_paths(daily_xarray_dataset, tmpdir_factory, items_per_file, file_spli fnames_by_variable = file_splitter_tuple[-1] if len(file_splitter_tuple) == 3 else None path_format = str(tmp_path) + "/{variable}_{time:03d}.nc" if fnames_by_variable else None - return full_paths, items_per_file, fnames_by_variable, path_format + kwargs = dict(fsspec_open_kwargs={}, query_string_secrets={}) + + return full_paths, items_per_file, fnames_by_variable, path_format, kwargs @pytest.fixture(scope="session") @@ -197,7 +200,7 @@ def teardown(): ], ) def netcdf_http_paths(netcdf_paths, request): - paths, items_per_file, fnames_by_variable, _ = netcdf_paths + paths, items_per_file, fnames_by_variable, _, kwargs = netcdf_paths url = start_http_server(paths, request, **request.param) path_format = url + "/{variable}_{time:03d}.nc" if fnames_by_variable else None @@ -205,7 +208,12 @@ def netcdf_http_paths(netcdf_paths, request): fnames = [path.basename for path in paths] all_urls = ["/".join([url, str(fname)]) for fname in fnames] - return all_urls, items_per_file, fnames_by_variable, path_format + if "username" in request.param.keys(): + kwargs.update(dict(fsspec_open_kwargs={"auth": aiohttp.BasicAuth("foo", "bar")})) + if "required_query_string" in request.param.keys(): + kwargs.update(dict(query_string_secrets={"foo": "foo", "bar": "bar"})) + + return all_urls, items_per_file, fnames_by_variable, path_format, kwargs @pytest.fixture() From 710011e6855934105e0f3ae24ab63b0cf33144cd Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Mon, 30 Aug 2021 18:23:21 -0700 Subject: [PATCH 065/102] assign auth kwargs from path fixtures in test_storage.py --- tests/test_storage.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/test_storage.py b/tests/test_storage.py index b5aa8f7b..0bdcb353 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -1,6 +1,4 @@ -import aiohttp import pytest -import requests import xarray as xr from dask import delayed from dask.distributed import Client @@ -57,18 +55,13 @@ def test_file_opener( dask_cluster, use_dask, use_xarray, - open_kwargs={}, - secrets={}, ): all_paths = file_paths[0] path = str(all_paths[0]) + open_kwargs = file_paths[-1]["fsspec_open_kwargs"] + secrets = file_paths[-1]["query_string_secrets"] cache = tmp_cache if use_cache else None - if path.split(":")[0] == "http": - r = requests.get(path) - open_kwargs = dict(auth=aiohttp.BasicAuth("foo", "bar")) if r.status_code == 401 else {} - secrets = {"foo": "foo", "bar": "bar"} if r.status_code == 400 else {} - def do_actual_test(): if cache_first: cache.cache_file(path, secrets, **open_kwargs) From 7f01b40cd5479be17a4f1a775041bdee0e419776 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Mon, 30 Aug 2021 18:30:28 -0700 Subject: [PATCH 066/102] combine always == 'by_coords' --- tests/test_fixtures.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index eb6951b3..50c9fb6d 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -9,8 +9,7 @@ def test_fixture_local_files(daily_xarray_dataset, netcdf_paths): paths = netcdf_paths[0] paths = [str(path) for path in paths] - combine = "nested" if len(netcdf_paths) == 2 else "by_coords" - ds = xr.open_mfdataset(paths, combine=combine, concat_dim="time", engine="h5netcdf") + ds = xr.open_mfdataset(paths, combine="by_coords", concat_dim="time", engine="h5netcdf") assert ds.identical(daily_xarray_dataset) @@ -20,7 +19,6 @@ def test_fixture_http_files(daily_xarray_dataset, netcdf_http_paths): if r.status_code == 400 or r.status_code == 401: pytest.skip("Authentication and required query strings are tested in test_storage.py") open_files = [fsspec.open(url).open() for url in urls] - combine = "nested" if len(netcdf_http_paths) == 2 else "by_coords" - ds = xr.open_mfdataset(open_files, combine=combine, concat_dim="time", engine="h5netcdf").load() + ds = xr.open_mfdataset(open_files, combine="by_coords", concat_dim="time", engine="h5netcdf").load() ds = fix_scalar_attr_encoding(ds) # necessary? assert ds.identical(daily_xarray_dataset) From e8999f85f233ac8f05d3cd1e89ca2db7ac869db3 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Mon, 30 Aug 2021 18:40:21 -0700 Subject: [PATCH 067/102] use presence of auth kwargs as skip trigger in fixture test --- tests/test_fixtures.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 50c9fb6d..1f8902cd 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -1,6 +1,5 @@ import fsspec import pytest -import requests import xarray as xr from pangeo_forge_recipes.utils import fix_scalar_attr_encoding @@ -15,8 +14,10 @@ def test_fixture_local_files(daily_xarray_dataset, netcdf_paths): def test_fixture_http_files(daily_xarray_dataset, netcdf_http_paths): urls = netcdf_http_paths[0] - r = requests.get(urls[0]) - if r.status_code == 400 or r.status_code == 401: + if ( + "auth" in netcdf_http_paths[-1]["fsspec_open_kwargs"].keys() + or netcdf_http_paths[-1]["query_string_secrets"].keys() + ): pytest.skip("Authentication and required query strings are tested in test_storage.py") open_files = [fsspec.open(url).open() for url in urls] ds = xr.open_mfdataset(open_files, combine="by_coords", concat_dim="time", engine="h5netcdf").load() From 0cc0b0dbe7283a16fd1a603cc1aaad40faface61 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Mon, 30 Aug 2021 18:40:55 -0700 Subject: [PATCH 068/102] lint line length in fixture test --- tests/test_fixtures.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 1f8902cd..5334f8d8 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -20,6 +20,8 @@ def test_fixture_http_files(daily_xarray_dataset, netcdf_http_paths): ): pytest.skip("Authentication and required query strings are tested in test_storage.py") open_files = [fsspec.open(url).open() for url in urls] - ds = xr.open_mfdataset(open_files, combine="by_coords", concat_dim="time", engine="h5netcdf").load() + ds = xr.open_mfdataset( + open_files, combine="by_coords", concat_dim="time", engine="h5netcdf" + ).load() ds = fix_scalar_attr_encoding(ds) # necessary? assert ds.identical(daily_xarray_dataset) From 0044348f08504f6b73440480d6577059a4639a2d Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 08:13:15 -0700 Subject: [PATCH 069/102] move fsspec_open_kwargs and query_string_secrets to FilePattern **kwargs --- pangeo_forge_recipes/patterns.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pangeo_forge_recipes/patterns.py b/pangeo_forge_recipes/patterns.py index 30bd3b81..97fa337e 100644 --- a/pangeo_forge_recipes/patterns.py +++ b/pangeo_forge_recipes/patterns.py @@ -138,14 +138,13 @@ class FilePattern: def __init__( self, format_function: Callable, - fsspec_open_kwargs: dict = {}, - query_string_secrets: dict = {}, *combine_dims: CombineDim, + **kwargs, ): self.format_function = format_function - self.fsspec_open_kwargs = fsspec_open_kwargs - self.query_string_secrets = query_string_secrets self.combine_dims = combine_dims + self.fsspec_open_kwargs = kwargs.pop("fsspec_open_kwargs", {}) + self.query_string_secrets = kwargs.pop("query_string_secrets", {}) def __repr__(self): return f"" @@ -226,7 +225,7 @@ def items(self): yield key, self[key] -def pattern_from_file_sequence(file_list, concat_dim, nitems_per_file=None): +def pattern_from_file_sequence(file_list, concat_dim, nitems_per_file=None, **kwargs): """Convenience function for creating a FilePattern from a list of files.""" keys = list(range(len(file_list))) @@ -235,7 +234,7 @@ def pattern_from_file_sequence(file_list, concat_dim, nitems_per_file=None): def format_function(**kwargs): return file_list[kwargs[concat_dim]] - return FilePattern(format_function, concat) + return FilePattern(format_function, concat, **kwargs) def prune_pattern(fp: FilePattern, nkeep: int = 2) -> FilePattern: From 404b0ac04794b090bcae4b417863657a58f09bde Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 08:14:10 -0700 Subject: [PATCH 070/102] edit make_file_pattern to reflect new FilePattern **kwargs --- tests/conftest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index bb889ea2..dc856d15 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -102,11 +102,11 @@ def make_file_pattern(path_fixture): path_fixture : callable One of `netcdf_paths` or `netcdf_http_paths` """ - paths, items_per_file, fnames_by_variable, path_format = path_fixture + paths, items_per_file, fnames_by_variable, path_format, kwargs = path_fixture if not fnames_by_variable: file_pattern = pattern_from_file_sequence( - [str(path) for path in paths], "time", items_per_file + [str(path) for path in paths], "time", items_per_file, **kwargs ) else: time_index = list(range(len(paths) // 2)) @@ -118,6 +118,7 @@ def format_function(variable, time): format_function, ConcatDim("time", time_index, items_per_file), MergeDim("variable", ["foo", "bar"]), + **kwargs ) return file_pattern From a1c43f365edf8b66b39254398fdb28c9652a6500 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 08:24:30 -0700 Subject: [PATCH 071/102] remove roundabout auth kwargs checking in test_recipes.py --- tests/test_recipes.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 2a2443a3..35760590 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -2,9 +2,7 @@ from dataclasses import replace from unittest.mock import patch -import aiohttp import pytest -import requests import xarray as xr # need to import this way (rather than use pytest.lazy_fixture) to make it work with dask @@ -140,15 +138,6 @@ def test_recipe_http_caching_copying( RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_http_recipe - first_url = list(file_pattern.items())[0][1] - r = requests.get(first_url) - if r.status_code == 401: - fsspec_open_kwargs = dict(auth=aiohttp.BasicAuth("foo", "bar")) - kwargs.update({"fsspec_open_kwargs": fsspec_open_kwargs}) - elif r.status_code == 400: - query_string_secrets = {"foo": "foo", "bar": "bar"} - kwargs.update({"query_string_secrets": query_string_secrets}) - if not cache_inputs: kwargs.pop("input_cache") # make sure recipe doesn't require input_cache rec = RecipeClass( From 03de7fd40793626022a4f3f3002572c84b69c8c0 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 10:39:37 -0700 Subject: [PATCH 072/102] make is_opendap an attribute of FilePattern --- pangeo_forge_recipes/patterns.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pangeo_forge_recipes/patterns.py b/pangeo_forge_recipes/patterns.py index 97fa337e..dd055ab7 100644 --- a/pangeo_forge_recipes/patterns.py +++ b/pangeo_forge_recipes/patterns.py @@ -127,12 +127,14 @@ class FilePattern: combine_op and returns a string representing the filename / url paths. Each argument name should correspond to a ``name`` in the ``combine_dims`` list. + :param combine_dims: A sequence of either concat or merge dimensions. The outer + product of the keys is used to generate the full list of file paths. :param fsspec_open_kwargs: Extra options for opening the inputs with fsspec. May include ``block_size``, ``username``, ``password``, etc. :param query_string_secrets: If provided, these key/value pairs are appended to the query string of each ``file_pattern`` url at runtime. - :param combine_dims: A sequence of either concat or merge dimensions. The outer - product of the keys is used to generate the full list of file paths. + :param is_opendap: If True, assume all input fnames represent opendap endpoints. + Cannot be used with caching. """ def __init__( @@ -145,6 +147,9 @@ def __init__( self.combine_dims = combine_dims self.fsspec_open_kwargs = kwargs.pop("fsspec_open_kwargs", {}) self.query_string_secrets = kwargs.pop("query_string_secrets", {}) + self.is_opendap = kwargs.pop("is_opendap", False) + if kwargs.keys(): + raise ValueError(f"{kwargs.keys()[0]} is not a valid keyword argument.") def __repr__(self): return f"" From de54dde4518a1688ee44581262a64dd75dde82ec Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 10:41:50 -0700 Subject: [PATCH 073/102] update XarrayZarrRecipe to reflect is_opendap as FilePattern attribute --- pangeo_forge_recipes/recipes/xarray_zarr.py | 27 +++------------------ 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/pangeo_forge_recipes/recipes/xarray_zarr.py b/pangeo_forge_recipes/recipes/xarray_zarr.py index 8a5cf668..951311be 100644 --- a/pangeo_forge_recipes/recipes/xarray_zarr.py +++ b/pangeo_forge_recipes/recipes/xarray_zarr.py @@ -145,7 +145,6 @@ def cache_input_metadata( xarray_open_kwargs: dict, delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], - is_opendap: bool, ) -> None: if metadata_cache is None: raise ValueError("metadata_cache is not set.") @@ -159,7 +158,6 @@ def cache_input_metadata( xarray_open_kwargs=xarray_open_kwargs, delete_input_encoding=delete_input_encoding, process_input=process_input, - is_opendap=is_opendap, ) as ds: input_metadata = ds.to_dict(data=False) metadata_cache[_input_metadata_fname(input_key)] = input_metadata @@ -176,10 +174,9 @@ def cache_input( delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], metadata_cache: Optional[MetadataTarget], - is_opendap=bool, ) -> None: if cache_inputs: - if is_opendap: + if file_pattern.is_opendap: raise ValueError("Can't cache opendap inputs") if input_cache is None: raise ValueError("input_cache is not set.") @@ -200,7 +197,6 @@ def cache_input( delete_input_encoding=delete_input_encoding, process_input=process_input, metadata_cache=metadata_cache, - is_opendap=is_opendap, ) @@ -261,12 +257,11 @@ def open_input( xarray_open_kwargs: dict, delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], - is_opendap: bool, ) -> xr.Dataset: fname = file_pattern[input_key] logger.info(f"Opening input with Xarray {input_key!s}: '{fname}'") - if is_opendap: + if file_pattern.is_opendap: if input_cache: raise ValueError("Can't cache opendap inputs") if copy_input_to_local_file: @@ -278,7 +273,7 @@ def open_input( fname, cache=cache, copy_to_local=copy_input_to_local_file, - bypass_open=is_opendap, + bypass_open=file_pattern.is_opendap, secrets=file_pattern.query_string_secrets, **file_pattern.fsspec_open_kwargs, ) as f: @@ -330,7 +325,6 @@ def open_chunk( xarray_open_kwargs: dict, delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], - is_opendap: bool, ) -> xr.Dataset: logger.info(f"Opening inputs for chunk {chunk_key!s}") ninputs = file_pattern.dims[file_pattern.concat_dims[0]] @@ -349,7 +343,6 @@ def open_chunk( xarray_open_kwargs=xarray_open_kwargs, delete_input_encoding=delete_input_encoding, process_input=process_input, - is_opendap=is_opendap, ) ) for i in inputs @@ -445,7 +438,6 @@ def prepare_target( delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], metadata_cache: Optional[MetadataTarget], - is_opendap: bool, ) -> None: try: ds = open_target(target) @@ -475,7 +467,6 @@ def prepare_target( xarray_open_kwargs=xarray_open_kwargs, delete_input_encoding=delete_input_encoding, process_input=process_input, - is_opendap=is_opendap, ) as ds: # ds is already chunked @@ -551,7 +542,6 @@ def store_chunk( delete_input_encoding: bool, process_input: Optional[Callable[[xr.Dataset, str], xr.Dataset]], metadata_cache: Optional[MetadataTarget], - is_opendap: bool, ) -> None: if target is None: raise ValueError("target has not been set.") @@ -570,7 +560,6 @@ def store_chunk( xarray_open_kwargs=xarray_open_kwargs, delete_input_encoding=delete_input_encoding, process_input=process_input, - is_opendap=is_opendap, ) as ds_chunk: # writing a region means that all the variables MUST have concat_dim to_drop = [v for v in ds_chunk.variables if concat_dim not in ds_chunk[v].dims] @@ -686,8 +675,6 @@ class XarrayZarrRecipe(BaseRecipe): along dimension according to the specified mapping. For example, ``{'time': 5}`` would split each input file into 5 chunks along the time dimension. Multiple dimensions are allowed. - :param is_opednap: If True, assume all input fnames represent opendap endpoints. - Cannot be used with caching. """ file_pattern: FilePattern @@ -706,7 +693,6 @@ class XarrayZarrRecipe(BaseRecipe): process_chunk: Optional[Callable[[xr.Dataset], xr.Dataset]] = None lock_timeout: Optional[int] = None subset_inputs: SubsetSpec = field(default_factory=dict) - is_opendap: bool = False # internal attributes not meant to be seen or accessed by user _concat_dim: str = field(default_factory=str, repr=False, init=False) @@ -727,7 +713,7 @@ def __post_init__(self): ) self._nitems_per_input = self.file_pattern.nitems_per_input[self._concat_dim] - if self.is_opendap: + if self.file_pattern.is_opendap: if self.cache_inputs: raise ValueError("Can't cache opendap inputs.") else: @@ -832,7 +818,6 @@ def prepare_target(self) -> Callable[[], None]: delete_input_encoding=self.delete_input_encoding, process_input=self.process_input, metadata_cache=self.metadata_cache, - is_opendap=self.is_opendap, ) @property @@ -848,7 +833,6 @@ def cache_input(self) -> Callable[[Hashable], None]: delete_input_encoding=self.delete_input_encoding, process_input=self.process_input, metadata_cache=self.metadata_cache, - is_opendap=self.is_opendap, ) @property @@ -873,7 +857,6 @@ def store_chunk(self) -> Callable[[Hashable], None]: delete_input_encoding=self.delete_input_encoding, process_input=self.process_input, metadata_cache=self.metadata_cache, - is_opendap=self.is_opendap, ) @property @@ -936,7 +919,6 @@ def open_input(self, input_key): xarray_open_kwargs=self.xarray_open_kwargs, delete_input_encoding=self.delete_input_encoding, process_input=self.process_input, - is_opendap=self.is_opendap, ) as ds: yield ds @@ -956,6 +938,5 @@ def open_chunk(self, chunk_key): xarray_open_kwargs=self.xarray_open_kwargs, delete_input_encoding=self.delete_input_encoding, process_input=self.process_input, - is_opendap=self.is_opendap, ) as ds: yield ds From 608cdead9221b956ec836382be2455d696d3578c Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 12:09:18 -0700 Subject: [PATCH 074/102] refactor test_patterns with fixtures --- tests/test_patterns.py | 50 +++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/tests/test_patterns.py b/tests/test_patterns.py index f7318a60..53dcc487 100644 --- a/tests/test_patterns.py +++ b/tests/test_patterns.py @@ -10,13 +10,37 @@ ) -def test_file_pattern_concat(): +@pytest.fixture +def concat_pattern(): concat = ConcatDim(name="time", keys=list(range(3))) def format_function(time): return f"T_{time}" - fp = FilePattern(format_function, concat) + return FilePattern(format_function, concat) + + +def make_concat_merge_pattern(**kwargs): + times = list(range(3)) + varnames = ["foo", "bar"] + concat = ConcatDim(name="time", keys=times) + merge = MergeDim(name="variable", keys=varnames) + + def format_function(time, variable): + return f"T_{time}_V_{variable}" + + fp = FilePattern(format_function, merge, concat, **kwargs) + + return fp, times, varnames, format_function, kwargs + + +@pytest.fixture +def concat_merge_pattern(): + return make_concat_merge_pattern() + + +def test_file_pattern_concat(concat_pattern): + fp = concat_pattern assert fp.dims == {"time": 3} assert fp.shape == (3,) assert fp.merge_dims == [] @@ -42,16 +66,8 @@ def test_pattern_from_file_sequence(): @pytest.mark.parametrize("pickle", [False, True]) -def test_file_pattern_concat_merge(pickle): - times = list(range(3)) - varnames = ["foo", "bar"] - concat = ConcatDim(name="time", keys=times) - merge = MergeDim(name="variable", keys=varnames) - - def format_function(time, variable): - return f"T_{time}_V_{variable}" - - fp = FilePattern(format_function, merge, concat) +def test_file_pattern_concat_merge(pickle, concat_merge_pattern): + fp, times, varnames, format_function, _ = concat_merge_pattern if pickle: # regular pickle doesn't work here because it can't pickle format_function @@ -81,14 +97,8 @@ def format_function(time, variable): @pytest.mark.parametrize("nkeep", [1, 2]) -def test_prune(nkeep): - concat = ConcatDim(name="time", keys=list(range(3))) - merge = MergeDim(name="variable", keys=["foo", "bar"]) - - def format_function(time, variable): - return f"T_{time}_V_{variable}" - - fp = FilePattern(format_function, merge, concat) +def test_prune(nkeep, concat_merge_pattern): + fp = concat_merge_pattern[0] fp_pruned = prune_pattern(fp, nkeep=nkeep) assert fp_pruned.dims == {"variable": 2, "time": nkeep} assert len(list(fp_pruned.items())) == 2 * nkeep From 30097310c88dd3c328aad2ccc731f46a8e5265a7 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 12:45:26 -0700 Subject: [PATCH 075/102] is_opendap and fsspec_open_kwargs are mutually exclusive --- pangeo_forge_recipes/patterns.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pangeo_forge_recipes/patterns.py b/pangeo_forge_recipes/patterns.py index dd055ab7..90bfc7a8 100644 --- a/pangeo_forge_recipes/patterns.py +++ b/pangeo_forge_recipes/patterns.py @@ -148,6 +148,11 @@ def __init__( self.fsspec_open_kwargs = kwargs.pop("fsspec_open_kwargs", {}) self.query_string_secrets = kwargs.pop("query_string_secrets", {}) self.is_opendap = kwargs.pop("is_opendap", False) + if self.fsspec_open_kwargs and self.is_opendap: + raise ValueError( + "OPeNDAP inputs are not opened with `fsspec`. " + "`is_opendap` must be `False` when passing `fsspec_open_kwargs`." + ) if kwargs.keys(): raise ValueError(f"{kwargs.keys()[0]} is not a valid keyword argument.") From cc0f9b5ab018062ff2e713e3b4c6dd7cf083b740 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 15:49:57 -0700 Subject: [PATCH 076/102] test new FilePattern attributes and __init__ ValueErrors --- pangeo_forge_recipes/patterns.py | 2 +- tests/test_patterns.py | 66 +++++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/pangeo_forge_recipes/patterns.py b/pangeo_forge_recipes/patterns.py index 90bfc7a8..d76c8cae 100644 --- a/pangeo_forge_recipes/patterns.py +++ b/pangeo_forge_recipes/patterns.py @@ -154,7 +154,7 @@ def __init__( "`is_opendap` must be `False` when passing `fsspec_open_kwargs`." ) if kwargs.keys(): - raise ValueError(f"{kwargs.keys()[0]} is not a valid keyword argument.") + raise ValueError(f"`{list(kwargs.keys())[0]}` is not a supported keyword argument.") def __repr__(self): return f"" diff --git a/tests/test_patterns.py b/tests/test_patterns.py index 53dcc487..7f33f7ac 100644 --- a/tests/test_patterns.py +++ b/tests/test_patterns.py @@ -29,9 +29,17 @@ def make_concat_merge_pattern(**kwargs): def format_function(time, variable): return f"T_{time}_V_{variable}" - fp = FilePattern(format_function, merge, concat, **kwargs) - - return fp, times, varnames, format_function, kwargs + if "fsspec_open_kwargs" in kwargs.keys() and "is_opendap" in kwargs.keys(): + with pytest.raises(ValueError): + fp = FilePattern(format_function, merge, concat, **kwargs) + return + elif "unsupported_kwarg" in kwargs.keys(): + with pytest.raises(ValueError): + fp = FilePattern(format_function, merge, concat, **kwargs) + return + else: + fp = FilePattern(format_function, merge, concat, **kwargs) + return fp, times, varnames, format_function, kwargs @pytest.fixture @@ -39,6 +47,18 @@ def concat_merge_pattern(): return make_concat_merge_pattern() +@pytest.fixture( + params=[ + dict(fsspec_open_kwargs={"block_size": "foo"}), + dict(is_opendap=True), + dict(fsspec_open_kwargs={"block_size": "foo"}, is_opendap=True), + dict(unsupported_kwarg="foo"), + ] +) +def concat_merge_pattern_with_kwargs(request): + return make_concat_merge_pattern(**request.param) + + def test_file_pattern_concat(concat_pattern): fp = concat_pattern assert fp.dims == {"time": 3} @@ -65,9 +85,31 @@ def test_pattern_from_file_sequence(): assert fp[key] == file_sequence[key[0].index] +@pytest.mark.parametrize( + "runtime_secrets", [ + {}, + dict(fsspec_open_kwargs={"username": "foo", "password": "bar"}), + dict(query_string_secrets={"token": "foo"}), + ] +) @pytest.mark.parametrize("pickle", [False, True]) -def test_file_pattern_concat_merge(pickle, concat_merge_pattern): - fp, times, varnames, format_function, _ = concat_merge_pattern +def test_file_pattern_concat_merge(runtime_secrets, pickle, concat_merge_pattern_with_kwargs): + if not concat_merge_pattern_with_kwargs: + # if `fsspec_open_kwargs` are passed with `is_opendap`, or if an unsupported kwarg is + # passed to `FilePattern`, `FilePattern.__init__` raises ValueError and + # `concat_merge_pattern_with_kwargs` returns None, so nothing to test in these cases + return + else: + fp, times, varnames, format_function, kwargs = concat_merge_pattern_with_kwargs + + if runtime_secrets: + if "fsspec_open_kwargs" in runtime_secrets.keys(): + if not fp.is_opendap: + fp.fsspec_open_kwargs.update(runtime_secrets["fsspec_open_kwargs"]) + else: + return + if "query_string_secrets" in runtime_secrets.keys(): + fp.query_string_secrets.update(runtime_secrets["query_string_secrets"]) if pickle: # regular pickle doesn't work here because it can't pickle format_function @@ -95,6 +137,20 @@ def test_file_pattern_concat_merge(pickle, concat_merge_pattern): # make sure key order doesn't matter assert fp[key[::-1]] == expected_fname + if "fsspec_open_kwargs" in kwargs.keys(): + assert fp.is_opendap is False + if "fsspec_open_kwargs" in runtime_secrets.keys(): + kwargs["fsspec_open_kwargs"].update(runtime_secrets["fsspec_open_kwargs"]) + assert fp.fsspec_open_kwargs == kwargs["fsspec_open_kwargs"] + else: + assert fp.fsspec_open_kwargs == kwargs["fsspec_open_kwargs"] + if "query_string_secrets" in runtime_secrets.keys(): + assert fp.query_string_secrets == runtime_secrets["query_string_secrets"] + if "is_opendap" in kwargs.keys(): + assert fp.is_opendap == kwargs["is_opendap"] + assert fp.is_opendap is True + assert fp.fsspec_open_kwargs == {} + @pytest.mark.parametrize("nkeep", [1, 2]) def test_prune(nkeep, concat_merge_pattern): From f455fe1948bb650e22a982b24892942a2084ffd0 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 15:57:58 -0700 Subject: [PATCH 077/102] lint --- pangeo_forge_recipes/patterns.py | 5 +---- tests/conftest.py | 2 +- tests/test_patterns.py | 5 +++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pangeo_forge_recipes/patterns.py b/pangeo_forge_recipes/patterns.py index d76c8cae..873be673 100644 --- a/pangeo_forge_recipes/patterns.py +++ b/pangeo_forge_recipes/patterns.py @@ -138,10 +138,7 @@ class FilePattern: """ def __init__( - self, - format_function: Callable, - *combine_dims: CombineDim, - **kwargs, + self, format_function: Callable, *combine_dims: CombineDim, **kwargs, ): self.format_function = format_function self.combine_dims = combine_dims diff --git a/tests/conftest.py b/tests/conftest.py index dc856d15..2083e4ab 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -118,7 +118,7 @@ def format_function(variable, time): format_function, ConcatDim("time", time_index, items_per_file), MergeDim("variable", ["foo", "bar"]), - **kwargs + **kwargs, ) return file_pattern diff --git a/tests/test_patterns.py b/tests/test_patterns.py index 7f33f7ac..7a3d3216 100644 --- a/tests/test_patterns.py +++ b/tests/test_patterns.py @@ -86,11 +86,12 @@ def test_pattern_from_file_sequence(): @pytest.mark.parametrize( - "runtime_secrets", [ + "runtime_secrets", + [ {}, dict(fsspec_open_kwargs={"username": "foo", "password": "bar"}), dict(query_string_secrets={"token": "foo"}), - ] + ], ) @pytest.mark.parametrize("pickle", [False, True]) def test_file_pattern_concat_merge(runtime_secrets, pickle, concat_merge_pattern_with_kwargs): From 1d1c8bd9d8b7e309f89e9487a87b1686e02e41af Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 16:30:40 -0700 Subject: [PATCH 078/102] revert local path fixture name --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2083e4ab..66ab64f8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -135,7 +135,7 @@ def file_splitter(request): @pytest.fixture(scope="session") -def netcdf_paths(daily_xarray_dataset, tmpdir_factory, items_per_file, file_splitter): +def netcdf_local_paths(daily_xarray_dataset, tmpdir_factory, items_per_file, file_splitter): tmp_path = tmpdir_factory.mktemp("netcdf_data") file_splitter_tuple = file_splitter(daily_xarray_dataset.copy(), items_per_file) From 62ec589f3af8cc6ae16252eed32813b0aa828ae3 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 16:31:17 -0700 Subject: [PATCH 079/102] update test_references for path fixture refactor --- tests/test_references.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_references.py b/tests/test_references.py index 8d65bec5..8f3efb93 100644 --- a/tests/test_references.py +++ b/tests/test_references.py @@ -15,7 +15,9 @@ @pytest.mark.parametrize("with_intake", [True, False]) def test_single(netcdf_local_paths, tmpdir, with_intake): - full_paths, items_per_file = netcdf_local_paths + full_paths, items_per_file, fnames_by_variable = netcdf_local_paths[:3] + if fnames_by_variable: + pytest.skip("This does not test merging operations.") path = str(full_paths[0]) expected = xr.open_dataset(path, engine="h5netcdf") @@ -47,7 +49,9 @@ def test_single(netcdf_local_paths, tmpdir, with_intake): @pytest.mark.parametrize("with_intake", [True, False]) def test_multi(netcdf_local_paths, tmpdir, with_intake): - full_paths, items_per_file = netcdf_local_paths + full_paths, items_per_file, fnames_by_variable = netcdf_local_paths[:3] + if fnames_by_variable: + pytest.skip("This does not test merging operations.") paths = [str(f) for f in full_paths] expected = xr.open_mfdataset(paths, engine="h5netcdf") From 548fc8252f4b77eb0389dc9da0f4f03032b78ab4 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 16:34:22 -0700 Subject: [PATCH 080/102] complete reversion of local fixture name --- tests/conftest.py | 10 +++++----- tests/test_fixtures.py | 4 ++-- tests/test_storage.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 66ab64f8..ad54985b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -100,7 +100,7 @@ def make_file_pattern(path_fixture): Parameters ---------- path_fixture : callable - One of `netcdf_paths` or `netcdf_http_paths` + One of `netcdf_local_paths` or `netcdf_http_paths` """ paths, items_per_file, fnames_by_variable, path_format, kwargs = path_fixture @@ -153,8 +153,8 @@ def netcdf_local_paths(daily_xarray_dataset, tmpdir_factory, items_per_file, fil @pytest.fixture(scope="session") -def netcdf_local_file_pattern(netcdf_paths): - return make_file_pattern(netcdf_paths) +def netcdf_local_file_pattern(netcdf_local_paths): + return make_file_pattern(netcdf_local_paths) @pytest.fixture(scope="session") @@ -200,8 +200,8 @@ def teardown(): dict(required_query_string="foo=foo&bar=bar"), ], ) -def netcdf_http_paths(netcdf_paths, request): - paths, items_per_file, fnames_by_variable, _, kwargs = netcdf_paths +def netcdf_http_paths(netcdf_local_paths, request): + paths, items_per_file, fnames_by_variable, _, kwargs = netcdf_local_paths url = start_http_server(paths, request, **request.param) path_format = url + "/{variable}_{time:03d}.nc" if fnames_by_variable else None diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 5334f8d8..ad43e720 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -5,8 +5,8 @@ from pangeo_forge_recipes.utils import fix_scalar_attr_encoding -def test_fixture_local_files(daily_xarray_dataset, netcdf_paths): - paths = netcdf_paths[0] +def test_fixture_local_files(daily_xarray_dataset, netcdf_local_paths): + paths = netcdf_local_paths[0] paths = [str(path) for path in paths] ds = xr.open_mfdataset(paths, combine="by_coords", concat_dim="time", engine="h5netcdf") assert ds.identical(daily_xarray_dataset) diff --git a/tests/test_storage.py b/tests/test_storage.py index 0bdcb353..90f015a1 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -40,7 +40,7 @@ def test_metadata_target(tmp_metadata_target): @pytest.mark.parametrize( - "file_paths", [lazy_fixture("netcdf_paths"), lazy_fixture("netcdf_http_paths")], + "file_paths", [lazy_fixture("netcdf_local_paths"), lazy_fixture("netcdf_http_paths")], ) @pytest.mark.parametrize("copy_to_local", [False, True]) @pytest.mark.parametrize("use_cache, cache_first", [(False, False), (True, False), (True, True)]) From 08e0136b0e1fe69a46c12e0bd2284c6b0159894a Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 16:41:57 -0700 Subject: [PATCH 081/102] terraclimate tutorial: remove deprecated mentions of fsspec_open_kwargs --- docs/tutorials/xarray_zarr/terraclimate.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/xarray_zarr/terraclimate.ipynb b/docs/tutorials/xarray_zarr/terraclimate.ipynb index 72f4c0fa..bec662a5 100755 --- a/docs/tutorials/xarray_zarr/terraclimate.ipynb +++ b/docs/tutorials/xarray_zarr/terraclimate.ipynb @@ -220,7 +220,7 @@ { "data": { "text/plain": [ - "XarrayZarrRecipe(file_pattern=, inputs_per_chunk=1, target_chunks={'lat': 1024, 'lon': 1024, 'time': 12}, target=None, input_cache=None, metadata_cache=None, cache_inputs=True, copy_input_to_local_file=False, consolidate_zarr=True, xarray_open_kwargs={}, xarray_concat_kwargs={}, delete_input_encoding=True, fsspec_open_kwargs={}, process_input=None, process_chunk=, lock_timeout=None, subset_inputs={})" + "XarrayZarrRecipe(file_pattern=, inputs_per_chunk=1, target_chunks={'lat': 1024, 'lon': 1024, 'time': 12}, target=None, input_cache=None, metadata_cache=None, cache_inputs=True, copy_input_to_local_file=False, consolidate_zarr=True, xarray_open_kwargs={}, xarray_concat_kwargs={}, delete_input_encoding=True, process_input=None, process_chunk=, lock_timeout=None, subset_inputs={})" ] }, "execution_count": 5, @@ -257,7 +257,7 @@ { "data": { "text/plain": [ - "XarrayZarrRecipe(file_pattern=, inputs_per_chunk=1, target_chunks={'lat': 1024, 'lon': 1024, 'time': 12}, target=FSSpecTarget(fs=, root_path='/var/folders/n8/63q49ms55wxcj_gfbtykwp5r0000gn/T/tmpbo124muo'), input_cache=CacheFSSpecTarget(fs=, root_path='/var/folders/n8/63q49ms55wxcj_gfbtykwp5r0000gn/T/tmpf4qd_07g'), metadata_cache=MetadataTarget(fs=, root_path='/var/folders/n8/63q49ms55wxcj_gfbtykwp5r0000gn/T/tmployas62r'), cache_inputs=True, copy_input_to_local_file=False, consolidate_zarr=True, xarray_open_kwargs={}, xarray_concat_kwargs={}, delete_input_encoding=True, fsspec_open_kwargs={}, process_input=None, process_chunk=, lock_timeout=None, subset_inputs={})" + "XarrayZarrRecipe(file_pattern=, inputs_per_chunk=1, target_chunks={'lat': 1024, 'lon': 1024, 'time': 12}, target=FSSpecTarget(fs=, root_path='/var/folders/n8/63q49ms55wxcj_gfbtykwp5r0000gn/T/tmpbo124muo'), input_cache=CacheFSSpecTarget(fs=, root_path='/var/folders/n8/63q49ms55wxcj_gfbtykwp5r0000gn/T/tmpf4qd_07g'), metadata_cache=MetadataTarget(fs=, root_path='/var/folders/n8/63q49ms55wxcj_gfbtykwp5r0000gn/T/tmployas62r'), cache_inputs=True, copy_input_to_local_file=False, consolidate_zarr=True, xarray_open_kwargs={}, xarray_concat_kwargs={}, delete_input_encoding=True, process_input=None, process_chunk=, lock_timeout=None, subset_inputs={})" ] }, "execution_count": 6, From fad573973f35c4c630da1626e4d29e1a8809f68d Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 16:47:29 -0700 Subject: [PATCH 082/102] netcdf sequential tutorial: remove deprecated mentions of fsspec_open_kwargs --- docs/tutorials/xarray_zarr/netcdf_zarr_sequential.ipynb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/tutorials/xarray_zarr/netcdf_zarr_sequential.ipynb b/docs/tutorials/xarray_zarr/netcdf_zarr_sequential.ipynb index 8707644a..b1908013 100755 --- a/docs/tutorials/xarray_zarr/netcdf_zarr_sequential.ipynb +++ b/docs/tutorials/xarray_zarr/netcdf_zarr_sequential.ipynb @@ -813,7 +813,6 @@ "\u001b[0;34m\u001b[0m \u001b[0mxarray_open_kwargs\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mdict\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m<\u001b[0m\u001b[0mfactory\u001b[0m\u001b[0;34m>\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mxarray_concat_kwargs\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mdict\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m<\u001b[0m\u001b[0mfactory\u001b[0m\u001b[0;34m>\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mdelete_input_encoding\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mfsspec_open_kwargs\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mdict\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m<\u001b[0m\u001b[0mfactory\u001b[0m\u001b[0;34m>\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mprocess_input\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mCallable\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mxarray\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcore\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdataset\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mDataset\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mxarray\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcore\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdataset\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mDataset\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mNoneType\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mprocess_chunk\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mCallable\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mxarray\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcore\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdataset\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mDataset\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mxarray\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcore\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdataset\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mDataset\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mNoneType\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mlock_timeout\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mint\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mNoneType\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", @@ -849,7 +848,6 @@ " the inputs to form a chunk.\n", ":param delete_input_encoding: Whether to remove Xarray encoding from variables\n", " in the input dataset\n", - ":param fsspec_open_kwargs: Extra options for opening the inputs with fsspec.\n", ":param process_input: Function to call on each opened input, with signature\n", " `(ds: xr.Dataset, filename: str) -> ds: xr.Dataset`.\n", ":param process_chunk: Function to call on each concatenated chunk, with signature\n", @@ -889,7 +887,7 @@ { "data": { "text/plain": [ - "XarrayZarrRecipe(file_pattern=, inputs_per_chunk=1, target_chunks={}, target=None, input_cache=None, metadata_cache=None, cache_inputs=True, copy_input_to_local_file=False, consolidate_zarr=True, xarray_open_kwargs={}, xarray_concat_kwargs={}, delete_input_encoding=True, fsspec_open_kwargs={}, process_input=None, process_chunk=None, lock_timeout=None, subset_inputs={})" + "XarrayZarrRecipe(file_pattern=, inputs_per_chunk=1, target_chunks={}, target=None, input_cache=None, metadata_cache=None, cache_inputs=True, copy_input_to_local_file=False, consolidate_zarr=True, xarray_open_kwargs={}, xarray_concat_kwargs={}, delete_input_encoding=True, process_input=None, process_chunk=None, lock_timeout=None, subset_inputs={})" ] }, "execution_count": 13, @@ -923,7 +921,7 @@ { "data": { "text/plain": [ - "XarrayZarrRecipe(file_pattern=, inputs_per_chunk=10, target_chunks={}, target=None, input_cache=None, metadata_cache=None, cache_inputs=True, copy_input_to_local_file=False, consolidate_zarr=True, xarray_open_kwargs={}, xarray_concat_kwargs={}, delete_input_encoding=True, fsspec_open_kwargs={}, process_input=None, process_chunk=None, lock_timeout=None, subset_inputs={})" + "XarrayZarrRecipe(file_pattern=, inputs_per_chunk=10, target_chunks={}, target=None, input_cache=None, metadata_cache=None, cache_inputs=True, copy_input_to_local_file=False, consolidate_zarr=True, xarray_open_kwargs={}, xarray_concat_kwargs={}, delete_input_encoding=True, process_input=None, process_chunk=None, lock_timeout=None, subset_inputs={})" ] }, "execution_count": 14, @@ -1852,7 +1850,7 @@ { "data": { "text/plain": [ - "XarrayZarrRecipe(file_pattern=, inputs_per_chunk=10, target_chunks={}, target=FSSpecTarget(fs=, root_path='/var/folders/n8/63q49ms55wxcj_gfbtykwp5r0000gn/T/tmpuz91tfhl'), input_cache=CacheFSSpecTarget(fs=, root_path='/var/folders/n8/63q49ms55wxcj_gfbtykwp5r0000gn/T/tmpq3zo16e1'), metadata_cache=None, cache_inputs=True, copy_input_to_local_file=False, consolidate_zarr=True, xarray_open_kwargs={}, xarray_concat_kwargs={}, delete_input_encoding=True, fsspec_open_kwargs={}, process_input=None, process_chunk=None, lock_timeout=None, subset_inputs={})" + "XarrayZarrRecipe(file_pattern=, inputs_per_chunk=10, target_chunks={}, target=FSSpecTarget(fs=, root_path='/var/folders/n8/63q49ms55wxcj_gfbtykwp5r0000gn/T/tmpuz91tfhl'), input_cache=CacheFSSpecTarget(fs=, root_path='/var/folders/n8/63q49ms55wxcj_gfbtykwp5r0000gn/T/tmpq3zo16e1'), metadata_cache=None, cache_inputs=True, copy_input_to_local_file=False, consolidate_zarr=True, xarray_open_kwargs={}, xarray_concat_kwargs={}, delete_input_encoding=True, process_input=None, process_chunk=None, lock_timeout=None, subset_inputs={})" ] }, "execution_count": 19, From f799309468ae080e3cdae68b0d6fdc1b7c59fdfc Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 16:59:11 -0700 Subject: [PATCH 083/102] re-run cmip6-recipe.ipynb with fsspec_open_kwargs passed to FilePattern --- docs/tutorials/xarray_zarr/cmip6-recipe.ipynb | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/tutorials/xarray_zarr/cmip6-recipe.ipynb b/docs/tutorials/xarray_zarr/cmip6-recipe.ipynb index 1c4c22ad..f4c17c1b 100755 --- a/docs/tutorials/xarray_zarr/cmip6-recipe.ipynb +++ b/docs/tutorials/xarray_zarr/cmip6-recipe.ipynb @@ -798,7 +798,9 @@ "metadata": {}, "source": [ "**Instantiating the file pattern**\n", - "- Now that we have a both file path function and our \"combine dimensions\" object, we can move on to instantiating to file pattern, passing these two objects as arguments:" + "- Now that we have a both file path function and our \"combine dimensions\" object, we can move on to instantiating to file pattern, passing these two objects as arguments.\n", + "- Note that we will use `fsspec.open` under the hood for most file opening, so if there are any special keyword arguments we want to pass to this function, now is the time to do it.\n", + "- Here we specify `fsspec_open_kwargs={'anon':True}` as a keyword argument in the `FilePattern`, because we want to access the source files anonymously." ] }, { @@ -819,7 +821,7 @@ ], "source": [ "from pangeo_forge_recipes.patterns import FilePattern\n", - "pattern = FilePattern(make_full_path, time_concat_dim)\n", + "pattern = FilePattern(make_full_path, time_concat_dim, fsspec_open_kwargs={'anon':True})\n", "pattern" ] }, @@ -872,7 +874,6 @@ " target_chunks=target_chunks,\n", " process_chunk=set_bnds_as_coords,\n", " xarray_concat_kwargs={'join':'exact'},\n", - " fsspec_open_kwargs={'anon':True},\n", ")" ] }, @@ -893,9 +894,9 @@ { "data": { "text/plain": [ - "('/var/folders/n8/63q49ms55wxcj_gfbtykwp5r0000gn/T/tmpshxlh9rm',\n", - " '/var/folders/n8/63q49ms55wxcj_gfbtykwp5r0000gn/T/tmptvher37y',\n", - " '/var/folders/n8/63q49ms55wxcj_gfbtykwp5r0000gn/T/tmpyzq07dbr')" + "('/var/folders/mz/gxy_z7dx1k153xf0c3fks9_40000gp/T/tmppas0xjj8',\n", + " '/var/folders/mz/gxy_z7dx1k153xf0c3fks9_40000gp/T/tmpsykkms9h',\n", + " '/var/folders/mz/gxy_z7dx1k153xf0c3fks9_40000gp/T/tmpx3_wx22z')" ] }, "execution_count": 22, @@ -1033,7 +1034,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 25, @@ -1042,7 +1043,7 @@ }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -1079,9 +1080,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "pangeo-forge3.8", "language": "python", - "name": "python3" + "name": "pangeo-forge3.8" }, "language_info": { "codemirror_mode": { From 3822c0cf58ef7fb82b6608d785c78328b5154f65 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 17:01:37 -0700 Subject: [PATCH 084/102] opendap subset tutorial: remove deprecated mentions of fsspec_open_kwargs --- docs/tutorials/xarray_zarr/opendap_subset_recipe.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/xarray_zarr/opendap_subset_recipe.ipynb b/docs/tutorials/xarray_zarr/opendap_subset_recipe.ipynb index f21b5518..9190f7c0 100644 --- a/docs/tutorials/xarray_zarr/opendap_subset_recipe.ipynb +++ b/docs/tutorials/xarray_zarr/opendap_subset_recipe.ipynb @@ -598,7 +598,7 @@ { "data": { "text/plain": [ - "XarrayZarrRecipe(file_pattern=, inputs_per_chunk=1, target_chunks={'time': 1}, target=None, input_cache=None, metadata_cache=None, cache_inputs=False, copy_input_to_local_file=False, consolidate_zarr=True, xarray_open_kwargs={'engine': 'netcdf4'}, xarray_concat_kwargs={}, delete_input_encoding=True, fsspec_open_kwargs={}, process_input=None, process_chunk=None, lock_timeout=None, subset_inputs={'time': 30}, is_opendap=True)" + "XarrayZarrRecipe(file_pattern=, inputs_per_chunk=1, target_chunks={'time': 1}, target=None, input_cache=None, metadata_cache=None, cache_inputs=False, copy_input_to_local_file=False, consolidate_zarr=True, xarray_open_kwargs={'engine': 'netcdf4'}, xarray_concat_kwargs={}, delete_input_encoding=True, process_input=None, process_chunk=None, lock_timeout=None, subset_inputs={'time': 30}, is_opendap=True)" ] }, "execution_count": 5, @@ -636,7 +636,7 @@ { "data": { "text/plain": [ - "XarrayZarrRecipe(file_pattern=, inputs_per_chunk=1, target_chunks={'time': 1}, target=FSSpecTarget(fs=, root_path='/var/folders/n8/63q49ms55wxcj_gfbtykwp5r0000gn/T/tmpt58fl_jv'), input_cache=None, metadata_cache=MetadataTarget(fs=, root_path='/var/folders/n8/63q49ms55wxcj_gfbtykwp5r0000gn/T/tmpb9_y3bnl'), cache_inputs=False, copy_input_to_local_file=False, consolidate_zarr=True, xarray_open_kwargs={'engine': 'netcdf4'}, xarray_concat_kwargs={}, delete_input_encoding=True, fsspec_open_kwargs={}, process_input=None, process_chunk=None, lock_timeout=None, subset_inputs={'time': 30}, is_opendap=True)" + "XarrayZarrRecipe(file_pattern=, inputs_per_chunk=1, target_chunks={'time': 1}, target=FSSpecTarget(fs=, root_path='/var/folders/n8/63q49ms55wxcj_gfbtykwp5r0000gn/T/tmpt58fl_jv'), input_cache=None, metadata_cache=MetadataTarget(fs=, root_path='/var/folders/n8/63q49ms55wxcj_gfbtykwp5r0000gn/T/tmpb9_y3bnl'), cache_inputs=False, copy_input_to_local_file=False, consolidate_zarr=True, xarray_open_kwargs={'engine': 'netcdf4'}, xarray_concat_kwargs={}, delete_input_encoding=True, process_input=None, process_chunk=None, lock_timeout=None, subset_inputs={'time': 30}, is_opendap=True)" ] }, "execution_count": 6, From fc25fa48290e51e702cd0fd498d42ba26ce63c2b Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 17:03:30 -0700 Subject: [PATCH 085/102] multi variable tutorial: remove deprecated mentions of fsspec_open_kwargs --- docs/tutorials/xarray_zarr/multi_variable_recipe.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/xarray_zarr/multi_variable_recipe.ipynb b/docs/tutorials/xarray_zarr/multi_variable_recipe.ipynb index e676cbc7..356e8e1c 100755 --- a/docs/tutorials/xarray_zarr/multi_variable_recipe.ipynb +++ b/docs/tutorials/xarray_zarr/multi_variable_recipe.ipynb @@ -2270,7 +2270,7 @@ { "data": { "text/plain": [ - "XarrayZarrRecipe(file_pattern=, inputs_per_chunk=1, target_chunks={}, target=None, input_cache=None, metadata_cache=None, cache_inputs=True, copy_input_to_local_file=False, consolidate_zarr=True, xarray_open_kwargs={'decode_times': False}, xarray_concat_kwargs={}, delete_input_encoding=True, fsspec_open_kwargs={}, process_input=, process_chunk=None, lock_timeout=None, subset_inputs={})" + "XarrayZarrRecipe(file_pattern=, inputs_per_chunk=1, target_chunks={}, target=None, input_cache=None, metadata_cache=None, cache_inputs=True, copy_input_to_local_file=False, consolidate_zarr=True, xarray_open_kwargs={'decode_times': False}, xarray_concat_kwargs={}, delete_input_encoding=True, process_input=, process_chunk=None, lock_timeout=None, subset_inputs={})" ] }, "execution_count": 12, From 2c5cc19eae3bd0e4655e0b54c8972519d03b0be1 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 17:07:03 -0700 Subject: [PATCH 086/102] reset kernelspec for cmip6-recipe.ipynb --- docs/tutorials/xarray_zarr/cmip6-recipe.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/xarray_zarr/cmip6-recipe.ipynb b/docs/tutorials/xarray_zarr/cmip6-recipe.ipynb index f4c17c1b..6edfc7ce 100755 --- a/docs/tutorials/xarray_zarr/cmip6-recipe.ipynb +++ b/docs/tutorials/xarray_zarr/cmip6-recipe.ipynb @@ -1080,9 +1080,9 @@ ], "metadata": { "kernelspec": { - "display_name": "pangeo-forge3.8", + "display_name": "Python 3", "language": "python", - "name": "pangeo-forge3.8" + "name": "python3" }, "language_info": { "codemirror_mode": { From 3573d1e266100f076369b3e76ab843eed2e3fb88 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 17:11:12 -0700 Subject: [PATCH 087/102] fix nitems_per_file typo in file pattern docs --- docs/recipe_user_guide/file_patterns.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/recipe_user_guide/file_patterns.md b/docs/recipe_user_guide/file_patterns.md index 1b9fc9de..fdc76ede 100644 --- a/docs/recipe_user_guide/file_patterns.md +++ b/docs/recipe_user_guide/file_patterns.md @@ -113,7 +113,7 @@ and type of combine dimensions they support. ``ConcatDim`` and allows at most one ``MergeDim``. -### Specifying `nitems_per_input` in a `ConcatDim` +### Specifying `nitems_per_file` in a `ConcatDim` FilePatterns are deliberately very simple. However, there is one case where we can annotate the FilePattern with a bit of extra information. @@ -127,7 +127,7 @@ have one record of daily temperature? Ten? In general, Pangeo Forge does not assume there is a constant, known number of records in each file; instead it will discover this information by peeking into each file. But _if we know a-priori that there is a fixed number of records per file_, we can -provide this as a hint, via `niterms_per_file` keyword in `ConcatDim`. +provide this as a hint, via `nitems_per_file` keyword in `ConcatDim`. Providing this hint will allow Pangeo Forge to work more quickly because it doesn't have to peek into the files. From 1c7450d05edb2765eb57df71d47aca974b390a81 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Tue, 31 Aug 2021 18:17:02 -0700 Subject: [PATCH 088/102] add narrative docs for new FilePattern attrs --- docs/recipe_user_guide/file_patterns.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/recipe_user_guide/file_patterns.md b/docs/recipe_user_guide/file_patterns.md index fdc76ede..1cf93027 100644 --- a/docs/recipe_user_guide/file_patterns.md +++ b/docs/recipe_user_guide/file_patterns.md @@ -113,6 +113,27 @@ and type of combine dimensions they support. ``ConcatDim`` and allows at most one ``MergeDim``. +### Extra keyword arguments for `FilePattern` + +`FilePattern` objects carry all of the information needed to open source files. The following additional keyword +arguments may passed to `FilePattern` instances as appropriate: + +- `fsspec_open_kwargs`: A dictionary of kwargs to pass to `fsspec.open` to aid opening of source files. For example, +`{"block_size": 0}` may be passed if an HTTP source file server does not permit range requests. Authentication for +`fsspec`-compatible filesystems may be handled here as well. +- `query_string_secrets`: A dictionary of key:value pairs to append to each source file url query at runtime. Query +parameters which are not secrets should instead be included in the `format_function`. +- `is_opendap`: Boolean value to specify whether or not the source files are served via OPeNDAP. Incompatible with caching, +and mutually exclusive with `fsspec_open_kwargs`. Defaults to `False`. + +```{warning} +Secrets including login credentials and API tokens should never be committed to a public repository. As such, +we strongly suggest that you do **not** instantiate your `FilePattern` with these or any other secrets when +developing your recipe. If your source files require authentication via `fsspec_open_kwargs` and/or +`query_string_secrets`, it is advisable to update these attributes at runtime. Pangeo Forge will soon offer a +mechanism for securely handling such recipe secrets on GitHub. +``` + ### Specifying `nitems_per_file` in a `ConcatDim` FilePatterns are deliberately very simple. However, there is one case where From f4b2bb42c8a5062ca95db45b55e78c7b270a3db4 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 1 Sep 2021 10:54:23 -0700 Subject: [PATCH 089/102] add HTTP authentication examples to docs --- docs/recipe_user_guide/file_patterns.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/recipe_user_guide/file_patterns.md b/docs/recipe_user_guide/file_patterns.md index 1cf93027..5210e45f 100644 --- a/docs/recipe_user_guide/file_patterns.md +++ b/docs/recipe_user_guide/file_patterns.md @@ -118,12 +118,20 @@ and type of combine dimensions they support. `FilePattern` objects carry all of the information needed to open source files. The following additional keyword arguments may passed to `FilePattern` instances as appropriate: -- `fsspec_open_kwargs`: A dictionary of kwargs to pass to `fsspec.open` to aid opening of source files. For example, +- **`fsspec_open_kwargs`**: A dictionary of kwargs to pass to `fsspec.open` to aid opening of source files. For example, `{"block_size": 0}` may be passed if an HTTP source file server does not permit range requests. Authentication for -`fsspec`-compatible filesystems may be handled here as well. -- `query_string_secrets`: A dictionary of key:value pairs to append to each source file url query at runtime. Query +`fsspec`-compatible filesystems may be handled here as well. For HTTP username/password-based authentication, your specific +`fsspec_open_kwargs` will depend on the configuration of the source file server, but are likely to conform to one of the following +two formats: + + ```ipython3 + fsspec_open_kwargs={"username": "", "password": ""} + fsspec_open_kwargs={"auth": aiohttp.BasicAuth("", "")} + ``` + +- **`query_string_secrets`**: A dictionary of key:value pairs to append to each source file url query at runtime. Query parameters which are not secrets should instead be included in the `format_function`. -- `is_opendap`: Boolean value to specify whether or not the source files are served via OPeNDAP. Incompatible with caching, +- **`is_opendap`**: Boolean value to specify whether or not the source files are served via OPeNDAP. Incompatible with caching, and mutually exclusive with `fsspec_open_kwargs`. Defaults to `False`. ```{warning} From d0982fa85a22585394d7040202ee8f9325893efd Mon Sep 17 00:00:00 2001 From: Charles Stern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 1 Sep 2021 10:54:56 -0700 Subject: [PATCH 090/102] Update docs/recipe_user_guide/file_patterns.md Co-authored-by: Ryan Abernathey --- docs/recipe_user_guide/file_patterns.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/recipe_user_guide/file_patterns.md b/docs/recipe_user_guide/file_patterns.md index 1cf93027..44d792fe 100644 --- a/docs/recipe_user_guide/file_patterns.md +++ b/docs/recipe_user_guide/file_patterns.md @@ -130,7 +130,7 @@ and mutually exclusive with `fsspec_open_kwargs`. Defaults to `False`. Secrets including login credentials and API tokens should never be committed to a public repository. As such, we strongly suggest that you do **not** instantiate your `FilePattern` with these or any other secrets when developing your recipe. If your source files require authentication via `fsspec_open_kwargs` and/or -`query_string_secrets`, it is advisable to update these attributes at runtime. Pangeo Forge will soon offer a +`query_string_secrets`, it is advisable to update these attributes at execution time. Pangeo Forge will soon offer a mechanism for securely handling such recipe secrets on GitHub. ``` From efbf3354bf3a05ee31800ed12c28c6aaeff58c1f Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 1 Sep 2021 11:38:35 -0700 Subject: [PATCH 091/102] make all FilePattern.__init__ kwargs explicit --- pangeo_forge_recipes/patterns.py | 15 +++++++++------ tests/test_patterns.py | 10 ++-------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/pangeo_forge_recipes/patterns.py b/pangeo_forge_recipes/patterns.py index 873be673..798efc31 100644 --- a/pangeo_forge_recipes/patterns.py +++ b/pangeo_forge_recipes/patterns.py @@ -138,20 +138,23 @@ class FilePattern: """ def __init__( - self, format_function: Callable, *combine_dims: CombineDim, **kwargs, + self, + format_function: Callable, + *combine_dims: CombineDim, + fsspec_open_kwargs: dict = {}, + query_string_secrets: dict = {}, + is_opendap: bool = False, ): self.format_function = format_function self.combine_dims = combine_dims - self.fsspec_open_kwargs = kwargs.pop("fsspec_open_kwargs", {}) - self.query_string_secrets = kwargs.pop("query_string_secrets", {}) - self.is_opendap = kwargs.pop("is_opendap", False) + self.fsspec_open_kwargs = fsspec_open_kwargs + self.query_string_secrets = query_string_secrets + self.is_opendap = is_opendap if self.fsspec_open_kwargs and self.is_opendap: raise ValueError( "OPeNDAP inputs are not opened with `fsspec`. " "`is_opendap` must be `False` when passing `fsspec_open_kwargs`." ) - if kwargs.keys(): - raise ValueError(f"`{list(kwargs.keys())[0]}` is not a supported keyword argument.") def __repr__(self): return f"" diff --git a/tests/test_patterns.py b/tests/test_patterns.py index 7a3d3216..b7684c05 100644 --- a/tests/test_patterns.py +++ b/tests/test_patterns.py @@ -33,10 +33,6 @@ def format_function(time, variable): with pytest.raises(ValueError): fp = FilePattern(format_function, merge, concat, **kwargs) return - elif "unsupported_kwarg" in kwargs.keys(): - with pytest.raises(ValueError): - fp = FilePattern(format_function, merge, concat, **kwargs) - return else: fp = FilePattern(format_function, merge, concat, **kwargs) return fp, times, varnames, format_function, kwargs @@ -52,7 +48,6 @@ def concat_merge_pattern(): dict(fsspec_open_kwargs={"block_size": "foo"}), dict(is_opendap=True), dict(fsspec_open_kwargs={"block_size": "foo"}, is_opendap=True), - dict(unsupported_kwarg="foo"), ] ) def concat_merge_pattern_with_kwargs(request): @@ -96,9 +91,8 @@ def test_pattern_from_file_sequence(): @pytest.mark.parametrize("pickle", [False, True]) def test_file_pattern_concat_merge(runtime_secrets, pickle, concat_merge_pattern_with_kwargs): if not concat_merge_pattern_with_kwargs: - # if `fsspec_open_kwargs` are passed with `is_opendap`, or if an unsupported kwarg is - # passed to `FilePattern`, `FilePattern.__init__` raises ValueError and - # `concat_merge_pattern_with_kwargs` returns None, so nothing to test in these cases + # if `fsspec_open_kwargs` are passed with `is_opendap`, `FilePattern.__init__` raises + # ValueError and `concat_merge_pattern_with_kwargs` returns None, so nothing to test return else: fp, times, varnames, format_function, kwargs = concat_merge_pattern_with_kwargs From 5f750f4ff5f054127cf421b96959a7bc05f5d2c2 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 1 Sep 2021 16:18:41 -0700 Subject: [PATCH 092/102] clean up redundant control flow for assertions in test_patterns --- tests/test_patterns.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_patterns.py b/tests/test_patterns.py index b7684c05..e1eb5428 100644 --- a/tests/test_patterns.py +++ b/tests/test_patterns.py @@ -136,9 +136,7 @@ def test_file_pattern_concat_merge(runtime_secrets, pickle, concat_merge_pattern assert fp.is_opendap is False if "fsspec_open_kwargs" in runtime_secrets.keys(): kwargs["fsspec_open_kwargs"].update(runtime_secrets["fsspec_open_kwargs"]) - assert fp.fsspec_open_kwargs == kwargs["fsspec_open_kwargs"] - else: - assert fp.fsspec_open_kwargs == kwargs["fsspec_open_kwargs"] + assert fp.fsspec_open_kwargs == kwargs["fsspec_open_kwargs"] if "query_string_secrets" in runtime_secrets.keys(): assert fp.query_string_secrets == runtime_secrets["query_string_secrets"] if "is_opendap" in kwargs.keys(): From 7117a661a190c8c1e3bf68a3678df5f319770bdc Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 1 Sep 2021 16:19:37 -0700 Subject: [PATCH 093/102] for optional FilePattern kwargs, set default to None --- pangeo_forge_recipes/patterns.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pangeo_forge_recipes/patterns.py b/pangeo_forge_recipes/patterns.py index 798efc31..74389323 100644 --- a/pangeo_forge_recipes/patterns.py +++ b/pangeo_forge_recipes/patterns.py @@ -141,14 +141,14 @@ def __init__( self, format_function: Callable, *combine_dims: CombineDim, - fsspec_open_kwargs: dict = {}, - query_string_secrets: dict = {}, + fsspec_open_kwargs: Optional[Dict[str, Any]] = None, + query_string_secrets: Optional[Dict[str, str]] = None, is_opendap: bool = False, ): self.format_function = format_function self.combine_dims = combine_dims - self.fsspec_open_kwargs = fsspec_open_kwargs - self.query_string_secrets = query_string_secrets + self.fsspec_open_kwargs = fsspec_open_kwargs if fsspec_open_kwargs else {} + self.query_string_secrets = query_string_secrets if query_string_secrets else {} self.is_opendap = is_opendap if self.fsspec_open_kwargs and self.is_opendap: raise ValueError( From 72da50fba4e023bbe170bf9e52ac86211c80934a Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 1 Sep 2021 16:45:18 -0700 Subject: [PATCH 094/102] remove test_recipe_caching_copying redundancy with lazy fixture --- tests/test_recipes.py | 31 +++++-------------------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 35760590..f60095ba 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -109,34 +109,13 @@ def test_prune_recipe(recipe_fixture, execute_recipe, nkeep): @pytest.mark.parametrize("cache_inputs", [True, False]) @pytest.mark.parametrize("copy_input_to_local_file", [True, False]) -def test_recipe_caching_copying( - netCDFtoZarr_recipe, execute_recipe, cache_inputs, copy_input_to_local_file -): +@pytest.mark.parametrize( + "recipe", [lazy_fixture("netCDFtoZarr_recipe"), lazy_fixture("netCDFtoZarr_http_recipe")], +) +def test_recipe_caching_copying(recipe, execute_recipe, cache_inputs, copy_input_to_local_file): """Test that caching and copying to local file work.""" - RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_recipe - - if not cache_inputs: - kwargs.pop("input_cache") # make sure recipe doesn't require input_cache - rec = RecipeClass( - file_pattern, - **kwargs, - cache_inputs=cache_inputs, - copy_input_to_local_file=copy_input_to_local_file - ) - execute_recipe(rec) - ds_actual = xr.open_zarr(target.get_mapper()).load() - xr.testing.assert_identical(ds_actual, ds_expected) - - -@pytest.mark.parametrize("cache_inputs", [True, False]) -@pytest.mark.parametrize("copy_input_to_local_file", [True, False]) -def test_recipe_http_caching_copying( - netCDFtoZarr_http_recipe, execute_recipe, cache_inputs, copy_input_to_local_file -): - """Test that caching and copying from http to local file work.""" - - RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_http_recipe + RecipeClass, file_pattern, kwargs, ds_expected, target = recipe if not cache_inputs: kwargs.pop("input_cache") # make sure recipe doesn't require input_cache From 4b2ae4ae82a695a857f4dff0b64dfec62bfcf5a9 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 1 Sep 2021 17:17:28 -0700 Subject: [PATCH 095/102] define make_netcdf_local_paths function --- tests/conftest.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ad54985b..a39f6f03 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -129,13 +129,7 @@ def items_per_file(request): return request.param -@pytest.fixture(scope="session", params=[split_up_files_by_day, split_up_files_by_variable_and_day]) -def file_splitter(request): - return request.param - - -@pytest.fixture(scope="session") -def netcdf_local_paths(daily_xarray_dataset, tmpdir_factory, items_per_file, file_splitter): +def make_netcdf_local_paths(daily_xarray_dataset, tmpdir_factory, items_per_file, file_splitter): tmp_path = tmpdir_factory.mktemp("netcdf_data") file_splitter_tuple = file_splitter(daily_xarray_dataset.copy(), items_per_file) @@ -152,6 +146,13 @@ def netcdf_local_paths(daily_xarray_dataset, tmpdir_factory, items_per_file, fil return full_paths, items_per_file, fnames_by_variable, path_format, kwargs +@pytest.fixture(scope="session", params=[split_up_files_by_day, split_up_files_by_variable_and_day]) +def netcdf_local_paths(daily_xarray_dataset, tmpdir_factory, items_per_file, request): + return make_netcdf_local_paths( + daily_xarray_dataset, tmpdir_factory, items_per_file, request.param + ) + + @pytest.fixture(scope="session") def netcdf_local_file_pattern(netcdf_local_paths): return make_file_pattern(netcdf_local_paths) From f6b97742775cfa5a980f9cf944f28cbc6db851e2 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 1 Sep 2021 18:13:02 -0700 Subject: [PATCH 096/102] refactor file pattern fixtures to distinguish between sequential and multivariable --- tests/conftest.py | 50 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a39f6f03..a1724881 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,8 @@ import xarray as xr from dask.distributed import Client, LocalCluster from prefect.executors import DaskExecutor +# need to import this way (rather than use pytest.lazy_fixture) to make it work with dask +from pytest_lazyfixture import lazy_fixture from pangeo_forge_recipes.executors import ( DaskPipelineExecutor, @@ -146,16 +148,54 @@ def make_netcdf_local_paths(daily_xarray_dataset, tmpdir_factory, items_per_file return full_paths, items_per_file, fnames_by_variable, path_format, kwargs -@pytest.fixture(scope="session", params=[split_up_files_by_day, split_up_files_by_variable_and_day]) -def netcdf_local_paths(daily_xarray_dataset, tmpdir_factory, items_per_file, request): +@pytest.fixture(scope="session") +def netcdf_local_paths_sequential(daily_xarray_dataset, tmpdir_factory, items_per_file): + return make_netcdf_local_paths( + daily_xarray_dataset, tmpdir_factory, items_per_file, split_up_files_by_day + ) + + +@pytest.fixture(scope="session") +def netcdf_local_paths_sequential_multi_variable( + daily_xarray_dataset, tmpdir_factory, items_per_file +): return make_netcdf_local_paths( - daily_xarray_dataset, tmpdir_factory, items_per_file, request.param + daily_xarray_dataset, tmpdir_factory, items_per_file, split_up_files_by_variable_and_day ) +@pytest.fixture( + scope="session", + params=[ + lazy_fixture("netcdf_local_paths_sequential"), + lazy_fixture("netcdf_local_paths_sequential_multi_variable"), + ], +) +def netcdf_local_paths(request): + return request.param + + +@pytest.fixture(scope="session") +def netcdf_local_file_pattern_sequential(netcdf_local_paths_sequential): + return make_file_pattern(netcdf_local_paths_sequential) + + @pytest.fixture(scope="session") -def netcdf_local_file_pattern(netcdf_local_paths): - return make_file_pattern(netcdf_local_paths) +def netcdf_local_file_pattern_sequential_multi_variable( + netcdf_local_paths_sequential_multi_variable +): + return make_file_pattern(netcdf_local_paths_sequential_multi_variable) + + +@pytest.fixture( + scope="session", + params=[ + lazy_fixture("netcdf_local_file_pattern_sequential"), + lazy_fixture("netcdf_local_file_pattern_sequential_multi_variable"), + ], +) +def netcdf_local_file_pattern(request): + return make_file_pattern(request.param) @pytest.fixture(scope="session") From 9f132ae378ce10e0f6ea1dac027f84f4b67c902f Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 1 Sep 2021 18:13:44 -0700 Subject: [PATCH 097/102] pass sequential file pattern fixture in test_references.py --- tests/test_references.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/tests/test_references.py b/tests/test_references.py index 8f3efb93..b6aac41e 100644 --- a/tests/test_references.py +++ b/tests/test_references.py @@ -4,7 +4,6 @@ import pytest import xarray as xr -from pangeo_forge_recipes.patterns import pattern_from_file_sequence from pangeo_forge_recipes.storage import FSSpecTarget, MetadataTarget pytest.importorskip("fsspec_reference_maker") @@ -14,14 +13,10 @@ @pytest.mark.parametrize("with_intake", [True, False]) -def test_single(netcdf_local_paths, tmpdir, with_intake): - full_paths, items_per_file, fnames_by_variable = netcdf_local_paths[:3] - if fnames_by_variable: - pytest.skip("This does not test merging operations.") - path = str(full_paths[0]) +def test_single(netcdf_local_file_pattern_sequential, tmpdir, with_intake): + file_pattern = netcdf_local_file_pattern_sequential + path = list(file_pattern.items())[0][1] expected = xr.open_dataset(path, engine="h5netcdf") - - file_pattern = pattern_from_file_sequence([path], "time") recipe = HDFReferenceRecipe(file_pattern) # make sure assigning storage later works @@ -48,14 +43,10 @@ def test_single(netcdf_local_paths, tmpdir, with_intake): @pytest.mark.parametrize("with_intake", [True, False]) -def test_multi(netcdf_local_paths, tmpdir, with_intake): - full_paths, items_per_file, fnames_by_variable = netcdf_local_paths[:3] - if fnames_by_variable: - pytest.skip("This does not test merging operations.") - paths = [str(f) for f in full_paths] +def test_multi(netcdf_local_file_pattern_sequential, tmpdir, with_intake): + file_pattern = netcdf_local_file_pattern_sequential + paths = [f for _, f in list(file_pattern.items())] expected = xr.open_mfdataset(paths, engine="h5netcdf") - - file_pattern = pattern_from_file_sequence(paths, "time") recipe = HDFReferenceRecipe(file_pattern) # make sure assigning storage later works From 725f173ebb0bdf68cf942ca168afada93bcdbcaf Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 1 Sep 2021 18:14:49 -0700 Subject: [PATCH 098/102] refactor xarray_zarr recipe fixtures using make_netCDFtoZarr_recipe function --- tests/test_recipes.py | 52 +++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index f60095ba..3fa3deae 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -13,48 +13,56 @@ from pangeo_forge_recipes.recipes.xarray_zarr import XarrayZarrRecipe -@pytest.fixture -def netCDFtoZarr_recipe( - daily_xarray_dataset, netcdf_local_file_pattern, tmp_target, tmp_cache, tmp_metadata_target +def make_netCDFtoZarr_recipe( + file_pattern, xarray_dataset, target, cache, metadata_target, extra_kwargs=None ): kwargs = dict( inputs_per_chunk=1, - target=tmp_target, - input_cache=tmp_cache, - metadata_cache=tmp_metadata_target, + target=target, + input_cache=cache, + metadata_cache=metadata_target, + ) + if extra_kwargs: + kwargs.update(extra_kwargs) + return XarrayZarrRecipe, file_pattern, kwargs, xarray_dataset, target + + +@pytest.fixture +def netCDFtoZarr_recipe( + netcdf_local_file_pattern, daily_xarray_dataset, tmp_target, tmp_cache, tmp_metadata_target +): + return make_netCDFtoZarr_recipe( + netcdf_local_file_pattern, daily_xarray_dataset, tmp_target, tmp_cache, tmp_metadata_target ) - return XarrayZarrRecipe, netcdf_local_file_pattern, kwargs, daily_xarray_dataset, tmp_target @pytest.fixture def netCDFtoZarr_http_recipe( - daily_xarray_dataset, netcdf_http_file_pattern, tmp_target, tmp_cache, tmp_metadata_target + netcdf_http_file_pattern, daily_xarray_dataset, tmp_target, tmp_cache, tmp_metadata_target ): - kwargs = dict( - inputs_per_chunk=1, - target=tmp_target, - input_cache=tmp_cache, - metadata_cache=tmp_metadata_target, + return make_netCDFtoZarr_recipe( + netcdf_http_file_pattern, daily_xarray_dataset, tmp_target, tmp_cache, tmp_metadata_target ) - return XarrayZarrRecipe, netcdf_http_file_pattern, kwargs, daily_xarray_dataset, tmp_target @pytest.fixture def netCDFtoZarr_subset_recipe( - daily_xarray_dataset, netcdf_local_file_pattern, tmp_target, tmp_cache, tmp_metadata_target + netcdf_local_file_pattern, daily_xarray_dataset, tmp_target, tmp_cache, tmp_metadata_target ): items_per_file = netcdf_local_file_pattern.nitems_per_input.get("time", None) if items_per_file != 2: pytest.skip("This recipe only makes sense with items_per_file == 2.") - kwargs = dict( - subset_inputs={"time": 2}, - inputs_per_chunk=1, - target=tmp_target, - input_cache=tmp_cache, - metadata_cache=tmp_metadata_target, + extra_kwargs = dict(subset_inputs={"time": 2}) + + return make_netCDFtoZarr_recipe( + netcdf_local_file_pattern, + daily_xarray_dataset, + tmp_target, + tmp_cache, + tmp_metadata_target, + extra_kwargs, ) - return XarrayZarrRecipe, netcdf_local_file_pattern, kwargs, daily_xarray_dataset, tmp_target all_recipes = [ From 5b11533304f63c359bdbcd46032c09338291b7d1 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 1 Sep 2021 18:40:40 -0700 Subject: [PATCH 099/102] fix typo in conftest.py --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index a1724881..5ba5dede 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -195,7 +195,7 @@ def netcdf_local_file_pattern_sequential_multi_variable( ], ) def netcdf_local_file_pattern(request): - return make_file_pattern(request.param) + return request.param @pytest.fixture(scope="session") From a59fbd744d69fe309c7c8351e50dfe6895d94ffb Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 1 Sep 2021 18:44:43 -0700 Subject: [PATCH 100/102] add sequential only recipe for test_lock_timeout --- tests/test_recipes.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 3fa3deae..bf6dad2f 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -27,6 +27,15 @@ def make_netCDFtoZarr_recipe( return XarrayZarrRecipe, file_pattern, kwargs, xarray_dataset, target +@pytest.fixture +def netCDFtoZarr_recipe_sequential_only( + netcdf_local_file_pattern_sequential, daily_xarray_dataset, tmp_target, tmp_cache, tmp_metadata_target +): + return make_netCDFtoZarr_recipe( + netcdf_local_file_pattern_sequential, daily_xarray_dataset, tmp_target, tmp_cache, tmp_metadata_target + ) + + @pytest.fixture def netCDFtoZarr_recipe( netcdf_local_file_pattern, daily_xarray_dataset, tmp_target, tmp_cache, tmp_metadata_target @@ -259,12 +268,8 @@ def test_chunks( xr.testing.assert_identical(ds_actual, ds_expected) -def test_lock_timeout(netCDFtoZarr_recipe, execute_recipe): - RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_recipe - - # `netCDFtoZarr_recipe` fixture is parametrized. We don't need to run this test more than once. - if len(file_pattern.merge_dims) != 0: - pytest.skip("It's redundant to run this test more than once.") +def test_lock_timeout(netCDFtoZarr_recipe_sequential_only, execute_recipe): + RecipeClass, file_pattern, kwargs, ds_expected, target = netCDFtoZarr_recipe_sequential_only recipe = RecipeClass(file_pattern=file_pattern, lock_timeout=1, **kwargs) From 9578870f2ec4f8732a16de6fe25ab8f3e07b67e4 Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 1 Sep 2021 18:48:09 -0700 Subject: [PATCH 101/102] lint --- tests/conftest.py | 2 +- tests/test_recipes.py | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5ba5dede..3f9b05d4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -182,7 +182,7 @@ def netcdf_local_file_pattern_sequential(netcdf_local_paths_sequential): @pytest.fixture(scope="session") def netcdf_local_file_pattern_sequential_multi_variable( - netcdf_local_paths_sequential_multi_variable + netcdf_local_paths_sequential_multi_variable, ): return make_file_pattern(netcdf_local_paths_sequential_multi_variable) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index bf6dad2f..f1c215fd 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -17,10 +17,7 @@ def make_netCDFtoZarr_recipe( file_pattern, xarray_dataset, target, cache, metadata_target, extra_kwargs=None ): kwargs = dict( - inputs_per_chunk=1, - target=target, - input_cache=cache, - metadata_cache=metadata_target, + inputs_per_chunk=1, target=target, input_cache=cache, metadata_cache=metadata_target, ) if extra_kwargs: kwargs.update(extra_kwargs) @@ -29,10 +26,18 @@ def make_netCDFtoZarr_recipe( @pytest.fixture def netCDFtoZarr_recipe_sequential_only( - netcdf_local_file_pattern_sequential, daily_xarray_dataset, tmp_target, tmp_cache, tmp_metadata_target + netcdf_local_file_pattern_sequential, + daily_xarray_dataset, + tmp_target, + tmp_cache, + tmp_metadata_target, ): return make_netCDFtoZarr_recipe( - netcdf_local_file_pattern_sequential, daily_xarray_dataset, tmp_target, tmp_cache, tmp_metadata_target + netcdf_local_file_pattern_sequential, + daily_xarray_dataset, + tmp_target, + tmp_cache, + tmp_metadata_target, ) From 6946ca1656ced9b9a44c18f78211ce9a1dd8c4ab Mon Sep 17 00:00:00 2001 From: cisaacstern <62192187+cisaacstern@users.noreply.github.com> Date: Wed, 1 Sep 2021 18:49:15 -0700 Subject: [PATCH 102/102] lint 2 --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index 3f9b05d4..ed50440d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,7 @@ import xarray as xr from dask.distributed import Client, LocalCluster from prefect.executors import DaskExecutor + # need to import this way (rather than use pytest.lazy_fixture) to make it work with dask from pytest_lazyfixture import lazy_fixture