xpk 0.5.0__py3-none-any.whl → 0.7.0__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.
Files changed (95) hide show
  1. xpk/__init__.py +15 -0
  2. xpk/api/__init__.py +15 -0
  3. xpk/api/storage_crd.yaml +52 -0
  4. xpk/commands/__init__.py +15 -0
  5. xpk/commands/batch.py +131 -0
  6. xpk/commands/cluster.py +808 -0
  7. xpk/commands/cluster_gcluster.py +269 -0
  8. xpk/commands/common.py +44 -0
  9. xpk/commands/config.py +29 -0
  10. xpk/commands/info.py +243 -0
  11. xpk/commands/inspector.py +357 -0
  12. xpk/commands/job.py +199 -0
  13. xpk/commands/kind.py +283 -0
  14. xpk/commands/kjob_common.py +44 -0
  15. xpk/commands/run.py +128 -0
  16. xpk/commands/shell.py +140 -0
  17. xpk/commands/storage.py +267 -0
  18. xpk/commands/version.py +27 -0
  19. xpk/commands/workload.py +889 -0
  20. xpk/core/__init__.py +15 -0
  21. xpk/core/blueprint/__init__.py +15 -0
  22. xpk/core/blueprint/blueprint_definitions.py +62 -0
  23. xpk/core/blueprint/blueprint_generator.py +708 -0
  24. xpk/core/capacity.py +185 -0
  25. xpk/core/cluster.py +564 -0
  26. xpk/core/cluster_private.py +200 -0
  27. xpk/core/commands.py +356 -0
  28. xpk/core/config.py +179 -0
  29. xpk/core/docker_container.py +225 -0
  30. xpk/core/docker_image.py +210 -0
  31. xpk/core/docker_manager.py +308 -0
  32. xpk/core/docker_resources.py +350 -0
  33. xpk/core/filestore.py +251 -0
  34. xpk/core/gcloud_context.py +196 -0
  35. xpk/core/gcluster_manager.py +176 -0
  36. xpk/core/gcsfuse.py +50 -0
  37. xpk/core/kjob.py +444 -0
  38. xpk/core/kueue.py +358 -0
  39. xpk/core/monitoring.py +134 -0
  40. xpk/core/nap.py +361 -0
  41. xpk/core/network.py +377 -0
  42. xpk/core/nodepool.py +581 -0
  43. xpk/core/pathways.py +377 -0
  44. xpk/core/ray.py +222 -0
  45. xpk/core/remote_state/__init__.py +15 -0
  46. xpk/core/remote_state/fuse_remote_state.py +99 -0
  47. xpk/core/remote_state/remote_state_client.py +38 -0
  48. xpk/core/resources.py +238 -0
  49. xpk/core/scheduling.py +253 -0
  50. xpk/core/storage.py +581 -0
  51. xpk/core/system_characteristics.py +1432 -0
  52. xpk/core/vertex.py +105 -0
  53. xpk/core/workload.py +341 -0
  54. xpk/core/workload_decorators/__init__.py +15 -0
  55. xpk/core/workload_decorators/rdma_decorator.py +129 -0
  56. xpk/core/workload_decorators/storage_decorator.py +52 -0
  57. xpk/core/workload_decorators/tcpxo_decorator.py +190 -0
  58. xpk/main.py +75 -0
  59. xpk/parser/__init__.py +15 -0
  60. xpk/parser/batch.py +43 -0
  61. xpk/parser/cluster.py +662 -0
  62. xpk/parser/common.py +259 -0
  63. xpk/parser/config.py +49 -0
  64. xpk/parser/core.py +135 -0
  65. xpk/parser/info.py +64 -0
  66. xpk/parser/inspector.py +65 -0
  67. xpk/parser/job.py +147 -0
  68. xpk/parser/kind.py +95 -0
  69. xpk/parser/run.py +47 -0
  70. xpk/parser/shell.py +59 -0
  71. xpk/parser/storage.py +316 -0
  72. xpk/parser/validators.py +39 -0
  73. xpk/parser/version.py +23 -0
  74. xpk/parser/workload.py +726 -0
  75. xpk/templates/__init__.py +15 -0
  76. xpk/templates/storage.yaml +13 -0
  77. xpk/utils/__init__.py +15 -0
  78. xpk/utils/console.py +55 -0
  79. xpk/utils/file.py +82 -0
  80. xpk/utils/gcs_utils.py +125 -0
  81. xpk/utils/kubectl.py +57 -0
  82. xpk/utils/network.py +168 -0
  83. xpk/utils/objects.py +88 -0
  84. xpk/utils/templates.py +28 -0
  85. xpk/utils/validation.py +80 -0
  86. xpk/utils/yaml.py +30 -0
  87. {xpk-0.5.0.dist-info → xpk-0.7.0.dist-info}/METADATA +456 -32
  88. xpk-0.7.0.dist-info/RECORD +92 -0
  89. {xpk-0.5.0.dist-info → xpk-0.7.0.dist-info}/WHEEL +1 -1
  90. xpk-0.7.0.dist-info/entry_points.txt +2 -0
  91. xpk-0.5.0.dist-info/RECORD +0 -7
  92. xpk-0.5.0.dist-info/entry_points.txt +0 -2
  93. xpk.py +0 -7282
  94. {xpk-0.5.0.dist-info → xpk-0.7.0.dist-info}/LICENSE +0 -0
  95. {xpk-0.5.0.dist-info → xpk-0.7.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,15 @@
1
+ """
2
+ Copyright 2023 Google LLC
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ https://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ """
@@ -0,0 +1,13 @@
1
+ apiVersion: xpk.x-k8s.io/v1
2
+ kind: Storage
3
+ metadata:
4
+ name: $NAME
5
+ spec:
6
+ auto_mount:
7
+ cluster:
8
+ manifest:
9
+ mount_point:
10
+ readonly:
11
+ type: $NAME
12
+ pvc:
13
+ pv:
xpk/utils/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ """
2
+ Copyright 2024 Google LLC
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ https://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ """
xpk/utils/console.py ADDED
@@ -0,0 +1,55 @@
1
+ """
2
+ Copyright 2024 Google LLC
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ https://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ """
16
+
17
+ import sys
18
+
19
+
20
+ def xpk_print(*args, **kwargs):
21
+ """Helper function to print a prefix before function provided args.
22
+
23
+ Args:
24
+ *args: user provided print args.
25
+ **kwargs: user provided print args.
26
+ """
27
+ sys.stdout.write('[XPK] ')
28
+ print(*args, **kwargs)
29
+ sys.stdout.flush()
30
+
31
+
32
+ def xpk_exit(error_code):
33
+ """Helper function to exit xpk with an associated error code.
34
+
35
+ Args:
36
+ error_code: If the code provided is zero, then no issues occurred.
37
+ """
38
+ if error_code == 0:
39
+ xpk_print('Exiting XPK cleanly')
40
+ sys.exit(0)
41
+ else:
42
+ xpk_print(f'XPK failed, error code {error_code}')
43
+ sys.exit(error_code)
44
+
45
+
46
+ def get_user_input(input_msg):
47
+ """Function to get the user input for a prompt.
48
+
49
+ Args:
50
+ input_msg: message to be displayed by the prompt.
51
+ Returns:
52
+ True if user enter y or yes at the prompt, False otherwise.
53
+ """
54
+ user_input = input(input_msg)
55
+ return user_input in ('y', 'yes')
xpk/utils/file.py ADDED
@@ -0,0 +1,82 @@
1
+ """
2
+ Copyright 2024 Google LLC
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ https://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ """
16
+
17
+ import tempfile
18
+ import os
19
+ from .console import xpk_print
20
+
21
+
22
+ def make_tmp_files(per_command_name):
23
+ """Make temporary files for each command.
24
+
25
+ Args:
26
+ per_command_name: list of command names.
27
+
28
+ Returns:
29
+ A list of temporary files for each command.
30
+ """
31
+ # Supports removal of spaces from command names before converting to file name.
32
+ return [
33
+ tempfile.NamedTemporaryFile(
34
+ delete=False, prefix=command.replace(' ', '-') + '-'
35
+ )
36
+ for command in per_command_name
37
+ ]
38
+
39
+
40
+ def write_tmp_file(payload):
41
+ """Writes `payload` to a temporary file.
42
+
43
+ Args:
44
+ payload: The string to be written to the file.
45
+
46
+ Returns:
47
+ A file object that was written to.
48
+ """
49
+ with tempfile.NamedTemporaryFile(delete=False) as tmp:
50
+ with open(file=tmp.name, mode='w', encoding='utf=8') as f:
51
+ f.write(payload)
52
+ f.flush()
53
+ return tmp
54
+
55
+
56
+ def append_tmp_file(payload, file):
57
+ """Appends `payload` to an already created file.
58
+
59
+ Use `write_temporary_file` to create a file.
60
+
61
+ Args:
62
+ payload: The string to be written to the file.
63
+ file: The file to append to.
64
+
65
+ Returns:
66
+ A file object that was written to.
67
+ """
68
+ with open(file=file.name, mode='a', encoding='utf=8') as f:
69
+ f.write(payload)
70
+ f.flush()
71
+ return file
72
+
73
+
74
+ def ensure_directory_exists(directory_path):
75
+ """Checks if a directory exists and creates it if it doesn't.
76
+
77
+ Args:
78
+ directory_path: The path to the directory.
79
+ """
80
+ if not os.path.exists(directory_path):
81
+ os.makedirs(directory_path)
82
+ xpk_print(f"Directory '{directory_path}' created successfully.")
xpk/utils/gcs_utils.py ADDED
@@ -0,0 +1,125 @@
1
+ """
2
+ Copyright 2025 Google LLC
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ https://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ """
16
+
17
+ from pathlib import Path
18
+
19
+ from google.cloud.storage import transfer_manager, Client
20
+ from .console import xpk_print
21
+
22
+
23
+ def upload_file_to_gcs(
24
+ storage_client: Client, bucket_name: str, bucket_path: str, file: str
25
+ ):
26
+ bucket = storage_client.bucket(bucket_name)
27
+ blob = bucket.blob(bucket_path)
28
+ blob.upload_from_filename(file)
29
+
30
+
31
+ def upload_directory_to_gcs(
32
+ storage_client: Client,
33
+ bucket_name: str,
34
+ bucket_path: str,
35
+ source_directory: str,
36
+ workers: int = 8,
37
+ ):
38
+ """Upload every file in a directory, including all files in subdirectories.
39
+
40
+ Each blob name is derived from the filename, not including the `directory`
41
+ parameter itself. For complete control of the blob name for each file (and
42
+ other aspects of individual blob metadata), use
43
+ transfer_manager.upload_many() instead.
44
+ """
45
+ xpk_print(f"Uploading directory {source_directory} to bucket {bucket_name}")
46
+ bucket = storage_client.bucket(bucket_name)
47
+
48
+ directory_as_path_obj = Path(source_directory)
49
+ paths = directory_as_path_obj.rglob("*")
50
+
51
+ # Filter so the list only includes files, not directories themselves.
52
+ file_paths = [path for path in paths if path.is_file()]
53
+
54
+ # These paths are relative to the current working directory. Next, make them
55
+ # relative to `directory`
56
+ relative_paths = [path.relative_to(source_directory) for path in file_paths]
57
+
58
+ # Finally, convert them all to strings.
59
+ string_paths = [str(path) for path in relative_paths]
60
+
61
+ xpk_print(f"Found {len(string_paths)} files.")
62
+ # Start the upload.
63
+ results = transfer_manager.upload_many_from_filenames(
64
+ bucket=bucket,
65
+ filenames=string_paths,
66
+ source_directory=source_directory,
67
+ max_workers=workers,
68
+ blob_name_prefix=bucket_path,
69
+ )
70
+
71
+ for name, result in zip(string_paths, results):
72
+ # The results list is either `None` or an exception for each filename in
73
+ # the input list, in order.
74
+
75
+ if isinstance(result, Exception):
76
+ xpk_print(f"Failed to upload {name} due to exception: {result}")
77
+ else:
78
+ xpk_print(f"Uploaded {name} to {bucket.name}.")
79
+
80
+
81
+ def check_file_exists(
82
+ storage_client: Client, bucket_name: str, filename: str
83
+ ) -> bool:
84
+ xpk_print(f"Checking if file {filename} exists in bucket: {bucket_name}")
85
+ bucket = storage_client.get_bucket(bucket_name)
86
+ is_file: bool = bucket.blob(filename).exists()
87
+ return is_file
88
+
89
+
90
+ def download_bucket_to_dir(
91
+ storage_client: Client,
92
+ bucket_name: str,
93
+ bucket_path: str,
94
+ destination_directory: str = "",
95
+ workers: int = 8,
96
+ max_results: int = 1000,
97
+ ):
98
+ """Download all of the blobs in a bucket, concurrently in a process pool.
99
+
100
+ The filename of each blob once downloaded is derived from the blob name and
101
+ the `destination_directory `parameter. For complete control of the filename
102
+ of each blob, use transfer_manager.download_many() instead.
103
+
104
+ Directories will be created automatically as needed, for instance to
105
+ accommodate blob names that include slashes.
106
+ """
107
+ bucket = storage_client.bucket(bucket_name)
108
+
109
+ blob_names = [
110
+ blob.name
111
+ for blob in bucket.list_blobs(max_results=max_results, prefix=bucket_path)
112
+ ]
113
+
114
+ results = transfer_manager.download_many_to_path(
115
+ bucket,
116
+ blob_names,
117
+ destination_directory=destination_directory,
118
+ max_workers=workers,
119
+ )
120
+
121
+ for name, result in zip(blob_names, results):
122
+ if isinstance(result, Exception):
123
+ xpk_print(f"Failed to download {name} due to exception: {result}")
124
+ else:
125
+ xpk_print(f"Downloaded {name} to {destination_directory + name}.")
xpk/utils/kubectl.py ADDED
@@ -0,0 +1,57 @@
1
+ """
2
+ Copyright 2025 Google LLC
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ https://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ """
16
+
17
+ from kubernetes.client.exceptions import ApiException
18
+ from kubernetes.dynamic import DynamicClient
19
+
20
+ from .console import xpk_print
21
+
22
+
23
+ def apply_kubectl_manifest(client, manifest):
24
+ xpk_print('Applying manifest')
25
+ dynamic_client = DynamicClient(client)
26
+
27
+ for obj in manifest:
28
+ api_version = obj['apiVersion']
29
+ kind = obj['kind']
30
+ namespace = obj.get('metadata', {}).get('namespace', 'default')
31
+
32
+ api_resource = dynamic_client.resources.get(
33
+ api_version=api_version, kind=kind
34
+ )
35
+
36
+ try:
37
+ api_resource.get(name=obj['metadata']['name'], namespace=namespace)
38
+ api_resource.patch(
39
+ body=obj,
40
+ namespace=namespace,
41
+ name=obj['metadata']['name'],
42
+ content_type='application/merge-patch+json',
43
+ )
44
+ xpk_print(
45
+ f"Updated {kind} '{obj['metadata']['name']}' in namespace"
46
+ f" '{namespace}'"
47
+ )
48
+
49
+ except ApiException as e:
50
+ if e.status == 404:
51
+ api_resource.create(body=obj, namespace=namespace)
52
+ xpk_print(
53
+ f"Applied {kind} '{obj['metadata']['name']}' in namespace"
54
+ f" '{namespace}'"
55
+ )
56
+ else:
57
+ xpk_print(f'Error applying {kind}: {e}')
xpk/utils/network.py ADDED
@@ -0,0 +1,168 @@
1
+ """
2
+ Copyright 2024 Google LLC
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ https://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ """
16
+
17
+ import ipaddress
18
+ import socket
19
+ import requests
20
+ from .console import xpk_print
21
+
22
+ # Retrives machine's external IP address
23
+ ip_resolver_url = "http://api.ipify.org"
24
+ all_IPs_cidr = "0.0.0.0/0"
25
+
26
+
27
+ def get_current_machine_ip(external_ip=True):
28
+ """
29
+ Gets the IP address of the current machine.
30
+
31
+ Args:
32
+ external: If True (default), retrieves the external IP address.
33
+ If False, retrieves the internal IP address.
34
+
35
+ Returns:
36
+ The IP address as a string.
37
+ """
38
+
39
+ try:
40
+ if external_ip:
41
+ # Get external IP address
42
+ response = requests.get(ip_resolver_url, timeout=30)
43
+ return 0, response.text
44
+ else:
45
+ # Get internal IP address
46
+ hostname = socket.gethostname()
47
+ return 0, socket.gethostbyname(hostname)
48
+ except (requests.exceptions.RequestException, socket.gaierror) as e:
49
+ xpk_print(f"Error getting IP address: {e}")
50
+ return 1, None
51
+
52
+
53
+ def is_ip_in_any_network(ip_address, cidrs):
54
+ """
55
+ Checks if an IP address is within any of the provided CIDR ranges.
56
+
57
+ Args:
58
+ ip_address: The IP address to check (as a string).
59
+ cidrs: A list of CIDR strings.
60
+
61
+ Returns:
62
+ True if the IP address is found in any of the CIDRs, False otherwise.
63
+ """
64
+
65
+ try:
66
+ if not are_cidrs_valid(cidrs):
67
+ return False
68
+
69
+ if cidrs is None:
70
+ return False
71
+
72
+ current_ip = ipaddress.ip_address(ip_address)
73
+ for cidr in cidrs:
74
+ network = ipaddress.ip_network(cidr)
75
+ if current_ip in network:
76
+ return True
77
+ except ValueError as e:
78
+ xpk_print(f"Error: {e}")
79
+ return False
80
+ return False
81
+
82
+
83
+ def is_current_machine_in_any_network(cidrs, external_ip=True):
84
+ """
85
+ Checks if the current machine's IP address is within any of the provided CIDR ranges.
86
+
87
+ Args:
88
+ cidrs: A list of CIDR strings.
89
+ external_ip: If True (default), checks the external IP. If False, checks the internal IP.
90
+
91
+ Returns:
92
+ True if the IP address is found in any of the CIDRs, False otherwise.
93
+ """
94
+ if not are_cidrs_valid(cidrs):
95
+ return 1, False
96
+
97
+ if cidrs is None:
98
+ return 0, False
99
+
100
+ return_code, ip_address = get_current_machine_ip(external_ip)
101
+ if return_code > 0:
102
+ return return_code, False
103
+ else:
104
+ return return_code, is_ip_in_any_network(ip_address, cidrs)
105
+
106
+
107
+ def add_current_machine_to_networks(cidrs, external_ip=True):
108
+ """
109
+ Adds the current machine's IP address to the list of CIDRs if it's not already present.
110
+
111
+ Args:
112
+ cidrs: A list of CIDR strings.
113
+ external_ip: If True, uses the external IP. If False (default), uses the internal IP.
114
+
115
+ Returns:
116
+ The updated list of CIDRs with the current machine's IP added (if necessary).
117
+ """
118
+ if not are_cidrs_valid(cidrs):
119
+ return 1, None
120
+
121
+ return_code, ip_address = get_current_machine_ip(external_ip)
122
+ if return_code > 0:
123
+ return return_code, None
124
+
125
+ if not is_ip_in_any_network(ip_address, cidrs):
126
+ cidrs.append(f"{ip_address}/32")
127
+
128
+ return 0, cidrs
129
+
130
+
131
+ def is_cidr_valid(cidr):
132
+ """
133
+ Validates a CIDR string.
134
+
135
+ Args:
136
+ cidr: The CIDR string to validate.
137
+
138
+ Returns:
139
+ True if the CIDR string is valid, False otherwise.
140
+ """
141
+ try:
142
+ ipaddress.ip_network(cidr)
143
+ return True
144
+ except ValueError:
145
+ return False
146
+
147
+
148
+ def are_cidrs_valid(cidrs):
149
+ """
150
+ Validates a list of CIDR strings.
151
+
152
+ Args:
153
+ cidrs: A list of CIDR strings to validate.
154
+
155
+ Returns:
156
+ True if all CIDR strings in the list are valid, False otherwise.
157
+ """
158
+ if cidrs is None:
159
+ return True
160
+
161
+ all_cidrs_are_valid = True
162
+
163
+ for cidr in cidrs:
164
+ if not is_cidr_valid(cidr):
165
+ all_cidrs_are_valid = False
166
+ xpk_print(f"Error: the string '{cidr}' is not a valid CIDR.")
167
+
168
+ return all_cidrs_are_valid
xpk/utils/objects.py ADDED
@@ -0,0 +1,88 @@
1
+ """
2
+ Copyright 2024 Google LLC
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ https://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ """
16
+
17
+ import hashlib
18
+ from .console import xpk_print
19
+
20
+
21
+ def chunks(lst: list, n: int):
22
+ """Return a list of n-sized chunks from lst.
23
+
24
+ Args:
25
+ lst: input list to get chunks from.
26
+ n: size of each chunk.
27
+
28
+ Returns:
29
+ List of n-sized chunks for lst.
30
+ """
31
+ return [lst[i : i + n] for i in range(0, len(lst), n)]
32
+
33
+
34
+ def get_value_from_map(
35
+ key: str, map_to_search: dict, verbose: bool = True
36
+ ) -> tuple[int, str | None]:
37
+ """Helper function to get value from a map if the key exists.
38
+
39
+ Args:
40
+ key: The key to look for in the map
41
+ map_to_search: The map to look in for the value
42
+
43
+ Returns:
44
+ Tuple of int, str where
45
+ int is the return code
46
+ str is the value if found
47
+ """
48
+ value = map_to_search.get(key)
49
+ if value:
50
+ return 0, value
51
+ else:
52
+ if verbose:
53
+ xpk_print(
54
+ f'Unable to find key: {key} in map: {map_to_search}.'
55
+ f'The map has the following keys: {map_to_search.keys()}'
56
+ )
57
+ return 1, value
58
+
59
+
60
+ def is_text_true(text: str) -> bool:
61
+ return text.strip().lower() == 'true'
62
+
63
+
64
+ def hash_string(input_string: str, length: int | None = None):
65
+ """
66
+ Generates a hash of a string using characters 0-9 and a-z.
67
+
68
+ Args:
69
+ input_string: The string to hash.
70
+ length: The desired length of the hash (optional).
71
+ If not provided, or less than 0, the full SHA256 hash is returned.
72
+
73
+ Returns:
74
+ A hash string of the specified length or the full SHA256 hash,
75
+ using characters 0-9 and a-z.
76
+ """
77
+ hash_value = int(hashlib.sha256(input_string.encode()).hexdigest(), 16)
78
+
79
+ if length is None or length < 0:
80
+ length = 64 # Use the full length of the SHA256 hash
81
+
82
+ charset = '0123456789abcdefghijklmnopqrstuvwxyz'
83
+ result = ''
84
+ while hash_value > 0 and len(result) < length:
85
+ hash_value, index = divmod(hash_value, 36) # Get quotient and remainder
86
+ result = charset[index] + result
87
+
88
+ return result.zfill(length).lower() # Pad with zeros if necessary
xpk/utils/templates.py ADDED
@@ -0,0 +1,28 @@
1
+ """
2
+ Copyright 2025 Google LLC
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ https://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ """
16
+
17
+ import os
18
+
19
+ import ruamel.yaml
20
+
21
+ yaml = ruamel.yaml.YAML()
22
+
23
+
24
+ def load(path: str) -> dict:
25
+ template_path = os.path.dirname(__file__) + path
26
+ with open(template_path, "r", encoding="utf-8") as file:
27
+ data: dict = yaml.load(file)
28
+ return data