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.
- esgvoc/__init__.py +3 -0
- esgvoc/api/__init__.py +91 -0
- esgvoc/api/data_descriptors/EMD_models/__init__.py +66 -0
- esgvoc/api/data_descriptors/EMD_models/arrangement.py +21 -0
- esgvoc/api/data_descriptors/EMD_models/calendar.py +5 -0
- esgvoc/api/data_descriptors/EMD_models/cell_variable_type.py +20 -0
- esgvoc/api/data_descriptors/EMD_models/component_type.py +5 -0
- esgvoc/api/data_descriptors/EMD_models/coordinate.py +52 -0
- esgvoc/api/data_descriptors/EMD_models/grid_mapping.py +19 -0
- esgvoc/api/data_descriptors/EMD_models/grid_region.py +19 -0
- esgvoc/api/data_descriptors/EMD_models/grid_type.py +19 -0
- esgvoc/api/data_descriptors/EMD_models/horizontal_computational_grid.py +56 -0
- esgvoc/api/data_descriptors/EMD_models/horizontal_grid_cells.py +230 -0
- esgvoc/api/data_descriptors/EMD_models/horizontal_subgrid.py +41 -0
- esgvoc/api/data_descriptors/EMD_models/horizontal_units.py +5 -0
- esgvoc/api/data_descriptors/EMD_models/model.py +139 -0
- esgvoc/api/data_descriptors/EMD_models/model_component.py +115 -0
- esgvoc/api/data_descriptors/EMD_models/reference.py +61 -0
- esgvoc/api/data_descriptors/EMD_models/resolution.py +48 -0
- esgvoc/api/data_descriptors/EMD_models/temporal_refinement.py +19 -0
- esgvoc/api/data_descriptors/EMD_models/truncation_method.py +17 -0
- esgvoc/api/data_descriptors/EMD_models/vertical_computational_grid.py +91 -0
- esgvoc/api/data_descriptors/EMD_models/vertical_coordinate.py +5 -0
- esgvoc/api/data_descriptors/EMD_models/vertical_units.py +19 -0
- esgvoc/api/data_descriptors/__init__.py +159 -0
- esgvoc/api/data_descriptors/activity.py +72 -0
- esgvoc/api/data_descriptors/archive.py +5 -0
- esgvoc/api/data_descriptors/area_label.py +30 -0
- esgvoc/api/data_descriptors/branded_suffix.py +30 -0
- esgvoc/api/data_descriptors/branded_variable.py +21 -0
- esgvoc/api/data_descriptors/citation_url.py +5 -0
- esgvoc/api/data_descriptors/contact.py +5 -0
- esgvoc/api/data_descriptors/conventions.py +28 -0
- esgvoc/api/data_descriptors/creation_date.py +18 -0
- esgvoc/api/data_descriptors/data_descriptor.py +127 -0
- esgvoc/api/data_descriptors/data_specs_version.py +25 -0
- esgvoc/api/data_descriptors/date.py +5 -0
- esgvoc/api/data_descriptors/directory_date.py +22 -0
- esgvoc/api/data_descriptors/drs_specs.py +38 -0
- esgvoc/api/data_descriptors/experiment.py +215 -0
- esgvoc/api/data_descriptors/forcing_index.py +21 -0
- esgvoc/api/data_descriptors/frequency.py +48 -0
- esgvoc/api/data_descriptors/further_info_url.py +5 -0
- esgvoc/api/data_descriptors/grid.py +43 -0
- esgvoc/api/data_descriptors/horizontal_label.py +20 -0
- esgvoc/api/data_descriptors/initialization_index.py +27 -0
- esgvoc/api/data_descriptors/institution.py +80 -0
- esgvoc/api/data_descriptors/known_branded_variable.py +75 -0
- esgvoc/api/data_descriptors/license.py +31 -0
- esgvoc/api/data_descriptors/member_id.py +9 -0
- esgvoc/api/data_descriptors/mip_era.py +26 -0
- esgvoc/api/data_descriptors/model_component.py +32 -0
- esgvoc/api/data_descriptors/models_test/models.py +17 -0
- esgvoc/api/data_descriptors/nominal_resolution.py +50 -0
- esgvoc/api/data_descriptors/obs_type.py +5 -0
- esgvoc/api/data_descriptors/organisation.py +22 -0
- esgvoc/api/data_descriptors/physics_index.py +21 -0
- esgvoc/api/data_descriptors/product.py +16 -0
- esgvoc/api/data_descriptors/publication_status.py +5 -0
- esgvoc/api/data_descriptors/realization_index.py +24 -0
- esgvoc/api/data_descriptors/realm.py +16 -0
- esgvoc/api/data_descriptors/regex.py +5 -0
- esgvoc/api/data_descriptors/region.py +35 -0
- esgvoc/api/data_descriptors/resolution.py +7 -0
- esgvoc/api/data_descriptors/source.py +120 -0
- esgvoc/api/data_descriptors/source_type.py +5 -0
- esgvoc/api/data_descriptors/sub_experiment.py +5 -0
- esgvoc/api/data_descriptors/table.py +28 -0
- esgvoc/api/data_descriptors/temporal_label.py +20 -0
- esgvoc/api/data_descriptors/time_range.py +17 -0
- esgvoc/api/data_descriptors/title.py +5 -0
- esgvoc/api/data_descriptors/tracking_id.py +67 -0
- esgvoc/api/data_descriptors/variable.py +56 -0
- esgvoc/api/data_descriptors/variant_label.py +25 -0
- esgvoc/api/data_descriptors/vertical_label.py +20 -0
- esgvoc/api/project_specs.py +143 -0
- esgvoc/api/projects.py +1253 -0
- esgvoc/api/py.typed +0 -0
- esgvoc/api/pydantic_handler.py +146 -0
- esgvoc/api/report.py +127 -0
- esgvoc/api/search.py +171 -0
- esgvoc/api/universe.py +434 -0
- esgvoc/apps/__init__.py +6 -0
- esgvoc/apps/cmor_tables/__init__.py +7 -0
- esgvoc/apps/cmor_tables/cvs_table.py +948 -0
- esgvoc/apps/drs/__init__.py +0 -0
- esgvoc/apps/drs/constants.py +2 -0
- esgvoc/apps/drs/generator.py +429 -0
- esgvoc/apps/drs/report.py +540 -0
- esgvoc/apps/drs/validator.py +312 -0
- esgvoc/apps/ga/__init__.py +104 -0
- esgvoc/apps/ga/example_usage.py +315 -0
- esgvoc/apps/ga/models/__init__.py +47 -0
- esgvoc/apps/ga/models/netcdf_header.py +306 -0
- esgvoc/apps/ga/models/validator.py +491 -0
- esgvoc/apps/ga/test_ga.py +161 -0
- esgvoc/apps/ga/validator.py +277 -0
- esgvoc/apps/jsg/json_schema_generator.py +341 -0
- esgvoc/apps/jsg/templates/template.jinja +241 -0
- esgvoc/apps/test_cv/README.md +214 -0
- esgvoc/apps/test_cv/__init__.py +0 -0
- esgvoc/apps/test_cv/cv_tester.py +1611 -0
- esgvoc/apps/test_cv/example_usage.py +216 -0
- esgvoc/apps/vr/__init__.py +12 -0
- esgvoc/apps/vr/build_variable_registry.py +71 -0
- esgvoc/apps/vr/example_usage.py +60 -0
- esgvoc/apps/vr/vr_app.py +333 -0
- esgvoc/cli/clean.py +304 -0
- esgvoc/cli/cmor.py +46 -0
- esgvoc/cli/config.py +1300 -0
- esgvoc/cli/drs.py +267 -0
- esgvoc/cli/find.py +138 -0
- esgvoc/cli/get.py +155 -0
- esgvoc/cli/install.py +41 -0
- esgvoc/cli/main.py +60 -0
- esgvoc/cli/offline.py +269 -0
- esgvoc/cli/status.py +79 -0
- esgvoc/cli/test_cv.py +258 -0
- esgvoc/cli/valid.py +147 -0
- esgvoc/core/constants.py +17 -0
- esgvoc/core/convert.py +0 -0
- esgvoc/core/data_handler.py +206 -0
- esgvoc/core/db/__init__.py +3 -0
- esgvoc/core/db/connection.py +40 -0
- esgvoc/core/db/models/mixins.py +25 -0
- esgvoc/core/db/models/project.py +102 -0
- esgvoc/core/db/models/universe.py +98 -0
- esgvoc/core/db/project_ingestion.py +231 -0
- esgvoc/core/db/universe_ingestion.py +172 -0
- esgvoc/core/exceptions.py +33 -0
- esgvoc/core/logging_handler.py +26 -0
- esgvoc/core/repo_fetcher.py +345 -0
- esgvoc/core/service/__init__.py +41 -0
- esgvoc/core/service/configuration/config_manager.py +196 -0
- esgvoc/core/service/configuration/setting.py +363 -0
- esgvoc/core/service/data_merger.py +634 -0
- esgvoc/core/service/esg_voc.py +77 -0
- esgvoc/core/service/resolver_config.py +56 -0
- esgvoc/core/service/state.py +324 -0
- esgvoc/core/service/string_heuristics.py +98 -0
- esgvoc/core/service/term_cache.py +108 -0
- esgvoc/core/service/uri_resolver.py +133 -0
- esgvoc-2.0.2.dist-info/METADATA +82 -0
- esgvoc-2.0.2.dist-info/RECORD +147 -0
- esgvoc-2.0.2.dist-info/WHEEL +4 -0
- esgvoc-2.0.2.dist-info/entry_points.txt +2 -0
- esgvoc-2.0.2.dist-info/licenses/LICENSE.txt +519 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from pydantic import BaseModel, ValidationError
|
|
11
|
+
|
|
12
|
+
_LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@contextmanager
|
|
16
|
+
def redirect_stdout_to_log(level=logging.INFO):
|
|
17
|
+
"""
|
|
18
|
+
Redirect stdout to the global _LOGGER temporarily.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
class StreamToLogger:
|
|
22
|
+
def __init__(self, log_level):
|
|
23
|
+
self.log_level = log_level
|
|
24
|
+
|
|
25
|
+
def write(self, message):
|
|
26
|
+
if message.strip(): # Avoid logging empty lines
|
|
27
|
+
_LOGGER.debug(self.log_level, message.strip())
|
|
28
|
+
|
|
29
|
+
def flush(self):
|
|
30
|
+
pass # No-op for compatibility
|
|
31
|
+
|
|
32
|
+
old_stdout = sys.stdout
|
|
33
|
+
old_stderr = sys.stderr
|
|
34
|
+
sys.stdout = StreamToLogger(level)
|
|
35
|
+
sys.stderr = StreamToLogger(level)
|
|
36
|
+
try:
|
|
37
|
+
yield
|
|
38
|
+
finally:
|
|
39
|
+
sys.stdout = old_stdout
|
|
40
|
+
sys.stderr = old_stderr
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class GitHubRepository(BaseModel):
|
|
44
|
+
id: int
|
|
45
|
+
name: str
|
|
46
|
+
full_name: str
|
|
47
|
+
description: Optional[str]
|
|
48
|
+
html_url: str
|
|
49
|
+
stargazers_count: int
|
|
50
|
+
forks_count: int
|
|
51
|
+
language: Optional[str]
|
|
52
|
+
created_at: str
|
|
53
|
+
updated_at: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class GitHubBranch(BaseModel):
|
|
57
|
+
name: str
|
|
58
|
+
commit: dict
|
|
59
|
+
protected: bool
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class RepoFetcher:
|
|
63
|
+
"""
|
|
64
|
+
DataFetcher is responsible for fetching data from external sources such as GitHub.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, base_url: str = "https://api.github.com", local_path: str = ".cache/repos", offline_mode: bool = False):
|
|
68
|
+
self.base_url = base_url
|
|
69
|
+
self.repo_dir = local_path
|
|
70
|
+
self.offline_mode = offline_mode
|
|
71
|
+
|
|
72
|
+
def fetch_repositories(self, user: str) -> List[GitHubRepository]:
|
|
73
|
+
"""
|
|
74
|
+
Fetch repositories of a given GitHub user.
|
|
75
|
+
:param user: GitHub username
|
|
76
|
+
:return: List of GitHubRepository objects
|
|
77
|
+
"""
|
|
78
|
+
if self.offline_mode:
|
|
79
|
+
raise Exception("Cannot fetch repositories in offline mode")
|
|
80
|
+
|
|
81
|
+
url = f"{self.base_url}/users/{user}/repos"
|
|
82
|
+
response = requests.get(url)
|
|
83
|
+
|
|
84
|
+
if response.status_code != 200:
|
|
85
|
+
raise Exception(f"Failed to fetch data: {response.status_code} - {response.text}")
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
data = response.json()
|
|
89
|
+
return [GitHubRepository(**repo) for repo in data]
|
|
90
|
+
except ValidationError as e:
|
|
91
|
+
raise Exception(f"Data validation error: {e}")
|
|
92
|
+
|
|
93
|
+
def fetch_repository_details(self, owner: str, repo: str) -> GitHubRepository:
|
|
94
|
+
"""
|
|
95
|
+
Fetch details of a specific repository.
|
|
96
|
+
:param owner: Repository owner
|
|
97
|
+
:param repo: Repository name
|
|
98
|
+
:return: GitHubRepository object
|
|
99
|
+
"""
|
|
100
|
+
if self.offline_mode:
|
|
101
|
+
raise Exception("Cannot fetch repository details in offline mode")
|
|
102
|
+
|
|
103
|
+
url = f"{self.base_url}/repos/{owner}/{repo}"
|
|
104
|
+
response = requests.get(url)
|
|
105
|
+
|
|
106
|
+
if response.status_code != 200:
|
|
107
|
+
raise Exception(f"Failed to fetch data: {response.status_code} - {response.text}")
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
data = response.json()
|
|
111
|
+
return GitHubRepository(**data)
|
|
112
|
+
except ValidationError as e:
|
|
113
|
+
raise Exception(f"Data validation error: {e}")
|
|
114
|
+
|
|
115
|
+
def fetch_branch_details(self, owner: str, repo: str, branch: str) -> GitHubBranch:
|
|
116
|
+
"""
|
|
117
|
+
Fetch details of a specific branch in a repository.
|
|
118
|
+
:param owner: Repository owner
|
|
119
|
+
:param repo: Repository name
|
|
120
|
+
:param branch: Branch name
|
|
121
|
+
:return: GitHubBranch object
|
|
122
|
+
"""
|
|
123
|
+
if self.offline_mode:
|
|
124
|
+
raise Exception("Cannot fetch branch details in offline mode")
|
|
125
|
+
|
|
126
|
+
url = f"{self.base_url}/repos/{owner}/{repo}/branches/{branch}"
|
|
127
|
+
response = requests.get(url)
|
|
128
|
+
|
|
129
|
+
if response.status_code != 200:
|
|
130
|
+
raise Exception(f"Failed to fetch branch data: {response.status_code} - {response.text}")
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
return GitHubBranch(**response.json())
|
|
134
|
+
except ValidationError as e:
|
|
135
|
+
raise Exception(f"Data validation error: {e}")
|
|
136
|
+
|
|
137
|
+
def list_directory(self, owner, repo, branch="main"):
|
|
138
|
+
"""
|
|
139
|
+
List directories in the root of a GitHub repository.
|
|
140
|
+
|
|
141
|
+
:param owner: GitHub username or organization name.
|
|
142
|
+
:param repo: Repository name.
|
|
143
|
+
:param branch: Branch name (default: 'main').
|
|
144
|
+
:return: List of directories in the repository.
|
|
145
|
+
"""
|
|
146
|
+
if self.offline_mode:
|
|
147
|
+
raise Exception("Cannot list directories in offline mode")
|
|
148
|
+
|
|
149
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/contents/?ref={branch}"
|
|
150
|
+
response = requests.get(url)
|
|
151
|
+
response.raise_for_status() # Raise an error for bad responses
|
|
152
|
+
contents = response.json()
|
|
153
|
+
directories = [item["name"] for item in contents if item["type"] == "dir"]
|
|
154
|
+
return directories
|
|
155
|
+
|
|
156
|
+
def list_files(self, owner, repo, directory, branch="main"):
|
|
157
|
+
"""
|
|
158
|
+
List files in a specific directory of a GitHub repository.
|
|
159
|
+
|
|
160
|
+
:param owner: GitHub username or organization name.
|
|
161
|
+
:param repo: Repository name.
|
|
162
|
+
:param directory: Target directory path within the repo.
|
|
163
|
+
:param branch: Branch name (default: 'main').
|
|
164
|
+
:return: List of files in the specified directory.
|
|
165
|
+
"""
|
|
166
|
+
if self.offline_mode:
|
|
167
|
+
raise Exception("Cannot list files in offline mode")
|
|
168
|
+
|
|
169
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/contents/{directory}?ref={branch}"
|
|
170
|
+
response = requests.get(url)
|
|
171
|
+
response.raise_for_status() # Raise an error for bad responses
|
|
172
|
+
contents = response.json()
|
|
173
|
+
files = [item["name"] for item in contents if item["type"] == "file"]
|
|
174
|
+
return files
|
|
175
|
+
|
|
176
|
+
def clone_repository(self, owner: str, repo: str, branch: Optional[str] = None, local_path: str | None = None, shallow: bool = True):
|
|
177
|
+
"""
|
|
178
|
+
Clone a GitHub repository to a target directory.
|
|
179
|
+
:param owner: Repository owner
|
|
180
|
+
:param repo: Repository name
|
|
181
|
+
:param target_dir: The directory where the repository should be cloned.
|
|
182
|
+
:param branch: (Optional) The branch to clone. Clones the default branch if None.
|
|
183
|
+
:param shallow: (Optional) If True, performs a shallow clone with --depth 1. Default is True.
|
|
184
|
+
"""
|
|
185
|
+
if self.offline_mode:
|
|
186
|
+
raise Exception("Cannot clone repository in offline mode")
|
|
187
|
+
|
|
188
|
+
repo_url = f"https://github.com/{owner}/{repo}.git"
|
|
189
|
+
destination = local_path if local_path else f"{self.repo_dir}/{repo}"
|
|
190
|
+
|
|
191
|
+
command = ["git", "clone", repo_url, destination]
|
|
192
|
+
if shallow:
|
|
193
|
+
command.extend(["--depth", "1"])
|
|
194
|
+
if branch:
|
|
195
|
+
command.extend(["--branch", branch])
|
|
196
|
+
with redirect_stdout_to_log():
|
|
197
|
+
try:
|
|
198
|
+
if not Path(destination).exists():
|
|
199
|
+
subprocess.run(command, check=True)
|
|
200
|
+
_LOGGER.debug(f"Repository cloned successfully into {destination}")
|
|
201
|
+
else:
|
|
202
|
+
current_work_dir = os.getcwd()
|
|
203
|
+
os.chdir(f"{destination}")
|
|
204
|
+
|
|
205
|
+
# Clean up any conflicted state first
|
|
206
|
+
try:
|
|
207
|
+
subprocess.run(["git", "reset", "--hard"], capture_output=True, check=False)
|
|
208
|
+
subprocess.run(["git", "clean", "-fd"], capture_output=True, check=False)
|
|
209
|
+
except Exception:
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
# Check if the requested branch exists locally
|
|
213
|
+
try:
|
|
214
|
+
result = subprocess.run(
|
|
215
|
+
["git", "rev-parse", "--verify", f"refs/heads/{branch}"],
|
|
216
|
+
capture_output=True,
|
|
217
|
+
check=False
|
|
218
|
+
)
|
|
219
|
+
branch_exists_locally = result.returncode == 0
|
|
220
|
+
except Exception:
|
|
221
|
+
branch_exists_locally = False
|
|
222
|
+
|
|
223
|
+
if not branch_exists_locally and branch:
|
|
224
|
+
# If branch doesn't exist locally, we need to fetch it
|
|
225
|
+
# For shallow repos, we need to unshallow first or fetch the specific branch
|
|
226
|
+
try:
|
|
227
|
+
# Try to fetch the specific branch
|
|
228
|
+
subprocess.run(["git", "fetch", "origin", f"{branch}:{branch}"], check=True)
|
|
229
|
+
_LOGGER.debug(f"Fetched new branch {branch}")
|
|
230
|
+
except subprocess.CalledProcessError:
|
|
231
|
+
# If that fails, unshallow and try again
|
|
232
|
+
subprocess.run(["git", "fetch", "--unshallow"], check=True)
|
|
233
|
+
subprocess.run(["git", "fetch", "origin", f"{branch}:{branch}"], check=True)
|
|
234
|
+
_LOGGER.debug(f"Unshallowed repo and fetched branch {branch}")
|
|
235
|
+
|
|
236
|
+
# Switch to the requested branch if specified
|
|
237
|
+
if branch:
|
|
238
|
+
subprocess.run(["git", "checkout", branch], check=True)
|
|
239
|
+
_LOGGER.debug(f"Switched to branch {branch}")
|
|
240
|
+
|
|
241
|
+
# For shallow repos that switched branches, just ensure we have latest
|
|
242
|
+
# to avoid merge conflicts from different commit histories
|
|
243
|
+
if branch and not branch_exists_locally:
|
|
244
|
+
# We already fetched the branch, no need for additional reset
|
|
245
|
+
_LOGGER.debug(f"Switched to newly fetched branch {branch}")
|
|
246
|
+
else:
|
|
247
|
+
# Pull latest changes for normal updates
|
|
248
|
+
try:
|
|
249
|
+
subprocess.run(["git", "pull"], check=True)
|
|
250
|
+
except subprocess.CalledProcessError:
|
|
251
|
+
# If pull fails, try to fetch and reset
|
|
252
|
+
subprocess.run(["git", "fetch"], check=True)
|
|
253
|
+
# Check if remote tracking branch exists
|
|
254
|
+
try:
|
|
255
|
+
subprocess.run(["git", "rev-parse", f"origin/{branch}"], capture_output=True, check=True)
|
|
256
|
+
subprocess.run(["git", "reset", "--hard", f"origin/{branch}"], check=True)
|
|
257
|
+
_LOGGER.debug(f"Reset to origin/{branch} after pull failure")
|
|
258
|
+
except subprocess.CalledProcessError:
|
|
259
|
+
# Remote tracking branch doesn't exist, just continue
|
|
260
|
+
_LOGGER.debug(f"No remote tracking branch for {branch}, continuing")
|
|
261
|
+
os.chdir(current_work_dir)
|
|
262
|
+
|
|
263
|
+
except Exception as e:
|
|
264
|
+
raise Exception(f"Failed to clone repository: {e}")
|
|
265
|
+
|
|
266
|
+
def get_github_version_with_api(self, owner: str, repo: str, branch: str = "main"):
|
|
267
|
+
"""Fetch the latest commit version (or any other versioning scheme) from GitHub."""
|
|
268
|
+
if self.offline_mode:
|
|
269
|
+
raise Exception("Cannot get GitHub version in offline mode")
|
|
270
|
+
details = self.fetch_branch_details(owner, repo, branch)
|
|
271
|
+
return details.commit.get("sha")
|
|
272
|
+
|
|
273
|
+
def get_github_version(self, owner: str, repo: str, branch: str = "main"):
|
|
274
|
+
"""Fetch the latest commit version (or any other versioning scheme) from GitHub. with command git fetch"""
|
|
275
|
+
if self.offline_mode:
|
|
276
|
+
_LOGGER.debug("Cannot get GitHub version in offline mode")
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
repo_url = f"https://github.com/{owner}/{repo}.git"
|
|
280
|
+
command = ["git", "ls-remote", repo_url, f"{self.repo_dir}/{repo}"]
|
|
281
|
+
if branch:
|
|
282
|
+
command.extend([branch])
|
|
283
|
+
|
|
284
|
+
# with redirect_stdout_to_log():
|
|
285
|
+
output = None
|
|
286
|
+
try:
|
|
287
|
+
result = subprocess.run(command, capture_output=True, text=True, check=True)
|
|
288
|
+
# Parse the output to get the commit hash
|
|
289
|
+
output = result.stdout.strip()
|
|
290
|
+
_LOGGER.debug(f"Repository fetch successfully from {self.repo_dir}/{repo}")
|
|
291
|
+
except Exception as e:
|
|
292
|
+
_LOGGER.debug("error in with git fetch " + repr(e))
|
|
293
|
+
if output is not None:
|
|
294
|
+
commit_hash = output.split()[0]
|
|
295
|
+
return commit_hash
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
# return git_hash
|
|
299
|
+
|
|
300
|
+
def get_local_repo_version(self, repo_path: str, branch: Optional[str] = "main"):
|
|
301
|
+
"""Check the version of the local repository by fetching the latest commit hash."""
|
|
302
|
+
# repo_path = os.path.join(self.repo_dir, repo)
|
|
303
|
+
if os.path.exists(repo_path):
|
|
304
|
+
# print("EXIST")
|
|
305
|
+
command = ["git", "-C", repo_path]
|
|
306
|
+
if branch:
|
|
307
|
+
command.extend(["switch", branch])
|
|
308
|
+
# Ensure we are on the correct branch
|
|
309
|
+
with redirect_stdout_to_log():
|
|
310
|
+
subprocess.run(
|
|
311
|
+
command,
|
|
312
|
+
stdout=subprocess.PIPE, # Capture stdout
|
|
313
|
+
stderr=subprocess.PIPE, # Capture stderr
|
|
314
|
+
text=True,
|
|
315
|
+
) # Decode output as text
|
|
316
|
+
# Get the latest commit hash (SHA) from the local repository
|
|
317
|
+
commit_hash = subprocess.check_output(
|
|
318
|
+
["git", "-C", repo_path, "rev-parse", "HEAD"], stderr=subprocess.PIPE, text=True
|
|
319
|
+
).strip()
|
|
320
|
+
return commit_hash
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
if __name__ == "__main__":
|
|
325
|
+
fetcher = RepoFetcher()
|
|
326
|
+
|
|
327
|
+
# Fetch repositories for a user
|
|
328
|
+
# repos = fetcher.fetch_repositories("ESPRI-Mod")
|
|
329
|
+
# for repo in repos:
|
|
330
|
+
# print(repo)
|
|
331
|
+
|
|
332
|
+
# Fetch a specific repository's details
|
|
333
|
+
# repo_details = fetcher.fetch_repository_details("ESPRI-Mod", "mip-cmor-tables")
|
|
334
|
+
# "print(repo_details)
|
|
335
|
+
# branch_details = fetcher.fetch_branch_details("ESPRI-Mod", "mip-cmor-tables", "uni_proj_ld")
|
|
336
|
+
# print(branch_details)
|
|
337
|
+
|
|
338
|
+
fetcher.clone_repository("ESPRI-Mod", "mip-cmor-tables", branch="uni_proj_ld")
|
|
339
|
+
|
|
340
|
+
# a =fetcher.get_github_version("ESPRI-Mod", "mip-cmor-tables", "uni_proj_ld")
|
|
341
|
+
# print(a)
|
|
342
|
+
# a = fetcher.get_local_repo_version("mip-cmor-tables","uni_proj_ld")
|
|
343
|
+
# print(a)
|
|
344
|
+
|
|
345
|
+
fetcher.clone_repository("ESPRI-Mod", "CMIP6Plus_CVs", branch="uni_proj_ld")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# from esgvoc.core.service.config_register import ConfigManager
|
|
2
|
+
# from esgvoc.core.service.settings import ServiceSettings
|
|
3
|
+
# from esgvoc.core.service.state import StateService
|
|
4
|
+
#
|
|
5
|
+
# config_manager = ConfigManager()
|
|
6
|
+
# active_setting = config_manager.get_active_config()
|
|
7
|
+
# active_setting["base_dir"] = str(config_manager.config_dir / config_manager.get_active_config_name())
|
|
8
|
+
# service_settings = ServiceSettings.from_config(active_setting)
|
|
9
|
+
# state_service = StateService(service_settings)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
from esgvoc.core.service.configuration.config_manager import ConfigManager
|
|
13
|
+
from esgvoc.core.service.configuration.setting import ServiceSettings
|
|
14
|
+
from esgvoc.core.service.state import StateService
|
|
15
|
+
|
|
16
|
+
config_manager : ConfigManager | None = None
|
|
17
|
+
current_state : StateService | None = None
|
|
18
|
+
|
|
19
|
+
def get_config_manager():
|
|
20
|
+
global config_manager
|
|
21
|
+
if config_manager is None:
|
|
22
|
+
|
|
23
|
+
config_manager = ConfigManager(ServiceSettings, app_name="esgvoc", app_author="ipsl", default_settings=ServiceSettings._get_default_settings())
|
|
24
|
+
active_config_name= config_manager.get_active_config_name()
|
|
25
|
+
config_manager.data_config_dir = config_manager.data_dir / active_config_name
|
|
26
|
+
config_manager.data_config_dir.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
|
|
28
|
+
return config_manager
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_state():
|
|
32
|
+
global current_state
|
|
33
|
+
if config_manager is not None:
|
|
34
|
+
service_settings = config_manager.get_active_config()
|
|
35
|
+
current_state = StateService(service_settings)
|
|
36
|
+
return current_state
|
|
37
|
+
|
|
38
|
+
# Singleton Access Function
|
|
39
|
+
config_manager = get_config_manager()
|
|
40
|
+
current_state = get_state()
|
|
41
|
+
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import toml
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from platformdirs import PlatformDirs
|
|
5
|
+
from typing import Type, TypeVar, Generic, Protocol
|
|
6
|
+
|
|
7
|
+
# Setup logging
|
|
8
|
+
# Use WARNING level to see important messages (errors, warnings) but not debug/info spam
|
|
9
|
+
logging.basicConfig(
|
|
10
|
+
level=logging.WARNING,
|
|
11
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
12
|
+
)
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Explicitly set data_merger logger to WARNING since something else seems to change it to ERROR
|
|
16
|
+
logging.getLogger("esgvoc.core.service.data_merger").setLevel(logging.WARNING)
|
|
17
|
+
|
|
18
|
+
# Define a generic type for configuration
|
|
19
|
+
T = TypeVar("T", bound="ConfigSchema")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConfigSchema(Protocol):
|
|
23
|
+
"""Protocol for application-specific configuration classes."""
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def load_from_file(cls, file_path: str): ...
|
|
27
|
+
|
|
28
|
+
def save_to_file(self, file_path: str): ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConfigManager(Generic[T]):
|
|
32
|
+
def __init__(self, config_cls: Type[T], app_name: str, app_author: str, default_settings: dict | None = None):
|
|
33
|
+
"""
|
|
34
|
+
Initialize the configuration manager.
|
|
35
|
+
- config_cls: A class that implements `ConfigSchema` (e.g., ServiceSettings).
|
|
36
|
+
- app_name: Name of the application (used for directory paths).
|
|
37
|
+
- app_author: Name of the author/organization (used for directory paths).
|
|
38
|
+
"""
|
|
39
|
+
self.config_cls = config_cls
|
|
40
|
+
self.dirs = PlatformDirs(app_name, app_author)
|
|
41
|
+
|
|
42
|
+
# Define standard paths
|
|
43
|
+
self.config_dir = Path(self.dirs.user_config_path).expanduser().resolve()
|
|
44
|
+
self.data_dir = Path(self.dirs.user_data_path).expanduser().resolve()
|
|
45
|
+
self.data_config_dir = None # depends on loaded settings
|
|
46
|
+
|
|
47
|
+
self.cache_dir = Path(self.dirs.user_cache_path).expanduser().resolve()
|
|
48
|
+
|
|
49
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
|
|
53
|
+
self.registry_path = self.config_dir / "config_registry.toml"
|
|
54
|
+
self.default_config_path = self.config_dir / "default_setting.toml"
|
|
55
|
+
self.default_settings = default_settings
|
|
56
|
+
self._init_registry()
|
|
57
|
+
|
|
58
|
+
def _init_registry(self):
|
|
59
|
+
"""Initialize the registry file if it doesn't exist."""
|
|
60
|
+
if not self.registry_path.exists():
|
|
61
|
+
logger.info("Initializing configuration registry...")
|
|
62
|
+
registry = {"configs": {"default": str(self.default_config_path)}, "active": "default"}
|
|
63
|
+
self._save_toml(self.registry_path, registry)
|
|
64
|
+
# Ensure the default settings file exists and save it if necessary
|
|
65
|
+
if not self.default_config_path.exists():
|
|
66
|
+
if self.default_settings:
|
|
67
|
+
logger.info("Saving default settings...")
|
|
68
|
+
self._save_toml(self.default_config_path, self.default_settings)
|
|
69
|
+
else:
|
|
70
|
+
logger.warning("No default settings provided.")
|
|
71
|
+
|
|
72
|
+
def _load_toml(self, path: Path) -> dict:
|
|
73
|
+
"""Load TOML data from a file."""
|
|
74
|
+
if not path.exists():
|
|
75
|
+
logger.error(f"Configuration file not found: {path}")
|
|
76
|
+
raise FileNotFoundError(f"Configuration file not found: {path}")
|
|
77
|
+
with open(path, "r") as f:
|
|
78
|
+
return toml.load(f)
|
|
79
|
+
|
|
80
|
+
def _save_toml(self, path: Path, data: dict) -> None:
|
|
81
|
+
"""Save TOML data to a file."""
|
|
82
|
+
with open(path, "w") as f:
|
|
83
|
+
toml.dump(data, f)
|
|
84
|
+
|
|
85
|
+
def _get_active_config_path(self) -> Path:
|
|
86
|
+
"""Retrieve the path of the active configuration file."""
|
|
87
|
+
registry = self._load_toml(self.registry_path)
|
|
88
|
+
active_config_name = registry["active"]
|
|
89
|
+
return Path(registry["configs"][active_config_name])
|
|
90
|
+
|
|
91
|
+
def get_config(self, config_name: str) -> T:
|
|
92
|
+
"""Load the configuration as an instance of the given config schema."""
|
|
93
|
+
registry = self._load_toml(self.registry_path)
|
|
94
|
+
if config_name not in registry["configs"]:
|
|
95
|
+
logger.error(f"Config '{config_name}' not found in registry.")
|
|
96
|
+
raise ValueError(f"Config '{config_name}' not found in registry.")
|
|
97
|
+
config_path = registry["configs"][config_name]
|
|
98
|
+
return self.config_cls.load_from_file(str(config_path))
|
|
99
|
+
|
|
100
|
+
def get_active_config(self) -> T:
|
|
101
|
+
"""Load the active configuration as an instance of the given config schema."""
|
|
102
|
+
active_config_path = self._get_active_config_path()
|
|
103
|
+
active_config_name = self.get_active_config_name()
|
|
104
|
+
|
|
105
|
+
settings = self.config_cls.load_from_file(str(active_config_path))
|
|
106
|
+
# Set the config name if the settings support it (duck typing)
|
|
107
|
+
if hasattr(settings, 'set_config_name'):
|
|
108
|
+
settings.set_config_name(active_config_name)
|
|
109
|
+
return settings
|
|
110
|
+
|
|
111
|
+
def get_active_config_name(self) -> str:
|
|
112
|
+
"""Retrieve the config name from the registry"""
|
|
113
|
+
registry = self._load_toml(self.registry_path)
|
|
114
|
+
return registry["active"]
|
|
115
|
+
|
|
116
|
+
def save_config(self, config_data: dict, name: str | None = None) -> None:
|
|
117
|
+
"""Save the modified configuration to the corresponding file and update the registry."""
|
|
118
|
+
|
|
119
|
+
if name:
|
|
120
|
+
# If a name is provided, save the configuration with that name
|
|
121
|
+
config_path = self.config_dir / f"{name}.toml"
|
|
122
|
+
self._save_toml(config_path, config_data)
|
|
123
|
+
|
|
124
|
+
# Update the registry with the new config name
|
|
125
|
+
registry = self._load_toml(self.registry_path)
|
|
126
|
+
registry["configs"][name] = str(config_path)
|
|
127
|
+
registry["active"] = name
|
|
128
|
+
self._save_toml(self.registry_path, registry)
|
|
129
|
+
|
|
130
|
+
logger.info(f"Saved configuration to {config_path} and updated registry.")
|
|
131
|
+
else:
|
|
132
|
+
# If no name is provided, give the user a default name, like "user_config"
|
|
133
|
+
default_name = "user_config"
|
|
134
|
+
config_path = self.config_dir / f"{default_name}.toml"
|
|
135
|
+
|
|
136
|
+
# Check if the user_config already exists, if so, warn them
|
|
137
|
+
if config_path.exists():
|
|
138
|
+
logger.warning(f"{default_name}.toml already exists. Overwriting with the new config.")
|
|
139
|
+
|
|
140
|
+
# Save the configuration with the default name
|
|
141
|
+
self._save_toml(config_path, config_data)
|
|
142
|
+
|
|
143
|
+
# Update the registry with the new config name
|
|
144
|
+
registry = self._load_toml(self.registry_path)
|
|
145
|
+
registry["configs"][default_name] = str(config_path)
|
|
146
|
+
registry["active"] = default_name
|
|
147
|
+
self._save_toml(self.registry_path, registry)
|
|
148
|
+
|
|
149
|
+
logger.info(f"Saved new configuration to {config_path} and updated registry.")
|
|
150
|
+
|
|
151
|
+
def save_active_config(self, config: T):
|
|
152
|
+
"""Save the current configuration to the active file."""
|
|
153
|
+
active_config_path = self._get_active_config_path()
|
|
154
|
+
config.save_to_file(str(active_config_path))
|
|
155
|
+
|
|
156
|
+
def switch_config(self, config_name: str):
|
|
157
|
+
"""Switch to a different configuration."""
|
|
158
|
+
registry = self._load_toml(self.registry_path)
|
|
159
|
+
if config_name not in registry["configs"]:
|
|
160
|
+
logger.error(f"Config '{config_name}' not found in registry.")
|
|
161
|
+
raise ValueError(f"Config '{config_name}' not found in registry.")
|
|
162
|
+
registry["active"] = config_name
|
|
163
|
+
|
|
164
|
+
self._save_toml(self.registry_path, registry)
|
|
165
|
+
logger.info(f"Switched to configuration: {config_name}")
|
|
166
|
+
|
|
167
|
+
def list_configs(self) -> dict:
|
|
168
|
+
"""Return a list of available configurations."""
|
|
169
|
+
return self._load_toml(self.registry_path)["configs"]
|
|
170
|
+
|
|
171
|
+
def add_config(self, config_name: str, config_data: dict):
|
|
172
|
+
"""Add a new configuration."""
|
|
173
|
+
registry = self._load_toml(self.registry_path)
|
|
174
|
+
if config_name in registry["configs"]:
|
|
175
|
+
raise ValueError(f"Config '{config_name}' already exists.")
|
|
176
|
+
config_path = self.config_dir / f"{config_name}.toml"
|
|
177
|
+
self._save_toml(config_path, config_data)
|
|
178
|
+
registry["configs"][config_name] = str(config_path)
|
|
179
|
+
self._save_toml(self.registry_path, registry)
|
|
180
|
+
|
|
181
|
+
def remove_config(self, config_name: str):
|
|
182
|
+
"""Remove a configuration."""
|
|
183
|
+
registry = self._load_toml(self.registry_path)
|
|
184
|
+
if config_name == "default":
|
|
185
|
+
raise ValueError("Cannot remove the default configuration.")
|
|
186
|
+
if config_name not in registry["configs"]:
|
|
187
|
+
raise ValueError(f"Config '{config_name}' not found.")
|
|
188
|
+
del registry["configs"][config_name]
|
|
189
|
+
config_path = self.config_dir / f"{config_name}.toml"
|
|
190
|
+
config_path.unlink()
|
|
191
|
+
|
|
192
|
+
self._save_toml(self.registry_path, registry)
|
|
193
|
+
logger.info(f"Removed configuration: {config_name}")
|
|
194
|
+
if registry["active"] not in registry["configs"]:
|
|
195
|
+
self.switch_config("default")
|
|
196
|
+
logger.info("active configuration doesnot exist anymore : Switch to default configuration")
|