Skip to content

Quantum: Add 'az quantum suite-offer quotas' command - #10285

Draft
v-elegacheva wants to merge 28 commits into
Azure:mainfrom
v-elegacheva:ekat/quantum-suite-offer-quotas
Draft

Quantum: Add 'az quantum suite-offer quotas' command#10285
v-elegacheva wants to merge 28 commits into
Azure:mainfrom
v-elegacheva:ekat/quantum-suite-offer-quotas

Conversation

@v-elegacheva

@v-elegacheva v-elegacheva commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🤖 PR Validation — ⚠️ Review suggested

Breaking Changes
⚠️ None
⚠️Azure CLI Extensions Breaking Change Test
⚠️quantum
rule cmd_name rule_message suggest_message
⚠️ 1001 - CmdAdd quantum suite-offer quotas cmd quantum suite-offer quotas added
⚠️ 1011 - SubgroupAdd quantum suite-offer target sub group quantum suite-offer target added

Summary

Adds suite-offer and V2 target-quota support to the quantum extension:

  • az quantum suite-offer list lists the suite offers available to the subscription.
  • az quantum suite-offer quotas --provider-id <id> lists per-target suite allocations merged with consumed Standard and High usage.
  • az quantum suite-offer target list --provider-id <id> lists targets available through a suite offer without requiring a workspace.
  • az quantum workspace quotas includes V2 target allocations and usages while preserving the existing V1 response format.
  • az quantum workspace create/update --quota validates requested V2 target allocations against current workspace usage and subscription-level suite allocations.

Suite-offer data-plane commands derive the regional V2 endpoint from the suite offer's location.

Changes

Suite offers

  • Added subscription-level suite-offer listing.
  • Added per-target quota allocation and usage output:
    • JSON retains the service contract's minute-based values.
    • Table output displays allocation and usage in hours.
    • Missing allocation or usage values are displayed as 0.
    • Backend target ordering is preserved.
  • Added suite-offer target status listing without requiring a workspace.
  • Added overall, Standard-priority, and High-priority average queue times to target output.
  • Removed the redundant provider column from the suite-offer target table.
  • Consolidated suite-offer data-plane client creation into cf_suite_offers_data_plane.
    • Callers supply the suite offer location.
    • The factory constructs the corresponding regional V2 endpoint.
  • Added compatibility for quota-usage responses returned as either a paged { "value": [...] } envelope or a top-level array.

Workspace quotas

  • Preserved the existing flat V1 quota response fields.
  • Added optional targetId to identify V2 target quota rows.
  • Added separate StandardMinutesLifetime and HighMinutesLifetime rows for each V2 target.
  • Combined target allocations from the ARM workspace resource with data-plane usage.
  • Included usage-only targets so historical usage is not hidden when an allocation is absent.
  • Used workspace.properties.endpointUri as the authoritative data-plane endpoint.
    • V1 workspaces call /quotas.
    • V2 workspaces skip /quotas and call only /quotaUsages.
  • Returned missing V2 allocation and usage values as 0.
  • Reordered table columns to:
    • Scope
    • Provider ID
    • Target
    • Dimension
    • Limit
    • Utilization
    • Holds
    • Period

Before this change, V1 quota table output had no dedicated transformer and relied on the generic serialized field order. The shared transformer now provides consistent V1 and V2 table output.

Workspace quota validation

Added always-on validation for V2 workspace target allocations on create and update.

For each target and priority, the requested absolute allocation must satisfy:

current workspace usage <= requested workspace allocation <= subscription suite target allocation

  • Equality is allowed at both boundaries.
  • Standard and High allocations are validated independently.
  • Create validates against the subscription allocation, using zero as initial workspace usage.
  • Update validates against live workspace usage and the subscription allocation.
  • Priority values omitted during update are preserved and validated.
  • Validation completes before the workspace write.
  • A missing workspace usage row is treated as zero.
  • A missing subscription target allocation is rejected with guidance to configure the allocation first.
  • Separate actionable errors are returned when:
    • The subscription has no allocation for the target.
    • The requested allocation is below current workspace usage.
    • The requested allocation is above the subscription allocation.
  • Error messages include the relevant command for viewing current workspace usage or subscription allocations.
  • Other failures retrieving required validation data prevent the operation.
  • No quota add, --validate-only, or validation bypass option was added.

V2 workspace creation

  • V2 workspace creation disables API-key authentication because API keys are not supported for V2 workspaces.
  • V2 workspace creation does not auto-add legacy free-plan providers.
  • Only explicitly selected V2 providers are included.
  • Existing V1 auto-add and API-key behavior is preserved.

Generated data-plane client

  • Regenerated the vendored Python data-plane client from the public TypeSpec specification.
  • Added optional Standard-priority and High-priority queue-time fields to TargetStatus.
  • Updated quota usage model naming and extensible Usage mapping handling.
  • Removed the explicit package-data entry for the private vendored py.typed marker.

Public terminology

  • Removed references to private product and implementation terminology from help, history, comments, docstrings, and error messages.
  • Updated quota help to use “target quota allocation” and “requested allocation” rather than “final allocation.”

Testing

  • Full Quantum test suite: 76 passed, 14 skipped.
  • Focused workspace validation and table tests: 9 passed.
  • Focused suite-offer endpoint and ordering tests: 7 passed.
  • Suite-offer tests cover:
    • Request construction.
    • Paged and top-level array response handling.
    • Generated-model deserialization.
    • Table transformation.
    • Single-object provider-status handling.
    • Quota merge behavior.
    • Backend allocation ordering.
  • Manual suite-offer validation:
    • List, quota, and target commands passed.
    • JSON and table output passed.
    • Overall, Standard, and High queue-time columns were verified.
  • Manual V1 and V2 workspace quota output passed.
  • Manual workspace create validation:
    • A valid Standard 1200 / High 120 allocation succeeded.
    • An allocation above the subscription limit was rejected.
    • An invalid create did not create an ARM resource.
    • The V2 workspace contained only the explicitly selected provider.
    • API keys were disabled.
  • Manual workspace update validation:
    • A valid idempotent allocation update succeeded.
    • An allocation below fractional workspace usage was rejected.
    • An allocation above the subscription allocation was rejected.
    • An omitted High allocation was preserved.
    • Rejected updates did not modify existing allocations.
  • A missing subscription target allocation was rejected with actionable guidance and did not modify the workspace.
  • Pylint: 10.00/10.
  • Flake8, compileall, help rendering, VS Code diagnostics, pre-commit checks, wheel build, and git diff --check: passed.
  • All temporary Azure resources were deleted after testing.

List the Quantum suite offers available to the subscription (provider, location, and subscription-level quota allocations) via the control-plane SuiteOffers API. Bumps the extension to 1.0.0b24.
…fer-list

# Conflicts:
#	src/quantum/HISTORY.rst
#	src/quantum/setup.py
Adds 'az quantum suite-offer quotas --provider-id' which returns v2 quota allocations merged with their consumed usages for a suite offer provider account. Combines the control-plane suite offer allocations with the data-plane (-v2 endpoint) quota usages, reporting allocated/used/remaining standard and high priority minutes per subscription and target scope.
@azure-client-tools-bot-prd

Copy link
Copy Markdown

Hi v-elegacheva,
Please write the description of changes which can be perceived by customers into HISTORY.rst.
If you want to release a new extension version, please update the version in pyproject.toml (or setup.py, if the extension has not migrated yet) as well.

@v-elegacheva v-elegacheva changed the title [Quantum] Add 'az quantum suite-offer quotas' command Quantum: Add 'az quantum suite-offer quotas' command Sep 1, 2026
@microsoft-github-policy-service microsoft-github-policy-service Bot added the customer-reported Issues that are reported by GitHub users external to the Azure organization. label Sep 1, 2026
@microsoft-github-policy-service

Copy link
Copy Markdown
Contributor

Thank you for your contribution v-elegacheva! We will review the pull request and get back to you soon.

Comment thread src/quantum/azext_quantum/operations/suite_offers.py Outdated
Comment thread src/quantum/azext_quantum/tests/latest/test_quantum_suite_offers.py Outdated
std_used = usage_values.standard_minutes_lifetime if usage_values is not None else None
high_used = usage_values.high_minutes_lifetime if usage_values 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

…age output

Per review: build one row per targetQuota (SubscriptionTarget scope only), restructure each row into nested 'allocation' and 'usage' blocks, and drop the computed 'remaining' field.
Non-functional follow-ups from code review: add the canary branch to base_url_v2 for parity with base_url, add a @live_only scenario test for 'suite-offer quotas', correct the 'suite-offer list' help summary, and comment the unused factory args.
@yonzhan

Copy link
Copy Markdown
Collaborator

Quantum

}

rows = []
for target_quota in sorted(offer.properties.target_quotas or [], key=lambda q: q.target_id or ""):

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.

what is the reason of sorting target quotas here?

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.

It is just to give deterministic, stable output ordering. The service does not guarantee an order for targetQuotas (or the usages list), so sorting by targetId keeps the JSON/ table rows consistent across runs. Which also keeps diffs and the live test stable. I can drop it if you'd rather preserve the service's order :)

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.

for tests we can use other mechanisms to check
as I remember cli provides some built in tools to sort, isn't it?


row = OrderedDict()
row["providerId"] = provider_id
row["scope"] = "SubscriptionTarget"

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.

we can reuse usage.Scope here instead of magic string

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 agree! I did find something worth attention however: a target row can have no matching usage (usage is None for targets with no consumption), so usage.scope isn't always available to read. Since every target row is SubscriptionTarget - scoped by definition, I'll lift the literal into a named constant so it is not a magic string and stays independent of whether a usage row exists. If you would prefer, I can instead read usage.scope when present and fall back to the constant. Please let me know what you would 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.

agree to use const for now

row["providerId"] = provider_id
row["scope"] = "SubscriptionTarget"
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

Lists targets and their status for a suite offer provider account via the data plane, without requiring a workspace. Fixes single-object ProviderStatus parsing, consolidates the data-plane suite-offer client factory, and bumps the extension to 1.0.0b27.
Address review: the data-plane getProviderStatus endpoint returns a single ProviderStatus object per the spec, not a list. Rename the vendored list_provider_status to get_provider_status (sync + async) returning a single ProviderStatus, drop the wrap-in-list workaround, and have the target-list handler wrap the result for the shared table transformer. Update tests accordingly.
…ota allocations merged with usages

Replaces the legacy data-plane quotas listing with v2 workspace target quota allocations (from ARM) merged with their consumed quota usages from the data-plane v2 quotaUsages endpoint. The workspace quotaUsages endpoint requires a providerId query parameter, so usages are fetched per provider. Adds a table transformer and unit tests.
Comment on lines +587 to +592

_WORKSPACE_QUOTA_SCOPE = "Workspace"
_WORKSPACE_QUOTA_PERIOD = "None"
_TARGET_QUOTA_DIMENSIONS = (
("StandardMinutesLifetime", "standard_minutes_lifetime"),
("HighMinutesLifetime", "high_minutes_lifetime"),

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.

wondering if we could reuse values from autogenerated part for these

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.

Yup! In the most recent commit, I updated the generated DimensionScope.WORKSPACE, MeterPeriod.NONE and Priority.STANDARD/Priority.HIGH values. The CLI-specific MinutesLifetime suffix is still added when constructing the dimension names. JSON and table output remain unchanged

Comment on lines +281 to +286
def _target_quota_usage_value(usage, attribute):
if usage is None:
return None
if hasattr(usage, "get"):
return usage.get(_TARGET_QUOTA_USAGE_FIELDS[attribute])
return getattr(usage, attribute, None)

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.

do we really need this? could we just use usage.standard_minutes_lifetime?

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 checked the generated model- Usage is an extensible key-value model, so it does not define standard_minutes_lifetime or high_minutes_lifetime attributes. Direct attribute access raises AttributeError. The values are exposed through camelCase mapping keys.
Updated in the most recent commit to remove the generic compatibility helper and read the generated model directly with usage.get("standardMinutesLifetime") and usage.get("highMinutesLifetime"). The tests now use the actual generated Usage model as well

Comment thread src/quantum/azext_quantum/operations/workspace.py Outdated
Comment thread src/quantum/azext_quantum/operations/workspace.py Outdated
Comment thread src/quantum/azext_quantum/_client_factory.py Outdated
Comment thread src/quantum/azext_quantum/operations/suite_offers.py Outdated
Comment thread src/quantum/azext_quantum/operations/suite_offers.py Outdated
Comment thread src/quantum/azext_quantum/operations/workspace.py Outdated
Comment thread src/quantum/setup.py Outdated
Comment thread src/quantum/azext_quantum/commands.py Outdated
Comment thread src/quantum/azext_quantum/_params.py Outdated
Comment thread src/quantum/azext_quantum/_client_factory.py Outdated
Comment thread src/quantum/azext_quantum/operations/suite_offers.py Outdated
Comment thread src/quantum/azext_quantum/_help.py Outdated
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.

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.

should we mention that table view converts to hours?

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 added the mention. The help now states that the underlying values are reported in minutes and table output converts them to hours!

Comment thread src/quantum/HISTORY.rst Outdated
Comment on lines +272 to +280
_TARGET_QUOTA_PRIORITIES = (
("Standard", "standard_minutes_lifetime"),
("High", "high_minutes_lifetime"),
)
_TARGET_QUOTA_USAGE_FIELDS = {
"standard_minutes_lifetime": "standardMinutesLifetime",
"high_minutes_lifetime": "highMinutesLifetime",
}

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.

could you move this in contants section before class WorkspaceInfo?

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.

Done!

None
)
for priority, attribute in _TARGET_QUOTA_PRIORITIES:
final_allocation = getattr(target_quota, attribute, None)

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.

rename to requested_allocation

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.

Done!

Comment on lines +272 to +275
_TARGET_QUOTA_PRIORITIES = (
("Standard", "standard_minutes_lifetime"),
("High", "high_minutes_lifetime"),
)

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.

priority part seems to be used only in error messages, let's skip it and only keep (standard_minutes_lifetime, high_minutes_lifetime)

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.

Done! Replaced _TARGET_QUOTA_PRIORITIES with _TARGET_QUOTA_ATTRIBUTES, containing only standard_minutes_lifetime and high_minutes_lifetime. The display name used in error messages is now derived from the attribute

if item.target_id is not None and item.target_id.lower() == target_id),
None
)
for priority, attribute in _TARGET_QUOTA_PRIORITIES:

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.

rename attribute to something more appropriate

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.

Done! Renames it to quota_attribute in the quota validation logic

if usage.target_id is not None:
usage_by_key[(provider_id, usage.target_id.lower())] = usage.usage

for provider_id, target_id in sorted(requested_keys):

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.

we have sorted this already above

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.

Done! requested_keys is sorted once when it is created and reused directly in both validation passes

Comment on lines +598 to +603
_WORKSPACE_QUOTA_SCOPE = DimensionScope.WORKSPACE.value
_WORKSPACE_QUOTA_PERIOD = MeterPeriod.NONE.value
_TARGET_QUOTA_DIMENSIONS = (
("StandardMinutesLifetime", "standard_minutes_lifetime"),
("HighMinutesLifetime", "high_minutes_lifetime"),
)

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.

Move to const sections above class WorkspaceInfo

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.

Done!

Comment thread src/quantum/azext_quantum/_help.py Outdated
Comment on lines +368 to +369
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.

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.

let's skip long summary for workspace create for now

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.

Done!

Comment thread src/quantum/azext_quantum/_help.py Outdated
Comment on lines +448 to +451
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.

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.

let's skip long-summary for workspace update for now

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.

Done!

_SUITE_OFFER_QUOTA_SCOPE = "SubscriptionTarget"


def _quota_usage_value(usage, field):

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.

let's put private methods (that starts with '_') after public ones here

"https://azure.microsoft.com/support/legal/preview-supplemental-terms/\n\n" \
"Continue? (Y/N) "

_TARGET_QUOTA_ATTRIBUTES = (

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.

we have 3 variables that contains standard_minutes_lifetime and high_minutes_lifetime in different forms, can we simplify this? maybe have only one variable, at least remove duplicated values

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

act-codegen-extensibility-squad Auto-Assign Auto assign by bot customer-reported Issues that are reported by GitHub users external to the Azure organization. Quantum az quantum

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants