dapla-toolbelt-metadata 0.2.1__py3-none-any.whl → 0.9.11__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 (97) hide show
  1. dapla_metadata/__init__.py +11 -1
  2. dapla_metadata/_shared/__init__.py +1 -0
  3. dapla_metadata/_shared/config.py +109 -0
  4. dapla_metadata/_shared/enums.py +27 -0
  5. dapla_metadata/_shared/py.typed +0 -0
  6. dapla_metadata/dapla/__init__.py +4 -0
  7. dapla_metadata/dapla/user_info.py +138 -0
  8. dapla_metadata/datasets/__init__.py +1 -1
  9. dapla_metadata/datasets/_merge.py +333 -0
  10. dapla_metadata/datasets/code_list.py +5 -6
  11. dapla_metadata/datasets/compatibility/__init__.py +10 -0
  12. dapla_metadata/datasets/compatibility/_handlers.py +363 -0
  13. dapla_metadata/datasets/compatibility/_utils.py +259 -0
  14. dapla_metadata/datasets/compatibility/model_backwards_compatibility.py +135 -0
  15. dapla_metadata/datasets/core.py +136 -182
  16. dapla_metadata/datasets/dapla_dataset_path_info.py +145 -19
  17. dapla_metadata/datasets/dataset_parser.py +41 -28
  18. dapla_metadata/datasets/model_validation.py +29 -20
  19. dapla_metadata/datasets/statistic_subject_mapping.py +5 -1
  20. dapla_metadata/datasets/utility/constants.py +22 -15
  21. dapla_metadata/datasets/utility/enums.py +8 -20
  22. dapla_metadata/datasets/utility/urn.py +234 -0
  23. dapla_metadata/datasets/utility/utils.py +183 -111
  24. dapla_metadata/standards/__init__.py +4 -0
  25. dapla_metadata/standards/name_validator.py +250 -0
  26. dapla_metadata/standards/standard_validators.py +98 -0
  27. dapla_metadata/standards/utils/__init__.py +1 -0
  28. dapla_metadata/standards/utils/constants.py +49 -0
  29. dapla_metadata/variable_definitions/__init__.py +11 -0
  30. dapla_metadata/variable_definitions/_generated/.openapi-generator/FILES +20 -0
  31. dapla_metadata/variable_definitions/_generated/.openapi-generator/VERSION +1 -0
  32. dapla_metadata/variable_definitions/_generated/.openapi-generator-ignore +6 -0
  33. dapla_metadata/variable_definitions/_generated/README.md +148 -0
  34. dapla_metadata/variable_definitions/_generated/__init__.py +0 -0
  35. dapla_metadata/variable_definitions/_generated/vardef_client/__init__.py +47 -0
  36. dapla_metadata/variable_definitions/_generated/vardef_client/api/__init__.py +8 -0
  37. dapla_metadata/variable_definitions/_generated/vardef_client/api/data_migration_api.py +766 -0
  38. dapla_metadata/variable_definitions/_generated/vardef_client/api/draft_variable_definitions_api.py +888 -0
  39. dapla_metadata/variable_definitions/_generated/vardef_client/api/patches_api.py +888 -0
  40. dapla_metadata/variable_definitions/_generated/vardef_client/api/validity_periods_api.py +583 -0
  41. dapla_metadata/variable_definitions/_generated/vardef_client/api/variable_definitions_api.py +613 -0
  42. dapla_metadata/variable_definitions/_generated/vardef_client/api_client.py +779 -0
  43. dapla_metadata/variable_definitions/_generated/vardef_client/api_response.py +27 -0
  44. dapla_metadata/variable_definitions/_generated/vardef_client/configuration.py +474 -0
  45. dapla_metadata/variable_definitions/_generated/vardef_client/docs/CompleteResponse.md +51 -0
  46. dapla_metadata/variable_definitions/_generated/vardef_client/docs/Contact.md +30 -0
  47. dapla_metadata/variable_definitions/_generated/vardef_client/docs/DataMigrationApi.md +90 -0
  48. dapla_metadata/variable_definitions/_generated/vardef_client/docs/Draft.md +42 -0
  49. dapla_metadata/variable_definitions/_generated/vardef_client/docs/DraftVariableDefinitionsApi.md +259 -0
  50. dapla_metadata/variable_definitions/_generated/vardef_client/docs/LanguageStringType.md +31 -0
  51. dapla_metadata/variable_definitions/_generated/vardef_client/docs/Owner.md +31 -0
  52. dapla_metadata/variable_definitions/_generated/vardef_client/docs/Patch.md +43 -0
  53. dapla_metadata/variable_definitions/_generated/vardef_client/docs/PatchesApi.md +249 -0
  54. dapla_metadata/variable_definitions/_generated/vardef_client/docs/PublicApi.md +218 -0
  55. dapla_metadata/variable_definitions/_generated/vardef_client/docs/SupportedLanguages.md +15 -0
  56. dapla_metadata/variable_definitions/_generated/vardef_client/docs/UpdateDraft.md +44 -0
  57. dapla_metadata/variable_definitions/_generated/vardef_client/docs/ValidityPeriod.md +42 -0
  58. dapla_metadata/variable_definitions/_generated/vardef_client/docs/ValidityPeriodsApi.md +236 -0
  59. dapla_metadata/variable_definitions/_generated/vardef_client/docs/VariableDefinitionsApi.md +304 -0
  60. dapla_metadata/variable_definitions/_generated/vardef_client/docs/VariableStatus.md +17 -0
  61. dapla_metadata/variable_definitions/_generated/vardef_client/exceptions.py +193 -0
  62. dapla_metadata/variable_definitions/_generated/vardef_client/models/__init__.py +31 -0
  63. dapla_metadata/variable_definitions/_generated/vardef_client/models/complete_response.py +260 -0
  64. dapla_metadata/variable_definitions/_generated/vardef_client/models/contact.py +94 -0
  65. dapla_metadata/variable_definitions/_generated/vardef_client/models/draft.py +228 -0
  66. dapla_metadata/variable_definitions/_generated/vardef_client/models/get_vardok_vardef_mapping_by_id200_response.py +158 -0
  67. dapla_metadata/variable_definitions/_generated/vardef_client/models/language_string_type.py +101 -0
  68. dapla_metadata/variable_definitions/_generated/vardef_client/models/owner.py +87 -0
  69. dapla_metadata/variable_definitions/_generated/vardef_client/models/patch.py +244 -0
  70. dapla_metadata/variable_definitions/_generated/vardef_client/models/problem.py +118 -0
  71. dapla_metadata/variable_definitions/_generated/vardef_client/models/update_draft.py +274 -0
  72. dapla_metadata/variable_definitions/_generated/vardef_client/models/validity_period.py +225 -0
  73. dapla_metadata/variable_definitions/_generated/vardef_client/models/vardok_id_response.py +81 -0
  74. dapla_metadata/variable_definitions/_generated/vardef_client/models/vardok_vardef_id_pair_response.py +84 -0
  75. dapla_metadata/variable_definitions/_generated/vardef_client/models/variable_status.py +33 -0
  76. dapla_metadata/variable_definitions/_generated/vardef_client/py.typed +0 -0
  77. dapla_metadata/variable_definitions/_generated/vardef_client/rest.py +249 -0
  78. dapla_metadata/variable_definitions/_utils/__init__.py +1 -0
  79. dapla_metadata/variable_definitions/_utils/_client.py +32 -0
  80. dapla_metadata/variable_definitions/_utils/config.py +54 -0
  81. dapla_metadata/variable_definitions/_utils/constants.py +80 -0
  82. dapla_metadata/variable_definitions/_utils/files.py +309 -0
  83. dapla_metadata/variable_definitions/_utils/template_files.py +99 -0
  84. dapla_metadata/variable_definitions/_utils/variable_definition_files.py +143 -0
  85. dapla_metadata/variable_definitions/exceptions.py +255 -0
  86. dapla_metadata/variable_definitions/vardef.py +372 -0
  87. dapla_metadata/variable_definitions/vardok_id.py +48 -0
  88. dapla_metadata/variable_definitions/vardok_vardef_id_pair.py +47 -0
  89. dapla_metadata/variable_definitions/variable_definition.py +422 -0
  90. {dapla_toolbelt_metadata-0.2.1.dist-info → dapla_toolbelt_metadata-0.9.11.dist-info}/METADATA +34 -36
  91. dapla_toolbelt_metadata-0.9.11.dist-info/RECORD +97 -0
  92. {dapla_toolbelt_metadata-0.2.1.dist-info → dapla_toolbelt_metadata-0.9.11.dist-info}/WHEEL +1 -1
  93. dapla_metadata/datasets/config.py +0 -80
  94. dapla_metadata/datasets/model_backwards_compatibility.py +0 -520
  95. dapla_metadata/datasets/user_info.py +0 -88
  96. dapla_toolbelt_metadata-0.2.1.dist-info/RECORD +0 -22
  97. {dapla_toolbelt_metadata-0.2.1.dist-info → dapla_toolbelt_metadata-0.9.11.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,255 @@
1
+ """Vardef client exceptions."""
2
+
3
+ import json
4
+ from functools import wraps
5
+ from http import HTTPStatus
6
+ from types import MappingProxyType
7
+
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 (
13
+ OpenApiException,
14
+ )
15
+ from dapla_metadata.variable_definitions._generated.vardef_client.exceptions import (
16
+ UnauthorizedException,
17
+ )
18
+ from dapla_metadata.variable_definitions._generated.vardef_client.models.variable_status import (
19
+ VariableStatus,
20
+ )
21
+ from dapla_metadata.variable_definitions._utils._client import VardefClient
22
+ from dapla_metadata.variable_definitions._utils.constants import (
23
+ PUBLISHING_BLOCKED_ERROR_MESSAGE,
24
+ )
25
+ from dapla_metadata.variable_definitions._utils.constants import VARDEF_PROD_URL
26
+
27
+ # Use MappingProxyType so the dict is immutable
28
+ STATUS_EXPLANATIONS: MappingProxyType[HTTPStatus | None, str] = MappingProxyType(
29
+ {
30
+ HTTPStatus.BAD_REQUEST: "There was a problem with the supplied data. Please review the data you supplied based on the details below.",
31
+ HTTPStatus.UNAUTHORIZED: "There is a problem with your token, it may have expired. Try logging out, logging in and trying again.",
32
+ HTTPStatus.FORBIDDEN: "Forbidden. You don't have access to this, possibly because your team is not an owner of this variable definition.",
33
+ HTTPStatus.NOT_FOUND: "The variable definition was not found, check that the `short_name` or `id` is correct and that the variable exists.",
34
+ HTTPStatus.METHOD_NOT_ALLOWED: "That won't work here. The status of your variable definition likely doesn't allow for this to happen.",
35
+ HTTPStatus.CONFLICT: "There was a conflict with existing data. Please change your data and try again.",
36
+ None: "",
37
+ },
38
+ )
39
+
40
+
41
+ class VardefClientError(Exception):
42
+ """Custom exception to represent errors encountered in the Vardef client.
43
+
44
+ This exception extracts and formats error details from a JSON response body
45
+ provided by the Vardef API, enabling more descriptive error messages.
46
+ If the response body cannot be parsed as JSON or lacks expected keys,
47
+ default values are used to provide meaningful feedback.
48
+ """
49
+
50
+ def __init__(self, response_body: str) -> None:
51
+ """Initialize the exception with a JSON-formatted response body.
52
+
53
+ Args:
54
+ response_body (str): The JSON string containing error details
55
+ from the Vardef API response.
56
+
57
+ Attributes:
58
+ status (str): The status code from the response, or "Unknown status"
59
+ if not available or the response is invalid.
60
+ detail (str || list): A detailed error message from the response, or
61
+ "No detail provided" if not provided. If "Constraint violation"
62
+ detail contains 'field' and 'message'.
63
+ response_body (str): The raw response body string, stored for
64
+ debugging purposes.
65
+ """
66
+ self.status: int | None = None
67
+ self.detail: str = ""
68
+ try:
69
+ data = json.loads(response_body)
70
+ self.status = data.get("status")
71
+ if data.get("title") == "Constraint Violation":
72
+ violations = data.get("violations", [])
73
+ self.detail = "\n" + "\n".join(
74
+ f"{violation.get('field', 'Unknown field')}: {violation.get('message', 'No message provided')}"
75
+ for violation in violations
76
+ )
77
+ else:
78
+ self.detail = data.get("detail")
79
+
80
+ if self.detail:
81
+ self.detail = f"\nDetail: {self.detail}"
82
+ self.response_body = response_body
83
+ except (json.JSONDecodeError, TypeError):
84
+ self.detail = "Could not decode error response from API"
85
+
86
+ super().__init__(
87
+ f"{self._get_status_explanation(self.status)}{self.detail if self.detail else ''}",
88
+ )
89
+
90
+ @staticmethod
91
+ def _get_status_explanation(status: int | None) -> str:
92
+ return (
93
+ ""
94
+ if not status
95
+ else (STATUS_EXPLANATIONS.get(HTTPStatus(status)) or f"Status {status}:")
96
+ )
97
+
98
+
99
+ def vardef_exception_handler(method): # noqa: ANN201, ANN001
100
+ """Decorator for handling exceptions in Vardef."""
101
+
102
+ @wraps(method)
103
+ def _impl(self, *method_args, **method_kwargs): # noqa: ANN001, ANN002, ANN003
104
+ try:
105
+ return method(self, *method_args, **method_kwargs)
106
+ except urllib3.exceptions.HTTPError as e:
107
+ # Catch all urllib3 exceptions by catching the base class.
108
+ # These exceptions typically arise from lower level network problems.
109
+ raise VardefClientError(
110
+ json.dumps(
111
+ {
112
+ "status": None,
113
+ "title": "Network problems",
114
+ "detail": f"""There was a network problem when sending the request to the server. Try again shortly.
115
+ Original exception:
116
+ {getattr(e, "message", repr(e))}""",
117
+ },
118
+ ),
119
+ ) from e
120
+ except UnauthorizedException as e:
121
+ raise VardefClientError(
122
+ json.dumps(
123
+ {
124
+ "status": e.status,
125
+ "title": "Unauthorized",
126
+ "detail": "Unauthorized",
127
+ },
128
+ ),
129
+ ) from e
130
+ except OpenApiException as e:
131
+ raise VardefClientError(e.body) from e
132
+
133
+ return _impl
134
+
135
+
136
+ class VariableNotFoundError(Exception):
137
+ """Custom exception for when a variable is not found.
138
+
139
+ Attributes:
140
+ message (str): Message describing the error.
141
+ """
142
+
143
+ def __init__(self, message: str) -> None:
144
+ """Initialize the VariableNotFoundError.
145
+
146
+ Args:
147
+ message (str): Description of the error.
148
+ """
149
+ super().__init__(message)
150
+ self.message = message
151
+
152
+ def __str__(self) -> str:
153
+ """Return the string representation of the exception."""
154
+ return f" {self.message}"
155
+
156
+
157
+ class VardefFileError(Exception):
158
+ """Custom exception for catching errors related to variable definition file handling.
159
+
160
+ Attributes:
161
+ message (str): Message describing the error.
162
+ """
163
+
164
+ def __init__(self, message: str, *args) -> None: # noqa: ANN002
165
+ """Accepting the message and any additional arguments."""
166
+ super().__init__(message, *args)
167
+ self.message = message
168
+ self.args = args
169
+
170
+ def __str__(self) -> str:
171
+ """Returning a custom string representation of the exception."""
172
+ return f"VardefFileError: {self.message}"
173
+
174
+
175
+ def vardef_file_error_handler(method): # noqa: ANN201, ANN001
176
+ """Decorator for handling exceptions when generating yaml files for variable definitions."""
177
+
178
+ @wraps(method)
179
+ def _impl(*method_args, **method_kwargs): # noqa: ANN002, ANN003
180
+ try:
181
+ return method(
182
+ *method_args,
183
+ **method_kwargs,
184
+ )
185
+ except FileExistsError as e:
186
+ msg = f"File already exists and can not be saved: {method_kwargs.get('file_path', 'unknown file path')}"
187
+ raise VardefFileError(msg) from e
188
+ except PermissionError as e:
189
+ msg = f"Permission denied for file path when accessing the file. Original error: {e!s}"
190
+ raise VardefFileError(msg) from e
191
+ except UnknownTimeZoneError as e:
192
+ msg = f"Timezone is unknown: {method_kwargs.get('time_zone', 'unknown')}"
193
+ raise VardefFileError(msg) from e
194
+ except YAMLError as e:
195
+ msg = f"Invalid yaml. Please fix the formatting in your yaml file.\nOriginal error:\n{e!s}"
196
+ raise VardefFileError(msg) from e
197
+ except EOFError as e:
198
+ msg = "Unexpected end of file"
199
+ raise VardefFileError(msg) from e
200
+ except NotADirectoryError as e:
201
+ msg = f"Path is not a directory: {method_kwargs.get('file_path', 'unknown file path')}. Original error: {e!s}"
202
+ raise VardefFileError(msg) from e
203
+
204
+ return _impl
205
+
206
+
207
+ class PublishingBlockedError(RuntimeError):
208
+ """Exception raised when publishing variable definitions is blocked in the production environment."""
209
+
210
+ default_message = PUBLISHING_BLOCKED_ERROR_MESSAGE
211
+
212
+ def __str__(self) -> str:
213
+ """Return the default publishing blocked error message."""
214
+ return self.default_message
215
+
216
+
217
+ def publishing_blocked_error_handler(method): # noqa: ANN201, ANN001
218
+ """Decorator that blocks publishing variable definitions in production.
219
+
220
+ - If the environment is production:
221
+ - If `variable_status` is present in the arguments and set to `PUBLISHED_INTERNAL` or `PUBLISHED_EXTERNAL`,
222
+ publishing is blocked by raising a `PublishingBlockedError`.
223
+ - If `variable_status` is set to `DRAFT`, the method is allowed to proceed.
224
+ - If no arguments are provided, all publishing attempts are blocked.
225
+ """
226
+
227
+ @wraps(method)
228
+ def _impl(*method_args, **method_kwargs): # noqa: ANN002, ANN003
229
+ try:
230
+ client = VardefClient()
231
+ host = client.get_config().host
232
+ except VardefClientError as e:
233
+ msg = f"Failed to get VardefClient config: {e}"
234
+ raise RuntimeError(msg) from e
235
+
236
+ if host == VARDEF_PROD_URL:
237
+ if len(method_args) > 1:
238
+ status = method_args[1].variable_status
239
+ if status == VariableStatus.DRAFT:
240
+ return method(
241
+ *method_args,
242
+ **method_kwargs,
243
+ )
244
+ if status in (
245
+ VariableStatus.PUBLISHED_INTERNAL,
246
+ VariableStatus.PUBLISHED_EXTERNAL,
247
+ ):
248
+ raise PublishingBlockedError
249
+ raise PublishingBlockedError
250
+ return method(
251
+ *method_args,
252
+ **method_kwargs,
253
+ )
254
+
255
+ return _impl
@@ -0,0 +1,372 @@
1
+ import logging
2
+ from datetime import date
3
+ from os import PathLike
4
+ from pathlib import Path
5
+
6
+ from dapla_metadata.variable_definitions._generated.vardef_client.api.data_migration_api import (
7
+ DataMigrationApi,
8
+ )
9
+ from dapla_metadata.variable_definitions._generated.vardef_client.api.draft_variable_definitions_api import (
10
+ DraftVariableDefinitionsApi,
11
+ )
12
+ from dapla_metadata.variable_definitions._generated.vardef_client.api.variable_definitions_api import (
13
+ VariableDefinitionsApi,
14
+ )
15
+ from dapla_metadata.variable_definitions._generated.vardef_client.models.complete_response import (
16
+ CompleteResponse,
17
+ )
18
+ from dapla_metadata.variable_definitions._generated.vardef_client.models.draft import (
19
+ Draft,
20
+ )
21
+ from dapla_metadata.variable_definitions._generated.vardef_client.models.vardok_id_response import (
22
+ VardokIdResponse,
23
+ )
24
+ from dapla_metadata.variable_definitions._utils import config
25
+ from dapla_metadata.variable_definitions._utils._client import VardefClient
26
+ from dapla_metadata.variable_definitions._utils.template_files import (
27
+ _find_latest_template_file,
28
+ )
29
+ from dapla_metadata.variable_definitions._utils.template_files import (
30
+ create_template_yaml,
31
+ )
32
+ from dapla_metadata.variable_definitions._utils.variable_definition_files import (
33
+ _read_file_to_model,
34
+ )
35
+ from dapla_metadata.variable_definitions.exceptions import VariableNotFoundError
36
+ from dapla_metadata.variable_definitions.exceptions import vardef_exception_handler
37
+ from dapla_metadata.variable_definitions.exceptions import vardef_file_error_handler
38
+ from dapla_metadata.variable_definitions.vardok_id import VardokId
39
+ from dapla_metadata.variable_definitions.vardok_vardef_id_pair import VardokVardefIdPair
40
+ from dapla_metadata.variable_definitions.variable_definition import VariableDefinition
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+
45
+ class Vardef:
46
+ """Create, maintain and read Variable Definitions.
47
+
48
+ ====================
49
+ Variable Definitions
50
+ ====================
51
+
52
+ Variable Definitions are centralized definitions of concrete variables which are typically present in multiple datasets. Variable Definitions
53
+ support standardization of data and metadata and facilitate sharing and joining of data by clarifying when variables have an identical
54
+ definition.
55
+
56
+ The methods in this class allow for creation, maintenance and access of Variable Definitions.
57
+
58
+ Creation and maintenance of variables may only be performed by Statistics Norway employees representing a specific Dapla team, who are defined
59
+ as the owners of a given Variable Definition. The group a user represents is chosen when starting a service in Dapla Lab. This class will
60
+ seamlessly detect this and send it to the API. All maintenance is to be performed by the owners, with no intervention from administrators.
61
+
62
+ ======
63
+ Status
64
+ ======
65
+ All Variable Definitions have an associated status. The possible values for status are `DRAFT`, `PUBLISHED_INTERNAL` and `PUBLISHED_EXTERNAL`.
66
+
67
+ -----
68
+ Draft
69
+ -----
70
+ When a Variable Definition is created it is assigned the status `DRAFT`. Under this status the Variable Definition is:
71
+ * Only visible to Statistics Norway employees.
72
+ * Mutable (it may be changed directly without need for versioning).
73
+ * Not suitable to refer to from other systems.
74
+ This status may be changed to `PUBLISHED_INTERNAL` or `PUBLISHED_EXTERNAL` with a direct update.
75
+
76
+ ------------------
77
+ Published Internal
78
+ ------------------
79
+ Under this status the Variable Definition is:
80
+ * Only visible to Statistics Norway employees.
81
+ * Immutable (all changes are versioned).
82
+ * Suitable to refer to in internal systems for statistics production.
83
+ * Not suitable to refer to for external use (for example in Statistikkbanken).
84
+
85
+ This status may be changed to `PUBLISHED_EXTERNAL` by creating a Patch version.
86
+
87
+ ------------------
88
+ Published External
89
+ ------------------
90
+ Under this status the Variable Definition is:
91
+ * Visible to the general public.
92
+ * Immutable (all changes are versioned).
93
+ * Suitable to refer to from any system.
94
+
95
+ This status may not be changed as it would break immutability. If a Variable Definition is no longer relevant then its period of validity
96
+ should be ended by specifying a `valid_until` date in a Patch version.
97
+
98
+ ============
99
+ Immutability
100
+ ============
101
+ Variable Definitions are immutable. This means that any changes must be performed in a strict versioning system. Consumers can avoid
102
+ being exposed to breaking changes by specifying a `date_of_validity` when they request a Variable Definition.
103
+ """
104
+
105
+ @classmethod
106
+ @vardef_exception_handler
107
+ def create_draft(cls, draft: Draft) -> VariableDefinition:
108
+ """Create a Draft Variable Definition."""
109
+ new_variable = VariableDefinition.from_model(
110
+ DraftVariableDefinitionsApi(
111
+ VardefClient.get_client(),
112
+ ).create_variable_definition(
113
+ active_group=config.get_active_group(),
114
+ draft=draft,
115
+ ),
116
+ )
117
+
118
+ logger.info(
119
+ "✅ Successfully created variable definition '%s' with ID '%s'",
120
+ new_variable.short_name,
121
+ new_variable.id,
122
+ )
123
+ return new_variable
124
+
125
+ @classmethod
126
+ @vardef_file_error_handler
127
+ def create_draft_from_file(
128
+ cls,
129
+ file_path: PathLike[str] | None = None,
130
+ ) -> VariableDefinition:
131
+ """Create a Draft Variable Definition from a stored yaml file.
132
+
133
+ By default the latest template file in the default directory is chosen, this may be overridden by providing a value for the optional `file_path` parameter.
134
+
135
+ Args:
136
+ file_path (PathLike[str], optional): Supply a file path to override the automatic one. Defaults to None.
137
+
138
+ Raises:
139
+ FileNotFoundError: When a file can't be found.
140
+
141
+ Returns:
142
+ VariableDefinition: The created draft variable definition.
143
+ """
144
+ return cls.create_draft(
145
+ _read_file_to_model(
146
+ file_path or _find_latest_template_file(),
147
+ Draft,
148
+ ),
149
+ )
150
+
151
+ @classmethod
152
+ @vardef_exception_handler
153
+ def migrate_from_vardok(cls, vardok_id: str) -> VariableDefinition:
154
+ """Migrate a Variable Definition from Vardok to Vardef.
155
+
156
+ - Each Vardok Variable Definition may only be migrated once.
157
+ - The Dapla team of the person who performs the migration will be set as the owner.
158
+ - All metadata should be checked for correctness before publication.
159
+
160
+ Args:
161
+ vardok_id (str): The ID of a Variable Definition in Vardok.
162
+
163
+ Returns:
164
+ VariableDefinition: The migrated Variable Definition in Vardef.
165
+ """
166
+ migrated_variable = VariableDefinition.from_model(
167
+ DataMigrationApi(
168
+ VardefClient.get_client(),
169
+ ).create_variable_definition_from_var_dok(
170
+ active_group=config.get_active_group(),
171
+ vardok_id=vardok_id,
172
+ ),
173
+ )
174
+
175
+ logger.info(
176
+ "✅ Successfully migrated variable definition '%s' with ID '%s'",
177
+ migrated_variable.short_name,
178
+ migrated_variable.id,
179
+ )
180
+ return migrated_variable
181
+
182
+ @classmethod
183
+ @vardef_exception_handler
184
+ def list_vardok_vardef_mapping(cls) -> list[VardokVardefIdPair]:
185
+ """List the mapping between vardok and vardef.
186
+
187
+ Returns:
188
+ List[VardokVardefIdPair]: The list with mappings between Vardok and Vardef
189
+ """
190
+ return [
191
+ VardokVardefIdPair.from_model(definition)
192
+ for definition in DataMigrationApi(
193
+ VardefClient.get_client(),
194
+ ).get_vardok_vardef_mapping()
195
+ ]
196
+
197
+ @classmethod
198
+ @vardef_exception_handler
199
+ def get_variable_definition_by_vardok_id(
200
+ cls,
201
+ vardok_id: str,
202
+ ) -> VariableDefinition:
203
+ """Get a Variable Definition by its Vardok ID.
204
+
205
+ Args:
206
+ vardok_id (str): The Vardok ID of the desired Variable Definition
207
+
208
+ Returns:
209
+ VariableDefinition: The Variable Definition.
210
+
211
+ Raises:
212
+ TypeError: If the incorrect type is returned.
213
+ """
214
+ raw_response = DataMigrationApi(
215
+ VardefClient.get_client()
216
+ ).get_vardok_vardef_mapping_by_id(
217
+ vardok_id,
218
+ )
219
+
220
+ if isinstance(raw_response.actual_instance, CompleteResponse):
221
+ return VariableDefinition.from_model(raw_response.actual_instance)
222
+ msg = "Unexpected response type"
223
+ raise TypeError(msg)
224
+
225
+ @classmethod
226
+ @vardef_exception_handler
227
+ def get_vardok_id_by_short_name(
228
+ cls,
229
+ short_name: str,
230
+ ) -> VardokId:
231
+ """Retrieve a Vardok id by short name.
232
+
233
+ Args:
234
+ short_name (str): The short name of the desired Variable Definition
235
+
236
+ Raises:
237
+ TypeError: If the incorrect type is returned.
238
+ """
239
+ variable_definition = cls.get_variable_definition_by_shortname(short_name)
240
+
241
+ raw_response = DataMigrationApi(
242
+ VardefClient.get_client()
243
+ ).get_vardok_vardef_mapping_by_id(
244
+ variable_definition.id,
245
+ )
246
+
247
+ if isinstance(raw_response.actual_instance, VardokIdResponse):
248
+ return VardokId.from_model(raw_response.actual_instance)
249
+ msg = "Unexpected response type"
250
+ raise TypeError(msg)
251
+
252
+ @classmethod
253
+ @vardef_exception_handler
254
+ def list_variable_definitions(
255
+ cls,
256
+ date_of_validity: date | None = None,
257
+ ) -> list[VariableDefinition]:
258
+ """List variable definitions.
259
+
260
+ ---------
261
+ Filtering
262
+ ---------
263
+ If no filter arguments are provided then all Variable Definitions are returned. See the documentation for the
264
+ individual arguments to understand their effect. Filter arguments are combined with AND logic.
265
+
266
+ Args:
267
+ date_of_validity (date | None, optional): List only variable definitions which are valid on this date. Defaults to None.
268
+
269
+ Returns:
270
+ list[VariableDefinition]: The list of Variable Definitions.
271
+ """
272
+ return [
273
+ VariableDefinition.from_model(definition)
274
+ for definition in VariableDefinitionsApi(
275
+ VardefClient.get_client(),
276
+ ).list_variable_definitions(
277
+ date_of_validity=date_of_validity,
278
+ )
279
+ ]
280
+
281
+ @classmethod
282
+ @vardef_exception_handler
283
+ def get_variable_definition_by_id(
284
+ cls,
285
+ variable_definition_id: str,
286
+ date_of_validity: date | None = None,
287
+ ) -> VariableDefinition:
288
+ """Get a Variable Definition by ID.
289
+
290
+ Args:
291
+ variable_definition_id (str): The ID of the desired Variable Definition
292
+ date_of_validity (date | None, optional): List only variable definitions which are valid on this date. Defaults to None.
293
+
294
+ Returns:
295
+ VariableDefinition: The Variable Definition.
296
+
297
+ Raises:
298
+ NotFoundException when the given ID is not found
299
+ """
300
+ return VariableDefinition.from_model(
301
+ VariableDefinitionsApi(
302
+ VardefClient.get_client(),
303
+ ).get_variable_definition_by_id(
304
+ variable_definition_id=variable_definition_id,
305
+ date_of_validity=date_of_validity,
306
+ ),
307
+ )
308
+
309
+ @classmethod
310
+ @vardef_exception_handler
311
+ def get_variable_definition_by_shortname(
312
+ cls,
313
+ short_name: str,
314
+ date_of_validity: date | None = None,
315
+ ) -> VariableDefinition:
316
+ """Retrieve a Variable Definition by short name.
317
+
318
+ Args:
319
+ short_name (str): The short name of the Variable Definition.
320
+ date_of_validity (date | None, optional): Filter by validity date. Defaults to None.
321
+
322
+ Returns:
323
+ VariableDefinition: The retrieved Variable Definition.
324
+
325
+ Raises:
326
+ VariableNotFoundError: If no matching Variable Definition is found.
327
+ ValueError: If multiple variables with the same shortname is found.
328
+ """
329
+ client = VardefClient.get_client()
330
+ api = VariableDefinitionsApi(client)
331
+
332
+ variable_definitions = api.list_variable_definitions(
333
+ short_name=short_name,
334
+ date_of_validity=date_of_validity,
335
+ )
336
+
337
+ if not variable_definitions:
338
+ msg = f"Variable with short name {short_name} not found"
339
+ raise VariableNotFoundError(msg)
340
+ if len(variable_definitions) > 1:
341
+ msg = f"Lookup by short name {short_name} found multiple variables which should not be possible."
342
+ raise VariableNotFoundError(msg)
343
+
344
+ return VariableDefinition.from_model(variable_definitions[0])
345
+
346
+ @classmethod
347
+ @vardef_file_error_handler
348
+ def write_template_to_file(cls, custom_file_path: str | None = None) -> Path:
349
+ """Write template with default values to a yaml file."""
350
+ file_path = create_template_yaml(
351
+ custom_directory=Path(custom_file_path) if custom_file_path else None,
352
+ )
353
+ logger.info(
354
+ f"✅ Created editable variable definition template file at {file_path}", # noqa: G004
355
+ )
356
+ return file_path
357
+
358
+ @classmethod
359
+ @vardef_exception_handler
360
+ def does_short_name_exist(
361
+ cls,
362
+ short_name: str,
363
+ ) -> bool:
364
+ """Return True if the short name exists in Vardef, otherwise False."""
365
+ variable_definitions = Vardef.list_variable_definitions()
366
+ for variable in variable_definitions:
367
+ if short_name.strip() == variable.short_name:
368
+ logger.info(
369
+ f"Found duplicate short name {short_name}", # noqa: G004
370
+ )
371
+ return True
372
+ return False
@@ -0,0 +1,48 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ from pydantic import ConfigDict
5
+ from pydantic import PrivateAttr
6
+
7
+ from dapla_metadata.variable_definitions._generated.vardef_client.models.vardok_id_response import (
8
+ VardokIdResponse,
9
+ )
10
+ from dapla_metadata.variable_definitions._utils.variable_definition_files import (
11
+ _convert_to_yaml_output,
12
+ )
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class VardokId(VardokIdResponse):
18
+ """A Vardok id.
19
+
20
+ - Provides access to the Vardok id filed.
21
+ - Provides methods allowing maintenance for nicer output of the Vardok id.
22
+
23
+ Args:
24
+ VardokIdResponse: The Pydantic model superclass, representing a Vardok id response.
25
+ """
26
+
27
+ _file_path: Path | None = PrivateAttr(None)
28
+
29
+ model_config = ConfigDict(use_enum_values=True, str_strip_whitespace=True)
30
+
31
+ @staticmethod
32
+ def from_model(
33
+ model: VardokIdResponse,
34
+ ) -> "VardokId":
35
+ """Create a VariableDefinition instance from a CompleteResponse."""
36
+ self = VardokId.from_dict(model.model_dump())
37
+ if not self:
38
+ msg = f"Could not construct a VardokId instance from {model}"
39
+ raise ValueError(msg)
40
+ return self
41
+
42
+ def __str__(self) -> str:
43
+ """Format as indented YAML."""
44
+ return _convert_to_yaml_output(self)
45
+
46
+ def __repr__(self) -> str:
47
+ """Format as indented YAML."""
48
+ return _convert_to_yaml_output(self)