esgvoc 1.1.2__py3-none-any.whl → 1.1.3__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/project_specs.py +24 -0
- esgvoc/apps/jsg/json_schema_generator.py +9 -7
- esgvoc/apps/jsg/templates/template.jinja +1 -1
- esgvoc/apps/test_cv/cv_tester.py +232 -31
- esgvoc/cli/clean.py +304 -0
- esgvoc/cli/config.py +226 -11
- esgvoc/cli/install.py +24 -0
- esgvoc/cli/main.py +4 -0
- esgvoc/cli/offline.py +269 -0
- esgvoc/cli/status.py +39 -7
- esgvoc/core/db/project_ingestion.py +6 -2
- esgvoc/core/repo_fetcher.py +87 -4
- esgvoc/core/service/__init__.py +4 -3
- esgvoc/core/service/configuration/config_manager.py +17 -16
- esgvoc/core/service/configuration/setting.py +162 -66
- esgvoc/core/service/data_merger.py +2 -1
- esgvoc/core/service/esg_voc.py +17 -19
- esgvoc/core/service/state.py +65 -27
- {esgvoc-1.1.2.dist-info → esgvoc-1.1.3.dist-info}/METADATA +1 -1
- {esgvoc-1.1.2.dist-info → esgvoc-1.1.3.dist-info}/RECORD +24 -22
- {esgvoc-1.1.2.dist-info → esgvoc-1.1.3.dist-info}/WHEEL +0 -0
- {esgvoc-1.1.2.dist-info → esgvoc-1.1.3.dist-info}/entry_points.txt +0 -0
- {esgvoc-1.1.2.dist-info → esgvoc-1.1.3.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -1,7 +1,34 @@
|
|
|
1
1
|
from typing import ClassVar, Dict, Optional
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
import toml
|
|
4
5
|
from pydantic import BaseModel, Field
|
|
6
|
+
from platformdirs import PlatformDirs
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def resolve_path_to_absolute(relative_path: Optional[str], config_name: Optional[str] = None) -> Optional[str]:
|
|
10
|
+
"""
|
|
11
|
+
Convert a relative path to an absolute path without modifying the original.
|
|
12
|
+
This is used for internal path resolution only.
|
|
13
|
+
"""
|
|
14
|
+
if relative_path is None:
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
path_obj = Path(relative_path)
|
|
18
|
+
|
|
19
|
+
if path_obj.is_absolute():
|
|
20
|
+
return str(path_obj.resolve())
|
|
21
|
+
|
|
22
|
+
# Handle dot-relative paths (./... or ../..) relative to current working directory
|
|
23
|
+
if relative_path.startswith('.'):
|
|
24
|
+
return str((Path.cwd() / relative_path).resolve())
|
|
25
|
+
|
|
26
|
+
# Handle plain relative paths using PlatformDirs with config name (default behavior)
|
|
27
|
+
dirs = PlatformDirs("esgvoc", "ipsl")
|
|
28
|
+
base_path = Path(dirs.user_data_path).expanduser().resolve()
|
|
29
|
+
if config_name:
|
|
30
|
+
base_path = base_path / config_name
|
|
31
|
+
return str(base_path / relative_path)
|
|
5
32
|
|
|
6
33
|
|
|
7
34
|
class ProjectSettings(BaseModel):
|
|
@@ -10,6 +37,20 @@ class ProjectSettings(BaseModel):
|
|
|
10
37
|
branch: Optional[str] = "main"
|
|
11
38
|
local_path: Optional[str] = None
|
|
12
39
|
db_path: Optional[str] = None
|
|
40
|
+
offline_mode: bool = False
|
|
41
|
+
_config_name: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
def set_config_name(self, config_name: str):
|
|
44
|
+
"""Set the config name for path resolution."""
|
|
45
|
+
self._config_name = config_name
|
|
46
|
+
|
|
47
|
+
def get_absolute_local_path(self) -> Optional[str]:
|
|
48
|
+
"""Get the absolute local path without modifying the stored value."""
|
|
49
|
+
return resolve_path_to_absolute(self.local_path, self._config_name)
|
|
50
|
+
|
|
51
|
+
def get_absolute_db_path(self) -> Optional[str]:
|
|
52
|
+
"""Get the absolute db path without modifying the stored value."""
|
|
53
|
+
return resolve_path_to_absolute(self.db_path, self._config_name)
|
|
13
54
|
|
|
14
55
|
|
|
15
56
|
class UniverseSettings(BaseModel):
|
|
@@ -17,71 +58,118 @@ class UniverseSettings(BaseModel):
|
|
|
17
58
|
branch: Optional[str] = None
|
|
18
59
|
local_path: Optional[str] = None
|
|
19
60
|
db_path: Optional[str] = None
|
|
61
|
+
offline_mode: bool = False
|
|
62
|
+
_config_name: Optional[str] = None
|
|
63
|
+
|
|
64
|
+
def set_config_name(self, config_name: str):
|
|
65
|
+
"""Set the config name for path resolution."""
|
|
66
|
+
self._config_name = config_name
|
|
67
|
+
|
|
68
|
+
def get_absolute_local_path(self) -> Optional[str]:
|
|
69
|
+
"""Get the absolute local path without modifying the stored value."""
|
|
70
|
+
return resolve_path_to_absolute(self.local_path, self._config_name)
|
|
71
|
+
|
|
72
|
+
def get_absolute_db_path(self) -> Optional[str]:
|
|
73
|
+
"""Get the absolute db path without modifying the stored value."""
|
|
74
|
+
return resolve_path_to_absolute(self.db_path, self._config_name)
|
|
20
75
|
|
|
21
76
|
|
|
22
77
|
class ServiceSettings(BaseModel):
|
|
23
78
|
universe: UniverseSettings
|
|
24
79
|
projects: Dict[str, ProjectSettings] = Field(default_factory=dict)
|
|
25
80
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"branch": "esgvoc",
|
|
32
|
-
"local_path": "repos/CMIP6_CVs",
|
|
33
|
-
"db_path": "dbs/cmip6.sqlite",
|
|
34
|
-
},
|
|
35
|
-
"cmip6plus": {
|
|
36
|
-
"project_name": "cmip6plus",
|
|
37
|
-
"github_repo": "https://github.com/WCRP-CMIP/CMIP6Plus_CVs",
|
|
38
|
-
"branch": "esgvoc",
|
|
39
|
-
"local_path": "repos/CMIP6Plus_CVs",
|
|
40
|
-
"db_path": "dbs/cmip6plus.sqlite",
|
|
41
|
-
},
|
|
42
|
-
"input4mip": {
|
|
43
|
-
"project_name": "input4mip",
|
|
44
|
-
"github_repo": "https://github.com/PCMDI/input4MIPs_CVs",
|
|
45
|
-
"branch": "esgvoc",
|
|
46
|
-
"local_path": "repos/Input4MIP_CVs",
|
|
47
|
-
"db_path": "dbs/input4mips.sqlite",
|
|
48
|
-
},
|
|
49
|
-
"obs4ref": {
|
|
50
|
-
"project_name": "obs4ref",
|
|
51
|
-
"github_repo": "https://github.com/Climate-REF/Obs4REF_CVs",
|
|
52
|
-
"branch": "main",
|
|
53
|
-
"local_path": "repos/obs4REF_CVs",
|
|
54
|
-
"db_path": "dbs/obs4ref.sqlite",
|
|
55
|
-
},
|
|
56
|
-
"cordex-cmip6": {
|
|
57
|
-
"project_name": "cordex-cmip6",
|
|
58
|
-
"github_repo": "https://github.com/WCRP-CORDEX/cordex-cmip6-cv",
|
|
59
|
-
"branch": "esgvoc",
|
|
60
|
-
"local_path": "repos/cordex-cmip6-cv",
|
|
61
|
-
"db_path": "dbs/cordex-cmip6.sqlite",
|
|
62
|
-
},
|
|
63
|
-
"cmip7": {
|
|
64
|
-
"project_name": "cmip7",
|
|
65
|
-
"github_repo": "https://github.com/WCRP-CMIP/CMIP7-CVs",
|
|
66
|
-
"branch": "esgvoc",
|
|
67
|
-
"local_path": "repos/CMIP7-CVs",
|
|
68
|
-
"db_path": "dbs/cmip7.sqlite",
|
|
69
|
-
},
|
|
70
|
-
}
|
|
81
|
+
def set_config_name(self, config_name: str):
|
|
82
|
+
"""Set the config name for all settings components."""
|
|
83
|
+
self.universe.set_config_name(config_name)
|
|
84
|
+
for project_settings in self.projects.values():
|
|
85
|
+
project_settings.set_config_name(config_name)
|
|
71
86
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _get_default_base_path() -> Path:
|
|
89
|
+
"""Get the default base path for data storage using PlatformDirs."""
|
|
90
|
+
dirs = PlatformDirs("esgvoc", "ipsl")
|
|
91
|
+
return Path(dirs.user_data_path).expanduser().resolve()
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def _get_default_project_configs(cls) -> Dict[str, dict]:
|
|
95
|
+
"""Generate default project configurations with relative paths."""
|
|
96
|
+
return {
|
|
97
|
+
"cmip6": {
|
|
98
|
+
"project_name": "cmip6",
|
|
99
|
+
"github_repo": "https://github.com/WCRP-CMIP/CMIP6_CVs",
|
|
100
|
+
"branch": "esgvoc",
|
|
101
|
+
"local_path": "repos/CMIP6_CVs",
|
|
102
|
+
"db_path": "dbs/cmip6.sqlite",
|
|
103
|
+
"offline_mode": False,
|
|
104
|
+
},
|
|
105
|
+
"cmip6plus": {
|
|
106
|
+
"project_name": "cmip6plus",
|
|
107
|
+
"github_repo": "https://github.com/WCRP-CMIP/CMIP6Plus_CVs",
|
|
108
|
+
"branch": "esgvoc",
|
|
109
|
+
"local_path": "repos/CMIP6Plus_CVs",
|
|
110
|
+
"db_path": "dbs/cmip6plus.sqlite",
|
|
111
|
+
"offline_mode": False,
|
|
112
|
+
},
|
|
113
|
+
"input4mip": {
|
|
114
|
+
"project_name": "input4mip",
|
|
115
|
+
"github_repo": "https://github.com/PCMDI/input4MIPs_CVs",
|
|
116
|
+
"branch": "esgvoc",
|
|
117
|
+
"local_path": "repos/Input4MIP_CVs",
|
|
118
|
+
"db_path": "dbs/input4mips.sqlite",
|
|
119
|
+
"offline_mode": False,
|
|
120
|
+
},
|
|
121
|
+
"obs4ref": {
|
|
122
|
+
"project_name": "obs4ref",
|
|
123
|
+
"github_repo": "https://github.com/Climate-REF/Obs4REF_CVs",
|
|
124
|
+
"branch": "main",
|
|
125
|
+
"local_path": "repos/obs4REF_CVs",
|
|
126
|
+
"db_path": "dbs/obs4ref.sqlite",
|
|
127
|
+
"offline_mode": False,
|
|
128
|
+
},
|
|
129
|
+
"cordex-cmip6": {
|
|
130
|
+
"project_name": "cordex-cmip6",
|
|
131
|
+
"github_repo": "https://github.com/WCRP-CORDEX/cordex-cmip6-cv",
|
|
132
|
+
"branch": "esgvoc",
|
|
133
|
+
"local_path": "repos/cordex-cmip6-cv",
|
|
134
|
+
"db_path": "dbs/cordex-cmip6.sqlite",
|
|
135
|
+
"offline_mode": False,
|
|
136
|
+
},
|
|
137
|
+
"cmip7": {
|
|
138
|
+
"project_name": "cmip7",
|
|
139
|
+
"github_repo": "https://github.com/WCRP-CMIP/CMIP7-CVs",
|
|
140
|
+
"branch": "esgvoc",
|
|
141
|
+
"local_path": "repos/CMIP7-CVs",
|
|
142
|
+
"db_path": "dbs/cmip7.sqlite",
|
|
143
|
+
"offline_mode": False,
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def _get_default_settings(cls) -> dict:
|
|
149
|
+
"""Generate default settings with relative paths."""
|
|
150
|
+
project_configs = cls._get_default_project_configs()
|
|
151
|
+
return {
|
|
152
|
+
"universe": {
|
|
153
|
+
"github_repo": "https://github.com/WCRP-CMIP/WCRP-universe",
|
|
154
|
+
"branch": "esgvoc",
|
|
155
|
+
"local_path": "repos/WCRP-universe",
|
|
156
|
+
"db_path": "dbs/universe.sqlite",
|
|
157
|
+
"offline_mode": False,
|
|
158
|
+
},
|
|
159
|
+
"projects": [
|
|
160
|
+
project_configs["cmip6"],
|
|
161
|
+
project_configs["cmip6plus"],
|
|
162
|
+
],
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# 🔹 Properties that provide access to the dynamic configurations
|
|
166
|
+
@property
|
|
167
|
+
def DEFAULT_PROJECT_CONFIGS(self) -> Dict[str, dict]:
|
|
168
|
+
return self._get_default_project_configs()
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def DEFAULT_SETTINGS(self) -> dict:
|
|
172
|
+
return self._get_default_settings()
|
|
85
173
|
|
|
86
174
|
@classmethod
|
|
87
175
|
def load_from_file(cls, file_path: str) -> "ServiceSettings":
|
|
@@ -89,7 +177,7 @@ class ServiceSettings(BaseModel):
|
|
|
89
177
|
try:
|
|
90
178
|
data = toml.load(file_path)
|
|
91
179
|
except FileNotFoundError:
|
|
92
|
-
data = cls.
|
|
180
|
+
data = cls._get_default_settings().copy() # Use defaults if the file is missing
|
|
93
181
|
|
|
94
182
|
projects = {p["project_name"]: ProjectSettings(**p) for p in data.pop("projects", [])}
|
|
95
183
|
return cls(universe=UniverseSettings(**data["universe"]), projects=projects)
|
|
@@ -97,7 +185,7 @@ class ServiceSettings(BaseModel):
|
|
|
97
185
|
@classmethod
|
|
98
186
|
def load_default(cls) -> "ServiceSettings":
|
|
99
187
|
"""Load default settings."""
|
|
100
|
-
return cls.load_from_dict(cls.
|
|
188
|
+
return cls.load_from_dict(cls._get_default_settings())
|
|
101
189
|
|
|
102
190
|
@classmethod
|
|
103
191
|
def load_from_dict(cls, config_data: dict) -> "ServiceSettings":
|
|
@@ -136,12 +224,13 @@ class ServiceSettings(BaseModel):
|
|
|
136
224
|
if project_name in self.projects:
|
|
137
225
|
return False # Project already exists
|
|
138
226
|
|
|
139
|
-
|
|
227
|
+
default_configs = self._get_default_project_configs()
|
|
228
|
+
if project_name not in default_configs:
|
|
140
229
|
raise ValueError(
|
|
141
|
-
f"Unknown project '{project_name}'. Available defaults: {list(
|
|
230
|
+
f"Unknown project '{project_name}'. Available defaults: {list(default_configs.keys())}"
|
|
142
231
|
)
|
|
143
232
|
|
|
144
|
-
config =
|
|
233
|
+
config = default_configs[project_name].copy()
|
|
145
234
|
self.projects[project_name] = ProjectSettings(**config)
|
|
146
235
|
return True
|
|
147
236
|
|
|
@@ -193,6 +282,11 @@ class ServiceSettings(BaseModel):
|
|
|
193
282
|
if project_name not in self.projects:
|
|
194
283
|
return False
|
|
195
284
|
|
|
285
|
+
# Handle boolean conversion for offline_mode if present
|
|
286
|
+
if 'offline_mode' in kwargs:
|
|
287
|
+
if isinstance(kwargs['offline_mode'], str):
|
|
288
|
+
kwargs['offline_mode'] = kwargs['offline_mode'].lower() in ("true", "1", "yes", "on")
|
|
289
|
+
|
|
196
290
|
# Get current config and update with new values
|
|
197
291
|
current_config = self.projects[project_name].model_dump()
|
|
198
292
|
current_config.update(kwargs)
|
|
@@ -203,7 +297,7 @@ class ServiceSettings(BaseModel):
|
|
|
203
297
|
|
|
204
298
|
def get_available_default_projects(self) -> list[str]:
|
|
205
299
|
"""Return list of available default project names."""
|
|
206
|
-
return list(self.
|
|
300
|
+
return list(self._get_default_project_configs().keys())
|
|
207
301
|
|
|
208
302
|
def has_project(self, project_name: str) -> bool:
|
|
209
303
|
"""Check if a project exists in the current configuration."""
|
|
@@ -218,11 +312,13 @@ class ServiceSettings(BaseModel):
|
|
|
218
312
|
def main():
|
|
219
313
|
# Create default settings (only cmip6 and cmip6plus)
|
|
220
314
|
settings = ServiceSettings.load_default()
|
|
221
|
-
|
|
315
|
+
# ['cmip6', 'cmip6plus']
|
|
316
|
+
print(f"Default projects: {list(settings.projects.keys())}")
|
|
222
317
|
|
|
223
318
|
# See what other projects are available to add
|
|
224
319
|
available = settings.get_available_default_projects()
|
|
225
|
-
|
|
320
|
+
# ['cmip6', 'cmip6plus', 'input4mip', 'obs4mip']
|
|
321
|
+
print(f"Available default projects: {available}")
|
|
226
322
|
|
|
227
323
|
# Add optional projects when needed
|
|
228
324
|
added_input4mip = settings.add_project_from_default("input4mip")
|
|
@@ -46,7 +46,8 @@ class DataMerger:
|
|
|
46
46
|
try:
|
|
47
47
|
"""Fetch and merge data recursively, returning a list of progressively merged Data json instances."""
|
|
48
48
|
result_list = [self.data.json_dict] # Start with the original json object
|
|
49
|
-
|
|
49
|
+
# Track visited URIs to prevent cycles
|
|
50
|
+
visited = set(self.data.uri)
|
|
50
51
|
current_data = self.data
|
|
51
52
|
# print(current_data.expanded)
|
|
52
53
|
while True:
|
esgvoc/core/service/esg_voc.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
|
|
2
1
|
import logging
|
|
3
2
|
import os
|
|
4
3
|
from rich.logging import RichHandler
|
|
5
4
|
from rich.console import Console
|
|
6
5
|
import shutil
|
|
7
|
-
import esgvoc.core.service as service
|
|
6
|
+
import esgvoc.core.service as service
|
|
8
7
|
|
|
9
8
|
_LOGGER = logging.getLogger(__name__)
|
|
10
9
|
|
|
@@ -13,67 +12,66 @@ _LOGGER.addHandler(rich_handler)
|
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
def reset_init_repo():
|
|
16
|
-
service_settings = service.service_settings
|
|
15
|
+
service_settings = service.service_settings
|
|
17
16
|
if (service_settings.universe.local_path) and os.path.exists(service_settings.universe.local_path):
|
|
18
17
|
shutil.rmtree(service_settings.universe.local_path)
|
|
19
18
|
|
|
20
|
-
for _, proj in service_settings.projects.items():
|
|
19
|
+
for _, proj in service_settings.projects.items():
|
|
21
20
|
if (proj.local_path) and os.path.exists(proj.local_path):
|
|
22
21
|
shutil.rmtree(proj.local_path)
|
|
23
22
|
service.state_service.get_state_summary()
|
|
24
23
|
|
|
24
|
+
|
|
25
25
|
def reset_init_db():
|
|
26
|
-
service_settings = service.service_settings
|
|
26
|
+
service_settings = service.service_settings
|
|
27
27
|
if (service_settings.universe.db_path) and os.path.exists(service_settings.universe.db_path):
|
|
28
28
|
os.remove(service_settings.universe.db_path)
|
|
29
|
-
for _, proj in service_settings.projects.items():
|
|
29
|
+
for _, proj in service_settings.projects.items():
|
|
30
30
|
if (proj.db_path) and os.path.exists(proj.db_path):
|
|
31
31
|
os.remove(proj.db_path)
|
|
32
32
|
service.state_service.get_state_summary()
|
|
33
33
|
|
|
34
|
+
|
|
34
35
|
def reset_init_all():
|
|
35
36
|
reset_init_db()
|
|
36
37
|
reset_init_repo()
|
|
37
38
|
|
|
39
|
+
|
|
38
40
|
def display(table):
|
|
39
|
-
console = Console(record=True,width=200)
|
|
41
|
+
console = Console(record=True, width=200)
|
|
40
42
|
console.print(table)
|
|
41
43
|
|
|
44
|
+
|
|
42
45
|
def install():
|
|
43
46
|
service.state_service.synchronize_all()
|
|
44
47
|
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
49
|
if __name__ == "__main__":
|
|
51
50
|
|
|
52
|
-
def Nothing():
|
|
51
|
+
def Nothing(): # IT WORKS
|
|
53
52
|
reset_init_all()
|
|
54
53
|
display(service.state_service.table())
|
|
55
54
|
service.state_service.universe.sync()
|
|
56
55
|
display(service.state_service.table())
|
|
57
|
-
for _,proj in service.state_service.projects.items():
|
|
56
|
+
for _, proj in service.state_service.projects.items():
|
|
58
57
|
proj.sync()
|
|
59
58
|
display(service.state_service.table())
|
|
60
|
-
|
|
61
|
-
def OnlyLocal():
|
|
59
|
+
|
|
60
|
+
def OnlyLocal(): # IT ALSO WORKS
|
|
62
61
|
reset_init_db()
|
|
63
62
|
service.state_service.universe.github_access = False
|
|
64
|
-
for _,proj in service.state_service.projects.items():
|
|
63
|
+
for _, proj in service.state_service.projects.items():
|
|
65
64
|
proj.github_access = False
|
|
66
65
|
display(service.state_service.table())
|
|
67
66
|
|
|
68
67
|
service.state_service.universe.sync()
|
|
69
68
|
display(service.state_service.table())
|
|
70
|
-
for _,proj in service.state_service.projects.items():
|
|
69
|
+
for _, proj in service.state_service.projects.items():
|
|
71
70
|
proj.sync()
|
|
72
71
|
display(service.state_service.table())
|
|
73
72
|
|
|
74
73
|
# TODO Some other test to do to be complete:
|
|
75
|
-
# Change the settings ... for now .. let say nobody change the settings !
|
|
74
|
+
# Change the settings ... for now .. let say nobody change the settings !
|
|
76
75
|
|
|
77
76
|
OnlyLocal()
|
|
78
77
|
# service.state_service.synchronize_all()
|
|
79
|
-
|
esgvoc/core/service/state.py
CHANGED
|
@@ -11,16 +11,14 @@ from esgvoc.core.db.connection import DBConnection
|
|
|
11
11
|
from esgvoc.core.db.models.project import Project
|
|
12
12
|
from esgvoc.core.db.models.universe import Universe
|
|
13
13
|
from esgvoc.core.repo_fetcher import RepoFetcher
|
|
14
|
-
from esgvoc.core.service.configuration.setting import
|
|
15
|
-
ServiceSettings,
|
|
16
|
-
UniverseSettings)
|
|
14
|
+
from esgvoc.core.service.configuration.setting import ProjectSettings, ServiceSettings, UniverseSettings
|
|
17
15
|
|
|
18
16
|
logger = logging.getLogger(__name__)
|
|
19
17
|
|
|
20
18
|
|
|
21
19
|
class BaseState:
|
|
22
20
|
def __init__(
|
|
23
|
-
self, github_repo: str, branch: str = "main", local_path: Optional[str] = None, db_path: Optional[str] = None
|
|
21
|
+
self, github_repo: str, branch: str = "main", local_path: Optional[str] = None, db_path: Optional[str] = None, offline_mode: bool = False
|
|
24
22
|
):
|
|
25
23
|
from esgvoc.core.service import config_manager
|
|
26
24
|
|
|
@@ -28,26 +26,24 @@ class BaseState:
|
|
|
28
26
|
|
|
29
27
|
self.github_repo: str = github_repo
|
|
30
28
|
self.branch: str = branch
|
|
31
|
-
self.
|
|
29
|
+
self.offline_mode: bool = offline_mode
|
|
30
|
+
# False if we dont have internet and some other cases
|
|
31
|
+
# In offline mode, disable github access from the start
|
|
32
|
+
self.github_access: bool = not offline_mode
|
|
32
33
|
self.github_version: str | None = None
|
|
33
34
|
|
|
34
|
-
self.local_path: str | None =
|
|
35
|
+
self.local_path: str | None = local_path
|
|
35
36
|
self.local_access: bool = True # False if we dont have cloned the remote repo yet
|
|
36
37
|
self.local_version: str | None = None
|
|
37
38
|
|
|
38
|
-
self.db_path: str | None =
|
|
39
|
+
self.db_path: str | None = db_path
|
|
39
40
|
self.db_access: bool = True # False if we cant access the db for some reason
|
|
40
41
|
self.db_version: str | None = None
|
|
41
42
|
|
|
42
|
-
self.rf = RepoFetcher(local_path=str(self.base_dir))
|
|
43
|
+
self.rf = RepoFetcher(local_path=str(self.base_dir), offline_mode=offline_mode)
|
|
43
44
|
self.db_connection: DBConnection | None = None
|
|
44
45
|
self.db_sqlmodel: Universe | Project | None = None
|
|
45
46
|
|
|
46
|
-
def _get_absolute_path(self, base_dir: str, path: str | None) -> str | None:
|
|
47
|
-
if base_dir != "" and path is not None:
|
|
48
|
-
return base_dir + "/" + path
|
|
49
|
-
if base_dir == "":
|
|
50
|
-
return path
|
|
51
47
|
|
|
52
48
|
def fetch_version_local(self):
|
|
53
49
|
if self.local_path:
|
|
@@ -60,11 +56,16 @@ class BaseState:
|
|
|
60
56
|
self.local_access = False
|
|
61
57
|
|
|
62
58
|
def fetch_version_remote(self):
|
|
59
|
+
if self.offline_mode:
|
|
60
|
+
logger.debug("Skipping remote version fetch due to offline mode")
|
|
61
|
+
self.github_access = False
|
|
62
|
+
return
|
|
63
|
+
|
|
63
64
|
if self.github_repo:
|
|
64
65
|
owner = None
|
|
65
66
|
repo = None
|
|
66
67
|
try:
|
|
67
|
-
owner, repo = self.github_repo.
|
|
68
|
+
owner, repo = self.github_repo.removeprefix("https://github.com/").split("/")
|
|
68
69
|
self.github_version = self.rf.get_github_version(owner, repo, self.branch)
|
|
69
70
|
self.github_access = True
|
|
70
71
|
logger.debug(f"Latest GitHub commit: {self.github_version}")
|
|
@@ -128,8 +129,19 @@ class BaseState:
|
|
|
128
129
|
else False,
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
def clone_remote(self):
|
|
132
|
-
|
|
132
|
+
def clone_remote(self, force_clean=False):
|
|
133
|
+
if self.offline_mode:
|
|
134
|
+
logger.warning("Cannot clone remote repository in offline mode")
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
# If force_clean is True or if local repo exists and we're handling divergence,
|
|
138
|
+
# remove the existing local repository to ensure clean state
|
|
139
|
+
if force_clean and self.local_path and os.path.exists(self.local_path):
|
|
140
|
+
print(f"Removing existing local repository: {self.local_path}")
|
|
141
|
+
import shutil
|
|
142
|
+
shutil.rmtree(self.local_path)
|
|
143
|
+
|
|
144
|
+
owner, repo = self.github_repo.removeprefix("https://github.com/").split("/")
|
|
133
145
|
# TODO add destination "local_path" in clone_repo, done in a wierd way Improve that:
|
|
134
146
|
self.rf.clone_repository(owner, repo, self.branch, self.local_path)
|
|
135
147
|
self.fetch_version_local()
|
|
@@ -138,8 +150,7 @@ class BaseState:
|
|
|
138
150
|
from esgvoc.core.db.models.project import project_create_db
|
|
139
151
|
from esgvoc.core.db.models.universe import universe_create_db
|
|
140
152
|
from esgvoc.core.db.project_ingestion import ingest_project
|
|
141
|
-
from esgvoc.core.db.universe_ingestion import
|
|
142
|
-
ingest_metadata_universe, ingest_universe)
|
|
153
|
+
from esgvoc.core.db.universe_ingestion import ingest_metadata_universe, ingest_universe
|
|
143
154
|
|
|
144
155
|
if self.db_path:
|
|
145
156
|
if os.path.exists(self.db_path):
|
|
@@ -168,6 +179,26 @@ class BaseState:
|
|
|
168
179
|
def sync(self):
|
|
169
180
|
summary = self.check_sync_status()
|
|
170
181
|
updated = False
|
|
182
|
+
|
|
183
|
+
if self.offline_mode:
|
|
184
|
+
print("Running in offline mode - only using local repositories and databases")
|
|
185
|
+
if self.local_access:
|
|
186
|
+
if not summary["local_db_sync"] and summary["local_db_sync"] is not None:
|
|
187
|
+
self.build_db()
|
|
188
|
+
updated = True
|
|
189
|
+
else:
|
|
190
|
+
print("Cache db is uptodate from local repository")
|
|
191
|
+
elif not self.db_access: # it can happen if the db is created but not filled
|
|
192
|
+
if self.local_path and os.path.exists(self.local_path):
|
|
193
|
+
self.build_db()
|
|
194
|
+
updated = True
|
|
195
|
+
else:
|
|
196
|
+
print(f"No local repository found at {self.local_path} - cannot sync in offline mode")
|
|
197
|
+
else:
|
|
198
|
+
print("Nothing to sync in offline mode - local repository and database are up to date")
|
|
199
|
+
return updated
|
|
200
|
+
|
|
201
|
+
# Online sync logic with offline-to-online transition detection
|
|
171
202
|
if (
|
|
172
203
|
self.github_access
|
|
173
204
|
and summary["github_db_sync"] is None
|
|
@@ -183,7 +214,11 @@ class BaseState:
|
|
|
183
214
|
self.build_db()
|
|
184
215
|
updated = True
|
|
185
216
|
elif not summary["github_local_sync"]:
|
|
186
|
-
|
|
217
|
+
# Critical fix: when local and remote diverge in online mode,
|
|
218
|
+
# prioritize remote truth by completely removing local repo and re-cloning
|
|
219
|
+
print(f"Local and remote repositories have diverged (local: {summary['local'][:8] if summary['local'] else 'N/A'}, remote: {summary['github'][:8] if summary['github'] else 'N/A'})")
|
|
220
|
+
print("Prioritizing remote repository truth - removing local repository and re-cloning from GitHub...")
|
|
221
|
+
self.clone_remote(force_clean=True)
|
|
187
222
|
self.build_db()
|
|
188
223
|
updated = True
|
|
189
224
|
else: # can be simply build in root and clone if neccessary
|
|
@@ -206,7 +241,10 @@ class BaseState:
|
|
|
206
241
|
|
|
207
242
|
class StateUniverse(BaseState):
|
|
208
243
|
def __init__(self, settings: UniverseSettings):
|
|
209
|
-
|
|
244
|
+
params = settings.model_dump()
|
|
245
|
+
params['local_path'] = settings.get_absolute_local_path()
|
|
246
|
+
params['db_path'] = settings.get_absolute_db_path()
|
|
247
|
+
super().__init__(**params)
|
|
210
248
|
self.db_sqlmodel = Universe
|
|
211
249
|
|
|
212
250
|
|
|
@@ -214,6 +252,8 @@ class StateProject(BaseState):
|
|
|
214
252
|
def __init__(self, settings: ProjectSettings):
|
|
215
253
|
mdict = settings.model_dump()
|
|
216
254
|
self.project_name = mdict.pop("project_name")
|
|
255
|
+
mdict['local_path'] = settings.get_absolute_local_path()
|
|
256
|
+
mdict['db_path'] = settings.get_absolute_db_path()
|
|
217
257
|
super().__init__(**mdict)
|
|
218
258
|
self.db_sqlmodel = Project
|
|
219
259
|
|
|
@@ -241,19 +281,17 @@ class StateService:
|
|
|
241
281
|
|
|
242
282
|
def synchronize_all(self):
|
|
243
283
|
print("sync universe")
|
|
284
|
+
if self.universe.offline_mode:
|
|
285
|
+
print("Universe is in offline mode")
|
|
244
286
|
universe_updated = self.universe.sync()
|
|
245
287
|
print("sync projects")
|
|
246
|
-
for project in self.projects.
|
|
288
|
+
for project_name, project in self.projects.items():
|
|
289
|
+
if project.offline_mode:
|
|
290
|
+
print(f"Project {project_name} is in offline mode")
|
|
247
291
|
project_updated = project.sync()
|
|
248
292
|
if universe_updated and not project_updated:
|
|
249
293
|
project.build_db()
|
|
250
294
|
self.connect_db()
|
|
251
|
-
|
|
252
|
-
# Display state table after synchronization
|
|
253
|
-
table = self.table()
|
|
254
|
-
from rich.console import Console
|
|
255
|
-
console = Console()
|
|
256
|
-
console.print(table)
|
|
257
295
|
|
|
258
296
|
def table(self):
|
|
259
297
|
table = Table(show_header=False, show_lines=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: esgvoc
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.3
|
|
4
4
|
Summary: python library and CLI to interact with WCRP CVs
|
|
5
5
|
Project-URL: Repository, https://github.com/ESGF/esgf-vocab
|
|
6
6
|
Author-email: Sébastien Gardoll <sebastien@gardoll.fr>, Guillaume Levavasseur <guillaume.levavasseur@ipsl.fr>, Laurent Troussellier <laurent.troussellier@ipsl.fr>
|