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,312 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
3
|
+
import esgvoc.api.projects as projects
|
|
4
|
+
import esgvoc.apps.drs.constants as constants
|
|
5
|
+
from esgvoc.api.project_specs import (
|
|
6
|
+
DrsPart,
|
|
7
|
+
DrsSpecification,
|
|
8
|
+
DrsType,
|
|
9
|
+
ProjectSpecs,
|
|
10
|
+
)
|
|
11
|
+
from esgvoc.apps.drs.report import (
|
|
12
|
+
BlankTerm,
|
|
13
|
+
ComplianceIssue,
|
|
14
|
+
DrsIssue,
|
|
15
|
+
DrsValidationReport,
|
|
16
|
+
ExtraChar,
|
|
17
|
+
ExtraSeparator,
|
|
18
|
+
ExtraTerm,
|
|
19
|
+
FileNameExtensionIssue,
|
|
20
|
+
InvalidTerm,
|
|
21
|
+
MissingTerm,
|
|
22
|
+
ParsingIssue,
|
|
23
|
+
Space,
|
|
24
|
+
Unparsable,
|
|
25
|
+
ValidationError,
|
|
26
|
+
ValidationWarning,
|
|
27
|
+
)
|
|
28
|
+
from esgvoc.core.exceptions import EsgvocDbError, EsgvocNotFoundError
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DrsApplication:
|
|
32
|
+
"""
|
|
33
|
+
Generic DRS application class.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, project_id: str, pedantic: bool = False) -> None:
|
|
37
|
+
self.project_id: str = project_id
|
|
38
|
+
"""The project id."""
|
|
39
|
+
self.pedantic: bool = pedantic
|
|
40
|
+
"""Same as the option of GCC: turn warnings into errors. Default False."""
|
|
41
|
+
project_specs: ProjectSpecs | None = projects.get_project(project_id)
|
|
42
|
+
if not project_specs or project_specs.drs_specs is None:
|
|
43
|
+
raise EsgvocNotFoundError(f"unable to find project spec or only drs_spec for '{project_id}'")
|
|
44
|
+
self.directory_specs: DrsSpecification = project_specs.drs_specs[DrsType.DIRECTORY]
|
|
45
|
+
"""The DRS directory specs of the project."""
|
|
46
|
+
self.file_name_specs: DrsSpecification = project_specs.drs_specs[DrsType.FILE_NAME]
|
|
47
|
+
"""The DRS file name specs of the project."""
|
|
48
|
+
self.dataset_id_specs: DrsSpecification = project_specs.drs_specs[DrsType.DATASET_ID]
|
|
49
|
+
"""The DRS dataset id specs of the project."""
|
|
50
|
+
|
|
51
|
+
def _get_full_file_name_extension(self) -> str:
|
|
52
|
+
"""
|
|
53
|
+
Returns the full file name extension (the separator plus the extension) of the DRS file
|
|
54
|
+
name specs of the project.
|
|
55
|
+
|
|
56
|
+
:returns: The full file name extension.
|
|
57
|
+
:rtype: str
|
|
58
|
+
"""
|
|
59
|
+
specs: DrsSpecification = self.file_name_specs
|
|
60
|
+
if specs.properties:
|
|
61
|
+
full_extension = (
|
|
62
|
+
specs.properties[constants.FILE_NAME_EXTENSION_SEPARATOR_KEY]
|
|
63
|
+
+ specs.properties[constants.FILE_NAME_EXTENSION_KEY]
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
raise EsgvocDbError(
|
|
67
|
+
"missing properties in the DRS file name specifications of the " + f"project '{self.project_id}'"
|
|
68
|
+
)
|
|
69
|
+
return full_extension
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class DrsValidator(DrsApplication):
|
|
73
|
+
"""
|
|
74
|
+
Valid a DRS directory, dataset id and file name expression against a project.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def validate_directory(self, drs_expression: str, prefix: str | None = None) -> DrsValidationReport:
|
|
78
|
+
"""
|
|
79
|
+
Validate a DRS directory expression.
|
|
80
|
+
|
|
81
|
+
:param drs_expression: A DRS directory expression.
|
|
82
|
+
:type drs_expression: str
|
|
83
|
+
:param prefix: A directory prefix to be removed from the directory expression.
|
|
84
|
+
:type prefix: str|None
|
|
85
|
+
:returns: A validation report.
|
|
86
|
+
:rtype: DrsValidationReport
|
|
87
|
+
"""
|
|
88
|
+
if prefix:
|
|
89
|
+
# Remove prefix if present. Always returns a copy.
|
|
90
|
+
drs_expression = drs_expression.removeprefix(prefix)
|
|
91
|
+
return self._validate(drs_expression, self.directory_specs)
|
|
92
|
+
|
|
93
|
+
def validate_dataset_id(self, drs_expression: str) -> DrsValidationReport:
|
|
94
|
+
"""
|
|
95
|
+
Validate a DRS dataset id expression.
|
|
96
|
+
|
|
97
|
+
:param drs_expression: A DRS dataset id expression.
|
|
98
|
+
:type drs_expression: str
|
|
99
|
+
:returns: A validation report.
|
|
100
|
+
:rtype: DrsValidationReport
|
|
101
|
+
"""
|
|
102
|
+
return self._validate(drs_expression, self.dataset_id_specs)
|
|
103
|
+
|
|
104
|
+
def validate_file_name(self, drs_expression: str) -> DrsValidationReport:
|
|
105
|
+
"""
|
|
106
|
+
Validate a file name expression.
|
|
107
|
+
|
|
108
|
+
:param drs_expression: A DRS file name expression.
|
|
109
|
+
:type drs_expression: str
|
|
110
|
+
:returns: A validation report.
|
|
111
|
+
:rtype: DrsValidationReport
|
|
112
|
+
"""
|
|
113
|
+
full_extension = self._get_full_file_name_extension()
|
|
114
|
+
if drs_expression.endswith(full_extension):
|
|
115
|
+
drs_expression = drs_expression.replace(full_extension, "")
|
|
116
|
+
result = self._validate(drs_expression, self.file_name_specs)
|
|
117
|
+
else:
|
|
118
|
+
issue = FileNameExtensionIssue(expected_extension=full_extension)
|
|
119
|
+
result = self._create_report(self.file_name_specs.type, drs_expression, [issue], [])
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
def validate(self, drs_expression: str, drs_type: DrsType | str) -> DrsValidationReport:
|
|
123
|
+
"""
|
|
124
|
+
Validate a DRS expression.
|
|
125
|
+
|
|
126
|
+
:param drs_expression: A DRS expression.
|
|
127
|
+
:type drs_expression: str
|
|
128
|
+
:param drs_type: The type of the given DRS expression (directory, file_name or dataset_id)
|
|
129
|
+
:type drs_type: DrsType|str
|
|
130
|
+
:returns: A validation report.
|
|
131
|
+
:rtype: DrsValidationReport
|
|
132
|
+
"""
|
|
133
|
+
match drs_type:
|
|
134
|
+
case DrsType.DIRECTORY:
|
|
135
|
+
return self.validate_directory(drs_expression=drs_expression)
|
|
136
|
+
case DrsType.FILE_NAME:
|
|
137
|
+
return self.validate_file_name(drs_expression=drs_expression)
|
|
138
|
+
case DrsType.DATASET_ID:
|
|
139
|
+
return self.validate_dataset_id(drs_expression=drs_expression)
|
|
140
|
+
case _:
|
|
141
|
+
raise EsgvocDbError(f"unsupported drs type '{drs_type}'")
|
|
142
|
+
|
|
143
|
+
def _parse(
|
|
144
|
+
self, drs_expression: str, separator: str, drs_type: DrsType
|
|
145
|
+
) -> tuple[
|
|
146
|
+
list[str] | None, # terms
|
|
147
|
+
list[DrsIssue], # Errors
|
|
148
|
+
list[DrsIssue],
|
|
149
|
+
]: # Warnings
|
|
150
|
+
errors: list[DrsIssue] = list()
|
|
151
|
+
warnings: list[DrsIssue] = list()
|
|
152
|
+
cursor_offset = 0
|
|
153
|
+
# Spaces at the beginning/end of expression:
|
|
154
|
+
start_with_space = drs_expression[0].isspace()
|
|
155
|
+
end_with_space = drs_expression[-1].isspace()
|
|
156
|
+
if start_with_space or end_with_space:
|
|
157
|
+
issue: ParsingIssue = Space()
|
|
158
|
+
if self.pedantic:
|
|
159
|
+
errors.append(issue)
|
|
160
|
+
else:
|
|
161
|
+
warnings.append(issue)
|
|
162
|
+
if start_with_space:
|
|
163
|
+
previous_len = len(drs_expression)
|
|
164
|
+
drs_expression = drs_expression.lstrip()
|
|
165
|
+
cursor_offset = previous_len - len(drs_expression)
|
|
166
|
+
if end_with_space:
|
|
167
|
+
drs_expression = drs_expression.rstrip()
|
|
168
|
+
terms = drs_expression.split(separator)
|
|
169
|
+
if len(terms) < 2:
|
|
170
|
+
errors.append(Unparsable(expected_drs_type=drs_type))
|
|
171
|
+
return None, errors, warnings # Early exit
|
|
172
|
+
max_term_index = len(terms)
|
|
173
|
+
cursor_position = initial_cursor_position = len(drs_expression) + 1
|
|
174
|
+
has_white_term = False
|
|
175
|
+
for index in range(max_term_index - 1, -1, -1):
|
|
176
|
+
term = terms[index]
|
|
177
|
+
if (is_white_term := term.isspace()) or (not term):
|
|
178
|
+
has_white_term = has_white_term or is_white_term
|
|
179
|
+
cursor_position -= len(term) + 1
|
|
180
|
+
del terms[index]
|
|
181
|
+
continue
|
|
182
|
+
else:
|
|
183
|
+
break
|
|
184
|
+
if cursor_position != initial_cursor_position:
|
|
185
|
+
max_term_index = len(terms)
|
|
186
|
+
column = cursor_position + cursor_offset
|
|
187
|
+
if (drs_type == DrsType.DIRECTORY) and (not has_white_term):
|
|
188
|
+
issue = ExtraSeparator(column=column)
|
|
189
|
+
if self.pedantic:
|
|
190
|
+
errors.append(issue)
|
|
191
|
+
else:
|
|
192
|
+
warnings.append(issue)
|
|
193
|
+
else:
|
|
194
|
+
issue = ExtraChar(column=column)
|
|
195
|
+
errors.append(issue)
|
|
196
|
+
for index in range(max_term_index - 1, -1, -1):
|
|
197
|
+
term = terms[index]
|
|
198
|
+
len_term = len(term)
|
|
199
|
+
if not term:
|
|
200
|
+
column = cursor_position + cursor_offset
|
|
201
|
+
issue = ExtraSeparator(column=column)
|
|
202
|
+
if self.pedantic or drs_type != DrsType.DIRECTORY or index == 0:
|
|
203
|
+
errors.append(issue)
|
|
204
|
+
else:
|
|
205
|
+
warnings.append(issue)
|
|
206
|
+
del terms[index]
|
|
207
|
+
if term.isspace():
|
|
208
|
+
column = cursor_position + cursor_offset - len_term
|
|
209
|
+
issue = BlankTerm(column=column)
|
|
210
|
+
errors.append(issue)
|
|
211
|
+
del terms[index]
|
|
212
|
+
cursor_position -= len_term + 1
|
|
213
|
+
|
|
214
|
+
# Mypy doesn't understand that ParsingIssues are DrsIssues...
|
|
215
|
+
sorted_errors = DrsValidator._sort_parser_issues(errors) # type: ignore
|
|
216
|
+
sorted_warnings = DrsValidator._sort_parser_issues(warnings) # type: ignore
|
|
217
|
+
return terms, sorted_errors, sorted_warnings # type: ignore
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def _sort_parser_issues(issues: list[ParsingIssue]) -> list[ParsingIssue]:
|
|
221
|
+
return sorted(issues, key=lambda issue: issue.column if issue.column else 0)
|
|
222
|
+
|
|
223
|
+
def _validate_term(self, term: str, part: DrsPart) -> bool:
|
|
224
|
+
if part.source_collection_term is None:
|
|
225
|
+
matching_terms = projects.valid_term_in_collection(term, self.project_id, part.source_collection)
|
|
226
|
+
if len(matching_terms) > 0:
|
|
227
|
+
return True
|
|
228
|
+
else:
|
|
229
|
+
return False
|
|
230
|
+
else:
|
|
231
|
+
return projects.valid_term(
|
|
232
|
+
term, self.project_id, part.source_collection, part.source_collection_term
|
|
233
|
+
).validated
|
|
234
|
+
|
|
235
|
+
def _create_report(
|
|
236
|
+
self,
|
|
237
|
+
type: DrsType,
|
|
238
|
+
drs_expression: str,
|
|
239
|
+
errors: list[DrsIssue],
|
|
240
|
+
warnings: list[DrsIssue],
|
|
241
|
+
mapping_used: dict[str, str] | None = None,
|
|
242
|
+
) -> DrsValidationReport:
|
|
243
|
+
if mapping_used is None:
|
|
244
|
+
mapping_used = {}
|
|
245
|
+
return DrsValidationReport(
|
|
246
|
+
project_id=self.project_id,
|
|
247
|
+
type=type,
|
|
248
|
+
expression=drs_expression,
|
|
249
|
+
mapping_used=mapping_used,
|
|
250
|
+
errors=cast(list[ValidationError], errors),
|
|
251
|
+
warnings=cast(list[ValidationWarning], warnings),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def _validate(self, drs_expression: str, specs: DrsSpecification) -> DrsValidationReport:
|
|
255
|
+
terms, errors, warnings = self._parse(drs_expression, specs.separator, specs.type)
|
|
256
|
+
if not terms:
|
|
257
|
+
# Early exit.
|
|
258
|
+
return self._create_report(specs.type, drs_expression, errors, warnings)
|
|
259
|
+
term_index = 0
|
|
260
|
+
term_max_index = len(terms)
|
|
261
|
+
part_index = 0
|
|
262
|
+
part_max_index = len(specs.parts)
|
|
263
|
+
matching_code_mapping = dict()
|
|
264
|
+
mapping_used: dict[str, str] = dict()
|
|
265
|
+
while part_index < part_max_index:
|
|
266
|
+
term = terms[term_index]
|
|
267
|
+
part: DrsPart = specs.parts[part_index]
|
|
268
|
+
if self._validate_term(term, part):
|
|
269
|
+
term_index += 1
|
|
270
|
+
part_index += 1
|
|
271
|
+
matching_code_mapping[part.__str__()] = 0
|
|
272
|
+
mapping_used[part.source_collection] = term
|
|
273
|
+
elif part.is_required:
|
|
274
|
+
issue: ComplianceIssue = InvalidTerm(
|
|
275
|
+
term=term, term_position=term_index + 1, collection_id_or_constant_value=str(part)
|
|
276
|
+
)
|
|
277
|
+
errors.append(issue)
|
|
278
|
+
matching_code_mapping[part.__str__()] = 1
|
|
279
|
+
term_index += 1
|
|
280
|
+
part_index += 1
|
|
281
|
+
else: # The part is not required so try to match the term with the next part.
|
|
282
|
+
part_index += 1
|
|
283
|
+
matching_code_mapping[part.__str__()] = -1
|
|
284
|
+
if term_index == term_max_index:
|
|
285
|
+
break
|
|
286
|
+
# Cases:
|
|
287
|
+
# - All terms and collections have been processed.
|
|
288
|
+
# - Not enough term to process all collections.
|
|
289
|
+
# - Extra terms left whereas all collections have been processed:
|
|
290
|
+
# + The last collections are required => report extra terms.
|
|
291
|
+
# + The last collections are not required and these terms were not validated by them.
|
|
292
|
+
# => Should report error even if the collections are not required.
|
|
293
|
+
if part_index < part_max_index: # Missing terms.
|
|
294
|
+
for index in range(part_index, part_max_index):
|
|
295
|
+
part = specs.parts[index]
|
|
296
|
+
issue = MissingTerm(collection_id=str(part), collection_position=index + 1)
|
|
297
|
+
if part.is_required:
|
|
298
|
+
errors.append(issue)
|
|
299
|
+
else:
|
|
300
|
+
warnings.append(issue)
|
|
301
|
+
elif term_index < term_max_index: # Extra terms.
|
|
302
|
+
part_index -= term_max_index - term_index
|
|
303
|
+
for index in range(term_index, term_max_index):
|
|
304
|
+
term = terms[index]
|
|
305
|
+
part = specs.parts[part_index]
|
|
306
|
+
if (not part.is_required) and matching_code_mapping[part.__str__()] < 0: # noqa E125
|
|
307
|
+
issue = ExtraTerm(term=term, term_position=index, collection_id=str(part))
|
|
308
|
+
else:
|
|
309
|
+
issue = ExtraTerm(term=term, term_position=index, collection_id=None)
|
|
310
|
+
errors.append(issue)
|
|
311
|
+
part_index += 1
|
|
312
|
+
return self._create_report(specs.type, drs_expression, errors, warnings, mapping_used)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""GA application for global attributes validation for netCDF files.
|
|
2
|
+
|
|
3
|
+
This package provides tools for validating NetCDF global attributes against
|
|
4
|
+
project specifications (like CMIP6, CMIP7) using controlled vocabularies
|
|
5
|
+
from the esgvoc API.
|
|
6
|
+
|
|
7
|
+
Key Features:
|
|
8
|
+
- YAML-based configuration for attribute specifications
|
|
9
|
+
- Integration with esgvoc controlled vocabularies
|
|
10
|
+
- NetCDF header parsing from ncdump output
|
|
11
|
+
- Comprehensive validation reporting
|
|
12
|
+
- Support for different project specifications
|
|
13
|
+
|
|
14
|
+
Example Usage:
|
|
15
|
+
```python
|
|
16
|
+
from esgvoc.apps.ga import GAValidator, validate_netcdf_attributes
|
|
17
|
+
|
|
18
|
+
# Quick validation from ncdump output
|
|
19
|
+
report = validate_netcdf_attributes(
|
|
20
|
+
ncdump_output=ncdump_text,
|
|
21
|
+
project_id="cmip6",
|
|
22
|
+
filename="my_file.nc"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
print(f"Validation result: {'PASS' if report.is_valid else 'FAIL'}")
|
|
26
|
+
print(f"Errors: {report.error_count}, Warnings: {report.warning_count}")
|
|
27
|
+
|
|
28
|
+
# Or use the full validator class
|
|
29
|
+
validator = GAValidator(project_id="cmip6")
|
|
30
|
+
report = validator.validate_from_ncdump(ncdump_text)
|
|
31
|
+
|
|
32
|
+
# Get detailed validation summary
|
|
33
|
+
from esgvoc.apps.ga import create_validation_summary
|
|
34
|
+
print(create_validation_summary(report))
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Advanced Usage:
|
|
38
|
+
```python
|
|
39
|
+
from esgvoc.apps.ga.models import NetCDFHeader, NetCDFHeaderParser
|
|
40
|
+
|
|
41
|
+
# Parse NetCDF header from ncdump output
|
|
42
|
+
ncdump_output = '''
|
|
43
|
+
netcdf test_file {
|
|
44
|
+
// global attributes:
|
|
45
|
+
:Conventions = "CF-1.7 CMIP-6.2" ;
|
|
46
|
+
:activity_id = "CMIP" ;
|
|
47
|
+
:experiment_id = "historical" ;
|
|
48
|
+
}
|
|
49
|
+
'''
|
|
50
|
+
|
|
51
|
+
header = NetCDFHeaderParser.parse_from_ncdump(ncdump_output)
|
|
52
|
+
print(f"File: {header.filename}")
|
|
53
|
+
print(f"Attributes: {header.global_attributes.list_attributes()}")
|
|
54
|
+
```
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# Main GA validator interface
|
|
58
|
+
from .validator import (
|
|
59
|
+
GAValidator,
|
|
60
|
+
GAValidatorFactory,
|
|
61
|
+
validate_netcdf_attributes,
|
|
62
|
+
create_validation_summary
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Core models
|
|
66
|
+
from .models import (
|
|
67
|
+
# Models for advanced usage
|
|
68
|
+
NetCDFHeader,
|
|
69
|
+
NetCDFHeaderParser,
|
|
70
|
+
ValidationReport,
|
|
71
|
+
ValidationSeverity,
|
|
72
|
+
ValidationIssue,
|
|
73
|
+
|
|
74
|
+
# Validator models
|
|
75
|
+
ESGVocAttributeValidator,
|
|
76
|
+
ValidatorFactory,
|
|
77
|
+
|
|
78
|
+
# Import AttributeProperty from project_specs
|
|
79
|
+
AttributeProperty,
|
|
80
|
+
AttributeSpecification,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
__all__ = [
|
|
84
|
+
# Main interface
|
|
85
|
+
"GAValidator",
|
|
86
|
+
"GAValidatorFactory",
|
|
87
|
+
"validate_netcdf_attributes",
|
|
88
|
+
"create_validation_summary",
|
|
89
|
+
|
|
90
|
+
# Models
|
|
91
|
+
"NetCDFHeader",
|
|
92
|
+
"NetCDFHeaderParser",
|
|
93
|
+
"ValidationReport",
|
|
94
|
+
"ValidationSeverity",
|
|
95
|
+
"ValidationIssue",
|
|
96
|
+
|
|
97
|
+
# Attribute specifications from project_specs
|
|
98
|
+
"AttributeProperty",
|
|
99
|
+
"AttributeSpecification",
|
|
100
|
+
|
|
101
|
+
# Validators
|
|
102
|
+
"ESGVocAttributeValidator",
|
|
103
|
+
"ValidatorFactory",
|
|
104
|
+
]
|