Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -408,3 +408,6 @@ azure-quantum/build
!azure-quantum/requirements*.txt
[.][v]env/
version.py

# Local scratch/manual test file (not part of the package)
azure-quantum/test_targets.py
2 changes: 1 addition & 1 deletion azure-quantum/azure/quantum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .job.session import *
from .workspace import *

from ._client.models._enums import JobStatus, SessionStatus, SessionJobFailurePolicy, ItemType
from ._client.models._enums import JobStatus, SessionStatus, SessionJobFailurePolicy, ItemType, Priority

logger = logging.getLogger(__name__)
logger.info(f"version: {__version__}")
Expand Down
33 changes: 32 additions & 1 deletion azure-quantum/azure/quantum/job/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import time
import json

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, List, Optional, Union

from azure.quantum._client.models import JobDetails
from azure.quantum.job.job_failed_with_results_error import JobFailedWithResultsError
Expand All @@ -21,6 +21,7 @@

if TYPE_CHECKING:
from azure.quantum.workspace import Workspace
from azure.quantum._client.models import Priority


_log = logging.getLogger(__name__)
Expand Down Expand Up @@ -60,6 +61,36 @@ def delete(self):
"""Delete the given job."""
self.workspace.delete_job(self)

def update(
self,
*,
name: Optional[str] = None,
priority: Optional[Union[str, "Priority"]] = None,
tags: Optional[List[str]] = None,
) -> "Job":
"""Update the job's name, priority and/or tags after submission.

Only the arguments that are explicitly provided are updated;
any argument left as ``None`` is left unchanged on the service.

:param name: The new name of the job.
:param priority: The new priority of the job
(one of :class:`~azure.quantum.Priority`, ``"Standard"`` or ``"High"``).
:param tags: The new list of user-supplied tags associated with the job.
This replaces the existing tags.

:return: This job, with refreshed details.
:rtype: Job
"""
updated = self.workspace.update_job(
self,
name=name,
priority=priority,
tags=tags,
)
self.details = updated.details
return self

def has_completed(self) -> bool:
"""Check if the job has completed."""
return (
Expand Down
63 changes: 63 additions & 0 deletions azure-quantum/azure/quantum/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
from azure.quantum._client.models import (
BlobDetails,
JobStatus,
JobUpdateOptions,
Priority,
TargetStatus,
)
from azure.quantum import Job, Session
Expand Down Expand Up @@ -464,6 +466,67 @@ def cancel_job(self, job: Job) -> Job:
job.id)
return Job(self, details)

def update_job(
self,
job: Job,
*,
name: Optional[str] = None,
priority: Optional[Union[str, Priority]] = None,
tags: Optional[List[str]] = None,
) -> Job:
"""
Updates the name, priority and/or tags of a job after it has
been submitted.

Only the arguments that are explicitly provided are updated;
any argument left as ``None`` is left unchanged on the service.

:param job:
Job to update.

:param name:
The new name of the job.

:param priority:
The new priority of the job.
One of :class:`~azure.quantum.Priority` (``"Standard"`` or ``"High"``).

:param tags:
The new list of user-supplied tags associated with the job.
This replaces the existing tags.

:return: Azure Quantum Job with updated details.
:rtype: Job
"""
if name is None and priority is None and tags is None:
raise ValueError(
"At least one of 'name', 'priority' or 'tags' must be specified.")

client = self._get_jobs_client()
job_id = job.id

update_options = JobUpdateOptions()
if name is not None:
update_options.name = name
if priority is not None:
update_options.priority = priority
if tags is not None:
update_options.tags = tags

client.update(
self.subscription_id,
self.resource_group,
self.name,
job_id,
update_options)

details = client.get(
self.subscription_id,
self.resource_group,
self.name,
job_id)
return Job(self, details)

def delete_job(self, job: Job) -> None:
"""Deletes a job.
:param job:
Expand Down
35 changes: 35 additions & 0 deletions azure-quantum/tests/mock_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from azure.quantum._client.models import (
ItemDetails,
JobDetails,
JobUpdateOptions,
ProviderStatus,
SessionDetails,
TargetStatus,
Expand Down Expand Up @@ -193,6 +194,40 @@ def get(
return jd
raise KeyError(job_id)

def update(
self,
subscription_id: str,
resource_group_name: str,
workspace_name: str,
job_id: str,
resource,
):
def _get(field):
if isinstance(resource, dict):
return resource.get(field)
return getattr(resource, field, None)

for jd in self._store:
if jd.id == job_id:
name = _get("name")
priority = _get("priority")
tags = _get("tags")
if name is not None:
jd.name = name
if priority is not None:
jd.priority = priority
if tags is not None:
jd.tags = tags
# Match the generated client's contract: the update response is a
# JobUpdateOptions that includes the required job id.
return JobUpdateOptions({
"id": jd.id,
"name": jd.name,
"priority": jd.priority,
"tags": jd.tags,
})
raise KeyError(job_id)

# Cancel/delete for older API; mark job as cancelled
def delete(
self,
Expand Down
144 changes: 144 additions & 0 deletions azure-quantum/tests/test_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from unittest import mock
from azure.quantum.job.job import Job
from azure.quantum._client.models import JobDetails
from azure.quantum import Priority
from azure.quantum._constants import EnvironmentVariables, ConnectionConstants
from azure.core.credentials import AzureKeyCredential
from azure.core.pipeline.policies import AzureKeyCredentialPolicy
Expand Down Expand Up @@ -439,6 +440,149 @@ def test_job_delete_success():
ws.get_job(job_id)


def test_workspace_update_job_success():
ws = WorkspaceMock(
subscription_id=SUBSCRIPTION_ID,
resource_group=RESOURCE_GROUP,
name=WORKSPACE,
)

job_id = "test-update-success"
details = JobDetails(
id=job_id,
name=f"job-{job_id}",
container_uri="https://example.com/container",
input_data_format="microsoft.resource-estimate.v2",
provider_id="ionq",
target="ionq.simulator",
status="Executing",
priority="Standard",
tags=["old-tag"],
)
ws._client.services.jobs._store.append(details)

job = Job(ws, details)
result = ws.update_job(
job,
name="new-name",
priority=Priority.HIGH,
tags=["tag-a", "tag-b"],
)

assert result.id == job_id
assert result.details.name == "new-name"
assert result.details.priority == "High"
assert result.details.tags == ["tag-a", "tag-b"]


def test_workspace_update_job_partial_leaves_other_fields_unchanged():
ws = WorkspaceMock(
subscription_id=SUBSCRIPTION_ID,
resource_group=RESOURCE_GROUP,
name=WORKSPACE,
)

job_id = "test-update-partial"
details = JobDetails(
id=job_id,
name="original-name",
container_uri="https://example.com/container",
input_data_format="microsoft.resource-estimate.v2",
provider_id="ionq",
target="ionq.simulator",
status="Executing",
priority="Standard",
tags=["keep-me"],
)
ws._client.services.jobs._store.append(details)

job = Job(ws, details)
result = ws.update_job(job, name="renamed-only")

assert result.details.name == "renamed-only"
# untouched fields remain unchanged
assert result.details.priority == "Standard"
assert result.details.tags == ["keep-me"]


def test_job_update_success():
ws = WorkspaceMock(
subscription_id=SUBSCRIPTION_ID,
resource_group=RESOURCE_GROUP,
name=WORKSPACE,
)

job_id = "test-job-update-success"
details = JobDetails(
id=job_id,
name=f"job-{job_id}",
container_uri="https://example.com/container",
input_data_format="microsoft.resource-estimate.v2",
provider_id="ionq",
target="ionq.simulator",
status="Executing",
priority="Standard",
tags=["old"],
)
ws._client.services.jobs._store.append(details)

job = Job(ws, details)
returned = job.update(name="updated", priority="High", tags=["new"])

# update mutates the job in place and returns itself
assert returned is job
assert job.details.name == "updated"
assert job.details.priority == "High"
assert job.details.tags == ["new"]


def test_workspace_update_job_not_found_raises():
ws = WorkspaceMock(
subscription_id=SUBSCRIPTION_ID,
resource_group=RESOURCE_GROUP,
name=WORKSPACE,
)

details = JobDetails(
id="missing-job",
name="missing-job",
container_uri="https://example.com/container",
input_data_format="microsoft.resource-estimate.v2",
provider_id="ionq",
target="ionq.simulator",
status="Executing",
)
job = Job(ws, details)

with pytest.raises(KeyError):
ws.update_job(job, name="nope")


def test_workspace_update_job_requires_at_least_one_field():
ws = WorkspaceMock(
subscription_id=SUBSCRIPTION_ID,
resource_group=RESOURCE_GROUP,
name=WORKSPACE,
)

job_id = "test-update-no-fields"
details = JobDetails(
id=job_id,
name=job_id,
container_uri="https://example.com/container",
input_data_format="microsoft.resource-estimate.v2",
provider_id="ionq",
target="ionq.simulator",
status="Executing",
)
ws._client.services.jobs._store.append(details)

job = Job(ws, details)

with pytest.raises(ValueError):
ws.update_job(job)


def test_workspace_user_agent_appid():
app_id = "MyEnvVarAppId"
user_agent = "MyUserAgent"
Expand Down
Loading