-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Quantum: Add 'az quantum suite-offer quotas' command #10285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
fd10c2b
25bb4d9
a3640fb
953efb6
d9da06e
0d79e29
eaeb8f7
d5193cd
90a027f
d8766b5
0cefd7d
febfd9c
1f6bf17
d89e4d7
357ffec
37148ab
cd1f80d
cd35c76
aea1be2
abd0c26
1627d73
8ee3abe
ce2a464
f8dcbfb
2fcca1f
2de9a90
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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): | ||
|
|
@@ -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() | ||
| row["providerId"] = provider_id | ||
| row["scope"] = _SUITE_OFFER_QUOTA_SCOPE | ||
| row["targetId"] = target_quota.target_id | ||
| row["allocation"] = _minutes( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 :)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment.
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
}
}
There was a problem hiding this comment.
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}
}
remainingandlastModifiedTimeare dropped. I added a small_minutes()helper to build the nested blocks. The table transformer and help text were also updated to matchThere was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
todictserializer 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 aTypedDict. 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.
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