q8s 0.7.0__tar.gz → 0.8.0.dev2__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.7.0 → q8s-0.8.0.dev2}/PKG-INFO +5 -1
  2. {q8s-0.7.0 → q8s-0.8.0.dev2}/README.md +4 -0
  3. {q8s-0.7.0 → q8s-0.8.0.dev2}/pyproject.toml +1 -1
  4. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/cli.py +8 -1
  5. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/execution.py +1 -1
  6. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/multifiles.py +9 -6
  7. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/plugins/cpu_job.py +9 -5
  8. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/plugins/cuda_job.py +9 -5
  9. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/plugins/job_template_spec.py +1 -4
  10. q8s-0.8.0.dev2/q8s/workload.py +167 -0
  11. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s.egg-info/PKG-INFO +5 -1
  12. q8s-0.7.0/q8s/workload.py +0 -82
  13. {q8s-0.7.0 → q8s-0.8.0.dev2}/LICENSE +0 -0
  14. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/__init__.py +0 -0
  15. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/__main__.py +0 -0
  16. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/constants.py +0 -0
  17. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/deps/__init__.py +0 -0
  18. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/deps/code_analyzer.py +0 -0
  19. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/deps/parser.py +0 -0
  20. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/enums.py +0 -0
  21. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/install.py +0 -0
  22. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/kernel.py +0 -0
  23. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/matplotlib/backend.py +0 -0
  24. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/plugins/__init__.py +0 -0
  25. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/project.py +0 -0
  26. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s/utils.py +0 -0
  27. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s.egg-info/SOURCES.txt +0 -0
  28. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s.egg-info/dependency_links.txt +0 -0
  29. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s.egg-info/entry_points.txt +0 -0
  30. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s.egg-info/requires.txt +0 -0
  31. {q8s-0.7.0 → q8s-0.8.0.dev2}/q8s.egg-info/top_level.txt +0 -0
  32. {q8s-0.7.0 → q8s-0.8.0.dev2}/setup.cfg +0 -0
  33. {q8s-0.7.0 → q8s-0.8.0.dev2}/tests/__init__.py +0 -0
  34. {q8s-0.7.0 → q8s-0.8.0.dev2}/tests/fixtures/cache/app.py +0 -0
  35. {q8s-0.7.0 → q8s-0.8.0.dev2}/tests/fixtures/simple/qiskit.py +0 -0
  36. {q8s-0.7.0 → q8s-0.8.0.dev2}/tests/test_backend.py +0 -0
  37. {q8s-0.7.0 → q8s-0.8.0.dev2}/tests/test_deps.py +0 -0
  38. {q8s-0.7.0 → q8s-0.8.0.dev2}/tests/test_job_template_spec.py +0 -0
  39. {q8s-0.7.0 → q8s-0.8.0.dev2}/tests/test_kernel.py +0 -0
  40. {q8s-0.7.0 → q8s-0.8.0.dev2}/tests/test_multifiles.py +0 -0
  41. {q8s-0.7.0 → q8s-0.8.0.dev2}/tests/test_project.py +0 -0
  42. {q8s-0.7.0 → q8s-0.8.0.dev2}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: q8s
3
- Version: 0.7.0
3
+ Version: 0.8.0.dev2
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
@@ -234,6 +234,10 @@ Dynamic: license-file
234
234
 
235
235
  # q8s
236
236
 
237
+ [![PyPI version](https://img.shields.io/pypi/v/q8s.svg)](https://pypi.org/project/q8s/)
238
+ [![Python versions](https://img.shields.io/pypi/pyversions/q8s.svg)](https://pypi.org/project/q8s/)
239
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/q8s?period=total&units=INTERNATIONAL_SYSTEM&left_color=GREY&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/q8s)
240
+
237
241
  Toolset for executing quantum jobs on [Qubernetes](https://www.qubernetes.dev).
238
242
 
239
243
  ## Installation
@@ -1,5 +1,9 @@
1
1
  # q8s
2
2
 
3
+ [![PyPI version](https://img.shields.io/pypi/v/q8s.svg)](https://pypi.org/project/q8s/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/q8s.svg)](https://pypi.org/project/q8s/)
5
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/q8s?period=total&units=INTERNATIONAL_SYSTEM&left_color=GREY&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/q8s)
6
+
3
7
  Toolset for executing quantum jobs on [Qubernetes](https://www.qubernetes.dev).
4
8
 
5
9
  ## Installation
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "q8s"
3
- version = "0.7.0"
3
+ version = "0.8.0.dev2"
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"
@@ -71,7 +71,12 @@ def build(
71
71
  project.update_images_cache()
72
72
 
73
73
 
74
- @app.command()
74
+ @app.command(
75
+ context_settings={
76
+ "allow_extra_args": True,
77
+ "ignore_unknown_options": True,
78
+ }
79
+ )
75
80
  def execute(
76
81
  file: Annotated[Path, typer.Argument(help="Python file to be executed")],
77
82
  target: Annotated[
@@ -88,6 +93,7 @@ def execute(
88
93
  envvar="REGISTRY_PAT",
89
94
  ),
90
95
  ] = None,
96
+ args: Annotated[list[str], typer.Argument(help="Additional arguments")] = None,
91
97
  ):
92
98
  project = Project()
93
99
 
@@ -117,6 +123,7 @@ def execute(
117
123
  k8s_context.set_registry_pat(registry_pat)
118
124
 
119
125
  workload = Workload.from_entry_script(entry_script=file)
126
+ workload.set_args(args or [])
120
127
 
121
128
  output, stream_name = k8s_context.execute_workload(workload=workload)
122
129
 
@@ -136,7 +136,7 @@ class K8sContext:
136
136
  self.__progress.console.print("Job created")
137
137
 
138
138
  self.__create_config_map_object_from_workload(job, workload=workload)
139
- self.__progress.console.print("Application code created")
139
+ self.__progress.console.print("Application code attached")
140
140
  self.__create_environment_secret()
141
141
  self.__progress.console.print("Environment variables created")
142
142
 
@@ -9,16 +9,17 @@ GLOBAL = getattr(sys, "base_prefix", sys.prefix)
9
9
  LOCAL = sys.prefix
10
10
 
11
11
 
12
- def _resolve_spec(modname: str, package: str | None = None):
13
- cwd = os.getcwd()
14
- sys.path.insert(0, cwd)
12
+ def _resolve_spec(modname: str, package: str | None = None, is_project: bool = False):
13
+ """Resolve a module spec, temporarily adjusting sys.path for project layout."""
14
+ cwd = Path(os.getcwd()) / "src" if is_project else Path(os.getcwd())
15
+ sys.path.insert(0, str(cwd))
15
16
 
16
17
  try:
17
18
  return importlib.util.find_spec(modname, package=package)
18
19
  except Exception:
19
20
  return None
20
21
  finally:
21
- sys.path.remove(cwd)
22
+ sys.path.remove(str(cwd))
22
23
 
23
24
 
24
25
  def _spec_origin_files(spec: ModuleSpec):
@@ -106,7 +107,9 @@ def pkg_name_for_file(fpath: Path | str):
106
107
  return ".".join(reversed(parts)) if parts else None
107
108
 
108
109
 
109
- def collect_imported_files(entry_script: str | Path):
110
+ def collect_imported_files(
111
+ entry_script: str | Path, is_project: bool = True
112
+ ) -> set[Path]:
110
113
  """
111
114
  Return a set of file paths transitively imported by entry_script
112
115
  except pip-installed and global interpreter packages.
@@ -136,7 +139,7 @@ def collect_imported_files(entry_script: str | Path):
136
139
  continue
137
140
 
138
141
  # Explore the resolved module/package
139
- spec = _resolve_spec(target)
142
+ spec = _resolve_spec(target, is_project=is_project)
140
143
  if not spec:
141
144
  continue
142
145
 
@@ -16,10 +16,7 @@ class CPUJobTemplatePlugin:
16
16
  registry_credentials_secret_name: str,
17
17
  container_image: str,
18
18
  workload: Workload,
19
- env: Dict[
20
- str,
21
- str | None,
22
- ],
19
+ env: list[client.V1EnvVar],
23
20
  target: Target,
24
21
  ) -> client.V1PodTemplateSpec:
25
22
 
@@ -28,12 +25,19 @@ class CPUJobTemplatePlugin:
28
25
 
29
26
  volume_name = f"app-volume-{name}"
30
27
 
28
+ if workload.is_src_project:
29
+ env.append(client.V1EnvVar(name="PYTHONPATH", value=f"{WORKSPACE}/src"))
30
+
31
31
  container = client.V1Container(
32
32
  name="quantum-routine",
33
33
  image=container_image,
34
34
  env=env,
35
35
  command=["python"],
36
- args=[f"{WORKSPACE}/{workload.entry_script}"],
36
+ args=(
37
+ ["-m", workload.entry_module] + workload.args
38
+ if workload.is_src_project
39
+ else [f"{WORKSPACE}/{workload.entry_script}"] + workload.args
40
+ ),
37
41
  image_pull_policy="Always",
38
42
  volume_mounts=[
39
43
  client.V1VolumeMount(
@@ -22,10 +22,7 @@ class CUDAJobTemplatePlugin:
22
22
  registry_credentials_secret_name: str,
23
23
  container_image: str,
24
24
  workload: Workload,
25
- env: Dict[
26
- str,
27
- str | None,
28
- ],
25
+ env: list[client.V1EnvVar],
29
26
  target: Target,
30
27
  ) -> client.V1PodTemplateSpec:
31
28
 
@@ -34,12 +31,19 @@ class CUDAJobTemplatePlugin:
34
31
 
35
32
  volume_name = f"app-volume-{name}"
36
33
 
34
+ if workload.is_src_project:
35
+ env.append(client.V1EnvVar(name="PYTHONPATH", value=f"{WORKSPACE}/src"))
36
+
37
37
  container = client.V1Container(
38
38
  name="quantum-routine",
39
39
  image=container_image,
40
40
  env=env,
41
41
  command=["python"],
42
- args=[f"{WORKSPACE}/{workload.entry_script}"],
42
+ args=(
43
+ ["-m", workload.entry_module] + workload.args
44
+ if workload.is_src_project
45
+ else [f"{WORKSPACE}/{workload.entry_script}"] + workload.args
46
+ ),
43
47
  image_pull_policy="Always",
44
48
  resources=(
45
49
  client.V1ResourceRequirements(
@@ -34,10 +34,7 @@ class JobTemplatePluginSpec:
34
34
  registry_credentials_secret_name: str,
35
35
  container_image: str,
36
36
  workload: Workload,
37
- env: Dict[
38
- str,
39
- str | None,
40
- ],
37
+ env: list[client.V1EnvVar],
41
38
  target: Target,
42
39
  ) -> client.V1PodTemplateSpec:
43
40
  return None
@@ -0,0 +1,167 @@
1
+ from pathlib import Path
2
+ import configparser
3
+ import os
4
+
5
+ from .multifiles import collect_imported_files
6
+
7
+
8
+ class Workload:
9
+ """
10
+ Represents a mutifile workload consisting of an entry script and its imported files
11
+ """
12
+
13
+ __pyproject_root: Path | None = None
14
+ __args: list[str] = []
15
+
16
+ def __init__(self, entry_script: Path | str = None, code: str = None):
17
+ if entry_script is not None:
18
+
19
+ if self.__is_src_layout():
20
+ self.__entry_script = Path(os.path.abspath(entry_script))
21
+ self.__base_path = self.__pyproject_root
22
+
23
+ self.__files = collect_imported_files(
24
+ self.__entry_script, is_project=True
25
+ )
26
+
27
+ else:
28
+ self.__entry_script = Path(os.path.abspath(entry_script))
29
+ self.__base_path = self.__entry_script.parent
30
+
31
+ self.__files = collect_imported_files(
32
+ self.__entry_script, is_project=False
33
+ )
34
+
35
+ self.__data = {
36
+ self.__path_mapping(f): open(f, "r").read() for f in self.__files
37
+ }
38
+
39
+ elif code is not None:
40
+ self.__entry_script = Path(os.path.abspath("main.py"))
41
+ self.__base_path = os.getcwd()
42
+ self.__files = [Path(self.__entry_script)]
43
+
44
+ self.__data = {self.__path_mapping(self.__entry_script): code}
45
+
46
+ else:
47
+ raise ValueError("Either entry_script or code must be provided.")
48
+
49
+ @classmethod
50
+ def from_code(cls, code: str):
51
+ """
52
+ Create a Workload from code string
53
+ """
54
+ return cls(code=code)
55
+
56
+ @classmethod
57
+ def from_entry_script(cls, entry_script: Path | str):
58
+ """
59
+ Create a Workload from an entry script file path
60
+ """
61
+ return cls(entry_script=entry_script)
62
+
63
+ @property
64
+ def files(self) -> list[Path]:
65
+ """
66
+ List of all files in the workload
67
+ """
68
+ return self.__files
69
+
70
+ @property
71
+ def entry_script(self) -> Path:
72
+ """
73
+ Entry script path relative to base path
74
+ """
75
+ return self.__relative_path(self.__entry_script)
76
+
77
+ @property
78
+ def is_src_project(self) -> bool:
79
+ """
80
+ Whether the project uses a src/ layout
81
+ """
82
+ return self.__pyproject_root is not None
83
+
84
+ @property
85
+ def entry_module(self) -> str:
86
+ """
87
+ Entry script module name
88
+ """
89
+ return self.entry_script.replace("/", ".")[:-3] # strip .py
90
+
91
+ @property
92
+ def data(self) -> dict[str, str]:
93
+ """
94
+ Dictionary mapping file paths to their contents
95
+ """
96
+ return self.__data
97
+
98
+ @property
99
+ def mappings(self):
100
+ """
101
+ Dictionary mapping relative file paths to their unique path mappings
102
+ """
103
+ return {self.__path_mapping(f): self.__relative_path(f) for f in self.__files}
104
+
105
+ @property
106
+ def args(self) -> list[str]:
107
+ """
108
+ Additional arguments for the workload
109
+ """
110
+ return self.__args
111
+
112
+ def set_args(self, args: list[str]) -> None:
113
+ """
114
+ Set additional arguments for the workload
115
+ """
116
+ self.__args = args
117
+
118
+ def __relative_path(self, file: Path | str) -> str:
119
+ """
120
+ Get the relative path of a file with respect to the base path
121
+ """
122
+ return os.path.relpath(os.path.abspath(file), self.__base_path)
123
+
124
+ def __path_mapping(self, file: Path | str) -> str:
125
+ """
126
+ Get the unique path mapping for a file
127
+ """
128
+ return self.__relative_path(file).replace("/", "__")
129
+
130
+ def __pyproject_root(self, start: Path | None = None) -> Path | None:
131
+ """
132
+ Find the project root by looking for setup.cfg or pyproject.toml
133
+ """
134
+
135
+ if (
136
+ Path(os.path.join(os.getcwd(), "setup.cfg")).exists()
137
+ or Path(os.path.join(os.getcwd(), "pyproject.toml")).exists()
138
+ ):
139
+ return Path(os.getcwd()).resolve()
140
+
141
+ return None
142
+
143
+ def __is_src_layout(self) -> bool:
144
+ """
145
+ Determine if the project uses a src/ layout
146
+ """
147
+ root = self.__pyproject_root()
148
+
149
+ if root is None:
150
+ return False
151
+
152
+ cfg_path = root / "setup.cfg"
153
+ if not cfg_path.exists():
154
+ return False
155
+
156
+ config = configparser.ConfigParser()
157
+ config.read(cfg_path)
158
+
159
+ # Very simple heuristic: [options.packages.find] where = src
160
+ if config.has_section("options.packages.find"):
161
+ where = config.get("options.packages.find", "where", fallback="").strip()
162
+ if where == "src":
163
+ self.__pyproject_root = root
164
+ return True
165
+
166
+ # You could add similar checks for pyproject.toml if you use that
167
+ return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: q8s
3
- Version: 0.7.0
3
+ Version: 0.8.0.dev2
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
@@ -234,6 +234,10 @@ Dynamic: license-file
234
234
 
235
235
  # q8s
236
236
 
237
+ [![PyPI version](https://img.shields.io/pypi/v/q8s.svg)](https://pypi.org/project/q8s/)
238
+ [![Python versions](https://img.shields.io/pypi/pyversions/q8s.svg)](https://pypi.org/project/q8s/)
239
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/q8s?period=total&units=INTERNATIONAL_SYSTEM&left_color=GREY&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/q8s)
240
+
237
241
  Toolset for executing quantum jobs on [Qubernetes](https://www.qubernetes.dev).
238
242
 
239
243
  ## Installation
q8s-0.7.0/q8s/workload.py DELETED
@@ -1,82 +0,0 @@
1
- from pathlib import Path
2
- import os
3
-
4
- from .multifiles import collect_imported_files
5
-
6
-
7
- class Workload:
8
- """
9
- Represents a mutifile workload consisting of an entry script and its imported files
10
- """
11
-
12
- def __init__(self, entry_script: Path | str = None, code: str = None):
13
- if entry_script is not None:
14
- self.__entry_script = Path(os.path.abspath(entry_script))
15
- self.__base_path = self.__entry_script.parent
16
-
17
- self.__files = collect_imported_files(self.__entry_script)
18
- self.__data = {
19
- self.__path_mapping(f): open(f, "r").read() for f in self.__files
20
- }
21
- elif code is not None:
22
- self.__entry_script = Path(os.path.abspath("main.py"))
23
- self.__base_path = os.getcwd()
24
- self.__files = [Path(self.__entry_script)]
25
-
26
- self.__data = {self.__path_mapping(self.__entry_script): code}
27
- else:
28
- raise ValueError("Either entry_script or code must be provided.")
29
-
30
- @classmethod
31
- def from_code(cls, code: str):
32
- """
33
- Create a Workload from code string
34
- """
35
- return cls(code=code)
36
-
37
- @classmethod
38
- def from_entry_script(cls, entry_script: Path | str):
39
- """
40
- Create a Workload from an entry script file path
41
- """
42
- return cls(entry_script=entry_script)
43
-
44
- @property
45
- def files(self) -> list[Path]:
46
- """
47
- List of all files in the workload
48
- """
49
- return self.__files
50
-
51
- @property
52
- def entry_script(self) -> Path:
53
- """
54
- Entry script path relative to base path
55
- """
56
- return self.__relative_path(self.__entry_script)
57
-
58
- @property
59
- def data(self) -> dict[str, str]:
60
- """
61
- Dictionary mapping file paths to their contents
62
- """
63
- return self.__data
64
-
65
- @property
66
- def mappings(self):
67
- """
68
- Dictionary mapping relative file paths to their unique path mappings
69
- """
70
- return {self.__path_mapping(f): self.__relative_path(f) for f in self.__files}
71
-
72
- def __relative_path(self, file: Path | str) -> str:
73
- """
74
- Get the relative path of a file with respect to the base path
75
- """
76
- return os.path.relpath(os.path.abspath(file), self.__base_path)
77
-
78
- def __path_mapping(self, file: Path | str) -> str:
79
- """
80
- Get the unique path mapping for a file
81
- """
82
- return self.__relative_path(file).replace("/", "__")
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
File without changes