Skip to content

Commit ddcea46

Browse files
committed
Merge branch 'main' into feat/set-function-as-disabled
# Conflicts: # gateway/api/views/programs.py
2 parents 7886df1 + f158a23 commit ddcea46

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1375
-934
lines changed

charts/qiskit-serverless/Chart.lock

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ dependencies:
1010
version: 13.4.4
1111
- name: kuberay-operator
1212
repository: https://ray-project.github.io/kuberay-helm
13-
version: 1.1.1
14-
digest: sha256:66aa676552fc569d63d2433414b9b116c6833f58801ca8de3aa0e4d7e66ace32
15-
generated: "2025-03-19T18:47:17.196619173Z"
13+
version: 1.3.2
14+
digest: sha256:d874fa990fa2725d03eef12526c082b702b8ebd1eb86089c14323236adb19c8b
15+
generated: "2025-04-21T15:57:24.067646-04:00"

charts/qiskit-serverless/Chart.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ dependencies:
2121
repository: https://charts.bitnami.com/bitnami
2222
- name: kuberay-operator
2323
condition: kuberayOperatorEnable
24-
version: 1.1.1
24+
version: 1.3.2
2525
repository: https://ray-project.github.io/kuberay-helm
2626

2727
maintainers:

charts/qiskit-serverless/values.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ kuberayOperatorEnable: true
114114
kuberay-operator:
115115
image:
116116
repository: quay.io/kuberay/operator
117-
tag: v1.1.1
117+
tag: v1.3.2
118118
pullPolicy: IfNotPresent
119119

120120
# ===================

client/qiskit_serverless/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
RayClient,
3434
LocalClient,
3535
save_result,
36+
update_status,
37+
Job,
3638
Configuration,
3739
is_running_in_serverless,
3840
is_trial,

client/qiskit_serverless/core/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
from .job import (
5858
Job,
5959
save_result,
60+
update_status,
6061
Configuration,
6162
is_running_in_serverless,
6263
is_trial,

client/qiskit_serverless/core/client.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
RunService,
3838
)
3939
from qiskit_serverless.utils import JsonSerializable
40-
from qiskit_serverless.visualizaiton import Widget
4140

4241

4342
class BaseClient(JobService, RunService, JsonSerializable, ABC):
@@ -191,4 +190,9 @@ def list(self, **kwargs) -> List[RunnableQiskitFunction]:
191190

192191
def widget(self):
193192
"""Widget for information about provider and jobs."""
193+
# prevent ciclic import
194+
from qiskit_serverless.visualization import ( # pylint: disable=import-outside-toplevel
195+
Widget,
196+
)
197+
194198
return Widget(self).show()

client/qiskit_serverless/core/clients/serverless_client.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,12 @@ def status(self, job_id: str):
310310
)
311311
)
312312

313-
return response_data.get("status", default_status)
313+
status = response_data.get("status", default_status)
314+
sub_status = response_data.get("sub_status")
315+
if status == Job.RUNNING and sub_status is not None:
316+
return sub_status
317+
318+
return status
314319

315320
@_trace_job
316321
def stop(self, job_id: str, service: Optional[QiskitRuntimeService] = None):

client/qiskit_serverless/core/decorators.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ def generated_decorator(traced_function: Union[FunctionType, str]):
467467
def decorator_trace(func: FunctionType):
468468
"""The decorator that python call"""
469469

470+
@functools.wraps(func)
470471
def wrapper(*args, **kwargs):
471472
"""The wrapper"""
472473
tracer = trace.get_tracer("client.tracer")
@@ -475,7 +476,7 @@ def wrapper(*args, **kwargs):
475476
if isinstance(traced_function, str)
476477
else func.__name__
477478
)
478-
with tracer.start_as_current_span(f"{traced_feature}.${function_name}"):
479+
with tracer.start_as_current_span(f"{traced_feature}.{function_name}"):
479480
result = func(*args, **kwargs)
480481
return result
481482

client/qiskit_serverless/core/job.py

+61-8
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,19 @@ def filtered_logs(self, job_id: str, **kwargs) -> str:
114114
class Job:
115115
"""Job."""
116116

117+
PENDING = "PENDING"
118+
RUNNING = "RUNNING"
119+
STOPPED = "STOPPED"
120+
SUCCEEDED = "SUCCEEDED"
121+
FAILED = "FAILED"
122+
QUEUED = "QUEUED"
123+
# RUNNING statuses
124+
MAPPING = "MAPPING"
125+
OPTIMIZING_HARDWARE = "OPTIMIZING_HARDWARE"
126+
WAITING_QPU = "WAITING_QPU"
127+
EXECUTING_QPU = "EXECUTING_QPU"
128+
POST_PROCESSING = "POST_PROCESSING"
129+
117130
def __init__(
118131
self,
119132
job_id: str,
@@ -200,8 +213,8 @@ def result(self, wait=True, cadence=30, verbose=False, maxwait=0):
200213

201214
def in_terminal_state(self) -> bool:
202215
"""Checks if job is in terminal state"""
203-
terminal_states = ["CANCELED", "DONE", "ERROR"]
204-
return self.status() in terminal_states
216+
terminal_status = ["CANCELED", "DONE", "ERROR"]
217+
return self.status() in terminal_status
205218

206219
def __repr__(self):
207220
return f"<Job | {self.job_id}>"
@@ -277,15 +290,55 @@ def save_result(result: Dict[str, Any]):
277290
return response.ok
278291

279292

293+
def update_status(status: str):
294+
"""Update sub status."""
295+
296+
version = os.environ.get(ENV_GATEWAY_PROVIDER_VERSION)
297+
if version is None:
298+
version = GATEWAY_PROVIDER_VERSION_DEFAULT
299+
300+
token = os.environ.get(ENV_JOB_GATEWAY_TOKEN)
301+
if token is None:
302+
logging.warning(
303+
"'sub_status' cannot be updated since"
304+
"there is no information about the"
305+
"authorization token in the environment."
306+
)
307+
return False
308+
309+
instance = os.environ.get(ENV_JOB_GATEWAY_INSTANCE, None)
310+
311+
url = (
312+
f"{os.environ.get(ENV_JOB_GATEWAY_HOST)}/"
313+
f"api/{version}/jobs/{os.environ.get(ENV_JOB_ID_GATEWAY)}/sub_status/"
314+
)
315+
response = requests.patch(
316+
url,
317+
data={"sub_status": status},
318+
headers=get_headers(token=token, instance=instance),
319+
timeout=REQUESTS_TIMEOUT,
320+
)
321+
if not response.ok:
322+
sanitized = response.text.replace("\n", "").replace("\r", "")
323+
logging.warning("Something went wrong: %s", sanitized)
324+
325+
return response.ok
326+
327+
280328
def _map_status_to_serverless(status: str) -> str:
281329
"""Map a status string from job client to the Qiskit terminology."""
282330
status_map = {
283-
"PENDING": "INITIALIZING",
284-
"RUNNING": "RUNNING",
285-
"STOPPED": "CANCELED",
286-
"SUCCEEDED": "DONE",
287-
"FAILED": "ERROR",
288-
"QUEUED": "QUEUED",
331+
Job.PENDING: "INITIALIZING",
332+
Job.RUNNING: "RUNNING",
333+
Job.STOPPED: "CANCELED",
334+
Job.SUCCEEDED: "DONE",
335+
Job.FAILED: "ERROR",
336+
Job.QUEUED: "QUEUED",
337+
Job.MAPPING: "RUNNING: MAPPING",
338+
Job.OPTIMIZING_HARDWARE: "RUNNING: OPTIMIZING_FOR_HARDWARE",
339+
Job.WAITING_QPU: "RUNNING: WAITING_FOR_QPU",
340+
Job.EXECUTING_QPU: "RUNNING: EXECUTING_QPU",
341+
Job.POST_PROCESSING: "RUNNING: POST_PROCESSING",
289342
}
290343

291344
try:

client/qiskit_serverless/visualizaiton/widget.py renamed to client/qiskit_serverless/visualization/widget.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from IPython.display import display, clear_output
3232
from ipywidgets import GridspecLayout, widgets, Layout
3333

34+
from qiskit_serverless.core.client import BaseClient
3435
from qiskit_serverless.exception import QiskitServerlessException
3536

3637
TABLE_STYLE = """
@@ -53,7 +54,7 @@
5354
class Widget: # pylint: disable=too-many-instance-attributes
5455
"""Widget for displaying information related to provider."""
5556

56-
def __init__(self, provider):
57+
def __init__(self, provider: BaseClient):
5758
"""Constructor for widget.
5859
5960
Args:
@@ -67,7 +68,7 @@ def __init__(self, provider):
6768

6869
self.job_offset = 0
6970
self.job_limit = 10
70-
self.jobs = self.provider.get_jobs()
71+
self.jobs = self.provider.jobs()
7172

7273
self.job_list_view = widgets.Output()
7374
with self.job_list_view:
@@ -79,7 +80,7 @@ def __init__(self, provider):
7980

8081
self.program_offset = 0
8182
self.program_limit = 10
82-
self.programs = self.provider.get_programs()
83+
self.programs = self.provider.functions()
8384

8485
self.program_list_view = widgets.Output()
8586
with self.program_list_view:
@@ -166,12 +167,12 @@ def render_job_pagination(self):
166167
def paginate(page_button):
167168
"""Handles pagination callback logic."""
168169
if page_button.tooltip == "prev":
169-
self.jobs = self.provider.get_jobs(
170+
self.jobs = self.provider.jobs(
170171
limit=self.job_limit, offset=self.job_offset - self.job_limit
171172
)
172173
self.job_offset = self.job_offset - self.job_limit
173174
elif page_button.tooltip == "next":
174-
self.jobs = self.provider.get_jobs(
175+
self.jobs = self.provider.jobs(
175176
limit=self.job_limit, offset=self.job_offset + self.job_limit
176177
)
177178
self.job_offset = self.job_offset + self.job_limit
@@ -219,13 +220,13 @@ def render_program_pagination(self):
219220
def paginate(page_button):
220221
"""Handles pagination callback logic."""
221222
if page_button.tooltip == "prev":
222-
self.programs = self.provider.get_programs(
223+
self.programs = self.provider.functions(
223224
limit=self.program_limit,
224225
offset=self.program_offset - self.program_limit,
225226
)
226227
self.job_offset = self.program_offset - self.job_limit
227228
elif page_button.tooltip == "next":
228-
self.jobs = self.provider.get_jobs(
229+
self.jobs = self.provider.jobs(
229230
limit=self.program_limit,
230231
offset=self.program_offset + self.program_limit,
231232
)

client/requirements.txt

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
ray[default,data]>=2.30.0, <2.35.0
1+
ray[default,data]>=2.30, <3
22
requests>=2.32.2, <3
33
importlib-metadata>=5.2.0, <9
4-
qiskit>=1.0.2
5-
qiskit-ibm-runtime>=0.29.0
4+
qiskit>=1.4, <3
5+
qiskit-ibm-runtime>=0.29.0, <1
66
# Make sure ray node and notebook node have the same version of cloudpickle
77
cloudpickle==2.2.1
88
tqdm>=4.66.3, <5

client/tests/core/test_job.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717
ENV_JOB_GATEWAY_TOKEN,
1818
ENV_ACCESS_TRIAL,
1919
)
20-
from qiskit_serverless.core.job import is_running_in_serverless, save_result, is_trial
20+
from qiskit_serverless.core.job import (
21+
is_running_in_serverless,
22+
save_result,
23+
is_trial,
24+
update_status,
25+
)
2126

2227

2328
# pylint: disable=redefined-outer-name
@@ -74,6 +79,14 @@ def test_save_result(self, job_env_variables):
7479
)
7580
assert result is True
7681

82+
@patch("requests.patch", Mock(return_value=ResponseMock()))
83+
def test_update_sub_status(self, job_env_variables):
84+
"""Tests update sub status."""
85+
_ = job_env_variables
86+
87+
result = update_status("MAPPING")
88+
assert result is True
89+
7790
@patch("requests.get", Mock(return_value=ResponseMock()))
7891
def test_filtered_logs(self):
7992
"""Tests job filtered log."""

gateway/api/access_policies/jobs.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
logger = logging.getLogger("gateway")
1212

1313

14-
class JobAccessPolocies:
14+
class JobAccessPolicies:
1515
"""
1616
The main objective of this class is to manage the access for the user
1717
to the Job entities.
@@ -89,3 +89,25 @@ def can_save_result(user: type[AbstractUser], job: Job) -> bool:
8989
job.author,
9090
)
9191
return has_access
92+
93+
@staticmethod
94+
def can_update_sub_status(user: type[AbstractUser], job: Job) -> bool:
95+
"""
96+
Checks if the user has permissions to update the substatus of a job:
97+
98+
Args:
99+
user: Django user from the request
100+
job: Job instance against to check the permission
101+
102+
Returns:
103+
bool: True or False in case the user has permissions
104+
"""
105+
106+
has_access = user.id == job.author.id
107+
if not has_access:
108+
logger.warning(
109+
"User [%s] has no access to update the sub_status of the job [%s].",
110+
user.username,
111+
job.id,
112+
)
113+
return has_access

gateway/api/decorators/__init__.py

Whitespace-only changes.
+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""
2+
======================================================
3+
Decorators (:mod:`gateway.api.decorators.trace_decorator`)
4+
======================================================
5+
6+
.. currentmodule:: gateway.api.decorators.trace_decorator
7+
8+
Gateway API decorators
9+
=============================
10+
11+
.. autosummary::
12+
:toctree: ../stubs/
13+
14+
trace_decorator_factory
15+
"""
16+
17+
from functools import wraps
18+
from types import FunctionType
19+
from typing import Union
20+
from opentelemetry import trace
21+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
22+
23+
24+
def trace_decorator_factory(traced_feature: str):
25+
"""Factory for generate decorators for classes or features."""
26+
27+
def generated_decorator(traced_function: Union[FunctionType, str]):
28+
"""
29+
The decorator wrapper to generate optional arguments
30+
if traced_function is string it will be used in the span,
31+
the function.__name__ attribute will be used otherwise
32+
"""
33+
34+
def decorator_trace(func: FunctionType):
35+
"""The decorator that python call"""
36+
37+
@wraps(func)
38+
def wrapper(*args, **kwargs):
39+
"""The wrapper"""
40+
tracer = trace.get_tracer("gateway.tracer")
41+
function_name = (
42+
traced_function
43+
if isinstance(traced_function, str)
44+
else func.__name__
45+
)
46+
request = args[0]
47+
ctx = TraceContextTextMapPropagator().extract(carrier=request.headers)
48+
with tracer.start_as_current_span(
49+
f"gateway.{traced_feature}.{function_name}", context=ctx
50+
):
51+
result = func(*args, **kwargs)
52+
return result
53+
54+
return wrapper
55+
56+
if callable(traced_function):
57+
return decorator_trace(traced_function)
58+
return decorator_trace
59+
60+
return generated_decorator

gateway/api/management/commands/free_resources.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def handle(self, *args, **options):
2424

2525
for compute_resource in compute_resources:
2626
alive_jobs = Job.objects.filter(
27-
status__in=Job.RUNNING_STATES, compute_resource=compute_resource
27+
status__in=Job.RUNNING_STATUSES, compute_resource=compute_resource
2828
)
2929

3030
# only kill cluster if not in local mode and no jobs are running there

0 commit comments

Comments
 (0)