dapla-toolbelt-metadata 0.4.2__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.

Files changed (81) hide show
  1. dapla_metadata/__init__.py +7 -0
  2. dapla_metadata/dapla/__init__.py +1 -0
  3. dapla_metadata/{_shared → dapla}/user_info.py +55 -8
  4. dapla_metadata/datasets/code_list.py +1 -1
  5. dapla_metadata/datasets/core.py +1 -1
  6. dapla_metadata/datasets/dataset_parser.py +1 -1
  7. dapla_metadata/datasets/model_backwards_compatibility.py +6 -6
  8. dapla_metadata/datasets/model_validation.py +2 -2
  9. dapla_metadata/datasets/utility/constants.py +1 -0
  10. dapla_metadata/datasets/utility/enums.py +1 -1
  11. dapla_metadata/datasets/utility/utils.py +7 -11
  12. dapla_metadata/variable_definitions/__init__.py +5 -3
  13. dapla_metadata/variable_definitions/{generated → _generated}/.openapi-generator/FILES +0 -5
  14. dapla_metadata/variable_definitions/_generated/.openapi-generator/VERSION +1 -0
  15. dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/__init__.py +0 -5
  16. dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/api/__init__.py +0 -1
  17. dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/api/data_migration_api.py +2 -2
  18. dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/api/draft_variable_definitions_api.py +14 -14
  19. dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/api/patches_api.py +15 -15
  20. dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/api/validity_periods_api.py +8 -281
  21. dapla_metadata/variable_definitions/{generated/vardef_client/api/public_api.py → _generated/vardef_client/api/variable_definitions_api.py} +73 -358
  22. dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/__init__.py +2 -6
  23. dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/complete_response.py +8 -32
  24. dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/contact.py +2 -2
  25. dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/draft.py +8 -23
  26. dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/language_string_type.py +7 -6
  27. dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/owner.py +2 -2
  28. dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/patch.py +16 -61
  29. dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/problem.py +2 -2
  30. dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/update_draft.py +22 -55
  31. dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/models/validity_period.py +14 -48
  32. dapla_metadata/variable_definitions/_generated/vardef_client/models/variable_status.py +33 -0
  33. dapla_metadata/variable_definitions/_utils/__init__.py +1 -0
  34. dapla_metadata/variable_definitions/{_client.py → _utils/_client.py} +5 -3
  35. dapla_metadata/variable_definitions/{config.py → _utils/config.py} +25 -1
  36. dapla_metadata/variable_definitions/_utils/constants.py +41 -0
  37. dapla_metadata/variable_definitions/_utils/descriptions.py +86 -0
  38. dapla_metadata/variable_definitions/_utils/files.py +273 -0
  39. dapla_metadata/variable_definitions/_utils/template_files.py +112 -0
  40. dapla_metadata/variable_definitions/_utils/variable_definition_files.py +93 -0
  41. dapla_metadata/variable_definitions/exceptions.py +141 -11
  42. dapla_metadata/variable_definitions/resources/vardef_model_descriptions_nb.yaml +63 -0
  43. dapla_metadata/variable_definitions/vardef.py +131 -10
  44. dapla_metadata/variable_definitions/variable_definition.py +241 -43
  45. {dapla_toolbelt_metadata-0.4.2.dist-info → dapla_toolbelt_metadata-0.5.0.dist-info}/METADATA +5 -7
  46. dapla_toolbelt_metadata-0.5.0.dist-info/RECORD +84 -0
  47. {dapla_toolbelt_metadata-0.4.2.dist-info → dapla_toolbelt_metadata-0.5.0.dist-info}/WHEEL +1 -1
  48. dapla_metadata/variable_definitions/generated/.openapi-generator/VERSION +0 -1
  49. dapla_metadata/variable_definitions/generated/vardef_client/api/variable_definitions_api.py +0 -1205
  50. dapla_metadata/variable_definitions/generated/vardef_client/models/klass_reference.py +0 -99
  51. dapla_metadata/variable_definitions/generated/vardef_client/models/rendered_contact.py +0 -92
  52. dapla_metadata/variable_definitions/generated/vardef_client/models/rendered_variable_definition.py +0 -235
  53. dapla_metadata/variable_definitions/generated/vardef_client/models/supported_languages.py +0 -33
  54. dapla_metadata/variable_definitions/generated/vardef_client/models/variable_status.py +0 -33
  55. dapla_toolbelt_metadata-0.4.2.dist-info/RECORD +0 -81
  56. /dapla_metadata/variable_definitions/{generated → _generated}/.openapi-generator-ignore +0 -0
  57. /dapla_metadata/variable_definitions/{generated → _generated}/README.md +0 -0
  58. /dapla_metadata/variable_definitions/{generated → _generated}/__init__.py +0 -0
  59. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/api_client.py +0 -0
  60. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/api_response.py +0 -0
  61. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/configuration.py +0 -0
  62. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/CompleteResponse.md +0 -0
  63. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/Contact.md +0 -0
  64. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/DataMigrationApi.md +0 -0
  65. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/Draft.md +0 -0
  66. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/DraftVariableDefinitionsApi.md +0 -0
  67. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/LanguageStringType.md +0 -0
  68. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/Owner.md +0 -0
  69. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/Patch.md +0 -0
  70. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/PatchesApi.md +0 -0
  71. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/PublicApi.md +0 -0
  72. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/SupportedLanguages.md +0 -0
  73. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/UpdateDraft.md +0 -0
  74. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/ValidityPeriod.md +0 -0
  75. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/ValidityPeriodsApi.md +0 -0
  76. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/VariableDefinitionsApi.md +0 -0
  77. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/docs/VariableStatus.md +0 -0
  78. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/exceptions.py +0 -0
  79. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/py.typed +0 -0
  80. /dapla_metadata/variable_definitions/{generated → _generated}/vardef_client/rest.py +0 -0
  81. {dapla_toolbelt_metadata-0.4.2.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
- from dapla_metadata.variable_definitions.generated.vardef_client.exceptions import (
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 VardefClientException(OpenApiException):
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", "Unknown 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"\n{violation.get('field', 'Unknown field')}: {violation.get('message', 'No message provided')}"
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", "No detail provided")
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
- data = None
53
- super().__init__(f"Status {self.status}: {self.detail}")
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 VardefClientException(e.body) from e
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