opentf-toolkit-nightly 0.63.0.dev1379__py3-none-any.whl → 0.63.0.dev1393__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.
@@ -0,0 +1,208 @@
1
+ # Copyright (c) 2025 Henix, Henix.fr
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Handling models from plugins configuration files."""
16
+
17
+ from typing import Any
18
+
19
+ import os
20
+
21
+ from opentf.commons import read_and_validate
22
+ from opentf.toolkit import watch_file
23
+
24
+ Model = dict[str, Any]
25
+ Spec = dict[str, Any]
26
+
27
+ ########################################################################
28
+
29
+ IMG_MODELS = []
30
+
31
+ ########################################################################
32
+ ### Configuration loader helpers
33
+
34
+
35
+ def deduplicate(
36
+ plugin,
37
+ models: list[Model],
38
+ ) -> tuple[list[Model], set[str]]:
39
+ """Deduplicate models in a list.
40
+
41
+ # Required parameter
42
+
43
+ - models: a list of dictionaries (models), in increasing priority order.
44
+
45
+ # Returned value
46
+
47
+ A tuple containing a list of deduplicated models and a possibly empty
48
+ list of warnings.
49
+ """
50
+ seen = {}
51
+ name, kind = None, None
52
+ warnings = set()
53
+ for model in reversed(models):
54
+ key = (name, kind) = model.get('name'), model.get('kind')
55
+ if key not in seen:
56
+ seen[key] = model
57
+ else:
58
+ if model.get('.source') != 'default':
59
+ msg = f'Duplicate definitions found for {plugin.name} {kind+' ' if kind else ''}"{name}", only the definition with the highest priority will be used.'
60
+ warnings.add(msg)
61
+ if warnings:
62
+ for msg in warnings:
63
+ plugin.logger.warning(msg)
64
+ return list(reversed(list(seen.values()))), warnings
65
+
66
+
67
+ def filter_listdir(plugin, path: str, kinds: tuple[str, ...]) -> list[str]:
68
+ """listdir-like, filtering for files with specified extensions."""
69
+ files = [
70
+ f
71
+ for f in os.listdir(path)
72
+ if os.path.isfile(os.path.join(path, f)) and f.endswith(kinds)
73
+ ]
74
+ if not files:
75
+ plugin.logger.debug('No %s files provided in %s.', ', '.join(kinds), path)
76
+ return sorted(files)
77
+
78
+
79
+ def _read_models(
80
+ plugin, schema: str, configfile: str, config_key: str
81
+ ) -> list[Model] | None:
82
+ """Read plugin models JSON or YAML and return models list."""
83
+ try:
84
+ models = read_and_validate(schema, configfile)
85
+ except ValueError as err:
86
+ plugin.logger.error(
87
+ 'Invalid %s definition file "%s": %s. Ignoring.',
88
+ plugin.name,
89
+ configfile,
90
+ str(err),
91
+ )
92
+ return None
93
+
94
+ return models[config_key]
95
+
96
+
97
+ def _load_image_models(
98
+ plugin, config_path: str, config_key: str, schema: str, default_models: list[Model]
99
+ ) -> list[dict[str, Any]]:
100
+ """Load models from `CONFIG_PATH` directory.
101
+
102
+ Storing models and possible warnings in plugin.config['CONFIG'].
103
+ """
104
+ models = default_models
105
+ for config_file in filter_listdir(plugin, config_path, ('.yaml', '.yml')):
106
+ filepath = os.path.join(config_path, config_file)
107
+ try:
108
+ if not (img_models := _read_models(plugin, schema, filepath, config_key)):
109
+ continue
110
+ plugin.logger.debug(
111
+ 'Loading %s models from file "%s".', plugin.name, config_file
112
+ )
113
+ models.extend(img_models)
114
+ except Exception as err:
115
+ raise ValueError(
116
+ f'Failed to load {plugin.name} models from file "{config_file}": {str(err)}.'
117
+ )
118
+ models, warnings = deduplicate(plugin, models)
119
+ plugin.config['CONFIG'][config_key] = models
120
+ plugin.config['CONFIG']['warnings'] = warnings
121
+ return models
122
+
123
+
124
+ def _refresh_configuration(
125
+ _, configfile: str, schema: str, plugin, config_key: str
126
+ ) -> None:
127
+ """Read plugin models from environment variable specified file.
128
+
129
+ Storing models in .config['CONFIG'], using the following entries:
130
+
131
+ - {config_key}: a list of models
132
+ - warnings: a list of duplicate models warnings
133
+ """
134
+ try:
135
+ config = plugin.config['CONFIG']
136
+ models = IMG_MODELS.copy()
137
+ plugin.logger.info(
138
+ f'Reading {plugin.name} models definition from {configfile}.'
139
+ )
140
+ env_models = _read_models(plugin, schema, configfile, config_key) or []
141
+ models.extend(env_models)
142
+ config[config_key], config['warnings'] = deduplicate(plugin, models)
143
+ except Exception as err:
144
+ plugin.logger.error(
145
+ 'Error while reading %s "%s" definition: %s.',
146
+ plugin.name,
147
+ configfile,
148
+ str(err),
149
+ )
150
+
151
+
152
+ ########################################################################
153
+ ### Configuration loader
154
+
155
+
156
+ def load_and_watch_models(
157
+ plugin,
158
+ config_path: str,
159
+ config_key: str,
160
+ schema: str,
161
+ default_models: list[Model],
162
+ env_var: str,
163
+ ) -> None:
164
+ """Load plugin configuration models.
165
+
166
+ Plugin configuration models are loaded from configuration files path
167
+ and filepath specified by the environment variable. File specified by the
168
+ environment variable is watched for modifications. Models list is stored
169
+ in `plugin.config['CONFIG'][{config_key}]` entry.
170
+
171
+ # Required parameters
172
+
173
+ - plugin: a Flask plugin
174
+ - config_path: a string, configuration models path, should be a directory
175
+ - config_key: a string, plugin configuration key name
176
+ - schema: a string, plugin models validation schema
177
+ - default_models: a list of plugin-specific default models
178
+ - env_var: a string, environment variable name
179
+
180
+ # Raised exception
181
+
182
+ ValueError is raised if configuration files path is not found or
183
+ is not a directory.
184
+ """
185
+ if not os.path.isdir(config_path):
186
+ raise ValueError(
187
+ f'Configuration files path "{config_path}" not found or not a directory.'
188
+ )
189
+
190
+ IMG_MODELS.extend(
191
+ _load_image_models(plugin, config_path, config_key, schema, default_models)
192
+ )
193
+
194
+ if os.environ.get(env_var):
195
+ watch_file(
196
+ plugin,
197
+ os.environ[env_var],
198
+ _refresh_configuration,
199
+ schema,
200
+ plugin,
201
+ config_key,
202
+ )
203
+
204
+ plugin.logger.info(
205
+ 'Loading default %s definitions and definitions from "%s".',
206
+ plugin.name,
207
+ config_path,
208
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opentf-toolkit-nightly
3
- Version: 0.63.0.dev1379
3
+ Version: 0.63.0.dev1393
4
4
  Summary: OpenTestFactory Orchestrator Toolkit
5
5
  Home-page: https://gitlab.com/henixdevelopment/open-source/opentestfactory/python-toolkit
6
6
  Author: Martin Lafaix
@@ -17,7 +17,7 @@ Requires-Python: >= 3.9.0
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: requests>=2.32
20
- Requires-Dist: PyJWT[crypto]>=2.9
20
+ Requires-Dist: PyJWT[crypto]>=2.10
21
21
  Requires-Dist: PyYAML>=6
22
22
  Requires-Dist: Flask<4,>=3
23
23
  Requires-Dist: jsonschema>=4.23
@@ -60,8 +60,9 @@ opentf/scripts/startup.py,sha256=K-uW-70EJb4Ou2dBFR_7utDU3oMWBczkomtikq_2qCc,231
60
60
  opentf/toolkit/__init__.py,sha256=YnH66dmePAIU7dq_xWFYTIEUrsL9qV9f82LRDiBzbzs,22057
61
61
  opentf/toolkit/channels.py,sha256=oXZzW5bwcnGbJ7WAIkV42ekFnOQq7HxIvnyvURWaoNs,25904
62
62
  opentf/toolkit/core.py,sha256=jMBDIYZ8Qn3BvsysfKoG0iTtjOnZsggetpH3eXygCsI,9636
63
- opentf_toolkit_nightly-0.63.0.dev1379.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
64
- opentf_toolkit_nightly-0.63.0.dev1379.dist-info/METADATA,sha256=CUgYI9jA1l5lGnvDp9blM-hcugUKcS59if3I1epdagQ,2214
65
- opentf_toolkit_nightly-0.63.0.dev1379.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
66
- opentf_toolkit_nightly-0.63.0.dev1379.dist-info/top_level.txt,sha256=_gPuE6GTT6UNXy1DjtmQSfCcZb_qYA2vWmjg7a30AGk,7
67
- opentf_toolkit_nightly-0.63.0.dev1379.dist-info/RECORD,,
63
+ opentf/toolkit/models.py,sha256=PNfXVQbeyOwDfaNrLjcfhYm6duMSlNWBtZsWZcs53ag,6583
64
+ opentf_toolkit_nightly-0.63.0.dev1393.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
65
+ opentf_toolkit_nightly-0.63.0.dev1393.dist-info/METADATA,sha256=qLd_GytuE-XM02HMzjQN_vxrvV6jkVN8_EwHahEtUvc,2215
66
+ opentf_toolkit_nightly-0.63.0.dev1393.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
67
+ opentf_toolkit_nightly-0.63.0.dev1393.dist-info/top_level.txt,sha256=_gPuE6GTT6UNXy1DjtmQSfCcZb_qYA2vWmjg7a30AGk,7
68
+ opentf_toolkit_nightly-0.63.0.dev1393.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (79.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5