esgvoc 2.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. esgvoc/__init__.py +3 -0
  2. esgvoc/api/__init__.py +91 -0
  3. esgvoc/api/data_descriptors/EMD_models/__init__.py +66 -0
  4. esgvoc/api/data_descriptors/EMD_models/arrangement.py +21 -0
  5. esgvoc/api/data_descriptors/EMD_models/calendar.py +5 -0
  6. esgvoc/api/data_descriptors/EMD_models/cell_variable_type.py +20 -0
  7. esgvoc/api/data_descriptors/EMD_models/component_type.py +5 -0
  8. esgvoc/api/data_descriptors/EMD_models/coordinate.py +52 -0
  9. esgvoc/api/data_descriptors/EMD_models/grid_mapping.py +19 -0
  10. esgvoc/api/data_descriptors/EMD_models/grid_region.py +19 -0
  11. esgvoc/api/data_descriptors/EMD_models/grid_type.py +19 -0
  12. esgvoc/api/data_descriptors/EMD_models/horizontal_computational_grid.py +56 -0
  13. esgvoc/api/data_descriptors/EMD_models/horizontal_grid_cells.py +230 -0
  14. esgvoc/api/data_descriptors/EMD_models/horizontal_subgrid.py +41 -0
  15. esgvoc/api/data_descriptors/EMD_models/horizontal_units.py +5 -0
  16. esgvoc/api/data_descriptors/EMD_models/model.py +139 -0
  17. esgvoc/api/data_descriptors/EMD_models/model_component.py +115 -0
  18. esgvoc/api/data_descriptors/EMD_models/reference.py +61 -0
  19. esgvoc/api/data_descriptors/EMD_models/resolution.py +48 -0
  20. esgvoc/api/data_descriptors/EMD_models/temporal_refinement.py +19 -0
  21. esgvoc/api/data_descriptors/EMD_models/truncation_method.py +17 -0
  22. esgvoc/api/data_descriptors/EMD_models/vertical_computational_grid.py +91 -0
  23. esgvoc/api/data_descriptors/EMD_models/vertical_coordinate.py +5 -0
  24. esgvoc/api/data_descriptors/EMD_models/vertical_units.py +19 -0
  25. esgvoc/api/data_descriptors/__init__.py +159 -0
  26. esgvoc/api/data_descriptors/activity.py +72 -0
  27. esgvoc/api/data_descriptors/archive.py +5 -0
  28. esgvoc/api/data_descriptors/area_label.py +30 -0
  29. esgvoc/api/data_descriptors/branded_suffix.py +30 -0
  30. esgvoc/api/data_descriptors/branded_variable.py +21 -0
  31. esgvoc/api/data_descriptors/citation_url.py +5 -0
  32. esgvoc/api/data_descriptors/contact.py +5 -0
  33. esgvoc/api/data_descriptors/conventions.py +28 -0
  34. esgvoc/api/data_descriptors/creation_date.py +18 -0
  35. esgvoc/api/data_descriptors/data_descriptor.py +127 -0
  36. esgvoc/api/data_descriptors/data_specs_version.py +25 -0
  37. esgvoc/api/data_descriptors/date.py +5 -0
  38. esgvoc/api/data_descriptors/directory_date.py +22 -0
  39. esgvoc/api/data_descriptors/drs_specs.py +38 -0
  40. esgvoc/api/data_descriptors/experiment.py +215 -0
  41. esgvoc/api/data_descriptors/forcing_index.py +21 -0
  42. esgvoc/api/data_descriptors/frequency.py +48 -0
  43. esgvoc/api/data_descriptors/further_info_url.py +5 -0
  44. esgvoc/api/data_descriptors/grid.py +43 -0
  45. esgvoc/api/data_descriptors/horizontal_label.py +20 -0
  46. esgvoc/api/data_descriptors/initialization_index.py +27 -0
  47. esgvoc/api/data_descriptors/institution.py +80 -0
  48. esgvoc/api/data_descriptors/known_branded_variable.py +75 -0
  49. esgvoc/api/data_descriptors/license.py +31 -0
  50. esgvoc/api/data_descriptors/member_id.py +9 -0
  51. esgvoc/api/data_descriptors/mip_era.py +26 -0
  52. esgvoc/api/data_descriptors/model_component.py +32 -0
  53. esgvoc/api/data_descriptors/models_test/models.py +17 -0
  54. esgvoc/api/data_descriptors/nominal_resolution.py +50 -0
  55. esgvoc/api/data_descriptors/obs_type.py +5 -0
  56. esgvoc/api/data_descriptors/organisation.py +22 -0
  57. esgvoc/api/data_descriptors/physics_index.py +21 -0
  58. esgvoc/api/data_descriptors/product.py +16 -0
  59. esgvoc/api/data_descriptors/publication_status.py +5 -0
  60. esgvoc/api/data_descriptors/realization_index.py +24 -0
  61. esgvoc/api/data_descriptors/realm.py +16 -0
  62. esgvoc/api/data_descriptors/regex.py +5 -0
  63. esgvoc/api/data_descriptors/region.py +35 -0
  64. esgvoc/api/data_descriptors/resolution.py +7 -0
  65. esgvoc/api/data_descriptors/source.py +120 -0
  66. esgvoc/api/data_descriptors/source_type.py +5 -0
  67. esgvoc/api/data_descriptors/sub_experiment.py +5 -0
  68. esgvoc/api/data_descriptors/table.py +28 -0
  69. esgvoc/api/data_descriptors/temporal_label.py +20 -0
  70. esgvoc/api/data_descriptors/time_range.py +17 -0
  71. esgvoc/api/data_descriptors/title.py +5 -0
  72. esgvoc/api/data_descriptors/tracking_id.py +67 -0
  73. esgvoc/api/data_descriptors/variable.py +56 -0
  74. esgvoc/api/data_descriptors/variant_label.py +25 -0
  75. esgvoc/api/data_descriptors/vertical_label.py +20 -0
  76. esgvoc/api/project_specs.py +143 -0
  77. esgvoc/api/projects.py +1253 -0
  78. esgvoc/api/py.typed +0 -0
  79. esgvoc/api/pydantic_handler.py +146 -0
  80. esgvoc/api/report.py +127 -0
  81. esgvoc/api/search.py +171 -0
  82. esgvoc/api/universe.py +434 -0
  83. esgvoc/apps/__init__.py +6 -0
  84. esgvoc/apps/cmor_tables/__init__.py +7 -0
  85. esgvoc/apps/cmor_tables/cvs_table.py +948 -0
  86. esgvoc/apps/drs/__init__.py +0 -0
  87. esgvoc/apps/drs/constants.py +2 -0
  88. esgvoc/apps/drs/generator.py +429 -0
  89. esgvoc/apps/drs/report.py +540 -0
  90. esgvoc/apps/drs/validator.py +312 -0
  91. esgvoc/apps/ga/__init__.py +104 -0
  92. esgvoc/apps/ga/example_usage.py +315 -0
  93. esgvoc/apps/ga/models/__init__.py +47 -0
  94. esgvoc/apps/ga/models/netcdf_header.py +306 -0
  95. esgvoc/apps/ga/models/validator.py +491 -0
  96. esgvoc/apps/ga/test_ga.py +161 -0
  97. esgvoc/apps/ga/validator.py +277 -0
  98. esgvoc/apps/jsg/json_schema_generator.py +341 -0
  99. esgvoc/apps/jsg/templates/template.jinja +241 -0
  100. esgvoc/apps/test_cv/README.md +214 -0
  101. esgvoc/apps/test_cv/__init__.py +0 -0
  102. esgvoc/apps/test_cv/cv_tester.py +1611 -0
  103. esgvoc/apps/test_cv/example_usage.py +216 -0
  104. esgvoc/apps/vr/__init__.py +12 -0
  105. esgvoc/apps/vr/build_variable_registry.py +71 -0
  106. esgvoc/apps/vr/example_usage.py +60 -0
  107. esgvoc/apps/vr/vr_app.py +333 -0
  108. esgvoc/cli/clean.py +304 -0
  109. esgvoc/cli/cmor.py +46 -0
  110. esgvoc/cli/config.py +1300 -0
  111. esgvoc/cli/drs.py +267 -0
  112. esgvoc/cli/find.py +138 -0
  113. esgvoc/cli/get.py +155 -0
  114. esgvoc/cli/install.py +41 -0
  115. esgvoc/cli/main.py +60 -0
  116. esgvoc/cli/offline.py +269 -0
  117. esgvoc/cli/status.py +79 -0
  118. esgvoc/cli/test_cv.py +258 -0
  119. esgvoc/cli/valid.py +147 -0
  120. esgvoc/core/constants.py +17 -0
  121. esgvoc/core/convert.py +0 -0
  122. esgvoc/core/data_handler.py +206 -0
  123. esgvoc/core/db/__init__.py +3 -0
  124. esgvoc/core/db/connection.py +40 -0
  125. esgvoc/core/db/models/mixins.py +25 -0
  126. esgvoc/core/db/models/project.py +102 -0
  127. esgvoc/core/db/models/universe.py +98 -0
  128. esgvoc/core/db/project_ingestion.py +231 -0
  129. esgvoc/core/db/universe_ingestion.py +172 -0
  130. esgvoc/core/exceptions.py +33 -0
  131. esgvoc/core/logging_handler.py +26 -0
  132. esgvoc/core/repo_fetcher.py +345 -0
  133. esgvoc/core/service/__init__.py +41 -0
  134. esgvoc/core/service/configuration/config_manager.py +196 -0
  135. esgvoc/core/service/configuration/setting.py +363 -0
  136. esgvoc/core/service/data_merger.py +634 -0
  137. esgvoc/core/service/esg_voc.py +77 -0
  138. esgvoc/core/service/resolver_config.py +56 -0
  139. esgvoc/core/service/state.py +324 -0
  140. esgvoc/core/service/string_heuristics.py +98 -0
  141. esgvoc/core/service/term_cache.py +108 -0
  142. esgvoc/core/service/uri_resolver.py +133 -0
  143. esgvoc-2.0.2.dist-info/METADATA +82 -0
  144. esgvoc-2.0.2.dist-info/RECORD +147 -0
  145. esgvoc-2.0.2.dist-info/WHEEL +4 -0
  146. esgvoc-2.0.2.dist-info/entry_points.txt +2 -0
  147. esgvoc-2.0.2.dist-info/licenses/LICENSE.txt +519 -0
@@ -0,0 +1,363 @@
1
+ from typing import ClassVar, Dict, Optional
2
+ from pathlib import Path
3
+
4
+ import toml
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)
32
+
33
+
34
+ class ProjectSettings(BaseModel):
35
+ project_name: str
36
+ github_repo: str
37
+ branch: Optional[str] = "main"
38
+ local_path: Optional[str] = None
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)
54
+
55
+
56
+ class UniverseSettings(BaseModel):
57
+ github_repo: str
58
+ branch: Optional[str] = None
59
+ local_path: Optional[str] = None
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)
75
+
76
+
77
+ class ServiceSettings(BaseModel):
78
+ universe: UniverseSettings
79
+ projects: Dict[str, ProjectSettings] = Field(default_factory=dict)
80
+
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)
86
+
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
+ "emd": {
146
+ "project_name": "emd",
147
+ "github_repo": "https://github.com/WCRP-CMIP/Essential-Model-Documentation",
148
+ "branch": "esgvoc",
149
+ "local_path": "repos/Essential-Model-Documentation",
150
+ "db_path": "dbs/emd.sqlite",
151
+ "offline_mode": False,
152
+ },
153
+ }
154
+
155
+ @classmethod
156
+ def _get_default_settings(cls) -> dict:
157
+ """Generate default settings with relative paths."""
158
+ project_configs = cls._get_default_project_configs()
159
+ return {
160
+ "universe": {
161
+ "github_repo": "https://github.com/WCRP-CMIP/WCRP-universe",
162
+ "branch": "esgvoc",
163
+ "local_path": "repos/WCRP-universe",
164
+ "db_path": "dbs/universe.sqlite",
165
+ "offline_mode": False,
166
+ },
167
+ "projects": [
168
+ project_configs["cmip6"],
169
+ project_configs["cmip6plus"],
170
+ ],
171
+ }
172
+
173
+ # 🔹 Properties that provide access to the dynamic configurations
174
+ @property
175
+ def DEFAULT_PROJECT_CONFIGS(self) -> Dict[str, dict]:
176
+ return self._get_default_project_configs()
177
+
178
+ @property
179
+ def DEFAULT_SETTINGS(self) -> dict:
180
+ return self._get_default_settings()
181
+
182
+ @classmethod
183
+ def load_from_file(cls, file_path: str) -> "ServiceSettings":
184
+ """Load configuration from a TOML file, falling back to defaults if necessary."""
185
+ try:
186
+ data = toml.load(file_path)
187
+ except FileNotFoundError:
188
+ data = cls._get_default_settings().copy() # Use defaults if the file is missing
189
+
190
+ projects = {p["project_name"]: ProjectSettings(**p) for p in data.pop("projects", [])}
191
+ return cls(universe=UniverseSettings(**data["universe"]), projects=projects)
192
+
193
+ @classmethod
194
+ def load_default(cls) -> "ServiceSettings":
195
+ """Load default settings."""
196
+ return cls.load_from_dict(cls._get_default_settings())
197
+
198
+ @classmethod
199
+ def load_from_dict(cls, config_data: dict) -> "ServiceSettings":
200
+ """Load configuration from a dictionary."""
201
+ projects = {p["project_name"]: ProjectSettings(**p) for p in config_data.get("projects", [])}
202
+ return cls(universe=UniverseSettings(**config_data["universe"]), projects=projects)
203
+
204
+ def save_to_file(self, file_path: str):
205
+ """Save the configuration to a TOML file."""
206
+ data = {
207
+ "universe": self.universe.model_dump(),
208
+ "projects": [p.model_dump() for p in self.projects.values()],
209
+ }
210
+ with open(file_path, "w") as f:
211
+ toml.dump(data, f)
212
+
213
+ def dump(self) -> dict:
214
+ """Return the configuration as a dictionary."""
215
+ return {
216
+ "universe": self.universe.model_dump(),
217
+ "projects": [p.model_dump() for p in self.projects.values()],
218
+ }
219
+
220
+ # 🔹 NEW: Project management methods
221
+
222
+ def add_project_from_default(self, project_name: str) -> bool:
223
+ """
224
+ Add a project using its default configuration.
225
+
226
+ Args:
227
+ project_name: Name of the project to add (must exist in DEFAULT_PROJECT_CONFIGS)
228
+
229
+ Returns:
230
+ bool: True if project was added, False if it already exists or is unknown
231
+ """
232
+ if project_name in self.projects:
233
+ return False # Project already exists
234
+
235
+ default_configs = self._get_default_project_configs()
236
+ if project_name not in default_configs:
237
+ raise ValueError(f"Unknown project '{project_name}'. Available defaults: {list(default_configs.keys())}")
238
+
239
+ config = default_configs[project_name].copy()
240
+ self.projects[project_name] = ProjectSettings(**config)
241
+ return True
242
+
243
+ def add_project_custom(self, project_config: dict) -> bool:
244
+ """
245
+ Add a project with custom configuration.
246
+
247
+ Args:
248
+ project_config: Dictionary containing project configuration
249
+
250
+ Returns:
251
+ bool: True if project was added, False if it already exists
252
+ """
253
+ project_settings = ProjectSettings(**project_config)
254
+ project_name = project_settings.project_name
255
+
256
+ if project_name in self.projects:
257
+ return False # Project already exists
258
+
259
+ self.projects[project_name] = project_settings
260
+ return True
261
+
262
+ def remove_project(self, project_name: str) -> bool:
263
+ """
264
+ Remove a project from the configuration.
265
+
266
+ Args:
267
+ project_name: Name of the project to remove
268
+
269
+ Returns:
270
+
271
+ bool: True if project was removed, False if it didn't exist
272
+ """
273
+
274
+ if project_name in self.projects:
275
+ del self.projects[project_name]
276
+ return True
277
+ return False
278
+
279
+ def update_project(self, project_name: str, **kwargs) -> bool:
280
+ """
281
+ Update specific fields of an existing project.
282
+
283
+ Args:
284
+ project_name: Name of the project to update
285
+ **kwargs: Fields to update
286
+
287
+ Returns:
288
+ bool: True if project was updated, False if it doesn't exist
289
+ """
290
+ if project_name not in self.projects:
291
+ return False
292
+
293
+ # Handle boolean conversion for offline_mode if present
294
+ if "offline_mode" in kwargs:
295
+ if isinstance(kwargs["offline_mode"], str):
296
+ kwargs["offline_mode"] = kwargs["offline_mode"].lower() in ("true", "1", "yes", "on")
297
+
298
+ # Get current config and update with new values
299
+ current_config = self.projects[project_name].model_dump()
300
+ current_config.update(kwargs)
301
+
302
+ # Recreate the ProjectSettings with updated config
303
+ self.projects[project_name] = ProjectSettings(**current_config)
304
+ return True
305
+
306
+ def get_available_default_projects(self) -> list[str]:
307
+ """Return list of available default project names."""
308
+ return list(self._get_default_project_configs().keys())
309
+
310
+ def has_project(self, project_name: str) -> bool:
311
+ """Check if a project exists in the current configuration."""
312
+ return project_name in self.projects
313
+
314
+ def get_project(self, project_name: str) -> Optional[ProjectSettings]:
315
+ """Get a specific project configuration."""
316
+ return self.projects.get(project_name)
317
+
318
+
319
+ # 🔹 Usage Examples
320
+ def main():
321
+ # Create default settings (only cmip6 and cmip6plus)
322
+ settings = ServiceSettings.load_default()
323
+ # ['cmip6', 'cmip6plus']
324
+ print(f"Default projects: {list(settings.projects.keys())}")
325
+
326
+ # See what other projects are available to add
327
+ available = settings.get_available_default_projects()
328
+ # ['cmip6', 'cmip6plus', 'input4mip', 'obs4mip']
329
+ print(f"Available default projects: {available}")
330
+
331
+ # Add optional projects when needed
332
+ added_input4mip = settings.add_project_from_default("input4mip")
333
+ print(f"Added input4mip: {added_input4mip}")
334
+
335
+ added_obs4mip = settings.add_project_from_default("obs4mip")
336
+ print(f"Added obs4mip: {added_obs4mip}")
337
+
338
+ print(f"Projects after adding optional ones: {list(settings.projects.keys())}")
339
+
340
+ # Remove a project if no longer needed
341
+ removed = settings.remove_project("obs4mip")
342
+ print(f"Removed obs4mip: {removed}")
343
+ print(f"Projects after removal: {list(settings.projects.keys())}")
344
+
345
+ # Try to add a custom project
346
+ custom_project = {
347
+ "project_name": "my_custom_project",
348
+ "github_repo": "https://github.com/me/my-project",
349
+ "branch": "develop",
350
+ "local_path": "repos/my_project",
351
+ "db_path": "dbs/my_project.sqlite",
352
+ }
353
+ added_custom = settings.add_project_custom(custom_project)
354
+ print(f"Added custom project: {added_custom}")
355
+ print(f"Final projects: {list(settings.projects.keys())}")
356
+
357
+ # Update a project
358
+ updated = settings.update_project("my_custom_project", branch="main", db_path="dbs/updated.sqlite")
359
+ print(f"Updated custom project: {updated}")
360
+
361
+
362
+ if __name__ == "__main__":
363
+ main()