konduktor-nightly 0.1.0.dev20250616105058__tar.gz → 0.1.0.dev20250616211921__tar.gz

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.
Files changed (98) hide show
  1. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/PKG-INFO +1 -1
  2. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/__init__.py +2 -2
  3. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/backends/jobset_utils.py +7 -4
  4. konduktor_nightly-0.1.0.dev20250616211921/konduktor/utils/validator.py +74 -0
  5. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/pyproject.toml +1 -1
  6. konduktor_nightly-0.1.0.dev20250616105058/konduktor/utils/validator.py +0 -20
  7. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/LICENSE +0 -0
  8. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/README.md +0 -0
  9. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/adaptors/__init__.py +0 -0
  10. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/adaptors/aws.py +0 -0
  11. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/adaptors/common.py +0 -0
  12. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/adaptors/gcp.py +0 -0
  13. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/authentication.py +0 -0
  14. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/backends/__init__.py +0 -0
  15. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/backends/backend.py +0 -0
  16. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/backends/jobset.py +0 -0
  17. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/check.py +0 -0
  18. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/cli.py +0 -0
  19. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/config.py +0 -0
  20. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/constants.py +0 -0
  21. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/controller/__init__.py +0 -0
  22. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/controller/constants.py +0 -0
  23. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/controller/launch.py +0 -0
  24. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/controller/node.py +0 -0
  25. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/controller/parse.py +0 -0
  26. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/README.md +0 -0
  27. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/backend/main.py +0 -0
  28. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/backend/sockets.py +0 -0
  29. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/.eslintrc.json +0 -0
  30. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/.gitignore +0 -0
  31. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/api/jobs/route.js +0 -0
  32. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/api/namespaces/route.js +0 -0
  33. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/components/Grafana.jsx +0 -0
  34. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/components/JobsData.jsx +0 -0
  35. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/components/LogsData.jsx +0 -0
  36. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/components/NavMenu.jsx +0 -0
  37. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/components/NavTabs.jsx +0 -0
  38. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/components/NavTabs2.jsx +0 -0
  39. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/components/SelectBtn.jsx +0 -0
  40. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/components/lib/utils.js +0 -0
  41. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/components/ui/chip-select.jsx +0 -0
  42. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/components/ui/input.jsx +0 -0
  43. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/components/ui/navigation-menu.jsx +0 -0
  44. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/components/ui/select.jsx +0 -0
  45. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/favicon.ico +0 -0
  46. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/globals.css +0 -0
  47. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/jobs/page.js +0 -0
  48. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/layout.js +0 -0
  49. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/logs/page.js +0 -0
  50. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/app/page.js +0 -0
  51. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/jsconfig.json +0 -0
  52. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/next.config.mjs +0 -0
  53. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/package-lock.json +0 -0
  54. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/package.json +0 -0
  55. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/postcss.config.mjs +0 -0
  56. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/server.js +0 -0
  57. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/dashboard/frontend/tailwind.config.js +0 -0
  58. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/data/__init__.py +0 -0
  59. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/data/aws/__init__.py +0 -0
  60. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/data/aws/s3.py +0 -0
  61. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/data/constants.py +0 -0
  62. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/data/data_utils.py +0 -0
  63. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/data/gcp/__init__.py +0 -0
  64. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/data/gcp/constants.py +0 -0
  65. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/data/gcp/gcs.py +0 -0
  66. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/data/gcp/utils.py +0 -0
  67. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/data/registry.py +0 -0
  68. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/data/storage.py +0 -0
  69. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/data/storage_utils.py +0 -0
  70. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/execution.py +0 -0
  71. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/kube_client.py +0 -0
  72. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/logging.py +0 -0
  73. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/manifests/controller_deployment.yaml +0 -0
  74. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/manifests/dashboard_deployment.yaml +0 -0
  75. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/manifests/dmesg_daemonset.yaml +0 -0
  76. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/manifests/pod_cleanup_controller.yaml +0 -0
  77. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/resource.py +0 -0
  78. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/task.py +0 -0
  79. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/templates/jobset.yaml.j2 +0 -0
  80. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/templates/pod.yaml.j2 +0 -0
  81. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/usage/__init__.py +0 -0
  82. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/usage/constants.py +0 -0
  83. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/utils/__init__.py +0 -0
  84. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/utils/accelerator_registry.py +0 -0
  85. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/utils/annotations.py +0 -0
  86. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/utils/base64_utils.py +0 -0
  87. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/utils/common_utils.py +0 -0
  88. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/utils/constants.py +0 -0
  89. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/utils/env_options.py +0 -0
  90. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/utils/exceptions.py +0 -0
  91. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/utils/kubernetes_enums.py +0 -0
  92. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/utils/kubernetes_utils.py +0 -0
  93. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/utils/log_utils.py +0 -0
  94. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/utils/loki_utils.py +0 -0
  95. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/utils/rich_utils.py +0 -0
  96. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/utils/schemas.py +0 -0
  97. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/utils/subprocess_utils.py +0 -0
  98. {konduktor_nightly-0.1.0.dev20250616105058 → konduktor_nightly-0.1.0.dev20250616211921}/konduktor/utils/ux_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: konduktor-nightly
3
- Version: 0.1.0.dev20250616105058
3
+ Version: 0.1.0.dev20250616211921
4
4
  Summary: GPU Cluster Health Management
5
5
  Author: Andrew Aikawa
6
6
  Author-email: asai@berkeley.edu
@@ -14,7 +14,7 @@ __all__ = [
14
14
  ]
15
15
 
16
16
  # Replaced with the current commit when building the wheels.
17
- _KONDUKTOR_COMMIT_SHA = '703fd488ab698fa7ea8f92841bdc19fcdca49f92'
17
+ _KONDUKTOR_COMMIT_SHA = '92109aa0f4a3a82ded90f33f6fe089392057ed96'
18
18
  os.makedirs(os.path.expanduser('~/.konduktor'), exist_ok=True)
19
19
 
20
20
 
@@ -48,5 +48,5 @@ def _get_git_commit():
48
48
 
49
49
 
50
50
  __commit__ = _get_git_commit()
51
- __version__ = '1.0.0.dev0.1.0.dev20250616105058'
51
+ __version__ = '1.0.0.dev0.1.0.dev20250616211921'
52
52
  __root_dir__ = os.path.dirname(os.path.abspath(__file__))
@@ -24,6 +24,7 @@ from konduktor.utils import (
24
24
  kubernetes_utils,
25
25
  log_utils,
26
26
  ux_utils,
27
+ validator,
27
28
  )
28
29
 
29
30
  if typing.TYPE_CHECKING:
@@ -270,9 +271,9 @@ def create_pod_spec(task: 'konduktor.Task') -> Dict[str, Any]:
270
271
  env_map.values()
271
272
  )
272
273
 
273
- # TODO(asaiacai): have some schema validations. see
274
- # https://github.com/skypilot-org/skypilot/pull/4466
275
- # TODO(asaiacai): where can we include policies for the pod spec.
274
+ # validate pod spec using json schema
275
+ # schema: https://github.com/instrumenta/kubernetes-json-schema/blob/master/v1.9.8-standalone-strict/podspec.json
276
+ validator.validate_pod_spec(pod_config['kubernetes']['pod_config']['spec'])
276
277
 
277
278
  return pod_config
278
279
 
@@ -310,7 +311,9 @@ def create_jobset(
310
311
  temp.name,
311
312
  )
312
313
  jobset_spec = common_utils.read_yaml(temp.name)
313
- jobset_spec['jobset']['metadata']['labels'].update(**task.resources.labels)
314
+ jobset_spec['jobset']['metadata']['labels'].update(
315
+ **(task.resources.labels or {})
316
+ )
314
317
  assert task.resources.labels is not None
315
318
  maxRunDurationSeconds = task.resources.labels.get('maxRunDurationSeconds', None)
316
319
  if not maxRunDurationSeconds:
@@ -0,0 +1,74 @@
1
+ """This module contains a custom validator for the JSON Schema specification.
2
+
3
+ The main motivation behind extending the existing JSON Schema validator is to
4
+ allow for case-insensitive enum matching since this is currently not supported
5
+ by the JSON Schema specification.
6
+ """
7
+
8
+ import json
9
+ import time
10
+ from pathlib import Path
11
+
12
+ import jsonschema
13
+ import requests
14
+ from colorama import Fore, Style
15
+
16
+ SCHEMA_VERSION = 'v1.9.8-standalone-strict'
17
+ SCHEMA_URL = f'https://raw.githubusercontent.com/instrumenta/kubernetes-json-schema/master/{SCHEMA_VERSION}/podspec.json'
18
+ SCHEMA_CACHE_PATH = Path.home() / '.konduktor/schemas/podspec.json'
19
+ CACHE_MAX_AGE_SECONDS = 86400 # 24 hours
20
+
21
+
22
+ def case_insensitive_enum(validator, enums, instance, schema):
23
+ del validator, schema # Unused.
24
+ if instance.lower() not in [enum.lower() for enum in enums]:
25
+ yield jsonschema.ValidationError(f'{instance!r} is not one of {enums!r}')
26
+
27
+
28
+ SchemaValidator = jsonschema.validators.extend(
29
+ jsonschema.Draft7Validator,
30
+ validators={'case_insensitive_enum': case_insensitive_enum},
31
+ )
32
+
33
+
34
+ def get_cached_schema() -> dict:
35
+ # Check if schema file exists and is fresh
36
+ if SCHEMA_CACHE_PATH.exists():
37
+ age = time.time() - SCHEMA_CACHE_PATH.stat().st_mtime
38
+ # if old
39
+ if age < CACHE_MAX_AGE_SECONDS:
40
+ with open(SCHEMA_CACHE_PATH, 'r') as f:
41
+ return json.load(f)
42
+
43
+ # Download schema
44
+ resp = requests.get(SCHEMA_URL)
45
+ resp.raise_for_status()
46
+
47
+ SCHEMA_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
48
+ with open(SCHEMA_CACHE_PATH, 'w') as f:
49
+ f.write(resp.text)
50
+
51
+ return resp.json()
52
+
53
+
54
+ def validate_pod_spec(pod_spec: dict) -> None:
55
+ schema = get_cached_schema()
56
+
57
+ validator = jsonschema.Draft7Validator(schema)
58
+ errors = sorted(validator.iter_errors(pod_spec), key=lambda e: e.path)
59
+
60
+ if not errors:
61
+ return
62
+
63
+ formatted = [
64
+ f'{Fore.RED}- {error.message}'
65
+ + (f" at path: {' → '.join(str(p) for p in error.path)}" if error.path else '')
66
+ + Style.RESET_ALL
67
+ for error in errors
68
+ ]
69
+
70
+ raise ValueError(
71
+ f'\n{Fore.RED}Invalid k8s pod spec/config: \
72
+ {Style.RESET_ALL}\n'
73
+ + '\n'.join(formatted)
74
+ )
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "konduktor-nightly"
3
- version = "0.1.0.dev20250616105058"
3
+ version = "0.1.0.dev20250616211921"
4
4
  description = "GPU Cluster Health Management"
5
5
  packages = [
6
6
  {include = "konduktor"}
@@ -1,20 +0,0 @@
1
- """This module contains a custom validator for the JSON Schema specification.
2
-
3
- The main motivation behind extending the existing JSON Schema validator is to
4
- allow for case-insensitive enum matching since this is currently not supported
5
- by the JSON Schema specification.
6
- """
7
-
8
- import jsonschema
9
-
10
-
11
- def case_insensitive_enum(validator, enums, instance, schema):
12
- del validator, schema # Unused.
13
- if instance.lower() not in [enum.lower() for enum in enums]:
14
- yield jsonschema.ValidationError(f'{instance!r} is not one of {enums!r}')
15
-
16
-
17
- SchemaValidator = jsonschema.validators.extend(
18
- jsonschema.Draft7Validator,
19
- validators={'case_insensitive_enum': case_insensitive_enum},
20
- )