freva-client 2508.0.0__py3-none-any.whl → 2509.1.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,16 +7,21 @@ 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, Optional, Tuple, Union, cast
10
+ from typing import Any, Dict, List, Optional, Tuple, Union, cast
11
11
 
12
12
  import typer
13
+ import typer.models
13
14
  import xarray as xr
14
15
 
16
+ # import pprinr from rich
17
+ from rich import print as pprint
18
+
15
19
  from freva_client import databrowser
16
20
  from freva_client.auth import Auth
17
21
  from freva_client.utils import exception_handler, logger
22
+ from freva_client.utils.auth_utils import requires_authentication
18
23
 
19
- from .cli_utils import parse_cli_args, version_callback
24
+ from .cli_utils import parse_cli_args, print_df, version_callback
20
25
 
21
26
 
22
27
  class UniqKeys(str, Enum):
@@ -26,14 +31,18 @@ class UniqKeys(str, Enum):
26
31
  uri = "uri"
27
32
 
28
33
 
29
- class Flavours(str, Enum):
30
- """Literal implementation for the cli."""
34
+ class FlavourAction(str, Enum):
35
+ add = "add"
36
+ delete = "delete"
37
+ list = "list"
38
+
31
39
 
40
+ class BuiltinFlavours(str, Enum):
41
+ """Literal implementation for the cli."""
32
42
  freva = "freva"
33
43
  cmip6 = "cmip6"
34
44
  cmip5 = "cmip5"
35
45
  cordex = "cordex"
36
- nextgems = "nextgems"
37
46
  user = "user"
38
47
 
39
48
 
@@ -131,10 +140,9 @@ def metadata_search(
131
140
  " containing era5, regardless of project, product etc."
132
141
  ),
133
142
  ),
134
- flavour: Flavours = typer.Option(
135
- "freva",
143
+ flavour: Optional[str] = typer.Option(
144
+ None,
136
145
  "--flavour",
137
- "-f",
138
146
  help=(
139
147
  "The Data Reference Syntax (DRS) standard specifying the type "
140
148
  "of climate datasets to query."
@@ -200,6 +208,16 @@ def metadata_search(
200
208
  parse_json: bool = typer.Option(
201
209
  False, "-j", "--json", help="Parse output in json format."
202
210
  ),
211
+ token_file: Optional[Path] = typer.Option(
212
+ None,
213
+ "--token-file",
214
+ "-tf",
215
+ help=(
216
+ "Instead of authenticating via code based authentication flow "
217
+ "you can set the path to the json file that contains a "
218
+ "`refresh token` containing a refresh_token key."
219
+ ),
220
+ ),
203
221
  verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
204
222
  version: Optional[bool] = typer.Option(
205
223
  False,
@@ -219,13 +237,15 @@ def metadata_search(
219
237
  """
220
238
  logger.set_verbosity(verbose)
221
239
  logger.debug("Search the databrowser")
240
+ if requires_authentication(flavour=flavour, databrowser_url=host):
241
+ Auth(token_file).authenticate(host=host, _cli=True)
222
242
  result = databrowser.metadata_search(
223
243
  *(facets or []),
224
244
  time=time or "",
225
245
  time_select=time_select.value,
226
246
  bbox=bbox or None,
227
247
  bbox_select=bbox_select.value,
228
- flavour=flavour.value,
248
+ flavour=flavour,
229
249
  host=host,
230
250
  extended_search=extended_search,
231
251
  multiversion=multiversion,
@@ -233,10 +253,9 @@ def metadata_search(
233
253
  **(parse_cli_args(search_keys or [])),
234
254
  )
235
255
  if parse_json:
236
- print(json.dumps(result))
256
+ print(result.to_json())
237
257
  return
238
- for key, values in result.items():
239
- print(f"{key}: {', '.join(values)}")
258
+ print_df(result)
240
259
 
241
260
 
242
261
  @databrowser_app.command(
@@ -270,10 +289,9 @@ def data_search(
270
289
  "based on file paths or Uniform Resource Identifiers"
271
290
  ),
272
291
  ),
273
- flavour: Flavours = typer.Option(
274
- "freva",
292
+ flavour: Optional[str] = typer.Option(
293
+ None,
275
294
  "--flavour",
276
- "-f",
277
295
  help=(
278
296
  "The Data Reference Syntax (DRS) standard specifying the type "
279
297
  "of climate datasets to query."
@@ -361,13 +379,14 @@ def data_search(
361
379
  """
362
380
  logger.set_verbosity(verbose)
363
381
  logger.debug("Search the databrowser")
382
+
364
383
  result = databrowser(
365
384
  *(facets or []),
366
385
  time=time or "",
367
386
  time_select=time_select.value,
368
387
  bbox=bbox or None,
369
388
  bbox_select=bbox_select.value,
370
- flavour=flavour.value,
389
+ flavour=flavour,
371
390
  uniq_key=uniq_key.value,
372
391
  host=host,
373
392
  fail_on_error=False,
@@ -375,7 +394,7 @@ def data_search(
375
394
  stream_zarr=zarr,
376
395
  **(parse_cli_args(search_keys or [])),
377
396
  )
378
- if zarr:
397
+ if requires_authentication(flavour=flavour, zarr=zarr, databrowser_url=host):
379
398
  Auth(token_file).authenticate(host=host, _cli=True)
380
399
  if parse_json:
381
400
  print(json.dumps(sorted(result)))
@@ -415,10 +434,9 @@ def intake_catalogue(
415
434
  "based on file paths or Uniform Resource Identifiers"
416
435
  ),
417
436
  ),
418
- flavour: Flavours = typer.Option(
419
- "freva",
437
+ flavour: Optional[str] = typer.Option(
438
+ None,
420
439
  "--flavour",
421
- "-f",
422
440
  help=(
423
441
  "The Data Reference Syntax (DRS) standard specifying the type "
424
442
  "of climate datasets to query."
@@ -508,7 +526,7 @@ def intake_catalogue(
508
526
  ) -> None:
509
527
  """Create an intake catalogue for climate datasets based on the specified "
510
528
  "Data Reference Syntax (DRS) standard (flavour) and the type of search "
511
- result (uniq_key), which can be either file or uri”."""
529
+ result (uniq_key), which can be either "file" or "uri"."""
512
530
  logger.set_verbosity(verbose)
513
531
  logger.debug("Search the databrowser")
514
532
  result = databrowser(
@@ -517,7 +535,7 @@ def intake_catalogue(
517
535
  time_select=time_select.value,
518
536
  bbox=bbox or None,
519
537
  bbox_select=bbox_select.value,
520
- flavour=flavour.value,
538
+ flavour=flavour,
521
539
  uniq_key=uniq_key.value,
522
540
  host=host,
523
541
  fail_on_error=False,
@@ -525,7 +543,7 @@ def intake_catalogue(
525
543
  stream_zarr=zarr,
526
544
  **(parse_cli_args(search_keys or [])),
527
545
  )
528
- if zarr:
546
+ if requires_authentication(flavour=flavour, zarr=zarr, databrowser_url=host):
529
547
  Auth(token_file).authenticate(host=host, _cli=True)
530
548
  with NamedTemporaryFile(suffix=".json") as temp_f:
531
549
  result._create_intake_catalogue_file(str(filename or temp_f.name))
@@ -564,10 +582,9 @@ def stac_catalogue(
564
582
  "based on file paths or Uniform Resource Identifiers"
565
583
  ),
566
584
  ),
567
- flavour: Flavours = typer.Option(
568
- "freva",
585
+ flavour: Optional[str] = typer.Option(
586
+ None,
569
587
  "--flavour",
570
- "-f",
571
588
  help=(
572
589
  "The Data Reference Syntax (DRS) standard specifying the type "
573
590
  "of climate datasets to query."
@@ -619,6 +636,16 @@ def stac_catalogue(
619
636
  "the hostname is read from a config file"
620
637
  ),
621
638
  ),
639
+ token_file: Optional[Path] = typer.Option(
640
+ None,
641
+ "--token-file",
642
+ "-tf",
643
+ help=(
644
+ "Instead of authenticating via code based authentication flow "
645
+ "you can set the path to the json file that contains a "
646
+ "`refresh token` containing a refresh_token key."
647
+ ),
648
+ ),
622
649
  verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
623
650
  multiversion: bool = typer.Option(
624
651
  False,
@@ -654,7 +681,7 @@ def stac_catalogue(
654
681
  time_select=time_select.value,
655
682
  bbox=bbox or None,
656
683
  bbox_select=bbox_select.value,
657
- flavour=flavour.value,
684
+ flavour=flavour,
658
685
  uniq_key=uniq_key.value,
659
686
  host=host,
660
687
  fail_on_error=False,
@@ -662,6 +689,8 @@ def stac_catalogue(
662
689
  stream_zarr=False,
663
690
  **(parse_cli_args(search_keys or [])),
664
691
  )
692
+ if requires_authentication(flavour=flavour, databrowser_url=host):
693
+ Auth(token_file).authenticate(host=host, _cli=True)
665
694
  print(result.stac_catalogue(filename=filename))
666
695
 
667
696
 
@@ -692,10 +721,9 @@ def count_values(
692
721
  "-d",
693
722
  help=("Separate the count by search facets."),
694
723
  ),
695
- flavour: Flavours = typer.Option(
696
- "freva",
724
+ flavour: Optional[str] = typer.Option(
725
+ None,
697
726
  "--flavour",
698
- "-f",
699
727
  help=(
700
728
  "The Data Reference Syntax (DRS) standard specifying the type "
701
729
  "of climate datasets to query."
@@ -761,6 +789,16 @@ def count_values(
761
789
  parse_json: bool = typer.Option(
762
790
  False, "-j", "--json", help="Parse output in json format."
763
791
  ),
792
+ token_file: Optional[Path] = typer.Option(
793
+ None,
794
+ "--token-file",
795
+ "-tf",
796
+ help=(
797
+ "Instead of authenticating via code based authentication flow "
798
+ "you can set the path to the json file that contains a "
799
+ "`refresh token` containing a refresh_token key."
800
+ ),
801
+ ),
764
802
  verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
765
803
  version: Optional[bool] = typer.Option(
766
804
  False,
@@ -788,6 +826,8 @@ def count_values(
788
826
  bbox or search_kws.pop("bbox", None),
789
827
  )
790
828
  facets = facets or []
829
+ if requires_authentication(flavour=flavour, databrowser_url=host):
830
+ Auth(token_file).authenticate(host=host, _cli=True)
791
831
  if detail:
792
832
  result = databrowser.count_values(
793
833
  *facets,
@@ -795,7 +835,7 @@ def count_values(
795
835
  time_select=time_select.value,
796
836
  bbox=bbox or None,
797
837
  bbox_select=bbox_select.value,
798
- flavour=flavour.value,
838
+ flavour=flavour,
799
839
  host=host,
800
840
  extended_search=extended_search,
801
841
  multiversion=multiversion,
@@ -810,7 +850,7 @@ def count_values(
810
850
  time_select=time_select.value,
811
851
  bbox=bbox or None,
812
852
  bbox_select=bbox_select.value,
813
- flavour=flavour.value,
853
+ flavour=flavour,
814
854
  host=host,
815
855
  multiversion=multiversion,
816
856
  fail_on_error=False,
@@ -945,3 +985,163 @@ def user_data_delete(
945
985
  key, value = search_key.split("=", 1)
946
986
  search_key_dict[key] = value
947
987
  databrowser.userdata(action="delete", metadata=search_key_dict, host=host)
988
+
989
+
990
+ flavour_app = typer.Typer(help="Manage custom flavours")
991
+ databrowser_app.add_typer(flavour_app, name="flavour")
992
+
993
+
994
+ @flavour_app.command("add", help="Add a new custom flavour.")
995
+ @exception_handler
996
+ def flavour_add(
997
+ name: str = typer.Argument(..., help="Name of the flavour to add"),
998
+ mapping: List[str] = typer.Option(
999
+ [],
1000
+ "--map",
1001
+ "-m",
1002
+ help="Key-value mappings in the format key=value",
1003
+ ),
1004
+ global_: bool = typer.Option(
1005
+ False,
1006
+ "--global",
1007
+ help="Make the flavour available to all users (requires admin privileges)",
1008
+ ),
1009
+ host: Optional[str] = typer.Option(
1010
+ None,
1011
+ "--host",
1012
+ help=(
1013
+ "Set the hostname of the databrowser. If not set (default), "
1014
+ "the hostname is read from a config file."
1015
+ ),
1016
+ ),
1017
+ token_file: Optional[Path] = typer.Option(
1018
+ None,
1019
+ "--token-file",
1020
+ "-tf",
1021
+ help=(
1022
+ "Instead of authenticating via code based authentication flow "
1023
+ "you can set the path to the json file that contains a "
1024
+ "`refresh token` containing a refresh_token key."
1025
+ ),
1026
+ ),
1027
+ verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
1028
+ ) -> None:
1029
+ """Add a new custom flavour to the databrowser.
1030
+ This command allows user to create custom DRS (Data Reference Syntax)
1031
+ flavours that define how search facets are mapped to different data
1032
+ standards."""
1033
+ logger.set_verbosity(verbose)
1034
+ logger.debug(f"Adding flavour '{name}' with mapping {mapping} and global={global_}")
1035
+ Auth(token_file).authenticate(host=host, _cli=True)
1036
+ mapping_dict = {}
1037
+ for map_item in mapping:
1038
+ if "=" not in map_item:
1039
+ logger.error(
1040
+ f"Invalid mapping format: {map_item}. Expected format: key=value."
1041
+ )
1042
+ raise typer.Exit(code=1)
1043
+ key, value = map_item.split("=", 1)
1044
+ mapping_dict[key] = value
1045
+
1046
+ if not mapping_dict:
1047
+ logger.error("At least one mapping must be provided using --map")
1048
+ raise typer.Exit(code=1)
1049
+
1050
+ databrowser.flavour(
1051
+ action="add",
1052
+ name=name,
1053
+ mapping=mapping_dict,
1054
+ is_global=global_,
1055
+ host=host,
1056
+ )
1057
+
1058
+
1059
+ @flavour_app.command("delete", help="Delete an existing custom flavour.")
1060
+ @exception_handler
1061
+ def flavour_delete(
1062
+ name: str = typer.Argument(..., help="Name of the flavour to delete"),
1063
+ global_: bool = typer.Option(
1064
+ False,
1065
+ "--global",
1066
+ help="Delete global flavour (requires admin privileges)",
1067
+ ),
1068
+ host: Optional[str] = typer.Option(
1069
+ None,
1070
+ "--host",
1071
+ help=(
1072
+ "Set the hostname of the databrowser. If not set (default), "
1073
+ "the hostname is read from a config file."
1074
+ ),
1075
+ ),
1076
+ token_file: Optional[Path] = typer.Option(
1077
+ None,
1078
+ "--token-file",
1079
+ "-tf",
1080
+ help=(
1081
+ "Instead of authenticating via code based authentication flow "
1082
+ "you can set the path to the json file that contains a "
1083
+ "`refresh token` containing a refresh_token key."
1084
+ ),
1085
+ ),
1086
+ verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
1087
+ ) -> None:
1088
+ """Delete a custom flavour from the databrowser.
1089
+ This command removes a custom flavour that was previously created.
1090
+ You can only delete flavours that you own, unless you are an admin
1091
+ user who can delete global flavours."""
1092
+ logger.set_verbosity(verbose)
1093
+ logger.debug(f"Deleting flavour '{name}'")
1094
+ Auth(token_file).authenticate(host=host, _cli=True)
1095
+
1096
+ databrowser.flavour(
1097
+ action="delete",
1098
+ name=name,
1099
+ is_global=global_,
1100
+ host=host,
1101
+ )
1102
+
1103
+
1104
+ @flavour_app.command("list", help="List all available custom flavours.")
1105
+ @exception_handler
1106
+ def flavour_list(
1107
+ host: Optional[str] = typer.Option(
1108
+ None,
1109
+ "--host",
1110
+ help=(
1111
+ "Set the hostname of the databrowser. If not set (default), "
1112
+ "the hostname is read from a config file."
1113
+ ),
1114
+ ),
1115
+ parse_json: bool = typer.Option(
1116
+ False, "-j", "--json", help="Parse output in json format."
1117
+ ),
1118
+ token_file: Optional[Path] = typer.Option(
1119
+ None,
1120
+ "--token-file",
1121
+ "-tf",
1122
+ help=(
1123
+ "Instead of authenticating via code based authentication flow "
1124
+ "you can set the path to the json file that contains a "
1125
+ "`refresh token` containing a refresh_token key."
1126
+ ),
1127
+ ),
1128
+ verbose: int = typer.Option(0, "-v", help="Increase verbosity", count=True),
1129
+ ) -> None:
1130
+ """List all custom flavours available to the current user.
1131
+ This command shows both personal flavours created by the user and
1132
+ global flavours available to all users."""
1133
+ logger.set_verbosity(verbose)
1134
+ logger.debug("Listing custom flavours")
1135
+ results = cast(Dict[str, Any], databrowser.flavour(action="list", host=host))
1136
+ flavours_json = json.dumps(results["flavours"], indent=2)
1137
+ if parse_json:
1138
+ print(flavours_json)
1139
+ else:
1140
+ pprint(f"[yellow]{results.get('Note', '')}[/yellow]")
1141
+ print("🎨 Available Data Reference Syntax (DRS) Flavours")
1142
+ print("=" * 55)
1143
+ for flavour in results["flavours"]:
1144
+ owner_icon = "🌐" if flavour['owner'] == "global" else "👤"
1145
+ print(f" {owner_icon} {flavour['flavour_name']} (by {flavour['owner']})")
1146
+ print(f" Mapping: {flavour['mapping']}")
1147
+ print()