granny-devops 0.19.3__tar.gz → 0.20.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.19.3 → granny_devops-0.20.0}/PKG-INFO +1 -1
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/credentials/secrets.py +90 -1
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/storage/hetzner.py +49 -12
- {granny_devops-0.19.3 → granny_devops-0.20.0}/pyproject.toml +1 -1
- {granny_devops-0.19.3 → granny_devops-0.20.0}/.gitignore +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/LICENSE +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/README.md +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/airtable/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/airtable/client.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/analyze/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/analyze/costs.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/analyze/credits.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/analyze/gpu_pricing.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/analyze/gpus.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/analyze/kubernetes.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/analyze/lambdas.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/analyze/vpcs.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/authentik/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/authentik/client.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/authentik/provision.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/aws/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/aws/bedrock.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/aws/quota.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/azure/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/azure/_client.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/azure/account.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/azure/deployment.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/azure/openai.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/azure/quota.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/azure/resourcegroup.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/azure/role.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/azure/sku.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/azure/vm.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/azure/webapp.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/bunny/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/bunny/billing.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cdn/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cdn/bunny.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/airtable.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/analyze.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/authentik.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/aws.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/azure.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/cdn.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/cloudflare.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/create.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/credentials.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/dns.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/docker.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/edge.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/elk.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/email.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/indexing.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/knative.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/main.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/oauth.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/serverless.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/storage.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cli/zitadel.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cloudflare/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cloudflare/d1.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cloudflare/r2.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/cloudflare/workers.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/auto_certificate.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/cloudfront-security-headers.js +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/letsencrypt_cert.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/manage-dns.sh +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/manage_mailjet_contacts.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/registrars.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/setup_aws_cloudfront.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/setup_bunny_edge_script.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/setup_bunny_storage.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/setup_cognito_identity_pool.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/setup_hetzner_bunny.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/setup_mailjet_dns.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/setup_private_cdn.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/setup_s3_website.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/setup_scaleway_container.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/setup_scaleway_faas.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/setup_workmail.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/create/www-redirect-function.js +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/credentials/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/dns/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/dns/base.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/dns/bunny.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/dns/cloudflare.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/dns/cloudns.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/dns/desec.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/dns/factory.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/dns/hetzner.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/dns/inwx.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/dns/manual.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/dns/records.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/docker/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/docker/build_base.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/edge/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/edge/bunny.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/elk/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/elk/client.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/email/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/email/mailjet.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/email/mailjet_contacts.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/email/ses_forwarding.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/email/stalwart.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/email/workmail.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/indexing/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/indexing/google.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/indexing/indexnow.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/knative/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/knative/client.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/oauth/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/oauth/productivity.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/report.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/scaleway/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/scaleway/billing.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/serverless/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/serverless/scaleway.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/storage/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/storage/aws.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/storage/bunny.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/zitadel/__init__.py +0 -0
- {granny_devops-0.19.3 → granny_devops-0.20.0}/granny/zitadel/client.py +0 -0
|
@@ -5,6 +5,9 @@ Resolution order for each secret:
|
|
|
5
5
|
1. Environment variable (from ``.env``, ``.deploy.env``, or shell) — explicit wins
|
|
6
6
|
2. Vaultwarden vault via Locke — only if the optional ``[vault]`` extra is
|
|
7
7
|
installed and a vault session is reachable
|
|
8
|
+
3. Locke OS keystore — including the ``OBACHAN_<ENV>_<NAME>`` entries obachan
|
|
9
|
+
stores, so a session that cannot reach Vaultwarden (locked workstation,
|
|
10
|
+
non-interactive shell where Windows Hello cannot prompt) still resolves
|
|
8
11
|
|
|
9
12
|
Without the ``[vault]`` extra, secrets are env-var only — the vault-related
|
|
10
13
|
helpers are no-ops that return ``None``. Install with vault support via::
|
|
@@ -196,6 +199,33 @@ SECRET_MAP: dict[str, str] = {
|
|
|
196
199
|
"OBACHAN_BAO_TOKEN": "obachan-bao-token",
|
|
197
200
|
}
|
|
198
201
|
|
|
202
|
+
# Map env var name -> the name the same secret carries in the Locke OS keystore.
|
|
203
|
+
#
|
|
204
|
+
# Vaultwarden is not the only place these live. obachan stores the same
|
|
205
|
+
# infrastructure credentials in the OS keystore under its own namespace,
|
|
206
|
+
# ``OBACHAN_<ENV>_<NAME>`` (see obachan's runners/locke_resolver._namespaced),
|
|
207
|
+
# and its object-storage pair is documented as holding Hetzner Object Storage
|
|
208
|
+
# credentials. A session that cannot reach Vaultwarden -- a locked
|
|
209
|
+
# workstation, a non-interactive shell where Windows Hello cannot prompt, a
|
|
210
|
+
# stale locke pin -- can still answer from the keystore, so it is worth asking
|
|
211
|
+
# before giving up.
|
|
212
|
+
#
|
|
213
|
+
# Values are the un-namespaced obachan name; ``_keystore_candidates`` expands
|
|
214
|
+
# them across environments.
|
|
215
|
+
KEYSTORE_ALIASES: dict[str, str] = {
|
|
216
|
+
"HETZNER_S3_ACCESS_KEY": "S3_ACCESS_KEY",
|
|
217
|
+
"HETZNER_S3_SECRET_KEY": "S3_SECRET_KEY",
|
|
218
|
+
"CLOUDFLARE_API_TOKEN": "CLOUDFLARE_API_TOKEN",
|
|
219
|
+
"INWX_USERNAME": "INWX_USERNAME",
|
|
220
|
+
"INWX_PASSWORD": "INWX_PASSWORD",
|
|
221
|
+
"INWX_SHARED_SECRET": "INWX_SHARED_SECRET",
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#: Environments to try, in order, when expanding a keystore alias. Override
|
|
225
|
+
#: with ``GRANNY_KEYSTORE_ENV`` to pin one -- worth doing deliberately, since
|
|
226
|
+
#: the default reaches production credentials first.
|
|
227
|
+
KEYSTORE_ENV_ORDER = ("prod", "prod1", "staging", "dev2")
|
|
228
|
+
|
|
199
229
|
# Module-level cache so vault is only contacted once per process
|
|
200
230
|
_cache: dict[str, str | None] = {}
|
|
201
231
|
_vault_client = None
|
|
@@ -285,13 +315,62 @@ def _fetch_from_vault(env_var: str) -> str | None:
|
|
|
285
315
|
return None
|
|
286
316
|
|
|
287
317
|
|
|
318
|
+
def _keystore_candidates(env_var: str) -> list[str]:
|
|
319
|
+
"""Names to try in the Locke keystore for ``env_var``, most specific first.
|
|
320
|
+
|
|
321
|
+
The credential's own name comes first: a keystore entry stored under
|
|
322
|
+
exactly what the caller asked for should always win over a namespaced
|
|
323
|
+
guess.
|
|
324
|
+
"""
|
|
325
|
+
candidates = [env_var]
|
|
326
|
+
alias = KEYSTORE_ALIASES.get(env_var)
|
|
327
|
+
if alias:
|
|
328
|
+
pinned = os.environ.get("GRANNY_KEYSTORE_ENV", "").strip()
|
|
329
|
+
envs = (pinned,) if pinned else KEYSTORE_ENV_ORDER
|
|
330
|
+
candidates.extend(f"OBACHAN_{env.upper()}_{alias}" for env in envs if env)
|
|
331
|
+
return candidates
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _fetch_from_keystore(env_var: str) -> str | None:
|
|
335
|
+
"""Resolve a secret from the Locke OS keystore.
|
|
336
|
+
|
|
337
|
+
Returns None when locke is absent or nothing matches. A keystore that
|
|
338
|
+
cannot be *consulted* -- biometric gating with no way to prompt -- is
|
|
339
|
+
reported as a miss here rather than raised: the vault backend below may
|
|
340
|
+
still answer, and this backend exists to add a source, never to remove one.
|
|
341
|
+
"""
|
|
342
|
+
try:
|
|
343
|
+
from locke.keystore import get_credential
|
|
344
|
+
except ImportError:
|
|
345
|
+
logger.debug("locke not installed — keystore backend disabled")
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
for name in _keystore_candidates(env_var):
|
|
349
|
+
try:
|
|
350
|
+
value = get_credential(name)
|
|
351
|
+
except Exception as exc: # keystore unavailable, biometric denied, ...
|
|
352
|
+
logger.debug("Keystore lookup failed for %s: %s", name, exc)
|
|
353
|
+
return None
|
|
354
|
+
if value:
|
|
355
|
+
if name != env_var:
|
|
356
|
+
# Say which entry answered. A credential arriving under a
|
|
357
|
+
# namespaced alias -- possibly a production one -- should
|
|
358
|
+
# never be silent about where it came from.
|
|
359
|
+
logger.info(
|
|
360
|
+
"Resolved %s from OS keystore entry %r", env_var, name
|
|
361
|
+
)
|
|
362
|
+
return value
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
|
|
288
366
|
def get_secret(env_var: str) -> str | None:
|
|
289
367
|
"""Get a secret by its environment variable name.
|
|
290
368
|
|
|
291
369
|
Resolution order:
|
|
292
|
-
1. Process cache (avoids repeated
|
|
370
|
+
1. Process cache (avoids repeated lookups)
|
|
293
371
|
2. Environment variable / .env / .deploy.env (explicit wins)
|
|
294
372
|
3. Vaultwarden vault via Locke (only if [vault] extra installed)
|
|
373
|
+
4. Locke OS keystore, including obachan-namespaced aliases
|
|
295
374
|
|
|
296
375
|
Args:
|
|
297
376
|
env_var: The environment variable name (e.g. "BUNNY_API_KEY").
|
|
@@ -315,6 +394,16 @@ def get_secret(env_var: str) -> str | None:
|
|
|
315
394
|
_cache[env_var] = value
|
|
316
395
|
return value
|
|
317
396
|
|
|
397
|
+
# Keystore last, so this backend only ever adds an answer where there was
|
|
398
|
+
# none. Placing it ahead of Vaultwarden would quietly re-point every
|
|
399
|
+
# already-working secret at a different store, which is not a change worth
|
|
400
|
+
# making as a side effect of teaching granny one new lookup.
|
|
401
|
+
if env_var in SECRET_MAP or env_var in KEYSTORE_ALIASES:
|
|
402
|
+
value = _fetch_from_keystore(env_var)
|
|
403
|
+
if value:
|
|
404
|
+
_cache[env_var] = value
|
|
405
|
+
return value
|
|
406
|
+
|
|
318
407
|
return None
|
|
319
408
|
|
|
320
409
|
|
|
@@ -10,6 +10,7 @@ import time
|
|
|
10
10
|
|
|
11
11
|
import boto3
|
|
12
12
|
from botocore.config import Config
|
|
13
|
+
from botocore.exceptions import ClientError
|
|
13
14
|
|
|
14
15
|
from granny.credentials.secrets import get_secret
|
|
15
16
|
|
|
@@ -82,20 +83,56 @@ class HetznerS3Client:
|
|
|
82
83
|
logger.info("Set public-read policy on %s", bucket_name)
|
|
83
84
|
|
|
84
85
|
def set_cors_config(
|
|
85
|
-
self,
|
|
86
|
+
self,
|
|
87
|
+
bucket_name: str,
|
|
88
|
+
origins: list[str] | None = None,
|
|
89
|
+
methods: list[str] | None = None,
|
|
90
|
+
expose_headers: list[str] | None = None,
|
|
91
|
+
max_age_seconds: int = 3600,
|
|
86
92
|
) -> None:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
"""Replace the bucket's CORS policy.
|
|
94
|
+
|
|
95
|
+
``methods`` defaults to read-only access. Pass ``PUT`` for a bucket
|
|
96
|
+
that browsers upload to directly: an app handed a presigned URL sends
|
|
97
|
+
the bytes from the page itself, so the hop is cross-origin, and a PUT
|
|
98
|
+
is never a CORS-simple request. The browser preflights it, and without
|
|
99
|
+
a matching rule it refuses to send the upload at all -- surfacing as a
|
|
100
|
+
bare network error with no status code, which reads like a
|
|
101
|
+
connectivity fault rather than a policy one.
|
|
102
|
+
|
|
103
|
+
``expose_headers`` is what the page is allowed to read back; ``ETag``
|
|
104
|
+
is the one a multipart or resumable upload needs.
|
|
105
|
+
"""
|
|
106
|
+
rule: dict[str, object] = {
|
|
107
|
+
"AllowedHeaders": ["*"],
|
|
108
|
+
"AllowedMethods": methods or ["GET", "HEAD"],
|
|
109
|
+
"AllowedOrigins": origins or ["*"],
|
|
110
|
+
"MaxAgeSeconds": max_age_seconds,
|
|
96
111
|
}
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
if expose_headers:
|
|
113
|
+
rule["ExposeHeaders"] = expose_headers
|
|
114
|
+
self.client.put_bucket_cors(
|
|
115
|
+
Bucket=bucket_name, CORSConfiguration={"CORSRules": [rule]}
|
|
116
|
+
)
|
|
117
|
+
logger.info(
|
|
118
|
+
"CORS configured on %s (methods=%s, origins=%s)",
|
|
119
|
+
bucket_name,
|
|
120
|
+
rule["AllowedMethods"],
|
|
121
|
+
rule["AllowedOrigins"],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def get_cors_config(self, bucket_name: str) -> list[dict] | None:
|
|
125
|
+
"""Return the bucket's CORS rules, or None when it has no policy.
|
|
126
|
+
|
|
127
|
+
The distinction matters: an empty list is a policy that allows
|
|
128
|
+
nothing, while None means the store has never been told anything.
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
return self.client.get_bucket_cors(Bucket=bucket_name).get("CORSRules", [])
|
|
132
|
+
except ClientError as exc:
|
|
133
|
+
if exc.response.get("Error", {}).get("Code") == "NoSuchCORSConfiguration":
|
|
134
|
+
return None
|
|
135
|
+
raise
|
|
99
136
|
|
|
100
137
|
def upload_file(
|
|
101
138
|
self,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|