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.
Files changed (121) hide show
  1. {granny_devops-0.19.0 → granny_devops-0.19.2}/PKG-INFO +1 -1
  2. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/analyze/costs.py +26 -3
  3. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/serverless.py +384 -324
  4. {granny_devops-0.19.0 → granny_devops-0.19.2}/pyproject.toml +1 -1
  5. {granny_devops-0.19.0 → granny_devops-0.19.2}/.gitignore +0 -0
  6. {granny_devops-0.19.0 → granny_devops-0.19.2}/LICENSE +0 -0
  7. {granny_devops-0.19.0 → granny_devops-0.19.2}/README.md +0 -0
  8. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/__init__.py +0 -0
  9. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/airtable/__init__.py +0 -0
  10. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/airtable/client.py +0 -0
  11. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/analyze/__init__.py +0 -0
  12. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/analyze/credits.py +0 -0
  13. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/analyze/gpu_pricing.py +0 -0
  14. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/analyze/gpus.py +0 -0
  15. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/analyze/kubernetes.py +0 -0
  16. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/analyze/lambdas.py +0 -0
  17. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/analyze/vpcs.py +0 -0
  18. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/authentik/__init__.py +0 -0
  19. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/authentik/client.py +0 -0
  20. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/authentik/provision.py +0 -0
  21. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/aws/__init__.py +0 -0
  22. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/aws/bedrock.py +0 -0
  23. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/aws/quota.py +0 -0
  24. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/__init__.py +0 -0
  25. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/_client.py +0 -0
  26. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/account.py +0 -0
  27. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/deployment.py +0 -0
  28. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/openai.py +0 -0
  29. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/quota.py +0 -0
  30. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/resourcegroup.py +0 -0
  31. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/role.py +0 -0
  32. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/sku.py +0 -0
  33. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/vm.py +0 -0
  34. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/azure/webapp.py +0 -0
  35. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cdn/__init__.py +0 -0
  36. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cdn/bunny.py +0 -0
  37. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/__init__.py +0 -0
  38. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/airtable.py +0 -0
  39. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/analyze.py +0 -0
  40. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/authentik.py +0 -0
  41. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/aws.py +0 -0
  42. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/azure.py +0 -0
  43. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/cdn.py +0 -0
  44. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/cloudflare.py +0 -0
  45. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/create.py +0 -0
  46. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/credentials.py +0 -0
  47. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/dns.py +0 -0
  48. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/docker.py +0 -0
  49. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/edge.py +0 -0
  50. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/elk.py +0 -0
  51. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/email.py +0 -0
  52. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/indexing.py +0 -0
  53. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/knative.py +0 -0
  54. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/main.py +0 -0
  55. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/oauth.py +0 -0
  56. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/storage.py +0 -0
  57. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cli/zitadel.py +0 -0
  58. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cloudflare/__init__.py +0 -0
  59. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cloudflare/d1.py +0 -0
  60. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cloudflare/r2.py +0 -0
  61. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/cloudflare/workers.py +0 -0
  62. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/__init__.py +0 -0
  63. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/auto_certificate.py +0 -0
  64. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/cloudfront-security-headers.js +0 -0
  65. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/letsencrypt_cert.py +0 -0
  66. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/manage-dns.sh +0 -0
  67. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/manage_mailjet_contacts.py +0 -0
  68. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/registrars.py +0 -0
  69. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_aws_cloudfront.py +0 -0
  70. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_bunny_edge_script.py +0 -0
  71. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_bunny_storage.py +0 -0
  72. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_cognito_identity_pool.py +0 -0
  73. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_hetzner_bunny.py +0 -0
  74. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_mailjet_dns.py +0 -0
  75. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_private_cdn.py +0 -0
  76. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_s3_website.py +0 -0
  77. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_scaleway_container.py +0 -0
  78. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_scaleway_faas.py +0 -0
  79. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/setup_workmail.py +0 -0
  80. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/create/www-redirect-function.js +0 -0
  81. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/credentials/__init__.py +0 -0
  82. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/credentials/secrets.py +0 -0
  83. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/__init__.py +0 -0
  84. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/base.py +0 -0
  85. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/bunny.py +0 -0
  86. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/cloudflare.py +0 -0
  87. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/cloudns.py +0 -0
  88. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/desec.py +0 -0
  89. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/factory.py +0 -0
  90. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/hetzner.py +0 -0
  91. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/inwx.py +0 -0
  92. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/manual.py +0 -0
  93. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/dns/records.py +0 -0
  94. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/docker/__init__.py +0 -0
  95. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/docker/build_base.py +0 -0
  96. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/edge/__init__.py +0 -0
  97. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/edge/bunny.py +0 -0
  98. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/elk/__init__.py +0 -0
  99. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/elk/client.py +0 -0
  100. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/email/__init__.py +0 -0
  101. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/email/mailjet.py +0 -0
  102. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/email/mailjet_contacts.py +0 -0
  103. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/email/ses_forwarding.py +0 -0
  104. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/email/stalwart.py +0 -0
  105. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/email/workmail.py +0 -0
  106. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/indexing/__init__.py +0 -0
  107. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/indexing/google.py +0 -0
  108. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/indexing/indexnow.py +0 -0
  109. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/knative/__init__.py +0 -0
  110. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/knative/client.py +0 -0
  111. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/oauth/__init__.py +0 -0
  112. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/oauth/productivity.py +0 -0
  113. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/report.py +0 -0
  114. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/serverless/__init__.py +0 -0
  115. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/serverless/scaleway.py +0 -0
  116. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/storage/__init__.py +0 -0
  117. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/storage/aws.py +0 -0
  118. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/storage/bunny.py +0 -0
  119. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/storage/hetzner.py +0 -0
  120. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/zitadel/__init__.py +0 -0
  121. {granny_devops-0.19.0 → granny_devops-0.19.2}/granny/zitadel/client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: granny-devops
3
- Version: 0.19.0
3
+ Version: 0.19.2
4
4
  Summary: Cloud tools collection -- AWS infrastructure, CDN, and DevOps automation
5
5
  Author-email: Martin Wieser <martin.wieser@pseekoo.com>
6
6
  License: MIT License
@@ -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
- session = boto3.Session(profile_name=profile if profile != "default" else None)
154
- ce = session.client("ce", region_name="us-east-1")
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)