freva-client 2411.0.0__tar.gz → 2505.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.
Files changed (19) hide show
  1. {freva_client-2411.0.0 → freva_client-2505.0.0}/PKG-INFO +4 -3
  2. {freva_client-2411.0.0 → freva_client-2505.0.0}/pyproject.toml +3 -2
  3. {freva_client-2411.0.0 → freva_client-2505.0.0}/src/freva_client/__init__.py +1 -1
  4. {freva_client-2411.0.0 → freva_client-2505.0.0}/src/freva_client/cli/auth_cli.py +1 -0
  5. {freva_client-2411.0.0 → freva_client-2505.0.0}/src/freva_client/cli/cli_app.py +1 -0
  6. {freva_client-2411.0.0 → freva_client-2505.0.0}/src/freva_client/cli/cli_parser.py +2 -0
  7. {freva_client-2411.0.0 → freva_client-2505.0.0}/src/freva_client/cli/cli_utils.py +2 -1
  8. {freva_client-2411.0.0 → freva_client-2505.0.0}/src/freva_client/cli/databrowser_cli.py +287 -34
  9. {freva_client-2411.0.0 → freva_client-2505.0.0}/src/freva_client/query.py +204 -25
  10. {freva_client-2411.0.0 → freva_client-2505.0.0}/src/freva_client/utils/databrowser_utils.py +62 -26
  11. {freva_client-2411.0.0 → freva_client-2505.0.0}/MANIFEST.in +0 -0
  12. {freva_client-2411.0.0 → freva_client-2505.0.0}/README.md +0 -0
  13. {freva_client-2411.0.0 → freva_client-2505.0.0}/assets/share/freva/freva.toml +0 -0
  14. {freva_client-2411.0.0 → freva_client-2505.0.0}/src/freva_client/__main__.py +0 -0
  15. {freva_client-2411.0.0 → freva_client-2505.0.0}/src/freva_client/auth.py +0 -0
  16. {freva_client-2411.0.0 → freva_client-2505.0.0}/src/freva_client/cli/__init__.py +0 -0
  17. {freva_client-2411.0.0 → freva_client-2505.0.0}/src/freva_client/py.typed +0 -0
  18. {freva_client-2411.0.0 → freva_client-2505.0.0}/src/freva_client/utils/__init__.py +0 -0
  19. {freva_client-2411.0.0 → freva_client-2505.0.0}/src/freva_client/utils/logger.py +0 -0
@@ -1,11 +1,11 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: freva-client
3
- Version: 2411.0.0
3
+ Version: 2505.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 :: 3 - Alpha
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 :: 3 - Alpha",
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
- freva_deployment = ["py.typed"]
52
+ freva_client = ["py.typed"]
@@ -17,5 +17,5 @@ need to apply data analysis plugins, please visit the
17
17
  from .auth import authenticate
18
18
  from .query import databrowser
19
19
 
20
- __version__ = "2411.0.0"
20
+ __version__ = "2505.0.0"
21
21
  __all__ = ["authenticate", "databrowser", "__version__"]
@@ -5,6 +5,7 @@ from getpass import getuser
5
5
  from typing import Optional
6
6
 
7
7
  import typer
8
+
8
9
  from freva_client import authenticate
9
10
  from freva_client.utils import exception_handler, logger
10
11
 
@@ -4,6 +4,7 @@ import os
4
4
  from typing import Optional
5
5
 
6
6
  import typer
7
+
7
8
  from freva_client.utils import logger
8
9
 
9
10
  from .auth_cli import authenticate_cli
@@ -69,6 +69,8 @@ class Completer:
69
69
  time=None,
70
70
  host=None,
71
71
  time_select="flexible",
72
+ bbox=None,
73
+ bbox_select="flexible",
72
74
  multiversion=False,
73
75
  extended_search=True,
74
76
  fail_on_error=False,
@@ -3,9 +3,10 @@
3
3
  from typing import Dict, List
4
4
 
5
5
  import typer
6
+ from rich import print as pprint
7
+
6
8
  from freva_client import __version__
7
9
  from freva_client.utils import logger
8
- from rich import print as pprint
9
10
 
10
11
  APP_NAME: str = "freva-client"
11
12
 
@@ -7,10 +7,11 @@ import json
7
7
  from enum import Enum
8
8
  from pathlib import Path
9
9
  from tempfile import NamedTemporaryFile
10
- from typing import Dict, List, Literal, Optional, Union, cast
10
+ from typing import Dict, List, Literal, Optional, Tuple, 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,41 +32,67 @@ 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: str = "file"
35
- uri: str = "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: str = "freva"
42
- cmip6: str = "cmip6"
43
- cmip5: str = "cmip5"
44
- cordex: str = "cordex"
45
- nextgems: str = "nextgems"
46
- user: str = "user"
42
+ freva = "freva"
43
+ cmip6 = "cmip6"
44
+ cmip5 = "cmip5"
45
+ cordex = "cordex"
46
+ nextgems = "nextgems"
47
+ user = "user"
47
48
 
48
49
 
49
- class TimeSelect(str, Enum):
50
+ class SelectMethod(str, Enum):
50
51
  """Literal implementation for the cli."""
51
52
 
52
- strict: str = "strict"
53
- flexible: str = "flexible"
54
- file: str = "file"
53
+ strict = "strict"
54
+ flexible = "flexible"
55
+ file = "file"
55
56
 
56
57
  @staticmethod
57
- def get_help() -> str:
58
- """Generate the help string."""
58
+ def get_help(context: str) -> str:
59
+ """Generate the help string for time or bbox selection methods.
60
+
61
+ Parameters
62
+ ----------
63
+ context: str, default: "time"
64
+ Either "time" or "bbox" to generate appropriate help text.
65
+ """
66
+ examples = {
67
+ "time": ("2000 to 2012", "2010 to 2020"),
68
+ "bbox": ("-10 10 -10 10", "0 5 0 5")
69
+ }
70
+ descriptions = {
71
+ "time": {
72
+ "unit": "time period",
73
+ "start_end": "start or end period",
74
+ "subset": "time period"
75
+ },
76
+ "bbox": {
77
+ "unit": "spatial extent",
78
+ "start_end": "any part of the extent",
79
+ "subset": "spatial extent"
80
+ }
81
+ }
82
+
83
+ context_info = descriptions.get(context, descriptions["time"])
84
+ example = examples.get(context, examples["time"])
85
+
59
86
  return (
60
- "Operator that specifies how the time period is selected. "
87
+ f"Operator that specifies how the {context_info['unit']} is selected. "
61
88
  "Choose from flexible (default), strict or file. "
62
89
  "``strict`` returns only those files that have the *entire* "
63
- "time period covered. The time search ``2000 to 2012`` will "
64
- "not select files containing data from 2010 to 2020 with "
65
- "the ``strict`` method. ``flexible`` will select those files "
66
- "as ``flexible`` returns those files that have either start "
67
- "or end period covered. ``file`` will only return files where "
68
- "the entire time period is contained within *one single* file."
90
+ f"{context_info['unit']} covered. The {context} search ``{example[0]}`` "
91
+ f"will not select files containing data from {example[1]} with "
92
+ "the ``strict`` method. ``flexible`` will select those files as "
93
+ f"``flexible`` returns those files that have {context_info['start_end']} "
94
+ f"covered. ``file`` will only return files where the entire "
95
+ f"{context_info['subset']} is contained within *one single* file."
69
96
  )
70
97
 
71
98
 
@@ -123,11 +150,11 @@ def metadata_search(
123
150
  "of climate datasets to query."
124
151
  ),
125
152
  ),
126
- time_select: TimeSelect = typer.Option(
153
+ time_select: SelectMethod = typer.Option(
127
154
  "flexible",
128
155
  "-ts",
129
156
  "--time-select",
130
- help=TimeSelect.get_help(),
157
+ help=SelectMethod.get_help("time"),
131
158
  ),
132
159
  time: Optional[str] = typer.Option(
133
160
  None,
@@ -143,6 +170,24 @@ def metadata_search(
143
170
  " valid."
144
171
  ),
145
172
  ),
173
+ bbox: Optional[Tuple[float, float, float, float]] = typer.Option(
174
+ None,
175
+ "-b",
176
+ "--bbox",
177
+ help=(
178
+ "Special search facet to refine/subset search results by spatial "
179
+ "extent. This can be a string representation of a bounding box. "
180
+ "The bounding box has to follow the format ``min_lon max_lon "
181
+ "min_lat,max_lat``. Valid strings are ``-10 10 -10 10`` to "
182
+ "``0 5 0 5``."
183
+ ),
184
+ ),
185
+ bbox_select: SelectMethod = typer.Option(
186
+ "flexible",
187
+ "-bs",
188
+ "--bbox-select",
189
+ help=SelectMethod.get_help("bbox"),
190
+ ),
146
191
  extended_search: bool = typer.Option(
147
192
  False,
148
193
  "-e",
@@ -188,6 +233,8 @@ def metadata_search(
188
233
  *(facets or []),
189
234
  time=time or "",
190
235
  time_select=cast(Literal["file", "flexible", "strict"], time_select.value),
236
+ bbox=bbox or None,
237
+ bbox_select=cast(Literal["file", "flexible", "strict"], bbox_select.value),
191
238
  flavour=cast(
192
239
  Literal["freva", "cmip6", "cmip5", "cordex", "nextgems", "user"],
193
240
  flavour.value,
@@ -245,11 +292,11 @@ def data_search(
245
292
  "of climate datasets to query."
246
293
  ),
247
294
  ),
248
- time_select: TimeSelect = typer.Option(
295
+ time_select: SelectMethod = typer.Option(
249
296
  "flexible",
250
297
  "-ts",
251
298
  "--time-select",
252
- help=TimeSelect.get_help(),
299
+ help=SelectMethod.get_help("time"),
253
300
  ),
254
301
  zarr: bool = typer.Option(False, "--zarr", help="Create zarr stream files."),
255
302
  access_token: Optional[str] = typer.Option(
@@ -274,6 +321,24 @@ def data_search(
274
321
  " valid."
275
322
  ),
276
323
  ),
324
+ bbox: Optional[Tuple[float, float, float, float]] = typer.Option(
325
+ None,
326
+ "-b",
327
+ "--bbox",
328
+ help=(
329
+ "Special search facet to refine/subset search results by spatial "
330
+ "extent. This can be a string representation of a bounding box. "
331
+ "The bounding box has to follow the format ``min_lon max_lon "
332
+ "min_lat max_lat``. Valid strings are ``-10 10 -10 10`` to "
333
+ "``0 5 0 5``."
334
+ ),
335
+ ),
336
+ bbox_select: SelectMethod = typer.Option(
337
+ "flexible",
338
+ "-bs",
339
+ "--bbox-select",
340
+ help=SelectMethod.get_help("bbox"),
341
+ ),
277
342
  parse_json: bool = typer.Option(
278
343
  False, "-j", "--json", help="Parse output in json format."
279
344
  ),
@@ -311,6 +376,8 @@ def data_search(
311
376
  *(facets or []),
312
377
  time=time or "",
313
378
  time_select=cast(Literal["file", "flexible", "strict"], time_select),
379
+ bbox=bbox or None,
380
+ bbox_select=cast(Literal["file", "flexible", "strict"], bbox_select),
314
381
  flavour=cast(
315
382
  Literal["freva", "cmip6", "cmip5", "cordex", "nextgems", "user"],
316
383
  flavour.value,
@@ -371,11 +438,11 @@ def intake_catalogue(
371
438
  "of climate datasets to query."
372
439
  ),
373
440
  ),
374
- time_select: TimeSelect = typer.Option(
441
+ time_select: SelectMethod = typer.Option(
375
442
  "flexible",
376
443
  "-ts",
377
444
  "--time-select",
378
- help=TimeSelect.get_help(),
445
+ help=SelectMethod.get_help("time"),
379
446
  ),
380
447
  time: Optional[str] = typer.Option(
381
448
  None,
@@ -391,6 +458,24 @@ def intake_catalogue(
391
458
  " valid."
392
459
  ),
393
460
  ),
461
+ bbox: Optional[Tuple[float, float, float, float]] = typer.Option(
462
+ None,
463
+ "-b",
464
+ "--bbox",
465
+ help=(
466
+ "Special search facet to refine/subset search results by spatial "
467
+ "extent. This can be a string representation of a bounding box. "
468
+ "The bounding box has to follow the format ``min_lon max_lon "
469
+ "min_lat max_lat``. Valid strings are ``-10 10 -10 10`` to "
470
+ "``0 5 0 5``."
471
+ ),
472
+ ),
473
+ bbox_select: SelectMethod = typer.Option(
474
+ "flexible",
475
+ "-bs",
476
+ "--bbox-select",
477
+ help=SelectMethod.get_help("bbox"),
478
+ ),
394
479
  zarr: bool = typer.Option(
395
480
  False, "--zarr", help="Create zarr stream files, as catalogue targets."
396
481
  ),
@@ -442,6 +527,8 @@ def intake_catalogue(
442
527
  *(facets or []),
443
528
  time=time or "",
444
529
  time_select=cast(Literal["file", "flexible", "strict"], time_select),
530
+ bbox=bbox or None,
531
+ bbox_select=cast(Literal["file", "flexible", "strict"], bbox_select),
445
532
  flavour=cast(
446
533
  Literal["freva", "cmip6", "cmip5", "cordex", "nextgems", "user"],
447
534
  flavour.value,
@@ -461,7 +548,145 @@ def intake_catalogue(
461
548
  print(Path(temp_f.name).read_text())
462
549
 
463
550
 
464
- @databrowser_app.command(name="data-count", help="Count the databrowser search results")
551
+ @databrowser_app.command(
552
+ name="stac-catalogue",
553
+ help="Create a static STAC catalogue from the search."
554
+ )
555
+ @exception_handler
556
+ def stac_catalogue(
557
+ search_keys: Optional[List[str]] = typer.Argument(
558
+ default=None,
559
+ help="Refine your data search with this `key=value` pair search "
560
+ "parameters. The parameters could be, depending on the DRS standard, "
561
+ "flavour product, project model etc.",
562
+ ),
563
+ facets: Optional[List[str]] = typer.Option(
564
+ None,
565
+ "--facet",
566
+ help=(
567
+ "If you are not sure about the correct search key's you can use"
568
+ " the ``--facet`` flag to search of any matching entries. For "
569
+ "example --facet 'era5' would allow you to search for any entries"
570
+ " containing era5, regardless of project, product etc."
571
+ ),
572
+ ),
573
+ uniq_key: UniqKeys = typer.Option(
574
+ "file",
575
+ "--uniq-key",
576
+ "-u",
577
+ help=(
578
+ "The type of search result, which can be either “file” "
579
+ "or “uri”. This parameter determines whether the search will be "
580
+ "based on file paths or Uniform Resource Identifiers"
581
+ ),
582
+ ),
583
+ flavour: Flavours = typer.Option(
584
+ "freva",
585
+ "--flavour",
586
+ "-f",
587
+ help=(
588
+ "The Data Reference Syntax (DRS) standard specifying the type "
589
+ "of climate datasets to query."
590
+ ),
591
+ ),
592
+ time_select: SelectMethod = typer.Option(
593
+ "flexible",
594
+ "-ts",
595
+ "--time-select",
596
+ help=SelectMethod.get_help("time"),
597
+ ),
598
+ time: Optional[str] = typer.Option(
599
+ None,
600
+ "-t",
601
+ "--time",
602
+ help=(
603
+ "Special search facet to refine/subset search results by time. "
604
+ "This can be a string representation of a time range or a single "
605
+ "time step. The time steps have to follow ISO-8601. Valid strings "
606
+ "are ``%Y-%m-%dT%H:%M`` to ``%Y-%m-%dT%H:%M`` for time ranges and "
607
+ "``%Y-%m-%dT%H:%M``. **Note**: You don't have to give the full "
608
+ "string format to subset time steps ``%Y``, ``%Y-%m`` etc are also"
609
+ " valid."
610
+ ),
611
+ ),
612
+ bbox: Optional[Tuple[float, float, float, float]] = typer.Option(
613
+ None,
614
+ "-b",
615
+ "--bbox",
616
+ help=(
617
+ "Special search facet to refine/subset search results by spatial "
618
+ "extent. This can be a string representation of a bounding box. "
619
+ "The bounding box has to follow the format ``min_lon max_lon "
620
+ "min_lat max_lat``. Valid strings are ``-10 10 -10 10`` to "
621
+ "``0 5 0 5``."
622
+ ),
623
+ ),
624
+ bbox_select: SelectMethod = typer.Option(
625
+ "flexible",
626
+ "-bs",
627
+ "--bbox-select",
628
+ help=SelectMethod.get_help("bbox"),
629
+ ),
630
+ host: Optional[str] = typer.Option(
631
+ None,
632
+ "--host",
633
+ help=(
634
+ "Set the hostname of the databrowser, if not set (default) "
635
+ "the hostname is read from a config file"
636
+ ),
637
+ ),
638
+ verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
639
+ multiversion: bool = typer.Option(
640
+ False,
641
+ "--multi-version",
642
+ help="Select all versions and not just the latest version (default).",
643
+ ),
644
+ version: Optional[bool] = typer.Option(
645
+ False,
646
+ "-V",
647
+ "--version",
648
+ help="Show version an exit",
649
+ callback=version_callback,
650
+ ),
651
+ filename: Optional[Path] = typer.Option(
652
+ None,
653
+ "-o",
654
+ "--filename",
655
+ help=(
656
+ "Path to the file where the static STAC catalogue, "
657
+ "should be written to. If you don't specify or the path "
658
+ "does not exist, the file will be created in the current "
659
+ "working directory. "
660
+ ),
661
+ ),
662
+ ) -> None:
663
+ """Create a STAC catalogue for climate datasets based on the specified
664
+ Data Reference Syntax (DRS) standard (flavour) and the type of search
665
+ result (uniq_key), which can be either "file" or "uri"."""
666
+ logger.set_verbosity(verbose)
667
+ result = databrowser(
668
+ *(facets or []),
669
+ time=time or "",
670
+ time_select=cast(Literal["file", "flexible", "strict"], time_select),
671
+ bbox=bbox or None,
672
+ bbox_select=cast(Literal["file", "flexible", "strict"], bbox_select),
673
+ flavour=cast(
674
+ Literal["freva", "cmip6", "cmip5", "cordex", "nextgems", "user"],
675
+ flavour.value,
676
+ ),
677
+ uniq_key=cast(Literal["uri", "file"], uniq_key.value),
678
+ host=host,
679
+ fail_on_error=False,
680
+ multiversion=multiversion,
681
+ stream_zarr=False,
682
+ **(parse_cli_args(search_keys or [])),
683
+ )
684
+ print(result.stac_catalogue(filename=filename))
685
+
686
+
687
+ @databrowser_app.command(
688
+ name="data-count", help="Count the databrowser search results"
689
+ )
465
690
  @exception_handler
466
691
  def count_values(
467
692
  search_keys: Optional[List[str]] = typer.Argument(
@@ -495,11 +720,11 @@ def count_values(
495
720
  "of climate datasets to query."
496
721
  ),
497
722
  ),
498
- time_select: TimeSelect = typer.Option(
723
+ time_select: SelectMethod = typer.Option(
499
724
  "flexible",
500
725
  "-ts",
501
726
  "--time-select",
502
- help=TimeSelect.get_help(),
727
+ help=SelectMethod.get_help("time"),
503
728
  ),
504
729
  time: Optional[str] = typer.Option(
505
730
  None,
@@ -515,6 +740,24 @@ def count_values(
515
740
  " valid."
516
741
  ),
517
742
  ),
743
+ bbox: Optional[Tuple[float, float, float, float]] = typer.Option(
744
+ None,
745
+ "-b",
746
+ "--bbox",
747
+ help=(
748
+ "Special search facet to refine/subset search results by spatial "
749
+ "extent. This can be a string representation of a bounding box. "
750
+ "The bounding box has to follow the format ``min_lon max_lon "
751
+ "min_lat max_lat``. Valid strings are ``-10 10 -10 10`` to "
752
+ "``0 5 0 5``."
753
+ ),
754
+ ),
755
+ bbox_select: SelectMethod = typer.Option(
756
+ "flexible",
757
+ "-bs",
758
+ "--bbox-select",
759
+ help=SelectMethod.get_help("bbox"),
760
+ ),
518
761
  extended_search: bool = typer.Option(
519
762
  False,
520
763
  "-e",
@@ -559,12 +802,16 @@ def count_values(
559
802
  result: Union[int, Dict[str, Dict[str, int]]] = 0
560
803
  search_kws = parse_cli_args(search_keys or [])
561
804
  time = cast(str, time or search_kws.pop("time", ""))
805
+ bbox = cast(Optional[Tuple[float, float, float, float]],
806
+ bbox or search_kws.pop("bbox", None))
562
807
  facets = facets or []
563
808
  if detail:
564
809
  result = databrowser.count_values(
565
810
  *facets,
566
811
  time=time or "",
567
812
  time_select=cast(Literal["file", "flexible", "strict"], time_select),
813
+ bbox=bbox or None,
814
+ bbox_select=cast(Literal["file", "flexible", "strict"], bbox_select),
568
815
  flavour=cast(
569
816
  Literal["freva", "cmip6", "cmip5", "cordex", "nextgems", "user"],
570
817
  flavour.value,
@@ -581,8 +828,12 @@ def count_values(
581
828
  *facets,
582
829
  time=time or "",
583
830
  time_select=cast(Literal["file", "flexible", "strict"], time_select),
831
+ bbox=bbox or None,
832
+ bbox_select=cast(Literal["file", "flexible", "strict"], bbox_select),
584
833
  flavour=cast(
585
- Literal["freva", "cmip6", "cmip5", "cordex", "nextgems", "user"],
834
+ Literal[
835
+ "freva", "cmip6", "cmip5", "cordex", "nextgems", "user"
836
+ ],
586
837
  flavour.value,
587
838
  ),
588
839
  host=host,
@@ -662,11 +913,13 @@ def user_data_add(
662
913
  action="add",
663
914
  userdata_items=cast(List[Union[str, xr.Dataset]], paths),
664
915
  metadata=facet_dict,
665
- host=host
916
+ host=host,
666
917
  )
667
918
 
668
919
 
669
- @user_data_app.command(name="delete", help="Delete user data from the databrowser.")
920
+ @user_data_app.command(
921
+ name="delete", help="Delete user data from the databrowser."
922
+ )
670
923
  @exception_handler
671
924
  def user_data_delete(
672
925
  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
@@ -64,8 +63,10 @@ class databrowser:
64
63
  timestamp. The timestamps has to follow ISO-8601. Valid strings are
65
64
  ``%Y-%m-%dT%H:%M to %Y-%m-%dT%H:%M`` for time ranges or
66
65
  ``%Y-%m-%dT%H:%M`` for single time stamps.
67
- **Note**: You don't have to give the full string format to subset
68
- time steps: `%Y`, `%Y-%m` etc are also valid.
66
+
67
+ .. note:: You don't have to give the full string format to subset time
68
+ steps ``%Y``, ``%Y-%m`` etc are also valid.
69
+
69
70
  time_select: str, default: flexible
70
71
  Operator that specifies how the time period is selected. Choose from
71
72
  flexible (default), strict or file. ``strict`` returns only those files
@@ -75,11 +76,25 @@ class databrowser:
75
76
  ``flexible`` returns those files that have either start or end period
76
77
  covered. ``file`` will only return files where the entire time
77
78
  period is contained within `one single` file.
79
+ bbox: str, default: ""
80
+ Special search facet to refine/subset search results by spatial extent.
81
+ This can be a list representation of a bounding box or a WKT polygon.
82
+ Valid lists are ``min_lon max_lon min_lat max_lat`` for bounding
83
+ boxes and Well-Known Text (WKT) format for polygons.
84
+
85
+ bbox_select: str, default: flexible
86
+ Operator that specifies how the spatial extent is selected. Choose from
87
+ flexible (default), strict or file. ``strict`` returns only those files
88
+ that fully contain the query extent. The bbox search ``-10 10 -10 10``
89
+ will not select files covering only ``0 5 0 5`` with the ``strict``
90
+ method. ``flexible`` will select those files as it returns files that
91
+ have any overlap with the query extent. ``file`` will only return files
92
+ where the entire spatial extent is contained by the query geometry.
78
93
  uniq_key: str, default: file
79
94
  Chose if the solr search query should return paths to files or
80
95
  uris, uris will have the file path along with protocol of the storage
81
- system. Uris can be useful if the search query result should be
82
- used libraries like fsspec.
96
+ system. URIs are useful when working with libraries like fsspec, which
97
+ require protocol information.
83
98
  host: str, default: None
84
99
  Override the host name of the databrowser server. This is usually the
85
100
  url where the freva web site can be found. Such as www.freva.dkrz.de.
@@ -215,6 +230,8 @@ class databrowser:
215
230
  time: Optional[str] = None,
216
231
  host: Optional[str] = None,
217
232
  time_select: Literal["flexible", "strict", "file"] = "flexible",
233
+ bbox: Optional[Tuple[float, float, float, float]] = None,
234
+ bbox_select: Literal["flexible", "strict", "file"] = "flexible",
218
235
  stream_zarr: bool = False,
219
236
  multiversion: bool = False,
220
237
  fail_on_error: bool = False,
@@ -239,6 +256,10 @@ class databrowser:
239
256
  if time:
240
257
  self._params["time"] = time
241
258
  self._params["time_select"] = time_select
259
+ if bbox:
260
+ bbox_str = ",".join(map(str, bbox))
261
+ self._params["bbox"] = bbox_str
262
+ self._params["bbox_select"] = bbox_select
242
263
  if facets:
243
264
  self._add_search_keyword_args_from_facet(facets, facet_search)
244
265
 
@@ -246,7 +267,8 @@ class databrowser:
246
267
  self, facets: Tuple[str, ...], search_kw: Dict[str, List[str]]
247
268
  ) -> None:
248
269
  metadata = {
249
- k: v[::2] for (k, v) in self._facet_search(extended_search=True).items()
270
+ k: v[::2]
271
+ for (k, v) in self._facet_search(extended_search=True).items()
250
272
  }
251
273
  primary_key = list(metadata.keys() or ["project"])[0]
252
274
  num_facets = 0
@@ -304,7 +326,9 @@ class databrowser:
304
326
 
305
327
  # Create a table-like structure for available flavors and search facets
306
328
  style = 'style="text-align: left"'
307
- facet_heading = f"Available search facets for <em>{self._flavour}</em> flavour"
329
+ facet_heading = (
330
+ f"Available search facets for <em>{self._flavour}</em> flavour"
331
+ )
308
332
  html_repr = (
309
333
  "<table>"
310
334
  f"<tr><th colspan='2' {style}>{self.__class__.__name__}"
@@ -345,7 +369,9 @@ class databrowser:
345
369
  if self._stream_zarr:
346
370
  token = self._auth.check_authentication(auth_url=self._cfg.auth_url)
347
371
  url = self._cfg.zarr_loader_url
348
- kwargs["headers"] = {"Authorization": f"Bearer {token['access_token']}"}
372
+ kwargs["headers"] = {
373
+ "Authorization": f"Bearer {token['access_token']}"
374
+ }
349
375
  kwargs["params"] = {"catalogue-type": "intake"}
350
376
  result = self._request("GET", url, **kwargs)
351
377
  if result is None:
@@ -357,7 +383,9 @@ class databrowser:
357
383
  for content in result.iter_content(decode_unicode=False):
358
384
  stream.write(content)
359
385
  except Exception as error:
360
- raise ValueError(f"Couldn't write catalogue content: {error}") from None
386
+ raise ValueError(
387
+ f"Couldn't write catalogue content: {error}"
388
+ ) from None
361
389
 
362
390
  def intake_catalogue(self) -> intake_esm.core.esm_datastore:
363
391
  """Create an intake esm catalogue object from the search.
@@ -388,7 +416,88 @@ class databrowser:
388
416
  """
389
417
  with NamedTemporaryFile(suffix=".json") as temp_f:
390
418
  self._create_intake_catalogue_file(temp_f.name)
391
- return intake.open_esm_datastore(temp_f.name)
419
+ return cast(
420
+ intake_esm.core.esm_datastore,
421
+ intake.open_esm_datastore(temp_f.name),
422
+ )
423
+
424
+ def stac_catalogue(
425
+ self,
426
+ filename: Optional[Union[str, Path]] = None,
427
+ **kwargs: Any,
428
+ ) -> str:
429
+ """Create a static STAC catalogue from
430
+ the search.
431
+
432
+ Parameters
433
+ ~~~~~~~~~~
434
+ filename: str, default: None
435
+ The filename of the STAC catalogue. If not given
436
+ or doesn't exist the STAC catalogue will be saved
437
+ to the current working directory.
438
+ **kwargs: Any
439
+ Additional keyword arguments to be passed to the request.
440
+
441
+ Returns
442
+ ~~~~~~~
443
+ BinaryIO
444
+ A zip file stream
445
+
446
+ Raises
447
+ ~~~~~~
448
+ ValueError: If stac-catalogue creation failed.
449
+
450
+ Example
451
+ ~~~~~~~
452
+ Let's create a static STAC catalogue:
453
+
454
+ .. execute_code::
455
+
456
+ from tempfile import mktemp
457
+ temp_path = mktemp(suffix=".zip")
458
+
459
+ from freva_client import databrowser
460
+ db = databrowser(dataset="cmip6-hsm")
461
+ db.stac_catalogue(filename=temp_path)
462
+ print(f"STAC catalog saved to: {temp_path}")
463
+
464
+ """
465
+
466
+ kwargs.update({"stream": True})
467
+ stac_url = self._cfg.stac_url
468
+ pprint("[b][green]Downloading the STAC catalog started ...[green][b]")
469
+ result = self._request("GET", stac_url, **kwargs)
470
+ if result is None or result.status_code == 404:
471
+ raise ValueError( # pragma: no cover
472
+ "No STAC catalog found. Please check if you have any search results."
473
+ )
474
+ default_filename = (
475
+ result.headers.get("Content-Disposition", "")
476
+ .split("filename=")[-1]
477
+ .strip('"')
478
+ )
479
+
480
+ if filename is None:
481
+ save_path = Path.cwd() / default_filename
482
+ else:
483
+ save_path = Path(cast(str, filename))
484
+ if save_path.is_dir() and save_path.exists():
485
+ save_path = save_path / default_filename
486
+
487
+ save_path.parent.mkdir(parents=True, exist_ok=True)
488
+
489
+ total_size = 0
490
+ with open(save_path, "wb") as f:
491
+ for chunk in result.iter_content(chunk_size=8192):
492
+ if chunk:
493
+ f.write(chunk)
494
+ total_size += len(chunk)
495
+
496
+ return (
497
+ f"STAC catalog saved to: {save_path} "
498
+ f"(size: {total_size / 1024 / 1024:.2f} MB). "
499
+ f"Or simply download from: {result.url}"
500
+ )
392
501
 
393
502
  @classmethod
394
503
  def count_values(
@@ -400,6 +509,8 @@ class databrowser:
400
509
  time: Optional[str] = None,
401
510
  host: Optional[str] = None,
402
511
  time_select: Literal["flexible", "strict", "file"] = "flexible",
512
+ bbox: Optional[Tuple[float, float, float, float]] = None,
513
+ bbox_select: Literal["flexible", "strict", "file"] = "flexible",
403
514
  multiversion: bool = False,
404
515
  fail_on_error: bool = False,
405
516
  extended_search: bool = False,
@@ -423,8 +534,11 @@ class databrowser:
423
534
  This can be a string representation of a time range or a single
424
535
  timestamp. The timestamp has to follow ISO-8601. Valid strings are
425
536
  ``%Y-%m-%dT%H:%M`` to ``%Y-%m-%dT%H:%M`` for time ranges and
426
- ``%Y-%m-%dT%H:%M``. **Note**: You don't have to give the full string
427
- format to subset time steps ``%Y``, ``%Y-%m`` etc are also valid.
537
+ ``%Y-%m-%dT%H:%M``.
538
+
539
+ .. note:: You don't have to give the full string format to subset time
540
+ steps ``%Y``, ``%Y-%m`` etc are also valid.
541
+
428
542
  time_select: str, default: flexible
429
543
  Operator that specifies how the time period is selected. Choose from
430
544
  flexible (default), strict or file. ``strict`` returns only those files
@@ -434,6 +548,20 @@ class databrowser:
434
548
  ``flexible`` returns those files that have either start or end period
435
549
  covered. ``file`` will only return files where the entire time
436
550
  period is contained within `one single` file.
551
+ bbox: str, default: ""
552
+ Special search facet to refine/subset search results by spatial extent.
553
+ This can be a list representation of a bounding box or a WKT polygon.
554
+ Valid lists are ``min_lon max_lon min_lat max_lat`` for bounding
555
+ boxes and Well-Known Text (WKT) format for polygons.
556
+
557
+ bbox_select: str, default: flexible
558
+ Operator that specifies how the spatial extent is selected. Choose from
559
+ flexible (default), strict or file. ``strict`` returns only those files
560
+ that fully contain the query extent. The bbox search ``-10 10 -10 10``
561
+ will not select files covering only ``0 5 0 5`` with the ``strict``
562
+ method. ``flexible`` will select those files as it returns files that
563
+ have any overlap with the query extent. ``file`` will only return files
564
+ where the entire spatial extent is contained by the query geometry.
437
565
  extended_search: bool, default: False
438
566
  Retrieve information on additional search keys.
439
567
  host: str, default: None
@@ -485,6 +613,8 @@ class databrowser:
485
613
  flavour=flavour,
486
614
  time=time,
487
615
  time_select=time_select,
616
+ bbox=bbox,
617
+ bbox_select=bbox_select,
488
618
  host=host,
489
619
  multiversion=multiversion,
490
620
  fail_on_error=fail_on_error,
@@ -495,7 +625,9 @@ class databrowser:
495
625
  result = this._facet_search(extended_search=extended_search)
496
626
  counts = {}
497
627
  for facet, value_counts in result.items():
498
- counts[facet] = dict(zip(value_counts[::2], map(int, value_counts[1::2])))
628
+ counts[facet] = dict(
629
+ zip(value_counts[::2], map(int, value_counts[1::2]))
630
+ )
499
631
  return counts
500
632
 
501
633
  @cached_property
@@ -520,7 +652,8 @@ class databrowser:
520
652
 
521
653
  """
522
654
  return {
523
- k: v[::2] for (k, v) in self._facet_search(extended_search=True).items()
655
+ k: v[::2]
656
+ for (k, v) in self._facet_search(extended_search=True).items()
524
657
  }
525
658
 
526
659
  @classmethod
@@ -533,6 +666,8 @@ class databrowser:
533
666
  time: Optional[str] = None,
534
667
  host: Optional[str] = None,
535
668
  time_select: Literal["flexible", "strict", "file"] = "flexible",
669
+ bbox: Optional[Tuple[float, float, float, float]] = None,
670
+ bbox_select: Literal["flexible", "strict", "file"] = "flexible",
536
671
  multiversion: bool = False,
537
672
  fail_on_error: bool = False,
538
673
  extended_search: bool = False,
@@ -559,8 +694,11 @@ class databrowser:
559
694
  This can be a string representation of a time range or a single
560
695
  timestamp. The timestamp has to follow ISO-8601. Valid strings are
561
696
  ``%Y-%m-%dT%H:%M`` to ``%Y-%m-%dT%H:%M`` for time ranges and
562
- ``%Y-%m-%dT%H:%M``. **Note**: You don't have to give the full string
563
- format to subset time steps ``%Y``, ``%Y-%m`` etc are also valid.
697
+ ``%Y-%m-%dT%H:%M``.
698
+
699
+ .. note:: You don't have to give the full string format to subset time
700
+ steps ``%Y``, ``%Y-%m`` etc are also valid.
701
+
564
702
  time_select: str, default: flexible
565
703
  Operator that specifies how the time period is selected. Choose from
566
704
  flexible (default), strict or file. ``strict`` returns only those files
@@ -570,6 +708,20 @@ class databrowser:
570
708
  ``flexible`` returns those files that have either start or end period
571
709
  covered. ``file`` will only return files where the entire time
572
710
  period is contained within *one single* file.
711
+ bbox: str, default: ""
712
+ Special search facet to refine/subset search results by spatial extent.
713
+ This can be a list representation of a bounding box or a WKT polygon.
714
+ Valid lists are ``min_lon max_lon min_lat max_lat`` for bounding
715
+ boxes and Well-Known Text (WKT) format for polygons.
716
+
717
+ bbox_select: str, default: flexible
718
+ Operator that specifies how the spatial extent is selected. Choose from
719
+ flexible (default), strict or file. ``strict`` returns only those files
720
+ that fully contain the query extent. The bbox search ``-10 10 -10 10``
721
+ will not select files covering only ``0 5 0 5`` with the ``strict``
722
+ method. ``flexible`` will select those files as it returns files that
723
+ have any overlap with the query extent. ``file`` will only return files
724
+ where the entire spatial extent is contained by the query geometry.
573
725
  extended_search: bool, default: False
574
726
  Retrieve information on additional search keys.
575
727
  multiversion: bool, default: False
@@ -638,12 +790,30 @@ class databrowser:
638
790
  from freva_client import databrowser
639
791
  print(databrowser.metadata_search("reana*", realm="ocean", flavour="cmip6"))
640
792
 
793
+ In datasets with multiple versions only the `latest` version (i.e.
794
+ `highest` version number) is returned by default. Querying a specific
795
+ version from a multi versioned datasets requires the ``multiversion``
796
+ flag in combination with the ``version`` special attribute:
797
+
798
+ .. execute_code::
799
+
800
+ from freva_client import databrowser
801
+ res = databrowser.metadata_search(dataset="cmip6-fs",
802
+ model="access-cm2", version="v20191108", extended_search=True,
803
+ multiversion=True)
804
+ print(res)
805
+
806
+ If no particular ``version`` is requested, information of all versions
807
+ will be returned.
808
+
641
809
  """
642
810
  this = cls(
643
811
  *facets,
644
812
  flavour=flavour,
645
813
  time=time,
646
814
  time_select=time_select,
815
+ bbox=bbox,
816
+ bbox_select=bbox_select,
647
817
  host=host,
648
818
  multiversion=multiversion,
649
819
  fail_on_error=fail_on_error,
@@ -653,7 +823,9 @@ class databrowser:
653
823
  )
654
824
  return {
655
825
  k: v[::2]
656
- for (k, v) in this._facet_search(extended_search=extended_search).items()
826
+ for (k, v) in this._facet_search(
827
+ extended_search=extended_search
828
+ ).items()
657
829
  }
658
830
 
659
831
  @classmethod
@@ -782,11 +954,11 @@ class databrowser:
782
954
  import xarray as xr
783
955
  token_info = authenticate(username="janedoe")
784
956
  filenames = (
785
- "../freva-rest/src/databrowser_api/mock/data/model/regional/cordex/output/EUR-11/"
957
+ "../freva-rest/src/freva_rest/databrowser_api/mock/data/model/regional/cordex/output/EUR-11/"
786
958
  "GERICS/NCC-NorESM1-M/rcp85/r1i1p1/GERICS-REMO2015/v1/3hr/pr/v20181212/*.nc"
787
959
  )
788
960
  filename1 = (
789
- "../freva-rest/src/databrowser_api/mock/data/model/regional/cordex/output/EUR-11/"
961
+ "../freva-rest/src/freva_rest/databrowser_api/mock/data/model/regional/cordex/output/EUR-11/"
790
962
  "CLMcom/MPI-M-MPI-ESM-LR/historical/r0i0p0/CLMcom-CCLM4-8-17/v1/fx/orog/v20140515/"
791
963
  "orog_EUR-11_MPI-M-MPI-ESM-LR_historical_r1i1p1_CLMcom-CCLM4-8-17_v1_fx.nc"
792
964
  )
@@ -856,7 +1028,7 @@ class databrowser:
856
1028
  method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"],
857
1029
  url: str,
858
1030
  data: Optional[Dict[str, Any]] = None,
859
- **kwargs: Any
1031
+ **kwargs: Any,
860
1032
  ) -> Optional[requests.models.Response]:
861
1033
  """Request method to handle CRUD operations (GET, POST, PUT, PATCH, DELETE)."""
862
1034
  method_upper = method.upper()
@@ -864,8 +1036,13 @@ class databrowser:
864
1036
  params = kwargs.pop("params", {})
865
1037
  stream = kwargs.pop("stream", False)
866
1038
 
867
- logger.debug("%s request to %s with data: %s and parameters: %s",
868
- method_upper, url, data, {**self._params, **params})
1039
+ logger.debug(
1040
+ "%s request to %s with data: %s and parameters: %s",
1041
+ method_upper,
1042
+ url,
1043
+ data,
1044
+ {**self._params, **params},
1045
+ )
869
1046
 
870
1047
  try:
871
1048
  req = requests.Request(
@@ -873,7 +1050,7 @@ class databrowser:
873
1050
  url=url,
874
1051
  params={**self._params, **params},
875
1052
  json=None if method_upper in "GET" else data,
876
- **kwargs
1053
+ **kwargs,
877
1054
  )
878
1055
  with requests.Session() as session:
879
1056
  prepared = session.prepare_request(req)
@@ -883,8 +1060,10 @@ class databrowser:
883
1060
 
884
1061
  except KeyboardInterrupt:
885
1062
  pprint("[red][b]User interrupt: Exit[/red][/b]", file=sys.stderr)
886
- except (requests.exceptions.ConnectionError,
887
- requests.exceptions.HTTPError) as error:
1063
+ except (
1064
+ requests.exceptions.ConnectionError,
1065
+ requests.exceptions.HTTPError,
1066
+ ) as error:
888
1067
  msg = f"{method_upper} request failed with {error}"
889
1068
  if self._fail_on_error:
890
1069
  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") or config.get("solr.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(f"Could not connect to {self.databrowser_url}") from None
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(os.environ.get("EVALUATION_SYSTEM_CONFIG_FILE") or eval_conf): "ini",
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 f"{self.databrowser_url}/data_search/{self.flavour}/{self.uniq_key}"
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,19 @@ 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}/intake_catalogue/{self.flavour}/{self.uniq_key}"
149
+ return f"{self.databrowser_url}/intake-catalogue/{self.flavour}/{self.uniq_key}"
150
+
151
+ @property
152
+ def stac_url(self) -> str:
153
+ """Define the url for creating stac catalogue."""
154
+ return f"{self.databrowser_url}/stac-catalogue/{self.flavour}/{self.uniq_key}"
144
155
 
145
156
  @property
146
157
  def metadata_url(self) -> str:
147
158
  """Define the endpoint for the metadata search."""
148
159
  return (
149
- f"{self.databrowser_url}/metadata_search/" f"{self.flavour}/{self.uniq_key}"
160
+ f"{self.databrowser_url}/metadata-search/"
161
+ f"{self.flavour}/{self.uniq_key}"
150
162
  )
151
163
 
152
164
  @staticmethod
@@ -166,7 +178,7 @@ class Config:
166
178
  if port:
167
179
  hostname = f"{hostname}:{port}"
168
180
  hostname = hostname.partition("/")[0]
169
- return f"{scheme}://{hostname}/api"
181
+ return f"{scheme}://{hostname}/api/freva-nextgen"
170
182
 
171
183
  @staticmethod
172
184
  def get_dirs(user: bool = True) -> Path:
@@ -199,9 +211,12 @@ class UserDataHandler:
199
211
  This class is used for processing user data and extracting metadata
200
212
  from the data files.
201
213
  """
214
+
202
215
  def __init__(self, userdata_items: List[Union[str, xr.Dataset]]) -> None:
203
216
  self._suffixes = [".nc", ".nc4", ".grb", ".grib", ".zarr", "zar"]
204
- self.user_metadata: List[Dict[str, Union[str, List[str], Dict[str, str]]]] = []
217
+ self.user_metadata: List[
218
+ Dict[str, Union[str, List[str], Dict[str, str]]]
219
+ ] = []
205
220
  self._metadata_collection: List[Dict[str, Union[str, List[str]]]] = []
206
221
  try:
207
222
  self._executor = concurrent.futures.ThreadPoolExecutor(
@@ -238,28 +253,37 @@ class UserDataHandler:
238
253
  validated_xarray_datasets.append(data)
239
254
 
240
255
  if not validated_paths and not validated_xarray_datasets:
241
- raise FileNotFoundError("No valid file paths or xarray datasets found.")
256
+ raise FileNotFoundError(
257
+ "No valid file paths or xarray datasets found."
258
+ )
242
259
  return {
243
260
  "validated_user_paths": validated_paths,
244
261
  "validated_user_xrdatasets": validated_xarray_datasets,
245
262
  }
246
263
 
247
- def _process_user_data(self, userdata_items: List[Union[str, xr.Dataset]],
248
- ) -> None:
264
+ def _process_user_data(
265
+ self,
266
+ userdata_items: List[Union[str, xr.Dataset]],
267
+ ) -> None:
249
268
  """Process xarray datasets and file paths using thread pool."""
250
269
  futures = []
251
- validated_userdata: Dict[str, Union[List[Path], List[xr.Dataset]]] = \
270
+ validated_userdata: Dict[str, Union[List[Path], List[xr.Dataset]]] = (
252
271
  self._validate_user_data(userdata_items)
272
+ )
253
273
  if validated_userdata["validated_user_xrdatasets"]:
254
274
  futures.append(
255
- self._executor.submit(self._process_userdata_in_executor,
256
- validated_userdata["validated_user_xrdatasets"])
275
+ self._executor.submit(
276
+ self._process_userdata_in_executor,
277
+ validated_userdata["validated_user_xrdatasets"],
278
+ )
257
279
  )
258
280
 
259
281
  if validated_userdata["validated_user_paths"]:
260
282
  futures.append(
261
- self._executor.submit(self._process_userdata_in_executor,
262
- validated_userdata["validated_user_paths"])
283
+ self._executor.submit(
284
+ self._process_userdata_in_executor,
285
+ validated_userdata["validated_user_paths"],
286
+ )
263
287
  )
264
288
  for future in futures:
265
289
  try:
@@ -309,11 +333,11 @@ class UserDataHandler:
309
333
 
310
334
  try:
311
335
  dset = (
312
- path if isinstance(path, xr.Dataset)
313
- else xr.open_mfdataset(str(path),
314
- parallel=False,
315
- use_cftime=True,
316
- lock=False)
336
+ path
337
+ if isinstance(path, xr.Dataset)
338
+ else xr.open_mfdataset(
339
+ str(path), parallel=False, use_cftime=True, lock=False
340
+ )
317
341
  )
318
342
  time_freq = dset.attrs.get("frequency", "")
319
343
  data_vars = list(map(str, dset.data_vars))
@@ -328,16 +352,26 @@ class UserDataHandler:
328
352
  return {}
329
353
  if len(times) > 0:
330
354
  try:
331
- time_str = f"[{times[0].isoformat()}Z TO {times[-1].isoformat()}Z]"
332
- dt = abs((times[1] - times[0]).total_seconds()) if len(times) > 1 else 0
355
+ time_str = (
356
+ f"[{times[0].isoformat()}Z TO {times[-1].isoformat()}Z]"
357
+ )
358
+ dt = (
359
+ abs((times[1] - times[0]).total_seconds())
360
+ if len(times) > 1
361
+ else 0
362
+ )
333
363
  except Exception as non_cftime:
334
- logger.info("The time var is not based on the cftime: %s", non_cftime)
364
+ logger.info(
365
+ "The time var is not based on the cftime: %s", non_cftime
366
+ )
335
367
  time_str = (
336
368
  f"[{np.datetime_as_string(times[0], unit='s')}Z TO "
337
369
  f"{np.datetime_as_string(times[-1], unit='s')}Z]"
338
370
  )
339
371
  dt = (
340
- abs((times[1] - times[0]).astype("timedelta64[s]").astype(int))
372
+ abs(
373
+ (times[1] - times[0]).astype("timedelta64[s]").astype(int)
374
+ )
341
375
  if len(times) > 1
342
376
  else 0
343
377
  )
@@ -357,7 +391,9 @@ class UserDataHandler:
357
391
 
358
392
  _data: Dict[str, Union[str, List[str], Dict[str, str]]] = {}
359
393
  _data.setdefault("variable", variables[0])
360
- _data.setdefault("time_frequency", self._get_time_frequency(dt, time_freq))
394
+ _data.setdefault(
395
+ "time_frequency", self._get_time_frequency(dt, time_freq)
396
+ )
361
397
  _data["time"] = time_str
362
398
  _data.setdefault("cmor_table", _data["time_frequency"])
363
399
  _data.setdefault("version", "")