opentf-toolkit-nightly 0.57.0.dev1057__py3-none-any.whl → 0.57.0.dev1074__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.
@@ -14,7 +14,7 @@
14
14
 
15
15
  """Datasources (testcases, tags and jobs) retrieval helpers"""
16
16
 
17
- from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
17
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
18
18
 
19
19
  from datetime import datetime
20
20
 
@@ -22,6 +22,7 @@ from datetime import datetime
22
22
  from flask import current_app
23
23
 
24
24
  from opentf.commons.expressions import evaluate_bool
25
+ from opentf.commons.selectors import match_selectors
25
26
 
26
27
 
27
28
  ########################################################################
@@ -99,9 +100,11 @@ def parse_testcase_name(full_name: str) -> Tuple[str, str]:
99
100
  ## Datasource: Testcases
100
101
 
101
102
 
102
- def in_scope(expr: str, contexts: Dict[str, Any]) -> bool:
103
+ def in_scope(expr: Union[str, bool], contexts: Dict[str, Any]) -> bool:
103
104
  """Safely evaluate datasource scope."""
104
105
  try:
106
+ if isinstance(expr, bool):
107
+ return expr
105
108
  return evaluate_bool(expr, contexts)
106
109
  except ValueError as err:
107
110
  raise ValueError(f'Invalid conditional {expr}: {err}.')
@@ -399,9 +402,7 @@ def _get_testresult_labels(
399
402
 
400
403
 
401
404
  def _make_testcase_from_testresult(
402
- item: Dict[str, Any],
403
- labels: Dict[str, Any],
404
- scope: str,
405
+ item: Dict[str, Any], labels: Dict[str, Any], scope: Union[str, bool]
405
406
  ) -> Dict[str, Any]:
406
407
  suite_name, testcase_name = parse_testcase_name(item['name'])
407
408
  item_data = {
@@ -434,8 +435,55 @@ def _make_testcase_from_testresult(
434
435
  return testcase
435
436
 
436
437
 
438
+ def _get_max_count(state: Dict[str, Any]) -> int:
439
+ if state['reset']:
440
+ return state['per_page'] * state['page']
441
+ return state['per_page']
442
+
443
+
444
+ def _extract_testcases(
445
+ testresults: List[Dict[str, Any]],
446
+ state: Dict[str, Any],
447
+ scope: Union[str, bool],
448
+ events: List[Dict[str, Any]],
449
+ ) -> Dict[str, Dict[str, Any]]:
450
+ testcases = {}
451
+ items = 0
452
+ for i, testresult in enumerate(
453
+ testresults[state['last_notification_used'] :],
454
+ start=state['last_notification_used'],
455
+ ):
456
+ if i == state['last_notification_used']:
457
+ last_testresult_used = state['last_testresult_used']
458
+ else:
459
+ last_testresult_used = 0
460
+ execution_id = testresult['metadata']['attachment_origin'][0]
461
+ labels = _get_testresult_labels(execution_id, events)
462
+ if not labels:
463
+ continue
464
+ for j, item in enumerate(
465
+ testresult['spec']['testResults'][last_testresult_used:],
466
+ start=last_testresult_used,
467
+ ):
468
+ testcase = _make_testcase_from_testresult(item, labels, scope)
469
+ if not testcase:
470
+ continue
471
+ if not match_selectors(testcase, state['fieldselector']):
472
+ continue
473
+ testcases[item['id']] = testcase
474
+ items += 1
475
+ if items > _get_max_count(state):
476
+ state['last_notification_used'] = i
477
+ state['last_testresult_used'] = j
478
+ return testcases
479
+
480
+ state['last_notification_used'] = i + 1
481
+ state['last_testresult_used'] = 0
482
+ return testcases
483
+
484
+
437
485
  def get_testcases(
438
- events: List[Dict[str, Any]], scope: str = 'true'
486
+ events: List[Dict[str, Any]], scope: Union[str, bool] = True, state=None
439
487
  ) -> Dict[str, Dict[str, Any]]:
440
488
  """Extract metadata for each test result.
441
489
 
@@ -489,6 +537,9 @@ def get_testcases(
489
537
  A _ValueError_ exception is raised if there were no test results in
490
538
  `events` or some scope errors occured retrieving test results.
491
539
  """
540
+ if not state:
541
+ raise ValueError('No workflow cache state received from observer.')
542
+
492
543
  if _uses_inception(events):
493
544
  testresults = _get_inception_testresults(events)
494
545
  else:
@@ -497,17 +548,10 @@ def get_testcases(
497
548
  if not testresults:
498
549
  raise ValueError('No test results in events.')
499
550
 
500
- testcases = {}
501
- for testresult in testresults:
502
- execution_id = testresult['metadata']['attachment_origin'][0]
503
- labels = _get_testresult_labels(execution_id, events)
504
- if not labels:
505
- continue
506
- for item in testresult['spec']['testResults']:
507
- if testcase := _make_testcase_from_testresult(item, labels, scope):
508
- testcases[item['id']] = testcase
551
+ testcases = _extract_testcases(testresults, state, scope, events)
509
552
  if not testcases:
510
553
  raise ValueError(f'No test cases matching scope `{scope}`.')
554
+
511
555
  return testcases
512
556
 
513
557
 
@@ -538,7 +582,9 @@ def _make_tag_datasource(tag: str, parent: Dict[str, Any]) -> Dict[str, Any]:
538
582
  }
539
583
 
540
584
 
541
- def get_tags(events: List[Dict[str, Any]], scope: str = 'true') -> Dict[str, Any]:
585
+ def get_tags(
586
+ events: List[Dict[str, Any]], scope: Union[str, bool] = True, state=None
587
+ ) -> Dict[str, Any]:
542
588
  """Extract metadata for each execution environment tag.
543
589
 
544
590
  # Required parameters:
@@ -574,7 +620,7 @@ def get_tags(events: List[Dict[str, Any]], scope: str = 'true') -> Dict[str, Any
574
620
  'No job events found in workflow. Cannot extract data for tags.'
575
621
  )
576
622
  try:
577
- testcases = get_testcases(events, scope)
623
+ testcases = get_testcases(events, scope, state)
578
624
  except ValueError as err:
579
625
  if str(err).startswith('[SCOPE ERROR] '):
580
626
  raise ValueError(str(err))
@@ -711,7 +757,9 @@ def _make_job_datasource(
711
757
  }
712
758
 
713
759
 
714
- def get_jobs(events: List[Dict[str, Any]], scope: str = 'true') -> Dict[str, Any]:
760
+ def get_jobs(
761
+ events: List[Dict[str, Any]], scope: Union[str, bool] = True, state=None
762
+ ) -> Dict[str, Any]:
715
763
  """Extract metadata for each job.
716
764
 
717
765
  # Required parameters:
@@ -764,7 +812,7 @@ def get_jobs(events: List[Dict[str, Any]], scope: str = 'true') -> Dict[str, Any
764
812
  )
765
813
 
766
814
  try:
767
- testcases = get_testcases(events, scope)
815
+ testcases = get_testcases(events, scope, state)
768
816
  except ValueError as err:
769
817
  if str(err).startswith('[SCOPE ERROR] '):
770
818
  raise ValueError(str(err))
opentf/commons/schemas.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 Henix, Henix.fr
1
+ # Copyright (c) Henix, Henix.fr
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@ import json
20
20
  import logging
21
21
  import os
22
22
 
23
- from jsonschema import validate, ValidationError
23
+ from jsonschema import Draft201909Validator, ValidationError
24
24
 
25
25
  import opentf.schemas
26
26
 
@@ -41,10 +41,10 @@ WORKFLOWCANCELED = 'opentestfactory.org/v1alpha1/WorkflowCanceled'
41
41
  WORKFLOWRESULT = 'opentestfactory.org/v1alpha1/WorkflowResult'
42
42
 
43
43
  GENERATORCOMMAND = 'opentestfactory.org/v1alpha1/GeneratorCommand'
44
- GENERATORRESULT = 'opentestfactory.org/v1beta1/GeneratorResult'
44
+ GENERATORRESULT = 'opentestfactory.org/v1/GeneratorResult'
45
45
 
46
46
  PROVIDERCOMMAND = 'opentestfactory.org/v1beta1/ProviderCommand'
47
- PROVIDERRESULT = 'opentestfactory.org/v1beta1/ProviderResult'
47
+ PROVIDERRESULT = 'opentestfactory.org/v1/ProviderResult'
48
48
 
49
49
  EXECUTIONCOMMAND = 'opentestfactory.org/v1beta1/ExecutionCommand'
50
50
  EXECUTIONRESULT = 'opentestfactory.org/v1alpha1/ExecutionResult'
@@ -67,6 +67,7 @@ INSIGHT_COLLECTOR = 'opentestfactory.org/v1alpha1/InsightCollector'
67
67
  # JSON Schema Helpers
68
68
 
69
69
  _schemas = {}
70
+ _validators = {}
70
71
 
71
72
  SCHEMAS_ROOT_DIRECTORY = list(opentf.schemas.__path__)[0]
72
73
 
@@ -101,6 +102,12 @@ def get_schema(name: str) -> Dict[str, Any]:
101
102
  return _schemas[name]
102
103
 
103
104
 
105
+ def _validator(schema: str):
106
+ if schema not in _validators:
107
+ _validators[schema] = Draft201909Validator(get_schema(schema))
108
+ return _validators[schema]
109
+
110
+
104
111
  def validate_schema(schema, instance) -> Tuple[bool, Any]:
105
112
  """Return True if instance validates schema.
106
113
 
@@ -120,7 +127,7 @@ def validate_schema(schema, instance) -> Tuple[bool, Any]:
120
127
  is logged and raised again.
121
128
  """
122
129
  try:
123
- validate(schema=get_schema(schema), instance=instance)
130
+ _validator(schema).validate(instance=instance)
124
131
  except ValidationError as err:
125
132
  return False, err
126
133
  return True, None
@@ -0,0 +1,484 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2019-09/schema#",
3
+ "title": "JSON SCHEMA for opentestfactory.org/v1 GeneratorResult manifests",
4
+ "type": "object",
5
+ "properties": {
6
+ "apiVersion": {
7
+ "union": [
8
+ "opentestfactory.org/v1alpha1",
9
+ "opentestfactory.org/v1beta1",
10
+ "opentestfactory.org/v1"
11
+ ]
12
+ },
13
+ "kind": {
14
+ "const": "GeneratorResult"
15
+ },
16
+ "metadata": {
17
+ "type": "object",
18
+ "properties": {
19
+ "name": {
20
+ "type": "string"
21
+ },
22
+ "namespace": {
23
+ "type": "string",
24
+ "pattern": "^[a-z0-9][a-z0-9-]*$"
25
+ },
26
+ "workflow_id": {
27
+ "type": "string"
28
+ },
29
+ "job_id": {
30
+ "type": "string"
31
+ },
32
+ "job_origin": {
33
+ "type": "array",
34
+ "items": {
35
+ "type": "string"
36
+ }
37
+ },
38
+ "labels": {
39
+ "type": "object",
40
+ "patternProperties": {
41
+ "^([a-zA-Z0-9-.]+/)?[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$": {
42
+ "type": "string"
43
+ }
44
+ },
45
+ "minProperties": 1,
46
+ "additionalProperties": false
47
+ }
48
+ },
49
+ "additionalProperties": true,
50
+ "required": [
51
+ "name",
52
+ "workflow_id",
53
+ "job_id",
54
+ "job_origin"
55
+ ]
56
+ },
57
+ "jobs": {
58
+ "$ref": "#/definitions/jobs"
59
+ },
60
+ "outputs": {
61
+ "$ref": "#/definitions/outputs"
62
+ }
63
+ },
64
+ "required": [
65
+ "apiVersion",
66
+ "kind",
67
+ "metadata",
68
+ "jobs"
69
+ ],
70
+ "additionalProperties": false,
71
+ "definitions": {
72
+ "defaults": {
73
+ "type": "object",
74
+ "propertyNames": {
75
+ "pattern": "^[a-zA-Z][a-zA-Z0-9-]*$"
76
+ }
77
+ },
78
+ "job-generator": {
79
+ "type": "object",
80
+ "properties": {
81
+ "name": {
82
+ "type": "string"
83
+ },
84
+ "if": {
85
+ "type": "string"
86
+ },
87
+ "concurrency": {
88
+ "$ref": "#/definitions/concurrency"
89
+ },
90
+ "runs-on": {
91
+ "$ref": "#/definitions/runs-on"
92
+ },
93
+ "needs": {
94
+ "$ref": "#/definitions/needs"
95
+ },
96
+ "defaults": {
97
+ "$ref": "#/definitions/defaults"
98
+ },
99
+ "variables": {
100
+ "$ref": "#/definitions/variables"
101
+ },
102
+ "timeout-minutes": {
103
+ "$ref": "#/definitions/number-expression"
104
+ },
105
+ "strategy": {
106
+ "$ref": "#/definitions/strategy"
107
+ },
108
+ "jobs": {
109
+ "$ref": "#/definitions/jobs"
110
+ },
111
+ "generator": {
112
+ "type": "string"
113
+ },
114
+ "uses": {
115
+ "type": "string"
116
+ },
117
+ "with": {
118
+ "$ref": "#/definitions/with"
119
+ },
120
+ "outputs": {
121
+ "$ref": "#/definitions/outputs"
122
+ },
123
+ "continue-on-error": {
124
+ "$ref": "#/definitions/boolean-expression"
125
+ }
126
+ },
127
+ "oneOf": [
128
+ {
129
+ "required": [
130
+ "generator"
131
+ ]
132
+ },
133
+ {
134
+ "required": [
135
+ "uses"
136
+ ]
137
+ },
138
+ {
139
+ "required": [
140
+ "jobs"
141
+ ]
142
+ }
143
+ ],
144
+ "additionalProperties": false
145
+ },
146
+ "job-steps": {
147
+ "type": "object",
148
+ "properties": {
149
+ "name": {
150
+ "type": "string"
151
+ },
152
+ "if": {
153
+ "type": "string"
154
+ },
155
+ "concurrency": {
156
+ "$ref": "#/definitions/concurrency"
157
+ },
158
+ "runs-on": {
159
+ "$ref": "#/definitions/runs-on"
160
+ },
161
+ "needs": {
162
+ "$ref": "#/definitions/needs"
163
+ },
164
+ "defaults": {
165
+ "$ref": "#/definitions/defaults"
166
+ },
167
+ "variables": {
168
+ "$ref": "#/definitions/variables"
169
+ },
170
+ "timeout-minutes": {
171
+ "$ref": "#/definitions/number-expression"
172
+ },
173
+ "strategy": {
174
+ "$ref": "#/definitions/strategy"
175
+ },
176
+ "steps": {
177
+ "type": "array",
178
+ "minItems": 1,
179
+ "items": {
180
+ "anyOf": [
181
+ {
182
+ "type": "object",
183
+ "properties": {
184
+ "name": {
185
+ "type": "string"
186
+ },
187
+ "id": {
188
+ "type": "string"
189
+ },
190
+ "if": {
191
+ "type": "string"
192
+ },
193
+ "uses": {
194
+ "type": "string"
195
+ },
196
+ "with": {
197
+ "$ref": "#/definitions/with"
198
+ },
199
+ "variables": {
200
+ "$ref": "#/definitions/variables"
201
+ },
202
+ "timeout-minutes": {
203
+ "$ref": "#/definitions/number-expression"
204
+ },
205
+ "continue-on-error": {
206
+ "$ref": "#/definitions/boolean-expression"
207
+ },
208
+ "working-directory": {
209
+ "type": "string"
210
+ }
211
+ },
212
+ "required": [
213
+ "uses"
214
+ ],
215
+ "additionalProperties": false
216
+ },
217
+ {
218
+ "type": "object",
219
+ "properties": {
220
+ "name": {
221
+ "type": "string"
222
+ },
223
+ "id": {
224
+ "type": "string"
225
+ },
226
+ "if": {
227
+ "type": "string"
228
+ },
229
+ "run": {
230
+ "type": "string"
231
+ },
232
+ "shell": {
233
+ "type": "string"
234
+ },
235
+ "variables": {
236
+ "$ref": "#/definitions/variables"
237
+ },
238
+ "timeout-minutes": {
239
+ "$ref": "#/definitions/number-expression"
240
+ },
241
+ "continue-on-error": {
242
+ "$ref": "#/definitions/boolean-expression"
243
+ },
244
+ "working-directory": {
245
+ "type": "string"
246
+ }
247
+ },
248
+ "required": [
249
+ "run"
250
+ ],
251
+ "additionalProperties": false
252
+ }
253
+ ]
254
+ }
255
+ },
256
+ "outputs": {
257
+ "$ref": "#/definitions/outputs"
258
+ },
259
+ "continue-on-error": {
260
+ "$ref": "#/definitions/boolean-expression"
261
+ }
262
+ },
263
+ "required": [
264
+ "steps"
265
+ ],
266
+ "additionalProperties": false
267
+ },
268
+ "needs": {
269
+ "anyOf": [
270
+ {
271
+ "type": "string",
272
+ "pattern": "(^[a-zA-Z_][a-zA-Z0-9_-]*$)|(^\\$\\{\\{.*\\}\\}$)"
273
+ },
274
+ {
275
+ "type": "array",
276
+ "minItems": 1,
277
+ "items": {
278
+ "type": "string",
279
+ "pattern": "(^[a-zA-Z_][a-zA-Z0-9_-]*$)|(^\\$\\{\\{.*\\}\\}$)"
280
+ }
281
+ }
282
+ ]
283
+ },
284
+ "number-expression": {
285
+ "anyOf": [
286
+ {
287
+ "type": "number"
288
+ },
289
+ {
290
+ "type": "string",
291
+ "pattern": "^\\$\\{\\{.*\\}\\}$"
292
+ }
293
+ ]
294
+ },
295
+ "boolean-expression": {
296
+ "anyOf": [
297
+ {
298
+ "type": "boolean"
299
+ },
300
+ {
301
+ "type": "string",
302
+ "pattern": "^\\$\\{\\{.*\\}\\}$"
303
+ }
304
+ ]
305
+ },
306
+ "identifier-expression": {
307
+ "type": "string",
308
+ "pattern": "(^[a-zA-Z][a-zA-Z0-9-]*$)|(^\\$\\{\\{.*\\}\\}$)"
309
+ },
310
+ "runs-on": {
311
+ "anyOf": [
312
+ {
313
+ "$ref": "#/definitions/identifier-expression"
314
+ },
315
+ {
316
+ "type": "array",
317
+ "minItems": 1,
318
+ "items": {
319
+ "$ref": "#/definitions/identifier-expression"
320
+ }
321
+ },
322
+ {
323
+ "type": "object"
324
+ }
325
+ ]
326
+ },
327
+ "variables": {
328
+ "type": "object",
329
+ "patternProperties": {
330
+ "^[a-zA-Z0-9_]+$": {
331
+ "oneOf": [
332
+ {
333
+ "type": "string"
334
+ },
335
+ {
336
+ "type": "object",
337
+ "properties": {
338
+ "value": {
339
+ "type": "string"
340
+ },
341
+ "verbatim": {
342
+ "type": "boolean"
343
+ }
344
+ },
345
+ "required": [
346
+ "value"
347
+ ],
348
+ "additionalProperties": false
349
+ }
350
+ ]
351
+ }
352
+ },
353
+ "minProperties": 1
354
+ },
355
+ "strategy": {
356
+ "type": "object",
357
+ "properties": {
358
+ "max-parallel": {
359
+ "$ref": "#/definitions/number-expression"
360
+ },
361
+ "fail-fast": {
362
+ "$ref": "#/definitions/boolean-expression"
363
+ },
364
+ "matrix": {
365
+ "type": "object",
366
+ "properties": {
367
+ "exclude": {
368
+ "type": "array",
369
+ "minItems": 1,
370
+ "items": {
371
+ "type": "object",
372
+ "patternProperties": {
373
+ "^[a-zA-Z][a-zA-Z0-9-]*$": {
374
+ "type": "string"
375
+ }
376
+ }
377
+ }
378
+ },
379
+ "include": {
380
+ "type": "array",
381
+ "minItems": 1,
382
+ "items": {
383
+ "type": "object",
384
+ "patternProperties": {
385
+ "^[a-zA-Z][a-zA-Z0-9-]*$": {
386
+ "type": "string"
387
+ }
388
+ }
389
+ }
390
+ }
391
+ },
392
+ "patternProperties": {
393
+ "^[a-zA-Z][a-zA-Z0-9-]*$": {
394
+ "type": "array",
395
+ "minItems": 1,
396
+ "items": {
397
+ "oneOf": [
398
+ {
399
+ "type": "string"
400
+ },
401
+ {
402
+ "type": "number"
403
+ },
404
+ {
405
+ "type": "object",
406
+ "patternProperties": {
407
+ "^[a-zA-Z][a-zA-Z0-9-]*$": {
408
+ "type": "string"
409
+ }
410
+ }
411
+ }
412
+ ]
413
+ }
414
+ }
415
+ }
416
+ }
417
+ },
418
+ "minProperties": 1,
419
+ "additionalProperties": false
420
+ },
421
+ "concurrency": {
422
+ "type": "object",
423
+ "properties": {
424
+ "group": {
425
+ "type": "string"
426
+ },
427
+ "cancel-in-progress": {
428
+ "$ref": "#/definitions/boolean-expression"
429
+ }
430
+ },
431
+ "additionalProperties": false,
432
+ "required": [
433
+ "group"
434
+ ]
435
+ },
436
+ "jobs": {
437
+ "type": "object",
438
+ "patternProperties": {
439
+ "^[a-zA-Z_][a-zA-Z0-9_-]*$": {
440
+ "oneOf": [
441
+ {
442
+ "$ref": "#/definitions/job-generator"
443
+ },
444
+ {
445
+ "$ref": "#/definitions/job-steps"
446
+ }
447
+ ]
448
+ }
449
+ },
450
+ "minProperties": 1
451
+ },
452
+ "with": {
453
+ "type": "object"
454
+ },
455
+ "outputs": {
456
+ "type": "object",
457
+ "patternProperties": {
458
+ "^[a-zA-Z_][a-zA-Z0-9_-]*$": {
459
+ "oneOf": [
460
+ {
461
+ "type": "object",
462
+ "properties": {
463
+ "description": {
464
+ "type": "string"
465
+ },
466
+ "value": {
467
+ "type": "string"
468
+ }
469
+ },
470
+ "required": [
471
+ "value"
472
+ ],
473
+ "additionalProperties": false
474
+ },
475
+ {
476
+ "type": "string"
477
+ }
478
+ ]
479
+ }
480
+ },
481
+ "minProperties": 1
482
+ }
483
+ }
484
+ }
@@ -0,0 +1,297 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2019-09/schema#",
3
+ "title": "JSON SCHEMA for opentestfactory.org/v1 ProviderResult manifests",
4
+ "type": "object",
5
+ "properties": {
6
+ "apiVersion": {
7
+ "enum": [
8
+ "opentestfactory.org/v1alpha1",
9
+ "opentestfactory.org/v1beta1",
10
+ "opentestfactory.org/v1"
11
+ ]
12
+ },
13
+ "kind": {
14
+ "const": "ProviderResult"
15
+ },
16
+ "metadata": {
17
+ "type": "object",
18
+ "properties": {
19
+ "name": {
20
+ "type": "string"
21
+ },
22
+ "namespace": {
23
+ "type": "string",
24
+ "pattern": "^[a-z0-9][a-z0-9-]*$"
25
+ },
26
+ "workflow_id": {
27
+ "type": "string"
28
+ },
29
+ "job_id": {
30
+ "type": "string"
31
+ },
32
+ "job_origin": {
33
+ "type": "array",
34
+ "items": {
35
+ "type": "string"
36
+ }
37
+ },
38
+ "step_id": {
39
+ "type": "string"
40
+ },
41
+ "step_origin": {
42
+ "type": "array",
43
+ "items": {
44
+ "type": "string"
45
+ }
46
+ },
47
+ "labels": {
48
+ "type": "object",
49
+ "patternProperties": {
50
+ "^([a-zA-Z0-9-.]+/)?[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$": {
51
+ "type": "string"
52
+ }
53
+ },
54
+ "minProperties": 1,
55
+ "additionalProperties": false
56
+ }
57
+ },
58
+ "additionalProperties": true,
59
+ "required": [
60
+ "name",
61
+ "workflow_id",
62
+ "job_id",
63
+ "job_origin",
64
+ "step_id",
65
+ "step_origin"
66
+ ]
67
+ },
68
+ "hooks": {
69
+ "$ref": "#/definitions/hooks"
70
+ },
71
+ "steps": {
72
+ "type": "array",
73
+ "minItems": 0,
74
+ "items": {
75
+ "anyOf": [
76
+ {
77
+ "type": "object",
78
+ "properties": {
79
+ "name": {
80
+ "type": "string"
81
+ },
82
+ "id": {
83
+ "type": "string"
84
+ },
85
+ "if": {
86
+ "type": "string"
87
+ },
88
+ "uses": {
89
+ "type": "string"
90
+ },
91
+ "with": {
92
+ "$ref": "#/definitions/with"
93
+ },
94
+ "variables": {
95
+ "$ref": "#/definitions/variables"
96
+ },
97
+ "timeout-minutes": {
98
+ "$ref": "#/definitions/number-expression"
99
+ },
100
+ "continue-on-error": {
101
+ "$ref": "#/definitions/boolean-expression"
102
+ },
103
+ "working-directory": {
104
+ "type": "string"
105
+ }
106
+ },
107
+ "required": [
108
+ "uses"
109
+ ],
110
+ "additionalProperties": false
111
+ },
112
+ {
113
+ "type": "object",
114
+ "properties": {
115
+ "name": {
116
+ "type": "string"
117
+ },
118
+ "id": {
119
+ "type": "string"
120
+ },
121
+ "if": {
122
+ "type": "string"
123
+ },
124
+ "run": {
125
+ "type": "string"
126
+ },
127
+ "shell": {
128
+ "type": "string"
129
+ },
130
+ "variables": {
131
+ "$ref": "#/definitions/variables"
132
+ },
133
+ "timeout-minutes": {
134
+ "$ref": "#/definitions/number-expression"
135
+ },
136
+ "continue-on-error": {
137
+ "$ref": "#/definitions/boolean-expression"
138
+ },
139
+ "working-directory": {
140
+ "type": "string"
141
+ }
142
+ },
143
+ "required": [
144
+ "run"
145
+ ],
146
+ "additionalProperties": false
147
+ }
148
+ ]
149
+ }
150
+ },
151
+ "outputs": {
152
+ "$ref": "#/definitions/outputs"
153
+ }
154
+ },
155
+ "required": [
156
+ "apiVersion",
157
+ "kind",
158
+ "metadata",
159
+ "steps"
160
+ ],
161
+ "additionalProperties": false,
162
+ "definitions": {
163
+ "number-expression": {
164
+ "anyOf": [
165
+ {
166
+ "type": "number"
167
+ },
168
+ {
169
+ "type": "string",
170
+ "pattern": "^\\$\\{\\{.*\\}\\}$"
171
+ }
172
+ ]
173
+ },
174
+ "boolean-expression": {
175
+ "anyOf": [
176
+ {
177
+ "type": "boolean"
178
+ },
179
+ {
180
+ "type": "string",
181
+ "pattern": "^\\$\\{\\{.*\\}\\}$"
182
+ }
183
+ ]
184
+ },
185
+ "variables": {
186
+ "type": "object",
187
+ "patternProperties": {
188
+ "^[a-zA-Z0-9_]+$": {
189
+ "oneOf": [
190
+ {
191
+ "type": "string"
192
+ },
193
+ {
194
+ "type": "object",
195
+ "properties": {
196
+ "value": {
197
+ "type": "string"
198
+ },
199
+ "verbatim": {
200
+ "type": "boolean"
201
+ }
202
+ },
203
+ "required": [
204
+ "value"
205
+ ],
206
+ "additionalProperties": false
207
+ }
208
+ ]
209
+ }
210
+ },
211
+ "minProperties": 1
212
+ },
213
+ "hooks": {
214
+ "type": "array",
215
+ "minItems": 1,
216
+ "items": {
217
+ "type": "object",
218
+ "properties": {
219
+ "name": {
220
+ "type": "string"
221
+ },
222
+ "if": {
223
+ "type": "string"
224
+ },
225
+ "events": {
226
+ "type": "array",
227
+ "minItems": 1
228
+ },
229
+ "before": {
230
+ "type": "array",
231
+ "minItems": 1
232
+ },
233
+ "after": {
234
+ "type": "array",
235
+ "minItems": 1
236
+ }
237
+ },
238
+ "anyOf": [
239
+ {
240
+ "required": [
241
+ "name",
242
+ "events",
243
+ "before"
244
+ ]
245
+ },
246
+ {
247
+ "required": [
248
+ "name",
249
+ "events",
250
+ "after",
251
+ "before"
252
+ ]
253
+ },
254
+ {
255
+ "required": [
256
+ "name",
257
+ "events",
258
+ "after"
259
+ ]
260
+ }
261
+ ],
262
+ "additionalProperties": false
263
+ }
264
+ },
265
+ "with": {
266
+ "type": "object"
267
+ },
268
+ "outputs": {
269
+ "type": "object",
270
+ "patternProperties": {
271
+ "^[a-zA-Z_][a-zA-Z0-9_-]*$": {
272
+ "oneOf": [
273
+ {
274
+ "type": "object",
275
+ "properties": {
276
+ "description": {
277
+ "type": "string"
278
+ },
279
+ "value": {
280
+ "type": "string"
281
+ }
282
+ },
283
+ "required": [
284
+ "value"
285
+ ],
286
+ "additionalProperties": false
287
+ },
288
+ {
289
+ "type": "string"
290
+ }
291
+ ]
292
+ }
293
+ },
294
+ "minProperties": 1
295
+ }
296
+ }
297
+ }
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2021-2023 Henix, Henix.fr
1
+ # Copyright (c) Henix, Henix.fr
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
14
14
 
15
15
  """A toolkit for creating OpenTestFactory plugins."""
16
16
 
17
- from typing import Any, Callable, Dict, Optional
17
+ from typing import Any, Callable, Dict, Optional, Tuple
18
18
 
19
19
  import os
20
20
  import threading
@@ -50,6 +50,7 @@ from opentf.toolkit import core
50
50
  SUBSCRIPTION_KEY = '__subscription uuid__'
51
51
  KIND_KEY = '__kind key__'
52
52
  INPUTS_KEY = '__inputs key__'
53
+ OUTPUTS_KEY = '__outputs key__'
53
54
  WATCHEDFILES_KEY = '__watched files__'
54
55
  WATCHEDFILES_EVENT_KEY = '__watched files event__'
55
56
  DISPATCHQUEUE_KEY = '__dispatch queue__'
@@ -91,6 +92,33 @@ def _normalize_inputs(inputs: Dict[str, Any]) -> None:
91
92
  inputs[normalized] = inputs.pop(key)
92
93
 
93
94
 
95
+ def _get_pcv(
96
+ labels: Dict[str, str], default: Optional[str] = None
97
+ ) -> Tuple[Optional[str], Optional[str], Optional[str]]:
98
+ """Extract prefix, category, version from labels."""
99
+ prefix = labels.get('opentestfactory.org/categoryPrefix', default)
100
+ category = labels.get('opentestfactory.org/category', default)
101
+ version = labels.get('opentestfactory.org/categoryVersion', default) or None
102
+ return prefix, category, version
103
+
104
+
105
+ def _maybe_get_item(cache: Dict[Any, Any], labels: Dict[str, str]) -> Optional[Any]:
106
+ """Get most relevant item from cache if it exists."""
107
+ prefix, category, version = _get_pcv(labels)
108
+
109
+ for keys in (
110
+ (prefix, category, version),
111
+ (None, category, version),
112
+ (prefix, category, None),
113
+ (None, category, None),
114
+ (prefix, None, None),
115
+ ):
116
+ if (entry := cache.get(keys)) is not None:
117
+ return entry
118
+
119
+ return None
120
+
121
+
94
122
  def _ensure_inputs_match(
95
123
  plugin, labels: Dict[str, str], inputs: Dict[str, Any]
96
124
  ) -> None:
@@ -105,20 +133,7 @@ def _ensure_inputs_match(
105
133
  or if an unexpected entry is found.
106
134
  """
107
135
  cache = plugin.config['CONTEXT'][INPUTS_KEY]
108
- prefix = labels.get('opentestfactory.org/categoryPrefix')
109
- category = labels.get('opentestfactory.org/category')
110
- version = labels.get('opentestfactory.org/categoryVersion') or None
111
-
112
- for keys in (
113
- (prefix, category, version),
114
- (None, category, version),
115
- (prefix, category, None),
116
- (None, category, None),
117
- (prefix, None, None),
118
- ):
119
- if (entry := cache.get(keys)) is not None:
120
- break
121
- else:
136
+ if (entry := _maybe_get_item(cache, labels)) is None:
122
137
  return
123
138
 
124
139
  declaration, additional_inputs = entry
@@ -159,9 +174,7 @@ def _get_target(
159
174
 
160
175
  `category[@vn]` is more specific than `prefix`.
161
176
  """
162
- prefix = labels['opentestfactory.org/categoryPrefix']
163
- category = labels['opentestfactory.org/category']
164
- version = labels.get('opentestfactory.org/categoryVersion')
177
+ prefix, category, version = _get_pcv(labels)
165
178
 
166
179
  for template in (f'{prefix}/{category}', category):
167
180
  if version:
@@ -282,13 +295,12 @@ def _dispatch_providercommand(plugin, handler: Handler, body: Dict[str, Any]) ->
282
295
  plugin.logger.debug(
283
296
  'Calling provider function %s (%s/%s@%s).',
284
297
  handler.__name__,
285
- labels.get('opentestfactory.org/categoryPrefix', '_'),
286
- labels.get('opentestfactory.org/category', '_'),
287
- labels.get('opentestfactory.org/categoryVersion', '_'),
298
+ *_get_pcv(labels, default='_'),
288
299
  )
289
300
  inputs: Dict[str, Any] = body['step'].get('with', {})
290
301
  _ensure_inputs_match(plugin, labels, inputs)
291
- core.publish_providerresult(handler(inputs))
302
+ outputs = _maybe_get_item(plugin.config['CONTEXT'][OUTPUTS_KEY], labels)
303
+ core.publish_providerresult(handler(inputs), outputs)
292
304
  except core.ExecutionError as err:
293
305
  core.publish_error(str(err))
294
306
  except Exception as err:
@@ -310,12 +322,14 @@ def _dispatch_generatorcommand(plugin, handler: Handler, body: Dict[str, Any]):
310
322
  plugin.logger.debug(
311
323
  'Calling generator %s (%s/%s@%s).',
312
324
  handler.__name__,
313
- labels.get('opentestfactory.org/categoryPrefix', '_'),
314
- labels.get('opentestfactory.org/category', '_'),
315
- labels.get('opentestfactory.org/categoryVersion', '_'),
325
+ *_get_pcv(labels, default='_'),
316
326
  )
317
327
  inputs: Dict[str, Any] = body.get('with', {})
318
- core.publish_generatorresult(handler(inputs))
328
+ _ensure_inputs_match(plugin, labels, inputs)
329
+ outputs = _maybe_get_item(plugin.config['CONTEXT'][OUTPUTS_KEY], labels)
330
+ core.publish_generatorresult(handler(inputs), outputs)
331
+ except core.ExecutionError as err:
332
+ core.publish_error(str(err))
319
333
  except Exception as err:
320
334
  core.publish_error(f'Unexpected execution error: {err}.')
321
335
 
@@ -464,6 +478,10 @@ def _subscribe(
464
478
  manifest.get('inputs', {}),
465
479
  manifest.get('additionalInputs'),
466
480
  )
481
+ context[OUTPUTS_KEY][(cat_prefix, cat, cat_version)] = {
482
+ k: v['value'] if isinstance(v, dict) else v
483
+ for k, v in manifest.get('outputs', {}).items()
484
+ }
467
485
  return subscribe(kind=kind, target='inbox', app=plugin, labels=labels)
468
486
 
469
487
 
@@ -479,7 +497,8 @@ def run_plugin(plugin):
479
497
  context = plugin.config['CONTEXT']
480
498
  context[SUBSCRIPTION_KEY] = []
481
499
  context[INPUTS_KEY] = {}
482
- if context[KIND_KEY] == PROVIDERCOMMAND:
500
+ context[OUTPUTS_KEY] = {}
501
+ if context[KIND_KEY] in (PROVIDERCOMMAND, GENERATORCOMMAND):
483
502
  for manifest in plugin.config['DESCRIPTOR']:
484
503
  metadata = manifest.get('metadata', {})
485
504
  if metadata.get('name', '').lower() != plugin.name.lower():
@@ -502,37 +521,6 @@ def run_plugin(plugin):
502
521
  context[SUBSCRIPTION_KEY].append(
503
522
  subscribe(kind=EXECUTIONCOMMAND, target='inbox', app=plugin)
504
523
  )
505
- elif context[KIND_KEY] == GENERATORCOMMAND:
506
- for manifest in plugin.config['DESCRIPTOR']:
507
- metadata = manifest.get('metadata', {})
508
- if metadata.get('name', '').lower() != plugin.name.lower():
509
- continue
510
- if 'action' not in metadata:
511
- continue
512
- for event in manifest.get('events', []):
513
- cat_prefix = event.get('categoryPrefix')
514
- cat = event.get('category')
515
- if cat or cat_prefix:
516
- cat_version = event.get('categoryVersion')
517
- labels = {}
518
- if cat is not None:
519
- labels['opentestfactory.org/category'] = cat
520
- if cat_prefix is not None:
521
- labels['opentestfactory.org/categoryPrefix'] = cat_prefix
522
- if cat_version is not None:
523
- labels['opentestfactory.org/categoryVersion'] = cat_version
524
- context[SUBSCRIPTION_KEY].append(
525
- subscribe(
526
- kind=GENERATORCOMMAND,
527
- target='inbox',
528
- labels=labels,
529
- app=plugin,
530
- )
531
- )
532
- else:
533
- plugin.logger.warning(
534
- "At least one of 'category', 'categoryPrefix' required, ignoring."
535
- )
536
524
  run_app(plugin)
537
525
  finally:
538
526
  for subscription_id in plugin.config['CONTEXT'][SUBSCRIPTION_KEY]:
@@ -566,10 +554,10 @@ def make_plugin(
566
554
  - Add publication handler
567
555
  - Create service (not started)
568
556
 
569
- Some 'optional' parameters are required for some publin types:
557
+ Some 'optional' parameters are required for some plugin types:
570
558
 
571
559
  `args` is required for channel handlers. It must be a list of one
572
- element that implements the `__contains__` interface.
560
+ element that implements the `__contains__` protocol.
573
561
 
574
562
  # Required parameters
575
563
 
@@ -615,12 +603,11 @@ def make_plugin(
615
603
  f'Not a valid {kind} request: Missing metadata section',
616
604
  )
617
605
 
618
- if request.remote_addr != '127.0.0.1':
619
- valid, extra = validate_schema(kind, body)
620
- if not valid:
621
- return make_status_response(
622
- 'BadRequest', f'Not a valid {kind} request: {extra}.'
623
- )
606
+ valid, extra = validate_schema(kind, body)
607
+ if not valid:
608
+ return make_status_response(
609
+ 'BadRequest', f'Not a valid {kind} request: {extra}.'
610
+ )
624
611
 
625
612
  if workflow_id := body.get('metadata', {}).get('workflow_id'):
626
613
  g.workflow_id = workflow_id
@@ -677,6 +664,8 @@ def make_plugin(
677
664
  if kind == PROVIDERCOMMAND:
678
665
  _maybe_add_hook_watcher(plugin, schema)
679
666
  plugin.config[DISPATCHQUEUE_KEY] = make_dispatchqueue(plugin)
667
+ elif kind == GENERATORCOMMAND:
668
+ plugin.config[DISPATCHQUEUE_KEY] = make_dispatchqueue(plugin)
680
669
  elif kind == EXECUTIONCOMMAND:
681
670
  _maybe_add_hook_watcher(plugin, CHANNEL_HOOKS)
682
671
  plugin.config[DISPATCHQUEUE_KEY] = make_dispatchqueue(plugin)
opentf/toolkit/core.py CHANGED
@@ -126,7 +126,7 @@ def publish_error(error_details) -> None:
126
126
  publish_event(error)
127
127
 
128
128
 
129
- def publish_providerresult(steps: Iterable) -> None:
129
+ def publish_providerresult(steps: Iterable, outputs: Optional[Dict[str, Any]]) -> None:
130
130
  """Publish ProviderResult event."""
131
131
  command = make_event(
132
132
  PROVIDERRESULT,
@@ -135,18 +135,24 @@ def publish_providerresult(steps: Iterable) -> None:
135
135
  )
136
136
  if hooks := _getplugin().config['CONFIG'].get('hooks'):
137
137
  command['hooks'] = hooks
138
+ if outputs:
139
+ command['outputs'] = outputs
138
140
  for step in command['steps']:
139
141
  step.setdefault('id', make_uuid())
140
142
  publish_event(command)
141
143
 
142
144
 
143
- def publish_generatorresult(jobs: Dict[str, Any]) -> None:
145
+ def publish_generatorresult(
146
+ jobs: Dict[str, Any], outputs: Optional[Dict[str, Any]]
147
+ ) -> None:
144
148
  """Publish GeneratorResult event."""
145
149
  command = make_event(
146
150
  GENERATORRESULT,
147
151
  metadata=_getbody()['metadata'],
148
152
  jobs={k: v.copy() for k, v in jobs.items()},
149
153
  )
154
+ if outputs:
155
+ command['outputs'] = outputs
150
156
  publish_event(command)
151
157
 
152
158
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: opentf-toolkit-nightly
3
- Version: 0.57.0.dev1057
3
+ Version: 0.57.0.dev1074
4
4
  Summary: OpenTestFactory Orchestrator Toolkit
5
5
  Home-page: https://gitlab.com/henixdevelopment/open-source/opentestfactory/python-toolkit
6
6
  Author: Martin Lafaix
@@ -19,8 +19,8 @@ License-File: LICENSE
19
19
  Requires-Dist: requests>=2.31
20
20
  Requires-Dist: PyJWT[crypto]<2.9,>=2.7
21
21
  Requires-Dist: PyYAML>=6
22
- Requires-Dist: Flask>=2.2.3
23
- Requires-Dist: jsonschema>=4.17
22
+ Requires-Dist: Flask<3,>=2.2.3
23
+ Requires-Dist: jsonschema>=4.23
24
24
  Requires-Dist: toposort>=1.10
25
25
  Requires-Dist: waitress>=2.1.2
26
26
  Requires-Dist: paste>=3.5.2
@@ -1,13 +1,15 @@
1
1
  opentf/commons/__init__.py,sha256=Uq-7WvkMoBiF3C1KnhwIL4LCKpT8EvomnuG4MBYpIhs,21994
2
2
  opentf/commons/auth.py,sha256=bM2Z3kxm2Wku1lKXaRAIg37LHvXWAXIZIqjplDfN2P8,15899
3
3
  opentf/commons/config.py,sha256=dyus4K5Zdmcftc3Y9Z1YRkzA1KwiRLHoeAlg2_A49QM,7876
4
- opentf/commons/datasources.py,sha256=4ye-TMtaE88O8GVcWx-FtKXOC8aIZLteR6wfIr7Do8U,25232
4
+ opentf/commons/datasources.py,sha256=LjIjZbf08u1VllPN4fDss0OAg-_7gtRqgpIZ2tLuiHo,26807
5
5
  opentf/commons/expressions.py,sha256=jM_YKXVOFhvOE2aE2IuacuvxhIsOYTFs2oQkpcbWR6g,19645
6
6
  opentf/commons/pubsub.py,sha256=DVrSara5FRfNdPBwXKUkTobqGki0RPDehylTEFcJnFc,7341
7
- opentf/commons/schemas.py,sha256=YSCvlmqc7satt-OqIoYXnmhOyo9h8wIpNyKaBAY4u9c,4039
7
+ opentf/commons/schemas.py,sha256=P6jyIJR5Zw2L7vH3_jhpTiJAHHwjE6MJUmva0_41B3E,4212
8
8
  opentf/commons/selectors.py,sha256=DEpLgRAr5HXSpSYI4liXP2hLUTvOSexFa9Vfa1xIQTk,7134
9
9
  opentf/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  opentf/schemas/abac.opentestfactory.org/v1alpha1/Policy.json,sha256=JXsfNAPSEYggeyaDutSQBeG38o4Bmcr70dPLWWeqIh8,2105
11
+ opentf/schemas/opentestfactory.org/v1/GeneratorResult.json,sha256=neoFocJGkVTQQPtnG5xnbqNLX8ivFBJbanbbPweId7s,16608
12
+ opentf/schemas/opentestfactory.org/v1/ProviderResult.json,sha256=Ej4zhCE3rCqCGKcaeAoIHwSJTV_7fw-rAxhJ52qA-Gs,9641
11
13
  opentf/schemas/opentestfactory.org/v1/Workflow.json,sha256=mpWxJfP3aV3sYzVxxWOivD55Top4J5WXTTKBzp7gjIw,22486
12
14
  opentf/schemas/opentestfactory.org/v1alpha1/AgentRegistration.json,sha256=NQykqU-lKE8LtBhBiFUcpVJq00MRG6dZsoM1xedx6uQ,1230
13
15
  opentf/schemas/opentestfactory.org/v1alpha1/AllureCollectorOutput.json,sha256=-L9DDWA0A4x54bPMn4m6Qwi2tf2nHvzIPFOElTjaVck,1366
@@ -46,11 +48,11 @@ opentf/schemas/opentestfactory.org/v1beta1/Workflow.json,sha256=QZ8mM9PhzsI9gTmw
46
48
  opentf/schemas/opentestfactory.org/v1beta2/ServiceConfig.json,sha256=rEvK2YWL5lG94_qYgR_GnLWNsaQhaQ-2kuZdWJr5NnY,3517
47
49
  opentf/scripts/launch_java_service.sh,sha256=S0jAaCuv2sZy0Gf2NGBuPX-eD531rcM-b0fNyhmzSjw,2423
48
50
  opentf/scripts/startup.py,sha256=Da2zo93pBWbdRmj-wgekgLcF94rpNc3ZkbvR8R0w8XY,21279
49
- opentf/toolkit/__init__.py,sha256=4UbExlqRO8Ew7GYRrMdEDruMIB0zTLSsoVCKfW3vPnQ,23488
51
+ opentf/toolkit/__init__.py,sha256=nbllYXON3Rzd-hU7Cred7r4hPaCPbfSJmoY7cQ6RShc,22589
50
52
  opentf/toolkit/channels.py,sha256=6xcVKHUK2FdyVKIQmPQbakngfVuQDzCcD_lInOdKpro,17171
51
- opentf/toolkit/core.py,sha256=GdmEJ0ikdMdpViEpR4jP-viqfvBUHnpiFCOXwLGThxg,9606
52
- opentf_toolkit_nightly-0.57.0.dev1057.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
53
- opentf_toolkit_nightly-0.57.0.dev1057.dist-info/METADATA,sha256=Rfyt1TuD1vqvhdTcSAs1G_3Iujzd6P8StHAFXiE9dYo,1943
54
- opentf_toolkit_nightly-0.57.0.dev1057.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
55
- opentf_toolkit_nightly-0.57.0.dev1057.dist-info/top_level.txt,sha256=_gPuE6GTT6UNXy1DjtmQSfCcZb_qYA2vWmjg7a30AGk,7
56
- opentf_toolkit_nightly-0.57.0.dev1057.dist-info/RECORD,,
53
+ opentf/toolkit/core.py,sha256=KN6-z8Hmty1thZ-c0mTbcqkF6q1nNrtE8A3F6-wjX4g,9788
54
+ opentf_toolkit_nightly-0.57.0.dev1074.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
55
+ opentf_toolkit_nightly-0.57.0.dev1074.dist-info/METADATA,sha256=bE5iD0Xm2FAWCYQHF8Y2Wvwy_4dZm45uEpnNEI7BWLk,1946
56
+ opentf_toolkit_nightly-0.57.0.dev1074.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
57
+ opentf_toolkit_nightly-0.57.0.dev1074.dist-info/top_level.txt,sha256=_gPuE6GTT6UNXy1DjtmQSfCcZb_qYA2vWmjg7a30AGk,7
58
+ opentf_toolkit_nightly-0.57.0.dev1074.dist-info/RECORD,,