Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fd10c2b
[Quantum] Add 'az quantum suite-offer list' command
v-elegacheva Aug 28, 2026
25bb4d9
[Quantum] Bump extension version to 1.0.0b25
v-elegacheva Aug 28, 2026
a3640fb
[Quantum] Address review: use 'View' in suite-offer group help
v-elegacheva Aug 31, 2026
953efb6
Merge remote-tracking branch 'origin/main' into ekat/quantum-suite-of…
v-elegacheva Aug 31, 2026
d9da06e
[Quantum] Address review: use 'Provider ID' column for consistency
v-elegacheva Aug 31, 2026
0d79e29
[Quantum] Add 'az quantum suite-offer quotas' command
v-elegacheva Sep 1, 2026
eaeb8f7
[Quantum] Address review: target-quota rows with nested allocation/us…
v-elegacheva Sep 1, 2026
d5193cd
[Quantum] Review polish: v2 canary endpoint, quotas live test, doc fixes
v-elegacheva Sep 1, 2026
90a027f
[Quantum] Add 'az quantum suite-offer target list' command
v-elegacheva Sep 2, 2026
d8766b5
[Quantum] Return single ProviderStatus for suite-offer target status
v-elegacheva Sep 2, 2026
0cefd7d
[Quantum] Update 'az quantum workspace quotas' to return v2 target qu…
v-elegacheva Sep 2, 2026
febfd9c
[Quantum] Address review: use 'Provider ID' column in workspace quota…
v-elegacheva Sep 2, 2026
1f6bf17
[Quantum] Address review: lift quota scope literal into a named constant
v-elegacheva Sep 2, 2026
d89e4d7
[Quantum] Address review: show quota allocation/usage in hours in tab…
v-elegacheva Sep 2, 2026
357ffec
Merge upstream/main into ekat/quantum-suite-offer-quotas
v-elegacheva Sep 3, 2026
37148ab
[Quantum] Preserve workspace quota response compatibility
v-elegacheva Sep 4, 2026
cd1f80d
[Quantum] Validate V2 workspace target quotas
v-elegacheva Sep 8, 2026
cd35c76
[Quantum] Remove provider from suite target table
v-elegacheva Sep 8, 2026
aea1be2
[Quantum] Add priority queue times to suite targets
v-elegacheva Sep 8, 2026
abd0c26
[Quantum] Sync quota usage model naming
v-elegacheva Sep 9, 2026
1627d73
[Quantum] Show zero for missing suite quota usage
v-elegacheva Sep 9, 2026
8ee3abe
[Quantum] Align suite quota table values
v-elegacheva Sep 9, 2026
ce2a464
[Quantum] Route workspace quotas by workspace kind
v-elegacheva Sep 10, 2026
f8dcbfb
[Quantum] Address workspace quota review feedback
v-elegacheva Sep 10, 2026
2fcca1f
[Quantum] Handle array suite quota usages
v-elegacheva Sep 10, 2026
2de9a90
[Quantum] Address latest suite offer review
v-elegacheva Sep 11, 2026
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
12 changes: 12 additions & 0 deletions src/quantum/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
Release History
===============

1.0.0b27
++++++++++++++
* Added the ``az quantum suite-offer target list`` command to list the targets, availability, and overall, Standard, and High average queue times available through a suite offer, without requiring a workspace.
* Removed the redundant provider column from the ``az quantum suite-offer target list`` table output.
* Updated ``az quantum suite-offer quotas`` to return ``0`` for missing Standard and High usage values.
* Updated the ``az quantum workspace quotas`` command to include v2 target quota allocations and usages while preserving the existing response format for v1 providers.
* Added always-on validation for V2 workspace target quota allocations on create and update, allowing requested Standard and High values between current workspace usage and suite target allocation, inclusive.

1.0.0b26
++++++++++++++
* Added the ``az quantum suite-offer quotas`` command to view quota allocations merged with their consumed usages for a suite offer in the subscription.

1.0.0b25
++++++++++++++
* Added the ``az quantum suite-offer list`` command to list the suite offers available to the subscription, including provider, location, and subscription-level quota allocations.
Expand Down
14 changes: 14 additions & 0 deletions src/quantum/azext_quantum/_client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ def base_url(location):
return f"https://{normalized_location}.quantum.azure.com/"


def base_url_v2(location):
if 'AZURE_QUANTUM_BASEURL_V2' in os.environ:
return os.environ['AZURE_QUANTUM_BASEURL_V2']
normalized_location = normalize_location(location)
if is_env('dogfood'):
return f"https://{normalized_location}-v2.quantum-test.azure.com/"
return f"https://{normalized_location}-v2.quantum.azure.com/"


def _get_data_credentials(cli_ctx, subscription_id=None):
from azure.cli.core._profile import Profile
profile = Profile(cli_ctx=cli_ctx)
Expand Down Expand Up @@ -85,6 +94,11 @@ def cf_quotas(cli_ctx, subscription: str, resource_group: str, ws_name: str, end
return cf_quantum(cli_ctx, subscription, resource_group, ws_name, endpoint).services.quotas


def cf_suite_offers_data_plane(cli_ctx, subscription: str, location: str):
endpoint = base_url_v2(location)
return cf_quantum(cli_ctx, subscription, None, None, endpoint).services.suite_offers


# Helper clients

def cf_vm_image_term(cli_ctx):
Expand Down
57 changes: 53 additions & 4 deletions src/quantum/azext_quantum/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@

helps['quantum suite-offer list'] = """
type: command
short-summary: List the Azure Quantum suite offers available to the current subscription, including provider, location, and subscription-level quota allocations.
short-summary: List the Azure Quantum suite offers available to the current subscription, including provider ID, name, company, and location.
examples:
- name: List all suite offers available to the current subscription.
text: |-
Expand All @@ -233,6 +233,44 @@
az quantum suite-offer list --query "[].{provider:properties.providerId, location:properties.location}" -o table
"""

helps['quantum suite-offer quotas'] = """
type: command
short-summary: View quota allocations and their consumed usages for a suite offer in the current subscription.
long-summary: |
Returns the v2 quota allocations (limits) for each target of the suite offer together
with the consumed usages. Each entry reports the allocated and used standard and high priority
minutes. Missing usage values are returned as 0.
examples:
- name: View the quota usages for a suite offer.
text: |-
az quantum suite-offer quotas --provider-id MyProvider -o table
- name: View the raw quota usage details for a suite offer.
text: |-
az quantum suite-offer quotas -p MyProvider
"""

helps['quantum suite-offer target'] = """
type: group
short-summary: List targets available through an Azure Quantum suite offer.
"""

helps['quantum suite-offer target list'] = """
type: command
short-summary: List the targets and their status available through a suite offer, without requiring a workspace.
long-summary: |
Returns each target exposed by the suite offer together with its current
availability and overall average queue time. Standard- and High-priority average queue times
are also returned when supplied by the provider. Data is resolved directly from the data plane
without requiring an Azure Quantum workspace.
examples:
- name: List the targets available in a suite offer.
text: |-
az quantum suite-offer target list --provider-id MyProvider -o table
- name: List the raw target status details for a suite offer.
text: |-
az quantum suite-offer target list -p MyProvider
"""

helps['quantum offerings'] = """
type: group
short-summary: Manage provider offerings for Azure Quantum.
Expand Down Expand Up @@ -326,6 +364,9 @@
helps['quantum workspace create'] = """
type: command
short-summary: Create a new Azure Quantum workspace.
long-summary: >-
Target quota values are absolute. For V2 workspaces, each requested Standard and High allocation is validated
against the provider's suite target allocation before the workspace is created.
examples:
- name: Create a new Azure Quantum workspace with the providers that offer free credit.
text: |-
Expand Down Expand Up @@ -369,11 +410,15 @@

helps['quantum workspace quotas'] = """
type: command
short-summary: List the quotas for the given (or current) Azure Quantum workspace.
short-summary: List quota allocations and consumed usages for an Azure Quantum workspace.
long-summary: |
Preserves the existing quota dimension response for v1 providers. For v2 providers, returns
separate StandardMinutesLifetime and HighMinutesLifetime rows for each target, with targetId,
limit, and utilization reported in minutes. Missing allocation or usage values are returned as 0.
examples:
- name: List the quota information of a specified Azure Quantum workspace. If a default workspace has been set, the -g and -w parameters are not required.
- name: View quota allocations and usages for the given (or current) workspace. If a default workspace has been set, the -g and -w parameters are not required.
text: |-
az quantum workspace quotas -g MyResourceGroup -w MyWorkspace
az quantum workspace quotas -g MyResourceGroup -w MyWorkspace -o table
"""

helps['quantum workspace set'] = """
Expand All @@ -400,6 +445,10 @@
helps['quantum workspace update'] = """
type: command
short-summary: Update the given (or current) Azure Quantum workspace.
long-summary: >-
Target quota values are absolute. Each requested Standard and High allocation is validated against the current
workspace target usage and provider's suite target allocation, with equality allowed at both boundaries.
Priority values omitted from an existing target allocation are preserved and validated.
examples:
- name: Enable a provided Azure Quantum workspace api keys.
text: |-
Expand Down
8 changes: 7 additions & 1 deletion src/quantum/azext_quantum/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def load_arguments(self, _): # pylint: disable=too-many-locals
entry_point_type = CLIArgumentType(help='The entry point for the QIR program or circuit. Required for some provider QIR jobs.')
skip_autoadd_type = CLIArgumentType(help='If specified, the plans that offer free credits will not automatically be added.')
workspace_kind_type = CLIArgumentType(options_list=['--workspace-kind'], help='The kind of the workspace to create.', choices=['V1', 'V2'])
quota_type = CLIArgumentType(options_list=['--quota'], help='Target quota allocation as provider-id, target-id, standard-minutes-lifetime, and optional high-minutes-lifetime key=value pairs, a JSON object or array, or `@{file}` with JSON content. standard-minutes-lifetime is required for a new allocation. camelCase keys (providerId, targetId, ...) are also accepted. Repeat --quota once per target.', action=QuotaAction, nargs='+')
quota_type = CLIArgumentType(options_list=['--quota'], help='Target quota allocation for a V2 workspace as provider-id, target-id, standard-minutes-lifetime, and optional high-minutes-lifetime key=value pairs, a JSON object or array, or `@{file}` with JSON content. Use --workspace-kind V2 when creating a workspace. Values are absolute and cannot exceed the suite target allocation or, when updating, be below current workspace usage. standard-minutes-lifetime is required for a new allocation. camelCase keys (providerId, targetId, ...) are also accepted. Repeat --quota once per target.', action=QuotaAction, nargs='+')
key_type = CLIArgumentType(options_list=['--key-type'], help='The api keys to be regenerated, should be Primary and/or Secondary.')
enable_key_type = CLIArgumentType(options_list=['--enable-api-key'], help='Enable or disable API key authentication.')
job_type_type = CLIArgumentType(options_list=['--job-type'], help='Job type to be listed, example "QuantumComputing".')
Expand Down Expand Up @@ -316,3 +316,9 @@ def load_arguments(self, _): # pylint: disable=too-many-locals
c.argument('workspace_name', workspace_name_type)
c.argument('enable_key', enable_key_type)
c.argument('quota', quota_type)

with self.argument_context('quantum suite-offer quotas') as c:
c.argument('provider_id', provider_id_type, required=True)

with self.argument_context('quantum suite-offer target list') as c:
c.argument('provider_id', provider_id_type, required=True)
69 changes: 68 additions & 1 deletion src/quantum/azext_quantum/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ def one(provider, target):
]


def transform_suite_offer_targets(providers):
return [
OrderedDict([
('Target-id', target['id']),
('Current Availability', target['currentAvailability']),
('Average Queue Time (seconds)', target['averageQueueTime']),
('Average Standard Queue Time (seconds)', target.get('averageQueueTimeStandardPriority')),
('Average High Queue Time (seconds)', target.get('averageQueueTimeHighPriority'))
])
for provider in providers
for target in provider['targets']
]


def transform_job(result):
transformed_result = OrderedDict([
('Name', result['name']),
Expand Down Expand Up @@ -103,6 +117,55 @@ def one(offer):
return [one(offer) for offer in suite_offers]


def _quota_hours(minutes):
"""Convert lifetime quota minutes to hours (2 dp)."""
hours = 0 if minutes is None else minutes / 60
return f"{hours:.2f}"


def transform_suite_offer_quotas(quotas):
def one(quota):
allocation = quota.get('allocation') or {}
usage = quota.get('usage') or {}

def cell(source, key):
return _quota_hours(source.get(key))

return OrderedDict([
('Target', quota.get('targetId', '')),
('Std Allocated (hrs)', cell(allocation, 'standardMinutesLifetime')),
('Std Used (hrs)', cell(usage, 'standardMinutesLifetime')),
('High Allocated (hrs)', cell(allocation, 'highMinutesLifetime')),
('High Used (hrs)', cell(usage, 'highMinutesLifetime'))
])

return [one(quota) for quota in quotas]


def transform_workspace_quotas(quotas):
def one(quota):
is_target_quota = quota.get('targetId') is not None

def value(key):
result = quota.get(key, 0)
if is_target_quota and isinstance(result, float):
return round(result, 2)
return result

return OrderedDict([
('Scope', quota.get('scope', '')),
('Provider ID', quota.get('providerId', '')),
('Target', quota.get('targetId', '')),
('Dimension', quota.get('dimension', '')),
('Limit', value('limit')),
('Utilization', value('utilization')),
('Holds', value('holds')),
('Period', quota.get('period', '')),
])

return [one(quota) for quota in quotas]


def transform_output(results):
def one(key, value):
repeat = round(20 * value)
Expand Down Expand Up @@ -167,7 +230,7 @@ def load_command_table(self, _):
w.show_command('show', validator=validate_workspace_info)
w.command('set', 'set', validator=validate_workspace_info)
w.command('clear', 'clear')
w.command('quotas', 'quotas', validator=validate_workspace_info)
w.command('quotas', 'quotas', validator=validate_workspace_info, table_transformer=transform_workspace_quotas)
w.command('keys list', 'list_keys')
w.command('keys regenerate', 'regenerate_keys')
w.command('update', 'update')
Expand Down Expand Up @@ -206,3 +269,7 @@ def load_command_table(self, _):

with self.command_group('quantum suite-offer', suite_offers_ops) as s:
s.command('list', 'list_suite_offers', table_transformer=transform_suite_offers)
s.command('quotas', 'suite_offer_quotas', table_transformer=transform_suite_offer_quotas)

with self.command_group('quantum suite-offer target', suite_offers_ops) as st:
st.command('list', 'suite_offer_targets', table_transformer=transform_suite_offer_targets)
125 changes: 124 additions & 1 deletion src/quantum/azext_quantum/operations/suite_offers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,21 @@

# pylint: disable=line-too-long

from .._client_factory import cf_suite_offers
from collections import OrderedDict

from azure.cli.core.azclierror import InvalidArgumentValueError, ResourceNotFoundError
from azure.cli.core.commands.client_factory import get_subscription_id
from azure.core.exceptions import ResourceNotFoundError as AzureResourceNotFoundError

from .._client_factory import cf_suite_offers, cf_suite_offers_data_plane

# Scope used for target quota rows returned by this command.
_SUITE_OFFER_QUOTA_SCOPE = "SubscriptionTarget"


def _quota_usage_value(usage, field):
value = usage.get(field) if usage is not None else None
return value if value is not None else 0


def list_suite_offers(cmd):
Expand All @@ -14,3 +28,112 @@ def list_suite_offers(cmd):
"""
client = cf_suite_offers(cmd.cli_ctx)
return client.list_by_subscription()


def suite_offer_quotas(cmd, provider_id):
"""
Return the v2 quota allocations, merged with their consumed usages, for a suite offer
in the current subscription.
"""
subscription_id = get_subscription_id(cmd.cli_ctx)

# 1. Control-plane: locate the suite offer for the requested provider.
offers = cf_suite_offers(cmd.cli_ctx).list_by_subscription()
offer = next(
(o for o in offers
if o.properties is not None
and o.properties.provider_id is not None
and o.properties.provider_id.lower() == provider_id.lower()),
None,
)
if offer is None:
raise InvalidArgumentValueError(
f"No suite offer was found for provider '{provider_id}' in subscription '{subscription_id}'."
)

# 2. Data-plane (v2): fetch the consumed quota usages for that provider.
client = cf_suite_offers_data_plane(cmd.cli_ctx, subscription_id, offer.properties.location)
try:
usages = client.list_quota_usages(subscription_id, provider_id)
except AzureResourceNotFoundError as ex:
raise ResourceNotFoundError(
f"No quota usages were found for provider '{provider_id}'."
) from ex

# 3. Merge allocations (limits) with usages (consumed).
return _merge_suite_offer_quotas(offer, usages, provider_id)


def suite_offer_targets(cmd, provider_id):
"""
List the targets and their status available through a suite offer,
without requiring an Azure Quantum workspace.
"""
subscription_id = get_subscription_id(cmd.cli_ctx)

# 1. Control-plane: locate the suite offer to resolve its region.
offers = cf_suite_offers(cmd.cli_ctx).list_by_subscription()
offer = next(
(o for o in offers
if o.properties is not None
and o.properties.provider_id is not None
and o.properties.provider_id.lower() == provider_id.lower()),
None,
)
if offer is None:
raise InvalidArgumentValueError(
f"No suite offer was found for provider '{provider_id}' in subscription '{subscription_id}'."
)

# 2. Data-plane (v2): fetch the provider and target status.
client = cf_suite_offers_data_plane(cmd.cli_ctx, subscription_id, offer.properties.location)
try:
status = client.get_provider_status(subscription_id, provider_id)
except AzureResourceNotFoundError as ex:
raise ResourceNotFoundError(
f"No target status was found for provider '{provider_id}'."
) from ex

# The endpoint returns a single provider; wrap it so the table transformer shared with
# 'az quantum target list' can iterate provider rows.
return [status]


def _minutes(standard, high):
"""Build a {standardMinutesLifetime, highMinutesLifetime} block."""
return OrderedDict([
("standardMinutesLifetime", standard),
("highMinutesLifetime", high),
])


def _merge_suite_offer_quotas(offer, usages, provider_id):
"""
Build one row per target quota allocation, attaching its matching data-plane usage.
Suite offer quotas are reported at the SubscriptionTarget scope only.
"""
# Data-plane usages keyed by target id.
usage_by_target = {
usage.target_id: usage for usage in (usages or []) if usage.target_id is not None
}

rows = []
for target_quota in offer.properties.target_quotas or []:
usage = usage_by_target.get(target_quota.target_id)
usage_values = usage.usage if usage is not None else None

row = OrderedDict()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you consider creating a structure for return type to improve readability? and probably we can skip remaining and stick to initial structure like this:
{
"providerId" : "atom-dev",
"scope" : "SubscriptionTarget",
"targetId" : "msft.sim.ac1000.physical",
"allocation" : {
"standardMinutesLifetime" : 600,
"highMinutesLifetime" : 60
},
"usage" : {
"standardMinutesLifetime" : 120,
"highMinutesLifetime" : 12
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I restructured to match your proposed shape. Each row is now:
{
"providerId": "...",
"scope": "SubcriptionTarget",
"targetId": "...",
"allocation": { "standardMinutesLifetime": 0, "highMinutesLifetime": 0 },
"usage": {"standardMinutesLifetime": 0, "highMinutesLifetime": 0}
}

remaining and lastModifiedTime are dropped. I added a small _minutes() helper to build the nested blocks. The table transformer and help text were also updated to match

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, but it still dict, do you think there will be benefit of creating a type with all of these fields and have dot access to the fields?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return value of a CLI custom command is serialized straight to the user-facing output, and the CLI's todict serializer uses each object's raw attribute names (vars()). So a plain class/dataclass would emit snake_case keys (provider_id, standard_minutes_lifetime) instead of the providerId / standardMinutesLifetime contract, and a namedTuple serializes as a JSON array. OrderedDict gives exact control over the camelCase keys and ordering that define this command's output, and it is consistent with the rest of the quantum extension (all handlers/ transformers return dicts or SDK models). The dot-access benefit would only apply inside this ~ 15 line builder, which _minutes() already simplifies. If you'd like the shape documented in code, i can switch the row to a TypedDict. That gives type-checking + editor hints and still serializes correctly as a dict. A full dataclass would need custom camelCase serialization to avoid changing the output. Which way would you prefer?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, let's keep using dict

row["providerId"] = provider_id
row["scope"] = _SUITE_OFFER_QUOTA_SCOPE
row["targetId"] = target_quota.target_id
row["allocation"] = _minutes(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in UI we show allocation and usage in hours? align with it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The underlying ARM / DP fields are standardMinutesLifetime / highMinutesLifetime. The values are minutes by contract and the key names literally say "Minutes". The CLI mirrors the service payload, so converting to hours would make the value diagree with its own filed name and with ARM. My instinct was keeping the JSON in minutes (true to contract) and if it helps parity with the UI, adding hours to the table view only. If you would prefer to fully match the UI, we would need new hour-names fields (like standardHoursLifetime) rather than silently dividing the existing ones. Please let me know how you'd like to proceed on this one :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

table view only is fine, thanks

target_quota.standard_minutes_lifetime,
target_quota.high_minutes_lifetime,
)
row["usage"] = _minutes(
_quota_usage_value(usage_values, "standardMinutesLifetime"),
_quota_usage_value(usage_values, "highMinutesLifetime"),
)
rows.append(row)

return rows
Loading
Loading