dapla-toolbelt-metadata 0.4.1__py3-none-any.whl → 0.5.0__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 dapla-toolbelt-metadata might be problematic. Click here for more details.
- dapla_metadata/__init__.py +7 -0
- dapla_metadata/dapla/__init__.py +1 -0
- dapla_metadata/{_shared → dapla}/user_info.py +55 -8
- dapla_metadata/datasets/code_list.py +1 -1
- dapla_metadata/datasets/core.py +1 -1
- dapla_metadata/datasets/dataset_parser.py +1 -1
- dapla_metadata/datasets/model_backwards_compatibility.py +6 -6
- dapla_metadata/datasets/model_validation.py +2 -2
- dapla_metadata/datasets/utility/constants.py +1 -0
- dapla_metadata/datasets/utility/enums.py +1 -1
- dapla_metadata/datasets/utility/utils.py +7 -11
- dapla_metadata/variable_definitions/__init__.py +5 -3
- dapla_metadata/variable_definitions/{generated → _generated}/.openapi-generator/FILES +0 -5
- dapla_metadata/variable_definitions/_generated/.openapi-generator/VERSION +1 -0
- dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/__init__.py +0 -5
- dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/api/__init__.py +0 -1
- dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/api/data_migration_api.py +2 -2
- dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/api/draft_variable_definitions_api.py +14 -14
- dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/api/patches_api.py +15 -15
- dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/api/validity_periods_api.py +8 -281
- dapla_metadata/variable_definitions/{generated/vardef_client/api/public_api.py → _generated/vardef_client/api/variable_definitions_api.py} +73 -358
- dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/__init__.py +2 -6
- dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/complete_response.py +8 -32
- dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/contact.py +2 -2
- dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/draft.py +8 -23
- dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/language_string_type.py +7 -6
- dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/owner.py +2 -2
- dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/patch.py +16 -61
- dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/problem.py +2 -2
- dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/update_draft.py +22 -55
- dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/validity_period.py +14 -48
- dapla_metadata/variable_definitions/_generated/vardef_client/models/variable_status.py +33 -0
- dapla_metadata/variable_definitions/_generated/vardef_client/py.typed +0 -0
- dapla_metadata/variable_definitions/_utils/__init__.py +1 -0
- dapla_metadata/variable_definitions/{_client.py → _utils/_client.py} +5 -3
- dapla_metadata/variable_definitions/{config.py → _utils/config.py} +25 -1
- dapla_metadata/variable_definitions/_utils/constants.py +41 -0
- dapla_metadata/variable_definitions/_utils/descriptions.py +86 -0
- dapla_metadata/variable_definitions/_utils/files.py +273 -0
- dapla_metadata/variable_definitions/_utils/template_files.py +112 -0
- dapla_metadata/variable_definitions/_utils/variable_definition_files.py +93 -0
- dapla_metadata/variable_definitions/exceptions.py +141 -11
- dapla_metadata/variable_definitions/resources/vardef_model_descriptions_nb.yaml +63 -0
- dapla_metadata/variable_definitions/vardef.py +131 -10
- dapla_metadata/variable_definitions/variable_definition.py +241 -43
- {dapla_toolbelt_metadata-0.4.1.dist-info → dapla_toolbelt_metadata-0.5.0.dist-info}/METADATA +5 -7
- dapla_toolbelt_metadata-0.5.0.dist-info/RECORD +84 -0
- {dapla_toolbelt_metadata-0.4.1.dist-info → dapla_toolbelt_metadata-0.5.0.dist-info}/WHEEL +1 -1
- dapla_metadata/variable_definitions/generated/.openapi-generator/VERSION +0 -1
- dapla_metadata/variable_definitions/generated/vardef_client/api/variable_definitions_api.py +0 -1205
- dapla_metadata/variable_definitions/generated/vardef_client/models/klass_reference.py +0 -99
- dapla_metadata/variable_definitions/generated/vardef_client/models/rendered_contact.py +0 -92
- dapla_metadata/variable_definitions/generated/vardef_client/models/rendered_variable_definition.py +0 -235
- dapla_metadata/variable_definitions/generated/vardef_client/models/supported_languages.py +0 -33
- dapla_metadata/variable_definitions/generated/vardef_client/models/variable_status.py +0 -33
- dapla_toolbelt_metadata-0.4.1.dist-info/RECORD +0 -80
- /dapla_metadata/{variable_definitions/generated/vardef_client → _shared}/py.typed +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/.openapi-generator-ignore +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/README.md +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/__init__.py +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/api_client.py +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/api_response.py +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/configuration.py +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/CompleteResponse.md +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/Contact.md +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/DataMigrationApi.md +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/Draft.md +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/DraftVariableDefinitionsApi.md +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/LanguageStringType.md +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/Owner.md +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/Patch.md +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/PatchesApi.md +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/PublicApi.md +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/SupportedLanguages.md +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/UpdateDraft.md +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/ValidityPeriod.md +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/ValidityPeriodsApi.md +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/VariableDefinitionsApi.md +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/VariableStatus.md +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/exceptions.py +0 -0
- /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/rest.py +0 -0
- {dapla_toolbelt_metadata-0.4.1.dist-info → dapla_toolbelt_metadata-0.5.0.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Lower level file utilities."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
import pytz
|
|
10
|
+
from pydantic.config import JsonDict
|
|
11
|
+
from ruamel.yaml import YAML
|
|
12
|
+
from ruamel.yaml import CommentedMap
|
|
13
|
+
|
|
14
|
+
from dapla_metadata.variable_definitions._generated.vardef_client.models.complete_response import (
|
|
15
|
+
CompleteResponse,
|
|
16
|
+
)
|
|
17
|
+
from dapla_metadata.variable_definitions._generated.vardef_client.models.variable_status import (
|
|
18
|
+
VariableStatus,
|
|
19
|
+
)
|
|
20
|
+
from dapla_metadata.variable_definitions._utils import config
|
|
21
|
+
from dapla_metadata.variable_definitions._utils.constants import (
|
|
22
|
+
MACHINE_GENERATED_FIELDS,
|
|
23
|
+
)
|
|
24
|
+
from dapla_metadata.variable_definitions._utils.constants import NORWEGIAN_DESCRIPTIONS
|
|
25
|
+
from dapla_metadata.variable_definitions._utils.constants import OPTIONAL_FIELD
|
|
26
|
+
from dapla_metadata.variable_definitions._utils.constants import OWNER_FIELD_NAME
|
|
27
|
+
from dapla_metadata.variable_definitions._utils.constants import REQUIRED_FIELD
|
|
28
|
+
from dapla_metadata.variable_definitions._utils.constants import (
|
|
29
|
+
TEMPLATE_SECTION_HEADER_MACHINE_GENERATED,
|
|
30
|
+
)
|
|
31
|
+
from dapla_metadata.variable_definitions._utils.constants import (
|
|
32
|
+
TEMPLATE_SECTION_HEADER_OWNER,
|
|
33
|
+
)
|
|
34
|
+
from dapla_metadata.variable_definitions._utils.constants import (
|
|
35
|
+
TEMPLATE_SECTION_HEADER_STATUS,
|
|
36
|
+
)
|
|
37
|
+
from dapla_metadata.variable_definitions._utils.constants import (
|
|
38
|
+
VARIABLE_DEFINITIONS_DIR,
|
|
39
|
+
)
|
|
40
|
+
from dapla_metadata.variable_definitions._utils.constants import (
|
|
41
|
+
VARIABLE_STATUS_FIELD_NAME,
|
|
42
|
+
)
|
|
43
|
+
from dapla_metadata.variable_definitions._utils.descriptions import (
|
|
44
|
+
apply_norwegian_descriptions_to_model,
|
|
45
|
+
)
|
|
46
|
+
from dapla_metadata.variable_definitions.exceptions import VardefFileError
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from pydantic import JsonValue
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _create_file_name(
|
|
55
|
+
base_name: str,
|
|
56
|
+
time_object: str,
|
|
57
|
+
short_name: str | None = None,
|
|
58
|
+
variable_definition_id: str | None = None,
|
|
59
|
+
) -> str:
|
|
60
|
+
"""Return file name with dynamic timestamp, and shortname and id if available."""
|
|
61
|
+
return (
|
|
62
|
+
"_".join(
|
|
63
|
+
filter(
|
|
64
|
+
None,
|
|
65
|
+
[
|
|
66
|
+
base_name,
|
|
67
|
+
short_name,
|
|
68
|
+
variable_definition_id,
|
|
69
|
+
time_object,
|
|
70
|
+
],
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
+ ".yaml"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _get_current_time() -> str:
|
|
78
|
+
"""Return a string format date now for filename."""
|
|
79
|
+
timezone = pytz.timezone("Europe/Oslo")
|
|
80
|
+
current_datetime = datetime.now(timezone).strftime("%Y-%m-%dT%H-%M-%S")
|
|
81
|
+
return str(current_datetime)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _get_workspace_dir() -> Path:
|
|
85
|
+
"""Determine the workspace directory."""
|
|
86
|
+
workspace_dir = config.get_workspace_dir()
|
|
87
|
+
|
|
88
|
+
if workspace_dir is None:
|
|
89
|
+
msg = "WORKSPACE_DIR is not set. Check your configuration or provide a custom directory."
|
|
90
|
+
raise VardefFileError(msg)
|
|
91
|
+
workspace_dir_path: Path
|
|
92
|
+
if workspace_dir is not None:
|
|
93
|
+
workspace_dir_path = Path(workspace_dir)
|
|
94
|
+
workspace_dir_path.resolve()
|
|
95
|
+
|
|
96
|
+
if not workspace_dir_path.exists():
|
|
97
|
+
msg = f"Directory '{workspace_dir_path}' does not exist."
|
|
98
|
+
raise FileNotFoundError(msg)
|
|
99
|
+
|
|
100
|
+
if not workspace_dir_path.is_dir():
|
|
101
|
+
msg = f"'{workspace_dir_path}' is not a directory."
|
|
102
|
+
raise NotADirectoryError(msg)
|
|
103
|
+
logger.debug("'WORKSPACE_DIR' value: %s", workspace_dir)
|
|
104
|
+
return workspace_dir_path
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _get_variable_definitions_dir():
|
|
108
|
+
"""Get or create the variable definitions directory inside the workspace."""
|
|
109
|
+
workspace_dir = _get_workspace_dir()
|
|
110
|
+
folder_path = workspace_dir / VARIABLE_DEFINITIONS_DIR
|
|
111
|
+
folder_path.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
return folder_path
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _populate_commented_map(
|
|
116
|
+
field_name: str,
|
|
117
|
+
value: str,
|
|
118
|
+
commented_map: CommentedMap,
|
|
119
|
+
model_instance: CompleteResponse,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Add data to a CommentedMap."""
|
|
122
|
+
commented_map[field_name] = value
|
|
123
|
+
field = model_instance.model_fields[field_name]
|
|
124
|
+
description: JsonValue = cast(
|
|
125
|
+
JsonDict,
|
|
126
|
+
field.json_schema_extra,
|
|
127
|
+
)[NORWEGIAN_DESCRIPTIONS]
|
|
128
|
+
if description is not None:
|
|
129
|
+
new_description = (
|
|
130
|
+
(REQUIRED_FIELD if field.is_required() else OPTIONAL_FIELD)
|
|
131
|
+
+ "\n"
|
|
132
|
+
+ str(description)
|
|
133
|
+
)
|
|
134
|
+
commented_map.yaml_set_comment_before_after_key(
|
|
135
|
+
field_name,
|
|
136
|
+
before=new_description,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _validate_and_create_directory(custom_directory: Path) -> Path:
|
|
141
|
+
"""Ensure that the given path is a valid directory, creating it if necessary.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
custom_directory (Path): The target directory path.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Path: The resolved absolute path of the directory.
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
ValueError: If the provided path has a file suffix, indicating a file name instead of a directory.
|
|
151
|
+
NotADirectoryError: If the path exists but is not a directory.
|
|
152
|
+
PermissionError: If there are insufficient permissions to create the directory.
|
|
153
|
+
OSError: If an OS-related error occurs while creating the directory.
|
|
154
|
+
"""
|
|
155
|
+
custom_directory = Path(custom_directory).resolve()
|
|
156
|
+
|
|
157
|
+
if custom_directory.suffix:
|
|
158
|
+
msg = f"Expected a directory but got a file name: %{custom_directory.name}"
|
|
159
|
+
raise ValueError(msg)
|
|
160
|
+
|
|
161
|
+
if custom_directory.exists() and not custom_directory.is_dir():
|
|
162
|
+
msg = f"Path exists but is not a directory: {custom_directory}"
|
|
163
|
+
raise NotADirectoryError(msg)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
custom_directory.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
except PermissionError as e:
|
|
168
|
+
msg = f"Insufficient permissions to create directory: {custom_directory}"
|
|
169
|
+
raise PermissionError(msg) from e
|
|
170
|
+
except OSError as e:
|
|
171
|
+
msg = f"Failed to create directory {custom_directory}: {e!s}"
|
|
172
|
+
raise OSError(msg) from e
|
|
173
|
+
|
|
174
|
+
return custom_directory
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _configure_yaml() -> YAML:
|
|
178
|
+
yaml = YAML() # Use ruamel.yaml library
|
|
179
|
+
yaml.default_flow_style = False # Ensures pretty YAML formatting
|
|
180
|
+
|
|
181
|
+
yaml.representer.add_representer(
|
|
182
|
+
VariableStatus,
|
|
183
|
+
lambda dumper, data: dumper.represent_scalar(
|
|
184
|
+
"tag:yaml.org,2002:str",
|
|
185
|
+
data.value,
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return yaml
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _model_to_yaml_with_comments(
|
|
193
|
+
model_instance: CompleteResponse,
|
|
194
|
+
file_name: str,
|
|
195
|
+
start_comment: str,
|
|
196
|
+
custom_directory: Path | None = None,
|
|
197
|
+
) -> Path:
|
|
198
|
+
"""Convert a model instance to a structured YAML file with Norwegian descriptions as comments.
|
|
199
|
+
|
|
200
|
+
Adds Norwegian descriptions to the model, organizes fields into sections, and saves
|
|
201
|
+
the YAML file with a structured format and timestamped filename.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
model_instance: The model instance to convert.
|
|
205
|
+
file_name: Name of the generated YAML file.
|
|
206
|
+
start_comment: Comment at the top of the file.
|
|
207
|
+
custom_directory: Optional directory to save the file.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Path: The file path of the generated YAML file.
|
|
211
|
+
"""
|
|
212
|
+
yaml = _configure_yaml()
|
|
213
|
+
|
|
214
|
+
from dapla_metadata.variable_definitions.variable_definition import (
|
|
215
|
+
VariableDefinition,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Apply new fields to model
|
|
219
|
+
apply_norwegian_descriptions_to_model(VariableDefinition)
|
|
220
|
+
|
|
221
|
+
# Convert Pydantic model instance to dictionary
|
|
222
|
+
data = model_instance.model_dump(
|
|
223
|
+
serialize_as_any=True,
|
|
224
|
+
warnings="error",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# One CommentMap for each section in the yaml file
|
|
228
|
+
machine_generated_map = CommentedMap()
|
|
229
|
+
commented_map = CommentedMap()
|
|
230
|
+
status_map = CommentedMap()
|
|
231
|
+
owner_map = CommentedMap()
|
|
232
|
+
|
|
233
|
+
# Loop through all fields in the model and populate the commented maps
|
|
234
|
+
for field_name, value in data.items():
|
|
235
|
+
if field_name == VARIABLE_STATUS_FIELD_NAME:
|
|
236
|
+
_populate_commented_map(field_name, value, status_map, model_instance)
|
|
237
|
+
elif field_name == OWNER_FIELD_NAME:
|
|
238
|
+
_populate_commented_map(field_name, value, owner_map, model_instance)
|
|
239
|
+
elif field_name in MACHINE_GENERATED_FIELDS:
|
|
240
|
+
_populate_commented_map(
|
|
241
|
+
field_name,
|
|
242
|
+
value,
|
|
243
|
+
machine_generated_map,
|
|
244
|
+
model_instance,
|
|
245
|
+
)
|
|
246
|
+
elif field_name not in {VARIABLE_STATUS_FIELD_NAME, OWNER_FIELD_NAME}:
|
|
247
|
+
_populate_commented_map(field_name, value, commented_map, model_instance)
|
|
248
|
+
|
|
249
|
+
base_path = (
|
|
250
|
+
_get_variable_definitions_dir()
|
|
251
|
+
if custom_directory is None
|
|
252
|
+
else _validate_and_create_directory(custom_directory)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
file_path = base_path / file_name
|
|
256
|
+
|
|
257
|
+
# It is important to preserve the order of the yaml dump operations when writing to file
|
|
258
|
+
# so that the file is predictable for the user
|
|
259
|
+
with file_path.open("w", encoding="utf-8") as file:
|
|
260
|
+
commented_map.yaml_set_start_comment(start_comment)
|
|
261
|
+
yaml.dump(commented_map, file)
|
|
262
|
+
|
|
263
|
+
status_map.yaml_set_start_comment(TEMPLATE_SECTION_HEADER_STATUS)
|
|
264
|
+
yaml.dump(status_map, file)
|
|
265
|
+
|
|
266
|
+
owner_map.yaml_set_start_comment(TEMPLATE_SECTION_HEADER_OWNER)
|
|
267
|
+
yaml.dump(owner_map, file)
|
|
268
|
+
|
|
269
|
+
machine_generated_map.yaml_set_start_comment(
|
|
270
|
+
TEMPLATE_SECTION_HEADER_MACHINE_GENERATED,
|
|
271
|
+
)
|
|
272
|
+
yaml.dump(machine_generated_map, file)
|
|
273
|
+
return file_path
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from dapla_metadata.variable_definitions._generated.vardef_client.models.complete_response import (
|
|
5
|
+
CompleteResponse,
|
|
6
|
+
)
|
|
7
|
+
from dapla_metadata.variable_definitions._generated.vardef_client.models.contact import (
|
|
8
|
+
Contact,
|
|
9
|
+
)
|
|
10
|
+
from dapla_metadata.variable_definitions._generated.vardef_client.models.language_string_type import (
|
|
11
|
+
LanguageStringType,
|
|
12
|
+
)
|
|
13
|
+
from dapla_metadata.variable_definitions._generated.vardef_client.models.owner import (
|
|
14
|
+
Owner,
|
|
15
|
+
)
|
|
16
|
+
from dapla_metadata.variable_definitions._generated.vardef_client.models.variable_status import (
|
|
17
|
+
VariableStatus,
|
|
18
|
+
)
|
|
19
|
+
from dapla_metadata.variable_definitions._utils.constants import DEFAULT_DATE
|
|
20
|
+
from dapla_metadata.variable_definitions._utils.constants import TEMPLATE_HEADER
|
|
21
|
+
from dapla_metadata.variable_definitions._utils.files import _create_file_name
|
|
22
|
+
from dapla_metadata.variable_definitions._utils.files import _get_current_time
|
|
23
|
+
from dapla_metadata.variable_definitions._utils.files import (
|
|
24
|
+
_get_variable_definitions_dir,
|
|
25
|
+
)
|
|
26
|
+
from dapla_metadata.variable_definitions._utils.files import (
|
|
27
|
+
_model_to_yaml_with_comments,
|
|
28
|
+
)
|
|
29
|
+
from dapla_metadata.variable_definitions._utils.files import logger
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from dapla_metadata.variable_definitions.variable_definition import (
|
|
33
|
+
VariableDefinition,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_default_template() -> "VariableDefinition":
|
|
38
|
+
from dapla_metadata.variable_definitions.variable_definition import (
|
|
39
|
+
VariableDefinition,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return VariableDefinition(
|
|
43
|
+
name=LanguageStringType(
|
|
44
|
+
nb="default navn",
|
|
45
|
+
nn="default namn",
|
|
46
|
+
en="default name",
|
|
47
|
+
),
|
|
48
|
+
short_name="default_kortnavn",
|
|
49
|
+
definition=LanguageStringType(
|
|
50
|
+
nb="default definisjon",
|
|
51
|
+
nn="default definisjon",
|
|
52
|
+
en="default definition",
|
|
53
|
+
),
|
|
54
|
+
classification_reference="class_id",
|
|
55
|
+
valid_from=DEFAULT_DATE,
|
|
56
|
+
unit_types=["00"],
|
|
57
|
+
subject_fields=["aa"],
|
|
58
|
+
contains_special_categories_of_personal_data=False,
|
|
59
|
+
variable_status=VariableStatus.DRAFT.value,
|
|
60
|
+
owner=Owner(team="default team", groups=["default group"]),
|
|
61
|
+
contact=Contact(
|
|
62
|
+
title=LanguageStringType(
|
|
63
|
+
nb="default tittel",
|
|
64
|
+
nn="default tittel",
|
|
65
|
+
en="default title",
|
|
66
|
+
),
|
|
67
|
+
email="default@ssb.no",
|
|
68
|
+
),
|
|
69
|
+
id="",
|
|
70
|
+
patch_id=0,
|
|
71
|
+
created_at=DEFAULT_DATE,
|
|
72
|
+
created_by="",
|
|
73
|
+
last_updated_at=DEFAULT_DATE,
|
|
74
|
+
last_updated_by="",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def create_template_yaml(
|
|
79
|
+
model_instance: CompleteResponse | None = None,
|
|
80
|
+
custom_directory: Path | None = None,
|
|
81
|
+
) -> Path:
|
|
82
|
+
"""Creates a template yaml file for a new variable definition."""
|
|
83
|
+
if model_instance is None:
|
|
84
|
+
model_instance = _get_default_template()
|
|
85
|
+
file_name = _create_file_name(
|
|
86
|
+
"variable_definition_template",
|
|
87
|
+
_get_current_time(),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
file_path = _model_to_yaml_with_comments(
|
|
91
|
+
model_instance,
|
|
92
|
+
file_name,
|
|
93
|
+
TEMPLATE_HEADER,
|
|
94
|
+
custom_directory=custom_directory,
|
|
95
|
+
)
|
|
96
|
+
logger.debug("Created %s", file_path)
|
|
97
|
+
return file_path
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _find_latest_template_file(directory: Path | None = None) -> Path | None:
|
|
101
|
+
def _filter_template_file(path: Path) -> bool:
|
|
102
|
+
return "variable_definition_template" in path.stem and path.suffix == ".yaml"
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
return sorted(
|
|
106
|
+
filter(
|
|
107
|
+
_filter_template_file,
|
|
108
|
+
(directory or _get_variable_definitions_dir()).iterdir(),
|
|
109
|
+
),
|
|
110
|
+
)[-1]
|
|
111
|
+
except IndexError:
|
|
112
|
+
return None
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Utilities for writing and reading existing variable definition files."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from os import PathLike
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TypeVar
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from ruamel.yaml import YAML
|
|
10
|
+
|
|
11
|
+
from dapla_metadata.variable_definitions._generated.vardef_client.models.complete_response import (
|
|
12
|
+
CompleteResponse,
|
|
13
|
+
)
|
|
14
|
+
from dapla_metadata.variable_definitions._utils.constants import HEADER
|
|
15
|
+
from dapla_metadata.variable_definitions._utils.files import _create_file_name
|
|
16
|
+
from dapla_metadata.variable_definitions._utils.files import _get_current_time
|
|
17
|
+
from dapla_metadata.variable_definitions._utils.files import (
|
|
18
|
+
_model_to_yaml_with_comments,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
T = TypeVar("T", bound=BaseModel)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_variable_yaml(
|
|
28
|
+
model_instance: CompleteResponse,
|
|
29
|
+
custom_directory: Path | None = None,
|
|
30
|
+
) -> Path:
|
|
31
|
+
"""Creates a yaml file for an existing variable definition."""
|
|
32
|
+
file_name = _create_file_name(
|
|
33
|
+
"variable_definition",
|
|
34
|
+
_get_current_time(),
|
|
35
|
+
model_instance.short_name,
|
|
36
|
+
model_instance.id,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return _model_to_yaml_with_comments(
|
|
40
|
+
model_instance,
|
|
41
|
+
file_name,
|
|
42
|
+
HEADER,
|
|
43
|
+
custom_directory=custom_directory,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _read_variable_definition_file(file_path: Path) -> dict:
|
|
48
|
+
yaml = YAML()
|
|
49
|
+
|
|
50
|
+
logger.debug("Full path to variable definition file %s", file_path)
|
|
51
|
+
logger.info("Reading from '%s'", file_path.name)
|
|
52
|
+
with file_path.open(encoding="utf-8") as f:
|
|
53
|
+
return yaml.load(f)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _read_file_to_model(
|
|
57
|
+
file_path: PathLike[str] | None,
|
|
58
|
+
model_class: type[T],
|
|
59
|
+
) -> T:
|
|
60
|
+
"""Read from a variable definition file into the given Pydantic model.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
file_path (PathLike[str]): The path to the file to read in.
|
|
64
|
+
model_class (type[T]): The model to instantiate. Must inherit from Pydantic's BaseModel.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
TypeError: If no file path could be deduced.
|
|
68
|
+
FileNotFoundError: If we could not instantiate the model.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
T: BaseModel: The instantiated Pydantic model
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
file_path = Path(
|
|
75
|
+
# type incongruence (i.e. None) is handled by catching the exception
|
|
76
|
+
file_path, # type: ignore [arg-type]
|
|
77
|
+
)
|
|
78
|
+
except TypeError as e:
|
|
79
|
+
msg = "Could not deduce a path to the file. Please supply a path to the yaml file you wish to submit with the `file_path` parameter."
|
|
80
|
+
raise FileNotFoundError(
|
|
81
|
+
msg,
|
|
82
|
+
) from e
|
|
83
|
+
model = model_class.from_dict( # type:ignore [attr-defined]
|
|
84
|
+
_read_variable_definition_file(
|
|
85
|
+
file_path,
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if model is None:
|
|
90
|
+
msg = f"Could not read data from {file_path}"
|
|
91
|
+
raise FileNotFoundError(msg)
|
|
92
|
+
|
|
93
|
+
return model
|
|
@@ -2,13 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from functools import wraps
|
|
5
|
+
from http import HTTPStatus
|
|
6
|
+
from types import MappingProxyType
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
import urllib3
|
|
9
|
+
from pytz import UnknownTimeZoneError
|
|
10
|
+
from ruamel.yaml.error import YAMLError
|
|
11
|
+
|
|
12
|
+
from dapla_metadata.variable_definitions._generated.vardef_client.exceptions import (
|
|
7
13
|
OpenApiException,
|
|
8
14
|
)
|
|
15
|
+
from dapla_metadata.variable_definitions._generated.vardef_client.exceptions import (
|
|
16
|
+
UnauthorizedException,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Use MappingProxyType so the dict is immutable
|
|
20
|
+
STATUS_EXPLANATIONS: MappingProxyType[HTTPStatus | None, str] = MappingProxyType(
|
|
21
|
+
{
|
|
22
|
+
HTTPStatus.BAD_REQUEST: "There was a problem with the supplied data. Please review the data you supplied based on the details below.",
|
|
23
|
+
HTTPStatus.UNAUTHORIZED: "There is a problem with your token, it may have expired. Try logging out, logging in and trying again.",
|
|
24
|
+
HTTPStatus.FORBIDDEN: "Forbidden. You don't have access to this, possibly because your team is not an owner of this variable definition.",
|
|
25
|
+
HTTPStatus.NOT_FOUND: "The variable definition was not found, check that the `short_name` or `id` is correct and that the variable exists.",
|
|
26
|
+
HTTPStatus.METHOD_NOT_ALLOWED: "That won't work here. The status of your variable definition likely doesn't allow for this to happen.",
|
|
27
|
+
HTTPStatus.CONFLICT: "There was a conflict with existing data. Please change your data and try again.",
|
|
28
|
+
None: "",
|
|
29
|
+
},
|
|
30
|
+
)
|
|
9
31
|
|
|
10
32
|
|
|
11
|
-
class
|
|
33
|
+
class VardefClientError(Exception):
|
|
12
34
|
"""Custom exception to represent errors encountered in the Vardef client.
|
|
13
35
|
|
|
14
36
|
This exception extracts and formats error details from a JSON response body
|
|
@@ -33,24 +55,37 @@ class VardefClientException(OpenApiException):
|
|
|
33
55
|
response_body (str): The raw response body string, stored for
|
|
34
56
|
debugging purposes.
|
|
35
57
|
"""
|
|
58
|
+
self.status: int | None = None
|
|
59
|
+
self.detail: str = ""
|
|
36
60
|
try:
|
|
37
61
|
data = json.loads(response_body)
|
|
38
|
-
self.status = data.get("status"
|
|
62
|
+
self.status = data.get("status")
|
|
39
63
|
if data.get("title") == "Constraint Violation":
|
|
40
64
|
violations = data.get("violations", [])
|
|
41
|
-
self.detail = "".join(
|
|
42
|
-
f"
|
|
65
|
+
self.detail = "\n" + "\n".join(
|
|
66
|
+
f"{violation.get('field', 'Unknown field')}: {violation.get('message', 'No message provided')}"
|
|
43
67
|
for violation in violations
|
|
44
68
|
)
|
|
45
|
-
|
|
46
69
|
else:
|
|
47
|
-
self.detail = data.get("detail"
|
|
70
|
+
self.detail = data.get("detail")
|
|
71
|
+
|
|
72
|
+
if self.detail:
|
|
73
|
+
self.detail = f"\nDetail: {self.detail}"
|
|
48
74
|
self.response_body = response_body
|
|
49
75
|
except (json.JSONDecodeError, TypeError):
|
|
50
|
-
self.status = "Unknown"
|
|
51
76
|
self.detail = "Could not decode error response from API"
|
|
52
|
-
|
|
53
|
-
super().__init__(
|
|
77
|
+
|
|
78
|
+
super().__init__(
|
|
79
|
+
f"{self._get_status_explanation(self.status)}{self.detail if self.detail else ''}",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def _get_status_explanation(status: int | None) -> str:
|
|
84
|
+
return (
|
|
85
|
+
""
|
|
86
|
+
if not status
|
|
87
|
+
else (STATUS_EXPLANATIONS.get(HTTPStatus(status)) or f"Status {status}:")
|
|
88
|
+
)
|
|
54
89
|
|
|
55
90
|
|
|
56
91
|
def vardef_exception_handler(method): # noqa: ANN201, ANN001
|
|
@@ -60,7 +95,102 @@ def vardef_exception_handler(method): # noqa: ANN201, ANN001
|
|
|
60
95
|
def _impl(self, *method_args, **method_kwargs): # noqa: ANN001, ANN002, ANN003
|
|
61
96
|
try:
|
|
62
97
|
return method(self, *method_args, **method_kwargs)
|
|
98
|
+
except urllib3.exceptions.HTTPError as e:
|
|
99
|
+
# Catch all urllib3 exceptions by catching the base class.
|
|
100
|
+
# These exceptions typically arise from lower level network problems.
|
|
101
|
+
raise VardefClientError(
|
|
102
|
+
json.dumps(
|
|
103
|
+
{
|
|
104
|
+
"status": None,
|
|
105
|
+
"title": "Network problems",
|
|
106
|
+
"detail": f"""There was a network problem when sending the request to the server. Try again shortly.
|
|
107
|
+
Original exception:
|
|
108
|
+
{getattr(e, "message", repr(e))}""",
|
|
109
|
+
},
|
|
110
|
+
),
|
|
111
|
+
) from e
|
|
112
|
+
except UnauthorizedException as e:
|
|
113
|
+
raise VardefClientError(
|
|
114
|
+
json.dumps(
|
|
115
|
+
{
|
|
116
|
+
"status": e.status,
|
|
117
|
+
"title": "Unauthorized",
|
|
118
|
+
"detail": "Unauthorized",
|
|
119
|
+
},
|
|
120
|
+
),
|
|
121
|
+
) from e
|
|
63
122
|
except OpenApiException as e:
|
|
64
|
-
raise
|
|
123
|
+
raise VardefClientError(e.body) from e
|
|
124
|
+
|
|
125
|
+
return _impl
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class VariableNotFoundError(Exception):
|
|
129
|
+
"""Custom exception for when a variable is not found.
|
|
130
|
+
|
|
131
|
+
Attributes:
|
|
132
|
+
message (str): Message describing the error.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def __init__(self, message: str) -> None:
|
|
136
|
+
"""Initialize the VariableNotFoundError.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
message (str): Description of the error.
|
|
140
|
+
"""
|
|
141
|
+
super().__init__(message)
|
|
142
|
+
self.message = message
|
|
143
|
+
|
|
144
|
+
def __str__(self) -> str:
|
|
145
|
+
"""Return the string representation of the exception."""
|
|
146
|
+
return f" {self.message}"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class VardefFileError(Exception):
|
|
150
|
+
"""Custom exception for catching errors related to variable definition file handling.
|
|
151
|
+
|
|
152
|
+
Attributes:
|
|
153
|
+
message (str): Message describing the error.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def __init__(self, message: str, *args) -> None: # noqa: ANN002
|
|
157
|
+
"""Accepting the message and any additional arguments."""
|
|
158
|
+
super().__init__(message, *args)
|
|
159
|
+
self.message = message
|
|
160
|
+
self.args = args
|
|
161
|
+
|
|
162
|
+
def __str__(self) -> str:
|
|
163
|
+
"""Returning a custom string representation of the exception."""
|
|
164
|
+
return f"VardefFileError: {self.message}"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def vardef_file_error_handler(method): # noqa: ANN201, ANN001
|
|
168
|
+
"""Decorator for handling exceptions when generating yaml files for variable definitions."""
|
|
169
|
+
|
|
170
|
+
@wraps(method)
|
|
171
|
+
def _impl(*method_args, **method_kwargs): # noqa: ANN002, ANN003
|
|
172
|
+
try:
|
|
173
|
+
return method(
|
|
174
|
+
*method_args,
|
|
175
|
+
**method_kwargs,
|
|
176
|
+
)
|
|
177
|
+
except FileExistsError as e:
|
|
178
|
+
msg = f"File already exists and can not be saved: {method_kwargs.get('file_path', 'unknown file path')}"
|
|
179
|
+
raise VardefFileError(msg) from e
|
|
180
|
+
except PermissionError as e:
|
|
181
|
+
msg = f"Permission denied for file path when accessing the file. Original error: {e!s}"
|
|
182
|
+
raise VardefFileError(msg) from e
|
|
183
|
+
except UnknownTimeZoneError as e:
|
|
184
|
+
msg = f"Timezone is unknown: {method_kwargs.get('time_zone', 'unknown')}"
|
|
185
|
+
raise VardefFileError(msg) from e
|
|
186
|
+
except YAMLError as e:
|
|
187
|
+
msg = f"Invalid yaml. Please fix the formatting in your yaml file.\nOriginal error:\n{e!s}"
|
|
188
|
+
raise VardefFileError(msg) from e
|
|
189
|
+
except EOFError as e:
|
|
190
|
+
msg = "Unexpected end of file"
|
|
191
|
+
raise VardefFileError(msg) from e
|
|
192
|
+
except NotADirectoryError as e:
|
|
193
|
+
msg = f"Path is not a directory: {method_kwargs.get('file_path', 'unknown file path')}. Original error: {e!s}"
|
|
194
|
+
raise VardefFileError(msg) from e
|
|
65
195
|
|
|
66
196
|
return _impl
|