esgvoc 0.4.0__py3-none-any.whl → 1.0.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.
- esgvoc/__init__.py +1 -1
- esgvoc/api/data_descriptors/__init__.py +52 -28
- esgvoc/api/data_descriptors/activity.py +3 -3
- esgvoc/api/data_descriptors/area_label.py +16 -1
- esgvoc/api/data_descriptors/branded_suffix.py +20 -0
- esgvoc/api/data_descriptors/branded_variable.py +12 -0
- esgvoc/api/data_descriptors/consortium.py +14 -13
- esgvoc/api/data_descriptors/contact.py +5 -0
- esgvoc/api/data_descriptors/conventions.py +6 -0
- esgvoc/api/data_descriptors/creation_date.py +5 -0
- esgvoc/api/data_descriptors/data_descriptor.py +14 -9
- esgvoc/api/data_descriptors/data_specs_version.py +5 -0
- esgvoc/api/data_descriptors/date.py +1 -1
- esgvoc/api/data_descriptors/directory_date.py +1 -1
- esgvoc/api/data_descriptors/experiment.py +13 -11
- esgvoc/api/data_descriptors/forcing_index.py +1 -1
- esgvoc/api/data_descriptors/frequency.py +3 -3
- esgvoc/api/data_descriptors/further_info_url.py +5 -0
- esgvoc/api/data_descriptors/grid_label.py +2 -2
- esgvoc/api/data_descriptors/horizontal_label.py +15 -1
- esgvoc/api/data_descriptors/initialisation_index.py +1 -1
- esgvoc/api/data_descriptors/institution.py +8 -5
- esgvoc/api/data_descriptors/known_branded_variable.py +23 -0
- esgvoc/api/data_descriptors/license.py +3 -3
- esgvoc/api/data_descriptors/member_id.py +9 -0
- esgvoc/api/data_descriptors/mip_era.py +1 -1
- esgvoc/api/data_descriptors/model_component.py +1 -1
- esgvoc/api/data_descriptors/obs_type.py +5 -0
- esgvoc/api/data_descriptors/organisation.py +1 -1
- esgvoc/api/data_descriptors/physic_index.py +1 -1
- esgvoc/api/data_descriptors/product.py +2 -2
- esgvoc/api/data_descriptors/publication_status.py +5 -0
- esgvoc/api/data_descriptors/realisation_index.py +1 -1
- esgvoc/api/data_descriptors/realm.py +1 -1
- esgvoc/api/data_descriptors/region.py +5 -0
- esgvoc/api/data_descriptors/resolution.py +3 -3
- esgvoc/api/data_descriptors/source.py +9 -5
- esgvoc/api/data_descriptors/source_type.py +1 -1
- esgvoc/api/data_descriptors/table.py +3 -2
- esgvoc/api/data_descriptors/temporal_label.py +15 -1
- esgvoc/api/data_descriptors/time_range.py +4 -3
- esgvoc/api/data_descriptors/title.py +5 -0
- esgvoc/api/data_descriptors/tracking_id.py +5 -0
- esgvoc/api/data_descriptors/variable.py +25 -12
- esgvoc/api/data_descriptors/variant_label.py +3 -3
- esgvoc/api/data_descriptors/vertical_label.py +14 -0
- esgvoc/api/project_specs.py +117 -2
- esgvoc/api/projects.py +328 -287
- esgvoc/api/search.py +30 -3
- esgvoc/api/universe.py +42 -27
- esgvoc/apps/drs/generator.py +87 -74
- esgvoc/apps/jsg/cmip6_template.json +74 -0
- esgvoc/apps/jsg/json_schema_generator.py +194 -0
- esgvoc/cli/config.py +500 -0
- esgvoc/cli/find.py +138 -0
- esgvoc/cli/get.py +43 -38
- esgvoc/cli/main.py +10 -3
- esgvoc/cli/status.py +27 -18
- esgvoc/cli/valid.py +10 -15
- esgvoc/core/db/models/project.py +11 -11
- esgvoc/core/db/models/universe.py +3 -3
- esgvoc/core/db/project_ingestion.py +40 -40
- esgvoc/core/db/universe_ingestion.py +36 -33
- esgvoc/core/logging_handler.py +24 -2
- esgvoc/core/repo_fetcher.py +61 -59
- esgvoc/core/service/data_merger.py +47 -34
- esgvoc/core/service/state.py +107 -83
- {esgvoc-0.4.0.dist-info → esgvoc-1.0.1.dist-info}/METADATA +5 -20
- esgvoc-1.0.1.dist-info/RECORD +95 -0
- esgvoc/core/logging.conf +0 -21
- esgvoc-0.4.0.dist-info/RECORD +0 -80
- {esgvoc-0.4.0.dist-info → esgvoc-1.0.1.dist-info}/WHEEL +0 -0
- {esgvoc-0.4.0.dist-info → esgvoc-1.0.1.dist-info}/entry_points.txt +0 -0
- {esgvoc-0.4.0.dist-info → esgvoc-1.0.1.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import json
|
|
3
|
+
from json import JSONEncoder
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
from sqlmodel import Session
|
|
8
|
+
|
|
9
|
+
from esgvoc.api import projects, search
|
|
10
|
+
from esgvoc.api.project_specs import (
|
|
11
|
+
GlobalAttributeSpecBase,
|
|
12
|
+
GlobalAttributeSpecSpecific,
|
|
13
|
+
GlobalAttributeVisitor,
|
|
14
|
+
)
|
|
15
|
+
from esgvoc.core.constants import DRS_SPECS_JSON_KEY, PATTERN_JSON_KEY
|
|
16
|
+
from esgvoc.core.db.models.project import PCollection, TermKind
|
|
17
|
+
from esgvoc.core.exceptions import EsgvocNotFoundError, EsgvocNotImplementedError
|
|
18
|
+
|
|
19
|
+
KEY_SEPARATOR = ':'
|
|
20
|
+
JSON_SCHEMA_TEMPLATE_DIR_PATH = Path(__file__).parent
|
|
21
|
+
JSON_SCHEMA_TEMPLATE_FILE_NAME_TEMPLATE = '{project_id}_template.json'
|
|
22
|
+
JSON_INDENTATION = 2
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _process_plain(collection: PCollection, selected_field: str) -> set[str]:
|
|
26
|
+
result: set[str] = set()
|
|
27
|
+
for term in collection.terms:
|
|
28
|
+
if selected_field in term.specs:
|
|
29
|
+
value = term.specs[selected_field]
|
|
30
|
+
result.add(value)
|
|
31
|
+
else:
|
|
32
|
+
raise EsgvocNotFoundError(f'missing key {selected_field} for term {term.id} in ' +
|
|
33
|
+
f'collection {collection.id}')
|
|
34
|
+
return result
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _process_composite(collection: PCollection, universe_session: Session,
|
|
38
|
+
project_session: Session) -> str:
|
|
39
|
+
result = ""
|
|
40
|
+
for term in collection.terms:
|
|
41
|
+
_, parts = projects._get_composite_term_separator_parts(term)
|
|
42
|
+
for part in parts:
|
|
43
|
+
resolved_term = projects._resolve_term(part, universe_session, project_session)
|
|
44
|
+
if resolved_term.kind == TermKind.PATTERN:
|
|
45
|
+
result += resolved_term.specs[PATTERN_JSON_KEY]
|
|
46
|
+
else:
|
|
47
|
+
raise EsgvocNotImplementedError(f'{term.kind} term is not supported yet')
|
|
48
|
+
# Patterns terms are meant to be validated individually.
|
|
49
|
+
# So their regex are defined as a whole (begins by a ^, ends by a $).
|
|
50
|
+
# As the pattern is a concatenation of plain or regex, multiple ^ and $ can exist.
|
|
51
|
+
# The later, must be removed.
|
|
52
|
+
result = result.replace('^', '').replace('$', '')
|
|
53
|
+
result = f'^{result}$'
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _process_pattern(collection: PCollection) -> str:
|
|
58
|
+
# The generation of the value of the field pattern for the collections with more than one term
|
|
59
|
+
# is not specified yet.
|
|
60
|
+
if len(collection.terms) == 1:
|
|
61
|
+
term = collection.terms[0]
|
|
62
|
+
return term.specs[PATTERN_JSON_KEY]
|
|
63
|
+
else:
|
|
64
|
+
msg = f"unsupported collection of term pattern with more than one term for '{collection.id}'"
|
|
65
|
+
raise EsgvocNotImplementedError(msg)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _generate_attribute_key(project_id: str, attribute_name) -> str:
|
|
69
|
+
return f'{project_id}{KEY_SEPARATOR}{attribute_name}'
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class JsonPropertiesVisitor(GlobalAttributeVisitor, contextlib.AbstractContextManager):
|
|
73
|
+
def __init__(self, project_id: str) -> None:
|
|
74
|
+
self.project_id = project_id
|
|
75
|
+
# Project session can't be None here.
|
|
76
|
+
self.universe_session: Session = search.get_universe_session()
|
|
77
|
+
self.project_session: Session = projects._get_project_session_with_exception(project_id)
|
|
78
|
+
self.collections: dict[str, PCollection] = dict()
|
|
79
|
+
for collection in projects._get_all_collections_in_project(self.project_session):
|
|
80
|
+
self.collections[collection.id] = collection
|
|
81
|
+
|
|
82
|
+
def __exit__(self, exception_type, exception_value, exception_traceback):
|
|
83
|
+
self.project_session.close()
|
|
84
|
+
self.universe_session.close()
|
|
85
|
+
if exception_type is not None:
|
|
86
|
+
raise exception_value
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
def _generate_attribute_property(self, attribute_name: str, source_collection: str,
|
|
90
|
+
selected_field: str) -> tuple[str, str | set[str]]:
|
|
91
|
+
property_value: str | set[str]
|
|
92
|
+
property_key: str
|
|
93
|
+
if source_collection not in self.collections:
|
|
94
|
+
raise EsgvocNotFoundError(f"collection '{source_collection}' referenced by attribute " +
|
|
95
|
+
f"{attribute_name} is not found")
|
|
96
|
+
collection = self.collections[source_collection]
|
|
97
|
+
match collection.term_kind:
|
|
98
|
+
case TermKind.PLAIN:
|
|
99
|
+
property_value = _process_plain(collection=collection,
|
|
100
|
+
selected_field=selected_field)
|
|
101
|
+
property_key = 'enum'
|
|
102
|
+
case TermKind.COMPOSITE:
|
|
103
|
+
property_value = _process_composite(collection=collection,
|
|
104
|
+
universe_session=self.universe_session,
|
|
105
|
+
project_session=self.project_session)
|
|
106
|
+
property_key = 'pattern'
|
|
107
|
+
case TermKind.PATTERN:
|
|
108
|
+
property_value = _process_pattern(collection)
|
|
109
|
+
property_key = 'pattern'
|
|
110
|
+
case _:
|
|
111
|
+
msg = f"unsupported term kind '{collection.term_kind}' " + \
|
|
112
|
+
f"for global attribute {attribute_name}"
|
|
113
|
+
raise EsgvocNotImplementedError(msg)
|
|
114
|
+
return property_key, property_value
|
|
115
|
+
|
|
116
|
+
def visit_base_attribute(self, attribute_name: str, attribute: GlobalAttributeSpecBase) \
|
|
117
|
+
-> tuple[str, dict[str, str | set[str]]]:
|
|
118
|
+
attribute_key = _generate_attribute_key(self.project_id, attribute_name)
|
|
119
|
+
attribute_properties: dict[str, str | set[str]] = dict()
|
|
120
|
+
attribute_properties['type'] = attribute.value_type.value
|
|
121
|
+
property_key, property_value = self._generate_attribute_property(attribute_name,
|
|
122
|
+
attribute.source_collection,
|
|
123
|
+
DRS_SPECS_JSON_KEY)
|
|
124
|
+
attribute_properties[property_key] = property_value
|
|
125
|
+
return attribute_key, attribute_properties
|
|
126
|
+
|
|
127
|
+
def visit_specific_attribute(self, attribute_name: str, attribute: GlobalAttributeSpecSpecific) \
|
|
128
|
+
-> tuple[str, dict[str, str | set[str]]]:
|
|
129
|
+
attribute_key = _generate_attribute_key(self.project_id, attribute_name)
|
|
130
|
+
attribute_properties: dict[str, str | set[str]] = dict()
|
|
131
|
+
attribute_properties['type'] = attribute.value_type.value
|
|
132
|
+
property_key, property_value = self._generate_attribute_property(attribute_name,
|
|
133
|
+
attribute.source_collection,
|
|
134
|
+
attribute.specific_key)
|
|
135
|
+
attribute_properties[property_key] = property_value
|
|
136
|
+
return attribute_key, attribute_properties
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _inject_global_attributes(json_root: dict, project_id: str, attribute_names: Iterable[str]) -> None:
|
|
140
|
+
attribute_properties = list()
|
|
141
|
+
for attribute_name in attribute_names:
|
|
142
|
+
attribute_key = _generate_attribute_key(project_id, attribute_name)
|
|
143
|
+
attribute_properties.append({"required": [attribute_key]})
|
|
144
|
+
json_root['definitions']['require_any']['anyOf'] = attribute_properties
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _inject_properties(json_root: dict, properties: list[tuple]) -> None:
|
|
148
|
+
for property in properties:
|
|
149
|
+
json_root['definitions']['fields']['properties'][property[0]] = property[1]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class SetEncoder(JSONEncoder):
|
|
153
|
+
def default(self, o):
|
|
154
|
+
if isinstance(o, set):
|
|
155
|
+
return list(o)
|
|
156
|
+
else:
|
|
157
|
+
return super().default(self, o)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def generate_json_schema(project_id: str) -> str:
|
|
161
|
+
"""
|
|
162
|
+
Generate json schema for the given project.
|
|
163
|
+
|
|
164
|
+
:param project_id: The id of the given project.
|
|
165
|
+
:type project_id: str
|
|
166
|
+
:returns: The content of a json schema
|
|
167
|
+
:rtype: str
|
|
168
|
+
:raises EsgvocNotFoundError: On missing information
|
|
169
|
+
:raises EsgvocNotImplementedError: On unexpected operations
|
|
170
|
+
"""
|
|
171
|
+
file_name = JSON_SCHEMA_TEMPLATE_FILE_NAME_TEMPLATE.format(project_id=project_id)
|
|
172
|
+
template_file_path = JSON_SCHEMA_TEMPLATE_DIR_PATH.joinpath(file_name)
|
|
173
|
+
if template_file_path.exists():
|
|
174
|
+
project_specs = projects.get_project(project_id)
|
|
175
|
+
if project_specs:
|
|
176
|
+
if project_specs.global_attributes_specs:
|
|
177
|
+
with open(file=template_file_path, mode='r') as file, \
|
|
178
|
+
JsonPropertiesVisitor(project_id) as visitor:
|
|
179
|
+
file_content = file.read()
|
|
180
|
+
root = json.loads(file_content)
|
|
181
|
+
properties: list[tuple[str, dict[str, str | set[str]]]] = list()
|
|
182
|
+
for attribute_name, attribute in project_specs.global_attributes_specs.items():
|
|
183
|
+
attribute_key, attribute_properties = attribute.accept(attribute_name, visitor)
|
|
184
|
+
properties.append((attribute_key, attribute_properties))
|
|
185
|
+
_inject_properties(root, properties)
|
|
186
|
+
_inject_global_attributes(root, project_id, project_specs.global_attributes_specs.keys())
|
|
187
|
+
return json.dumps(root, indent=JSON_INDENTATION, cls=SetEncoder)
|
|
188
|
+
else:
|
|
189
|
+
raise EsgvocNotFoundError(f"global attributes for the project '{project_id}' " +
|
|
190
|
+
"are not provided")
|
|
191
|
+
else:
|
|
192
|
+
raise EsgvocNotFoundError(f"specs of project '{project_id}' is not found")
|
|
193
|
+
else:
|
|
194
|
+
raise EsgvocNotFoundError(f"template for project '{project_id}' is not found")
|
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()
|