Skip to content

Commit ef987c7

Browse files
committed
Add update job operation to support updating name, priority and tags
Adds Workspace.update_job and Job.update to update a submitted job's name, priority and/or tags via the JobUpdateOptions JSON Merge Patch operation. Only provided fields are sent; requires at least one field. Exposes Priority publicly and adds unit tests plus mock client support.
1 parent faae7c8 commit ef987c7

6 files changed

Lines changed: 270 additions & 2 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,3 +408,6 @@ azure-quantum/build
408408
!azure-quantum/requirements*.txt
409409
[.][v]env/
410410
version.py
411+
412+
# Local scratch/manual test file (not part of the package)
413+
azure-quantum/test_targets.py

azure-quantum/azure/quantum/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .job.session import *
1414
from .workspace import *
1515

16-
from ._client.models._enums import JobStatus, SessionStatus, SessionJobFailurePolicy, ItemType
16+
from ._client.models._enums import JobStatus, SessionStatus, SessionJobFailurePolicy, ItemType, Priority
1717

1818
logger = logging.getLogger(__name__)
1919
logger.info(f"version: {__version__}")

azure-quantum/azure/quantum/job/job.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import time
99
import json
1010

11-
from typing import TYPE_CHECKING
11+
from typing import TYPE_CHECKING, List, Optional, Union
1212

1313
from azure.quantum._client.models import JobDetails
1414
from azure.quantum.job.job_failed_with_results_error import JobFailedWithResultsError
@@ -21,6 +21,7 @@
2121

2222
if TYPE_CHECKING:
2323
from azure.quantum.workspace import Workspace
24+
from azure.quantum._client.models import Priority
2425

2526

2627
_log = logging.getLogger(__name__)
@@ -56,6 +57,36 @@ def refresh(self):
5657
"""Refreshes the Job's details by querying the workspace."""
5758
self.details = self.workspace.get_job(self.id).details
5859

60+
def update(
61+
self,
62+
*,
63+
name: Optional[str] = None,
64+
priority: Optional[Union[str, "Priority"]] = None,
65+
tags: Optional[List[str]] = None,
66+
) -> "Job":
67+
"""Update the job's name, priority and/or tags after submission.
68+
69+
Only the arguments that are explicitly provided are updated;
70+
any argument left as ``None`` is left unchanged on the service.
71+
72+
:param name: The new name of the job.
73+
:param priority: The new priority of the job
74+
(one of :class:`~azure.quantum.Priority`, ``"Standard"`` or ``"High"``).
75+
:param tags: The new list of user-supplied tags associated with the job.
76+
This replaces the existing tags.
77+
78+
:return: This job, with refreshed details.
79+
:rtype: Job
80+
"""
81+
updated = self.workspace.update_job(
82+
self,
83+
name=name,
84+
priority=priority,
85+
tags=tags,
86+
)
87+
self.details = updated.details
88+
return self
89+
5990
def has_completed(self) -> bool:
6091
"""Check if the job has completed."""
6192
return (

azure-quantum/azure/quantum/workspace.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
from azure.quantum._client.models import (
3737
BlobDetails,
3838
JobStatus,
39+
JobUpdateOptions,
40+
Priority,
3941
TargetStatus,
4042
)
4143
from azure.quantum import Job, Session
@@ -464,6 +466,66 @@ def cancel_job(self, job: Job) -> Job:
464466
job.id)
465467
return Job(self, details)
466468

469+
def update_job(
470+
self,
471+
job: Job,
472+
*,
473+
name: Optional[str] = None,
474+
priority: Optional[Union[str, Priority]] = None,
475+
tags: Optional[List[str]] = None,
476+
) -> Job:
477+
"""
478+
Updates the name, priority and/or tags of a job after it has
479+
been submitted.
480+
481+
Only the arguments that are explicitly provided are updated;
482+
any argument left as ``None`` is left unchanged on the service.
483+
484+
:param job:
485+
Job to update.
486+
487+
:param name:
488+
The new name of the job.
489+
490+
:param priority:
491+
The new priority of the job.
492+
One of :class:`~azure.quantum.Priority` (``\"Standard\"`` or ``\"High\"``).
493+
494+
:param tags:
495+
The new list of user-supplied tags associated with the job.
496+
This replaces the existing tags.
497+
498+
:return: Azure Quantum Job with updated details.
499+
:rtype: Job
500+
"""
501+
if name is None and priority is None and tags is None:
502+
raise ValueError(
503+
"At least one of 'name', 'priority' or 'tags' must be specified.")
504+
505+
client = self._get_jobs_client()
506+
507+
update_options = JobUpdateOptions()
508+
if name is not None:
509+
update_options.name = name
510+
if priority is not None:
511+
update_options.priority = priority
512+
if tags is not None:
513+
update_options.tags = tags
514+
515+
client.update(
516+
self.subscription_id,
517+
self.resource_group,
518+
self.name,
519+
job.details.id,
520+
update_options)
521+
522+
details = client.get(
523+
self.subscription_id,
524+
self.resource_group,
525+
self.name,
526+
job.id)
527+
return Job(self, details)
528+
467529
def get_job(self, job_id: str) -> Job:
468530
"""
469531
Returns the job corresponding to the given id.

azure-quantum/tests/mock_client.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,33 @@ def get(
193193
return jd
194194
raise KeyError(job_id)
195195

196+
def update(
197+
self,
198+
subscription_id: str,
199+
resource_group_name: str,
200+
workspace_name: str,
201+
job_id: str,
202+
resource,
203+
):
204+
def _get(field):
205+
if isinstance(resource, dict):
206+
return resource.get(field)
207+
return getattr(resource, field, None)
208+
209+
for jd in self._store:
210+
if jd.id == job_id:
211+
name = _get("name")
212+
priority = _get("priority")
213+
tags = _get("tags")
214+
if name is not None:
215+
jd.name = name
216+
if priority is not None:
217+
jd.priority = priority
218+
if tags is not None:
219+
jd.tags = tags
220+
return resource
221+
raise KeyError(job_id)
222+
196223
# Cancel/delete for older API; mark job as cancelled
197224
def delete(
198225
self,

azure-quantum/tests/test_workspace.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
##
55

66
import os
7+
import pytest
78
from unittest import mock
89
from azure.quantum.job.job import Job
910
from azure.quantum._client.models import JobDetails
11+
from azure.quantum._client.models import Priority
1012
from azure.quantum._constants import EnvironmentVariables, ConnectionConstants
1113
from azure.core.credentials import AzureKeyCredential
1214
from azure.core.pipeline.policies import AzureKeyCredentialPolicy
@@ -385,6 +387,149 @@ def test_workspace_cancel_job_success():
385387
assert result.id == job_id
386388

387389

390+
def test_workspace_update_job_success():
391+
ws = WorkspaceMock(
392+
subscription_id=SUBSCRIPTION_ID,
393+
resource_group=RESOURCE_GROUP,
394+
name=WORKSPACE,
395+
)
396+
397+
job_id = "test-update-success"
398+
details = JobDetails(
399+
id=job_id,
400+
name=f"job-{job_id}",
401+
container_uri="https://example.com/container",
402+
input_data_format="microsoft.resource-estimate.v2",
403+
provider_id="ionq",
404+
target="ionq.simulator",
405+
status="Executing",
406+
priority="Standard",
407+
tags=["old-tag"],
408+
)
409+
ws._client.services.jobs._store.append(details)
410+
411+
job = Job(ws, details)
412+
result = ws.update_job(
413+
job,
414+
name="new-name",
415+
priority=Priority.HIGH,
416+
tags=["tag-a", "tag-b"],
417+
)
418+
419+
assert result.id == job_id
420+
assert result.details.name == "new-name"
421+
assert result.details.priority == "High"
422+
assert result.details.tags == ["tag-a", "tag-b"]
423+
424+
425+
def test_workspace_update_job_partial_leaves_other_fields_unchanged():
426+
ws = WorkspaceMock(
427+
subscription_id=SUBSCRIPTION_ID,
428+
resource_group=RESOURCE_GROUP,
429+
name=WORKSPACE,
430+
)
431+
432+
job_id = "test-update-partial"
433+
details = JobDetails(
434+
id=job_id,
435+
name="original-name",
436+
container_uri="https://example.com/container",
437+
input_data_format="microsoft.resource-estimate.v2",
438+
provider_id="ionq",
439+
target="ionq.simulator",
440+
status="Executing",
441+
priority="Standard",
442+
tags=["keep-me"],
443+
)
444+
ws._client.services.jobs._store.append(details)
445+
446+
job = Job(ws, details)
447+
result = ws.update_job(job, name="renamed-only")
448+
449+
assert result.details.name == "renamed-only"
450+
# untouched fields remain unchanged
451+
assert result.details.priority == "Standard"
452+
assert result.details.tags == ["keep-me"]
453+
454+
455+
def test_job_update_success():
456+
ws = WorkspaceMock(
457+
subscription_id=SUBSCRIPTION_ID,
458+
resource_group=RESOURCE_GROUP,
459+
name=WORKSPACE,
460+
)
461+
462+
job_id = "test-job-update-success"
463+
details = JobDetails(
464+
id=job_id,
465+
name=f"job-{job_id}",
466+
container_uri="https://example.com/container",
467+
input_data_format="microsoft.resource-estimate.v2",
468+
provider_id="ionq",
469+
target="ionq.simulator",
470+
status="Executing",
471+
priority="Standard",
472+
tags=["old"],
473+
)
474+
ws._client.services.jobs._store.append(details)
475+
476+
job = Job(ws, details)
477+
returned = job.update(name="updated", priority="High", tags=["new"])
478+
479+
# update mutates the job in place and returns itself
480+
assert returned is job
481+
assert job.details.name == "updated"
482+
assert job.details.priority == "High"
483+
assert job.details.tags == ["new"]
484+
485+
486+
def test_workspace_update_job_not_found_raises():
487+
ws = WorkspaceMock(
488+
subscription_id=SUBSCRIPTION_ID,
489+
resource_group=RESOURCE_GROUP,
490+
name=WORKSPACE,
491+
)
492+
493+
details = JobDetails(
494+
id="missing-job",
495+
name="missing-job",
496+
container_uri="https://example.com/container",
497+
input_data_format="microsoft.resource-estimate.v2",
498+
provider_id="ionq",
499+
target="ionq.simulator",
500+
status="Executing",
501+
)
502+
job = Job(ws, details)
503+
504+
with pytest.raises(KeyError):
505+
ws.update_job(job, name="nope")
506+
507+
508+
def test_workspace_update_job_requires_at_least_one_field():
509+
ws = WorkspaceMock(
510+
subscription_id=SUBSCRIPTION_ID,
511+
resource_group=RESOURCE_GROUP,
512+
name=WORKSPACE,
513+
)
514+
515+
job_id = "test-update-no-fields"
516+
details = JobDetails(
517+
id=job_id,
518+
name=job_id,
519+
container_uri="https://example.com/container",
520+
input_data_format="microsoft.resource-estimate.v2",
521+
provider_id="ionq",
522+
target="ionq.simulator",
523+
status="Executing",
524+
)
525+
ws._client.services.jobs._store.append(details)
526+
527+
job = Job(ws, details)
528+
529+
with pytest.raises(ValueError):
530+
ws.update_job(job)
531+
532+
388533
def test_workspace_user_agent_appid():
389534
app_id = "MyEnvVarAppId"
390535
user_agent = "MyUserAgent"

0 commit comments

Comments
 (0)