Quantum: Add 'az quantum suite-offer quotas' command - #10285
Quantum: Add 'az quantum suite-offer quotas' command#10285v-elegacheva wants to merge 28 commits into
Conversation
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.
|
Hi v-elegacheva, |
|
Thank you for your contribution v-elegacheva! We will review the pull request and get back to you soon. |
| 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() |
There was a problem hiding this comment.
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
}
}
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
|
Quantum |
| } | ||
|
|
||
| rows = [] | ||
| for target_quota in sorted(offer.properties.target_quotas or [], key=lambda q: q.target_id or ""): |
There was a problem hiding this comment.
what is the reason of sorting target quotas here?
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
we can reuse usage.Scope here instead of magic string
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
agree to use const for now
| row["providerId"] = provider_id | ||
| row["scope"] = "SubscriptionTarget" | ||
| row["targetId"] = target_quota.target_id | ||
| row["allocation"] = _minutes( |
There was a problem hiding this comment.
I think in UI we show allocation and usage in hours? align with it
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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.
|
|
||
| _WORKSPACE_QUOTA_SCOPE = "Workspace" | ||
| _WORKSPACE_QUOTA_PERIOD = "None" | ||
| _TARGET_QUOTA_DIMENSIONS = ( | ||
| ("StandardMinutesLifetime", "standard_minutes_lifetime"), | ||
| ("HighMinutesLifetime", "high_minutes_lifetime"), |
There was a problem hiding this comment.
wondering if we could reuse values from autogenerated part for these
There was a problem hiding this comment.
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
| 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) |
There was a problem hiding this comment.
do we really need this? could we just use usage.standard_minutes_lifetime?
There was a problem hiding this comment.
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
| 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. |
There was a problem hiding this comment.
should we mention that table view converts to hours?
There was a problem hiding this comment.
I added the mention. The help now states that the underlying values are reported in minutes and table output converts them to hours!
| _TARGET_QUOTA_PRIORITIES = ( | ||
| ("Standard", "standard_minutes_lifetime"), | ||
| ("High", "high_minutes_lifetime"), | ||
| ) | ||
| _TARGET_QUOTA_USAGE_FIELDS = { | ||
| "standard_minutes_lifetime": "standardMinutesLifetime", | ||
| "high_minutes_lifetime": "highMinutesLifetime", | ||
| } | ||
|
|
There was a problem hiding this comment.
could you move this in contants section before class WorkspaceInfo?
| None | ||
| ) | ||
| for priority, attribute in _TARGET_QUOTA_PRIORITIES: | ||
| final_allocation = getattr(target_quota, attribute, None) |
There was a problem hiding this comment.
rename to requested_allocation
| _TARGET_QUOTA_PRIORITIES = ( | ||
| ("Standard", "standard_minutes_lifetime"), | ||
| ("High", "high_minutes_lifetime"), | ||
| ) |
There was a problem hiding this comment.
priority part seems to be used only in error messages, let's skip it and only keep (standard_minutes_lifetime, high_minutes_lifetime)
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
rename attribute to something more appropriate
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
we have sorted this already above
There was a problem hiding this comment.
Done! requested_keys is sorted once when it is created and reused directly in both validation passes
| _WORKSPACE_QUOTA_SCOPE = DimensionScope.WORKSPACE.value | ||
| _WORKSPACE_QUOTA_PERIOD = MeterPeriod.NONE.value | ||
| _TARGET_QUOTA_DIMENSIONS = ( | ||
| ("StandardMinutesLifetime", "standard_minutes_lifetime"), | ||
| ("HighMinutesLifetime", "high_minutes_lifetime"), | ||
| ) |
There was a problem hiding this comment.
Move to const sections above class WorkspaceInfo
| 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. |
There was a problem hiding this comment.
let's skip long summary for workspace create for now
| 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. |
There was a problem hiding this comment.
let's skip long-summary for workspace update for now
| _SUITE_OFFER_QUOTA_SCOPE = "SubscriptionTarget" | ||
|
|
||
|
|
||
| def _quota_usage_value(usage, field): |
There was a problem hiding this comment.
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 = ( |
There was a problem hiding this comment.
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
🤖 PR Validation —⚠️ Review suggested
Summary
Adds suite-offer and V2 target-quota support to the
quantumextension:az quantum suite-offer listlists 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 quotasincludes V2 target allocations and usages while preserving the existing V1 response format.az quantum workspace create/update --quotavalidates 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
0.cf_suite_offers_data_plane.{ "value": [...] }envelope or a top-level array.Workspace quotas
targetIdto identify V2 target quota rows.StandardMinutesLifetimeandHighMinutesLifetimerows for each V2 target.workspace.properties.endpointUrias the authoritative data-plane endpoint./quotas./quotasand call only/quotaUsages.0.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 allocationquota add,--validate-only, or validation bypass option was added.V2 workspace creation
Generated data-plane client
TargetStatus.Usagemapping handling.py.typedmarker.Public terminology
Testing
1200/ High120allocation succeeded.git diff --check: passed.