esgvoc 2.0.2__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.
Files changed (147) hide show
  1. esgvoc/__init__.py +3 -0
  2. esgvoc/api/__init__.py +91 -0
  3. esgvoc/api/data_descriptors/EMD_models/__init__.py +66 -0
  4. esgvoc/api/data_descriptors/EMD_models/arrangement.py +21 -0
  5. esgvoc/api/data_descriptors/EMD_models/calendar.py +5 -0
  6. esgvoc/api/data_descriptors/EMD_models/cell_variable_type.py +20 -0
  7. esgvoc/api/data_descriptors/EMD_models/component_type.py +5 -0
  8. esgvoc/api/data_descriptors/EMD_models/coordinate.py +52 -0
  9. esgvoc/api/data_descriptors/EMD_models/grid_mapping.py +19 -0
  10. esgvoc/api/data_descriptors/EMD_models/grid_region.py +19 -0
  11. esgvoc/api/data_descriptors/EMD_models/grid_type.py +19 -0
  12. esgvoc/api/data_descriptors/EMD_models/horizontal_computational_grid.py +56 -0
  13. esgvoc/api/data_descriptors/EMD_models/horizontal_grid_cells.py +230 -0
  14. esgvoc/api/data_descriptors/EMD_models/horizontal_subgrid.py +41 -0
  15. esgvoc/api/data_descriptors/EMD_models/horizontal_units.py +5 -0
  16. esgvoc/api/data_descriptors/EMD_models/model.py +139 -0
  17. esgvoc/api/data_descriptors/EMD_models/model_component.py +115 -0
  18. esgvoc/api/data_descriptors/EMD_models/reference.py +61 -0
  19. esgvoc/api/data_descriptors/EMD_models/resolution.py +48 -0
  20. esgvoc/api/data_descriptors/EMD_models/temporal_refinement.py +19 -0
  21. esgvoc/api/data_descriptors/EMD_models/truncation_method.py +17 -0
  22. esgvoc/api/data_descriptors/EMD_models/vertical_computational_grid.py +91 -0
  23. esgvoc/api/data_descriptors/EMD_models/vertical_coordinate.py +5 -0
  24. esgvoc/api/data_descriptors/EMD_models/vertical_units.py +19 -0
  25. esgvoc/api/data_descriptors/__init__.py +159 -0
  26. esgvoc/api/data_descriptors/activity.py +72 -0
  27. esgvoc/api/data_descriptors/archive.py +5 -0
  28. esgvoc/api/data_descriptors/area_label.py +30 -0
  29. esgvoc/api/data_descriptors/branded_suffix.py +30 -0
  30. esgvoc/api/data_descriptors/branded_variable.py +21 -0
  31. esgvoc/api/data_descriptors/citation_url.py +5 -0
  32. esgvoc/api/data_descriptors/contact.py +5 -0
  33. esgvoc/api/data_descriptors/conventions.py +28 -0
  34. esgvoc/api/data_descriptors/creation_date.py +18 -0
  35. esgvoc/api/data_descriptors/data_descriptor.py +127 -0
  36. esgvoc/api/data_descriptors/data_specs_version.py +25 -0
  37. esgvoc/api/data_descriptors/date.py +5 -0
  38. esgvoc/api/data_descriptors/directory_date.py +22 -0
  39. esgvoc/api/data_descriptors/drs_specs.py +38 -0
  40. esgvoc/api/data_descriptors/experiment.py +215 -0
  41. esgvoc/api/data_descriptors/forcing_index.py +21 -0
  42. esgvoc/api/data_descriptors/frequency.py +48 -0
  43. esgvoc/api/data_descriptors/further_info_url.py +5 -0
  44. esgvoc/api/data_descriptors/grid.py +43 -0
  45. esgvoc/api/data_descriptors/horizontal_label.py +20 -0
  46. esgvoc/api/data_descriptors/initialization_index.py +27 -0
  47. esgvoc/api/data_descriptors/institution.py +80 -0
  48. esgvoc/api/data_descriptors/known_branded_variable.py +75 -0
  49. esgvoc/api/data_descriptors/license.py +31 -0
  50. esgvoc/api/data_descriptors/member_id.py +9 -0
  51. esgvoc/api/data_descriptors/mip_era.py +26 -0
  52. esgvoc/api/data_descriptors/model_component.py +32 -0
  53. esgvoc/api/data_descriptors/models_test/models.py +17 -0
  54. esgvoc/api/data_descriptors/nominal_resolution.py +50 -0
  55. esgvoc/api/data_descriptors/obs_type.py +5 -0
  56. esgvoc/api/data_descriptors/organisation.py +22 -0
  57. esgvoc/api/data_descriptors/physics_index.py +21 -0
  58. esgvoc/api/data_descriptors/product.py +16 -0
  59. esgvoc/api/data_descriptors/publication_status.py +5 -0
  60. esgvoc/api/data_descriptors/realization_index.py +24 -0
  61. esgvoc/api/data_descriptors/realm.py +16 -0
  62. esgvoc/api/data_descriptors/regex.py +5 -0
  63. esgvoc/api/data_descriptors/region.py +35 -0
  64. esgvoc/api/data_descriptors/resolution.py +7 -0
  65. esgvoc/api/data_descriptors/source.py +120 -0
  66. esgvoc/api/data_descriptors/source_type.py +5 -0
  67. esgvoc/api/data_descriptors/sub_experiment.py +5 -0
  68. esgvoc/api/data_descriptors/table.py +28 -0
  69. esgvoc/api/data_descriptors/temporal_label.py +20 -0
  70. esgvoc/api/data_descriptors/time_range.py +17 -0
  71. esgvoc/api/data_descriptors/title.py +5 -0
  72. esgvoc/api/data_descriptors/tracking_id.py +67 -0
  73. esgvoc/api/data_descriptors/variable.py +56 -0
  74. esgvoc/api/data_descriptors/variant_label.py +25 -0
  75. esgvoc/api/data_descriptors/vertical_label.py +20 -0
  76. esgvoc/api/project_specs.py +143 -0
  77. esgvoc/api/projects.py +1253 -0
  78. esgvoc/api/py.typed +0 -0
  79. esgvoc/api/pydantic_handler.py +146 -0
  80. esgvoc/api/report.py +127 -0
  81. esgvoc/api/search.py +171 -0
  82. esgvoc/api/universe.py +434 -0
  83. esgvoc/apps/__init__.py +6 -0
  84. esgvoc/apps/cmor_tables/__init__.py +7 -0
  85. esgvoc/apps/cmor_tables/cvs_table.py +948 -0
  86. esgvoc/apps/drs/__init__.py +0 -0
  87. esgvoc/apps/drs/constants.py +2 -0
  88. esgvoc/apps/drs/generator.py +429 -0
  89. esgvoc/apps/drs/report.py +540 -0
  90. esgvoc/apps/drs/validator.py +312 -0
  91. esgvoc/apps/ga/__init__.py +104 -0
  92. esgvoc/apps/ga/example_usage.py +315 -0
  93. esgvoc/apps/ga/models/__init__.py +47 -0
  94. esgvoc/apps/ga/models/netcdf_header.py +306 -0
  95. esgvoc/apps/ga/models/validator.py +491 -0
  96. esgvoc/apps/ga/test_ga.py +161 -0
  97. esgvoc/apps/ga/validator.py +277 -0
  98. esgvoc/apps/jsg/json_schema_generator.py +341 -0
  99. esgvoc/apps/jsg/templates/template.jinja +241 -0
  100. esgvoc/apps/test_cv/README.md +214 -0
  101. esgvoc/apps/test_cv/__init__.py +0 -0
  102. esgvoc/apps/test_cv/cv_tester.py +1611 -0
  103. esgvoc/apps/test_cv/example_usage.py +216 -0
  104. esgvoc/apps/vr/__init__.py +12 -0
  105. esgvoc/apps/vr/build_variable_registry.py +71 -0
  106. esgvoc/apps/vr/example_usage.py +60 -0
  107. esgvoc/apps/vr/vr_app.py +333 -0
  108. esgvoc/cli/clean.py +304 -0
  109. esgvoc/cli/cmor.py +46 -0
  110. esgvoc/cli/config.py +1300 -0
  111. esgvoc/cli/drs.py +267 -0
  112. esgvoc/cli/find.py +138 -0
  113. esgvoc/cli/get.py +155 -0
  114. esgvoc/cli/install.py +41 -0
  115. esgvoc/cli/main.py +60 -0
  116. esgvoc/cli/offline.py +269 -0
  117. esgvoc/cli/status.py +79 -0
  118. esgvoc/cli/test_cv.py +258 -0
  119. esgvoc/cli/valid.py +147 -0
  120. esgvoc/core/constants.py +17 -0
  121. esgvoc/core/convert.py +0 -0
  122. esgvoc/core/data_handler.py +206 -0
  123. esgvoc/core/db/__init__.py +3 -0
  124. esgvoc/core/db/connection.py +40 -0
  125. esgvoc/core/db/models/mixins.py +25 -0
  126. esgvoc/core/db/models/project.py +102 -0
  127. esgvoc/core/db/models/universe.py +98 -0
  128. esgvoc/core/db/project_ingestion.py +231 -0
  129. esgvoc/core/db/universe_ingestion.py +172 -0
  130. esgvoc/core/exceptions.py +33 -0
  131. esgvoc/core/logging_handler.py +26 -0
  132. esgvoc/core/repo_fetcher.py +345 -0
  133. esgvoc/core/service/__init__.py +41 -0
  134. esgvoc/core/service/configuration/config_manager.py +196 -0
  135. esgvoc/core/service/configuration/setting.py +363 -0
  136. esgvoc/core/service/data_merger.py +634 -0
  137. esgvoc/core/service/esg_voc.py +77 -0
  138. esgvoc/core/service/resolver_config.py +56 -0
  139. esgvoc/core/service/state.py +324 -0
  140. esgvoc/core/service/string_heuristics.py +98 -0
  141. esgvoc/core/service/term_cache.py +108 -0
  142. esgvoc/core/service/uri_resolver.py +133 -0
  143. esgvoc-2.0.2.dist-info/METADATA +82 -0
  144. esgvoc-2.0.2.dist-info/RECORD +147 -0
  145. esgvoc-2.0.2.dist-info/WHEEL +4 -0
  146. esgvoc-2.0.2.dist-info/entry_points.txt +2 -0
  147. esgvoc-2.0.2.dist-info/licenses/LICENSE.txt +519 -0
esgvoc/cli/offline.py ADDED
@@ -0,0 +1,269 @@
1
+ """
2
+ Offline mode management CLI commands.
3
+
4
+ This module provides CLI commands for managing offline mode settings
5
+ for universe and project components.
6
+ """
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+ from esgvoc.core.service import config_manager
13
+
14
+
15
+ app = typer.Typer()
16
+ console = Console()
17
+
18
+
19
+ @app.command()
20
+ def show(
21
+ component: str = typer.Argument(
22
+ None,
23
+ help="Component to show offline status for (universe or project name). Shows all if not specified."
24
+ ),
25
+ config: str = typer.Option(
26
+ None,
27
+ "--config",
28
+ "-c",
29
+ help="Configuration name to use"
30
+ )
31
+ ):
32
+ """Show offline mode status for components."""
33
+ try:
34
+ if config:
35
+ settings = config_manager.get_config(config)
36
+ else:
37
+ settings = config_manager.get_active_config()
38
+
39
+ if component:
40
+ # Show specific component
41
+ if component == "universe":
42
+ if settings.universe.offline_mode:
43
+ console.print(f"[green]Universe is in offline mode[/green]")
44
+ else:
45
+ console.print(f"[yellow]Universe is in online mode[/yellow]")
46
+ elif component in settings.projects:
47
+ project = settings.projects[component]
48
+ if project.offline_mode:
49
+ console.print(f"[green]Project '{component}' is in offline mode[/green]")
50
+ else:
51
+ console.print(f"[yellow]Project '{component}' is in online mode[/yellow]")
52
+ else:
53
+ console.print(f"[red]Component '{component}' not found[/red]")
54
+ raise typer.Exit(1)
55
+ else:
56
+ # Show all components
57
+ table = Table(title="Offline Mode Status")
58
+ table.add_column("Component", style="cyan")
59
+ table.add_column("Type", style="magenta")
60
+ table.add_column("Offline Mode", style="bold")
61
+
62
+ # Universe
63
+ status = "[green]✓ Enabled[/green]" if settings.universe.offline_mode else "[yellow]✗ Disabled[/yellow]"
64
+ table.add_row("Universe", "Universe", status)
65
+
66
+ # Projects
67
+ for project_name, project in settings.projects.items():
68
+ status = "[green]✓ Enabled[/green]" if project.offline_mode else "[yellow]✗ Disabled[/yellow]"
69
+ table.add_row(project_name, "Project", status)
70
+
71
+ console.print(table)
72
+
73
+ except Exception as e:
74
+ console.print(f"[red]Error: {str(e)}[/red]")
75
+ raise typer.Exit(1)
76
+
77
+
78
+ @app.command()
79
+ def enable(
80
+ component: str = typer.Argument(
81
+ None,
82
+ help="Component to enable offline mode for (universe or project name). If not specified, enables for all components."
83
+ ),
84
+ config: str = typer.Option(
85
+ None,
86
+ "--config",
87
+ "-c",
88
+ help="Configuration name to use"
89
+ )
90
+ ):
91
+ """Enable offline mode for a component or all components if none specified."""
92
+ try:
93
+ if config:
94
+ settings = config_manager.get_config(config)
95
+ else:
96
+ settings = config_manager.get_active_config()
97
+
98
+ if component is None:
99
+ # Enable for all components
100
+ settings.universe.offline_mode = True
101
+ for project in settings.projects.values():
102
+ project.offline_mode = True
103
+ console.print(f"[green]✓ Enabled offline mode for all components[/green]")
104
+ elif component == "universe":
105
+ settings.universe.offline_mode = True
106
+ console.print(f"[green]✓ Enabled offline mode for universe[/green]")
107
+ elif component in settings.projects:
108
+ settings.projects[component].offline_mode = True
109
+ console.print(f"[green]✓ Enabled offline mode for project '{component}'[/green]")
110
+ else:
111
+ console.print(f"[red]Component '{component}' not found[/red]")
112
+ raise typer.Exit(1)
113
+
114
+ # Save the updated settings
115
+ if config:
116
+ # Use the correct format for saving
117
+ data = {
118
+ "universe": settings.universe.model_dump(),
119
+ "projects": [p.model_dump() for p in settings.projects.values()],
120
+ }
121
+ config_manager.save_config(data, config)
122
+ else:
123
+ config_manager.save_active_config(settings)
124
+ console.print(f"[blue]Configuration saved[/blue]")
125
+
126
+ except Exception as e:
127
+ console.print(f"[red]Error: {str(e)}[/red]")
128
+ raise typer.Exit(1)
129
+
130
+
131
+ @app.command()
132
+ def disable(
133
+ component: str = typer.Argument(
134
+ None,
135
+ help="Component to disable offline mode for (universe or project name). If not specified, disables for all components."
136
+ ),
137
+ config: str = typer.Option(
138
+ None,
139
+ "--config",
140
+ "-c",
141
+ help="Configuration name to use"
142
+ )
143
+ ):
144
+ """Disable offline mode for a component or all components if none specified."""
145
+ try:
146
+ if config:
147
+ settings = config_manager.get_config(config)
148
+ else:
149
+ settings = config_manager.get_active_config()
150
+
151
+ if component is None:
152
+ # Disable for all components
153
+ settings.universe.offline_mode = False
154
+ for project in settings.projects.values():
155
+ project.offline_mode = False
156
+ console.print(f"[yellow]✓ Disabled offline mode for all components[/yellow]")
157
+ elif component == "universe":
158
+ settings.universe.offline_mode = False
159
+ console.print(f"[yellow]✓ Disabled offline mode for universe[/yellow]")
160
+ elif component in settings.projects:
161
+ settings.projects[component].offline_mode = False
162
+ console.print(f"[yellow]✓ Disabled offline mode for project '{component}'[/yellow]")
163
+ else:
164
+ console.print(f"[red]Component '{component}' not found[/red]")
165
+ raise typer.Exit(1)
166
+
167
+ # Save the updated settings
168
+ if config:
169
+ # Use the correct format for saving
170
+ data = {
171
+ "universe": settings.universe.model_dump(),
172
+ "projects": [p.model_dump() for p in settings.projects.values()],
173
+ }
174
+ config_manager.save_config(data, config)
175
+ else:
176
+ config_manager.save_active_config(settings)
177
+ console.print(f"[blue]Configuration saved[/blue]")
178
+
179
+ except Exception as e:
180
+ console.print(f"[red]Error: {str(e)}[/red]")
181
+ raise typer.Exit(1)
182
+
183
+
184
+ @app.command()
185
+ def enable_all(
186
+ config: str = typer.Option(
187
+ None,
188
+ "--config",
189
+ "-c",
190
+ help="Configuration name to use"
191
+ )
192
+ ):
193
+ """Enable offline mode for all components."""
194
+ try:
195
+ if config:
196
+ settings = config_manager.get_config(config)
197
+ else:
198
+ settings = config_manager.get_active_config()
199
+
200
+ # Enable for universe
201
+ settings.universe.offline_mode = True
202
+
203
+ # Enable for all projects
204
+ for project in settings.projects.values():
205
+ project.offline_mode = True
206
+
207
+ # Save the updated settings
208
+ if config:
209
+ # Use the correct format for saving
210
+ data = {
211
+ "universe": settings.universe.model_dump(),
212
+ "projects": [p.model_dump() for p in settings.projects.values()],
213
+ }
214
+ config_manager.save_config(data, config)
215
+ else:
216
+ config_manager.save_active_config(settings)
217
+
218
+ console.print(f"[green]✓ Enabled offline mode for all components[/green]")
219
+ console.print(f"[blue]Configuration saved[/blue]")
220
+
221
+ except Exception as e:
222
+ console.print(f"[red]Error: {str(e)}[/red]")
223
+ raise typer.Exit(1)
224
+
225
+
226
+ @app.command()
227
+ def disable_all(
228
+ config: str = typer.Option(
229
+ None,
230
+ "--config",
231
+ "-c",
232
+ help="Configuration name to use"
233
+ )
234
+ ):
235
+ """Disable offline mode for all components."""
236
+ try:
237
+ if config:
238
+ settings = config_manager.get_config(config)
239
+ else:
240
+ settings = config_manager.get_active_config()
241
+
242
+ # Disable for universe
243
+ settings.universe.offline_mode = False
244
+
245
+ # Disable for all projects
246
+ for project in settings.projects.values():
247
+ project.offline_mode = False
248
+
249
+ # Save the updated settings
250
+ if config:
251
+ # Use the correct format for saving
252
+ data = {
253
+ "universe": settings.universe.model_dump(),
254
+ "projects": [p.model_dump() for p in settings.projects.values()],
255
+ }
256
+ config_manager.save_config(data, config)
257
+ else:
258
+ config_manager.save_active_config(settings)
259
+
260
+ console.print(f"[yellow]✓ Disabled offline mode for all components[/yellow]")
261
+ console.print(f"[blue]Configuration saved[/blue]")
262
+
263
+ except Exception as e:
264
+ console.print(f"[red]Error: {str(e)}[/red]")
265
+ raise typer.Exit(1)
266
+
267
+
268
+ if __name__ == "__main__":
269
+ app()
esgvoc/cli/status.py ADDED
@@ -0,0 +1,79 @@
1
+ import typer
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+
5
+ from esgvoc.core import service
6
+
7
+ app = typer.Typer()
8
+ console = Console()
9
+
10
+
11
+ def display(table):
12
+ console = Console(record=True, width=200)
13
+ console.print(table)
14
+
15
+
16
+ @app.command()
17
+ def status():
18
+ """
19
+ Command to display status
20
+ i.e summary of version of usable ressources (between remote/cached)
21
+
22
+ """
23
+ assert service.current_state is not None
24
+ service.current_state.get_state_summary()
25
+
26
+ # Check for offline mode components and display summary
27
+ offline_components = []
28
+ if service.current_state.universe.offline_mode:
29
+ offline_components.append("universe")
30
+ for project_name, project in service.current_state.projects.items():
31
+ if project.offline_mode:
32
+ offline_components.append(project_name)
33
+
34
+ if offline_components:
35
+ console.print(f"[yellow]Offline mode enabled for: {', '.join(offline_components)}[/yellow]")
36
+
37
+ table = Table(show_header=False, show_lines=True)
38
+
39
+ table.add_row("", "Remote github repo", "Local repository", "Cache Database", "Offline Mode", style="bright_green")
40
+
41
+ # Universe row
42
+ universe_offline_status = "✓" if service.current_state.universe.offline_mode else "✗"
43
+ table.add_row(
44
+ "Universe path",
45
+ service.current_state.universe.github_repo,
46
+ service.current_state.universe.local_path,
47
+ service.current_state.universe.db_path,
48
+ universe_offline_status,
49
+ style="white",
50
+ )
51
+ table.add_row(
52
+ "Version",
53
+ service.current_state.universe.github_version or "N/A",
54
+ service.current_state.universe.local_version or "N/A",
55
+ service.current_state.universe.db_version or "N/A",
56
+ "",
57
+ style="bright_blue",
58
+ )
59
+
60
+ # Projects rows
61
+ for proj_name, proj in service.current_state.projects.items():
62
+ proj_offline_status = "✓" if proj.offline_mode else "✗"
63
+ table.add_row(
64
+ f"{proj_name} path",
65
+ proj.github_repo,
66
+ proj.local_path,
67
+ proj.db_path,
68
+ proj_offline_status,
69
+ style="white"
70
+ )
71
+ table.add_row(
72
+ "Version",
73
+ proj.github_version or "N/A",
74
+ proj.local_version or "N/A",
75
+ proj.db_version or "N/A",
76
+ "",
77
+ style="bright_blue"
78
+ )
79
+ display(table)
esgvoc/cli/test_cv.py ADDED
@@ -0,0 +1,258 @@
1
+ """
2
+ Test CV CLI commands
3
+
4
+ Provides commands for testing project CVs and Universe CVs integrated with esgvoc CLI.
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ from esgvoc.apps.test_cv.cv_tester import CVTester
14
+ from esgvoc.core.service.configuration.setting import ServiceSettings
15
+
16
+ app = typer.Typer()
17
+ console = Console()
18
+
19
+
20
+ @app.command()
21
+ def list_projects():
22
+ """List all available CV projects that can be tested."""
23
+ tester = CVTester()
24
+ projects = tester.get_available_projects()
25
+
26
+ table = Table(title="Available CV Projects for Testing")
27
+ table.add_column("Project Name", style="cyan")
28
+ table.add_column("Repository", style="green")
29
+ table.add_column("Default Branch", style="yellow")
30
+ table.add_column("Local Path", style="blue")
31
+
32
+ default_configs = ServiceSettings._get_default_project_configs()
33
+ for project_name in projects:
34
+ config = default_configs[project_name]
35
+ table.add_row(project_name, config["github_repo"], config["branch"], config["local_path"])
36
+
37
+ console.print(table)
38
+ console.print(f"\n[blue]Total: {len(projects)} projects available for testing[/blue]")
39
+
40
+
41
+ @app.command()
42
+ def configure(
43
+ project: str = typer.Argument(..., help="Project name to configure for testing"),
44
+ repo_url: Optional[str] = typer.Option(None, "--repo", "-r", help="Custom repository URL"),
45
+ branch: Optional[str] = typer.Option(None, "--branch", "-b", help="Custom branch to test"),
46
+ universe_branch: Optional[str] = typer.Option(None, "--universe-branch", "-u", help="Custom universe branch"),
47
+ sync: bool = typer.Option(True, "--sync/--no-sync", help="Synchronize CVs after configuration"),
48
+ ):
49
+ """
50
+ Configure esgvoc with a specific project for testing.
51
+
52
+ Examples:
53
+ esgvoc test configure obs4mip
54
+ esgvoc test configure cmip6 --branch my-test-branch
55
+ esgvoc test configure cmip6 --universe-branch my-universe-branch
56
+ esgvoc test configure custom --repo https://github.com/me/my-cvs --branch main --universe-branch dev
57
+ """
58
+ tester = CVTester()
59
+
60
+ try:
61
+ # Configure
62
+ if not tester.configure_for_testing(project, repo_url, branch, None, universe_branch):
63
+ raise typer.Exit(1)
64
+
65
+ # Optionally synchronize
66
+ if sync:
67
+ if not tester.synchronize_cvs():
68
+ raise typer.Exit(1)
69
+
70
+ console.print(f"[green]✅ Successfully configured project '{project}' for testing[/green]")
71
+ if not sync:
72
+ console.print("[yellow]Note: CVs not synchronized. Run 'esgvoc test sync' to download.[/yellow]")
73
+
74
+ except Exception as e:
75
+ console.print(f"[red]❌ Configuration failed: {e}[/red]")
76
+ raise typer.Exit(1)
77
+
78
+
79
+ @app.command()
80
+ def sync():
81
+ """Synchronize/download CVs for the currently configured project."""
82
+ tester = CVTester()
83
+
84
+ try:
85
+ if not tester.synchronize_cvs():
86
+ raise typer.Exit(1)
87
+ console.print("[green]✅ CVs synchronized successfully[/green]")
88
+ except Exception as e:
89
+ console.print(f"[red]❌ Synchronization failed: {e}[/red]")
90
+ raise typer.Exit(1)
91
+
92
+
93
+ @app.command()
94
+ def structure(
95
+ path: str = typer.Argument(".", help="Path to CV repository to validate"),
96
+ ):
97
+ """
98
+ Test CV repository structure and file format compliance.
99
+
100
+ Validates:
101
+ - Collection directory structure
102
+ - JSONLD context files
103
+ - Element JSON files
104
+ - project_specs.json references
105
+
106
+ Examples:
107
+ esgvoc test structure .
108
+ esgvoc test structure /path/to/cv/repo
109
+ """
110
+ tester = CVTester()
111
+
112
+ try:
113
+ if not tester.test_repository_structure(path):
114
+ raise typer.Exit(1)
115
+ console.print("[green]✅ Repository structure validation passed[/green]")
116
+ except Exception as e:
117
+ console.print(f"[red]❌ Structure validation failed: {e}[/red]")
118
+ raise typer.Exit(1)
119
+
120
+
121
+ @app.command()
122
+ def api(
123
+ project: str = typer.Argument(..., help="Project name to test API access for"),
124
+ path: str = typer.Argument(".", help="Path to CV repository"),
125
+ debug_terms: bool = typer.Option(True, "--debug-terms/--no-debug-terms", help="Show detailed debugging info for missing terms"),
126
+ ):
127
+ """
128
+ Test esgvoc API access for all repository collections and elements.
129
+
130
+ Validates:
131
+ - Project is accessible via esgvoc API
132
+ - All repository collections are queryable
133
+ - All repository elements are accessible
134
+ - API functions work correctly
135
+
136
+ Examples:
137
+ esgvoc test api obs4mip .
138
+ esgvoc test api cmip6 /path/to/cmip6/repo
139
+ """
140
+ tester = CVTester(debug_missing_terms=debug_terms)
141
+
142
+ try:
143
+ if not tester.test_esgvoc_api_access(project, path):
144
+ raise typer.Exit(1)
145
+ console.print("[green]✅ ESGVoc API access validation passed[/green]")
146
+ except Exception as e:
147
+ console.print(f"[red]❌ API validation failed: {e}[/red]")
148
+ raise typer.Exit(1)
149
+
150
+
151
+ @app.command()
152
+ def run(
153
+ project: str = typer.Argument(..., help="Project name to test"),
154
+ path: Optional[str] = typer.Argument(None, help="Path to CV repository (auto-detected if not provided)"),
155
+ repo_url: Optional[str] = typer.Option(None, "--repo", "-r", help="Custom repository URL"),
156
+ branch: Optional[str] = typer.Option(None, "--branch", "-b", help="Custom branch to test"),
157
+ universe_branch: Optional[str] = typer.Option(None, "--universe-branch", "-u", help="Custom universe branch"),
158
+ debug_terms: bool = typer.Option(True, "--debug-terms/--no-debug-terms", help="Show detailed debugging info for missing terms"),
159
+ ):
160
+ """
161
+ Run complete CV test suite: configure, sync, structure, and API tests.
162
+
163
+ This is the comprehensive test that runs all validation steps:
164
+ 1. Configure esgvoc with the specified project
165
+ 2. Synchronize/download CVs
166
+ 3. Validate repository structure
167
+ 4. Test esgvoc API access
168
+
169
+ Examples:
170
+ esgvoc test run obs4mip
171
+ esgvoc test run cmip6 --branch my-test-branch
172
+ esgvoc test run cmip6 --universe-branch my-universe-branch
173
+ esgvoc test run cmip6 /path/to/custom/repo --branch my-test-branch --universe-branch dev
174
+ esgvoc test run custom --repo https://github.com/me/cvs --branch main --universe-branch main
175
+ """
176
+ tester = CVTester(debug_missing_terms=debug_terms)
177
+
178
+ try:
179
+ success = tester.run_complete_test(project, repo_url, branch, path, None, universe_branch)
180
+ if success:
181
+ console.print(f"[bold green]🎉 All tests passed for project '{project}'![/bold green]")
182
+ else:
183
+ # The detailed failure information is already printed by cv_tester
184
+ raise typer.Exit(1)
185
+ except Exception as e:
186
+ console.print(f"[red]❌ Test suite failed: {e}[/red]")
187
+ raise typer.Exit(1)
188
+ finally:
189
+ tester.cleanup()
190
+
191
+
192
+ @app.command()
193
+ def env(
194
+ command: str = typer.Argument(..., help="Environment mode command: 'configure' or 'test'"),
195
+ project: Optional[str] = typer.Option(None, "--project", "-p", help="Project name (auto-detected if not provided)"),
196
+ repo_url: Optional[str] = typer.Option(
197
+ None, "--repo-url", help="Repository URL (from REPO_URL env var if not provided)"
198
+ ),
199
+ branch: Optional[str] = typer.Option(None, "--branch", help="Branch (from TEST_BRANCH env var if not provided)"),
200
+ universe_branch: Optional[str] = typer.Option(None, "--universe-branch", help="Universe branch (from UNIVERSE_BRANCH env var if not provided)"),
201
+ debug_terms: bool = typer.Option(True, "--debug-terms/--no-debug-terms", help="Show detailed debugging info for missing terms"),
202
+ ):
203
+ """
204
+ Environment variable mode for CI/CD integration and automated testing.
205
+
206
+ Reads configuration from environment variables:
207
+ - REPO_URL: Repository URL to test
208
+ - TEST_BRANCH: Branch to test
209
+ - PROJECT_NAME: Project name (auto-detected if not set)
210
+ - UNIVERSE_BRANCH: Universe branch to test (optional)
211
+ - ESGVOC_LIBRARY_BRANCH: ESGVoc library branch (informational)
212
+
213
+ Examples:
214
+ # Set environment and run
215
+ export REPO_URL=https://github.com/me/obs4MIPs_CVs
216
+ export TEST_BRANCH=test-branch
217
+ export UNIVERSE_BRANCH=my-universe-branch
218
+ esgvoc test env configure
219
+ esgvoc test env test
220
+
221
+ # Or use options
222
+ esgvoc test env configure --project obs4mip --repo-url https://github.com/me/repo --branch main --universe-branch dev
223
+ """
224
+ import os
225
+
226
+ # Get config from environment or options
227
+ final_repo_url = repo_url or os.environ.get("REPO_URL")
228
+ final_branch = branch or os.environ.get("TEST_BRANCH")
229
+ final_universe_branch = universe_branch or os.environ.get("UNIVERSE_BRANCH")
230
+ final_project = project or os.environ.get("PROJECT_NAME")
231
+
232
+ # Auto-detect project if not provided
233
+ if not final_project:
234
+ from esgvoc.apps.test_cv.cv_tester import detect_project_name
235
+
236
+ final_project = detect_project_name()
237
+
238
+ if command == "configure":
239
+ if not final_repo_url or not final_branch:
240
+ console.print("[red]❌ REPO_URL and TEST_BRANCH are required for env configure[/red]")
241
+ console.print("Set environment variables or use --repo-url and --branch options")
242
+ raise typer.Exit(1)
243
+
244
+ # Use configure command
245
+ configure(final_project, final_repo_url, final_branch, final_universe_branch, sync=True)
246
+
247
+ elif command == "test":
248
+ # Use run command
249
+ run(final_project, None, final_repo_url, final_branch, final_universe_branch, debug_terms)
250
+
251
+ else:
252
+ console.print(f"[red]❌ Invalid env command '{command}'. Use 'configure' or 'test'[/red]")
253
+ raise typer.Exit(1)
254
+
255
+
256
+ if __name__ == "__main__":
257
+ app()
258
+