freva-client 2502.0.0__py3-none-any.whl → 2506.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of freva-client might be problematic. Click here for more details.

@@ -7,7 +7,7 @@ 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, Optional, Tuple, Union, cast
11
11
 
12
12
  import typer
13
13
  import xarray as xr
@@ -19,16 +19,6 @@ from freva_client.utils import exception_handler, logger
19
19
  from .cli_utils import parse_cli_args, version_callback
20
20
 
21
21
 
22
- def _auth(url: str, token: Optional[str]) -> None:
23
- if token:
24
- auth = Auth()
25
- auth.set_token(
26
- access_token=token, expires=auth.token_expiration_time.timestamp()
27
- )
28
- else:
29
- raise ValueError("`--access-token` is required for authentication.")
30
-
31
-
32
22
  class UniqKeys(str, Enum):
33
23
  """Literal implementation for the cli."""
34
24
 
@@ -47,7 +37,7 @@ class Flavours(str, Enum):
47
37
  user = "user"
48
38
 
49
39
 
50
- class TimeSelect(str, Enum):
40
+ class SelectMethod(str, Enum):
51
41
  """Literal implementation for the cli."""
52
42
 
53
43
  strict = "strict"
@@ -55,18 +45,44 @@ class TimeSelect(str, Enum):
55
45
  file = "file"
56
46
 
57
47
  @staticmethod
58
- def get_help() -> str:
59
- """Generate the help string."""
48
+ def get_help(context: str) -> str:
49
+ """Generate the help string for time or bbox selection methods.
50
+
51
+ Parameters
52
+ ----------
53
+ context: str, default: "time"
54
+ Either "time" or "bbox" to generate appropriate help text.
55
+ """
56
+ examples = {
57
+ "time": ("2000 to 2012", "2010 to 2020"),
58
+ "bbox": ("-10 10 -10 10", "0 5 0 5"),
59
+ }
60
+ descriptions = {
61
+ "time": {
62
+ "unit": "time period",
63
+ "start_end": "start or end period",
64
+ "subset": "time period",
65
+ },
66
+ "bbox": {
67
+ "unit": "spatial extent",
68
+ "start_end": "any part of the extent",
69
+ "subset": "spatial extent",
70
+ },
71
+ }
72
+
73
+ context_info = descriptions.get(context, descriptions["time"])
74
+ example = examples.get(context, examples["time"])
75
+
60
76
  return (
61
- "Operator that specifies how the time period is selected. "
77
+ f"Operator that specifies how the {context_info['unit']} is selected. "
62
78
  "Choose from flexible (default), strict or file. "
63
79
  "``strict`` returns only those files that have the *entire* "
64
- "time period covered. The time search ``2000 to 2012`` will "
65
- "not select files containing data from 2010 to 2020 with "
66
- "the ``strict`` method. ``flexible`` will select those files "
67
- "as ``flexible`` returns those files that have either start "
68
- "or end period covered. ``file`` will only return files where "
69
- "the entire time period is contained within *one single* file."
80
+ f"{context_info['unit']} covered. The {context} search ``{example[0]}`` "
81
+ f"will not select files containing data from {example[1]} with "
82
+ "the ``strict`` method. ``flexible`` will select those files as "
83
+ f"``flexible`` returns those files that have {context_info['start_end']} "
84
+ f"covered. ``file`` will only return files where the entire "
85
+ f"{context_info['subset']} is contained within *one single* file."
70
86
  )
71
87
 
72
88
 
@@ -124,11 +140,11 @@ def metadata_search(
124
140
  "of climate datasets to query."
125
141
  ),
126
142
  ),
127
- time_select: TimeSelect = typer.Option(
143
+ time_select: SelectMethod = typer.Option(
128
144
  "flexible",
129
145
  "-ts",
130
146
  "--time-select",
131
- help=TimeSelect.get_help(),
147
+ help=SelectMethod.get_help("time"),
132
148
  ),
133
149
  time: Optional[str] = typer.Option(
134
150
  None,
@@ -144,6 +160,24 @@ def metadata_search(
144
160
  " valid."
145
161
  ),
146
162
  ),
163
+ bbox: Optional[Tuple[float, float, float, float]] = typer.Option(
164
+ None,
165
+ "-b",
166
+ "--bbox",
167
+ help=(
168
+ "Special search facet to refine/subset search results by spatial "
169
+ "extent. This can be a string representation of a bounding box. "
170
+ "The bounding box has to follow the format ``min_lon max_lon "
171
+ "min_lat,max_lat``. Valid strings are ``-10 10 -10 10`` to "
172
+ "``0 5 0 5``."
173
+ ),
174
+ ),
175
+ bbox_select: SelectMethod = typer.Option(
176
+ "flexible",
177
+ "-bs",
178
+ "--bbox-select",
179
+ help=SelectMethod.get_help("bbox"),
180
+ ),
147
181
  extended_search: bool = typer.Option(
148
182
  False,
149
183
  "-e",
@@ -188,13 +222,10 @@ def metadata_search(
188
222
  result = databrowser.metadata_search(
189
223
  *(facets or []),
190
224
  time=time or "",
191
- time_select=cast(
192
- Literal["file", "flexible", "strict"], time_select.value
193
- ),
194
- flavour=cast(
195
- Literal["freva", "cmip6", "cmip5", "cordex", "nextgems", "user"],
196
- flavour.value,
197
- ),
225
+ time_select=time_select.value,
226
+ bbox=bbox or None,
227
+ bbox_select=bbox_select.value,
228
+ flavour=flavour.value,
198
229
  host=host,
199
230
  extended_search=extended_search,
200
231
  multiversion=multiversion,
@@ -248,19 +279,21 @@ def data_search(
248
279
  "of climate datasets to query."
249
280
  ),
250
281
  ),
251
- time_select: TimeSelect = typer.Option(
282
+ time_select: SelectMethod = typer.Option(
252
283
  "flexible",
253
284
  "-ts",
254
285
  "--time-select",
255
- help=TimeSelect.get_help(),
286
+ help=SelectMethod.get_help("time"),
256
287
  ),
257
288
  zarr: bool = typer.Option(False, "--zarr", help="Create zarr stream files."),
258
- access_token: Optional[str] = typer.Option(
289
+ token_file: Optional[Path] = typer.Option(
259
290
  None,
260
- "--access-token",
291
+ "--token-file",
292
+ "-t",
261
293
  help=(
262
- "Use this access token for authentication"
263
- " when creating a zarr stream files."
294
+ "Instead of authenticating via code based authentication flow "
295
+ "you can set the path to the json file that contains a "
296
+ "`refresh token` containing a refresh_token key."
264
297
  ),
265
298
  ),
266
299
  time: Optional[str] = typer.Option(
@@ -277,6 +310,24 @@ def data_search(
277
310
  " valid."
278
311
  ),
279
312
  ),
313
+ bbox: Optional[Tuple[float, float, float, float]] = typer.Option(
314
+ None,
315
+ "-b",
316
+ "--bbox",
317
+ help=(
318
+ "Special search facet to refine/subset search results by spatial "
319
+ "extent. This can be a string representation of a bounding box. "
320
+ "The bounding box has to follow the format ``min_lon max_lon "
321
+ "min_lat max_lat``. Valid strings are ``-10 10 -10 10`` to "
322
+ "``0 5 0 5``."
323
+ ),
324
+ ),
325
+ bbox_select: SelectMethod = typer.Option(
326
+ "flexible",
327
+ "-bs",
328
+ "--bbox-select",
329
+ help=SelectMethod.get_help("bbox"),
330
+ ),
280
331
  parse_json: bool = typer.Option(
281
332
  False, "-j", "--json", help="Parse output in json format."
282
333
  ),
@@ -313,12 +364,11 @@ def data_search(
313
364
  result = databrowser(
314
365
  *(facets or []),
315
366
  time=time or "",
316
- time_select=cast(Literal["file", "flexible", "strict"], time_select),
317
- flavour=cast(
318
- Literal["freva", "cmip6", "cmip5", "cordex", "nextgems", "user"],
319
- flavour.value,
320
- ),
321
- uniq_key=cast(Literal["uri", "file"], uniq_key.value),
367
+ time_select=time_select.value,
368
+ bbox=bbox or None,
369
+ bbox_select=bbox_select.value,
370
+ flavour=flavour.value,
371
+ uniq_key=uniq_key.value,
322
372
  host=host,
323
373
  fail_on_error=False,
324
374
  multiversion=multiversion,
@@ -326,7 +376,7 @@ def data_search(
326
376
  **(parse_cli_args(search_keys or [])),
327
377
  )
328
378
  if zarr:
329
- _auth(result._cfg.auth_url, access_token)
379
+ Auth(token_file).authenticate(host=host, _cli=True)
330
380
  if parse_json:
331
381
  print(json.dumps(sorted(result)))
332
382
  else:
@@ -374,11 +424,11 @@ def intake_catalogue(
374
424
  "of climate datasets to query."
375
425
  ),
376
426
  ),
377
- time_select: TimeSelect = typer.Option(
427
+ time_select: SelectMethod = typer.Option(
378
428
  "flexible",
379
429
  "-ts",
380
430
  "--time-select",
381
- help=TimeSelect.get_help(),
431
+ help=SelectMethod.get_help("time"),
382
432
  ),
383
433
  time: Optional[str] = typer.Option(
384
434
  None,
@@ -394,15 +444,34 @@ def intake_catalogue(
394
444
  " valid."
395
445
  ),
396
446
  ),
447
+ bbox: Optional[Tuple[float, float, float, float]] = typer.Option(
448
+ None,
449
+ "-b",
450
+ "--bbox",
451
+ help=(
452
+ "Special search facet to refine/subset search results by spatial "
453
+ "extent. This can be a string representation of a bounding box. "
454
+ "The bounding box has to follow the format ``min_lon max_lon "
455
+ "min_lat max_lat``. Valid strings are ``-10 10 -10 10`` to "
456
+ "``0 5 0 5``."
457
+ ),
458
+ ),
459
+ bbox_select: SelectMethod = typer.Option(
460
+ "flexible",
461
+ "-bs",
462
+ "--bbox-select",
463
+ help=SelectMethod.get_help("bbox"),
464
+ ),
397
465
  zarr: bool = typer.Option(
398
466
  False, "--zarr", help="Create zarr stream files, as catalogue targets."
399
467
  ),
400
- access_token: Optional[str] = typer.Option(
468
+ token_file: Optional[Path] = typer.Option(
401
469
  None,
402
- "--access-token",
470
+ "--token-file",
403
471
  help=(
404
- "Use this access token for authentication"
405
- " when creating a zarr based intake catalogue."
472
+ "Instead of authenticating via code based authentication flow "
473
+ "you can set the path to the json file that contains a "
474
+ "`refresh token` containing a refresh_token key."
406
475
  ),
407
476
  ),
408
477
  filename: Optional[Path] = typer.Option(
@@ -444,12 +513,11 @@ def intake_catalogue(
444
513
  result = databrowser(
445
514
  *(facets or []),
446
515
  time=time or "",
447
- time_select=cast(Literal["file", "flexible", "strict"], time_select),
448
- flavour=cast(
449
- Literal["freva", "cmip6", "cmip5", "cordex", "nextgems", "user"],
450
- flavour.value,
451
- ),
452
- uniq_key=cast(Literal["uri", "file"], uniq_key.value),
516
+ time_select=time_select.value,
517
+ bbox=bbox or None,
518
+ bbox_select=bbox_select.value,
519
+ flavour=flavour.value,
520
+ uniq_key=uniq_key.value,
453
521
  host=host,
454
522
  fail_on_error=False,
455
523
  multiversion=multiversion,
@@ -457,13 +525,145 @@ def intake_catalogue(
457
525
  **(parse_cli_args(search_keys or [])),
458
526
  )
459
527
  if zarr:
460
- _auth(result._cfg.auth_url, access_token)
528
+ Auth(token_file).authenticate(host=host, _cli=True)
461
529
  with NamedTemporaryFile(suffix=".json") as temp_f:
462
530
  result._create_intake_catalogue_file(str(filename or temp_f.name))
463
531
  if not filename:
464
532
  print(Path(temp_f.name).read_text())
465
533
 
466
534
 
535
+ @databrowser_app.command(
536
+ name="stac-catalogue", help="Create a static STAC catalogue from the search."
537
+ )
538
+ @exception_handler
539
+ def stac_catalogue(
540
+ search_keys: Optional[List[str]] = typer.Argument(
541
+ default=None,
542
+ help="Refine your data search with this `key=value` pair search "
543
+ "parameters. The parameters could be, depending on the DRS standard, "
544
+ "flavour product, project model etc.",
545
+ ),
546
+ facets: Optional[List[str]] = typer.Option(
547
+ None,
548
+ "--facet",
549
+ help=(
550
+ "If you are not sure about the correct search key's you can use"
551
+ " the ``--facet`` flag to search of any matching entries. For "
552
+ "example --facet 'era5' would allow you to search for any entries"
553
+ " containing era5, regardless of project, product etc."
554
+ ),
555
+ ),
556
+ uniq_key: UniqKeys = typer.Option(
557
+ "file",
558
+ "--uniq-key",
559
+ "-u",
560
+ help=(
561
+ "The type of search result, which can be either “file” "
562
+ "or “uri”. This parameter determines whether the search will be "
563
+ "based on file paths or Uniform Resource Identifiers"
564
+ ),
565
+ ),
566
+ flavour: Flavours = typer.Option(
567
+ "freva",
568
+ "--flavour",
569
+ "-f",
570
+ help=(
571
+ "The Data Reference Syntax (DRS) standard specifying the type "
572
+ "of climate datasets to query."
573
+ ),
574
+ ),
575
+ time_select: SelectMethod = typer.Option(
576
+ "flexible",
577
+ "-ts",
578
+ "--time-select",
579
+ help=SelectMethod.get_help("time"),
580
+ ),
581
+ time: Optional[str] = typer.Option(
582
+ None,
583
+ "-t",
584
+ "--time",
585
+ help=(
586
+ "Special search facet to refine/subset search results by time. "
587
+ "This can be a string representation of a time range or a single "
588
+ "time step. The time steps have to follow ISO-8601. Valid strings "
589
+ "are ``%Y-%m-%dT%H:%M`` to ``%Y-%m-%dT%H:%M`` for time ranges and "
590
+ "``%Y-%m-%dT%H:%M``. **Note**: You don't have to give the full "
591
+ "string format to subset time steps ``%Y``, ``%Y-%m`` etc are also"
592
+ " valid."
593
+ ),
594
+ ),
595
+ bbox: Optional[Tuple[float, float, float, float]] = typer.Option(
596
+ None,
597
+ "-b",
598
+ "--bbox",
599
+ help=(
600
+ "Special search facet to refine/subset search results by spatial "
601
+ "extent. This can be a string representation of a bounding box. "
602
+ "The bounding box has to follow the format ``min_lon max_lon "
603
+ "min_lat max_lat``. Valid strings are ``-10 10 -10 10`` to "
604
+ "``0 5 0 5``."
605
+ ),
606
+ ),
607
+ bbox_select: SelectMethod = typer.Option(
608
+ "flexible",
609
+ "-bs",
610
+ "--bbox-select",
611
+ help=SelectMethod.get_help("bbox"),
612
+ ),
613
+ host: Optional[str] = typer.Option(
614
+ None,
615
+ "--host",
616
+ help=(
617
+ "Set the hostname of the databrowser, if not set (default) "
618
+ "the hostname is read from a config file"
619
+ ),
620
+ ),
621
+ verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
622
+ multiversion: bool = typer.Option(
623
+ False,
624
+ "--multi-version",
625
+ help="Select all versions and not just the latest version (default).",
626
+ ),
627
+ version: Optional[bool] = typer.Option(
628
+ False,
629
+ "-V",
630
+ "--version",
631
+ help="Show version an exit",
632
+ callback=version_callback,
633
+ ),
634
+ filename: Optional[Path] = typer.Option(
635
+ None,
636
+ "-o",
637
+ "--filename",
638
+ help=(
639
+ "Path to the file where the static STAC catalogue, "
640
+ "should be written to. If you don't specify or the path "
641
+ "does not exist, the file will be created in the current "
642
+ "working directory. "
643
+ ),
644
+ ),
645
+ ) -> None:
646
+ """Create a STAC catalogue for climate datasets based on the specified
647
+ Data Reference Syntax (DRS) standard (flavour) and the type of search
648
+ result (uniq_key), which can be either "file" or "uri"."""
649
+ logger.set_verbosity(verbose)
650
+ result = databrowser(
651
+ *(facets or []),
652
+ time=time or "",
653
+ time_select=time_select.value,
654
+ bbox=bbox or None,
655
+ bbox_select=bbox_select.value,
656
+ flavour=flavour.value,
657
+ uniq_key=uniq_key.value,
658
+ host=host,
659
+ fail_on_error=False,
660
+ multiversion=multiversion,
661
+ stream_zarr=False,
662
+ **(parse_cli_args(search_keys or [])),
663
+ )
664
+ print(result.stac_catalogue(filename=filename))
665
+
666
+
467
667
  @databrowser_app.command(
468
668
  name="data-count", help="Count the databrowser search results"
469
669
  )
@@ -500,11 +700,11 @@ def count_values(
500
700
  "of climate datasets to query."
501
701
  ),
502
702
  ),
503
- time_select: TimeSelect = typer.Option(
703
+ time_select: SelectMethod = typer.Option(
504
704
  "flexible",
505
705
  "-ts",
506
706
  "--time-select",
507
- help=TimeSelect.get_help(),
707
+ help=SelectMethod.get_help("time"),
508
708
  ),
509
709
  time: Optional[str] = typer.Option(
510
710
  None,
@@ -520,6 +720,24 @@ def count_values(
520
720
  " valid."
521
721
  ),
522
722
  ),
723
+ bbox: Optional[Tuple[float, float, float, float]] = typer.Option(
724
+ None,
725
+ "-b",
726
+ "--bbox",
727
+ help=(
728
+ "Special search facet to refine/subset search results by spatial "
729
+ "extent. This can be a string representation of a bounding box. "
730
+ "The bounding box has to follow the format ``min_lon max_lon "
731
+ "min_lat max_lat``. Valid strings are ``-10 10 -10 10`` to "
732
+ "``0 5 0 5``."
733
+ ),
734
+ ),
735
+ bbox_select: SelectMethod = typer.Option(
736
+ "flexible",
737
+ "-bs",
738
+ "--bbox-select",
739
+ help=SelectMethod.get_help("bbox"),
740
+ ),
523
741
  extended_search: bool = typer.Option(
524
742
  False,
525
743
  "-e",
@@ -564,16 +782,19 @@ def count_values(
564
782
  result: Union[int, Dict[str, Dict[str, int]]] = 0
565
783
  search_kws = parse_cli_args(search_keys or [])
566
784
  time = cast(str, time or search_kws.pop("time", ""))
785
+ bbox = cast(
786
+ Optional[Tuple[float, float, float, float]],
787
+ bbox or search_kws.pop("bbox", None),
788
+ )
567
789
  facets = facets or []
568
790
  if detail:
569
791
  result = databrowser.count_values(
570
792
  *facets,
571
793
  time=time or "",
572
- time_select=cast(Literal["file", "flexible", "strict"], time_select),
573
- flavour=cast(
574
- Literal["freva", "cmip6", "cmip5", "cordex", "nextgems", "user"],
575
- flavour.value,
576
- ),
794
+ time_select=time_select.value,
795
+ bbox=bbox or None,
796
+ bbox_select=bbox_select.value,
797
+ flavour=flavour.value,
577
798
  host=host,
578
799
  extended_search=extended_search,
579
800
  multiversion=multiversion,
@@ -585,15 +806,10 @@ def count_values(
585
806
  databrowser(
586
807
  *facets,
587
808
  time=time or "",
588
- time_select=cast(
589
- Literal["file", "flexible", "strict"], time_select
590
- ),
591
- flavour=cast(
592
- Literal[
593
- "freva", "cmip6", "cmip5", "cordex", "nextgems", "user"
594
- ],
595
- flavour.value,
596
- ),
809
+ time_select=time_select.value,
810
+ bbox=bbox or None,
811
+ bbox_select=bbox_select.value,
812
+ flavour=flavour.value,
597
813
  host=host,
598
814
  multiversion=multiversion,
599
815
  fail_on_error=False,
@@ -642,18 +858,21 @@ def user_data_add(
642
858
  "the hostname is read from a config file."
643
859
  ),
644
860
  ),
645
- access_token: Optional[str] = typer.Option(
861
+ token_file: Optional[Path] = typer.Option(
646
862
  None,
647
- "--access-token",
648
- help="Access token for authentication when adding user data.",
863
+ "--token-file",
864
+ help=(
865
+ "Instead of authenticating via code based authentication flow "
866
+ "you can set the path to the json file that contains a "
867
+ "`refresh token` containing a refresh_token key."
868
+ ),
649
869
  ),
650
870
  verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
651
871
  ) -> None:
652
872
  """Add user data into the databrowser."""
653
873
  logger.set_verbosity(verbose)
654
874
  logger.debug("Checking if the user has the right to add data")
655
- result = databrowser(host=host)
656
- _auth(result._cfg.auth_url, access_token)
875
+ Auth(token_file).authenticate(host=host, _cli=True)
657
876
 
658
877
  facet_dict = {}
659
878
  if facets:
@@ -695,18 +914,21 @@ def user_data_delete(
695
914
  "the hostname is read from a config file."
696
915
  ),
697
916
  ),
698
- access_token: Optional[str] = typer.Option(
917
+ token_file: Optional[Path] = typer.Option(
699
918
  None,
700
- "--access-token",
701
- help="Access token for authentication when deleting user data.",
919
+ "--token-file",
920
+ help=(
921
+ "Instead of authenticating via code based authentication flow "
922
+ "you can set the path to the json file that contains a "
923
+ "`refresh token` containing a refresh_token key."
924
+ ),
702
925
  ),
703
926
  verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
704
927
  ) -> None:
705
928
  """Delete user data from the databrowser."""
706
929
  logger.set_verbosity(verbose)
707
930
  logger.debug("Checking if the user has the right to delete data")
708
- result = databrowser(host=host)
709
- _auth(result._cfg.auth_url, access_token)
931
+ Auth(token_file).authenticate(host=host, _cli=True)
710
932
 
711
933
  search_key_dict = {}
712
934
  if search_keys: