esgvoc 0.1.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.
Potentially problematic release.
This version of esgvoc might be problematic. Click here for more details.
- esgvoc/__init__.py +1 -0
- esgvoc/api/__init__.py +62 -0
- esgvoc/api/_utils.py +39 -0
- esgvoc/api/data_descriptors/__init__.py +60 -0
- esgvoc/api/data_descriptors/activity.py +51 -0
- esgvoc/api/data_descriptors/consortium.py +66 -0
- esgvoc/api/data_descriptors/date.py +48 -0
- esgvoc/api/data_descriptors/experiment.py +60 -0
- esgvoc/api/data_descriptors/forcing_index.py +47 -0
- esgvoc/api/data_descriptors/frequency.py +45 -0
- esgvoc/api/data_descriptors/grid_label.py +46 -0
- esgvoc/api/data_descriptors/initialisation_index.py +46 -0
- esgvoc/api/data_descriptors/institution.py +58 -0
- esgvoc/api/data_descriptors/license.py +47 -0
- esgvoc/api/data_descriptors/mip_era.py +46 -0
- esgvoc/api/data_descriptors/model_component.py +47 -0
- esgvoc/api/data_descriptors/organisation.py +42 -0
- esgvoc/api/data_descriptors/physic_index.py +47 -0
- esgvoc/api/data_descriptors/product.py +45 -0
- esgvoc/api/data_descriptors/realisation_index.py +46 -0
- esgvoc/api/data_descriptors/realm.py +44 -0
- esgvoc/api/data_descriptors/resolution.py +46 -0
- esgvoc/api/data_descriptors/source.py +57 -0
- esgvoc/api/data_descriptors/source_type.py +43 -0
- esgvoc/api/data_descriptors/sub_experiment.py +43 -0
- esgvoc/api/data_descriptors/table.py +50 -0
- esgvoc/api/data_descriptors/time_range.py +28 -0
- esgvoc/api/data_descriptors/variable.py +77 -0
- esgvoc/api/data_descriptors/variant_label.py +49 -0
- esgvoc/api/projects.py +854 -0
- esgvoc/api/report.py +86 -0
- esgvoc/api/search.py +92 -0
- esgvoc/api/universe.py +218 -0
- esgvoc/apps/drs/__init__.py +16 -0
- esgvoc/apps/drs/models.py +43 -0
- esgvoc/apps/drs/parser.py +27 -0
- esgvoc/cli/config.py +79 -0
- esgvoc/cli/get.py +142 -0
- esgvoc/cli/install.py +14 -0
- esgvoc/cli/main.py +22 -0
- esgvoc/cli/status.py +26 -0
- esgvoc/cli/valid.py +156 -0
- esgvoc/core/constants.py +13 -0
- esgvoc/core/convert.py +0 -0
- esgvoc/core/data_handler.py +133 -0
- esgvoc/core/db/__init__.py +5 -0
- esgvoc/core/db/connection.py +31 -0
- esgvoc/core/db/models/mixins.py +18 -0
- esgvoc/core/db/models/project.py +65 -0
- esgvoc/core/db/models/universe.py +59 -0
- esgvoc/core/db/project_ingestion.py +152 -0
- esgvoc/core/db/universe_ingestion.py +120 -0
- esgvoc/core/logging.conf +21 -0
- esgvoc/core/logging_handler.py +4 -0
- esgvoc/core/repo_fetcher.py +259 -0
- esgvoc/core/service/__init__.py +8 -0
- esgvoc/core/service/data_merger.py +83 -0
- esgvoc/core/service/esg_voc.py +79 -0
- esgvoc/core/service/settings.py +64 -0
- esgvoc/core/service/settings.toml +12 -0
- esgvoc/core/service/settings_default.toml +20 -0
- esgvoc/core/service/state.py +222 -0
- esgvoc-0.1.2.dist-info/METADATA +54 -0
- esgvoc-0.1.2.dist-info/RECORD +66 -0
- esgvoc-0.1.2.dist-info/WHEEL +4 -0
- esgvoc-0.1.2.dist-info/entry_points.txt +2 -0
esgvoc/api/report.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import esgvoc.core.constants as api_settings
|
|
5
|
+
from esgvoc.core.db.models.mixins import TermKind
|
|
6
|
+
from esgvoc.core.db.models.project import PTerm
|
|
7
|
+
from esgvoc.core.db.models.universe import UTerm
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ValidationErrorVisitor(ABC):
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def visit_universe_term_error(self, error: "UniverseTermError") -> Any:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def visit_project_term_error(self, error: "ProjectTermError") -> Any:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BasicValidationErrorVisitor(ValidationErrorVisitor):
|
|
21
|
+
def visit_universe_term_error(self, error: "UniverseTermError") -> Any:
|
|
22
|
+
term_id = error.term[api_settings.TERM_ID_JSON_KEY]
|
|
23
|
+
result = f"The term {term_id} from the data descriptor {error.data_descriptor_id} "+\
|
|
24
|
+
f"does not validate the given value '{error.value}'"
|
|
25
|
+
return result
|
|
26
|
+
|
|
27
|
+
def visit_project_term_error(self, error: "ProjectTermError") -> Any:
|
|
28
|
+
term_id = error.term[api_settings.TERM_ID_JSON_KEY]
|
|
29
|
+
result = f"The term {term_id} from the collection {error.collection_id} "+\
|
|
30
|
+
f"does not validate the given value '{error.value}'"
|
|
31
|
+
return result
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ValidationError(ABC):
|
|
35
|
+
def __init__(self,
|
|
36
|
+
value: str):
|
|
37
|
+
self.value: str = value
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def accept(self, visitor: ValidationErrorVisitor) -> Any:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
class UniverseTermError(ValidationError):
|
|
44
|
+
def __init__(self,
|
|
45
|
+
value: str,
|
|
46
|
+
term: UTerm):
|
|
47
|
+
super().__init__(value)
|
|
48
|
+
self.term: dict = term.specs
|
|
49
|
+
self.term_kind: TermKind = term.kind
|
|
50
|
+
self.data_descriptor_id: str = term.data_descriptor.id
|
|
51
|
+
|
|
52
|
+
def accept(self, visitor: ValidationErrorVisitor) -> Any:
|
|
53
|
+
return visitor.visit_universe_term_error(self)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ProjectTermError(ValidationError):
|
|
57
|
+
def __init__(self,
|
|
58
|
+
value: str,
|
|
59
|
+
term: PTerm):
|
|
60
|
+
super().__init__(value)
|
|
61
|
+
self.term: dict = term.specs
|
|
62
|
+
self.term_kind: TermKind = term.kind
|
|
63
|
+
self.collection_id: str = term.collection.id
|
|
64
|
+
|
|
65
|
+
def accept(self, visitor: ValidationErrorVisitor) -> Any:
|
|
66
|
+
return visitor.visit_project_term_error(self)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ValidationReport:
|
|
70
|
+
def __init__(self,
|
|
71
|
+
given_expression: str,
|
|
72
|
+
errors: list[ValidationError]):
|
|
73
|
+
self.expression: str = given_expression
|
|
74
|
+
self.errors: list[ValidationError] = errors
|
|
75
|
+
self.nb_errors = len(self.errors) if self.errors else 0
|
|
76
|
+
self.validated: bool = False if errors else True
|
|
77
|
+
self.message = f"'{self.expression}' has {self.nb_errors} error(s)"
|
|
78
|
+
|
|
79
|
+
def __len__(self) -> int:
|
|
80
|
+
return self.nb_errors
|
|
81
|
+
|
|
82
|
+
def __bool__(self) -> bool:
|
|
83
|
+
return self.validated
|
|
84
|
+
|
|
85
|
+
def __repr__(self) -> str:
|
|
86
|
+
return self.message
|
esgvoc/api/search.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from sqlalchemy import ColumnElement, func
|
|
6
|
+
from sqlmodel import col
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class MatchingTerm:
|
|
11
|
+
project_id: str
|
|
12
|
+
collection_id: str
|
|
13
|
+
term_id: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SearchType(Enum):
|
|
17
|
+
EXACT = ("exact",)
|
|
18
|
+
LIKE = ("like",) # can interpret %
|
|
19
|
+
STARTS_WITH = ("starts_with",) # can interpret %
|
|
20
|
+
ENDS_WITH = "ends_with" # can interpret %
|
|
21
|
+
REGEX = ("regex",)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SearchSettings(BaseModel):
|
|
25
|
+
type: SearchType = SearchType.EXACT
|
|
26
|
+
case_sensitive: bool = True
|
|
27
|
+
not_operator: bool = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def create_str_comparison_expression(field: str,
|
|
31
|
+
value: str,
|
|
32
|
+
settings: SearchSettings|None) -> ColumnElement:
|
|
33
|
+
'''
|
|
34
|
+
SQLite LIKE is case insensitive (and so STARTS/ENDS_WITH which are implemented with LIKE).
|
|
35
|
+
So the case sensitive LIKE is implemented with REGEX.
|
|
36
|
+
The i versions of SQLAlchemy operators (icontains, etc.) are not useful
|
|
37
|
+
(but other dbs than SQLite should use them).
|
|
38
|
+
If the provided `settings` is None, this functions returns an exact search expression.
|
|
39
|
+
'''
|
|
40
|
+
does_wild_cards_in_value_have_to_be_interpreted = False
|
|
41
|
+
# Shortcut.
|
|
42
|
+
if settings is None:
|
|
43
|
+
return col(field).is_(other=value)
|
|
44
|
+
else:
|
|
45
|
+
match settings.type:
|
|
46
|
+
# Early return because not operator is not implement with tilde symbol.
|
|
47
|
+
case SearchType.EXACT:
|
|
48
|
+
if settings.case_sensitive:
|
|
49
|
+
if settings.not_operator:
|
|
50
|
+
return col(field).is_not(other=value)
|
|
51
|
+
else:
|
|
52
|
+
return col(field).is_(other=value)
|
|
53
|
+
else:
|
|
54
|
+
if settings.not_operator:
|
|
55
|
+
return func.lower(field) != func.lower(value)
|
|
56
|
+
else:
|
|
57
|
+
return func.lower(field) == func.lower(value)
|
|
58
|
+
case SearchType.LIKE:
|
|
59
|
+
if settings.case_sensitive:
|
|
60
|
+
result = col(field).regexp_match(pattern=f".*{value}.*")
|
|
61
|
+
else:
|
|
62
|
+
result = col(field).contains(
|
|
63
|
+
other=value,
|
|
64
|
+
autoescape=not does_wild_cards_in_value_have_to_be_interpreted,
|
|
65
|
+
)
|
|
66
|
+
case SearchType.STARTS_WITH:
|
|
67
|
+
if settings.case_sensitive:
|
|
68
|
+
result = col(field).regexp_match(pattern=f"^{value}.*")
|
|
69
|
+
else:
|
|
70
|
+
result = col(field).startswith(
|
|
71
|
+
other=value,
|
|
72
|
+
autoescape=not does_wild_cards_in_value_have_to_be_interpreted,
|
|
73
|
+
)
|
|
74
|
+
case SearchType.ENDS_WITH:
|
|
75
|
+
if settings.case_sensitive:
|
|
76
|
+
result = col(field).regexp_match(pattern=f"{value}$")
|
|
77
|
+
else:
|
|
78
|
+
result = col(field).endswith(
|
|
79
|
+
other=value,
|
|
80
|
+
autoescape=not does_wild_cards_in_value_have_to_be_interpreted,
|
|
81
|
+
)
|
|
82
|
+
case SearchType.REGEX:
|
|
83
|
+
if settings.case_sensitive:
|
|
84
|
+
result = col(field).regexp_match(pattern=value)
|
|
85
|
+
else:
|
|
86
|
+
raise NotImplementedError(
|
|
87
|
+
"regex string comparison case insensitive is not implemented"
|
|
88
|
+
)
|
|
89
|
+
if settings.not_operator:
|
|
90
|
+
return ~result
|
|
91
|
+
else:
|
|
92
|
+
return result
|
esgvoc/api/universe.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from typing import Sequence
|
|
2
|
+
|
|
3
|
+
from esgvoc.api._utils import (get_universe_session,
|
|
4
|
+
instantiate_pydantic_terms)
|
|
5
|
+
from esgvoc.api.search import SearchSettings, create_str_comparison_expression
|
|
6
|
+
from esgvoc.core.db.models.universe import DataDescriptor, UTerm
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from sqlmodel import Session, select
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _find_terms_in_data_descriptor(data_descriptor_id: str,
|
|
12
|
+
term_id: str,
|
|
13
|
+
session: Session,
|
|
14
|
+
settings: SearchSettings|None) -> Sequence[UTerm]:
|
|
15
|
+
"""Settings only apply on the term_id comparison."""
|
|
16
|
+
where_expression = create_str_comparison_expression(field=UTerm.id,
|
|
17
|
+
value=term_id,
|
|
18
|
+
settings=settings)
|
|
19
|
+
statement = select(UTerm).join(DataDescriptor).where(DataDescriptor.id==data_descriptor_id,
|
|
20
|
+
where_expression)
|
|
21
|
+
results = session.exec(statement)
|
|
22
|
+
result = results.all()
|
|
23
|
+
return result
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def find_terms_in_data_descriptor(data_descriptor_id: str,
|
|
27
|
+
term_id: str,
|
|
28
|
+
settings: SearchSettings|None = None) \
|
|
29
|
+
-> list[BaseModel]:
|
|
30
|
+
"""
|
|
31
|
+
Finds one or more terms in the given data descriptor based on the specified search settings.
|
|
32
|
+
This function performs an exact match on the `data_descriptor_id` and
|
|
33
|
+
does **not** search for similar or related descriptors.
|
|
34
|
+
The given `term_id` is searched according to the search type specified in
|
|
35
|
+
the parameter `settings`,
|
|
36
|
+
which allows a flexible matching (e.g., `LIKE` may return multiple results).
|
|
37
|
+
If the parameter `settings` is `None`, this function performs an exact match on the `term_id`.
|
|
38
|
+
If any of the provided ids (`data_descriptor_id` or `term_id`) is not found, the function
|
|
39
|
+
returns an empty list.
|
|
40
|
+
|
|
41
|
+
Behavior based on search type:
|
|
42
|
+
- `EXACT` and absence of `settings`: returns zero or one Pydantic term instance in the list.
|
|
43
|
+
- `REGEX`, `LIKE`, `STARTS_WITH` and `ENDS_WITH`: returns zero, one or more Pydantic term
|
|
44
|
+
instances in the list.
|
|
45
|
+
|
|
46
|
+
:param data_descriptor_id: A data descriptor id
|
|
47
|
+
:type data_descriptor_id: str
|
|
48
|
+
:param term_id: A term id to be found
|
|
49
|
+
:type term_id: str
|
|
50
|
+
:param settings: The search settings
|
|
51
|
+
:type settings: SearchSettings|None
|
|
52
|
+
:returns: A list of Pydantic model term instances.
|
|
53
|
+
Returns an empty list if no matches are found.
|
|
54
|
+
:rtype: list[BaseModel]
|
|
55
|
+
"""
|
|
56
|
+
result: list[BaseModel] = list()
|
|
57
|
+
with get_universe_session() as session:
|
|
58
|
+
terms = _find_terms_in_data_descriptor(data_descriptor_id, term_id, session, settings)
|
|
59
|
+
instantiate_pydantic_terms(terms, result)
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _find_terms_in_universe(term_id: str,
|
|
64
|
+
session: Session,
|
|
65
|
+
settings: SearchSettings|None) -> Sequence[UTerm]:
|
|
66
|
+
where_expression = create_str_comparison_expression(field=UTerm.id,
|
|
67
|
+
value=term_id,
|
|
68
|
+
settings=settings)
|
|
69
|
+
statement = select(UTerm).where(where_expression)
|
|
70
|
+
results = session.exec(statement).all()
|
|
71
|
+
return results
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def find_terms_in_universe(term_id: str,
|
|
75
|
+
settings: SearchSettings|None = None) \
|
|
76
|
+
-> list[BaseModel]:
|
|
77
|
+
"""
|
|
78
|
+
Finds one or more terms of the universe.
|
|
79
|
+
The given `term_id` is searched according to the search type specified in
|
|
80
|
+
the parameter `settings`,
|
|
81
|
+
which allows a flexible matching (e.g., `LIKE` may return multiple results).
|
|
82
|
+
If the parameter `settings` is `None`, this function performs an exact match on the `term_id`.
|
|
83
|
+
Terms are unique within a data descriptor but may have some synonyms in the universe.
|
|
84
|
+
If the provided `term_id` is not found, the function returns an empty list.
|
|
85
|
+
|
|
86
|
+
:param term_id: A term id to be found
|
|
87
|
+
:type term_id: str
|
|
88
|
+
:param settings: The search settings
|
|
89
|
+
:type settings: SearchSettings|None
|
|
90
|
+
:returns: A list of Pydantic term instances. Returns an empty list if no matches are found.
|
|
91
|
+
:rtype: list[BaseModel]
|
|
92
|
+
"""
|
|
93
|
+
result: list[BaseModel] = list()
|
|
94
|
+
with get_universe_session() as session:
|
|
95
|
+
terms = _find_terms_in_universe(term_id, session, settings)
|
|
96
|
+
instantiate_pydantic_terms(terms, result)
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _get_all_terms_in_data_descriptor(data_descriptor: DataDescriptor) -> list[BaseModel]:
|
|
101
|
+
result: list[BaseModel] = list()
|
|
102
|
+
instantiate_pydantic_terms(data_descriptor.terms, result)
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _find_data_descriptors_in_universe(data_descriptor_id: str,
|
|
107
|
+
session: Session,
|
|
108
|
+
settings: SearchSettings|None) -> Sequence[DataDescriptor]:
|
|
109
|
+
where_expression = create_str_comparison_expression(field=DataDescriptor.id,
|
|
110
|
+
value=data_descriptor_id,
|
|
111
|
+
settings=settings)
|
|
112
|
+
statement = select(DataDescriptor).where(where_expression)
|
|
113
|
+
results = session.exec(statement)
|
|
114
|
+
result = results.all()
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_all_terms_in_data_descriptor(data_descriptor_id: str) \
|
|
119
|
+
-> list[BaseModel]:
|
|
120
|
+
"""
|
|
121
|
+
Gets all the terms of the given data descriptor.
|
|
122
|
+
This function performs an exact match on the `data_descriptor_id` and does **not** search
|
|
123
|
+
for similar or related descriptors.
|
|
124
|
+
If the provided `data_descriptor_id` is not found, the function returns an empty list.
|
|
125
|
+
|
|
126
|
+
:param data_descriptor_id: A data descriptor id
|
|
127
|
+
:type data_descriptor_id: str
|
|
128
|
+
:returns: a list of Pydantic term instances. Returns an empty list if no matches are found.
|
|
129
|
+
:rtype: list[BaseModel]
|
|
130
|
+
"""
|
|
131
|
+
with get_universe_session() as session:
|
|
132
|
+
data_descriptors = _find_data_descriptors_in_universe(data_descriptor_id,
|
|
133
|
+
session,
|
|
134
|
+
None)
|
|
135
|
+
if data_descriptors:
|
|
136
|
+
data_descriptor = data_descriptors[0]
|
|
137
|
+
result = _get_all_terms_in_data_descriptor(data_descriptor)
|
|
138
|
+
else:
|
|
139
|
+
result = list()
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def find_data_descriptors_in_universe(data_descriptor_id: str,
|
|
144
|
+
settings: SearchSettings|None = None) \
|
|
145
|
+
-> list[dict]:
|
|
146
|
+
"""
|
|
147
|
+
Finds one or more data descriptor of the universe, based on the specified search settings.
|
|
148
|
+
The given `data_descriptor_id` is searched according to the search type specified in
|
|
149
|
+
the parameter `settings`,
|
|
150
|
+
which allows a flexible matching (e.g., `LIKE` may return multiple results).
|
|
151
|
+
If the parameter `settings` is `None`, this function performs an exact match on
|
|
152
|
+
the `data_descriptor_id`.
|
|
153
|
+
If the provided `data_descriptor_id` is not found, the function returns an empty list.
|
|
154
|
+
|
|
155
|
+
Behavior based on search type:
|
|
156
|
+
- `EXACT` and absence of `settings`: returns zero or one data descriptor context in the list.
|
|
157
|
+
- `REGEX`, `LIKE`, `STARTS_WITH` and `ENDS_WITH`: returns zero, one or more
|
|
158
|
+
data descriptor contexts in the list.
|
|
159
|
+
|
|
160
|
+
:param data_descriptor_id: A data descriptor id to be found
|
|
161
|
+
:type data_descriptor_id: str
|
|
162
|
+
:param settings: The search settings
|
|
163
|
+
:type settings: SearchSettings|None
|
|
164
|
+
:returns: A list of data descriptor contexts. Returns an empty list if no matches are found.
|
|
165
|
+
:rtype: list[dict]
|
|
166
|
+
"""
|
|
167
|
+
result = list()
|
|
168
|
+
with get_universe_session() as session:
|
|
169
|
+
data_descriptors = _find_data_descriptors_in_universe(data_descriptor_id,
|
|
170
|
+
session,
|
|
171
|
+
settings)
|
|
172
|
+
for data_descriptor in data_descriptors:
|
|
173
|
+
result.append(data_descriptor.context)
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _get_all_data_descriptors_in_universe(session: Session) -> Sequence[DataDescriptor]:
|
|
178
|
+
statement = select(DataDescriptor)
|
|
179
|
+
data_descriptors = session.exec(statement)
|
|
180
|
+
result = data_descriptors.all()
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_all_data_descriptors_in_universe() -> list[str]:
|
|
185
|
+
"""
|
|
186
|
+
Gets all the data descriptors of the universe.
|
|
187
|
+
|
|
188
|
+
:returns: A list of data descriptor ids.
|
|
189
|
+
:rtype: list[str]
|
|
190
|
+
"""
|
|
191
|
+
result = list()
|
|
192
|
+
with get_universe_session() as session:
|
|
193
|
+
data_descriptors = _get_all_data_descriptors_in_universe(session)
|
|
194
|
+
for data_descriptor in data_descriptors:
|
|
195
|
+
result.append(data_descriptor.id)
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_all_terms_in_universe() -> list[BaseModel]:
|
|
200
|
+
"""
|
|
201
|
+
Gets all the terms of the universe.
|
|
202
|
+
Terms are unique within a data descriptor but may have some synonyms in the universe.
|
|
203
|
+
|
|
204
|
+
:returns: A list of Pydantic term instances.
|
|
205
|
+
:rtype: list[BaseModel]
|
|
206
|
+
"""
|
|
207
|
+
result = list()
|
|
208
|
+
with get_universe_session() as session:
|
|
209
|
+
data_descriptors = _get_all_data_descriptors_in_universe(session)
|
|
210
|
+
for data_descriptor in data_descriptors:
|
|
211
|
+
# Term may have some synonyms within the whole universe.
|
|
212
|
+
terms = _get_all_terms_in_data_descriptor(data_descriptor)
|
|
213
|
+
result.extend(terms)
|
|
214
|
+
return result
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
print(find_terms_in_data_descriptor('institution', 'ipsl'))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from esgvoc.apps.drs.models import (DrsType,
|
|
2
|
+
DrsPartType,
|
|
3
|
+
DrsConstant,
|
|
4
|
+
DrsCollection,
|
|
5
|
+
DrsPart,
|
|
6
|
+
DrsSpecification,
|
|
7
|
+
ProjectSpecs)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = ["DrsType",
|
|
11
|
+
"DrsPartType",
|
|
12
|
+
"DrsConstant",
|
|
13
|
+
"DrsCollection",
|
|
14
|
+
"DrsPart",
|
|
15
|
+
"DrsSpecification",
|
|
16
|
+
"ProjectSpecs"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Annotated, Literal
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DrsType(str, Enum):
|
|
8
|
+
directory = "directory"
|
|
9
|
+
filename = "filename"
|
|
10
|
+
dataset_id = "dataset_id"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DrsPartType(str, Enum):
|
|
14
|
+
constant = "constant"
|
|
15
|
+
collection = "collection"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DrsConstant(BaseModel):
|
|
19
|
+
value: str
|
|
20
|
+
kind: Literal[DrsPartType.constant] = DrsPartType.constant
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DrsCollection(BaseModel):
|
|
24
|
+
collection_id: str
|
|
25
|
+
is_required: bool
|
|
26
|
+
kind: Literal[DrsPartType.collection] = DrsPartType.collection
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
DrsPart = Annotated[DrsConstant | DrsCollection, Field(discriminator="kind")]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DrsSpecification(BaseModel):
|
|
33
|
+
type: DrsType
|
|
34
|
+
separator: str
|
|
35
|
+
properties: dict|None = None
|
|
36
|
+
parts: list[DrsPart]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ProjectSpecs(BaseModel):
|
|
40
|
+
project_id: str
|
|
41
|
+
description: str
|
|
42
|
+
drs_specs: list[DrsSpecification]
|
|
43
|
+
model_config = ConfigDict(extra = "allow")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from esgvoc.apps.drs.models import ProjectSpecs
|
|
4
|
+
import esgvoc.api.projects as projects
|
|
5
|
+
|
|
6
|
+
_LOGGER = logging.getLogger("drs")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_project_specs(project_id: str) -> ProjectSpecs:
|
|
10
|
+
project_specs = projects.find_project(project_id)
|
|
11
|
+
if not project_specs:
|
|
12
|
+
msg = f'Unable to find project {project_id}'
|
|
13
|
+
_LOGGER.fatal(msg)
|
|
14
|
+
raise ValueError(msg)
|
|
15
|
+
try:
|
|
16
|
+
result = ProjectSpecs(**project_specs)
|
|
17
|
+
except Exception as e:
|
|
18
|
+
msg = f'Unable to read specs in project {project_id}'
|
|
19
|
+
_LOGGER.fatal(msg)
|
|
20
|
+
raise RuntimeError(msg) from e
|
|
21
|
+
return result
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
drs_specs = parse_project_specs('cmip6plus').drs_specs
|
|
26
|
+
print(drs_specs[1])
|
|
27
|
+
|
esgvoc/cli/config.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
|
|
2
|
+
import json
|
|
3
|
+
from rich.syntax import Syntax
|
|
4
|
+
import typer
|
|
5
|
+
from esgvoc.core.service.settings import SETTINGS_FILE, ServiceSettings, load_settings
|
|
6
|
+
from rich import print
|
|
7
|
+
import toml
|
|
8
|
+
|
|
9
|
+
app = typer.Typer()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_nested_value(settings_dict: dict, key_path: str):
|
|
13
|
+
"""Navigate through nested dictionary keys using dot-separated key paths."""
|
|
14
|
+
keys = key_path.split(".")
|
|
15
|
+
value = settings_dict
|
|
16
|
+
for key in keys:
|
|
17
|
+
value = value[key]
|
|
18
|
+
return value
|
|
19
|
+
|
|
20
|
+
def set_nested_value(settings_dict: dict, key_path: str, new_value):
|
|
21
|
+
"""Set a value in a nested dictionary using a dot-separated key path."""
|
|
22
|
+
keys = key_path.split(".")
|
|
23
|
+
sub_dict = settings_dict
|
|
24
|
+
for key in keys[:-1]:
|
|
25
|
+
sub_dict = sub_dict[key]
|
|
26
|
+
sub_dict[keys[-1]] = new_value
|
|
27
|
+
return settings_dict
|
|
28
|
+
|
|
29
|
+
@app.command()
|
|
30
|
+
def config(key: str |None = typer.Argument(None), value: str|None = typer.Argument(None)):
|
|
31
|
+
"""
|
|
32
|
+
Manage configuration settings.
|
|
33
|
+
|
|
34
|
+
- With no arguments: display all settings.
|
|
35
|
+
- With one argument (key): display the value of the key.
|
|
36
|
+
- With two arguments (key and value): modify the key's value and save.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
settings = load_settings()
|
|
40
|
+
if key is None:
|
|
41
|
+
# No key provided, print all settings
|
|
42
|
+
# typer.echo(settings.model_dump())
|
|
43
|
+
syntax = Syntax(toml.dumps(settings.model_dump()), "toml")
|
|
44
|
+
print(syntax)
|
|
45
|
+
return
|
|
46
|
+
if value is None:
|
|
47
|
+
# Key provided but no value, print the specific key's value
|
|
48
|
+
try:
|
|
49
|
+
selected_value = get_nested_value(json.loads(settings.model_dump_json()),key)
|
|
50
|
+
typer.echo(selected_value)
|
|
51
|
+
except KeyError:
|
|
52
|
+
try:
|
|
53
|
+
selected_value = get_nested_value(json.loads(settings.model_dump_json()),"projects."+key)
|
|
54
|
+
typer.echo(selected_value)
|
|
55
|
+
return
|
|
56
|
+
except KeyError:
|
|
57
|
+
pass
|
|
58
|
+
typer.echo(f"Key '{key}' not found in settings.")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
# Modify the key's value
|
|
62
|
+
try :
|
|
63
|
+
selected_value = get_nested_value(json.loads(settings.model_dump_json()),key)
|
|
64
|
+
except Exception:
|
|
65
|
+
key = "projects."+key
|
|
66
|
+
try :
|
|
67
|
+
selected_value = get_nested_value(json.loads(settings.model_dump_json()),key)
|
|
68
|
+
if selected_value:
|
|
69
|
+
new_settings_dict = set_nested_value(json.loads(settings.model_dump_json()),key, value )
|
|
70
|
+
new_settings = ServiceSettings(**new_settings_dict)
|
|
71
|
+
new_settings.save_to_file(str(SETTINGS_FILE)) #TODO improved that .. remove SETTINGS_FILE dependancy
|
|
72
|
+
# save_settings(new_settings)
|
|
73
|
+
typer.echo(f"New settings {new_settings.model_dump_json(indent=4)}")
|
|
74
|
+
typer.echo(f"Updated '{key}' to '{value}'.")
|
|
75
|
+
else:
|
|
76
|
+
typer.echo(f"Key '{key}' not found in settings.")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
typer.echo(f"Error updating settings: {e}")
|
|
79
|
+
|