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.
Files changed (42) hide show
  1. {q8s-0.11.0.dev2 → q8s-0.11.2}/PKG-INFO +2 -1
  2. {q8s-0.11.0.dev2 → q8s-0.11.2}/pyproject.toml +2 -1
  3. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/__init__.py +2 -0
  4. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/bakefile.py +1 -1
  5. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/execution.py +77 -0
  6. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s.egg-info/PKG-INFO +2 -1
  7. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s.egg-info/SOURCES.txt +1 -0
  8. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s.egg-info/requires.txt +1 -0
  9. q8s-0.11.2/tests/test_execution.py +103 -0
  10. {q8s-0.11.0.dev2 → q8s-0.11.2}/LICENSE +0 -0
  11. {q8s-0.11.0.dev2 → q8s-0.11.2}/README.md +0 -0
  12. {q8s-0.11.0.dev2 → q8s-0.11.2}/setup.cfg +0 -0
  13. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/__main__.py +0 -0
  14. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/cli.py +1 -1
  15. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/constants.py +0 -0
  16. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/enums.py +0 -0
  17. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/install.py +0 -0
  18. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/kernel.py +1 -1
  19. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/matplotlib/backend.py +0 -0
  20. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/multifiles.py +0 -0
  21. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/plugins/__init__.py +0 -0
  22. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/plugins/cpu_job.py +0 -0
  23. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/plugins/cuda_job.py +0 -0
  24. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/plugins/job.py +0 -0
  25. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/plugins/job_template_spec.py +0 -0
  26. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/plugins/utils/__init__.py +0 -0
  27. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/plugins/utils/git_info.py +0 -0
  28. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/project.py +0 -0
  29. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/utils.py +0 -0
  30. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s/workload.py +0 -0
  31. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s.egg-info/dependency_links.txt +0 -0
  32. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s.egg-info/entry_points.txt +0 -0
  33. {q8s-0.11.0.dev2 → q8s-0.11.2}/src/q8s.egg-info/top_level.txt +0 -0
  34. {q8s-0.11.0.dev2 → q8s-0.11.2}/tests/test_backend.py +0 -0
  35. {q8s-0.11.0.dev2 → q8s-0.11.2}/tests/test_bakefile.py +0 -0
  36. {q8s-0.11.0.dev2 → q8s-0.11.2}/tests/test_git_info.py +0 -0
  37. {q8s-0.11.0.dev2 → q8s-0.11.2}/tests/test_job_plugin.py +0 -0
  38. {q8s-0.11.0.dev2 → q8s-0.11.2}/tests/test_job_template_spec.py +0 -0
  39. {q8s-0.11.0.dev2 → q8s-0.11.2}/tests/test_kernel.py +0 -0
  40. {q8s-0.11.0.dev2 → q8s-0.11.2}/tests/test_multifiles.py +0 -0
  41. {q8s-0.11.0.dev2 → q8s-0.11.2}/tests/test_project.py +0 -0
  42. {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.0.dev2
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.0-dev2"
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
 
@@ -1 +1,3 @@
1
1
  """A Python kernel for Jupyter that uses Kubernetes to execute code."""
2
+
3
+ from .kernel import Q8sKernel # noqa: F401
@@ -46,7 +46,7 @@ class Bakefile:
46
46
  self,
47
47
  name: str,
48
48
  tags: list[str],
49
- platforms: list[BuildPlatform],
49
+ platforms: list[BuildPlatform | str],
50
50
  ):
51
51
  bake_target = BakeTargetName(name)
52
52
 
@@ -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.0.dev2
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"
@@ -30,6 +30,7 @@ src/q8s/plugins/utils/__init__.py
30
30
  src/q8s/plugins/utils/git_info.py
31
31
  tests/test_backend.py
32
32
  tests/test_bakefile.py
33
+ tests/test_execution.py
33
34
  tests/test_git_info.py
34
35
  tests/test_job_plugin.py
35
36
  tests/test_job_template_spec.py
@@ -7,6 +7,7 @@ python-dotenv==1.0.1
7
7
  pyyaml==6.0.2
8
8
  typer>=0.15.4
9
9
  GitPython>=3.1.43
10
+ python-dxf>=12.1.1
10
11
 
11
12
  [benchmark]
12
13
  pandas
@@ -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