opentf-toolkit-nightly 0.63.0.dev1385__py3-none-any.whl → 0.63.0.dev1397__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.
@@ -210,26 +210,31 @@ def _add_default_variables(
210
210
  script.append(VARIABLE_MAKER[runner_os]('CI', 'true'))
211
211
 
212
212
 
213
- def _get_opentf_variables_path(metadata: Dict[str, Any]) -> str:
213
+ def get_opentf_variables_path(metadata: Dict[str, Any]) -> str:
214
214
  return VARIABLES_TEMPLATE[metadata['channel_os']].format(
215
215
  job_id=metadata['job_id'], root=metadata['channel_temp']
216
216
  )
217
217
 
218
218
 
219
- def _get_opentf_variables(path: str) -> Dict[str, str]:
219
+ def _read_opentf_variables(lines: List[str]) -> Dict[str, str]:
220
220
  variables = {}
221
+ for line in lines:
222
+ if '=' not in line:
223
+ continue
224
+ line = line.strip()
225
+ if set_export := OPENTF_VARIABLES_REGEX.match(line):
226
+ line = set_export.group(2)
227
+ if line.startswith('"'):
228
+ line = line[1:-1]
229
+ key, _, value = line.partition('=')
230
+ if OPENTF_VARIABLES_NAME_REGEX.match(key):
231
+ variables[key] = value
232
+ return variables
233
+
234
+
235
+ def _get_opentf_variables(path: str) -> Dict[str, str]:
221
236
  with open(path, 'r') as f:
222
- for line in f.readlines():
223
- if '=' not in line:
224
- continue
225
- line = line.strip()
226
- if set_export := OPENTF_VARIABLES_REGEX.match(line):
227
- line = set_export.group(2)
228
- if line.startswith('"'):
229
- line = line[1:-1]
230
- key, _, value = line.partition('=')
231
- if OPENTF_VARIABLES_NAME_REGEX.match(key):
232
- variables[key] = value
237
+ variables = _read_opentf_variables(f.readlines())
233
238
  try:
234
239
  os.remove(path)
235
240
  except FileNotFoundError:
@@ -428,6 +433,7 @@ def process_output(
428
433
  jobstate: JobState,
429
434
  _get: Callable[[str, str], None],
430
435
  _put: Callable[[str, str], None],
436
+ variables: Optional[List[str]] = None,
431
437
  ) -> Dict[str, Any]:
432
438
  """Process output, filling structures.
433
439
 
@@ -566,8 +572,14 @@ def process_output(
566
572
  if metadata.get('artifacts'):
567
573
  del metadata['artifacts']
568
574
 
575
+ opentf_variables = None
569
576
  if metadata['step_sequence_id'] != CHANNEL_RELEASE:
570
- _attach(_get_opentf_variables_path(metadata), f'type={OPENTF_VARIABLES_TYPE}')
577
+ if variables:
578
+ opentf_variables = _read_opentf_variables(variables)
579
+ else:
580
+ _attach(
581
+ get_opentf_variables_path(metadata), f'type={OPENTF_VARIABLES_TYPE}'
582
+ )
571
583
 
572
584
  result = make_event(EXECUTIONRESULT, metadata=metadata, status=resp)
573
585
  if outputs:
@@ -579,7 +591,8 @@ def process_output(
579
591
  result['metadata']['attachments'] = attachments_metadata
580
592
  if has_artifacts:
581
593
  result['metadata']['upload'] = resp
582
-
594
+ if opentf_variables:
595
+ result['variables'] = opentf_variables
583
596
  return result
584
597
 
585
598
 
@@ -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.dev1385
3
+ Version: 0.63.0.dev1397
4
4
  Summary: OpenTestFactory Orchestrator Toolkit
5
5
  Home-page: https://gitlab.com/henixdevelopment/open-source/opentestfactory/python-toolkit
6
6
  Author: Martin Lafaix
@@ -58,10 +58,11 @@ opentf/schemas/opentestfactory.org/v1beta2/ServiceConfig.json,sha256=rEvK2YWL5lG
58
58
  opentf/scripts/launch_java_service.sh,sha256=S0jAaCuv2sZy0Gf2NGBuPX-eD531rcM-b0fNyhmzSjw,2423
59
59
  opentf/scripts/startup.py,sha256=K-uW-70EJb4Ou2dBFR_7utDU3oMWBczkomtikq_2qCc,23119
60
60
  opentf/toolkit/__init__.py,sha256=YnH66dmePAIU7dq_xWFYTIEUrsL9qV9f82LRDiBzbzs,22057
61
- opentf/toolkit/channels.py,sha256=oXZzW5bwcnGbJ7WAIkV42ekFnOQq7HxIvnyvURWaoNs,25904
61
+ opentf/toolkit/channels.py,sha256=7uHpQUCWCzSxcQifeUL9SB9fvsq6_9cZt_8IdBgw8FQ,26272
62
62
  opentf/toolkit/core.py,sha256=jMBDIYZ8Qn3BvsysfKoG0iTtjOnZsggetpH3eXygCsI,9636
63
- opentf_toolkit_nightly-0.63.0.dev1385.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
64
- opentf_toolkit_nightly-0.63.0.dev1385.dist-info/METADATA,sha256=89FePBoy_JqHGy-Dcqcuht35E623SA_AI1PAkFONrqU,2215
65
- opentf_toolkit_nightly-0.63.0.dev1385.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
66
- opentf_toolkit_nightly-0.63.0.dev1385.dist-info/top_level.txt,sha256=_gPuE6GTT6UNXy1DjtmQSfCcZb_qYA2vWmjg7a30AGk,7
67
- opentf_toolkit_nightly-0.63.0.dev1385.dist-info/RECORD,,
63
+ opentf/toolkit/models.py,sha256=PNfXVQbeyOwDfaNrLjcfhYm6duMSlNWBtZsWZcs53ag,6583
64
+ opentf_toolkit_nightly-0.63.0.dev1397.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
65
+ opentf_toolkit_nightly-0.63.0.dev1397.dist-info/METADATA,sha256=vEYD3Fg0XWwNxtheNasZuvo8NDBsGF3ffn_o52w6Ffk,2215
66
+ opentf_toolkit_nightly-0.63.0.dev1397.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
67
+ opentf_toolkit_nightly-0.63.0.dev1397.dist-info/top_level.txt,sha256=_gPuE6GTT6UNXy1DjtmQSfCcZb_qYA2vWmjg7a30AGk,7
68
+ opentf_toolkit_nightly-0.63.0.dev1397.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (80.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5