granny-devops 0.17.0__tar.gz → 0.17.1__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.17.0 → granny_devops-0.17.1}/PKG-INFO +1 -1
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/__init__.py +1 -1
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/zitadel.py +177 -7
- {granny_devops-0.17.0 → granny_devops-0.17.1}/pyproject.toml +1 -1
- {granny_devops-0.17.0 → granny_devops-0.17.1}/.gitignore +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/LICENSE +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/README.md +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/analyze/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/analyze/costs.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/analyze/credits.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/analyze/gpu_pricing.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/analyze/gpus.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/analyze/lambdas.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/analyze/vpcs.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/authentik/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/authentik/client.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/authentik/provision.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/aws/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/aws/bedrock.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/aws/quota.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/azure/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/azure/_client.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/azure/account.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/azure/deployment.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/azure/openai.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/azure/quota.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/azure/resourcegroup.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/azure/role.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/azure/sku.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/azure/vm.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/azure/webapp.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cdn/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cdn/bunny.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/analyze.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/authentik.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/aws.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/azure.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/cdn.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/cloudflare.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/create.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/credentials.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/dns.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/docker.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/edge.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/elk.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/email.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/indexing.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/main.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/serverless.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cli/storage.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cloudflare/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cloudflare/d1.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cloudflare/r2.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/cloudflare/workers.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/auto_certificate.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/cloudfront-security-headers.js +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/letsencrypt_cert.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/manage-dns.sh +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/manage_mailjet_contacts.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/registrars.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/setup_aws_cloudfront.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/setup_bunny_edge_script.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/setup_bunny_storage.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/setup_cognito_identity_pool.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/setup_hetzner_bunny.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/setup_mailjet_dns.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/setup_private_cdn.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/setup_s3_website.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/setup_scaleway_container.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/setup_scaleway_faas.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/setup_workmail.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/create/www-redirect-function.js +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/credentials/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/credentials/secrets.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/dns/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/dns/base.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/dns/bunny.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/dns/cloudflare.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/dns/cloudns.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/dns/desec.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/dns/factory.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/dns/hetzner.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/dns/inwx.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/dns/manual.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/dns/records.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/docker/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/docker/build_base.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/edge/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/edge/bunny.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/elk/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/elk/client.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/email/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/email/mailjet.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/email/mailjet_contacts.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/email/ses_forwarding.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/email/stalwart.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/email/workmail.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/indexing/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/indexing/google.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/indexing/indexnow.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/report.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/serverless/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/serverless/scaleway.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/storage/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/storage/aws.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/storage/bunny.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/storage/hetzner.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/zitadel/__init__.py +0 -0
- {granny_devops-0.17.0 → granny_devops-0.17.1}/granny/zitadel/client.py +0 -0
|
@@ -8,7 +8,10 @@ env/vault pipeline. See :class:`granny.zitadel.ZitadelClient`.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
|
+
import secrets
|
|
12
|
+
import string
|
|
11
13
|
import sys
|
|
14
|
+
from pathlib import Path
|
|
12
15
|
|
|
13
16
|
import click
|
|
14
17
|
|
|
@@ -23,6 +26,35 @@ def _client() -> ZitadelClient:
|
|
|
23
26
|
sys.exit(1)
|
|
24
27
|
|
|
25
28
|
|
|
29
|
+
def _client_for(domain: str, key_file: str | None, pat: str | None) -> ZitadelClient:
|
|
30
|
+
"""Build a client for an EXPLICIT instance (SA key file or PAT).
|
|
31
|
+
|
|
32
|
+
Unlike :func:`_client` (``from_environment``), this never falls back to the
|
|
33
|
+
ambient/locke credential — required when addressing a specific instance
|
|
34
|
+
(e.g. the dev Zitadel) rather than whatever the environment defaults to.
|
|
35
|
+
"""
|
|
36
|
+
if key_file:
|
|
37
|
+
try:
|
|
38
|
+
key = json.loads(Path(key_file).read_text(encoding="utf-8"))
|
|
39
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
40
|
+
raise click.ClickException(f"cannot read SA key {key_file}: {exc}") from exc
|
|
41
|
+
return ZitadelClient.from_service_account(key, domain)
|
|
42
|
+
if pat:
|
|
43
|
+
return ZitadelClient(base_url=domain, token=pat)
|
|
44
|
+
raise click.ClickException(
|
|
45
|
+
f"provide a service-account key file or PAT for {domain}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _generate_temp_password() -> str:
|
|
50
|
+
"""Strong, policy-safe temp password: 4 upper + 4 lower + 3 digit + 2 symbol, shuffled."""
|
|
51
|
+
pools = (string.ascii_uppercase, string.ascii_lowercase, string.digits, "!@#$%*-_?")
|
|
52
|
+
counts = (4, 4, 3, 2)
|
|
53
|
+
chars = [secrets.choice(p) for p, n in zip(pools, counts) for _ in range(n)]
|
|
54
|
+
secrets.SystemRandom().shuffle(chars)
|
|
55
|
+
return "".join(chars)
|
|
56
|
+
|
|
57
|
+
|
|
26
58
|
@click.group()
|
|
27
59
|
def zitadel() -> None:
|
|
28
60
|
"""Manage Zitadel (projects, OIDC apps, MFA policy) via its REST API."""
|
|
@@ -355,11 +387,15 @@ def create_pat_cmd(user: str, expiration: str | None, dry_run: bool) -> None:
|
|
|
355
387
|
at vault `granny/infra/zitadel-api-token` (or export ZITADEL_API_TOKEN).
|
|
356
388
|
"""
|
|
357
389
|
if dry_run:
|
|
358
|
-
click.echo(
|
|
390
|
+
click.echo(
|
|
391
|
+
f"[DRY RUN] Would create a PAT for {user!r} (expiration={expiration})"
|
|
392
|
+
)
|
|
359
393
|
return
|
|
360
394
|
client = _client()
|
|
361
395
|
out = client.create_pat(_resolve_user(client, user), expiration_date=expiration)
|
|
362
|
-
click.echo(
|
|
396
|
+
click.echo(
|
|
397
|
+
json.dumps({"tokenId": out.get("tokenId"), "token": out.get("token")}, indent=2)
|
|
398
|
+
)
|
|
363
399
|
|
|
364
400
|
|
|
365
401
|
@zitadel.command("revoke-pat")
|
|
@@ -373,20 +409,22 @@ def revoke_pat_cmd(user: str, token_id: str, dry_run: bool) -> None:
|
|
|
373
409
|
return
|
|
374
410
|
client = _client()
|
|
375
411
|
client.delete_pat(_resolve_user(client, user), token_id)
|
|
376
|
-
click.echo(
|
|
412
|
+
click.echo(
|
|
413
|
+
json.dumps({"user": user, "tokenId": token_id, "revoked": True}, indent=2)
|
|
414
|
+
)
|
|
377
415
|
|
|
378
416
|
|
|
379
417
|
# ── list ─────────────────────────────────────────────────────────────────
|
|
380
418
|
|
|
381
419
|
|
|
382
420
|
@zitadel.command("list")
|
|
383
|
-
@click.argument(
|
|
384
|
-
|
|
385
|
-
"--project", default=None, help="Project (id or name) — required for apps/roles."
|
|
421
|
+
@click.argument(
|
|
422
|
+
"kind", type=click.Choice(["users", "projects", "apps", "roles", "pats"])
|
|
386
423
|
)
|
|
387
424
|
@click.option(
|
|
388
|
-
"--
|
|
425
|
+
"--project", default=None, help="Project (id or name) — required for apps/roles."
|
|
389
426
|
)
|
|
427
|
+
@click.option("--user", default=None, help="User (userName or id) — required for pats.")
|
|
390
428
|
@click.option(
|
|
391
429
|
"--json", "as_json", is_flag=True, help="Emit full JSON instead of a table."
|
|
392
430
|
)
|
|
@@ -527,3 +565,135 @@ def revoke_user_role_cmd(project: str, user: str, dry_run: bool) -> None:
|
|
|
527
565
|
click.echo(
|
|
528
566
|
json.dumps({"project": project, "user": user, "revoked": True}, indent=2)
|
|
529
567
|
)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
# ── mirror users across instances ──────────────────────────────────────────
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
@zitadel.command("mirror-users")
|
|
574
|
+
@click.option(
|
|
575
|
+
"--source-domain",
|
|
576
|
+
required=True,
|
|
577
|
+
help="Source Zitadel base URL (e.g. https://id.pseekoo.io).",
|
|
578
|
+
)
|
|
579
|
+
@click.option(
|
|
580
|
+
"--source-key-file", default=None, help="Source service-account key JSON file."
|
|
581
|
+
)
|
|
582
|
+
@click.option(
|
|
583
|
+
"--source-pat", default=None, help="Source PAT (alternative to --source-key-file)."
|
|
584
|
+
)
|
|
585
|
+
@click.option(
|
|
586
|
+
"--target-domain",
|
|
587
|
+
required=True,
|
|
588
|
+
help="Target Zitadel base URL (e.g. https://id-dev.obachan.dev).",
|
|
589
|
+
)
|
|
590
|
+
@click.option(
|
|
591
|
+
"--target-key-file", default=None, help="Target service-account key JSON file."
|
|
592
|
+
)
|
|
593
|
+
@click.option(
|
|
594
|
+
"--target-pat", default=None, help="Target PAT (alternative to --target-key-file)."
|
|
595
|
+
)
|
|
596
|
+
@click.option(
|
|
597
|
+
"--password-out",
|
|
598
|
+
required=True,
|
|
599
|
+
type=click.Path(dir_okay=False),
|
|
600
|
+
help="File to WRITE generated temp passwords (TSV). Secrets go here only, never stdout.",
|
|
601
|
+
)
|
|
602
|
+
@click.option("--skip", "skip", multiple=True, help="Username to skip (repeatable).")
|
|
603
|
+
@click.option(
|
|
604
|
+
"--no-email-verified",
|
|
605
|
+
is_flag=True,
|
|
606
|
+
help="Do not pre-verify copied emails (default: mark verified).",
|
|
607
|
+
)
|
|
608
|
+
@click.option(
|
|
609
|
+
"--dry-run", is_flag=True, help="List what would be created; write nothing."
|
|
610
|
+
)
|
|
611
|
+
def mirror_users_cmd(
|
|
612
|
+
source_domain: str,
|
|
613
|
+
source_key_file: str | None,
|
|
614
|
+
source_pat: str | None,
|
|
615
|
+
target_domain: str,
|
|
616
|
+
target_key_file: str | None,
|
|
617
|
+
target_pat: str | None,
|
|
618
|
+
password_out: str,
|
|
619
|
+
skip: tuple[str, ...],
|
|
620
|
+
no_email_verified: bool,
|
|
621
|
+
dry_run: bool,
|
|
622
|
+
) -> None:
|
|
623
|
+
"""Copy HUMAN users from a source Zitadel to a target with generated temp passwords.
|
|
624
|
+
|
|
625
|
+
Each created user gets a random strong temp password with
|
|
626
|
+
``passwordChangeRequired`` (forced reset on first login). Passwords are
|
|
627
|
+
WRITTEN ONLY to ``--password-out`` (TSV), never printed. Idempotent: users
|
|
628
|
+
already present on the target are skipped; machine/service accounts are never
|
|
629
|
+
copied. Source and target are addressed EXPLICITLY (key file or PAT) so this
|
|
630
|
+
works across two instances (e.g. prod -> dev) without relying on the ambient
|
|
631
|
+
credential.
|
|
632
|
+
"""
|
|
633
|
+
src = _client_for(source_domain, source_key_file, source_pat)
|
|
634
|
+
tgt = _client_for(target_domain, target_key_file, target_pat)
|
|
635
|
+
skipset = set(skip)
|
|
636
|
+
|
|
637
|
+
src_humans: dict[str, dict[str, str | None]] = {}
|
|
638
|
+
for u in src.list_users():
|
|
639
|
+
human = u.get("human")
|
|
640
|
+
if not human:
|
|
641
|
+
continue # never copy machine/service accounts
|
|
642
|
+
username = u.get("userName") or u.get("username")
|
|
643
|
+
if not username or username in skipset:
|
|
644
|
+
continue
|
|
645
|
+
profile = human.get("profile", {})
|
|
646
|
+
src_humans[username] = {
|
|
647
|
+
"email": human.get("email", {}).get("email"),
|
|
648
|
+
"first": profile.get("firstName", ""),
|
|
649
|
+
"last": profile.get("lastName", ""),
|
|
650
|
+
"display": profile.get("displayName"),
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
existing = {u.get("userName") or u.get("username") for u in tgt.list_users()}
|
|
654
|
+
todo = {un: info for un, info in src_humans.items() if un not in existing}
|
|
655
|
+
|
|
656
|
+
if dry_run:
|
|
657
|
+
for un, info in todo.items():
|
|
658
|
+
click.echo(f"[DRY RUN] would create {un} ({info['email']})")
|
|
659
|
+
click.echo(
|
|
660
|
+
f"[DRY RUN] {len(todo)} to create, "
|
|
661
|
+
f"{len(src_humans) - len(todo)} already present on target"
|
|
662
|
+
)
|
|
663
|
+
return
|
|
664
|
+
|
|
665
|
+
out_path = Path(password_out)
|
|
666
|
+
created: list[str] = []
|
|
667
|
+
failed: list[dict[str, str]] = []
|
|
668
|
+
with out_path.open("w", encoding="utf-8") as fh:
|
|
669
|
+
fh.write("username\temail\ttemp_password\tuserId\n")
|
|
670
|
+
for un, info in todo.items():
|
|
671
|
+
pw = _generate_temp_password()
|
|
672
|
+
try:
|
|
673
|
+
res = tgt.create_user(
|
|
674
|
+
un,
|
|
675
|
+
email=info["email"],
|
|
676
|
+
first_name=info["first"] or "",
|
|
677
|
+
last_name=info["last"] or "",
|
|
678
|
+
display_name=info["display"],
|
|
679
|
+
password=pw,
|
|
680
|
+
email_verified=not no_email_verified,
|
|
681
|
+
password_change_required=True,
|
|
682
|
+
)
|
|
683
|
+
uid = res.get("userId") or res.get("id") or ""
|
|
684
|
+
fh.write(f"{un}\t{info['email']}\t{pw}\t{uid}\n")
|
|
685
|
+
created.append(un)
|
|
686
|
+
except ZitadelError as exc:
|
|
687
|
+
failed.append({"user": un, "error": str(exc)[:160]})
|
|
688
|
+
|
|
689
|
+
click.echo(
|
|
690
|
+
json.dumps(
|
|
691
|
+
{
|
|
692
|
+
"created": created,
|
|
693
|
+
"skipped_existing": len(src_humans) - len(todo),
|
|
694
|
+
"failed": failed,
|
|
695
|
+
"passwords_written_to": str(out_path),
|
|
696
|
+
},
|
|
697
|
+
indent=2,
|
|
698
|
+
)
|
|
699
|
+
)
|
|
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
|