q8s 0.11.0.dev2__tar.gz → 0.11.2__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.
- {q8s-0.11.0.dev2 → q8s-0.11.2}/PKG-INFO +2 -1
- {q8s-0.11.0.dev2 → q8s-0.11.2}/pyproject.toml +2 -1
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/__init__.py +2 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/bakefile.py +1 -1
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/execution.py +77 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s.egg-info/PKG-INFO +2 -1
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s.egg-info/SOURCES.txt +1 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s.egg-info/requires.txt +1 -0
- q8s-0.11.2/tests/test_execution.py +103 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/LICENSE +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/README.md +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/setup.cfg +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/__main__.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/cli.py +1 -1
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/constants.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/enums.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/install.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/kernel.py +1 -1
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/matplotlib/backend.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/multifiles.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/plugins/__init__.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/plugins/cpu_job.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/plugins/cuda_job.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/plugins/job.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/plugins/job_template_spec.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/plugins/utils/__init__.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/plugins/utils/git_info.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/project.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/utils.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/workload.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s.egg-info/dependency_links.txt +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s.egg-info/entry_points.txt +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s.egg-info/top_level.txt +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/tests/test_backend.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/tests/test_bakefile.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/tests/test_git_info.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/tests/test_job_plugin.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/tests/test_job_template_spec.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/tests/test_kernel.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/tests/test_multifiles.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/tests/test_project.py +0 -0
- {q8s-0.11.0.dev2 → q8s-0.11.2}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: q8s
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.2
|
|
4
4
|
Summary: Kernel extension for executing quantum programs in simulators on q8s clusters
|
|
5
5
|
Author-email: Vlad Stirbu <vstirbu@gmail.com>
|
|
6
6
|
License: Apache License
|
|
@@ -229,6 +229,7 @@ Requires-Dist: python-dotenv==1.0.1
|
|
|
229
229
|
Requires-Dist: pyyaml==6.0.2
|
|
230
230
|
Requires-Dist: typer>=0.15.4
|
|
231
231
|
Requires-Dist: GitPython>=3.1.43
|
|
232
|
+
Requires-Dist: python-dxf>=12.1.1
|
|
232
233
|
Provides-Extra: benchmark
|
|
233
234
|
Requires-Dist: pandas; extra == "benchmark"
|
|
234
235
|
Requires-Dist: testbook; extra == "benchmark"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "q8s"
|
|
3
|
-
version = "0.11.
|
|
3
|
+
version = "0.11.2"
|
|
4
4
|
description = "Kernel extension for executing quantum programs in simulators on q8s clusters"
|
|
5
5
|
authors = [{ name = "Vlad Stirbu", email = "vstirbu@gmail.com" }]
|
|
6
6
|
readme = "README.md"
|
|
@@ -27,6 +27,7 @@ dependencies = [
|
|
|
27
27
|
"pyyaml==6.0.2",
|
|
28
28
|
"typer>=0.15.4",
|
|
29
29
|
"GitPython>=3.1.43",
|
|
30
|
+
"python-dxf>=12.1.1",
|
|
30
31
|
]
|
|
31
32
|
requires-python = ">= 3.10"
|
|
32
33
|
|
|
@@ -5,7 +5,10 @@ import string
|
|
|
5
5
|
from json import JSONEncoder, loads
|
|
6
6
|
|
|
7
7
|
import pluggy
|
|
8
|
+
import requests
|
|
8
9
|
from dotenv import dotenv_values
|
|
10
|
+
from dxf import DXF
|
|
11
|
+
from dxf.exceptions import DXFUnauthorizedError
|
|
9
12
|
from kubernetes import client, config, watch
|
|
10
13
|
from rich.progress import Progress
|
|
11
14
|
|
|
@@ -24,6 +27,78 @@ def load_env():
|
|
|
24
27
|
return env
|
|
25
28
|
|
|
26
29
|
|
|
30
|
+
class ContainerImageValidator:
|
|
31
|
+
"""
|
|
32
|
+
Validate that a container image exists in its registry.
|
|
33
|
+
|
|
34
|
+
The image reference should be in the format:
|
|
35
|
+
- 'user/repo:tag' for Docker Hub images
|
|
36
|
+
- 'registry.com/user/repo:tag' for images in other registries
|
|
37
|
+
|
|
38
|
+
Primarily, the image URL values are generated by `q8sctl build --init`
|
|
39
|
+
command and stored in the `.q8s_cache/images` file. Images are built in CI pipelines.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
image: Container image reference (e.g., 'user/repo:tag' or 'registry.com/user/repo:tag')
|
|
43
|
+
registry_pat: Optional personal access token for registry authentication
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
True if the image exists and is accessible
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: If the image is invalid, not found, or authentication fails
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def validate(image: str, registry_pat: str | None = None) -> bool:
|
|
54
|
+
if not image:
|
|
55
|
+
raise ValueError("Container image must be a non-empty string")
|
|
56
|
+
|
|
57
|
+
segments = image.split("/")
|
|
58
|
+
if len(segments) < 2:
|
|
59
|
+
raise ValueError(f"Invalid container image reference: {image}")
|
|
60
|
+
|
|
61
|
+
if len(segments) == 3:
|
|
62
|
+
registry_host = segments[0]
|
|
63
|
+
repo_part = "/".join(segments[1:])
|
|
64
|
+
username = segments[1]
|
|
65
|
+
else:
|
|
66
|
+
registry_host = "registry-1.docker.io"
|
|
67
|
+
repo_part = "/".join(segments)
|
|
68
|
+
username = segments[0]
|
|
69
|
+
|
|
70
|
+
if ":" in repo_part:
|
|
71
|
+
repository, tag = repo_part.split(":", 1)
|
|
72
|
+
else:
|
|
73
|
+
repository, tag = repo_part, "latest"
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
dxf = DXF(registry_host, repo=repository)
|
|
77
|
+
|
|
78
|
+
if registry_pat:
|
|
79
|
+
dxf.authenticate(
|
|
80
|
+
username=username,
|
|
81
|
+
password=registry_pat,
|
|
82
|
+
actions=["pull"],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
dxf.get_alias(tag)
|
|
86
|
+
|
|
87
|
+
except DXFUnauthorizedError:
|
|
88
|
+
raise ValueError(f"Container image '{image}' requires authentication")
|
|
89
|
+
except requests.exceptions.HTTPError as e:
|
|
90
|
+
if e.response is not None and hasattr(e.response, "status_code"):
|
|
91
|
+
if e.response.status_code == 403:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"Container image '{image}' requires authentication; invalid registry PAT"
|
|
94
|
+
)
|
|
95
|
+
elif e.response.status_code == 404:
|
|
96
|
+
raise ValueError(f"Container image '{image}' not found")
|
|
97
|
+
raise ValueError(f"Error validating container image '{image}': {str(e)}")
|
|
98
|
+
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
|
|
27
102
|
class K8sContext:
|
|
28
103
|
container_image: str | None = None
|
|
29
104
|
registry_pat: str | None = None
|
|
@@ -74,6 +149,8 @@ class K8sContext:
|
|
|
74
149
|
return "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
|
75
150
|
|
|
76
151
|
def set_container_image(self, image: str):
|
|
152
|
+
ContainerImageValidator.validate(image, self.registry_pat)
|
|
153
|
+
|
|
77
154
|
self.container_image = image
|
|
78
155
|
|
|
79
156
|
def set_registry_pat(self, pat: str):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: q8s
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.2
|
|
4
4
|
Summary: Kernel extension for executing quantum programs in simulators on q8s clusters
|
|
5
5
|
Author-email: Vlad Stirbu <vstirbu@gmail.com>
|
|
6
6
|
License: Apache License
|
|
@@ -229,6 +229,7 @@ Requires-Dist: python-dotenv==1.0.1
|
|
|
229
229
|
Requires-Dist: pyyaml==6.0.2
|
|
230
230
|
Requires-Dist: typer>=0.15.4
|
|
231
231
|
Requires-Dist: GitPython>=3.1.43
|
|
232
|
+
Requires-Dist: python-dxf>=12.1.1
|
|
232
233
|
Provides-Extra: benchmark
|
|
233
234
|
Requires-Dist: pandas; extra == "benchmark"
|
|
234
235
|
Requires-Dist: testbook; extra == "benchmark"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
from dxf.exceptions import DXFUnauthorizedError
|
|
6
|
+
|
|
7
|
+
from q8s.execution import ContainerImageValidator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MockResponse:
|
|
11
|
+
def __init__(self, status_code):
|
|
12
|
+
self.status_code = status_code
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestContainerImageValidator(unittest.TestCase):
|
|
16
|
+
|
|
17
|
+
@patch("q8s.execution.DXF")
|
|
18
|
+
def test_validate_dockerhub_success_without_pat(self, MockDXF):
|
|
19
|
+
instance = MockDXF.return_value
|
|
20
|
+
|
|
21
|
+
result = ContainerImageValidator.validate("user/repo:tag")
|
|
22
|
+
|
|
23
|
+
self.assertTrue(result)
|
|
24
|
+
MockDXF.assert_called_once_with("registry-1.docker.io", repo="user/repo")
|
|
25
|
+
instance.authenticate.assert_not_called()
|
|
26
|
+
instance.get_alias.assert_called_once_with("tag")
|
|
27
|
+
|
|
28
|
+
@patch("q8s.execution.DXF")
|
|
29
|
+
def test_validate_custom_registry_with_pat_authenticate_called(self, MockDXF):
|
|
30
|
+
instance = MockDXF.return_value
|
|
31
|
+
|
|
32
|
+
result = ContainerImageValidator.validate(
|
|
33
|
+
"myregistry.com/user/repo:tag", registry_pat="token123"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self.assertTrue(result)
|
|
37
|
+
MockDXF.assert_called_once_with("myregistry.com", repo="user/repo")
|
|
38
|
+
instance.authenticate.assert_called_once_with(
|
|
39
|
+
username="user", password="token123", actions=["pull"]
|
|
40
|
+
)
|
|
41
|
+
instance.get_alias.assert_called_once_with("tag")
|
|
42
|
+
|
|
43
|
+
@patch("q8s.execution.DXF")
|
|
44
|
+
def test_validate_uses_latest_tag_by_default(self, MockDXF):
|
|
45
|
+
instance = MockDXF.return_value
|
|
46
|
+
|
|
47
|
+
result = ContainerImageValidator.validate("user/repo")
|
|
48
|
+
|
|
49
|
+
self.assertTrue(result)
|
|
50
|
+
MockDXF.assert_called_once_with("registry-1.docker.io", repo="user/repo")
|
|
51
|
+
instance.get_alias.assert_called_once_with("latest")
|
|
52
|
+
|
|
53
|
+
@patch("q8s.execution.DXF")
|
|
54
|
+
def test_validate_unauthorized_raises_value_error(self, MockDXF):
|
|
55
|
+
instance = MockDXF.return_value
|
|
56
|
+
instance.get_alias.side_effect = DXFUnauthorizedError()
|
|
57
|
+
|
|
58
|
+
with self.assertRaises(ValueError) as ctx:
|
|
59
|
+
ContainerImageValidator.validate("user/repo:tag")
|
|
60
|
+
|
|
61
|
+
self.assertIn("requires authentication", str(ctx.exception))
|
|
62
|
+
|
|
63
|
+
@patch("q8s.execution.DXF")
|
|
64
|
+
def test_validate_http_403_invalid_pat(self, MockDXF):
|
|
65
|
+
instance = MockDXF.return_value
|
|
66
|
+
|
|
67
|
+
http_error = requests.exceptions.HTTPError()
|
|
68
|
+
|
|
69
|
+
http_error.response = MockResponse(403)
|
|
70
|
+
instance.get_alias.side_effect = http_error
|
|
71
|
+
|
|
72
|
+
with self.assertRaises(ValueError) as ctx:
|
|
73
|
+
ContainerImageValidator.validate("user/repo:tag", registry_pat="badpat")
|
|
74
|
+
|
|
75
|
+
self.assertIn("invalid registry PAT", str(ctx.exception))
|
|
76
|
+
|
|
77
|
+
@patch("q8s.execution.DXF")
|
|
78
|
+
def test_validate_http_404_not_found(self, MockDXF):
|
|
79
|
+
instance = MockDXF.return_value
|
|
80
|
+
|
|
81
|
+
http_error = requests.exceptions.HTTPError()
|
|
82
|
+
|
|
83
|
+
http_error.response = MockResponse(404)
|
|
84
|
+
instance.get_alias.side_effect = http_error
|
|
85
|
+
|
|
86
|
+
with self.assertRaises(ValueError) as ctx:
|
|
87
|
+
ContainerImageValidator.validate("user/repo:missing")
|
|
88
|
+
|
|
89
|
+
self.assertIn("not found", str(ctx.exception))
|
|
90
|
+
|
|
91
|
+
def test_validate_empty_image_raises(self):
|
|
92
|
+
with self.assertRaises(ValueError):
|
|
93
|
+
ContainerImageValidator.validate("")
|
|
94
|
+
|
|
95
|
+
@patch("q8s.execution.DXF")
|
|
96
|
+
def test_validate_invalid_reference_raises(self, MockDXF):
|
|
97
|
+
with self.assertRaises(ValueError) as ctx:
|
|
98
|
+
ContainerImageValidator.validate("invalidimage")
|
|
99
|
+
self.assertIn("Invalid container image reference", str(ctx.exception))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -141,8 +141,8 @@ def execute(
|
|
|
141
141
|
) as progress:
|
|
142
142
|
k8s_context = K8sContext(kubeconfig.as_posix(), progress=progress)
|
|
143
143
|
k8s_context.set_target(target)
|
|
144
|
-
k8s_context.set_container_image(image)
|
|
145
144
|
k8s_context.set_registry_pat(registry_pat)
|
|
145
|
+
k8s_context.set_container_image(image)
|
|
146
146
|
|
|
147
147
|
workload = Workload.from_entry_script(entry_script=file)
|
|
148
148
|
workload.set_args(args or [])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -58,8 +58,8 @@ class Q8sKernel(Kernel):
|
|
|
58
58
|
expand=True,
|
|
59
59
|
),
|
|
60
60
|
)
|
|
61
|
-
self.k8s_context.set_container_image(self.docker_image)
|
|
62
61
|
self.k8s_context.set_registry_pat(os.environ.get("REGISTRY_PAT", None))
|
|
62
|
+
self.k8s_context.set_container_image(self.docker_image)
|
|
63
63
|
|
|
64
64
|
logging.info("q8s kernel started")
|
|
65
65
|
logging.info(f"docker image: {self.docker_image}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|