esgvoc 0.4.0__py3-none-any.whl → 1.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 esgvoc might be problematic. Click here for more details.

Files changed (73) hide show
  1. esgvoc/__init__.py +1 -1
  2. esgvoc/api/data_descriptors/__init__.py +50 -28
  3. esgvoc/api/data_descriptors/activity.py +3 -3
  4. esgvoc/api/data_descriptors/area_label.py +16 -1
  5. esgvoc/api/data_descriptors/branded_suffix.py +20 -0
  6. esgvoc/api/data_descriptors/branded_variable.py +12 -0
  7. esgvoc/api/data_descriptors/consortium.py +14 -13
  8. esgvoc/api/data_descriptors/contact.py +5 -0
  9. esgvoc/api/data_descriptors/conventions.py +6 -0
  10. esgvoc/api/data_descriptors/creation_date.py +5 -0
  11. esgvoc/api/data_descriptors/data_descriptor.py +14 -9
  12. esgvoc/api/data_descriptors/data_specs_version.py +5 -0
  13. esgvoc/api/data_descriptors/date.py +1 -1
  14. esgvoc/api/data_descriptors/directory_date.py +1 -1
  15. esgvoc/api/data_descriptors/experiment.py +13 -11
  16. esgvoc/api/data_descriptors/forcing_index.py +1 -1
  17. esgvoc/api/data_descriptors/frequency.py +3 -3
  18. esgvoc/api/data_descriptors/further_info_url.py +5 -0
  19. esgvoc/api/data_descriptors/grid_label.py +2 -2
  20. esgvoc/api/data_descriptors/horizontal_label.py +15 -1
  21. esgvoc/api/data_descriptors/initialisation_index.py +1 -1
  22. esgvoc/api/data_descriptors/institution.py +8 -5
  23. esgvoc/api/data_descriptors/known_branded_variable.py +23 -0
  24. esgvoc/api/data_descriptors/license.py +3 -3
  25. esgvoc/api/data_descriptors/mip_era.py +1 -1
  26. esgvoc/api/data_descriptors/model_component.py +1 -1
  27. esgvoc/api/data_descriptors/obs_type.py +5 -0
  28. esgvoc/api/data_descriptors/organisation.py +1 -1
  29. esgvoc/api/data_descriptors/physic_index.py +1 -1
  30. esgvoc/api/data_descriptors/product.py +2 -2
  31. esgvoc/api/data_descriptors/publication_status.py +5 -0
  32. esgvoc/api/data_descriptors/realisation_index.py +1 -1
  33. esgvoc/api/data_descriptors/realm.py +1 -1
  34. esgvoc/api/data_descriptors/region.py +5 -0
  35. esgvoc/api/data_descriptors/resolution.py +3 -3
  36. esgvoc/api/data_descriptors/source.py +9 -5
  37. esgvoc/api/data_descriptors/source_type.py +1 -1
  38. esgvoc/api/data_descriptors/table.py +3 -2
  39. esgvoc/api/data_descriptors/temporal_label.py +15 -1
  40. esgvoc/api/data_descriptors/time_range.py +4 -3
  41. esgvoc/api/data_descriptors/title.py +5 -0
  42. esgvoc/api/data_descriptors/tracking_id.py +5 -0
  43. esgvoc/api/data_descriptors/variable.py +25 -12
  44. esgvoc/api/data_descriptors/variant_label.py +3 -3
  45. esgvoc/api/data_descriptors/vertical_label.py +14 -0
  46. esgvoc/api/project_specs.py +117 -2
  47. esgvoc/api/projects.py +242 -279
  48. esgvoc/api/search.py +30 -3
  49. esgvoc/api/universe.py +42 -27
  50. esgvoc/apps/jsg/cmip6_template.json +74 -0
  51. esgvoc/apps/jsg/cmip6plus_template.json +74 -0
  52. esgvoc/apps/jsg/json_schema_generator.py +185 -0
  53. esgvoc/cli/config.py +500 -0
  54. esgvoc/cli/find.py +138 -0
  55. esgvoc/cli/get.py +43 -38
  56. esgvoc/cli/main.py +10 -3
  57. esgvoc/cli/status.py +27 -18
  58. esgvoc/cli/valid.py +10 -15
  59. esgvoc/core/db/models/project.py +11 -11
  60. esgvoc/core/db/models/universe.py +3 -3
  61. esgvoc/core/db/project_ingestion.py +40 -40
  62. esgvoc/core/db/universe_ingestion.py +36 -33
  63. esgvoc/core/logging_handler.py +24 -2
  64. esgvoc/core/repo_fetcher.py +61 -59
  65. esgvoc/core/service/data_merger.py +47 -34
  66. esgvoc/core/service/state.py +107 -83
  67. {esgvoc-0.4.0.dist-info → esgvoc-1.0.0.dist-info}/METADATA +7 -20
  68. esgvoc-1.0.0.dist-info/RECORD +95 -0
  69. esgvoc/core/logging.conf +0 -21
  70. esgvoc-0.4.0.dist-info/RECORD +0 -80
  71. {esgvoc-0.4.0.dist-info → esgvoc-1.0.0.dist-info}/WHEEL +0 -0
  72. {esgvoc-0.4.0.dist-info → esgvoc-1.0.0.dist-info}/entry_points.txt +0 -0
  73. {esgvoc-0.4.0.dist-info → esgvoc-1.0.0.dist-info}/licenses/LICENSE.txt +0 -0
esgvoc/cli/config.py ADDED
@@ -0,0 +1,500 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import List, Optional
4
+
5
+ import toml
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.syntax import Syntax
9
+ from rich.table import Table
10
+
11
+ import esgvoc.core.service as service
12
+ from esgvoc.core.service.configuration.setting import ServiceSettings
13
+
14
+ app = typer.Typer()
15
+ console = Console()
16
+
17
+
18
+ def display(table):
19
+ """
20
+ Function to display a rich table in the console.
21
+
22
+ :param table: The table to be displayed
23
+ """
24
+ console = Console(record=True, width=200)
25
+ console.print(table)
26
+
27
+
28
+ @app.command()
29
+ def list():
30
+ """
31
+ List all available configurations.
32
+
33
+ Displays all available configurations along with the active one.
34
+ """
35
+ config_manager = service.get_config_manager()
36
+ configs = config_manager.list_configs()
37
+ active_config = config_manager.get_active_config_name()
38
+
39
+ table = Table(title="Available Configurations")
40
+ table.add_column("Name", style="cyan")
41
+ table.add_column("Path", style="green")
42
+ table.add_column("Status", style="magenta")
43
+
44
+ for name, path in configs.items():
45
+ status = "🟢 Active" if name == active_config else ""
46
+ table.add_row(name, path, status)
47
+
48
+ display(table)
49
+
50
+
51
+ @app.command()
52
+ def show(
53
+ name: Optional[str] = typer.Argument(
54
+ None, help="Name of the configuration to show. If not provided, shows the active configuration."
55
+ ),
56
+ ):
57
+ """
58
+ Show the content of a specific configuration.
59
+
60
+ Args:
61
+ name: Name of the configuration to show. Shows the active configuration if not specified.
62
+ """
63
+ config_manager = service.get_config_manager()
64
+ if name is None:
65
+ name = config_manager.get_active_config_name()
66
+ console.print(f"Showing active configuration: [cyan]{name}[/cyan]")
67
+
68
+ configs = config_manager.list_configs()
69
+ if name not in configs:
70
+ console.print(f"[red]Error: Configuration '{name}' not found.[/red]")
71
+ raise typer.Exit(1)
72
+
73
+ config_path = configs[name]
74
+ try:
75
+ with open(config_path, "r") as f:
76
+ content = f.read()
77
+
78
+ syntax = Syntax(content, "toml", theme="monokai", line_numbers=True)
79
+ console.print(syntax)
80
+ except Exception as e:
81
+ console.print(f"[red]Error reading configuration file: {str(e)}[/red]")
82
+ raise typer.Exit(1)
83
+
84
+
85
+ @app.command()
86
+ def switch(name: str = typer.Argument(..., help="Name of the configuration to switch to.")):
87
+ """
88
+ Switch to a different configuration.
89
+
90
+ Args:
91
+ name: Name of the configuration to switch to.
92
+ """
93
+ config_manager = service.get_config_manager()
94
+ configs = config_manager.list_configs()
95
+
96
+ if name not in configs:
97
+ console.print(f"[red]Error: Configuration '{name}' not found.[/red]")
98
+ raise typer.Exit(1)
99
+
100
+ try:
101
+ config_manager.switch_config(name)
102
+ console.print(f"[green]Successfully switched to configuration: [cyan]{name}[/cyan][/green]")
103
+
104
+ # Reset the state to use the new configuration
105
+ service.current_state = service.get_state()
106
+ except Exception as e:
107
+ console.print(f"[red]Error switching configuration: {str(e)}[/red]")
108
+ raise typer.Exit(1)
109
+
110
+
111
+ @app.command()
112
+ def create(
113
+ name: str = typer.Argument(..., help="Name for the new configuration."),
114
+ base: Optional[str] = typer.Option(
115
+ None, "--base", "-b", help="Base the new configuration on an existing one. Uses the default if not specified."
116
+ ),
117
+ switch_to: bool = typer.Option(False, "--switch", "-s", help="Switch to the new configuration after creating it."),
118
+ ):
119
+ """
120
+ Create a new configuration.
121
+
122
+ Args:
123
+ name: Name for the new configuration.
124
+ base: Base the new configuration on an existing one. Uses the default if not specified.
125
+ switch_to: Switch to the new configuration after creating it.
126
+ """
127
+ config_manager = service.get_config_manager()
128
+ configs = config_manager.list_configs()
129
+
130
+ if name in configs:
131
+ console.print(f"[red]Error: Configuration '{name}' already exists.[/red]")
132
+ raise typer.Exit(1)
133
+
134
+ if base and base not in configs:
135
+ console.print(f"[red]Error: Base configuration '{base}' not found.[/red]")
136
+ raise typer.Exit(1)
137
+
138
+ try:
139
+ if base:
140
+ # Load the base configuration
141
+ base_config = config_manager.get_config(base)
142
+ config_data = base_config.dump()
143
+ else:
144
+ # Use default settings
145
+ config_data = ServiceSettings.DEFAULT_SETTINGS
146
+
147
+ # Add the new configuration
148
+ config_manager.add_config(name, config_data)
149
+ console.print(f"[green]Successfully created configuration: [cyan]{name}[/cyan][/green]")
150
+
151
+ if switch_to:
152
+ config_manager.switch_config(name)
153
+ console.print(f"[green]Switched to configuration: [cyan]{name}[/cyan][/green]")
154
+ # Reset the state to use the new configuration
155
+ service.current_state = service.get_state()
156
+
157
+ except Exception as e:
158
+ console.print(f"[red]Error creating configuration: {str(e)}[/red]")
159
+ raise typer.Exit(1)
160
+
161
+
162
+ @app.command()
163
+ def remove(name: str = typer.Argument(..., help="Name of the configuration to remove.")):
164
+ """
165
+ Remove a configuration.
166
+
167
+ Args:
168
+ name: Name of the configuration to remove.
169
+ """
170
+ config_manager = service.get_config_manager()
171
+ configs = config_manager.list_configs()
172
+
173
+ if name not in configs:
174
+ console.print(f"[red]Error: Configuration '{name}' not found.[/red]")
175
+ raise typer.Exit(1)
176
+
177
+ if name == "default":
178
+ console.print("[red]Error: Cannot remove the default configuration.[/red]")
179
+ raise typer.Exit(1)
180
+
181
+ confirm = typer.confirm(f"Are you sure you want to remove configuration '{name}'?")
182
+ if not confirm:
183
+ console.print("Operation cancelled.")
184
+ return
185
+
186
+ try:
187
+ active_config = config_manager.get_active_config_name()
188
+ config_manager.remove_config(name)
189
+ console.print(f"[green]Successfully removed configuration: [cyan]{name}[/cyan][/green]")
190
+
191
+ if active_config == name:
192
+ console.print("[yellow]Active configuration was removed. Switched to default.[/yellow]")
193
+ # Reset the state to use the default configuration
194
+ service.current_state = service.get_state()
195
+ except Exception as e:
196
+ console.print(f"[red]Error removing configuration: {str(e)}[/red]")
197
+ raise typer.Exit(1)
198
+
199
+
200
+ @app.command()
201
+ def edit(
202
+ name: Optional[str] = typer.Argument(
203
+ None, help="Name of the configuration to edit. Edits the active configuration if not specified."
204
+ ),
205
+ editor: Optional[str] = typer.Option(
206
+ None, "--editor", "-e", help="Editor to use. Uses the system default if not specified."
207
+ ),
208
+ ):
209
+ """
210
+ Edit a configuration using the system's default editor or a specified one.
211
+
212
+ Args:
213
+ name: Name of the configuration to edit. Edits the active configuration if not specified.
214
+ editor: Editor to use. Uses the system default if not specified.
215
+ """
216
+ config_manager = service.get_config_manager()
217
+ if name is None:
218
+ name = config_manager.get_active_config_name()
219
+ console.print(f"Editing active configuration: [cyan]{name}[/cyan]")
220
+
221
+ configs = config_manager.list_configs()
222
+ if name not in configs:
223
+ console.print(f"[red]Error: Configuration '{name}' not found.[/red]")
224
+ raise typer.Exit(1)
225
+
226
+ config_path = configs[name]
227
+
228
+ editor_cmd = editor or os.environ.get("EDITOR", "vim")
229
+ try:
230
+ # Launch the editor properly by using a list of arguments instead of a string
231
+ import subprocess
232
+
233
+ result = subprocess.run([editor_cmd, str(config_path)], check=True)
234
+ if result.returncode == 0:
235
+ console.print(f"[green]Successfully edited configuration: [cyan]{name}[/cyan][/green]")
236
+
237
+ # Reset the state if we edited the active configuration
238
+ if name == config_manager.get_active_config_name():
239
+ service.current_state = service.get_state()
240
+ else:
241
+ console.print("[yellow]Editor exited with an error.[/yellow]")
242
+ except Exception as e:
243
+ console.print(f"[red]Error launching editor: {str(e)}[/red]")
244
+ raise typer.Exit(1)
245
+
246
+
247
+ @app.command()
248
+ def set(
249
+ changes: List[str] = typer.Argument(
250
+ ...,
251
+ help="Changes in format 'component:key=value', where component is 'universe' or a project name. Multiple can be specified.",
252
+ ),
253
+ config_name: Optional[str] = typer.Option(
254
+ None,
255
+ "--config",
256
+ "-c",
257
+ help="Name of the configuration to modify. Modifies the active configuration if not specified.",
258
+ ),
259
+ ):
260
+ """
261
+ Modify configuration settings using a consistent syntax for universe and projects.
262
+
263
+ Args:
264
+ changes: List of changes in format 'component:key=value'. For example:
265
+ 'universe:branch=main' - Change the universe branch
266
+ 'cmip6:github_repo=https://github.com/new/repo' - Change a project's repository
267
+ config_name: Name of the configuration to modify. Modifies the active configuration if not specified.
268
+
269
+ Examples:
270
+ # Change the universe branch in the active configuration
271
+ esgvoc set 'universe:branch=esgvoc_dev'
272
+
273
+ # Change multiple components at once
274
+ esgvoc set 'universe:branch=esgvoc_dev' 'cmip6:branch=esgvoc_dev'
275
+
276
+ # Change settings in a specific configuration
277
+ esgvoc set 'universe:local_path=repos/prod/universe' --config prod
278
+
279
+ # Change the GitHub repository URL for a project
280
+ esgvoc set 'cmip6:github_repo=https://github.com/WCRP-CMIP/CMIP6_CVs_new'
281
+ """
282
+ config_manager = service.get_config_manager()
283
+ if config_name is None:
284
+ config_name = config_manager.get_active_config_name()
285
+ console.print(f"Modifying active configuration: [cyan]{config_name}[/cyan]")
286
+
287
+ configs = config_manager.list_configs()
288
+ if config_name not in configs:
289
+ console.print(f"[red]Error: Configuration '{config_name}' not found.[/red]")
290
+ raise typer.Exit(1)
291
+
292
+ try:
293
+ # Load the configuration
294
+ config = config_manager.get_config(config_name)
295
+ modified = False
296
+
297
+ # Process all changes with the same format
298
+ for change in changes:
299
+ try:
300
+ # Format should be component:setting=value (where component is 'universe' or a project name)
301
+ component_part, setting_part = change.split(":", 1)
302
+ setting_key, setting_value = setting_part.split("=", 1)
303
+
304
+ # Handle universe settings
305
+ if component_part == "universe":
306
+ if setting_key == "github_repo":
307
+ config.universe.github_repo = setting_value
308
+ modified = True
309
+ elif setting_key == "branch":
310
+ config.universe.branch = setting_value
311
+ modified = True
312
+ elif setting_key == "local_path":
313
+ config.universe.local_path = setting_value
314
+ modified = True
315
+ elif setting_key == "db_path":
316
+ config.universe.db_path = setting_value
317
+ modified = True
318
+ else:
319
+ console.print(f"[yellow]Warning: Unknown universe setting '{setting_key}'. Skipping.[/yellow]")
320
+ continue
321
+
322
+ console.print(f"[green]Updated universe.{setting_key} = {setting_value}[/green]")
323
+
324
+ # Handle project settings
325
+ elif component_part in config.projects:
326
+ project = config.projects[component_part]
327
+ if setting_key == "github_repo":
328
+ project.github_repo = setting_value
329
+ elif setting_key == "branch":
330
+ project.branch = setting_value
331
+ elif setting_key == "local_path":
332
+ project.local_path = setting_value
333
+ elif setting_key == "db_path":
334
+ project.db_path = setting_value
335
+ else:
336
+ console.print(f"[yellow]Warning: Unknown project setting '{setting_key}'. Skipping.[/yellow]")
337
+ continue
338
+
339
+ modified = True
340
+ console.print(f"[green]Updated {component_part}.{setting_key} = {setting_value}[/green]")
341
+ else:
342
+ console.print(
343
+ f"[yellow]Warning: Component '{component_part}' not found in configuration. Skipping.[/yellow]"
344
+ )
345
+ continue
346
+
347
+ except ValueError:
348
+ console.print(
349
+ f"[yellow]Warning: Invalid change format '{change}'. Should be 'component:key=value'. Skipping.[/yellow]"
350
+ )
351
+
352
+ if modified:
353
+ # Save the modified configuration
354
+ config_manager.save_active_config(config)
355
+ console.print(f"[green]Successfully updated configuration: [cyan]{config_name}[/cyan][/green]")
356
+
357
+ # Reset the state if we modified the active configuration
358
+ if config_name == config_manager.get_active_config_name():
359
+ service.current_state = service.get_state()
360
+ else:
361
+ console.print("[yellow]No changes were made to the configuration.[/yellow]")
362
+
363
+ except Exception as e:
364
+ console.print(f"[red]Error updating configuration: {str(e)}[/red]")
365
+ raise typer.Exit(1)
366
+
367
+
368
+ @app.command()
369
+ def add_project(
370
+ name: Optional[str] = typer.Argument(
371
+ None, help="Name of the configuration to modify. Modifies the active configuration if not specified."
372
+ ),
373
+ project_name: str = typer.Option(..., "--name", "-n", help="Name of the project to add."),
374
+ github_repo: str = typer.Option(..., "--repo", "-r", help="GitHub repository URL for the project."),
375
+ branch: str = typer.Option("main", "--branch", "-b", help="Branch for the project repository."),
376
+ local_path: Optional[str] = typer.Option(None, "--local", "-l", help="Local path for the project repository."),
377
+ db_path: Optional[str] = typer.Option(None, "--db", "-d", help="Database path for the project."),
378
+ ):
379
+ """
380
+ Add a new project to a configuration.
381
+
382
+ Args:
383
+ name: Name of the configuration to modify. Modifies the active configuration if not specified.
384
+ project_name: Name of the project to add.
385
+ github_repo: GitHub repository URL for the project.
386
+ branch: Branch for the project repository.
387
+ local_path: Local path for the project repository.
388
+ db_path: Database path for the project.
389
+ """
390
+ config_manager = service.get_config_manager()
391
+ if name is None:
392
+ name = config_manager.get_active_config_name()
393
+ console.print(f"Modifying active configuration: [cyan]{name}[/cyan]")
394
+
395
+ configs = config_manager.list_configs()
396
+ if name not in configs:
397
+ console.print(f"[red]Error: Configuration '{name}' not found.[/red]")
398
+ raise typer.Exit(1)
399
+
400
+ try:
401
+ # Load the configuration
402
+ config = config_manager.get_config(name)
403
+
404
+ # Check if project already exists
405
+ if project_name in config.projects:
406
+ console.print(f"[red]Error: Project '{project_name}' already exists in configuration '{name}'.[/red]")
407
+ raise typer.Exit(1)
408
+
409
+ # Set default paths if not provided
410
+ if local_path is None:
411
+ local_path = f"repos/{project_name}"
412
+ if db_path is None:
413
+ db_path = f"dbs/{project_name}.sqlite"
414
+
415
+ # Create the project settings
416
+ from esgvoc.core.service.configuration.setting import ProjectSettings
417
+
418
+ project_settings = ProjectSettings(
419
+ project_name=project_name, github_repo=github_repo, branch=branch, local_path=local_path, db_path=db_path
420
+ )
421
+
422
+ # Add to configuration
423
+ config.projects[project_name] = project_settings
424
+
425
+ # Save the configuration
426
+ config_manager.save_active_config(config)
427
+ console.print(
428
+ f"[green]Successfully added project [cyan]{project_name}[/cyan] to configuration [cyan]{name}[/cyan][/green]"
429
+ )
430
+
431
+ # Reset the state if we modified the active configuration
432
+ if name == config_manager.get_active_config_name():
433
+ service.current_state = service.get_state()
434
+
435
+ except Exception as e:
436
+ console.print(f"[red]Error adding project: {str(e)}[/red]")
437
+ raise typer.Exit(1)
438
+
439
+
440
+ @app.command()
441
+ def remove_project(
442
+ name: Optional[str] = typer.Argument(
443
+ None, help="Name of the configuration to modify. Modifies the active configuration if not specified."
444
+ ),
445
+ project_name: str = typer.Argument(..., help="Name of the project to remove."),
446
+ ):
447
+ """
448
+ Remove a project from a configuration.
449
+
450
+ Args:
451
+ name: Name of the configuration to modify. Modifies the active configuration if not specified.
452
+ project_name: Name of the project to remove.
453
+ """
454
+ config_manager = service.get_config_manager()
455
+ if name is None:
456
+ name = config_manager.get_active_config_name()
457
+ console.print(f"Modifying active configuration: [cyan]{name}[/cyan]")
458
+
459
+ configs = config_manager.list_configs()
460
+ if name not in configs:
461
+ console.print(f"[red]Error: Configuration '{name}' not found.[/red]")
462
+ raise typer.Exit(1)
463
+
464
+ try:
465
+ # Load the configuration
466
+ config = config_manager.get_config(name)
467
+
468
+ # Check if project exists
469
+ if project_name not in config.projects:
470
+ console.print(f"[red]Error: Project '{project_name}' not found in configuration '{name}'.[/red]")
471
+ raise typer.Exit(1)
472
+
473
+ # Confirm removal
474
+ confirm = typer.confirm(
475
+ f"Are you sure you want to remove project '{project_name}' from configuration '{name}'?"
476
+ )
477
+ if not confirm:
478
+ console.print("Operation cancelled.")
479
+ return
480
+
481
+ # Remove project
482
+ del config.projects[project_name]
483
+
484
+ # Save the configuration
485
+ config_manager.save_active_config(config)
486
+ console.print(
487
+ f"[green]Successfully removed project [cyan]{project_name}[/cyan] from configuration [cyan]{name}[/cyan][/green]"
488
+ )
489
+
490
+ # Reset the state if we modified the active configuration
491
+ if name == config_manager.get_active_config_name():
492
+ service.current_state = service.get_state()
493
+
494
+ except Exception as e:
495
+ console.print(f"[red]Error removing project: {str(e)}[/red]")
496
+ raise typer.Exit(1)
497
+
498
+
499
+ if __name__ == "__main__":
500
+ app()
esgvoc/cli/find.py ADDED
@@ -0,0 +1,138 @@
1
+ import re
2
+ from typing import Any
3
+
4
+ import typer
5
+ from pydantic import BaseModel
6
+ from requests import logging
7
+ from rich.console import Console
8
+ from rich.json import JSON
9
+ from rich.table import Table
10
+
11
+ from esgvoc.api.projects import (
12
+ find_collections_in_project,
13
+ find_items_in_project,
14
+ find_terms_in_all_projects,
15
+ find_terms_in_collection,
16
+ find_terms_in_project,
17
+ get_all_projects,
18
+ )
19
+ from esgvoc.api.universe import (
20
+ find_data_descriptors_in_universe,
21
+ find_items_in_universe,
22
+ find_terms_in_data_descriptor,
23
+ find_terms_in_universe,
24
+ )
25
+
26
+ app = typer.Typer()
27
+ console = Console()
28
+
29
+ _LOGGER = logging.getLogger(__name__)
30
+
31
+
32
+ def validate_key_format(key: str):
33
+ """
34
+ Validate if the key matches the XXXX:YYYY:ZZZZ format.
35
+ """
36
+ if not re.match(r"^[a-zA-Z0-9\/_-]*:[a-zA-Z0-9\/_-]*:[a-zA-Z0-9\/_.-]*$", key):
37
+ raise typer.BadParameter(f"Invalid key format: {key}. Must be XXXX:YYYY:ZZZZ.")
38
+ return key.split(":")
39
+
40
+
41
+ def handle_universe(expression: str, data_descriptor_id: str | None, term_id: str | None, options=None):
42
+ _LOGGER.debug(f"Handling universe with data_descriptor_id={data_descriptor_id}, term_id={term_id}")
43
+
44
+ if data_descriptor_id:
45
+ return find_terms_in_data_descriptor(expression, data_descriptor_id)
46
+ # BaseModel|dict[str: BaseModel]|None:
47
+
48
+ else:
49
+ return find_terms_in_universe(expression)
50
+ # dict[str, dict]:
51
+
52
+
53
+ def handle_project(expression: str, project_id: str, collection_id: str | None, term_id: str | None, options=None):
54
+ _LOGGER.debug(f"Handling project {project_id} with Y={collection_id}, Z={term_id}, options = {options}")
55
+
56
+ if project_id == "all":
57
+ return find_terms_in_all_projects(expression)
58
+
59
+ elif collection_id:
60
+ return find_terms_in_collection(expression, project_id, collection_id)
61
+ # dict[str, BaseModel]|None:
62
+
63
+ else:
64
+ res = find_terms_in_project(expression, project_id)
65
+ if res is None:
66
+ return None
67
+ else:
68
+ return res
69
+ # dict[str, dict]:
70
+
71
+
72
+ def handle_unknown(x: str | None, y: str | None, z: str | None):
73
+ print(f"Something wrong in X,Y or Z : X={x}, Y={y}, Z={z}")
74
+
75
+
76
+ def display(data: Any):
77
+ if isinstance(data, BaseModel):
78
+ # Pydantic Model
79
+ console.print(JSON.from_data(data.model_dump()))
80
+ elif isinstance(data, dict):
81
+ # Dictionary as JSON
82
+ console.print(data.keys())
83
+ elif isinstance(data, list):
84
+ # List as Table
85
+ table = Table(title="List")
86
+ table.add_column("Index")
87
+ table.add_column("Item")
88
+ for i, item in enumerate(data):
89
+ table.add_row(str(i), str(item))
90
+ console.print(table)
91
+ else:
92
+ # Fallback to simple print
93
+ console.print(data)
94
+
95
+
96
+ @app.command()
97
+ def find(expression: str, keys: list[str] = typer.Argument(..., help="List of keys in XXXX:YYYY:ZZZZ format")):
98
+ """
99
+ Retrieve a specific value from the database system.\n
100
+ This command allows you to fetch a value by specifying the universe/project, data_descriptor/collection,
101
+ and term in a structured format.\n
102
+ \n
103
+
104
+ Usage:\n
105
+ `find <expression> <project>:<collection>:<term>`\n
106
+ \n
107
+ Arguments:\n
108
+ <expression>\t The full text search expression.
109
+ <project>\tThe project id to query. like `cmip6plus`\n
110
+ <collection>\tThe collection id in the specified database.\n
111
+ <term>\t\tThe term id within the specified collection.\n
112
+ \n
113
+ Example:
114
+ \n
115
+ Notes:\n
116
+ - Ensure data exist in your system before using this command (use `esgvoc status` command to see whats available).\n
117
+ - Use a colon (`:`) to separate the parts of the argument. \n
118
+ - if more than one argument is given i.e get X:Y:Z A:B:C the 2 results are appended. \n
119
+ \n
120
+ """
121
+ known_projects = get_all_projects()
122
+ _LOGGER.debug(f"Processed expression: {expression}")
123
+
124
+ # Validate and process each key
125
+ for key in keys:
126
+ validated_key = validate_key_format(key)
127
+ _LOGGER.debug(f"Processed key: {validated_key}")
128
+ where, what, who = validated_key
129
+ what = what if what != "" else None
130
+ who = who if who != "" else None
131
+ if where == "" or where == "universe":
132
+ res = handle_universe(expression, what, who)
133
+ elif where in known_projects:
134
+ res = handle_project(expression, where, what, who, None)
135
+ else:
136
+ res = handle_unknown(where, what, who)
137
+
138
+ display(res)