esgvoc 1.0.1__py3-none-any.whl → 1.1.1__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 (41) hide show
  1. esgvoc/__init__.py +1 -1
  2. esgvoc/api/__init__.py +0 -6
  3. esgvoc/api/data_descriptors/__init__.py +6 -0
  4. esgvoc/api/data_descriptors/archive.py +5 -0
  5. esgvoc/api/data_descriptors/citation_url.py +5 -0
  6. esgvoc/api/data_descriptors/experiment.py +2 -2
  7. esgvoc/api/data_descriptors/known_branded_variable.py +58 -5
  8. esgvoc/api/data_descriptors/regex.py +5 -0
  9. esgvoc/api/data_descriptors/vertical_label.py +2 -2
  10. esgvoc/api/project_specs.py +48 -130
  11. esgvoc/api/projects.py +104 -63
  12. esgvoc/apps/drs/generator.py +47 -42
  13. esgvoc/apps/drs/validator.py +22 -38
  14. esgvoc/apps/jsg/json_schema_generator.py +252 -136
  15. esgvoc/apps/jsg/templates/template.jinja +249 -0
  16. esgvoc/apps/test_cv/README.md +214 -0
  17. esgvoc/apps/test_cv/cv_tester.py +1368 -0
  18. esgvoc/apps/test_cv/example_usage.py +216 -0
  19. esgvoc/apps/vr/__init__.py +12 -0
  20. esgvoc/apps/vr/build_variable_registry.py +71 -0
  21. esgvoc/apps/vr/example_usage.py +60 -0
  22. esgvoc/apps/vr/vr_app.py +333 -0
  23. esgvoc/cli/config.py +671 -86
  24. esgvoc/cli/drs.py +39 -21
  25. esgvoc/cli/main.py +2 -0
  26. esgvoc/cli/test_cv.py +257 -0
  27. esgvoc/core/constants.py +10 -7
  28. esgvoc/core/data_handler.py +24 -22
  29. esgvoc/core/db/connection.py +7 -0
  30. esgvoc/core/db/project_ingestion.py +34 -9
  31. esgvoc/core/db/universe_ingestion.py +1 -2
  32. esgvoc/core/service/configuration/setting.py +192 -21
  33. esgvoc/core/service/data_merger.py +1 -1
  34. esgvoc/core/service/state.py +18 -2
  35. {esgvoc-1.0.1.dist-info → esgvoc-1.1.1.dist-info}/METADATA +2 -1
  36. {esgvoc-1.0.1.dist-info → esgvoc-1.1.1.dist-info}/RECORD +40 -29
  37. esgvoc/apps/jsg/cmip6_template.json +0 -74
  38. /esgvoc/apps/{py.typed → test_cv/__init__.py} +0 -0
  39. {esgvoc-1.0.1.dist-info → esgvoc-1.1.1.dist-info}/WHEEL +0 -0
  40. {esgvoc-1.0.1.dist-info → esgvoc-1.1.1.dist-info}/entry_points.txt +0 -0
  41. {esgvoc-1.0.1.dist-info → esgvoc-1.1.1.dist-info}/licenses/LICENSE.txt +0 -0
esgvoc/cli/config.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import shutil
2
3
  from pathlib import Path
3
4
  from typing import List, Optional
4
5
 
@@ -15,6 +16,51 @@ app = typer.Typer()
15
16
  console = Console()
16
17
 
17
18
 
19
+ def _get_fresh_config(config_manager, config_name: str):
20
+ """
21
+ Get a fresh configuration, bypassing any potential caching issues.
22
+ """
23
+ # Force reload from file to ensure we have the latest state
24
+ configs = config_manager.list_configs()
25
+ config_path = configs[config_name]
26
+
27
+ # Load directly from file to avoid any caching
28
+ try:
29
+ data = toml.load(config_path)
30
+ projects = {p["project_name"]: ServiceSettings.ProjectSettings(**p) for p in data.pop("projects", [])}
31
+ from esgvoc.core.service.configuration.setting import UniverseSettings
32
+
33
+ return ServiceSettings(universe=UniverseSettings(**data["universe"]), projects=projects)
34
+ except Exception:
35
+ # Fallback to config manager if direct load fails
36
+ return config_manager.get_config(config_name)
37
+
38
+
39
+ def _save_and_reload_config(config_manager, config_name: str, config):
40
+ """
41
+ Save configuration and ensure proper state reload.
42
+ """
43
+ config_manager.save_active_config(config)
44
+
45
+ # Reset the state if we modified the active configuration
46
+ if config_name == config_manager.get_active_config_name():
47
+ service.current_state = service.get_state()
48
+
49
+ # Clear any potential caches in the config manager
50
+ if hasattr(config_manager, "_cached_config"):
51
+ config_manager._cached_config = None
52
+ if hasattr(config_manager, "cache"):
53
+ config_manager.cache.clear()
54
+
55
+ """
56
+ Function to display a rich table in the console.
57
+
58
+ :param table: The table to be displayed
59
+ """
60
+ console = Console(record=True, width=200)
61
+ console.print(table)
62
+
63
+
18
64
  def display(table):
19
65
  """
20
66
  Function to display a rich table in the console.
@@ -321,23 +367,14 @@ def set(
321
367
 
322
368
  console.print(f"[green]Updated universe.{setting_key} = {setting_value}[/green]")
323
369
 
324
- # Handle project settings
370
+ # Handle project settings using the new update_project method
325
371
  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
372
+ # Use the new update_project method
373
+ if config.update_project(component_part, **{setting_key: setting_value}):
374
+ modified = True
375
+ console.print(f"[green]Updated {component_part}.{setting_key} = {setting_value}[/green]")
335
376
  else:
336
377
  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
378
  else:
342
379
  console.print(
343
380
  f"[yellow]Warning: Component '{component_part}' not found in configuration. Skipping.[/yellow]"
@@ -365,73 +402,191 @@ def set(
365
402
  raise typer.Exit(1)
366
403
 
367
404
 
405
+ # 🔹 NEW: Enhanced project management commands using ServiceSettings methods
406
+
407
+
368
408
  @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."
409
+ def list_available_projects():
410
+ """
411
+ List all available default projects that can be added.
412
+ """
413
+ available_projects = ServiceSettings.DEFAULT_PROJECT_CONFIGS.keys()
414
+
415
+ table = Table(title="Available Default Projects")
416
+ table.add_column("Project Name", style="cyan")
417
+ table.add_column("Repository", style="green")
418
+ table.add_column("Branch", style="yellow")
419
+
420
+ for project_name in available_projects:
421
+ config = ServiceSettings.DEFAULT_PROJECT_CONFIGS[project_name]
422
+ table.add_row(project_name, config["github_repo"], config["branch"])
423
+
424
+ display(table)
425
+
426
+
427
+ @app.command()
428
+ def list_projects(
429
+ config_name: Optional[str] = typer.Option(
430
+ None, "--config", "-c", help="Configuration name. Uses active configuration if not specified."
372
431
  ),
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
432
  ):
379
433
  """
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.
434
+ List all projects in a configuration.
389
435
  """
390
436
  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]")
437
+ if config_name is None:
438
+ config_name = config_manager.get_active_config_name()
439
+ console.print(f"Showing projects in active configuration: [cyan]{config_name}[/cyan]")
394
440
 
395
441
  configs = config_manager.list_configs()
396
- if name not in configs:
397
- console.print(f"[red]Error: Configuration '{name}' not found.[/red]")
442
+ if config_name not in configs:
443
+ console.print(f"[red]Error: Configuration '{config_name}' not found.[/red]")
398
444
  raise typer.Exit(1)
399
445
 
400
446
  try:
401
- # Load the configuration
402
- config = config_manager.get_config(name)
447
+ config = config_manager.get_config(config_name)
403
448
 
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)
449
+ if not config.projects:
450
+ console.print(f"[yellow]No projects found in configuration '{config_name}'.[/yellow]")
451
+ return
408
452
 
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"
453
+ table = Table(title=f"Projects in Configuration: {config_name}")
454
+ table.add_column("Project Name", style="cyan")
455
+ table.add_column("Repository", style="green")
456
+ table.add_column("Branch", style="yellow")
457
+ table.add_column("Local Path", style="blue")
458
+ table.add_column("DB Path", style="magenta")
414
459
 
415
- # Create the project settings
416
- from esgvoc.core.service.configuration.setting import ProjectSettings
460
+ for project_name, project in config.projects.items():
461
+ table.add_row(
462
+ project_name,
463
+ project.github_repo,
464
+ project.branch or "main",
465
+ project.local_path or "N/A",
466
+ project.db_path or "N/A",
467
+ )
417
468
 
418
- project_settings = ProjectSettings(
419
- project_name=project_name, github_repo=github_repo, branch=branch, local_path=local_path, db_path=db_path
420
- )
469
+ display(table)
470
+
471
+ except Exception as e:
472
+ console.print(f"[red]Error listing projects: {str(e)}[/red]")
473
+ raise typer.Exit(1)
421
474
 
422
- # Add to configuration
423
- config.projects[project_name] = project_settings
475
+
476
+ @app.command()
477
+ def add_project(
478
+ project_name: str = typer.Argument(..., help="Name of the project to add."),
479
+ config_name: Optional[str] = typer.Option(
480
+ None, "--config", "-c", help="Configuration name. Uses active configuration if not specified."
481
+ ),
482
+ from_default: bool = typer.Option(
483
+ True, "--from-default/--custom", help="Add from default configuration or specify custom settings."
484
+ ),
485
+ # Custom project options (only used when --custom is specified)
486
+ github_repo: Optional[str] = typer.Option(
487
+ None, "--repo", "-r", help="GitHub repository URL (for custom projects)."
488
+ ),
489
+ branch: Optional[str] = typer.Option("main", "--branch", "-b", help="Branch (for custom projects)."),
490
+ local_path: Optional[str] = typer.Option(None, "--local", "-l", help="Local path (for custom projects)."),
491
+ db_path: Optional[str] = typer.Option(None, "--db", "-d", help="Database path (for custom projects)."),
492
+ ):
493
+ """
494
+ Add a project to a configuration.
495
+
496
+ By default, adds from available default projects. Use --custom to specify custom settings.
497
+
498
+ Examples:
499
+ # Add a default project
500
+ esgvoc add-project input4mip
501
+
502
+ # Add a custom project
503
+ esgvoc add-project my_project --custom --repo https://github.com/me/repo
504
+ """
505
+ config_manager = service.get_config_manager()
506
+ if config_name is None:
507
+ config_name = config_manager.get_active_config_name()
508
+ console.print(f"Modifying active configuration: [cyan]{config_name}[/cyan]")
509
+
510
+ configs = config_manager.list_configs()
511
+ if config_name not in configs:
512
+ console.print(f"[red]Error: Configuration '{config_name}' not found.[/red]")
513
+ raise typer.Exit(1)
514
+
515
+ try:
516
+ # 🔹 FORCE FRESH LOAD: Load configuration directly from file to bypass any caching
517
+ configs = config_manager.list_configs()
518
+ config_path = configs[config_name]
519
+
520
+ # Load fresh configuration from file
521
+ try:
522
+ config = ServiceSettings.load_from_file(config_path)
523
+ console.print(f"[blue]Debug: Loaded fresh config from file[/blue]")
524
+ except Exception as e:
525
+ console.print(f"[yellow]Debug: Failed to load from file ({e}), using config manager[/yellow]")
526
+ config = config_manager.get_config(config_name)
527
+
528
+ # 🔹 DEBUG: Show current projects before adding
529
+ current_projects = []
530
+ if hasattr(config, "projects") and config.projects:
531
+ current_projects = [name for name in config.projects.keys()]
532
+ console.print(f"[blue]Debug: Current projects: {current_projects}[/blue]")
533
+
534
+ if from_default:
535
+ # Add from default configuration
536
+ if config.add_project_from_default(project_name):
537
+ console.print(
538
+ f"[green]Successfully added default project [cyan]{project_name}[/cyan] to configuration [cyan]{config_name}[/cyan][/green]"
539
+ )
540
+ else:
541
+ if config.has_project(project_name):
542
+ console.print(
543
+ f"[red]Error: Project '{project_name}' already exists in configuration '{config_name}'.[/red]"
544
+ )
545
+ else:
546
+ available = config.get_available_default_projects()
547
+ console.print(f"[red]Error: '{project_name}' is not a valid default project.[/red]")
548
+ console.print(f"[yellow]Available default projects: {', '.join(available)}[/yellow]")
549
+ raise typer.Exit(1)
550
+ else:
551
+ # Add custom project
552
+ if not github_repo:
553
+ console.print("[red]Error: --repo is required when adding custom projects.[/red]")
554
+ raise typer.Exit(1)
555
+
556
+ # Set default paths if not provided
557
+ if local_path is None:
558
+ local_path = f"repos/{project_name}"
559
+ if db_path is None:
560
+ db_path = f"dbs/{project_name}.sqlite"
561
+
562
+ custom_config = {
563
+ "project_name": project_name,
564
+ "github_repo": github_repo,
565
+ "branch": branch,
566
+ "local_path": local_path,
567
+ "db_path": db_path,
568
+ }
569
+
570
+ if config.add_project_custom(custom_config):
571
+ console.print(
572
+ f"[green]Successfully added custom project [cyan]{project_name}[/cyan] to configuration [cyan]{config_name}[/cyan][/green]"
573
+ )
574
+ else:
575
+ console.print(
576
+ f"[red]Error: Project '{project_name}' already exists in configuration '{config_name}'.[/red]"
577
+ )
578
+ raise typer.Exit(1)
424
579
 
425
580
  # Save the configuration
426
581
  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
582
 
431
583
  # Reset the state if we modified the active configuration
432
- if name == config_manager.get_active_config_name():
584
+ if config_name == config_manager.get_active_config_name():
433
585
  service.current_state = service.get_state()
434
586
 
587
+ except ValueError as e:
588
+ console.print(f"[red]Error: {str(e)}[/red]")
589
+ raise typer.Exit(1)
435
590
  except Exception as e:
436
591
  console.print(f"[red]Error adding project: {str(e)}[/red]")
437
592
  raise typer.Exit(1)
@@ -439,62 +594,492 @@ def add_project(
439
594
 
440
595
  @app.command()
441
596
  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
597
  project_name: str = typer.Argument(..., help="Name of the project to remove."),
598
+ config_name: Optional[str] = typer.Option(
599
+ None, "--config", "-c", help="Configuration name. Uses active configuration if not specified."
600
+ ),
601
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt."),
446
602
  ):
447
603
  """
448
604
  Remove a project from a configuration.
605
+ """
606
+ config_manager = service.get_config_manager()
607
+ if config_name is None:
608
+ config_name = config_manager.get_active_config_name()
609
+ console.print(f"Modifying active configuration: [cyan]{config_name}[/cyan]")
449
610
 
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.
611
+ configs = config_manager.list_configs()
612
+ if config_name not in configs:
613
+ console.print(f"[red]Error: Configuration '{config_name}' not found.[/red]")
614
+ raise typer.Exit(1)
615
+
616
+ try:
617
+ # 🔹 FORCE FRESH LOAD for removal too
618
+ configs = config_manager.list_configs()
619
+ config_path = configs[config_name]
620
+
621
+ try:
622
+ config = ServiceSettings.load_from_file(config_path)
623
+ console.print(f"[blue]Debug: Loaded fresh config from file for removal[/blue]")
624
+ except Exception as e:
625
+ console.print(f"[yellow]Debug: Failed to load from file ({e}), using config manager[/yellow]")
626
+ config = config_manager.get_config(config_name)
627
+
628
+ if not config.has_project(project_name):
629
+ console.print(f"[red]Error: Project '{project_name}' not found in configuration '{config_name}'.[/red]")
630
+ raise typer.Exit(1)
631
+
632
+ # Confirm removal unless forced
633
+ if not force:
634
+ confirm = typer.confirm(
635
+ f"Are you sure you want to remove project '{project_name}' from configuration '{config_name}'?"
636
+ )
637
+ if not confirm:
638
+ console.print("Operation cancelled.")
639
+ return
640
+
641
+ # Remove project using the new method
642
+ if config.remove_project(project_name):
643
+ console.print(
644
+ f"[green]Successfully removed project [cyan]{project_name}[/cyan] from configuration [cyan]{config_name}[/cyan][/green]"
645
+ )
646
+ else:
647
+ console.print(f"[red]Error: Failed to remove project '{project_name}'.[/red]")
648
+ raise typer.Exit(1)
649
+
650
+ # Save the configuration
651
+ config_manager.save_active_config(config)
652
+
653
+ # 🔹 DEBUG: Verify the project was actually removed
654
+ remaining_projects = []
655
+ if hasattr(config, "projects") and config.projects:
656
+ remaining_projects = [name for name in config.projects.keys()]
657
+ console.print(f"[blue]Debug: Projects after removal: {remaining_projects}[/blue]")
658
+
659
+ # Reset the state if we modified the active configuration
660
+ if config_name == config_manager.get_active_config_name():
661
+ service.current_state = service.get_state()
662
+
663
+ except Exception as e:
664
+ console.print(f"[red]Error removing project: {str(e)}[/red]")
665
+ raise typer.Exit(1)
666
+
667
+
668
+ @app.command()
669
+ def update_project(
670
+ project_name: str = typer.Argument(..., help="Name of the project to update."),
671
+ config_name: Optional[str] = typer.Option(
672
+ None, "--config", "-c", help="Configuration name. Uses active configuration if not specified."
673
+ ),
674
+ github_repo: Optional[str] = typer.Option(None, "--repo", "-r", help="New GitHub repository URL."),
675
+ branch: Optional[str] = typer.Option(None, "--branch", "-b", help="New branch."),
676
+ local_path: Optional[str] = typer.Option(None, "--local", "-l", help="New local path."),
677
+ db_path: Optional[str] = typer.Option(None, "--db", "-d", help="New database path."),
678
+ ):
679
+ """
680
+ Update settings for an existing project.
453
681
  """
454
682
  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]")
683
+ if config_name is None:
684
+ config_name = config_manager.get_active_config_name()
685
+ console.print(f"Modifying active configuration: [cyan]{config_name}[/cyan]")
458
686
 
459
687
  configs = config_manager.list_configs()
460
- if name not in configs:
461
- console.print(f"[red]Error: Configuration '{name}' not found.[/red]")
688
+ if config_name not in configs:
689
+ console.print(f"[red]Error: Configuration '{config_name}' not found.[/red]")
462
690
  raise typer.Exit(1)
463
691
 
464
692
  try:
465
- # Load the configuration
466
- config = config_manager.get_config(name)
693
+ config = config_manager.get_config(config_name)
467
694
 
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]")
695
+ if not config.has_project(project_name):
696
+ console.print(f"[red]Error: Project '{project_name}' not found in configuration '{config_name}'.[/red]")
471
697
  raise typer.Exit(1)
472
698
 
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.")
699
+ # Build update dict with non-None values
700
+ updates = {}
701
+ if github_repo is not None:
702
+ updates["github_repo"] = github_repo
703
+ if branch is not None:
704
+ updates["branch"] = branch
705
+ if local_path is not None:
706
+ updates["local_path"] = local_path
707
+ if db_path is not None:
708
+ updates["db_path"] = db_path
709
+
710
+ if not updates:
711
+ console.print(
712
+ "[yellow]No updates specified. Use --repo, --branch, --local, or --db to specify changes.[/yellow]"
713
+ )
479
714
  return
480
715
 
481
- # Remove project
482
- del config.projects[project_name]
716
+ # Update project using the new method
717
+ if config.update_project(project_name, **updates):
718
+ console.print(
719
+ f"[green]Successfully updated project [cyan]{project_name}[/cyan] in configuration [cyan]{config_name}[/cyan][/green]"
720
+ )
721
+ for key, value in updates.items():
722
+ console.print(f" [green]{key} = {value}[/green]")
723
+ else:
724
+ console.print(f"[red]Error: Failed to update project '{project_name}'.[/red]")
725
+ raise typer.Exit(1)
483
726
 
484
727
  # Save the configuration
485
728
  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
729
 
490
730
  # Reset the state if we modified the active configuration
491
- if name == config_manager.get_active_config_name():
731
+ if config_name == config_manager.get_active_config_name():
492
732
  service.current_state = service.get_state()
493
733
 
734
+ except Exception as e:
735
+ console.print(f"[red]Error updating project: {str(e)}[/red]")
736
+ raise typer.Exit(1)
737
+
738
+
739
+ # 🔹 NEW: Simple config management commands
740
+
741
+
742
+ @app.command()
743
+ def add(
744
+ project_names: List[str] = typer.Argument(..., help="Names of the projects to add from defaults."),
745
+ config_name: Optional[str] = typer.Option(
746
+ None, "--config", "-c", help="Configuration name. Uses active configuration if not specified."
747
+ ),
748
+ ):
749
+ """
750
+ Add one or more default projects to the current configuration and install their CVs.
751
+
752
+ This will:
753
+ 1. Add the projects to the configuration using default settings
754
+ 2. Download the projects' CVs by running synchronize_all
755
+
756
+ Examples:
757
+ esgvoc config add input4mip
758
+ esgvoc config add input4mip obs4mip cordex-cmip6
759
+ esgvoc config add obs4mip --config my_config
760
+ """
761
+ config_manager = service.get_config_manager()
762
+ if config_name is None:
763
+ config_name = config_manager.get_active_config_name()
764
+ console.print(f"Adding to active configuration: [cyan]{config_name}[/cyan]")
765
+
766
+ configs = config_manager.list_configs()
767
+ if config_name not in configs:
768
+ console.print(f"[red]Error: Configuration '{config_name}' not found.[/red]")
769
+ raise typer.Exit(1)
770
+
771
+ try:
772
+ # Load fresh configuration from file
773
+ configs = config_manager.list_configs()
774
+ config_path = configs[config_name]
775
+ config = ServiceSettings.load_from_file(config_path)
776
+
777
+ added_projects = []
778
+ skipped_projects = []
779
+ invalid_projects = []
780
+
781
+ # Process each project
782
+ for project_name in project_names:
783
+ # Check if project already exists
784
+ if config.has_project(project_name):
785
+ skipped_projects.append(project_name)
786
+ console.print(f"[yellow]⚠ Project '{project_name}' already exists - skipping[/yellow]")
787
+ continue
788
+
789
+ # Add the project from defaults
790
+ try:
791
+ if config.add_project_from_default(project_name):
792
+ added_projects.append(project_name)
793
+ console.print(f"[green]✓ Added project [cyan]{project_name}[/cyan][/green]")
794
+ else:
795
+ invalid_projects.append(project_name)
796
+ console.print(f"[red]✗ Invalid project '{project_name}'[/red]")
797
+ except ValueError as e:
798
+ invalid_projects.append(project_name)
799
+ console.print(f"[red]✗ Invalid project '{project_name}'[/red]")
800
+
801
+ # Show summary of what was processed
802
+ if added_projects:
803
+ console.print(
804
+ f"[green]Successfully added {len(added_projects)} project(s): {', '.join(added_projects)}[/green]"
805
+ )
806
+ if skipped_projects:
807
+ console.print(
808
+ f"[yellow]Skipped {len(skipped_projects)} existing project(s): {', '.join(skipped_projects)}[/yellow]"
809
+ )
810
+ if invalid_projects:
811
+ available = config.get_available_default_projects()
812
+ console.print(f"[red]Invalid project(s): {', '.join(invalid_projects)}[/red]")
813
+ console.print(f"[yellow]Available projects: {', '.join(available)}[/yellow]")
814
+
815
+ # Only proceed if we actually added something
816
+ if added_projects:
817
+ # Save the configuration to the correct file
818
+ if config_name == config_manager.get_active_config_name():
819
+ config_manager.save_active_config(config)
820
+ # Reset the state if we modified the active configuration
821
+ service.current_state = service.get_state()
822
+ else:
823
+ # Save to specific config file
824
+ config_path = configs[config_name]
825
+ config.save_to_file(config_path)
826
+
827
+ # Download the CVs for all added projects
828
+ console.print(f"[blue]Downloading CVs for {len(added_projects)} project(s)...[/blue]")
829
+ service.current_state.synchronize_all()
830
+ console.print(f"[green]✓ Successfully installed CVs for all added projects[/green]")
831
+ elif invalid_projects and not skipped_projects:
832
+ # Exit with error only if we had invalid projects and nothing was skipped
833
+ raise typer.Exit(1)
834
+
835
+ except ValueError as e:
836
+ console.print(f"[red]Error: {str(e)}[/red]")
837
+ raise typer.Exit(1)
838
+ except Exception as e:
839
+ console.print(f"[red]Error adding project: {str(e)}[/red]")
840
+ raise typer.Exit(1)
841
+
842
+
843
+ @app.command()
844
+ def rm(
845
+ project_names: List[str] = typer.Argument(..., help="Names of the projects to remove."),
846
+ config_name: Optional[str] = typer.Option(
847
+ None, "--config", "-c", help="Configuration name. Uses active configuration if not specified."
848
+ ),
849
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt."),
850
+ keep_files: bool = typer.Option(
851
+ False, "--keep-files", help="Keep local repos and databases (only remove from config)."
852
+ ),
853
+ ):
854
+ """
855
+ Remove one or more projects from the configuration and delete their repos/databases.
856
+
857
+ This will:
858
+ 1. Remove the projects from the configuration
859
+ 2. Delete the local repository directories (unless --keep-files)
860
+ 3. Delete the database files (unless --keep-files)
861
+
862
+ Examples:
863
+ esgvoc config rm input4mip
864
+ esgvoc config rm input4mip obs4mip cordex-cmip6
865
+ esgvoc config rm obs4mip --force
866
+ esgvoc config rm cmip6 input4mip --keep-files # Remove from config but keep files
867
+ """
868
+ config_manager = service.get_config_manager()
869
+ if config_name is None:
870
+ config_name = config_manager.get_active_config_name()
871
+ console.print(f"Removing from active configuration: [cyan]{config_name}[/cyan]")
872
+
873
+ configs = config_manager.list_configs()
874
+ if config_name not in configs:
875
+ console.print(f"[red]Error: Configuration '{config_name}' not found.[/red]")
876
+ raise typer.Exit(1)
877
+
878
+ try:
879
+ # Load fresh configuration from file
880
+ configs = config_manager.list_configs()
881
+ config_path = configs[config_name]
882
+ config = ServiceSettings.load_from_file(config_path)
883
+
884
+ # Check which projects exist and collect their details
885
+ valid_projects = []
886
+ invalid_projects = []
887
+ projects_to_remove = {} # project_name -> project_object
888
+
889
+ for project_name in project_names:
890
+ if config.has_project(project_name):
891
+ project = config.get_project(project_name)
892
+ projects_to_remove[project_name] = project
893
+ valid_projects.append(project_name)
894
+ else:
895
+ invalid_projects.append(project_name)
896
+ console.print(f"[red]✗ Project '{project_name}' not found in configuration[/red]")
897
+
898
+ if invalid_projects:
899
+ console.print(f"[red]Invalid project(s): {', '.join(invalid_projects)}[/red]")
900
+
901
+ if not valid_projects:
902
+ console.print("[red]No valid projects to remove.[/red]")
903
+ raise typer.Exit(1)
904
+
905
+ # Show what will be removed and confirm unless forced
906
+ console.print(f"[yellow]Projects to remove: {', '.join(valid_projects)}[/yellow]")
907
+ if not force:
908
+ action_desc = "remove from config only" if keep_files else "remove from config and delete all files"
909
+ project_word = "project" if len(valid_projects) == 1 else "projects"
910
+ confirm = typer.confirm(f"Are you sure you want to {action_desc} for {len(valid_projects)} {project_word}?")
911
+ if not confirm:
912
+ console.print("Operation cancelled.")
913
+ return
914
+
915
+ # Get base directory for file cleanup
916
+ base_dir = config_manager.data_config_dir or str(config_manager.data_dir)
917
+
918
+ removed_projects = []
919
+ # Remove each project
920
+ for project_name in valid_projects:
921
+ project = projects_to_remove[project_name]
922
+
923
+ if config.remove_project(project_name):
924
+ removed_projects.append(project_name)
925
+ console.print(f"[green]✓ Removed [cyan]{project_name}[/cyan] from configuration[/green]")
926
+
927
+ # Clean up filesystem unless --keep-files
928
+ if not keep_files and project:
929
+ # Clean up local repository
930
+ if project.local_path:
931
+ repo_path = Path(base_dir) / project.local_path
932
+ if repo_path.exists():
933
+ shutil.rmtree(repo_path)
934
+ console.print(f"[green] ✓ Deleted repository: {repo_path}[/green]")
935
+ else:
936
+ console.print(f"[yellow] Repository not found: {repo_path}[/yellow]")
937
+
938
+ # Clean up database
939
+ if project.db_path:
940
+ db_path = Path(base_dir) / project.db_path
941
+ if db_path.exists():
942
+ db_path.unlink()
943
+ console.print(f"[green] ✓ Deleted database: {db_path}[/green]")
944
+ else:
945
+ console.print(f"[yellow] Database not found: {db_path}[/yellow]")
946
+ else:
947
+ console.print(f"[red]✗ Failed to remove '{project_name}'[/red]")
948
+
949
+ if removed_projects:
950
+ console.print(
951
+ f"[green]Successfully removed {len(removed_projects)} project(s): {', '.join(removed_projects)}[/green]"
952
+ )
953
+
954
+ # Save the configuration to the correct file
955
+ if config_name == config_manager.get_active_config_name():
956
+ config_manager.save_active_config(config)
957
+ # Reset the state if we modified the active configuration
958
+ service.current_state = service.get_state()
959
+ else:
960
+ # Save to specific config file
961
+ config_path = configs[config_name]
962
+ config.save_to_file(config_path)
963
+ else:
964
+ console.print("[red]No projects were successfully removed.[/red]")
965
+ raise typer.Exit(1)
966
+
494
967
  except Exception as e:
495
968
  console.print(f"[red]Error removing project: {str(e)}[/red]")
496
969
  raise typer.Exit(1)
497
970
 
498
971
 
972
+ @app.command()
973
+ def init(
974
+ name: str = typer.Argument(..., help="Name for the new empty configuration."),
975
+ no_switch: bool = typer.Option(
976
+ False, "--no-switch", help="Don't switch to the new configuration (stays on current)."
977
+ ),
978
+ ):
979
+ """
980
+ Create a new empty configuration with only universe settings (no projects).
981
+
982
+ This creates a minimal configuration with just the universe component,
983
+ allowing you to add projects selectively using 'esgvoc config add'.
984
+ By default, switches to the new configuration after creation.
985
+
986
+ Examples:
987
+ esgvoc config init minimal
988
+ esgvoc config init test --no-switch # Create but don't switch
989
+ """
990
+ config_manager = service.get_config_manager()
991
+ configs = config_manager.list_configs()
992
+
993
+ if name in configs:
994
+ console.print(f"[red]Error: Configuration '{name}' already exists.[/red]")
995
+ raise typer.Exit(1)
996
+
997
+ try:
998
+ # Create empty configuration with only universe settings
999
+ empty_config_data = {
1000
+ "universe": ServiceSettings.DEFAULT_SETTINGS["universe"],
1001
+ "projects": [], # No projects - completely empty
1002
+ }
1003
+
1004
+ # Add the new configuration
1005
+ config_manager.add_config(name, empty_config_data)
1006
+ console.print(f"[green]✓ Created empty configuration: [cyan]{name}[/cyan][/green]")
1007
+
1008
+ # Switch to new config by default (unless --no-switch is used)
1009
+ if not no_switch:
1010
+ config_manager.switch_config(name)
1011
+ console.print(f"[green]✓ Switched to configuration: [cyan]{name}[/cyan][/green]")
1012
+ # Reset the state to use the new configuration
1013
+ service.current_state = service.get_state()
1014
+
1015
+ except Exception as e:
1016
+ console.print(f"[red]Error creating configuration: {str(e)}[/red]")
1017
+ raise typer.Exit(1)
1018
+
1019
+
1020
+ @app.command()
1021
+ def avail(
1022
+ config_name: Optional[str] = typer.Option(
1023
+ None, "--config", "-c", help="Configuration name. Uses active configuration if not specified."
1024
+ ),
1025
+ ):
1026
+ """
1027
+ Show a table of all available default projects and their status in the configuration.
1028
+
1029
+ Projects are marked as:
1030
+ - ✓ Active: Project is in the current configuration
1031
+ - ○ Available: Project can be added to the configuration
1032
+
1033
+ Examples:
1034
+ esgvoc config avail
1035
+ esgvoc config avail --config my_config
1036
+ """
1037
+ config_manager = service.get_config_manager()
1038
+ if config_name is None:
1039
+ config_name = config_manager.get_active_config_name()
1040
+ console.print(f"Showing project availability for: [cyan]{config_name}[/cyan]")
1041
+
1042
+ configs = config_manager.list_configs()
1043
+ if config_name not in configs:
1044
+ console.print(f"[red]Error: Configuration '{config_name}' not found.[/red]")
1045
+ raise typer.Exit(1)
1046
+
1047
+ try:
1048
+ # Load configuration
1049
+ config_path = configs[config_name]
1050
+ config = ServiceSettings.load_from_file(config_path)
1051
+
1052
+ # Get all available default projects
1053
+ available_projects = ServiceSettings.DEFAULT_PROJECT_CONFIGS
1054
+
1055
+ table = Table(title=f"Available Projects (Configuration: {config_name})")
1056
+ table.add_column("Status", style="bold")
1057
+ table.add_column("Project Name", style="cyan")
1058
+ table.add_column("Repository", style="green")
1059
+ table.add_column("Branch", style="yellow")
1060
+
1061
+ for project_name, project_config in available_projects.items():
1062
+ # Check if project is in current configuration
1063
+ if config.has_project(project_name):
1064
+ status = "[green]✓ Active[/green]"
1065
+ else:
1066
+ status = "[dim]○ Available[/dim]"
1067
+
1068
+ table.add_row(status, project_name, project_config["github_repo"], project_config["branch"])
1069
+
1070
+ display(table)
1071
+
1072
+ # Show summary
1073
+ active_count = len([p for p in available_projects.keys() if config.has_project(p)])
1074
+ total_count = len(available_projects)
1075
+ console.print(
1076
+ f"\n[blue]Summary: {active_count}/{total_count} projects active in configuration '{config_name}'[/blue]"
1077
+ )
1078
+
1079
+ except Exception as e:
1080
+ console.print(f"[red]Error showing available projects: {str(e)}[/red]")
1081
+ raise typer.Exit(1)
1082
+
1083
+
499
1084
  if __name__ == "__main__":
500
1085
  app()