opentf-toolkit-nightly 0.62.0.dev1326__py3-none-any.whl → 0.62.0.dev1333__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.
@@ -524,11 +524,11 @@ def get_context_parameter(
524
524
 
525
525
  def _maybe_validate(v):
526
526
  newv, reason = validator(name, v) if validator else (v, None)
527
- lhs = f' {spec["descriptiveName"]} ({name}):' if spec else f' {name}:'
527
+ lhs = f'{spec["descriptiveName"]} ({name})' if spec else name
528
528
  if newv != v:
529
- app.logger.info(f'{lhs} {newv} (was defined as {v}, but {reason})')
529
+ app.logger.info(f' {lhs}: {newv} (was defined as {v}, but {reason})')
530
530
  else:
531
- app.logger.info(f'{lhs} {newv}')
531
+ app.logger.info(f' {lhs}: {newv}')
532
532
  return newv
533
533
 
534
534
  def _fatal(msg: str) -> NoReturn:
@@ -830,14 +830,17 @@ def validate_inputs(
830
830
  declaration: Dict[str, Dict[str, Any]],
831
831
  inputs: Dict[str, Any],
832
832
  additional_inputs: bool = False,
833
+ normalize: bool = True,
833
834
  ) -> None:
834
835
  """Validate inputs.
835
836
 
836
837
  Default values are filled in `inputs` as appropriate.
837
838
 
838
- Input names are normalized to use hyphens instead of underscores.
839
+ Input names are normalized to use hyphens instead of underscores
840
+ by default (declaration is expected to be normalized).
839
841
 
840
- Non-normalized inputs are removed from the dictionary.
842
+ If `normalize` is set, non-normalized inputs are removed from the
843
+ dictionary.
841
844
 
842
845
  Choices values are validated. Types are not checked.
843
846
 
@@ -849,16 +852,14 @@ def validate_inputs(
849
852
  # Optional parameters
850
853
 
851
854
  - additional_inputs: a boolean (False by default)
855
+ - normalize: a boolean (True by default)
852
856
 
853
857
  # Raised exceptions
854
858
 
855
859
  A _ValueError_ exception is raised if inputs do not match
856
860
  declaration.
857
861
  """
858
- for key in declaration:
859
- if key.startswith('{'): # Skip template entries
860
- break
861
- else:
862
+ if normalize and not any(key.startswith('{') for key in declaration):
862
863
  _normalize_inputs(inputs)
863
864
 
864
865
  for key, definition in declaration.items():
@@ -868,19 +869,20 @@ def validate_inputs(
868
869
  if definition.get('required', False):
869
870
  raise ValueError(f'Mandatory input "{key}" not provided.')
870
871
  _set_default(inputs, key, definition)
872
+ continue
871
873
  if definition.get('type') == 'choice':
872
- if inputs.get(key) not in definition['options']:
874
+ if (val := inputs.get(key)) not in definition['options']:
873
875
  allowed = '", "'.join(sorted(definition['options']))
874
876
  raise ValueError(
875
- f'Invalid value "{inputs.get(key)}" for input "{key}". Allowed values: "{allowed}".'
877
+ f'Invalid value "{val}" for input "{key}". Allowed values: "{allowed}".'
876
878
  )
877
879
 
878
880
  if additional_inputs:
879
881
  return
880
882
 
881
- for key in inputs:
882
- if key not in declaration and key.replace('_', '-') not in declaration:
883
- allowed = '", "'.join(sorted(declaration))
884
- raise ValueError(
885
- f'Unexpected input "{key}" found. Allowed inputs: "{allowed}".'
886
- )
883
+ if unexpected := set(inputs) - set(declaration):
884
+ allowed = '", "'.join(sorted(declaration))
885
+ unexpected = '", "'.join(sorted(unexpected))
886
+ raise ValueError(
887
+ f'Unexpected inputs "{unexpected}" found. Allowed inputs: "{allowed}".'
888
+ )
opentf/commons/meta.py ADDED
@@ -0,0 +1,100 @@
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
+ """Toolkit metadata helpers."""
16
+
17
+ from typing import Dict, Iterable, Optional, Tuple
18
+
19
+
20
+ ########################################################################
21
+ ## Constants
22
+
23
+ CATEGORY_LABEL = 'opentestfactory.org/category'
24
+ CATEGORYPREFIX_LABEL = 'opentestfactory.org/categoryPrefix'
25
+ CATEGORYVERSION_LABEL = 'opentestfactory.org/categoryVersion'
26
+
27
+
28
+ ########################################################################
29
+
30
+
31
+ def set_category_labels(labels: Dict[str, str], category: str) -> None:
32
+ """Set category labels.
33
+
34
+ `labels` is updated.
35
+
36
+ # Required parameters
37
+
38
+ - labels: a dictionary of labels, possibly empty
39
+ - category: a string
40
+ """
41
+ category_prefix = category_version = '_'
42
+ if '/' in category:
43
+ category_prefix, category = category.split('/')
44
+ if '@' in category:
45
+ category, category_version = category.split('@')
46
+ labels[CATEGORY_LABEL] = category
47
+ labels[CATEGORYPREFIX_LABEL] = category_prefix
48
+ labels[CATEGORYVERSION_LABEL] = category_version
49
+
50
+
51
+ def maybe_set_category_labels(
52
+ labels: Dict[str, str],
53
+ prefix: Optional[str],
54
+ cat: Optional[str],
55
+ version: Optional[str],
56
+ ) -> None:
57
+ """Set category labels if not None.
58
+
59
+ `labels` is updated if at least one parameter is not None.
60
+ """
61
+ if cat is not None:
62
+ labels[CATEGORY_LABEL] = cat
63
+ if prefix is not None:
64
+ labels[CATEGORYPREFIX_LABEL] = prefix
65
+ if version is not None:
66
+ labels[CATEGORYVERSION_LABEL] = version
67
+
68
+
69
+ def read_category_labels(
70
+ labels: Dict[str, str], default: Optional[str] = None
71
+ ) -> Tuple[Optional[str], Optional[str], Optional[str]]:
72
+ """Get categorty items from labels."""
73
+ prefix = labels.get(CATEGORYPREFIX_LABEL, default)
74
+ category = labels.get(CATEGORY_LABEL, default)
75
+ version = labels.get(CATEGORYVERSION_LABEL, default) or None
76
+ return prefix, category, version
77
+
78
+
79
+ def event_match(labels: Dict[str, str], event: Dict[str, str]) -> bool:
80
+ """Return True if event matches labels."""
81
+
82
+ def _match(what: str) -> bool:
83
+ return event[what] in ('_', labels.get(f'opentestfactory.org/{what}'))
84
+
85
+ if not ('category' in event or 'categoryPrefix' in event):
86
+ return False
87
+ if 'category' in event and 'categoryPrefix' in event:
88
+ match = _match('category') and _match('categoryPrefix')
89
+ elif 'category' in event:
90
+ match = _match('category')
91
+ else:
92
+ match = _match('categoryPrefix')
93
+ if match and 'categoryVersion' in event:
94
+ match = _match('categoryVersion')
95
+ return match
96
+
97
+
98
+ def match_any(labels: Dict[str, str], events: Iterable[Dict[str, str]]) -> bool:
99
+ """Check if any event matches category labels."""
100
+ return any(event_match(labels, event) for event in events)
opentf/commons/schemas.py CHANGED
@@ -134,31 +134,30 @@ def validate_schema(
134
134
 
135
135
 
136
136
  def read_and_validate(filename: str, schema: str) -> Dict[str, Any]:
137
- """Read and validate a configuration file.
137
+ """Read and validate a JSON or YAML file.
138
138
 
139
139
  # Required parameters
140
140
 
141
- - filename: a string, the configuration file name
142
- - schema: a string, the schema to validate the configuration
141
+ - filename: a string, the file name
142
+ - schema: a string, the schema to validate the file content
143
143
 
144
144
  # Returned value
145
145
 
146
- A dictionary, the configuration.
146
+ A dictionary, the definition.
147
147
 
148
148
  # Raised exceptions
149
149
 
150
- An _OSError_ exception is raised if the configuration file cannot
151
- be read.
150
+ An _OSError_ exception is raised if the file cannot be read.
152
151
 
153
- A _ValueError_ exception is raised if the configuration file is
152
+ A _ValueError_ exception is raised if the JSON or YAML file is
154
153
  invalid.
155
154
  """
156
155
  with open(filename, 'r', encoding='utf-8') as cnf:
157
156
  config = safe_load(cnf)
158
157
 
159
158
  if not isinstance(config, dict):
160
- raise ValueError('Config file is not an object.')
159
+ raise ValueError('File is not a JSON object.')
161
160
  valid, extra = validate_schema(schema or SERVICECONFIG, config)
162
161
  if not valid:
163
- raise ValueError(f'Config file "{filename}" is invalid: {extra}.')
162
+ raise ValueError(f'Invalid content: {extra}.')
164
163
  return config
@@ -14,7 +14,7 @@
14
14
 
15
15
  """A toolkit for creating OpenTestFactory plugins."""
16
16
 
17
- from typing import Any, Callable, Dict, Optional, Tuple
17
+ from typing import Any, Callable, Dict, Optional
18
18
 
19
19
  import os
20
20
  import threading
@@ -44,6 +44,7 @@ from opentf.commons import (
44
44
  validate_inputs,
45
45
  validate_schema,
46
46
  )
47
+ from opentf.commons.meta import read_category_labels, maybe_set_category_labels
47
48
  from opentf.toolkit import core
48
49
 
49
50
 
@@ -73,19 +74,9 @@ def _one_and_only_one(*args) -> bool:
73
74
  return len([arg for arg in args if arg is not None]) == 1
74
75
 
75
76
 
76
- def _get_pcv(
77
- labels: Dict[str, str], default: Optional[str] = None
78
- ) -> Tuple[Optional[str], Optional[str], Optional[str]]:
79
- """Extract prefix, category, version from labels."""
80
- prefix = labels.get('opentestfactory.org/categoryPrefix', default)
81
- category = labels.get('opentestfactory.org/category', default)
82
- version = labels.get('opentestfactory.org/categoryVersion', default) or None
83
- return prefix, category, version
84
-
85
-
86
77
  def _maybe_get_item(cache: Dict[Any, Any], labels: Dict[str, str]) -> Optional[Any]:
87
78
  """Get most relevant item from cache if it exists."""
88
- prefix, category, version = _get_pcv(labels)
79
+ prefix, category, version = read_category_labels(labels)
89
80
 
90
81
  for keys in (
91
82
  (prefix, category, version),
@@ -132,16 +123,21 @@ def _get_target(
132
123
  ) -> Optional[Handler]:
133
124
  """Find target for labels.
134
125
 
135
- Finds the most specific provider. Returns None if no provider
136
- matches.
126
+ - `prefix/category[@vn]` is more specific than `category[@vn]`
127
+ - `category@vn` is more specific than `category`
128
+ - `category[@vn]` is more specific than `prefix`
137
129
 
138
- `prefix/category[@vn]` is more specific than `category[@vn]`.
130
+ # Required parameters
139
131
 
140
- `category@vn` is more specific than `category`.
132
+ - labels: a dictionary, the labels
133
+ - providers: a dictionary, the providers
141
134
 
142
- `category[@vn]` is more specific than `prefix`.
135
+ # Returned value
136
+
137
+ Finds the most specific provider. Returns None if no provider
138
+ matches.
143
139
  """
144
- prefix, category, version = _get_pcv(labels)
140
+ prefix, category, version = read_category_labels(labels)
145
141
 
146
142
  for template in (f'{prefix}/{category}', category):
147
143
  if version:
@@ -174,6 +170,19 @@ INVALID_HOOKS_DEFINITION_TEMPLATE = {
174
170
 
175
171
 
176
172
  def _maybe_add_hook_watcher(plugin: Flask, schema: str) -> None:
173
+ """Add hook watcher if needed.
174
+
175
+ If the `{name}_{type}_HOOKS` environment variable is set, the plugin
176
+ will watch the file and update its hooks accordingly.
177
+
178
+ If the hooks definition is invalid, a default hook is defined
179
+ instead, informing workflows of the issue.
180
+
181
+ # Required parameters
182
+
183
+ - plugin: a Flask object
184
+ - schema: a string, the schema name
185
+ """
177
186
  if plugin.config['CONTEXT'][KIND_KEY] == EXECUTIONCOMMAND:
178
187
  type_ = 'CHANNEL'
179
188
  else:
@@ -264,7 +273,7 @@ def _dispatch_providercommand(
264
273
  plugin.logger.debug(
265
274
  'Calling provider function %s (%s/%s@%s).',
266
275
  handler.__name__,
267
- *_get_pcv(labels, default='_'),
276
+ *read_category_labels(labels, default='_'),
268
277
  )
269
278
  inputs: Dict[str, Any] = body['step'].get('with', {})
270
279
  _ensure_inputs_match(plugin, labels, inputs)
@@ -293,7 +302,7 @@ def _dispatch_generatorcommand(
293
302
  plugin.logger.debug(
294
303
  'Calling generator %s (%s/%s@%s).',
295
304
  handler.__name__,
296
- *_get_pcv(labels, default='_'),
305
+ *read_category_labels(labels, default='_'),
297
306
  )
298
307
  inputs: Dict[str, Any] = body.get('with', {})
299
308
  _ensure_inputs_match(plugin, labels, inputs)
@@ -330,12 +339,12 @@ def _watchdog(plugin: Flask, polling_delay: int) -> None:
330
339
  try:
331
340
  current_modified_time = os.stat(file).st_mtime
332
341
  except OSError as err:
333
- plugin.logger.debug("Could not stat file '%s': %s.", file, err)
342
+ plugin.logger.debug('Could not stat file "%s": %s.', file, err)
334
343
  current_modified_time = 0
335
344
  if current_modified_time == files_stat[file] and not first:
336
345
  continue
337
346
  if files_stat[file] != current_modified_time and not first:
338
- plugin.logger.debug("Watched file '%s' has changed.", file)
347
+ plugin.logger.debug('Watched file "%s" has changed.', file)
339
348
  files_stat[file] = current_modified_time
340
349
  _run_handlers(plugin, file, list(files_handlers[file]))
341
350
  first = False
@@ -451,12 +460,7 @@ def _subscribe(
451
460
  context = plugin.config['CONTEXT']
452
461
  kind = context[KIND_KEY]
453
462
  labels = {}
454
- if cat is not None:
455
- labels['opentestfactory.org/category'] = cat
456
- if cat_prefix is not None:
457
- labels['opentestfactory.org/categoryPrefix'] = cat_prefix
458
- if cat_version is not None:
459
- labels['opentestfactory.org/categoryVersion'] = cat_version
463
+ maybe_set_category_labels(labels, cat_prefix, cat, cat_version)
460
464
  context[INPUTS_KEY][(cat_prefix, cat, cat_version)] = (
461
465
  manifest.get('inputs', {}),
462
466
  manifest.get('additionalInputs'),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opentf-toolkit-nightly
3
- Version: 0.62.0.dev1326
3
+ Version: 0.62.0.dev1333
4
4
  Summary: OpenTestFactory Orchestrator Toolkit
5
5
  Home-page: https://gitlab.com/henixdevelopment/open-source/opentestfactory/python-toolkit
6
6
  Author: Martin Lafaix
@@ -1,10 +1,11 @@
1
- opentf/commons/__init__.py,sha256=965jZDUnSWT8h6Bw283JgNp5mkxzEM0KMkoIwijWXAw,27083
1
+ opentf/commons/__init__.py,sha256=sa_sDz-XnbmJTqe9E_o6orixV9RoMLm-bqm-A3BRvvc,27210
2
2
  opentf/commons/auth.py,sha256=gXRp_0Tf3bfd65F4QiQmh6C6vR9y3ugag_0DSvozJFk,15898
3
3
  opentf/commons/config.py,sha256=RVSSdQhMle4oCo_z_AR2EQ4U6sUjSxw-qVBtjKuJVfo,10219
4
4
  opentf/commons/exceptions.py,sha256=7dhUXO8iyAbqVwlUKxZhgRzGqVcb7LkG39hFlm-VxIA,2407
5
5
  opentf/commons/expressions.py,sha256=jM_YKXVOFhvOE2aE2IuacuvxhIsOYTFs2oQkpcbWR6g,19645
6
+ opentf/commons/meta.py,sha256=ygSO3mE2d-Ux62abzK1wYk86noT4R5Tumd90nyZo0MU,3322
6
7
  opentf/commons/pubsub.py,sha256=M0bvajR9raUP-xe5mfRjdrweZyHQw1_Qsy56gS-Sck4,7676
7
- opentf/commons/schemas.py,sha256=u1TdoGFNWoslWV7foBqXPgkGTQtRZWtXTr_PEd_TsFY,5189
8
+ opentf/commons/schemas.py,sha256=MbUt4XLiFKdyL6g-gKwgnKVUs7f5CXrY89vuqT0ehRM,5128
8
9
  opentf/commons/selectors.py,sha256=2mmnvfZ13KizBQLsIvHXPU0Qtf6hkIvJpYdejNRszUs,7203
9
10
  opentf/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
11
  opentf/schemas/abac.opentestfactory.org/v1alpha1/Policy.json,sha256=JXsfNAPSEYggeyaDutSQBeG38o4Bmcr70dPLWWeqIh8,2105
@@ -55,11 +56,11 @@ opentf/schemas/opentestfactory.org/v1beta1/Workflow.json,sha256=QZ8mM9PhzsI9gTmw
55
56
  opentf/schemas/opentestfactory.org/v1beta2/ServiceConfig.json,sha256=rEvK2YWL5lG94_qYgR_GnLWNsaQhaQ-2kuZdWJr5NnY,3517
56
57
  opentf/scripts/launch_java_service.sh,sha256=S0jAaCuv2sZy0Gf2NGBuPX-eD531rcM-b0fNyhmzSjw,2423
57
58
  opentf/scripts/startup.py,sha256=AcVXU2auPvqMb_6OpGzkVqrpgYV6vz7x_Rnv8YbAEkk,23114
58
- opentf/toolkit/__init__.py,sha256=xh0XggCuR4jumiYDeMGaklktMmV8_9HAb6K5z1oJzdU,22327
59
+ opentf/toolkit/__init__.py,sha256=7MGAfKb5V9ckzWE8ozfj0PcC7g0a-0VfdyrClkWTk38,22319
59
60
  opentf/toolkit/channels.py,sha256=6qKSsAgq_oJpuDRiKqVUz-EAjdfikcCG3SFAGmKZdhQ,25551
60
61
  opentf/toolkit/core.py,sha256=fqnGgaYnuVcd4fyeNIwpc0QtyUo7jsKeVgdkBfY3iqo,9443
61
- opentf_toolkit_nightly-0.62.0.dev1326.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
62
- opentf_toolkit_nightly-0.62.0.dev1326.dist-info/METADATA,sha256=ytSfAqquNNTmxyGd9HHWdZe9LqWjQdU77cIc4-Ox0Ek,2214
63
- opentf_toolkit_nightly-0.62.0.dev1326.dist-info/WHEEL,sha256=tTnHoFhvKQHCh4jz3yCn0WPTYIy7wXx3CJtJ7SJGV7c,91
64
- opentf_toolkit_nightly-0.62.0.dev1326.dist-info/top_level.txt,sha256=_gPuE6GTT6UNXy1DjtmQSfCcZb_qYA2vWmjg7a30AGk,7
65
- opentf_toolkit_nightly-0.62.0.dev1326.dist-info/RECORD,,
62
+ opentf_toolkit_nightly-0.62.0.dev1333.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
63
+ opentf_toolkit_nightly-0.62.0.dev1333.dist-info/METADATA,sha256=Fw7qG9XcomeHvCOAz_zt_HxYs3nfs1ObRWYkYLV3ntA,2214
64
+ opentf_toolkit_nightly-0.62.0.dev1333.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
65
+ opentf_toolkit_nightly-0.62.0.dev1333.dist-info/top_level.txt,sha256=_gPuE6GTT6UNXy1DjtmQSfCcZb_qYA2vWmjg7a30AGk,7
66
+ opentf_toolkit_nightly-0.62.0.dev1333.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (77.0.1)
2
+ Generator: setuptools (77.0.3)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5