granny-devops 0.14.0__tar.gz → 0.15.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {granny_devops-0.14.0 → granny_devops-0.15.0}/PKG-INFO +1 -1
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/__init__.py +1 -1
- granny_devops-0.15.0/granny/aws/bedrock.py +90 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/azure/_client.py +10 -0
- granny_devops-0.15.0/granny/azure/openai.py +311 -0
- granny_devops-0.15.0/granny/azure/resourcegroup.py +52 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/aws.py +79 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/azure.py +243 -9
- {granny_devops-0.14.0 → granny_devops-0.15.0}/pyproject.toml +1 -1
- granny_devops-0.14.0/granny/azure/openai.py +0 -147
- {granny_devops-0.14.0 → granny_devops-0.15.0}/.gitignore +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/LICENSE +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/README.md +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/analyze/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/analyze/costs.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/analyze/credits.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/analyze/gpu_pricing.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/analyze/gpus.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/analyze/lambdas.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/analyze/vpcs.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/authentik/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/authentik/client.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/authentik/provision.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/aws/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/aws/quota.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/azure/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/azure/account.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/azure/deployment.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/azure/quota.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/azure/role.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/azure/sku.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/azure/vm.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/azure/webapp.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cdn/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cdn/bunny.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/analyze.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/authentik.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/cdn.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/cloudflare.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/create.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/credentials.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/dns.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/docker.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/edge.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/elk.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/email.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/indexing.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/main.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/serverless.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/storage.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cli/zitadel.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cloudflare/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cloudflare/d1.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cloudflare/r2.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/cloudflare/workers.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/auto_certificate.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/cloudfront-security-headers.js +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/manage-dns.sh +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/manage_mailjet_contacts.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/registrars.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/setup_aws_cloudfront.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/setup_bunny_edge_script.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/setup_bunny_storage.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/setup_cognito_identity_pool.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/setup_hetzner_bunny.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/setup_mailjet_dns.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/setup_private_cdn.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/setup_s3_website.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/setup_scaleway_container.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/setup_scaleway_faas.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/setup_workmail.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/create/www-redirect-function.js +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/credentials/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/credentials/secrets.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/dns/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/dns/base.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/dns/bunny.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/dns/cloudflare.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/dns/cloudns.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/dns/desec.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/dns/factory.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/dns/hetzner.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/dns/inwx.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/dns/manual.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/dns/records.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/docker/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/docker/build_base.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/edge/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/edge/bunny.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/elk/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/elk/client.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/email/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/email/mailjet.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/email/mailjet_contacts.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/email/ses_forwarding.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/email/stalwart.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/email/workmail.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/indexing/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/indexing/google.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/indexing/indexnow.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/report.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/serverless/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/serverless/scaleway.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/storage/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/storage/aws.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/storage/bunny.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/storage/hetzner.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/zitadel/__init__.py +0 -0
- {granny_devops-0.14.0 → granny_devops-0.15.0}/granny/zitadel/client.py +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Amazon Bedrock foundation-model inspection helpers.
|
|
2
|
+
|
|
3
|
+
Thin boto3 wrapper around the Bedrock control-plane (``bedrock``) client.
|
|
4
|
+
``list-models`` / ``check-access`` need ``bedrock:ListFoundationModels`` /
|
|
5
|
+
``bedrock:GetFoundationModel``.
|
|
6
|
+
|
|
7
|
+
Bedrock has no per-model "deployment" object and **no public API to grant
|
|
8
|
+
on-demand foundation-model access** — that is done once in the console (Model
|
|
9
|
+
access). Once granted, callers invoke a model directly by id
|
|
10
|
+
(e.g. ``anthropic.claude-sonnet-4-20250514-v1:0``); the gateway routes it as
|
|
11
|
+
``bedrock/<model-id>``. These helpers therefore discover model ids and report a
|
|
12
|
+
model's availability/lifecycle so a caller can build a route; they do not (and
|
|
13
|
+
cannot) enable access. The only API-driven "deployment" is Provisioned
|
|
14
|
+
Throughput (``create_provisioned_model_throughput``), which is a paid,
|
|
15
|
+
committed capacity resource and out of scope here.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import boto3
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class FoundationModel:
|
|
28
|
+
model_id: str
|
|
29
|
+
provider_name: str | None
|
|
30
|
+
model_name: str | None
|
|
31
|
+
input_modalities: list[str]
|
|
32
|
+
output_modalities: list[str]
|
|
33
|
+
response_streaming: bool | None
|
|
34
|
+
inference_types: list[str]
|
|
35
|
+
lifecycle: str | None
|
|
36
|
+
raw: dict[str, Any]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _client(profile: str | None, region: str | None) -> Any:
|
|
40
|
+
session = boto3.Session(
|
|
41
|
+
profile_name=profile if profile and profile != "default" else None,
|
|
42
|
+
region_name=region,
|
|
43
|
+
)
|
|
44
|
+
return session.client("bedrock")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _parse_model(item: dict[str, Any]) -> FoundationModel:
|
|
48
|
+
lifecycle = item.get("modelLifecycle", {})
|
|
49
|
+
return FoundationModel(
|
|
50
|
+
model_id=item.get("modelId", ""),
|
|
51
|
+
provider_name=item.get("providerName"),
|
|
52
|
+
model_name=item.get("modelName"),
|
|
53
|
+
input_modalities=list(item.get("inputModalities", [])),
|
|
54
|
+
output_modalities=list(item.get("outputModalities", [])),
|
|
55
|
+
response_streaming=item.get("responseStreamingSupported"),
|
|
56
|
+
inference_types=list(item.get("inferenceTypesSupported", [])),
|
|
57
|
+
lifecycle=lifecycle.get("status") if isinstance(lifecycle, dict) else None,
|
|
58
|
+
raw=item,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def list_foundation_models(
|
|
63
|
+
*,
|
|
64
|
+
profile: str | None = None,
|
|
65
|
+
region: str | None = None,
|
|
66
|
+
by_provider: str | None = None,
|
|
67
|
+
) -> list[FoundationModel]:
|
|
68
|
+
"""List Bedrock foundation models available in the region."""
|
|
69
|
+
client = _client(profile, region)
|
|
70
|
+
kwargs: dict[str, Any] = {}
|
|
71
|
+
if by_provider:
|
|
72
|
+
kwargs["byProvider"] = by_provider
|
|
73
|
+
resp = client.list_foundation_models(**kwargs)
|
|
74
|
+
return [_parse_model(m) for m in resp.get("modelSummaries", [])]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_foundation_model(
|
|
78
|
+
*,
|
|
79
|
+
model_id: str,
|
|
80
|
+
profile: str | None = None,
|
|
81
|
+
region: str | None = None,
|
|
82
|
+
) -> FoundationModel:
|
|
83
|
+
"""Fetch one foundation model's details (id, modalities, lifecycle).
|
|
84
|
+
|
|
85
|
+
Reports the model's availability, not the account's access grant — Bedrock
|
|
86
|
+
exposes no per-account on-demand access status via API.
|
|
87
|
+
"""
|
|
88
|
+
client = _client(profile, region)
|
|
89
|
+
resp = client.get_foundation_model(modelIdentifier=model_id)
|
|
90
|
+
return _parse_model(resp.get("modelDetails", {}))
|
|
@@ -85,6 +85,16 @@ def post(
|
|
|
85
85
|
return response.status_code
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
def post_json(
|
|
89
|
+
url: str, body: dict[str, Any] | None = None, *, token: str | None = None
|
|
90
|
+
) -> dict[str, Any]:
|
|
91
|
+
"""POST an ARM action endpoint (e.g. listKeys) and return the JSON body."""
|
|
92
|
+
response = requests.post(url, headers=headers(token), json=body, timeout=60)
|
|
93
|
+
if response.status_code not in (200, 201, 202):
|
|
94
|
+
raise_response_error("POST", url, response)
|
|
95
|
+
return response.json() if response.content else {}
|
|
96
|
+
|
|
97
|
+
|
|
88
98
|
def paginate(url: str, *, token: str | None = None) -> Iterator[dict[str, Any]]:
|
|
89
99
|
"""Yield each item from a paginated ARM ``value``/``nextLink`` collection."""
|
|
90
100
|
bearer = token if token is not None else get_management_token()
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Azure Cognitive Services (incl. Azure OpenAI) account and deployment listing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from granny.azure import _client
|
|
10
|
+
|
|
11
|
+
_API_VERSION = "2023-05-01"
|
|
12
|
+
# Deployment create/update needs a newer API version than the read path so the
|
|
13
|
+
# GlobalStandard / DataZoneStandard deployment SKUs are accepted.
|
|
14
|
+
_DEPLOY_API_VERSION = "2024-10-01"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class CognitiveAccount:
|
|
19
|
+
subscription: str
|
|
20
|
+
resource_group: str | None
|
|
21
|
+
name: str
|
|
22
|
+
kind: str | None
|
|
23
|
+
sku: str | None
|
|
24
|
+
location: str | None
|
|
25
|
+
endpoint: str | None
|
|
26
|
+
provisioning_state: str | None
|
|
27
|
+
id: str
|
|
28
|
+
raw: dict[str, Any]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class CognitiveDeployment:
|
|
33
|
+
subscription: str
|
|
34
|
+
resource_group: str | None
|
|
35
|
+
account_name: str
|
|
36
|
+
name: str
|
|
37
|
+
model_name: str | None
|
|
38
|
+
model_version: str | None
|
|
39
|
+
sku: str | None
|
|
40
|
+
sku_capacity: int | None
|
|
41
|
+
provisioning_state: str | None
|
|
42
|
+
created_at: str | None
|
|
43
|
+
last_used: str | None
|
|
44
|
+
id: str
|
|
45
|
+
raw: dict[str, Any]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def list_accounts(subscription: str) -> list[CognitiveAccount]:
|
|
49
|
+
"""List Cognitive Services accounts in the subscription."""
|
|
50
|
+
url = (
|
|
51
|
+
f"{_client.MANAGEMENT_URL}/subscriptions/{subscription}"
|
|
52
|
+
f"/providers/Microsoft.CognitiveServices/accounts"
|
|
53
|
+
f"?api-version={_API_VERSION}"
|
|
54
|
+
)
|
|
55
|
+
return [
|
|
56
|
+
_parse_account(subscription=subscription, item=item)
|
|
57
|
+
for item in _client.paginate(url)
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def list_account_deployments(
|
|
62
|
+
subscription: str,
|
|
63
|
+
resource_group: str,
|
|
64
|
+
account_name: str,
|
|
65
|
+
) -> list[CognitiveDeployment]:
|
|
66
|
+
"""List model deployments under one Cognitive Services account."""
|
|
67
|
+
url = (
|
|
68
|
+
f"{_client.MANAGEMENT_URL}/subscriptions/{subscription}"
|
|
69
|
+
f"/resourceGroups/{resource_group}"
|
|
70
|
+
f"/providers/Microsoft.CognitiveServices/accounts/{account_name}/deployments"
|
|
71
|
+
f"?api-version={_API_VERSION}"
|
|
72
|
+
)
|
|
73
|
+
return [
|
|
74
|
+
_parse_deployment(
|
|
75
|
+
subscription=subscription,
|
|
76
|
+
resource_group=resource_group,
|
|
77
|
+
account_name=account_name,
|
|
78
|
+
item=item,
|
|
79
|
+
)
|
|
80
|
+
for item in _client.paginate(url)
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def list_all_deployments(
|
|
85
|
+
subscription: str,
|
|
86
|
+
kind_filter: str | None = None,
|
|
87
|
+
) -> list[CognitiveDeployment]:
|
|
88
|
+
"""List every model deployment across every Cognitive Services account."""
|
|
89
|
+
deployments: list[CognitiveDeployment] = []
|
|
90
|
+
for account in list_accounts(subscription):
|
|
91
|
+
if kind_filter and (account.kind or "").lower() != kind_filter.lower():
|
|
92
|
+
continue
|
|
93
|
+
if not account.resource_group:
|
|
94
|
+
continue
|
|
95
|
+
deployments.extend(
|
|
96
|
+
list_account_deployments(
|
|
97
|
+
subscription=subscription,
|
|
98
|
+
resource_group=account.resource_group,
|
|
99
|
+
account_name=account.name,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
return deployments
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def account_deployment_usage(
|
|
106
|
+
subscription: str,
|
|
107
|
+
resource_group: str,
|
|
108
|
+
account_name: str,
|
|
109
|
+
days: int = 90,
|
|
110
|
+
) -> dict[str, str | None]:
|
|
111
|
+
"""Return ``{deployment_name: last_used_iso}`` from Azure Monitor.
|
|
112
|
+
|
|
113
|
+
Uses the ``ModelRequests`` metric (Total, daily, split by
|
|
114
|
+
``ModelDeploymentName``) over the last ``days`` (platform-metric retention
|
|
115
|
+
is ~93 days). ``None`` means no requests in the window. Requires
|
|
116
|
+
``Microsoft.Insights/metrics/read`` on the account.
|
|
117
|
+
"""
|
|
118
|
+
end = datetime.now(timezone.utc)
|
|
119
|
+
start = end - timedelta(days=days)
|
|
120
|
+
fmt = "%Y-%m-%dT%H:%M:%SZ"
|
|
121
|
+
rid = (
|
|
122
|
+
f"/subscriptions/{subscription}/resourceGroups/{resource_group}"
|
|
123
|
+
f"/providers/Microsoft.CognitiveServices/accounts/{account_name}"
|
|
124
|
+
)
|
|
125
|
+
url = (
|
|
126
|
+
f"{_client.MANAGEMENT_URL}{rid}/providers/microsoft.insights/metrics"
|
|
127
|
+
f"?api-version=2019-07-01&metricnames=ModelRequests&aggregation=Total"
|
|
128
|
+
f"&interval=P1D×pan={start.strftime(fmt)}/{end.strftime(fmt)}"
|
|
129
|
+
f"&$filter=ModelDeploymentName eq '*'"
|
|
130
|
+
)
|
|
131
|
+
data = _client.get(url)
|
|
132
|
+
out: dict[str, str | None] = {}
|
|
133
|
+
for metric in data.get("value", []):
|
|
134
|
+
for ts in metric.get("timeseries", []):
|
|
135
|
+
dep = None
|
|
136
|
+
for md in ts.get("metadatavalues", []):
|
|
137
|
+
mn = md.get("name", {})
|
|
138
|
+
if (mn.get("value") if isinstance(mn, dict) else mn) == "ModelDeploymentName":
|
|
139
|
+
dep = md.get("value")
|
|
140
|
+
if dep is None:
|
|
141
|
+
continue
|
|
142
|
+
last = None
|
|
143
|
+
for pt in ts.get("data", []):
|
|
144
|
+
if pt.get("total"):
|
|
145
|
+
last = pt.get("timeStamp")
|
|
146
|
+
out[dep] = last
|
|
147
|
+
return out
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_account(
|
|
151
|
+
subscription: str,
|
|
152
|
+
resource_group: str,
|
|
153
|
+
account_name: str,
|
|
154
|
+
) -> CognitiveAccount:
|
|
155
|
+
"""Fetch a single Cognitive Services account (incl. its endpoint)."""
|
|
156
|
+
url = (
|
|
157
|
+
f"{_client.MANAGEMENT_URL}/subscriptions/{subscription}"
|
|
158
|
+
f"/resourceGroups/{resource_group}"
|
|
159
|
+
f"/providers/Microsoft.CognitiveServices/accounts/{account_name}"
|
|
160
|
+
f"?api-version={_API_VERSION}"
|
|
161
|
+
)
|
|
162
|
+
return _parse_account(subscription=subscription, item=_client.get(url))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_account_keys(
|
|
166
|
+
subscription: str,
|
|
167
|
+
resource_group: str,
|
|
168
|
+
account_name: str,
|
|
169
|
+
) -> dict[str, str]:
|
|
170
|
+
"""Return the account's access keys (``{"key1": ..., "key2": ...}``)."""
|
|
171
|
+
url = (
|
|
172
|
+
f"{_client.MANAGEMENT_URL}/subscriptions/{subscription}"
|
|
173
|
+
f"/resourceGroups/{resource_group}"
|
|
174
|
+
f"/providers/Microsoft.CognitiveServices/accounts/{account_name}"
|
|
175
|
+
f"/listKeys?api-version={_API_VERSION}"
|
|
176
|
+
)
|
|
177
|
+
keys = _client.post_json(url)
|
|
178
|
+
return {k: v for k, v in keys.items() if k in ("key1", "key2")}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def create_account(
|
|
182
|
+
*,
|
|
183
|
+
subscription: str,
|
|
184
|
+
resource_group: str,
|
|
185
|
+
account_name: str,
|
|
186
|
+
location: str,
|
|
187
|
+
custom_subdomain: str | None = None,
|
|
188
|
+
sku_name: str = "S0",
|
|
189
|
+
) -> CognitiveAccount:
|
|
190
|
+
"""Create (or update) an Azure OpenAI (Cognitive Services) account via ARM PUT.
|
|
191
|
+
|
|
192
|
+
``custom_subdomain`` must be globally unique (it becomes the ``*.openai.azure.com``
|
|
193
|
+
host); defaults to ``account_name``. The create is async — the returned account
|
|
194
|
+
may show provisioningState ``Accepted``/``Creating``; poll :func:`get_account`
|
|
195
|
+
until ``Succeeded`` before creating deployments.
|
|
196
|
+
"""
|
|
197
|
+
url = (
|
|
198
|
+
f"{_client.MANAGEMENT_URL}/subscriptions/{subscription}"
|
|
199
|
+
f"/resourceGroups/{resource_group}"
|
|
200
|
+
f"/providers/Microsoft.CognitiveServices/accounts/{account_name}"
|
|
201
|
+
f"?api-version={_API_VERSION}"
|
|
202
|
+
)
|
|
203
|
+
body = {
|
|
204
|
+
"location": location,
|
|
205
|
+
"kind": "OpenAI",
|
|
206
|
+
"sku": {"name": sku_name},
|
|
207
|
+
"properties": {
|
|
208
|
+
"customSubDomainName": custom_subdomain or account_name,
|
|
209
|
+
"publicNetworkAccess": "Enabled",
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
item = _client.put(url, body)
|
|
213
|
+
return _parse_account(subscription=subscription, item=item)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def create_deployment(
|
|
217
|
+
*,
|
|
218
|
+
subscription: str,
|
|
219
|
+
resource_group: str,
|
|
220
|
+
account_name: str,
|
|
221
|
+
deployment_name: str,
|
|
222
|
+
model_name: str,
|
|
223
|
+
model_version: str | None = None,
|
|
224
|
+
model_format: str = "OpenAI",
|
|
225
|
+
sku_name: str = "GlobalStandard",
|
|
226
|
+
sku_capacity: int = 1,
|
|
227
|
+
) -> CognitiveDeployment:
|
|
228
|
+
"""Create (or update) an Azure OpenAI model deployment via ARM PUT.
|
|
229
|
+
|
|
230
|
+
``deployment_name`` is the name callers request at inference time;
|
|
231
|
+
``model_name`` (+ optional ``model_version``) is the underlying model, e.g.
|
|
232
|
+
``gpt-4o`` / ``2024-08-06``. ``sku_capacity`` is in thousands of TPM for the
|
|
233
|
+
standard SKUs. Idempotent: PUT on an existing deployment updates it.
|
|
234
|
+
"""
|
|
235
|
+
url = (
|
|
236
|
+
f"{_client.MANAGEMENT_URL}/subscriptions/{subscription}"
|
|
237
|
+
f"/resourceGroups/{resource_group}"
|
|
238
|
+
f"/providers/Microsoft.CognitiveServices/accounts/{account_name}"
|
|
239
|
+
f"/deployments/{deployment_name}"
|
|
240
|
+
f"?api-version={_DEPLOY_API_VERSION}"
|
|
241
|
+
)
|
|
242
|
+
model: dict[str, Any] = {"format": model_format, "name": model_name}
|
|
243
|
+
if model_version:
|
|
244
|
+
model["version"] = model_version
|
|
245
|
+
body = {
|
|
246
|
+
"sku": {"name": sku_name, "capacity": sku_capacity},
|
|
247
|
+
"properties": {"model": model},
|
|
248
|
+
}
|
|
249
|
+
item = _client.put(url, body)
|
|
250
|
+
return _parse_deployment(
|
|
251
|
+
subscription=subscription,
|
|
252
|
+
resource_group=resource_group,
|
|
253
|
+
account_name=account_name,
|
|
254
|
+
item=item,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _parse_account(subscription: str, item: dict[str, Any]) -> CognitiveAccount:
|
|
259
|
+
properties = item.get("properties", {}) if isinstance(item, dict) else {}
|
|
260
|
+
sku = item.get("sku", {}) if isinstance(item, dict) else {}
|
|
261
|
+
resource_id = item.get("id", "")
|
|
262
|
+
return CognitiveAccount(
|
|
263
|
+
subscription=subscription,
|
|
264
|
+
resource_group=_resource_group_from_id(resource_id),
|
|
265
|
+
name=item.get("name", ""),
|
|
266
|
+
kind=item.get("kind"),
|
|
267
|
+
sku=sku.get("name") if isinstance(sku, dict) else None,
|
|
268
|
+
location=item.get("location"),
|
|
269
|
+
endpoint=properties.get("endpoint"),
|
|
270
|
+
provisioning_state=properties.get("provisioningState"),
|
|
271
|
+
id=resource_id,
|
|
272
|
+
raw=item,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _parse_deployment(
|
|
277
|
+
subscription: str,
|
|
278
|
+
resource_group: str,
|
|
279
|
+
account_name: str,
|
|
280
|
+
item: dict[str, Any],
|
|
281
|
+
) -> CognitiveDeployment:
|
|
282
|
+
properties = item.get("properties", {}) if isinstance(item, dict) else {}
|
|
283
|
+
model = properties.get("model", {}) if isinstance(properties, dict) else {}
|
|
284
|
+
sku = item.get("sku", {}) if isinstance(item, dict) else {}
|
|
285
|
+
system = item.get("systemData", {}) if isinstance(item, dict) else {}
|
|
286
|
+
return CognitiveDeployment(
|
|
287
|
+
subscription=subscription,
|
|
288
|
+
resource_group=resource_group,
|
|
289
|
+
account_name=account_name,
|
|
290
|
+
name=item.get("name", ""),
|
|
291
|
+
model_name=model.get("name") if isinstance(model, dict) else None,
|
|
292
|
+
model_version=model.get("version") if isinstance(model, dict) else None,
|
|
293
|
+
sku=sku.get("name") if isinstance(sku, dict) else None,
|
|
294
|
+
sku_capacity=sku.get("capacity") if isinstance(sku, dict) else None,
|
|
295
|
+
provisioning_state=properties.get("provisioningState"),
|
|
296
|
+
created_at=system.get("createdAt") if isinstance(system, dict) else None,
|
|
297
|
+
last_used=None, # filled in on demand via account_deployment_usage()
|
|
298
|
+
id=item.get("id", ""),
|
|
299
|
+
raw=item,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _resource_group_from_id(resource_id: str) -> str | None:
|
|
304
|
+
parts = resource_id.lower().split("/")
|
|
305
|
+
try:
|
|
306
|
+
idx = parts.index("resourcegroups")
|
|
307
|
+
except ValueError:
|
|
308
|
+
return None
|
|
309
|
+
if idx + 1 < len(parts):
|
|
310
|
+
return resource_id.split("/")[idx + 1]
|
|
311
|
+
return None
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Azure resource-group create/list helpers (ARM REST via ``_client``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from granny.azure import _client
|
|
9
|
+
|
|
10
|
+
_API_VERSION = "2021-04-01"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ResourceGroup:
|
|
15
|
+
subscription: str
|
|
16
|
+
name: str
|
|
17
|
+
location: str | None
|
|
18
|
+
provisioning_state: str | None
|
|
19
|
+
id: str
|
|
20
|
+
raw: dict[str, Any]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _parse(subscription: str, item: dict[str, Any]) -> ResourceGroup:
|
|
24
|
+
props = item.get("properties", {}) if isinstance(item, dict) else {}
|
|
25
|
+
return ResourceGroup(
|
|
26
|
+
subscription=subscription,
|
|
27
|
+
name=item.get("name", ""),
|
|
28
|
+
location=item.get("location"),
|
|
29
|
+
provisioning_state=props.get("provisioningState") if isinstance(props, dict) else None,
|
|
30
|
+
id=item.get("id", ""),
|
|
31
|
+
raw=item,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_resource_group(
|
|
36
|
+
*, subscription: str, name: str, location: str
|
|
37
|
+
) -> ResourceGroup:
|
|
38
|
+
"""Create (or update) a resource group. Idempotent."""
|
|
39
|
+
url = (
|
|
40
|
+
f"{_client.MANAGEMENT_URL}/subscriptions/{subscription}"
|
|
41
|
+
f"/resourcegroups/{name}?api-version={_API_VERSION}"
|
|
42
|
+
)
|
|
43
|
+
return _parse(subscription, _client.put(url, {"location": location}))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def list_resource_groups(subscription: str) -> list[ResourceGroup]:
|
|
47
|
+
"""List resource groups in the subscription."""
|
|
48
|
+
url = (
|
|
49
|
+
f"{_client.MANAGEMENT_URL}/subscriptions/{subscription}"
|
|
50
|
+
f"/resourcegroups?api-version={_API_VERSION}"
|
|
51
|
+
)
|
|
52
|
+
return [_parse(subscription, item) for item in _client.paginate(url)]
|
|
@@ -168,6 +168,85 @@ def quota_request(
|
|
|
168
168
|
click.echo(f"Case ID: {result.case_id}")
|
|
169
169
|
|
|
170
170
|
|
|
171
|
+
@aws.group()
|
|
172
|
+
def bedrock() -> None:
|
|
173
|
+
"""Inspect and enable Amazon Bedrock foundation models."""
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@bedrock.command("list-models")
|
|
177
|
+
@click.option("--by-provider", default=None, help="Filter by provider, e.g. Anthropic / Meta / Amazon")
|
|
178
|
+
@click.option("--profile", default=None, help="AWS profile name")
|
|
179
|
+
@click.option("--region", default=None, help="AWS region, e.g. eu-central-1")
|
|
180
|
+
@click.option("--json-output", "as_json", is_flag=True, help="Output as JSON")
|
|
181
|
+
def bedrock_list_models(
|
|
182
|
+
by_provider: str | None,
|
|
183
|
+
profile: str | None,
|
|
184
|
+
region: str | None,
|
|
185
|
+
as_json: bool,
|
|
186
|
+
) -> None:
|
|
187
|
+
"""List Bedrock foundation models available in the region."""
|
|
188
|
+
from dataclasses import asdict
|
|
189
|
+
|
|
190
|
+
from granny.aws.bedrock import list_foundation_models
|
|
191
|
+
|
|
192
|
+
with _friendly_errors():
|
|
193
|
+
rows = list_foundation_models(
|
|
194
|
+
profile=profile, region=region, by_provider=by_provider
|
|
195
|
+
)
|
|
196
|
+
if as_json:
|
|
197
|
+
click.echo(json.dumps([asdict(r) for r in rows], indent=2, default=str))
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
if not rows:
|
|
201
|
+
click.echo("No Bedrock foundation models found.")
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
click.echo(f"\n{'Model ID':<52} {'Provider':<16} Streaming")
|
|
205
|
+
click.echo("-" * 90)
|
|
206
|
+
for row in rows:
|
|
207
|
+
streaming = "-" if row.response_streaming is None else ("yes" if row.response_streaming else "no")
|
|
208
|
+
click.echo(f"{row.model_id:<52} {(row.provider_name or ''):<16} {streaming}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@bedrock.command("check-access")
|
|
212
|
+
@click.option("--model-id", required=True, help="Foundation model id, e.g. anthropic.claude-sonnet-4-20250514-v1:0")
|
|
213
|
+
@click.option("--profile", default=None, help="AWS profile name")
|
|
214
|
+
@click.option("--region", default=None, help="AWS region, e.g. eu-central-1")
|
|
215
|
+
@click.option("--json-output", "as_json", is_flag=True, help="Output as JSON")
|
|
216
|
+
def bedrock_check_access(
|
|
217
|
+
model_id: str,
|
|
218
|
+
profile: str | None,
|
|
219
|
+
region: str | None,
|
|
220
|
+
as_json: bool,
|
|
221
|
+
) -> None:
|
|
222
|
+
"""Report a Bedrock foundation model's availability and on-demand support.
|
|
223
|
+
|
|
224
|
+
Bedrock has no API to grant on-demand access — that is a one-time console
|
|
225
|
+
step (Model access). This confirms the model exists, is ACTIVE, and supports
|
|
226
|
+
ON_DEMAND, then tells you the route id. If a later invoke returns
|
|
227
|
+
AccessDenied, enable the model in the console.
|
|
228
|
+
"""
|
|
229
|
+
from dataclasses import asdict
|
|
230
|
+
|
|
231
|
+
from granny.aws.bedrock import get_foundation_model
|
|
232
|
+
|
|
233
|
+
with _friendly_errors():
|
|
234
|
+
fm = get_foundation_model(model_id=model_id, profile=profile, region=region)
|
|
235
|
+
on_demand = "ON_DEMAND" in fm.inference_types
|
|
236
|
+
if as_json:
|
|
237
|
+
out = asdict(fm)
|
|
238
|
+
out["on_demand"] = on_demand
|
|
239
|
+
out["route_model"] = f"bedrock/{fm.model_id}"
|
|
240
|
+
click.echo(json.dumps(out, indent=2, default=str))
|
|
241
|
+
return
|
|
242
|
+
click.echo(f"Model: {fm.model_id}")
|
|
243
|
+
click.echo(f"Provider: {fm.provider_name or '-'}")
|
|
244
|
+
click.echo(f"Lifecycle: {fm.lifecycle or '-'}")
|
|
245
|
+
click.echo(f"On-demand: {'yes' if on_demand else 'no'}")
|
|
246
|
+
click.echo(f"Route as: bedrock/{fm.model_id}")
|
|
247
|
+
click.echo("Access is granted in the Bedrock console (Model access); no public API.")
|
|
248
|
+
|
|
249
|
+
|
|
171
250
|
@quota.command("status")
|
|
172
251
|
@click.argument("quota_ref", default="gpu")
|
|
173
252
|
@click.option("--service-code", default=None, help="Override service code (default: ec2)")
|