freva-client 2411.0.0__tar.gz → 2502.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of freva-client might be problematic. Click here for more details.
- {freva_client-2411.0.0 → freva_client-2502.0.0}/PKG-INFO +4 -3
- {freva_client-2411.0.0 → freva_client-2502.0.0}/pyproject.toml +3 -2
- {freva_client-2411.0.0 → freva_client-2502.0.0}/src/freva_client/__init__.py +1 -1
- {freva_client-2411.0.0 → freva_client-2502.0.0}/src/freva_client/cli/auth_cli.py +1 -0
- {freva_client-2411.0.0 → freva_client-2502.0.0}/src/freva_client/cli/cli_app.py +1 -0
- {freva_client-2411.0.0 → freva_client-2502.0.0}/src/freva_client/cli/cli_utils.py +2 -1
- {freva_client-2411.0.0 → freva_client-2502.0.0}/src/freva_client/cli/databrowser_cli.py +28 -17
- {freva_client-2411.0.0 → freva_client-2502.0.0}/src/freva_client/query.py +38 -17
- {freva_client-2411.0.0 → freva_client-2502.0.0}/src/freva_client/utils/databrowser_utils.py +57 -26
- {freva_client-2411.0.0 → freva_client-2502.0.0}/MANIFEST.in +0 -0
- {freva_client-2411.0.0 → freva_client-2502.0.0}/README.md +0 -0
- {freva_client-2411.0.0 → freva_client-2502.0.0}/assets/share/freva/freva.toml +0 -0
- {freva_client-2411.0.0 → freva_client-2502.0.0}/src/freva_client/__main__.py +0 -0
- {freva_client-2411.0.0 → freva_client-2502.0.0}/src/freva_client/auth.py +0 -0
- {freva_client-2411.0.0 → freva_client-2502.0.0}/src/freva_client/cli/__init__.py +0 -0
- {freva_client-2411.0.0 → freva_client-2502.0.0}/src/freva_client/cli/cli_parser.py +0 -0
- {freva_client-2411.0.0 → freva_client-2502.0.0}/src/freva_client/py.typed +0 -0
- {freva_client-2411.0.0 → freva_client-2502.0.0}/src/freva_client/utils/__init__.py +0 -0
- {freva_client-2411.0.0 → freva_client-2502.0.0}/src/freva_client/utils/logger.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: freva-client
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2502.0.0
|
|
4
4
|
Summary: Search for climate data based on key-value pairs
|
|
5
5
|
Author-email: "DKRZ, Clint" <freva@dkrz.de>
|
|
6
6
|
Requires-Python: >=3.8
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
|
-
Classifier: Development Status ::
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
9
|
Classifier: Environment :: Console
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
20
|
Requires-Dist: appdirs
|
|
20
21
|
Requires-Dist: pyyaml
|
|
21
22
|
Requires-Dist: authlib
|
|
@@ -8,7 +8,7 @@ description = "Search for climate data based on key-value pairs"
|
|
|
8
8
|
authors = [{name = "DKRZ, Clint", email = "freva@dkrz.de"}]
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
classifiers = [
|
|
11
|
-
"Development Status ::
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
12
|
"Environment :: Console",
|
|
13
13
|
"Intended Audience :: Developers",
|
|
14
14
|
"Intended Audience :: Science/Research",
|
|
@@ -19,6 +19,7 @@ classifiers = [
|
|
|
19
19
|
"Programming Language :: Python :: 3.10",
|
|
20
20
|
"Programming Language :: Python :: 3.11",
|
|
21
21
|
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
22
23
|
]
|
|
23
24
|
requires-python = ">=3.8"
|
|
24
25
|
dependencies = [
|
|
@@ -48,4 +49,4 @@ include = ["assets/*"]
|
|
|
48
49
|
[tool.flit.external-data]
|
|
49
50
|
directory = "assets"
|
|
50
51
|
[package-data]
|
|
51
|
-
|
|
52
|
+
freva_client = ["py.typed"]
|
|
@@ -11,6 +11,7 @@ from typing import Dict, List, Literal, Optional, Union, cast
|
|
|
11
11
|
|
|
12
12
|
import typer
|
|
13
13
|
import xarray as xr
|
|
14
|
+
|
|
14
15
|
from freva_client import databrowser
|
|
15
16
|
from freva_client.auth import Auth
|
|
16
17
|
from freva_client.utils import exception_handler, logger
|
|
@@ -31,27 +32,27 @@ def _auth(url: str, token: Optional[str]) -> None:
|
|
|
31
32
|
class UniqKeys(str, Enum):
|
|
32
33
|
"""Literal implementation for the cli."""
|
|
33
34
|
|
|
34
|
-
file
|
|
35
|
-
uri
|
|
35
|
+
file = "file"
|
|
36
|
+
uri = "uri"
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
class Flavours(str, Enum):
|
|
39
40
|
"""Literal implementation for the cli."""
|
|
40
41
|
|
|
41
|
-
freva
|
|
42
|
-
cmip6
|
|
43
|
-
cmip5
|
|
44
|
-
cordex
|
|
45
|
-
nextgems
|
|
46
|
-
user
|
|
42
|
+
freva = "freva"
|
|
43
|
+
cmip6 = "cmip6"
|
|
44
|
+
cmip5 = "cmip5"
|
|
45
|
+
cordex = "cordex"
|
|
46
|
+
nextgems = "nextgems"
|
|
47
|
+
user = "user"
|
|
47
48
|
|
|
48
49
|
|
|
49
50
|
class TimeSelect(str, Enum):
|
|
50
51
|
"""Literal implementation for the cli."""
|
|
51
52
|
|
|
52
|
-
strict
|
|
53
|
-
flexible
|
|
54
|
-
file
|
|
53
|
+
strict = "strict"
|
|
54
|
+
flexible = "flexible"
|
|
55
|
+
file = "file"
|
|
55
56
|
|
|
56
57
|
@staticmethod
|
|
57
58
|
def get_help() -> str:
|
|
@@ -187,7 +188,9 @@ def metadata_search(
|
|
|
187
188
|
result = databrowser.metadata_search(
|
|
188
189
|
*(facets or []),
|
|
189
190
|
time=time or "",
|
|
190
|
-
time_select=cast(
|
|
191
|
+
time_select=cast(
|
|
192
|
+
Literal["file", "flexible", "strict"], time_select.value
|
|
193
|
+
),
|
|
191
194
|
flavour=cast(
|
|
192
195
|
Literal["freva", "cmip6", "cmip5", "cordex", "nextgems", "user"],
|
|
193
196
|
flavour.value,
|
|
@@ -461,7 +464,9 @@ def intake_catalogue(
|
|
|
461
464
|
print(Path(temp_f.name).read_text())
|
|
462
465
|
|
|
463
466
|
|
|
464
|
-
@databrowser_app.command(
|
|
467
|
+
@databrowser_app.command(
|
|
468
|
+
name="data-count", help="Count the databrowser search results"
|
|
469
|
+
)
|
|
465
470
|
@exception_handler
|
|
466
471
|
def count_values(
|
|
467
472
|
search_keys: Optional[List[str]] = typer.Argument(
|
|
@@ -580,9 +585,13 @@ def count_values(
|
|
|
580
585
|
databrowser(
|
|
581
586
|
*facets,
|
|
582
587
|
time=time or "",
|
|
583
|
-
time_select=cast(
|
|
588
|
+
time_select=cast(
|
|
589
|
+
Literal["file", "flexible", "strict"], time_select
|
|
590
|
+
),
|
|
584
591
|
flavour=cast(
|
|
585
|
-
Literal[
|
|
592
|
+
Literal[
|
|
593
|
+
"freva", "cmip6", "cmip5", "cordex", "nextgems", "user"
|
|
594
|
+
],
|
|
586
595
|
flavour.value,
|
|
587
596
|
),
|
|
588
597
|
host=host,
|
|
@@ -662,11 +671,13 @@ def user_data_add(
|
|
|
662
671
|
action="add",
|
|
663
672
|
userdata_items=cast(List[Union[str, xr.Dataset]], paths),
|
|
664
673
|
metadata=facet_dict,
|
|
665
|
-
host=host
|
|
674
|
+
host=host,
|
|
666
675
|
)
|
|
667
676
|
|
|
668
677
|
|
|
669
|
-
@user_data_app.command(
|
|
678
|
+
@user_data_app.command(
|
|
679
|
+
name="delete", help="Delete user data from the databrowser."
|
|
680
|
+
)
|
|
670
681
|
@exception_handler
|
|
671
682
|
def user_data_delete(
|
|
672
683
|
search_keys: List[str] = typer.Option(
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Query climate data sets by using-key value pair search queries."""
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
import sys
|
|
5
4
|
from collections import defaultdict
|
|
6
5
|
from fnmatch import fnmatch
|
|
@@ -246,7 +245,8 @@ class databrowser:
|
|
|
246
245
|
self, facets: Tuple[str, ...], search_kw: Dict[str, List[str]]
|
|
247
246
|
) -> None:
|
|
248
247
|
metadata = {
|
|
249
|
-
k: v[::2]
|
|
248
|
+
k: v[::2]
|
|
249
|
+
for (k, v) in self._facet_search(extended_search=True).items()
|
|
250
250
|
}
|
|
251
251
|
primary_key = list(metadata.keys() or ["project"])[0]
|
|
252
252
|
num_facets = 0
|
|
@@ -304,7 +304,9 @@ class databrowser:
|
|
|
304
304
|
|
|
305
305
|
# Create a table-like structure for available flavors and search facets
|
|
306
306
|
style = 'style="text-align: left"'
|
|
307
|
-
facet_heading =
|
|
307
|
+
facet_heading = (
|
|
308
|
+
f"Available search facets for <em>{self._flavour}</em> flavour"
|
|
309
|
+
)
|
|
308
310
|
html_repr = (
|
|
309
311
|
"<table>"
|
|
310
312
|
f"<tr><th colspan='2' {style}>{self.__class__.__name__}"
|
|
@@ -345,7 +347,9 @@ class databrowser:
|
|
|
345
347
|
if self._stream_zarr:
|
|
346
348
|
token = self._auth.check_authentication(auth_url=self._cfg.auth_url)
|
|
347
349
|
url = self._cfg.zarr_loader_url
|
|
348
|
-
kwargs["headers"] = {
|
|
350
|
+
kwargs["headers"] = {
|
|
351
|
+
"Authorization": f"Bearer {token['access_token']}"
|
|
352
|
+
}
|
|
349
353
|
kwargs["params"] = {"catalogue-type": "intake"}
|
|
350
354
|
result = self._request("GET", url, **kwargs)
|
|
351
355
|
if result is None:
|
|
@@ -357,7 +361,9 @@ class databrowser:
|
|
|
357
361
|
for content in result.iter_content(decode_unicode=False):
|
|
358
362
|
stream.write(content)
|
|
359
363
|
except Exception as error:
|
|
360
|
-
raise ValueError(
|
|
364
|
+
raise ValueError(
|
|
365
|
+
f"Couldn't write catalogue content: {error}"
|
|
366
|
+
) from None
|
|
361
367
|
|
|
362
368
|
def intake_catalogue(self) -> intake_esm.core.esm_datastore:
|
|
363
369
|
"""Create an intake esm catalogue object from the search.
|
|
@@ -388,7 +394,10 @@ class databrowser:
|
|
|
388
394
|
"""
|
|
389
395
|
with NamedTemporaryFile(suffix=".json") as temp_f:
|
|
390
396
|
self._create_intake_catalogue_file(temp_f.name)
|
|
391
|
-
return
|
|
397
|
+
return cast(
|
|
398
|
+
intake_esm.core.esm_datastore,
|
|
399
|
+
intake.open_esm_datastore(temp_f.name),
|
|
400
|
+
)
|
|
392
401
|
|
|
393
402
|
@classmethod
|
|
394
403
|
def count_values(
|
|
@@ -495,7 +504,9 @@ class databrowser:
|
|
|
495
504
|
result = this._facet_search(extended_search=extended_search)
|
|
496
505
|
counts = {}
|
|
497
506
|
for facet, value_counts in result.items():
|
|
498
|
-
counts[facet] = dict(
|
|
507
|
+
counts[facet] = dict(
|
|
508
|
+
zip(value_counts[::2], map(int, value_counts[1::2]))
|
|
509
|
+
)
|
|
499
510
|
return counts
|
|
500
511
|
|
|
501
512
|
@cached_property
|
|
@@ -520,7 +531,8 @@ class databrowser:
|
|
|
520
531
|
|
|
521
532
|
"""
|
|
522
533
|
return {
|
|
523
|
-
k: v[::2]
|
|
534
|
+
k: v[::2]
|
|
535
|
+
for (k, v) in self._facet_search(extended_search=True).items()
|
|
524
536
|
}
|
|
525
537
|
|
|
526
538
|
@classmethod
|
|
@@ -653,7 +665,9 @@ class databrowser:
|
|
|
653
665
|
)
|
|
654
666
|
return {
|
|
655
667
|
k: v[::2]
|
|
656
|
-
for (k, v) in this._facet_search(
|
|
668
|
+
for (k, v) in this._facet_search(
|
|
669
|
+
extended_search=extended_search
|
|
670
|
+
).items()
|
|
657
671
|
}
|
|
658
672
|
|
|
659
673
|
@classmethod
|
|
@@ -782,11 +796,11 @@ class databrowser:
|
|
|
782
796
|
import xarray as xr
|
|
783
797
|
token_info = authenticate(username="janedoe")
|
|
784
798
|
filenames = (
|
|
785
|
-
"../freva-rest/src/databrowser_api/mock/data/model/regional/cordex/output/EUR-11/"
|
|
799
|
+
"../freva-rest/src/freva_rest/databrowser_api/mock/data/model/regional/cordex/output/EUR-11/"
|
|
786
800
|
"GERICS/NCC-NorESM1-M/rcp85/r1i1p1/GERICS-REMO2015/v1/3hr/pr/v20181212/*.nc"
|
|
787
801
|
)
|
|
788
802
|
filename1 = (
|
|
789
|
-
"../freva-rest/src/databrowser_api/mock/data/model/regional/cordex/output/EUR-11/"
|
|
803
|
+
"../freva-rest/src/freva_rest/databrowser_api/mock/data/model/regional/cordex/output/EUR-11/"
|
|
790
804
|
"CLMcom/MPI-M-MPI-ESM-LR/historical/r0i0p0/CLMcom-CCLM4-8-17/v1/fx/orog/v20140515/"
|
|
791
805
|
"orog_EUR-11_MPI-M-MPI-ESM-LR_historical_r1i1p1_CLMcom-CCLM4-8-17_v1_fx.nc"
|
|
792
806
|
)
|
|
@@ -856,7 +870,7 @@ class databrowser:
|
|
|
856
870
|
method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
857
871
|
url: str,
|
|
858
872
|
data: Optional[Dict[str, Any]] = None,
|
|
859
|
-
**kwargs: Any
|
|
873
|
+
**kwargs: Any,
|
|
860
874
|
) -> Optional[requests.models.Response]:
|
|
861
875
|
"""Request method to handle CRUD operations (GET, POST, PUT, PATCH, DELETE)."""
|
|
862
876
|
method_upper = method.upper()
|
|
@@ -864,8 +878,13 @@ class databrowser:
|
|
|
864
878
|
params = kwargs.pop("params", {})
|
|
865
879
|
stream = kwargs.pop("stream", False)
|
|
866
880
|
|
|
867
|
-
logger.debug(
|
|
868
|
-
|
|
881
|
+
logger.debug(
|
|
882
|
+
"%s request to %s with data: %s and parameters: %s",
|
|
883
|
+
method_upper,
|
|
884
|
+
url,
|
|
885
|
+
data,
|
|
886
|
+
{**self._params, **params},
|
|
887
|
+
)
|
|
869
888
|
|
|
870
889
|
try:
|
|
871
890
|
req = requests.Request(
|
|
@@ -873,7 +892,7 @@ class databrowser:
|
|
|
873
892
|
url=url,
|
|
874
893
|
params={**self._params, **params},
|
|
875
894
|
json=None if method_upper in "GET" else data,
|
|
876
|
-
**kwargs
|
|
895
|
+
**kwargs,
|
|
877
896
|
)
|
|
878
897
|
with requests.Session() as session:
|
|
879
898
|
prepared = session.prepare_request(req)
|
|
@@ -883,8 +902,10 @@ class databrowser:
|
|
|
883
902
|
|
|
884
903
|
except KeyboardInterrupt:
|
|
885
904
|
pprint("[red][b]User interrupt: Exit[/red][/b]", file=sys.stderr)
|
|
886
|
-
except (
|
|
887
|
-
|
|
905
|
+
except (
|
|
906
|
+
requests.exceptions.ConnectionError,
|
|
907
|
+
requests.exceptions.HTTPError,
|
|
908
|
+
) as error:
|
|
888
909
|
msg = f"{method_upper} request failed with {error}"
|
|
889
910
|
if self._fail_on_error:
|
|
890
911
|
raise ValueError(msg) from None
|
|
@@ -53,7 +53,7 @@ class Config:
|
|
|
53
53
|
ini_parser.read_string(path.read_text())
|
|
54
54
|
config = ini_parser["evaluation_system"]
|
|
55
55
|
scheme, host = self._split_url(
|
|
56
|
-
config.get("databrowser.host"
|
|
56
|
+
config.get("databrowser.host", config.get("solr.host", ""))
|
|
57
57
|
)
|
|
58
58
|
host, _, port = (host or "").partition(":")
|
|
59
59
|
port = port or config.get("databrowser.port", "")
|
|
@@ -88,7 +88,9 @@ class Config:
|
|
|
88
88
|
try:
|
|
89
89
|
res = requests.get(f"{self.databrowser_url}/overview", timeout=15)
|
|
90
90
|
except requests.exceptions.ConnectionError:
|
|
91
|
-
raise ValueError(
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"Could not connect to {self.databrowser_url}"
|
|
93
|
+
) from None
|
|
92
94
|
return cast(Dict[str, Any], res.json())
|
|
93
95
|
|
|
94
96
|
def _get_databrowser_host_from_config(self) -> str:
|
|
@@ -103,7 +105,9 @@ class Config:
|
|
|
103
105
|
Path(appdirs.user_config_dir("freva")) / "freva.toml": "toml",
|
|
104
106
|
Path(self.get_dirs(user=True)) / "freva.toml": "toml",
|
|
105
107
|
freva_config: "toml",
|
|
106
|
-
Path(
|
|
108
|
+
Path(
|
|
109
|
+
os.environ.get("EVALUATION_SYSTEM_CONFIG_FILE") or eval_conf
|
|
110
|
+
): "ini",
|
|
107
111
|
}
|
|
108
112
|
for config_path, config_type in paths.items():
|
|
109
113
|
if config_path.is_file():
|
|
@@ -130,7 +134,9 @@ class Config:
|
|
|
130
134
|
@property
|
|
131
135
|
def search_url(self) -> str:
|
|
132
136
|
"""Define the data search endpoint."""
|
|
133
|
-
return
|
|
137
|
+
return (
|
|
138
|
+
f"{self.databrowser_url}/data-search/{self.flavour}/{self.uniq_key}"
|
|
139
|
+
)
|
|
134
140
|
|
|
135
141
|
@property
|
|
136
142
|
def zarr_loader_url(self) -> str:
|
|
@@ -140,13 +146,14 @@ class Config:
|
|
|
140
146
|
@property
|
|
141
147
|
def intake_url(self) -> str:
|
|
142
148
|
"""Define the url for creating intake catalogues."""
|
|
143
|
-
return f"{self.databrowser_url}/
|
|
149
|
+
return f"{self.databrowser_url}/intake-catalogue/{self.flavour}/{self.uniq_key}"
|
|
144
150
|
|
|
145
151
|
@property
|
|
146
152
|
def metadata_url(self) -> str:
|
|
147
153
|
"""Define the endpoint for the metadata search."""
|
|
148
154
|
return (
|
|
149
|
-
f"{self.databrowser_url}/
|
|
155
|
+
f"{self.databrowser_url}/metadata-search/"
|
|
156
|
+
f"{self.flavour}/{self.uniq_key}"
|
|
150
157
|
)
|
|
151
158
|
|
|
152
159
|
@staticmethod
|
|
@@ -166,7 +173,7 @@ class Config:
|
|
|
166
173
|
if port:
|
|
167
174
|
hostname = f"{hostname}:{port}"
|
|
168
175
|
hostname = hostname.partition("/")[0]
|
|
169
|
-
return f"{scheme}://{hostname}/api"
|
|
176
|
+
return f"{scheme}://{hostname}/api/freva-nextgen"
|
|
170
177
|
|
|
171
178
|
@staticmethod
|
|
172
179
|
def get_dirs(user: bool = True) -> Path:
|
|
@@ -199,9 +206,12 @@ class UserDataHandler:
|
|
|
199
206
|
This class is used for processing user data and extracting metadata
|
|
200
207
|
from the data files.
|
|
201
208
|
"""
|
|
209
|
+
|
|
202
210
|
def __init__(self, userdata_items: List[Union[str, xr.Dataset]]) -> None:
|
|
203
211
|
self._suffixes = [".nc", ".nc4", ".grb", ".grib", ".zarr", "zar"]
|
|
204
|
-
self.user_metadata: List[
|
|
212
|
+
self.user_metadata: List[
|
|
213
|
+
Dict[str, Union[str, List[str], Dict[str, str]]]
|
|
214
|
+
] = []
|
|
205
215
|
self._metadata_collection: List[Dict[str, Union[str, List[str]]]] = []
|
|
206
216
|
try:
|
|
207
217
|
self._executor = concurrent.futures.ThreadPoolExecutor(
|
|
@@ -238,28 +248,37 @@ class UserDataHandler:
|
|
|
238
248
|
validated_xarray_datasets.append(data)
|
|
239
249
|
|
|
240
250
|
if not validated_paths and not validated_xarray_datasets:
|
|
241
|
-
raise FileNotFoundError(
|
|
251
|
+
raise FileNotFoundError(
|
|
252
|
+
"No valid file paths or xarray datasets found."
|
|
253
|
+
)
|
|
242
254
|
return {
|
|
243
255
|
"validated_user_paths": validated_paths,
|
|
244
256
|
"validated_user_xrdatasets": validated_xarray_datasets,
|
|
245
257
|
}
|
|
246
258
|
|
|
247
|
-
def _process_user_data(
|
|
248
|
-
|
|
259
|
+
def _process_user_data(
|
|
260
|
+
self,
|
|
261
|
+
userdata_items: List[Union[str, xr.Dataset]],
|
|
262
|
+
) -> None:
|
|
249
263
|
"""Process xarray datasets and file paths using thread pool."""
|
|
250
264
|
futures = []
|
|
251
|
-
validated_userdata: Dict[str, Union[List[Path], List[xr.Dataset]]] =
|
|
265
|
+
validated_userdata: Dict[str, Union[List[Path], List[xr.Dataset]]] = (
|
|
252
266
|
self._validate_user_data(userdata_items)
|
|
267
|
+
)
|
|
253
268
|
if validated_userdata["validated_user_xrdatasets"]:
|
|
254
269
|
futures.append(
|
|
255
|
-
self._executor.submit(
|
|
256
|
-
|
|
270
|
+
self._executor.submit(
|
|
271
|
+
self._process_userdata_in_executor,
|
|
272
|
+
validated_userdata["validated_user_xrdatasets"],
|
|
273
|
+
)
|
|
257
274
|
)
|
|
258
275
|
|
|
259
276
|
if validated_userdata["validated_user_paths"]:
|
|
260
277
|
futures.append(
|
|
261
|
-
self._executor.submit(
|
|
262
|
-
|
|
278
|
+
self._executor.submit(
|
|
279
|
+
self._process_userdata_in_executor,
|
|
280
|
+
validated_userdata["validated_user_paths"],
|
|
281
|
+
)
|
|
263
282
|
)
|
|
264
283
|
for future in futures:
|
|
265
284
|
try:
|
|
@@ -309,11 +328,11 @@ class UserDataHandler:
|
|
|
309
328
|
|
|
310
329
|
try:
|
|
311
330
|
dset = (
|
|
312
|
-
path
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
331
|
+
path
|
|
332
|
+
if isinstance(path, xr.Dataset)
|
|
333
|
+
else xr.open_mfdataset(
|
|
334
|
+
str(path), parallel=False, use_cftime=True, lock=False
|
|
335
|
+
)
|
|
317
336
|
)
|
|
318
337
|
time_freq = dset.attrs.get("frequency", "")
|
|
319
338
|
data_vars = list(map(str, dset.data_vars))
|
|
@@ -328,16 +347,26 @@ class UserDataHandler:
|
|
|
328
347
|
return {}
|
|
329
348
|
if len(times) > 0:
|
|
330
349
|
try:
|
|
331
|
-
time_str =
|
|
332
|
-
|
|
350
|
+
time_str = (
|
|
351
|
+
f"[{times[0].isoformat()}Z TO {times[-1].isoformat()}Z]"
|
|
352
|
+
)
|
|
353
|
+
dt = (
|
|
354
|
+
abs((times[1] - times[0]).total_seconds())
|
|
355
|
+
if len(times) > 1
|
|
356
|
+
else 0
|
|
357
|
+
)
|
|
333
358
|
except Exception as non_cftime:
|
|
334
|
-
logger.info(
|
|
359
|
+
logger.info(
|
|
360
|
+
"The time var is not based on the cftime: %s", non_cftime
|
|
361
|
+
)
|
|
335
362
|
time_str = (
|
|
336
363
|
f"[{np.datetime_as_string(times[0], unit='s')}Z TO "
|
|
337
364
|
f"{np.datetime_as_string(times[-1], unit='s')}Z]"
|
|
338
365
|
)
|
|
339
366
|
dt = (
|
|
340
|
-
abs(
|
|
367
|
+
abs(
|
|
368
|
+
(times[1] - times[0]).astype("timedelta64[s]").astype(int)
|
|
369
|
+
)
|
|
341
370
|
if len(times) > 1
|
|
342
371
|
else 0
|
|
343
372
|
)
|
|
@@ -357,7 +386,9 @@ class UserDataHandler:
|
|
|
357
386
|
|
|
358
387
|
_data: Dict[str, Union[str, List[str], Dict[str, str]]] = {}
|
|
359
388
|
_data.setdefault("variable", variables[0])
|
|
360
|
-
_data.setdefault(
|
|
389
|
+
_data.setdefault(
|
|
390
|
+
"time_frequency", self._get_time_frequency(dt, time_freq)
|
|
391
|
+
)
|
|
361
392
|
_data["time"] = time_str
|
|
362
393
|
_data.setdefault("cmor_table", _data["time_frequency"])
|
|
363
394
|
_data.setdefault("version", "")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|