esgvoc 1.1.1__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.

@@ -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
- # 🔹 Centralized default project configurations
27
- DEFAULT_PROJECT_CONFIGS: ClassVar[Dict[str, dict]] = {
28
- "cmip6": {
29
- "project_name": "cmip6",
30
- "github_repo": "https://github.com/WCRP-CMIP/CMIP6_CVs",
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
- # 🔹 Default settings - only includes cmip6 and cmip6plus by default
73
- DEFAULT_SETTINGS: ClassVar[dict] = {
74
- "universe": {
75
- "github_repo": "https://github.com/WCRP-CMIP/WCRP-universe",
76
- "branch": "esgvoc",
77
- "local_path": "repos/WCRP-universe",
78
- "db_path": "dbs/universe.sqlite",
79
- },
80
- "projects": [
81
- DEFAULT_PROJECT_CONFIGS["cmip6"],
82
- DEFAULT_PROJECT_CONFIGS["cmip6plus"],
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.DEFAULT_SETTINGS.copy() # Use defaults if the file is missing
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.DEFAULT_SETTINGS)
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
- if project_name not in self.DEFAULT_PROJECT_CONFIGS:
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(self.DEFAULT_PROJECT_CONFIGS.keys())}"
230
+ f"Unknown project '{project_name}'. Available defaults: {list(default_configs.keys())}"
142
231
  )
143
232
 
144
- config = self.DEFAULT_PROJECT_CONFIGS[project_name].copy()
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.DEFAULT_PROJECT_CONFIGS.keys())
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
- print(f"Default projects: {list(settings.projects.keys())}") # ['cmip6', 'cmip6plus']
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
- print(f"Available default projects: {available}") # ['cmip6', 'cmip6plus', 'input4mip', 'obs4mip']
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
- visited = set(self.data.uri) # Track visited URIs to prevent cycles
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:
@@ -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(): # IT WORKS
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(): #IT ALSO WORKS
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
-
@@ -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 (ProjectSettings,
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.github_access: bool = True # False if we dont have internet and some other cases
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 = self._get_absolute_path(str(self.base_dir), local_path)
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 = self._get_absolute_path(str(self.base_dir), db_path)
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.lstrip("https://github.com/").split("/")
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
- owner, repo = self.github_repo.lstrip("https://github.com/").split("/")
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
- self.clone_remote()
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
- super().__init__(**settings.model_dump())
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.values():
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.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>
@@ -11,6 +11,7 @@ Requires-Dist: jinja2>=3.1.6
11
11
  Requires-Dist: platformdirs>=4.3.6
12
12
  Requires-Dist: pydantic>=2.9.2
13
13
  Requires-Dist: pyld>=2.0.4
14
+ Requires-Dist: pyyaml>=6.0.2
14
15
  Requires-Dist: requests>=2.32.3
15
16
  Requires-Dist: sqlalchemy>=2.0.36
16
17
  Requires-Dist: sqlmodel>=0.0.22