opentf-toolkit-nightly 0.62.0.dev1286__py3-none-any.whl → 0.62.0.dev1295__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.
- opentf/commons/__init__.py +83 -35
- opentf/toolkit/__init__.py +36 -19
- {opentf_toolkit_nightly-0.62.0.dev1286.dist-info → opentf_toolkit_nightly-0.62.0.dev1295.dist-info}/METADATA +1 -1
- {opentf_toolkit_nightly-0.62.0.dev1286.dist-info → opentf_toolkit_nightly-0.62.0.dev1295.dist-info}/RECORD +7 -7
- {opentf_toolkit_nightly-0.62.0.dev1286.dist-info → opentf_toolkit_nightly-0.62.0.dev1295.dist-info}/LICENSE +0 -0
- {opentf_toolkit_nightly-0.62.0.dev1286.dist-info → opentf_toolkit_nightly-0.62.0.dev1295.dist-info}/WHEEL +0 -0
- {opentf_toolkit_nightly-0.62.0.dev1286.dist-info → opentf_toolkit_nightly-0.62.0.dev1295.dist-info}/top_level.txt +0 -0
opentf/commons/__init__.py
CHANGED
|
@@ -181,7 +181,10 @@ def _get_contextparameter_spec(app: Flask, name: str) -> Optional[Dict[str, Any]
|
|
|
181
181
|
Initialize cache if needed, ignoring context parameters specs from
|
|
182
182
|
other services.
|
|
183
183
|
|
|
184
|
-
Adds the
|
|
184
|
+
Adds the following specs if not already present:
|
|
185
|
+
|
|
186
|
+
- `watchdog_polling_delay_seconds`
|
|
187
|
+
- `availability_check_delay_seconds`
|
|
185
188
|
"""
|
|
186
189
|
if PARAMETERS_KEY not in app.config:
|
|
187
190
|
app.config[PARAMETERS_KEY] = []
|
|
@@ -191,14 +194,28 @@ def _get_contextparameter_spec(app: Flask, name: str) -> Optional[Dict[str, Any]
|
|
|
191
194
|
app.config[PARAMETERS_KEY] += manifest.get('spec', {}).get(
|
|
192
195
|
'contextParameters', []
|
|
193
196
|
)
|
|
194
|
-
app.config[PARAMETERS_KEY]
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
197
|
+
known = {spec['name'] for spec in app.config[PARAMETERS_KEY]}
|
|
198
|
+
if 'watchdog_polling_delay_seconds' not in known:
|
|
199
|
+
app.config[PARAMETERS_KEY].append(
|
|
200
|
+
{
|
|
201
|
+
'name': 'watchdog_polling_delay_seconds',
|
|
202
|
+
'descriptiveName': 'files watchdog polling delay in seconds',
|
|
203
|
+
'default': 30,
|
|
204
|
+
'type': 'int',
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
if 'availability_check_delay_seconds' not in known:
|
|
208
|
+
app.config[PARAMETERS_KEY].append(
|
|
209
|
+
{
|
|
210
|
+
'name': 'availability_check_delay_seconds',
|
|
211
|
+
'deprecatedNames': ['availability_check_delay'],
|
|
212
|
+
'descriptiveName': 'availability check frequency in seconds',
|
|
213
|
+
'type': 'int',
|
|
214
|
+
'default': 10,
|
|
215
|
+
'minValue': 10,
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
|
|
202
219
|
app.logger.info('Configuration:')
|
|
203
220
|
parameters = app.config[PARAMETERS_KEY]
|
|
204
221
|
try:
|
|
@@ -465,8 +482,9 @@ def get_context_parameter(
|
|
|
465
482
|
- `{app.name.upper()}_{name.upper()}` environment variable
|
|
466
483
|
- `{name.upper()}` environment variable (if spec is shared)
|
|
467
484
|
- `name` in configuration context
|
|
468
|
-
-
|
|
485
|
+
- for each deprecated name, in order, repeat the three steps above
|
|
469
486
|
- `default` if not None
|
|
487
|
+
- `spec['default']` if spec defines a default value
|
|
470
488
|
|
|
471
489
|
# Required parameters
|
|
472
490
|
|
|
@@ -482,9 +500,29 @@ def get_context_parameter(
|
|
|
482
500
|
|
|
483
501
|
An integer if the parameter has a specification and is expected to
|
|
484
502
|
be of type int. The actual parameter type otherwise.
|
|
503
|
+
|
|
504
|
+
# Spec format
|
|
505
|
+
|
|
506
|
+
(Optional, in `spec.contextParameters` in the service descriptor.)
|
|
507
|
+
|
|
508
|
+
```yaml
|
|
509
|
+
- name: parameter_name
|
|
510
|
+
deprecatedNames: [alternative_parameter_names]
|
|
511
|
+
descriptiveName: parameter description
|
|
512
|
+
shared: true
|
|
513
|
+
type: int
|
|
514
|
+
default: 66
|
|
515
|
+
minValue: 10
|
|
516
|
+
maxValue: 100
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
If `minValue` and/or `maxValue` are defined, the parameter must be
|
|
520
|
+
within the range.
|
|
521
|
+
|
|
522
|
+
If `type` is defined, the parameter must be of the specified type.
|
|
485
523
|
"""
|
|
486
524
|
|
|
487
|
-
def _maybe_validate(v)
|
|
525
|
+
def _maybe_validate(v):
|
|
488
526
|
newv, reason = validator(name, v) if validator else (v, None)
|
|
489
527
|
lhs = f' {spec["descriptiveName"]} ({name}):' if spec else f' {name}:'
|
|
490
528
|
if newv != v:
|
|
@@ -493,36 +531,46 @@ def get_context_parameter(
|
|
|
493
531
|
app.logger.info(f'{lhs} {newv}')
|
|
494
532
|
return newv
|
|
495
533
|
|
|
496
|
-
def _fatal(
|
|
497
|
-
app.logger.error(
|
|
534
|
+
def _fatal(msg: str) -> NoReturn:
|
|
535
|
+
app.logger.error(msg)
|
|
498
536
|
sys.exit(2)
|
|
499
537
|
|
|
500
538
|
spec = _get_contextparameter_spec(app, name)
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
539
|
+
shared = spec and spec.get('shared')
|
|
540
|
+
deprecateds: List[str] = spec.get('deprecatedNames', []) if spec else []
|
|
541
|
+
|
|
542
|
+
for alternative in [name] + deprecateds:
|
|
543
|
+
val = os.environ.get(alternative.upper()) if shared else None
|
|
544
|
+
val = os.environ.get(f'{app.name.upper()}_{alternative.upper()}', val)
|
|
545
|
+
val = val if val is not None else app.config['CONTEXT'].get(alternative)
|
|
546
|
+
if val is not None:
|
|
547
|
+
if alternative != name:
|
|
548
|
+
app.logger.warning(
|
|
549
|
+
f' "{alternative}" is deprecated. Consider using "{name}" instead.'
|
|
550
|
+
)
|
|
551
|
+
break
|
|
552
|
+
else:
|
|
553
|
+
val = default
|
|
554
|
+
|
|
555
|
+
if val is None and spec:
|
|
556
|
+
val = spec.get('default')
|
|
557
|
+
if val is None:
|
|
508
558
|
_fatal(
|
|
509
|
-
'Context parameter
|
|
510
|
-
name,
|
|
559
|
+
f'Context parameter "{name}" not in current context and no default value specified.'
|
|
511
560
|
)
|
|
512
561
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
_fatal(f'{desc} must be less that {spec["maxValue"]+1}.')
|
|
562
|
+
if spec:
|
|
563
|
+
if spec.get('type') == 'int':
|
|
564
|
+
try:
|
|
565
|
+
val = int(val)
|
|
566
|
+
except ValueError as err:
|
|
567
|
+
_fatal(f'Context parameter "{name}" not an integer: {err}.')
|
|
568
|
+
desc = spec['descriptiveName'][0].upper() + spec['descriptiveName'][1:]
|
|
569
|
+
if 'minValue' in spec and val < spec['minValue']:
|
|
570
|
+
_fatal(f'{desc} must be greater than {spec["minValue"]-1}.')
|
|
571
|
+
if 'maxValue' in spec and val > spec['maxValue']:
|
|
572
|
+
_fatal(f'{desc} must be less that {spec["maxValue"]+1}.')
|
|
573
|
+
|
|
526
574
|
return _maybe_validate(val)
|
|
527
575
|
|
|
528
576
|
|
opentf/toolkit/__init__.py
CHANGED
|
@@ -23,7 +23,7 @@ import sys
|
|
|
23
23
|
from collections import defaultdict
|
|
24
24
|
from time import sleep
|
|
25
25
|
|
|
26
|
-
from flask import request, g
|
|
26
|
+
from flask import Flask, request, g
|
|
27
27
|
|
|
28
28
|
import yaml
|
|
29
29
|
|
|
@@ -58,6 +58,7 @@ DISPATCHQUEUE_KEY = '__dispatch queue__'
|
|
|
58
58
|
|
|
59
59
|
WATCHDOG_POLLING_DELAY_SECONDS = 30
|
|
60
60
|
WATCHDOG_POLLING_DELAY_KEY = 'watchdog_polling_delay_seconds'
|
|
61
|
+
AVAILABILITY_CHECK_DELAY_SECONDS = 'availability_check_delay_seconds'
|
|
61
62
|
|
|
62
63
|
Handler = Callable[[Dict[str, Any]], Any]
|
|
63
64
|
|
|
@@ -121,7 +122,7 @@ def _maybe_get_item(cache: Dict[Any, Any], labels: Dict[str, str]) -> Optional[A
|
|
|
121
122
|
|
|
122
123
|
|
|
123
124
|
def _ensure_inputs_match(
|
|
124
|
-
plugin, labels: Dict[str, str], inputs: Dict[str, Any]
|
|
125
|
+
plugin: Flask, labels: Dict[str, str], inputs: Dict[str, Any]
|
|
125
126
|
) -> None:
|
|
126
127
|
"""Check inputs.
|
|
127
128
|
|
|
@@ -207,7 +208,7 @@ INVALID_HOOKS_DEFINITION_TEMPLATE = {
|
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
|
|
210
|
-
def _maybe_add_hook_watcher(plugin, schema: str) -> None:
|
|
211
|
+
def _maybe_add_hook_watcher(plugin: Flask, schema: str) -> None:
|
|
211
212
|
if plugin.config['CONTEXT'][KIND_KEY] == EXECUTIONCOMMAND:
|
|
212
213
|
type_ = 'CHANNEL'
|
|
213
214
|
else:
|
|
@@ -235,7 +236,7 @@ def _maybe_add_hook_watcher(plugin, schema: str) -> None:
|
|
|
235
236
|
|
|
236
237
|
|
|
237
238
|
def _read_hooks_definition(
|
|
238
|
-
plugin, hooksfile: str, schema: str, invalid: Dict[str, Any]
|
|
239
|
+
plugin: Flask, hooksfile: str, schema: str, invalid: Dict[str, Any]
|
|
239
240
|
) -> None:
|
|
240
241
|
"""Read hooks definition file.
|
|
241
242
|
|
|
@@ -285,7 +286,9 @@ def _read_hooks_definition(
|
|
|
285
286
|
# Dispatchers
|
|
286
287
|
|
|
287
288
|
|
|
288
|
-
def _dispatch_providercommand(
|
|
289
|
+
def _dispatch_providercommand(
|
|
290
|
+
plugin: Flask, handler: Handler, body: Dict[str, Any]
|
|
291
|
+
) -> None:
|
|
289
292
|
"""Provider plugin dispatcher.
|
|
290
293
|
|
|
291
294
|
`handler` is expected to return either a list of steps or raise a
|
|
@@ -308,7 +311,7 @@ def _dispatch_providercommand(plugin, handler: Handler, body: Dict[str, Any]) ->
|
|
|
308
311
|
core.publish_error(f'Unexpected execution error: {err}.')
|
|
309
312
|
|
|
310
313
|
|
|
311
|
-
def _dispatch_executioncommand(_, handler: Handler, body: Dict[str, Any]):
|
|
314
|
+
def _dispatch_executioncommand(_, handler: Handler, body: Dict[str, Any]) -> None:
|
|
312
315
|
"""Channel plugin dispatcher."""
|
|
313
316
|
try:
|
|
314
317
|
handler(body)
|
|
@@ -316,7 +319,9 @@ def _dispatch_executioncommand(_, handler: Handler, body: Dict[str, Any]):
|
|
|
316
319
|
core.publish_error(f'Unexpected execution error: {err}.')
|
|
317
320
|
|
|
318
321
|
|
|
319
|
-
def _dispatch_generatorcommand(
|
|
322
|
+
def _dispatch_generatorcommand(
|
|
323
|
+
plugin: Flask, handler: Handler, body: Dict[str, Any]
|
|
324
|
+
) -> None:
|
|
320
325
|
"""Generator plugin dispatcher."""
|
|
321
326
|
try:
|
|
322
327
|
labels = body['metadata'].get('labels', {})
|
|
@@ -339,7 +344,7 @@ def _dispatch_generatorcommand(plugin, handler: Handler, body: Dict[str, Any]):
|
|
|
339
344
|
# Watchdog
|
|
340
345
|
|
|
341
346
|
|
|
342
|
-
def _run_handlers(plugin, file, handlers):
|
|
347
|
+
def _run_handlers(plugin: Flask, file, handlers) -> None:
|
|
343
348
|
"""Run file handlers."""
|
|
344
349
|
for handler, args, kwargs in handlers:
|
|
345
350
|
try:
|
|
@@ -350,7 +355,7 @@ def _run_handlers(plugin, file, handlers):
|
|
|
350
355
|
)
|
|
351
356
|
|
|
352
357
|
|
|
353
|
-
def _watchdog(plugin, polling_delay):
|
|
358
|
+
def _watchdog(plugin: Flask, polling_delay: int) -> None:
|
|
354
359
|
"""Watch changes and call handlers when appropriate."""
|
|
355
360
|
files_stat = defaultdict(float)
|
|
356
361
|
files_handlers = plugin.config[WATCHEDFILES_KEY]
|
|
@@ -373,7 +378,7 @@ def _watchdog(plugin, polling_delay):
|
|
|
373
378
|
plugin.config[WATCHEDFILES_EVENT_KEY].clear()
|
|
374
379
|
|
|
375
380
|
|
|
376
|
-
def _start_watchdog(plugin) -> None:
|
|
381
|
+
def _start_watchdog(plugin: Flask) -> None:
|
|
377
382
|
"""Set up a watchdog that monitors specified files for changes."""
|
|
378
383
|
polling_delay = max(
|
|
379
384
|
WATCHDOG_POLLING_DELAY_SECONDS,
|
|
@@ -386,7 +391,7 @@ def _start_watchdog(plugin) -> None:
|
|
|
386
391
|
).start()
|
|
387
392
|
|
|
388
393
|
|
|
389
|
-
def watch_file(plugin, path: str, handler, *args, **kwargs) -> None:
|
|
394
|
+
def watch_file(plugin: Flask, path: str, handler, *args, **kwargs) -> None:
|
|
390
395
|
"""Watch file changes.
|
|
391
396
|
|
|
392
397
|
There can be more than one handler watching a given file. A handler
|
|
@@ -394,6 +399,10 @@ def watch_file(plugin, path: str, handler, *args, **kwargs) -> None:
|
|
|
394
399
|
a file path (a string). It may take additional parameters. It will
|
|
395
400
|
be called whenever the file changes.
|
|
396
401
|
|
|
402
|
+
The watchdog polls every 30 seconds by default. This can be
|
|
403
|
+
adjusted by setting the `watchdog_polling_delay_seconds` context
|
|
404
|
+
parameter (but it cannot be more frequent).
|
|
405
|
+
|
|
397
406
|
# Required parameters
|
|
398
407
|
|
|
399
408
|
- plugin: a Flask application
|
|
@@ -420,12 +429,18 @@ def watch_file(plugin, path: str, handler, *args, **kwargs) -> None:
|
|
|
420
429
|
plugin.config[WATCHEDFILES_EVENT_KEY].set()
|
|
421
430
|
|
|
422
431
|
|
|
423
|
-
def _watchnotifier(
|
|
432
|
+
def _watchnotifier(
|
|
433
|
+
plugin: Flask,
|
|
434
|
+
polling_delay: int,
|
|
435
|
+
check: Callable[..., bool],
|
|
436
|
+
items,
|
|
437
|
+
notify: Callable[[], None],
|
|
438
|
+
):
|
|
424
439
|
reference = {}
|
|
425
440
|
while True:
|
|
426
441
|
sleep(polling_delay)
|
|
427
442
|
try:
|
|
428
|
-
statuses = {item: check(item) for item in items
|
|
443
|
+
statuses = {item: check(item) for item in list(items)}
|
|
429
444
|
if statuses != reference:
|
|
430
445
|
notify()
|
|
431
446
|
reference = statuses
|
|
@@ -435,7 +450,9 @@ def _watchnotifier(plugin, polling_delay, check, items, notify):
|
|
|
435
450
|
)
|
|
436
451
|
|
|
437
452
|
|
|
438
|
-
def watch_and_notify(
|
|
453
|
+
def watch_and_notify(
|
|
454
|
+
plugin: Flask, status: Callable[..., Any], items, notify: Callable[[], None]
|
|
455
|
+
) -> None:
|
|
439
456
|
"""Watch statuses changes in items.
|
|
440
457
|
|
|
441
458
|
Check item status change at regular interval, call notify if
|
|
@@ -444,11 +461,11 @@ def watch_and_notify(plugin, status, items, notify):
|
|
|
444
461
|
# Required parameters
|
|
445
462
|
|
|
446
463
|
- plugin: a Flask application
|
|
447
|
-
- status: a function taking an item and returning a
|
|
464
|
+
- status: a function taking an item and returning a value
|
|
448
465
|
- items: an iterable
|
|
449
466
|
- notify: a function of no arguments
|
|
450
467
|
"""
|
|
451
|
-
polling_delay = get_context_parameter(plugin,
|
|
468
|
+
polling_delay = get_context_parameter(plugin, AVAILABILITY_CHECK_DELAY_SECONDS)
|
|
452
469
|
|
|
453
470
|
plugin.logger.debug('Starting watch notifier thread.')
|
|
454
471
|
threading.Thread(
|
|
@@ -459,7 +476,7 @@ def watch_and_notify(plugin, status, items, notify):
|
|
|
459
476
|
|
|
460
477
|
|
|
461
478
|
def _subscribe(
|
|
462
|
-
plugin,
|
|
479
|
+
plugin: Flask,
|
|
463
480
|
cat_prefix: Optional[str],
|
|
464
481
|
cat: Optional[str],
|
|
465
482
|
cat_version: Optional[str],
|
|
@@ -499,7 +516,7 @@ def _subscribe(
|
|
|
499
516
|
return subscribe(kind=kind, target='inbox', app=plugin, labels=labels)
|
|
500
517
|
|
|
501
518
|
|
|
502
|
-
def run_plugin(plugin):
|
|
519
|
+
def run_plugin(plugin: Flask) -> None:
|
|
503
520
|
"""Start and run plugin.
|
|
504
521
|
|
|
505
522
|
Subscribe to the relevant events before startup and tries to
|
|
@@ -553,7 +570,7 @@ def make_plugin(
|
|
|
553
570
|
schema=None,
|
|
554
571
|
configfile=None,
|
|
555
572
|
args: Optional[Any] = None,
|
|
556
|
-
):
|
|
573
|
+
) -> Flask:
|
|
557
574
|
"""Create and return a new plugin service.
|
|
558
575
|
|
|
559
576
|
One and only one of `channel`, `generator`, `provider`, `providers`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: opentf-toolkit-nightly
|
|
3
|
-
Version: 0.62.0.
|
|
3
|
+
Version: 0.62.0.dev1295
|
|
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,4 +1,4 @@
|
|
|
1
|
-
opentf/commons/__init__.py,sha256=
|
|
1
|
+
opentf/commons/__init__.py,sha256=2hd1g14g9pnZl1uip6Sh90YSmLDWzkH1SGeZweRJ4z8,24180
|
|
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
|
|
@@ -55,11 +55,11 @@ opentf/schemas/opentestfactory.org/v1beta1/Workflow.json,sha256=QZ8mM9PhzsI9gTmw
|
|
|
55
55
|
opentf/schemas/opentestfactory.org/v1beta2/ServiceConfig.json,sha256=rEvK2YWL5lG94_qYgR_GnLWNsaQhaQ-2kuZdWJr5NnY,3517
|
|
56
56
|
opentf/scripts/launch_java_service.sh,sha256=S0jAaCuv2sZy0Gf2NGBuPX-eD531rcM-b0fNyhmzSjw,2423
|
|
57
57
|
opentf/scripts/startup.py,sha256=vOGxl7xBcp1-_LsAKiOmeOqFl2iT81A2XRrXBLUrNi4,22785
|
|
58
|
-
opentf/toolkit/__init__.py,sha256=
|
|
58
|
+
opentf/toolkit/__init__.py,sha256=ohrde5mcMY26p64E0Z2XunZAWYOiEkXKTg5E1J4TGGc,23571
|
|
59
59
|
opentf/toolkit/channels.py,sha256=whLfPVT5PksVlprmoeb2ktaZ3KEhqyryUCVWBJq7PeY,24308
|
|
60
60
|
opentf/toolkit/core.py,sha256=fqnGgaYnuVcd4fyeNIwpc0QtyUo7jsKeVgdkBfY3iqo,9443
|
|
61
|
-
opentf_toolkit_nightly-0.62.0.
|
|
62
|
-
opentf_toolkit_nightly-0.62.0.
|
|
63
|
-
opentf_toolkit_nightly-0.62.0.
|
|
64
|
-
opentf_toolkit_nightly-0.62.0.
|
|
65
|
-
opentf_toolkit_nightly-0.62.0.
|
|
61
|
+
opentf_toolkit_nightly-0.62.0.dev1295.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
62
|
+
opentf_toolkit_nightly-0.62.0.dev1295.dist-info/METADATA,sha256=Dftf4ExDEznTennMkNJ7wCZoK1HONVmPhb_jwMqi6W4,2192
|
|
63
|
+
opentf_toolkit_nightly-0.62.0.dev1295.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
|
64
|
+
opentf_toolkit_nightly-0.62.0.dev1295.dist-info/top_level.txt,sha256=_gPuE6GTT6UNXy1DjtmQSfCcZb_qYA2vWmjg7a30AGk,7
|
|
65
|
+
opentf_toolkit_nightly-0.62.0.dev1295.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|