granny-devops 0.19.0__tar.gz → 0.19.2__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.19.0 → granny_devops-0.19.2}/PKG-INFO +1 -1
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/analyze/costs.py +26 -3
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/serverless.py +384 -324
- {granny_devops-0.19.0 → granny_devops-0.19.2}/pyproject.toml +1 -1
- {granny_devops-0.19.0 → granny_devops-0.19.2}/.gitignore +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/LICENSE +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/README.md +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/airtable/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/airtable/client.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/analyze/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/analyze/credits.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/analyze/gpu_pricing.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/analyze/gpus.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/analyze/kubernetes.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/analyze/lambdas.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/analyze/vpcs.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/authentik/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/authentik/client.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/authentik/provision.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/aws/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/aws/bedrock.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/aws/quota.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/_client.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/account.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/deployment.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/openai.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/quota.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/resourcegroup.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/role.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/sku.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/vm.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/webapp.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cdn/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cdn/bunny.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/airtable.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/analyze.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/authentik.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/aws.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/azure.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/cdn.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/cloudflare.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/create.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/credentials.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/dns.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/docker.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/edge.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/elk.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/email.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/indexing.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/knative.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/main.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/oauth.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/storage.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/zitadel.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cloudflare/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cloudflare/d1.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cloudflare/r2.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cloudflare/workers.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/auto_certificate.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/cloudfront-security-headers.js +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/letsencrypt_cert.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/manage-dns.sh +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/manage_mailjet_contacts.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/registrars.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_aws_cloudfront.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_bunny_edge_script.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_bunny_storage.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_cognito_identity_pool.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_hetzner_bunny.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_mailjet_dns.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_private_cdn.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_s3_website.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_scaleway_container.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_scaleway_faas.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_workmail.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/www-redirect-function.js +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/credentials/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/credentials/secrets.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/base.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/bunny.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/cloudflare.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/cloudns.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/desec.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/factory.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/hetzner.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/inwx.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/manual.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/records.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/docker/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/docker/build_base.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/edge/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/edge/bunny.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/elk/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/elk/client.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/email/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/email/mailjet.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/email/mailjet_contacts.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/email/ses_forwarding.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/email/stalwart.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/email/workmail.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/indexing/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/indexing/google.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/indexing/indexnow.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/knative/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/knative/client.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/oauth/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/oauth/productivity.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/report.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/serverless/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/serverless/scaleway.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/storage/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/storage/aws.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/storage/bunny.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/storage/hetzner.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/zitadel/__init__.py +0 -0
- {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/zitadel/client.py +0 -0
|
@@ -50,6 +50,22 @@ def _month_bounds(today: date | None = None) -> tuple[date, date, date]:
|
|
|
50
50
|
# ---------------------------------------------------------------------------
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
# Cost Explorer returns every RECORD_TYPE unless filtered. Promotional credits
|
|
54
|
+
# (e.g. AWS Activate) land as negative line items carrying the same SERVICE
|
|
55
|
+
# dimension as the usage they pay for, so an unfiltered query nets a fully
|
|
56
|
+
# credit-covered account to ~0.00 -- both in the total and per service. These
|
|
57
|
+
# helpers report *spend*, so credits and refunds are excluded; callers that
|
|
58
|
+
# track a credit balance model the drawdown separately.
|
|
59
|
+
#
|
|
60
|
+
# Exclusion (Not) rather than a Usage-only whitelist is deliberate: Savings Plan
|
|
61
|
+
# and Reserved Instance coverage arrives under additional record types
|
|
62
|
+
# (DiscountedUsage, SavingsPlanCoveredUsage, ...) that a whitelist would
|
|
63
|
+
# silently drop the moment a commitment is purchased.
|
|
64
|
+
_SPEND_ONLY_FILTER: dict = {
|
|
65
|
+
"Not": {"Dimensions": {"Key": "RECORD_TYPE", "Values": ["Credit", "Refund"]}}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
53
69
|
def _ce_client(profile: str) -> "botocore.client.BaseClient":
|
|
54
70
|
"""Build a Cost Explorer client for a named AWS profile (us-east-1 global endpoint)."""
|
|
55
71
|
import boto3
|
|
@@ -94,6 +110,7 @@ def aws_costs_by_service(
|
|
|
94
110
|
Granularity="MONTHLY",
|
|
95
111
|
Metrics=["UnblendedCost"],
|
|
96
112
|
GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}],
|
|
113
|
+
Filter=_SPEND_ONLY_FILTER,
|
|
97
114
|
)
|
|
98
115
|
for bucket in resp.get("ResultsByTime", []):
|
|
99
116
|
label = bucket.get("TimePeriod", {}).get("Start", "")[:7]
|
|
@@ -128,6 +145,7 @@ def aws_cost_total(profile: str, start: date, end: date) -> float | None:
|
|
|
128
145
|
TimePeriod={"Start": start.isoformat(), "End": end.isoformat()},
|
|
129
146
|
Granularity="MONTHLY",
|
|
130
147
|
Metrics=["UnblendedCost"],
|
|
148
|
+
Filter=_SPEND_ONLY_FILTER,
|
|
131
149
|
)
|
|
132
150
|
total = 0.0
|
|
133
151
|
for bucket in resp.get("ResultsByTime", []):
|
|
@@ -140,7 +158,6 @@ def aws_cost_total(profile: str, start: date, end: date) -> float | None:
|
|
|
140
158
|
|
|
141
159
|
def aws_costs(profiles: list[str] | None = None) -> list[CostSummary]:
|
|
142
160
|
"""Month-to-date spend + month-end forecast via Cost Explorer."""
|
|
143
|
-
import boto3
|
|
144
161
|
from botocore.exceptions import BotoCoreError, ClientError
|
|
145
162
|
|
|
146
163
|
from granny.analyze.gpus import aws_profiles
|
|
@@ -150,8 +167,12 @@ def aws_costs(profiles: list[str] | None = None) -> list[CostSummary]:
|
|
|
150
167
|
out: list[CostSummary] = []
|
|
151
168
|
|
|
152
169
|
for profile in profiles:
|
|
153
|
-
|
|
154
|
-
|
|
170
|
+
# Build via _ce_client so tests can substitute a fake client.
|
|
171
|
+
try:
|
|
172
|
+
ce = _ce_client(profile)
|
|
173
|
+
except Exception as e: # noqa: BLE001 - bad/missing profile must not abort the sweep
|
|
174
|
+
logger.warning("AWS %s cost client: %s", profile, e)
|
|
175
|
+
continue
|
|
155
176
|
|
|
156
177
|
mtd: float | None = None
|
|
157
178
|
currency = "USD"
|
|
@@ -160,6 +181,7 @@ def aws_costs(profiles: list[str] | None = None) -> list[CostSummary]:
|
|
|
160
181
|
TimePeriod={"Start": start.isoformat(), "End": today.isoformat()},
|
|
161
182
|
Granularity="MONTHLY",
|
|
162
183
|
Metrics=["UnblendedCost"],
|
|
184
|
+
Filter=_SPEND_ONLY_FILTER,
|
|
163
185
|
)
|
|
164
186
|
result = resp.get("ResultsByTime", [])
|
|
165
187
|
if result:
|
|
@@ -177,6 +199,7 @@ def aws_costs(profiles: list[str] | None = None) -> list[CostSummary]:
|
|
|
177
199
|
TimePeriod={"Start": today.isoformat(), "End": forecast_end.isoformat()},
|
|
178
200
|
Granularity="MONTHLY",
|
|
179
201
|
Metric="UNBLENDED_COST",
|
|
202
|
+
Filter=_SPEND_ONLY_FILTER,
|
|
180
203
|
)
|
|
181
204
|
forecast = (
|
|
182
205
|
float(resp.get("Total", {}).get("Amount", 0.0)) + (mtd or 0.0)
|