granny-devops 0.6.0__tar.gz → 0.8.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.
Files changed (82) hide show
  1. granny_devops-0.8.0/PKG-INFO +358 -0
  2. granny_devops-0.8.0/README.md +226 -0
  3. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/__init__.py +1 -1
  4. granny_devops-0.8.0/granny/analyze/__init__.py +43 -0
  5. granny_devops-0.8.0/granny/analyze/costs.py +222 -0
  6. granny_devops-0.8.0/granny/analyze/credits.py +164 -0
  7. granny_devops-0.8.0/granny/analyze/gpus.py +819 -0
  8. granny_devops-0.8.0/granny/cli/analyze.py +452 -0
  9. {granny_devops-0.6.0 → granny_devops-0.8.0}/pyproject.toml +18 -2
  10. granny_devops-0.6.0/PKG-INFO +0 -445
  11. granny_devops-0.6.0/README.md +0 -335
  12. granny_devops-0.6.0/granny/analyze/__init__.py +0 -6
  13. granny_devops-0.6.0/granny/cli/analyze.py +0 -66
  14. {granny_devops-0.6.0 → granny_devops-0.8.0}/.gitignore +0 -0
  15. {granny_devops-0.6.0 → granny_devops-0.8.0}/LICENSE +0 -0
  16. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/analyze/lambdas.py +0 -0
  17. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/analyze/vpcs.py +0 -0
  18. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cdn/__init__.py +0 -0
  19. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cdn/bunny.py +0 -0
  20. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cli/__init__.py +0 -0
  21. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cli/cdn.py +0 -0
  22. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cli/cloudflare.py +0 -0
  23. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cli/create.py +0 -0
  24. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cli/credentials.py +0 -0
  25. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cli/dns.py +0 -0
  26. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cli/docker.py +0 -0
  27. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cli/edge.py +0 -0
  28. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cli/email.py +0 -0
  29. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cli/main.py +0 -0
  30. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cli/serverless.py +0 -0
  31. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cli/storage.py +0 -0
  32. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cloudflare/__init__.py +0 -0
  33. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cloudflare/d1.py +0 -0
  34. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cloudflare/r2.py +0 -0
  35. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/cloudflare/workers.py +0 -0
  36. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/__init__.py +0 -0
  37. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/auto_certificate.py +0 -0
  38. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/cloudfront-security-headers.js +0 -0
  39. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/manage-dns.sh +0 -0
  40. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/manage_mailjet_contacts.py +0 -0
  41. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/registrars.py +0 -0
  42. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/setup_aws_cloudfront.py +0 -0
  43. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/setup_bunny_edge_script.py +0 -0
  44. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/setup_bunny_storage.py +0 -0
  45. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/setup_cognito_identity_pool.py +0 -0
  46. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/setup_hetzner_bunny.py +0 -0
  47. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/setup_mailjet_dns.py +0 -0
  48. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/setup_private_cdn.py +0 -0
  49. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/setup_s3_website.py +0 -0
  50. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/setup_scaleway_container.py +0 -0
  51. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/setup_scaleway_faas.py +0 -0
  52. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/setup_workmail.py +0 -0
  53. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/create/www-redirect-function.js +0 -0
  54. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/credentials/__init__.py +0 -0
  55. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/credentials/secrets.py +0 -0
  56. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/dns/__init__.py +0 -0
  57. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/dns/base.py +0 -0
  58. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/dns/bunny.py +0 -0
  59. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/dns/cloudflare.py +0 -0
  60. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/dns/cloudns.py +0 -0
  61. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/dns/desec.py +0 -0
  62. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/dns/factory.py +0 -0
  63. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/dns/hetzner.py +0 -0
  64. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/dns/inwx.py +0 -0
  65. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/dns/manual.py +0 -0
  66. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/dns/records.py +0 -0
  67. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/docker/__init__.py +0 -0
  68. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/docker/build_base.py +0 -0
  69. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/edge/__init__.py +0 -0
  70. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/edge/bunny.py +0 -0
  71. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/email/__init__.py +0 -0
  72. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/email/mailjet.py +0 -0
  73. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/email/mailjet_contacts.py +0 -0
  74. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/email/ses_forwarding.py +0 -0
  75. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/email/workmail.py +0 -0
  76. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/report.py +0 -0
  77. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/serverless/__init__.py +0 -0
  78. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/serverless/scaleway.py +0 -0
  79. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/storage/__init__.py +0 -0
  80. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/storage/aws.py +0 -0
  81. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/storage/bunny.py +0 -0
  82. {granny_devops-0.6.0 → granny_devops-0.8.0}/granny/storage/hetzner.py +0 -0
@@ -0,0 +1,358 @@
1
+ Metadata-Version: 2.4
2
+ Name: granny-devops
3
+ Version: 0.8.0
4
+ Summary: Cloud tools collection -- AWS infrastructure, CDN, and DevOps automation
5
+ Author-email: Martin Wieser <martin.wieser@pseekoo.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Martin Wieser
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Requires-Python: >=3.13
29
+ Requires-Dist: boto3>=1.38
30
+ Requires-Dist: click>=8.1
31
+ Requires-Dist: python-dotenv>=1.1.0
32
+ Requires-Dist: requests>=2.32
33
+ Provides-Extra: all
34
+ Requires-Dist: anthropic>=0.54.0; extra == 'all'
35
+ Requires-Dist: azure-identity>=1.23; extra == 'all'
36
+ Requires-Dist: azure-identity>=1.23.0; extra == 'all'
37
+ Requires-Dist: azure-mgmt-billing>=6.1; extra == 'all'
38
+ Requires-Dist: azure-mgmt-compute>=33; extra == 'all'
39
+ Requires-Dist: azure-mgmt-consumption>=10.0; extra == 'all'
40
+ Requires-Dist: azure-mgmt-costmanagement>=4.0; extra == 'all'
41
+ Requires-Dist: azure-mgmt-reservations>=2.3; extra == 'all'
42
+ Requires-Dist: azure-mgmt-subscription>=3.1; extra == 'all'
43
+ Requires-Dist: beautifulsoup4~=4.13.4; extra == 'all'
44
+ Requires-Dist: boto3-stubs>=1.40; extra == 'all'
45
+ Requires-Dist: certifi>=2025.6.15; extra == 'all'
46
+ Requires-Dist: cloudflare==4.3.1; extra == 'all'
47
+ Requires-Dist: earthengine-api>=1.5.20; extra == 'all'
48
+ Requires-Dist: elasticsearch-dsl<9.0.0,>=8.0.0; extra == 'all'
49
+ Requires-Dist: elasticsearch<9.0.0,>=8.0.0; extra == 'all'
50
+ Requires-Dist: flask-cors>=6.0.1; extra == 'all'
51
+ Requires-Dist: flask>=3.1.1; extra == 'all'
52
+ Requires-Dist: google-auth>=2.40.3; extra == 'all'
53
+ Requires-Dist: google-cloud-billing>=1.16; extra == 'all'
54
+ Requires-Dist: google-cloud-compute>=1.21; extra == 'all'
55
+ Requires-Dist: google-cloud-resource-manager>=1.12; extra == 'all'
56
+ Requires-Dist: google-generativeai>=0.8.5; extra == 'all'
57
+ Requires-Dist: gspread>=6.2.1; extra == 'all'
58
+ Requires-Dist: hetzner-dns-api>=1.0.0; extra == 'all'
59
+ Requires-Dist: ipython~=9.3.0; extra == 'all'
60
+ Requires-Dist: json5>=0.12.0; extra == 'all'
61
+ Requires-Dist: jsonschema>=4.0.0; extra == 'all'
62
+ Requires-Dist: langchain-openai>=0.3.24; extra == 'all'
63
+ Requires-Dist: langchain>=0.3.26; extra == 'all'
64
+ Requires-Dist: langdetect>=1.0.9; extra == 'all'
65
+ Requires-Dist: litellm>=1.73.0; extra == 'all'
66
+ Requires-Dist: matplotlib>=3.10.3; extra == 'all'
67
+ Requires-Dist: msoffcrypto-tool>=5.4.2; extra == 'all'
68
+ Requires-Dist: numpy>=2.3.1; extra == 'all'
69
+ Requires-Dist: ollama>=0.5.1; extra == 'all'
70
+ Requires-Dist: openai>=1.90.0; extra == 'all'
71
+ Requires-Dist: openpyxl==3.1.5; extra == 'all'
72
+ Requires-Dist: pandas>=2.3.0; extra == 'all'
73
+ Requires-Dist: pdf2image>=1.17.0; extra == 'all'
74
+ Requires-Dist: pillow>=11.2.1; extra == 'all'
75
+ Requires-Dist: playwright>=1.52.0; extra == 'all'
76
+ Requires-Dist: pyairtable~=3.1.1; extra == 'all'
77
+ Requires-Dist: pydantic>=2.11.7; extra == 'all'
78
+ Requires-Dist: pymilvus>=2.5.11; extra == 'all'
79
+ Requires-Dist: pymongo>=4.13; extra == 'all'
80
+ Requires-Dist: pypdf>=5.6.1; extra == 'all'
81
+ Requires-Dist: pytesseract>=0.3.13; extra == 'all'
82
+ Requires-Dist: pyzbar>=0.1.9; extra == 'all'
83
+ Requires-Dist: selenium>=4.33.0; extra == 'all'
84
+ Requires-Dist: sentinelhub>=3.11.1; extra == 'all'
85
+ Requires-Dist: sentry-sdk>=2.30.0; extra == 'all'
86
+ Requires-Dist: tiktoken>=0.9.0; extra == 'all'
87
+ Requires-Dist: webdriver-manager>=4.0.2; extra == 'all'
88
+ Requires-Dist: zxing>=1.0.3; extra == 'all'
89
+ Provides-Extra: azure
90
+ Requires-Dist: azure-identity>=1.23; extra == 'azure'
91
+ Requires-Dist: azure-mgmt-billing>=6.1; extra == 'azure'
92
+ Requires-Dist: azure-mgmt-compute>=33; extra == 'azure'
93
+ Requires-Dist: azure-mgmt-consumption>=10.0; extra == 'azure'
94
+ Requires-Dist: azure-mgmt-costmanagement>=4.0; extra == 'azure'
95
+ Requires-Dist: azure-mgmt-reservations>=2.3; extra == 'azure'
96
+ Requires-Dist: azure-mgmt-subscription>=3.1; extra == 'azure'
97
+ Provides-Extra: browser
98
+ Requires-Dist: playwright>=1.52.0; extra == 'browser'
99
+ Requires-Dist: selenium>=4.33.0; extra == 'browser'
100
+ Requires-Dist: webdriver-manager>=4.0.2; extra == 'browser'
101
+ Provides-Extra: cdn
102
+ Requires-Dist: cloudflare==4.3.1; extra == 'cdn'
103
+ Requires-Dist: hetzner-dns-api>=1.0.0; extra == 'cdn'
104
+ Provides-Extra: data
105
+ Requires-Dist: beautifulsoup4~=4.13.4; extra == 'data'
106
+ Requires-Dist: gspread>=6.2.1; extra == 'data'
107
+ Requires-Dist: openpyxl==3.1.5; extra == 'data'
108
+ Requires-Dist: pandas>=2.3.0; extra == 'data'
109
+ Requires-Dist: pyairtable~=3.1.1; extra == 'data'
110
+ Requires-Dist: pymongo>=4.13; extra == 'data'
111
+ Provides-Extra: dev
112
+ Requires-Dist: hatch>=1.14; extra == 'dev'
113
+ Requires-Dist: prek>=0.2; extra == 'dev'
114
+ Requires-Dist: pytest>=8.3; extra == 'dev'
115
+ Requires-Dist: ruff>=0.11; extra == 'dev'
116
+ Requires-Dist: twine>=6.1; extra == 'dev'
117
+ Provides-Extra: gcp
118
+ Requires-Dist: google-cloud-billing>=1.16; extra == 'gcp'
119
+ Requires-Dist: google-cloud-compute>=1.21; extra == 'gcp'
120
+ Requires-Dist: google-cloud-resource-manager>=1.12; extra == 'gcp'
121
+ Provides-Extra: ml
122
+ Requires-Dist: anthropic>=0.54.0; extra == 'ml'
123
+ Requires-Dist: google-generativeai>=0.8.5; extra == 'ml'
124
+ Requires-Dist: langchain-openai>=0.3.24; extra == 'ml'
125
+ Requires-Dist: langchain>=0.3.26; extra == 'ml'
126
+ Requires-Dist: litellm>=1.73.0; extra == 'ml'
127
+ Requires-Dist: ollama>=0.5.1; extra == 'ml'
128
+ Requires-Dist: openai>=1.90.0; extra == 'ml'
129
+ Requires-Dist: tiktoken>=0.9.0; extra == 'ml'
130
+ Provides-Extra: vault
131
+ Description-Content-Type: text/markdown
132
+
133
+ # Granny
134
+
135
+ > One CLI for the cloud chores you'd otherwise do across six tabs.
136
+
137
+ `granny` is a pragmatic, multi-provider DevOps toolkit with a strong bias
138
+ toward European cloud infrastructure. It wraps the parts of provider APIs
139
+ you actually use day-to-day — Bunny pull zones, Cloudflare DNS, Hetzner S3,
140
+ Scaleway functions, Mailjet sender setup, INWX zones, AWS Lambda inventory
141
+ — behind a single command:
142
+
143
+ ```shell
144
+ granny dns add api.example.com --type A --value 203.0.113.4 --provider hetzner
145
+ granny cdn purge 12345
146
+ granny storage bunny upload my-zone ./dist
147
+ granny credentials status
148
+ ```
149
+
150
+ ## What it is
151
+
152
+ - **Pragmatic, not exhaustive.** Granny implements the parts of each
153
+ provider API the maintainers needed in production. It will not cover
154
+ every endpoint of every service — and it will not pretend to. Add what
155
+ you need; the [contributor guide](Project_Guidelines.md) explains how.
156
+ - **Multi-cloud, with a European tilt.** First-class support for Bunny,
157
+ Cloudflare, Hetzner, deSEC, ClouDNS, INWX, Scaleway, and Mailjet
158
+ alongside AWS S3 / Lambda / WorkMail. The cloud world doesn't end at
159
+ AWS, and granny doesn't pretend it does.
160
+ - **Secrets done right.** Every credential goes through one resolver
161
+ chain — environment variable first, optional Vaultwarden vault second,
162
+ never hardcoded. `.env` and `.deploy.env` are auto-loaded; vault support
163
+ is a one-line activation when you want it.
164
+ - **Library or CLI.** Every CLI subcommand is a thin shim over a small
165
+ Python module. Import `granny.dns.cloudflare`, `granny.cdn.bunny`,
166
+ `granny.cloudflare.d1` directly when you need to script something the
167
+ CLI doesn't expose yet.
168
+ - **Small, composable, no plugin system.** The whole package is one
169
+ `pip install` away. No daemon, no service, no opinionated framework.
170
+ Drop it into your CI image and call it a day.
171
+
172
+ ## Install
173
+
174
+ ```shell
175
+ pip install granny-devops
176
+ # or
177
+ uv add granny-devops
178
+ ```
179
+
180
+ Optional extras enable additional providers:
181
+
182
+ ```shell
183
+ pip install "granny-devops[gcp]" # GCP for granny analyze (gpus|credits|costs)
184
+ pip install "granny-devops[azure]" # Azure for granny analyze (gpus|credits|costs)
185
+ pip install "granny-devops[cdn]" # Cloudflare, Hetzner DNS
186
+ ```
187
+
188
+ Tagged releases land on **pypi.org** and the public GitLab PyPI registry
189
+ in parallel. If PyPI is propagating slowly, fall back to the registry:
190
+
191
+ ```shell
192
+ pip install --extra-index-url https://gitlab.com/api/v4/projects/81189862/packages/pypi/simple granny-devops
193
+ ```
194
+
195
+ For Vaultwarden (Locke) credential resolution, install separately:
196
+
197
+ ```shell
198
+ pip install "locke @ git+https://gitlab.com/martin-wieser/locke.git#subdirectory=python"
199
+ ```
200
+
201
+ ## Configure
202
+
203
+ Granny reads secrets from environment variables, falling back to a
204
+ Vaultwarden vault when Locke is installed. Copy the example and fill in
205
+ what you need:
206
+
207
+ ```shell
208
+ cp .env.example .env
209
+ ```
210
+
211
+ Common keys:
212
+
213
+ | Provider | Variables |
214
+ |---|---|
215
+ | Bunny | `BUNNY_API_KEY` (+ `BUNNY_API_KEY_<CUSTOMER>` for multi-account) |
216
+ | Cloudflare | `CLOUDFLARE_API_TOKEN` |
217
+ | Hetzner | `HETZNER_S3_ACCESS_KEY`, `HETZNER_S3_SECRET_KEY`, `HETZNER_DNS_API_TOKEN` |
218
+ | Scaleway | `SCW_ACCESS_KEY`, `SCW_SECRET_KEY`, `SCW_DEFAULT_PROJECT_ID` |
219
+ | Mailjet | `MAILJET_API_KEY`, `MAILJET_SECRET_KEY` |
220
+ | deSEC | `DESEC_API_TOKEN` |
221
+ | ClouDNS | `CLOUDNS_AUTH_ID`/`_PASSWORD` (or `_SUB_AUTH_ID`/`_SUB_AUTH_USER`) |
222
+ | INWX | `INWX_USERNAME`, `INWX_PASSWORD`, `INWX_SHARED_SECRET` (only with 2FA) |
223
+ | Docker Hub | `DOCKER_HUB_USER`, `DOCKER_HUB_TOKEN` |
224
+
225
+ Set only the ones you need. Use `granny credentials status` to verify
226
+ what's configured at any time.
227
+
228
+ ## Use
229
+
230
+ ```shell
231
+ granny --help # see every command group
232
+ granny <group> --help # drill into one
233
+ ```
234
+
235
+ The CLI is organized by capability, not by provider. You pick the
236
+ provider per command:
237
+
238
+ ```shell
239
+ # DNS — same command, any of seven providers
240
+ granny dns list example.com --provider cloudflare
241
+ granny dns add www.example.com --type CNAME --value example.com --provider bunny
242
+ granny dns nameservers example.com --provider inwx
243
+
244
+ # CDN — Bunny pull zones
245
+ granny cdn list-zones
246
+ granny cdn purge 12345
247
+ granny cdn ssl www.example.com --dns01
248
+
249
+ # Object storage — three providers, one verb set
250
+ granny storage bunny create my-assets --region DE
251
+ granny storage hetzner create my-bucket --region fsn1 --public
252
+ granny storage aws create my-website --website
253
+
254
+ # AWS inventory (read-only)
255
+ granny analyze vpcs --json-output
256
+ granny analyze lambdas --region us-east-1 eu-west-1
257
+
258
+ # Cross-cloud GPU / credit / cost inventory (AWS + GCP + Azure)
259
+ granny analyze gpus # running GPUs everywhere
260
+ granny analyze gpus --filter h100,h200 # find Hopper clusters
261
+ granny analyze gpus --include-reserved # also capacity blocks / RIs
262
+ granny analyze gpus --provider aws --profile prod --profile dev
263
+ granny analyze credits # available balances
264
+ granny analyze costs # MTD + month-end forecast
265
+
266
+ # AWS Capacity Blocks for ML -- discover available H100/H200/A100 blocks
267
+ granny analyze capacity-blocks --instance-type p5.48xlarge --hours 24
268
+ granny analyze capacity-blocks --instance-type p5e.48xlarge --count 2 --hours 168
269
+
270
+ # Cloudflare account resources (Workers, D1, R2, KV)
271
+ granny cloudflare d1 create my-app
272
+ granny cloudflare r2 create my-app-media
273
+ granny cloudflare site provision my-app --secret-from-vault MAILJET_API_KEY
274
+
275
+ # Multi-arch Docker builds with deterministic tags
276
+ granny docker build-base --image myapp-base --hash-file requirements.txt
277
+
278
+ # Scaleway FaaS
279
+ granny serverless deploy my-fn --source-dir ./dist --namespace my-app
280
+
281
+ # Mailjet + WorkMail
282
+ granny email mailjet setup-dns example.com
283
+ granny email workmail create-user example.com --email user@example.com
284
+
285
+ # One-shot infrastructure provisioning (dispatches granny/create/*.py)
286
+ granny create s3-website example.com --help
287
+ granny create scaleway-container --name my-app --port 3000
288
+ granny create mailjet-dns example.com
289
+ ```
290
+
291
+ ## Capability matrix
292
+
293
+ | Capability | Providers |
294
+ |---|---|
295
+ | DNS | Cloudflare, Bunny, Hetzner, deSEC, ClouDNS, INWX, manual |
296
+ | CDN | Bunny |
297
+ | Edge scripting | Bunny |
298
+ | Object storage | AWS S3, Bunny Storage, Hetzner S3 |
299
+ | Serverless functions | Scaleway FaaS, Scaleway Containers |
300
+ | Workers / KV / D1 / R2 | Cloudflare |
301
+ | Email send infra | Mailjet, AWS SES, AWS WorkMail |
302
+ | AWS inventory | VPCs, Lambdas |
303
+ | Cross-cloud inventory | GPU instances + reservations, credit balances, MTD spend + forecast (AWS, GCP, Azure) |
304
+ | SSL automation | Bunny, Cloudflare, ACM |
305
+
306
+ ## As a library
307
+
308
+ Every CLI command is a thin wrapper. Import the underlying module when
309
+ the CLI doesn't have what you need:
310
+
311
+ ```python
312
+ from granny.dns.factory import get_provider
313
+ from granny.cloudflare.d1 import D1Client
314
+ from granny.credentials import get_secret, load_secrets_into_env
315
+
316
+ # Multi-provider DNS
317
+ dns = get_provider("inwx")
318
+ zone = dns.get_zone_id("example.com")
319
+ dns.upsert_record(zone, "_acme-challenge", "TXT", "abc-token", ttl=300)
320
+
321
+ # Cloudflare D1 directly
322
+ db = D1Client().create_database("my-app", primary_location_hint="weur")
323
+
324
+ # Bulk-load registered secrets into os.environ for downstream tools
325
+ load_secrets_into_env()
326
+ ```
327
+
328
+ ## Project layout
329
+
330
+ ```
331
+ granny/
332
+ cli/ Click command groups (granny <group> <verb>)
333
+ analyze/ Cross-cloud inventory (AWS, GCP, Azure)
334
+ cdn/ Bunny CDN
335
+ cloudflare/ Cloudflare Workers / D1 / R2 / KV
336
+ create/ Standalone setup scripts (granny create <name>)
337
+ credentials/ Env + vault secret resolution
338
+ dns/ Provider-agnostic DNS CRUD
339
+ docker/ Multi-arch image builds
340
+ edge/ Bunny Edge Scripting
341
+ email/ Mailjet, WorkMail, SES forwarding
342
+ serverless/ Scaleway FaaS
343
+ storage/ Object storage (AWS / Bunny / Hetzner)
344
+ ```
345
+
346
+ ## Where to look next
347
+
348
+ - [`Project_Guidelines.md`](Project_Guidelines.md) — contributor toolchain,
349
+ conventions, extension checklists, release flow.
350
+ - [`AGENTS.md`](AGENTS.md) — instructions for AI coding agents working
351
+ in the repository (Claude Code reads `CLAUDE.md` which imports this).
352
+ - `.env.example` — full list of supported environment variables.
353
+ - `granny --help` and `granny <group> --help` — authoritative command docs,
354
+ always in sync with the installed version.
355
+
356
+ ## License
357
+
358
+ MIT — see [`LICENSE`](LICENSE). Built and maintained by Martin Wieser.
@@ -0,0 +1,226 @@
1
+ # Granny
2
+
3
+ > One CLI for the cloud chores you'd otherwise do across six tabs.
4
+
5
+ `granny` is a pragmatic, multi-provider DevOps toolkit with a strong bias
6
+ toward European cloud infrastructure. It wraps the parts of provider APIs
7
+ you actually use day-to-day — Bunny pull zones, Cloudflare DNS, Hetzner S3,
8
+ Scaleway functions, Mailjet sender setup, INWX zones, AWS Lambda inventory
9
+ — behind a single command:
10
+
11
+ ```shell
12
+ granny dns add api.example.com --type A --value 203.0.113.4 --provider hetzner
13
+ granny cdn purge 12345
14
+ granny storage bunny upload my-zone ./dist
15
+ granny credentials status
16
+ ```
17
+
18
+ ## What it is
19
+
20
+ - **Pragmatic, not exhaustive.** Granny implements the parts of each
21
+ provider API the maintainers needed in production. It will not cover
22
+ every endpoint of every service — and it will not pretend to. Add what
23
+ you need; the [contributor guide](Project_Guidelines.md) explains how.
24
+ - **Multi-cloud, with a European tilt.** First-class support for Bunny,
25
+ Cloudflare, Hetzner, deSEC, ClouDNS, INWX, Scaleway, and Mailjet
26
+ alongside AWS S3 / Lambda / WorkMail. The cloud world doesn't end at
27
+ AWS, and granny doesn't pretend it does.
28
+ - **Secrets done right.** Every credential goes through one resolver
29
+ chain — environment variable first, optional Vaultwarden vault second,
30
+ never hardcoded. `.env` and `.deploy.env` are auto-loaded; vault support
31
+ is a one-line activation when you want it.
32
+ - **Library or CLI.** Every CLI subcommand is a thin shim over a small
33
+ Python module. Import `granny.dns.cloudflare`, `granny.cdn.bunny`,
34
+ `granny.cloudflare.d1` directly when you need to script something the
35
+ CLI doesn't expose yet.
36
+ - **Small, composable, no plugin system.** The whole package is one
37
+ `pip install` away. No daemon, no service, no opinionated framework.
38
+ Drop it into your CI image and call it a day.
39
+
40
+ ## Install
41
+
42
+ ```shell
43
+ pip install granny-devops
44
+ # or
45
+ uv add granny-devops
46
+ ```
47
+
48
+ Optional extras enable additional providers:
49
+
50
+ ```shell
51
+ pip install "granny-devops[gcp]" # GCP for granny analyze (gpus|credits|costs)
52
+ pip install "granny-devops[azure]" # Azure for granny analyze (gpus|credits|costs)
53
+ pip install "granny-devops[cdn]" # Cloudflare, Hetzner DNS
54
+ ```
55
+
56
+ Tagged releases land on **pypi.org** and the public GitLab PyPI registry
57
+ in parallel. If PyPI is propagating slowly, fall back to the registry:
58
+
59
+ ```shell
60
+ pip install --extra-index-url https://gitlab.com/api/v4/projects/81189862/packages/pypi/simple granny-devops
61
+ ```
62
+
63
+ For Vaultwarden (Locke) credential resolution, install separately:
64
+
65
+ ```shell
66
+ pip install "locke @ git+https://gitlab.com/martin-wieser/locke.git#subdirectory=python"
67
+ ```
68
+
69
+ ## Configure
70
+
71
+ Granny reads secrets from environment variables, falling back to a
72
+ Vaultwarden vault when Locke is installed. Copy the example and fill in
73
+ what you need:
74
+
75
+ ```shell
76
+ cp .env.example .env
77
+ ```
78
+
79
+ Common keys:
80
+
81
+ | Provider | Variables |
82
+ |---|---|
83
+ | Bunny | `BUNNY_API_KEY` (+ `BUNNY_API_KEY_<CUSTOMER>` for multi-account) |
84
+ | Cloudflare | `CLOUDFLARE_API_TOKEN` |
85
+ | Hetzner | `HETZNER_S3_ACCESS_KEY`, `HETZNER_S3_SECRET_KEY`, `HETZNER_DNS_API_TOKEN` |
86
+ | Scaleway | `SCW_ACCESS_KEY`, `SCW_SECRET_KEY`, `SCW_DEFAULT_PROJECT_ID` |
87
+ | Mailjet | `MAILJET_API_KEY`, `MAILJET_SECRET_KEY` |
88
+ | deSEC | `DESEC_API_TOKEN` |
89
+ | ClouDNS | `CLOUDNS_AUTH_ID`/`_PASSWORD` (or `_SUB_AUTH_ID`/`_SUB_AUTH_USER`) |
90
+ | INWX | `INWX_USERNAME`, `INWX_PASSWORD`, `INWX_SHARED_SECRET` (only with 2FA) |
91
+ | Docker Hub | `DOCKER_HUB_USER`, `DOCKER_HUB_TOKEN` |
92
+
93
+ Set only the ones you need. Use `granny credentials status` to verify
94
+ what's configured at any time.
95
+
96
+ ## Use
97
+
98
+ ```shell
99
+ granny --help # see every command group
100
+ granny <group> --help # drill into one
101
+ ```
102
+
103
+ The CLI is organized by capability, not by provider. You pick the
104
+ provider per command:
105
+
106
+ ```shell
107
+ # DNS — same command, any of seven providers
108
+ granny dns list example.com --provider cloudflare
109
+ granny dns add www.example.com --type CNAME --value example.com --provider bunny
110
+ granny dns nameservers example.com --provider inwx
111
+
112
+ # CDN — Bunny pull zones
113
+ granny cdn list-zones
114
+ granny cdn purge 12345
115
+ granny cdn ssl www.example.com --dns01
116
+
117
+ # Object storage — three providers, one verb set
118
+ granny storage bunny create my-assets --region DE
119
+ granny storage hetzner create my-bucket --region fsn1 --public
120
+ granny storage aws create my-website --website
121
+
122
+ # AWS inventory (read-only)
123
+ granny analyze vpcs --json-output
124
+ granny analyze lambdas --region us-east-1 eu-west-1
125
+
126
+ # Cross-cloud GPU / credit / cost inventory (AWS + GCP + Azure)
127
+ granny analyze gpus # running GPUs everywhere
128
+ granny analyze gpus --filter h100,h200 # find Hopper clusters
129
+ granny analyze gpus --include-reserved # also capacity blocks / RIs
130
+ granny analyze gpus --provider aws --profile prod --profile dev
131
+ granny analyze credits # available balances
132
+ granny analyze costs # MTD + month-end forecast
133
+
134
+ # AWS Capacity Blocks for ML -- discover available H100/H200/A100 blocks
135
+ granny analyze capacity-blocks --instance-type p5.48xlarge --hours 24
136
+ granny analyze capacity-blocks --instance-type p5e.48xlarge --count 2 --hours 168
137
+
138
+ # Cloudflare account resources (Workers, D1, R2, KV)
139
+ granny cloudflare d1 create my-app
140
+ granny cloudflare r2 create my-app-media
141
+ granny cloudflare site provision my-app --secret-from-vault MAILJET_API_KEY
142
+
143
+ # Multi-arch Docker builds with deterministic tags
144
+ granny docker build-base --image myapp-base --hash-file requirements.txt
145
+
146
+ # Scaleway FaaS
147
+ granny serverless deploy my-fn --source-dir ./dist --namespace my-app
148
+
149
+ # Mailjet + WorkMail
150
+ granny email mailjet setup-dns example.com
151
+ granny email workmail create-user example.com --email user@example.com
152
+
153
+ # One-shot infrastructure provisioning (dispatches granny/create/*.py)
154
+ granny create s3-website example.com --help
155
+ granny create scaleway-container --name my-app --port 3000
156
+ granny create mailjet-dns example.com
157
+ ```
158
+
159
+ ## Capability matrix
160
+
161
+ | Capability | Providers |
162
+ |---|---|
163
+ | DNS | Cloudflare, Bunny, Hetzner, deSEC, ClouDNS, INWX, manual |
164
+ | CDN | Bunny |
165
+ | Edge scripting | Bunny |
166
+ | Object storage | AWS S3, Bunny Storage, Hetzner S3 |
167
+ | Serverless functions | Scaleway FaaS, Scaleway Containers |
168
+ | Workers / KV / D1 / R2 | Cloudflare |
169
+ | Email send infra | Mailjet, AWS SES, AWS WorkMail |
170
+ | AWS inventory | VPCs, Lambdas |
171
+ | Cross-cloud inventory | GPU instances + reservations, credit balances, MTD spend + forecast (AWS, GCP, Azure) |
172
+ | SSL automation | Bunny, Cloudflare, ACM |
173
+
174
+ ## As a library
175
+
176
+ Every CLI command is a thin wrapper. Import the underlying module when
177
+ the CLI doesn't have what you need:
178
+
179
+ ```python
180
+ from granny.dns.factory import get_provider
181
+ from granny.cloudflare.d1 import D1Client
182
+ from granny.credentials import get_secret, load_secrets_into_env
183
+
184
+ # Multi-provider DNS
185
+ dns = get_provider("inwx")
186
+ zone = dns.get_zone_id("example.com")
187
+ dns.upsert_record(zone, "_acme-challenge", "TXT", "abc-token", ttl=300)
188
+
189
+ # Cloudflare D1 directly
190
+ db = D1Client().create_database("my-app", primary_location_hint="weur")
191
+
192
+ # Bulk-load registered secrets into os.environ for downstream tools
193
+ load_secrets_into_env()
194
+ ```
195
+
196
+ ## Project layout
197
+
198
+ ```
199
+ granny/
200
+ cli/ Click command groups (granny <group> <verb>)
201
+ analyze/ Cross-cloud inventory (AWS, GCP, Azure)
202
+ cdn/ Bunny CDN
203
+ cloudflare/ Cloudflare Workers / D1 / R2 / KV
204
+ create/ Standalone setup scripts (granny create <name>)
205
+ credentials/ Env + vault secret resolution
206
+ dns/ Provider-agnostic DNS CRUD
207
+ docker/ Multi-arch image builds
208
+ edge/ Bunny Edge Scripting
209
+ email/ Mailjet, WorkMail, SES forwarding
210
+ serverless/ Scaleway FaaS
211
+ storage/ Object storage (AWS / Bunny / Hetzner)
212
+ ```
213
+
214
+ ## Where to look next
215
+
216
+ - [`Project_Guidelines.md`](Project_Guidelines.md) — contributor toolchain,
217
+ conventions, extension checklists, release flow.
218
+ - [`AGENTS.md`](AGENTS.md) — instructions for AI coding agents working
219
+ in the repository (Claude Code reads `CLAUDE.md` which imports this).
220
+ - `.env.example` — full list of supported environment variables.
221
+ - `granny --help` and `granny <group> --help` — authoritative command docs,
222
+ always in sync with the installed version.
223
+
224
+ ## License
225
+
226
+ MIT — see [`LICENSE`](LICENSE). Built and maintained by Martin Wieser.
@@ -1,6 +1,6 @@
1
1
  """Granny -- Cloud tools collection for AWS infrastructure and DevOps automation."""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.8.0"
4
4
  __all__ = [
5
5
  "get_secret",
6
6
  "load_secrets_into_env",
@@ -0,0 +1,43 @@
1
+ """Cloud resource analysis and inventory (AWS, GCP, Azure)."""
2
+
3
+ from granny.analyze.lambdas import get_all_regions, list_lambdas
4
+ from granny.analyze.vpcs import list_vpcs
5
+
6
+ __all__ = [
7
+ "get_all_regions",
8
+ "list_lambdas",
9
+ "list_vpcs",
10
+ ]
11
+
12
+
13
+ def __getattr__(name: str):
14
+ # Lazy re-exports so importing `granny.analyze` doesn't drag in boto3
15
+ # for the new cross-cloud helpers, and so optional GCP/Azure SDKs aren't
16
+ # required for AWS-only callers.
17
+ if name in {
18
+ "GpuInstance",
19
+ "GpuReservation",
20
+ "CapacityBlockOffering",
21
+ "aws_profiles",
22
+ "list_aws_gpu_instances",
23
+ "list_aws_gpu_reservations",
24
+ "list_aws_capacity_block_offerings",
25
+ "gcp_projects",
26
+ "list_gcp_gpu_instances",
27
+ "list_gcp_gpu_reservations",
28
+ "azure_subscriptions",
29
+ "list_azure_gpu_instances",
30
+ "list_azure_gpu_reservations",
31
+ }:
32
+ from granny.analyze import gpus
33
+
34
+ return getattr(gpus, name)
35
+ if name in {"CreditBalance", "aws_credits", "gcp_credits", "azure_credits"}:
36
+ from granny.analyze import credits
37
+
38
+ return getattr(credits, name)
39
+ if name in {"CostSummary", "aws_costs", "gcp_costs", "azure_costs"}:
40
+ from granny.analyze import costs
41
+
42
+ return getattr(costs, name)
43
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")