blitz-cli 0.16.1__tar.gz → 0.17.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 (140) hide show
  1. blitz_cli-0.17.0/.claude/settings.json +41 -0
  2. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/.gitignore +3 -2
  3. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/PKG-INFO +5 -1
  4. blitz_cli-0.17.0/blitz_cli/_secrets.py +182 -0
  5. blitz_cli-0.17.0/blitz_cli/_secretscan.py +676 -0
  6. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/cli.py +155 -0
  7. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/pyproject.toml +7 -1
  8. blitz_cli-0.17.0/specs/008-secrets-scan/checklists/requirements.md +35 -0
  9. blitz_cli-0.17.0/specs/008-secrets-scan/contracts/cli-secrets-install-hook.md +53 -0
  10. blitz_cli-0.17.0/specs/008-secrets-scan/contracts/cli-secrets-scan.md +72 -0
  11. blitz_cli-0.17.0/specs/008-secrets-scan/data-model.md +100 -0
  12. blitz_cli-0.17.0/specs/008-secrets-scan/plan.md +129 -0
  13. blitz_cli-0.17.0/specs/008-secrets-scan/quickstart.md +100 -0
  14. blitz_cli-0.17.0/specs/008-secrets-scan/research.md +167 -0
  15. blitz_cli-0.17.0/specs/008-secrets-scan/spec.md +147 -0
  16. blitz_cli-0.17.0/specs/008-secrets-scan/tasks.md +459 -0
  17. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_cli.py +35 -0
  18. blitz_cli-0.17.0/tests/test_secrets.py +199 -0
  19. blitz_cli-0.17.0/tests/test_secretscan.py +436 -0
  20. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/.blitz/memory/playbook.md +0 -0
  21. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/.github/workflows/homebrew.yml +0 -0
  22. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/.github/workflows/publish.yml +0 -0
  23. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/.mcp.json +0 -0
  24. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/LICENSE +0 -0
  25. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/README.md +0 -0
  26. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/__init__.py +0 -0
  27. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_activity.py +0 -0
  28. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_code_graph.py +0 -0
  29. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_collect.py +0 -0
  30. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_collect_http.py +0 -0
  31. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_collect_server.py +0 -0
  32. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_corpus.py +0 -0
  33. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_dataset.py +0 -0
  34. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_execute.py +0 -0
  35. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_git.py +0 -0
  36. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_gitignore.py +0 -0
  37. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_make.py +0 -0
  38. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_ontology.py +0 -0
  39. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_playbooks.py +0 -0
  40. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_redact.py +0 -0
  41. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_scaffold.py +0 -0
  42. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_tool_schemas.py +0 -0
  43. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_train.py +0 -0
  44. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/__init__.py +0 -0
  45. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/commands/__init__.py +0 -0
  46. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/commands/analyze.md +0 -0
  47. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/commands/clarify.md +0 -0
  48. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/commands/implement.md +0 -0
  49. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/commands/plan.md +0 -0
  50. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/commands/playbook.md +0 -0
  51. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/commands/specify.md +0 -0
  52. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/commands/tasks.md +0 -0
  53. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/memory/playbook.md +0 -0
  54. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/check-prerequisites.sh +0 -0
  55. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/common.sh +0 -0
  56. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/create-new-feature.sh +0 -0
  57. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/setup-plan.sh +0 -0
  58. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/setup-tasks.sh +0 -0
  59. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/templates/plan-template.md +0 -0
  60. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/templates/spec-template.md +0 -0
  61. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/templates/tasks-template.md +0 -0
  62. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/workspace/__init__.py +0 -0
  63. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/workspace/huddle.md +0 -0
  64. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/templates/__init__.py +0 -0
  65. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/templates/locker_room_Makefile.tmpl +0 -0
  66. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/templates/locker_room_README.md.tmpl +0 -0
  67. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/templates/locker_room_compose.yml.tmpl +0 -0
  68. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/templates/workspace_playbook.md.tmpl +0 -0
  69. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/001-cli-model-namespace/checklists/requirements.md +0 -0
  70. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/001-cli-model-namespace/contracts/cli-commands.md +0 -0
  71. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/001-cli-model-namespace/plan.md +0 -0
  72. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/001-cli-model-namespace/quickstart.md +0 -0
  73. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/001-cli-model-namespace/research.md +0 -0
  74. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/001-cli-model-namespace/spec.md +0 -0
  75. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/001-cli-model-namespace/tasks.md +0 -0
  76. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/002-label-experimental-surfaces/checklists/requirements.md +0 -0
  77. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/002-label-experimental-surfaces/contracts/experimental-labels.md +0 -0
  78. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/002-label-experimental-surfaces/data-model.md +0 -0
  79. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/002-label-experimental-surfaces/plan.md +0 -0
  80. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/002-label-experimental-surfaces/quickstart.md +0 -0
  81. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/002-label-experimental-surfaces/research.md +0 -0
  82. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/002-label-experimental-surfaces/spec.md +0 -0
  83. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/002-label-experimental-surfaces/tasks.md +0 -0
  84. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/checklists/requirements.md +0 -0
  85. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/contracts/cli-output.md +0 -0
  86. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/contracts/ensure-rules.md +0 -0
  87. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/contracts/gitignore-block.md +0 -0
  88. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/data-model.md +0 -0
  89. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/plan.md +0 -0
  90. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/quickstart.md +0 -0
  91. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/research.md +0 -0
  92. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/spec.md +0 -0
  93. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/tasks.md +0 -0
  94. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/checklists/requirements.md +0 -0
  95. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/contracts/cli.md +0 -0
  96. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/contracts/export-format.md +0 -0
  97. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/data-model.md +0 -0
  98. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/plan.md +0 -0
  99. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/quickstart.md +0 -0
  100. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/research.md +0 -0
  101. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/spec.md +0 -0
  102. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/tasks.md +0 -0
  103. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/checklists/requirements.md +0 -0
  104. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/contracts/client-sink.md +0 -0
  105. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/contracts/ingest-api.md +0 -0
  106. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/data-model.md +0 -0
  107. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/plan.md +0 -0
  108. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/quickstart.md +0 -0
  109. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/research.md +0 -0
  110. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/spec.md +0 -0
  111. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/tasks.md +0 -0
  112. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/006-init-ignore-generated/checklists/requirements.md +0 -0
  113. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/006-init-ignore-generated/contracts/gitignore-block.md +0 -0
  114. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/006-init-ignore-generated/data-model.md +0 -0
  115. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/006-init-ignore-generated/plan.md +0 -0
  116. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/006-init-ignore-generated/quickstart.md +0 -0
  117. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/006-init-ignore-generated/research.md +0 -0
  118. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/006-init-ignore-generated/spec.md +0 -0
  119. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/006-init-ignore-generated/tasks.md +0 -0
  120. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/007-usage-data-layer/checklists/requirements.md +0 -0
  121. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/007-usage-data-layer/contracts/activity-db-v2.md +0 -0
  122. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/007-usage-data-layer/data-model.md +0 -0
  123. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/007-usage-data-layer/plan.md +0 -0
  124. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/007-usage-data-layer/quickstart.md +0 -0
  125. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/007-usage-data-layer/research.md +0 -0
  126. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/007-usage-data-layer/spec.md +0 -0
  127. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/007-usage-data-layer/tasks.md +0 -0
  128. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_activity.py +0 -0
  129. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_collect.py +0 -0
  130. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_collect_http.py +0 -0
  131. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_collect_server.py +0 -0
  132. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_corpus.py +0 -0
  133. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_execute.py +0 -0
  134. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_gitignore.py +0 -0
  135. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_make.py +0 -0
  136. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_ontology.py +0 -0
  137. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_playbooks.py +0 -0
  138. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_redact.py +0 -0
  139. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_scaffold.py +0 -0
  140. {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_tool_schemas.py +0 -0
@@ -0,0 +1,41 @@
1
+ {
2
+ "permissions": {
3
+ "ask": [
4
+ "Bash(aws secretsmanager:*)",
5
+ "Bash(blitz secrets:*)",
6
+ "Bash(python -m blitz_cli.cli secrets:*)",
7
+ "Bash(python3 -m blitz_cli.cli secrets:*)"
8
+ ]
9
+ },
10
+ "hooks": {
11
+ "PreToolUse": [
12
+ {
13
+ "matcher": "Bash",
14
+ "hooks": [
15
+ {
16
+ "type": "command",
17
+ "command": "cmd=$(jq -r '.tool_input.command // \"\"'); if printf '%s' \"$cmd\" | grep -qiE 'secretsmanager|blitz +secrets|blitz_cli\\.cli +secrets'; then printf '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"ask\",\"permissionDecisionReason\":\"Command accesses AWS Secrets Manager \u2014 confirm before running.\"}}'; fi"
18
+ }
19
+ ]
20
+ },
21
+ {
22
+ "matcher": "Bash",
23
+ "hooks": [
24
+ {
25
+ "type": "command",
26
+ "command": "cmd=$(jq -r '.tool_input.command // \"\"')\nprintf '%s' \"$cmd\" | grep -qE '(^|[^[:alnum:]_-])git([^[:alnum:]_-]|$)' || exit 0\nis_commit=$(printf '%s' \"$cmd\" | grep -qE 'git[^|;&]*\\bcommit\\b' && echo 1)\nis_push=$(printf '%s' \"$cmd\" | grep -qE 'git[^|;&]*\\bpush\\b' && echo 1)\n[ -z \"$is_commit$is_push\" ] && exit 0\ndir=$(printf '%s' \"$cmd\" | grep -oE 'cd +[^&|;]+' | tail -1 | sed -E 's/^cd +//; s/ +$//; s/^[\"'\"'\"']//; s/[\"'\"'\"']$//')\n[ -z \"$dir\" ] && dir=\".\"\nbranch=$(git -C \"$dir\" rev-parse --abbrev-ref HEAD 2>/dev/null)\nask=\"\"\n[ -n \"$is_push\" ] && printf '%s' \"$cmd\" | grep -qE '\\bpush\\b[^|;]*(\\b|:)(main|master)\\b' && ask=1\ncase \"$branch\" in main|master) [ -n \"$is_commit$is_push\" ] && ask=1 ;; esac\n[ -n \"$ask\" ] && printf '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"ask\",\"permissionDecisionReason\":\"This commits or pushes to main/master \u2014 confirm.\"}}'\nexit 0"
27
+ }
28
+ ]
29
+ },
30
+ {
31
+ "matcher": "Bash",
32
+ "hooks": [
33
+ {
34
+ "type": "command",
35
+ "command": "cmd=$(jq -r '.tool_input.command // \"\"')\nprintf '%s' \"$cmd\" | grep -qi docker || exit 0\nask=\"\"\nprintf '%s' \"$cmd\" | grep -qE 'compose[^|;&]*\\bdown\\b' && printf '%s' \"$cmd\" | grep -qwE '\\-v|--volumes' && ask=1\nprintf '%s' \"$cmd\" | grep -qE '\\bvolume\\b[^|;&]*\\b(rm|prune)\\b' && ask=1\nprintf '%s' \"$cmd\" | grep -qE '\\bsystem\\b[^|;&]*\\bprune\\b' && printf '%s' \"$cmd\" | grep -qwE '\\-a|--all|--volumes|-[a-zA-Z]*a' && ask=1\nprintf '%s' \"$cmd\" | grep -qE 'docker[^|;&]*(\\bpush\\b|--push)' && ask=1\nprintf '%s' \"$cmd\" | grep -qE 'docker([- ]compose)?[[:space:]]+(container[[:space:]]+)?rm([[:space:]]|$)' && printf '%s' \"$cmd\" | grep -qwE '\\-f|--force' && ask=1\nprintf '%s' \"$cmd\" | grep -qE 'docker[[:space:]]+(rmi([[:space:]]|$)|image[[:space:]]+rm([[:space:]]|$))' && ask=1\nprintf '%s' \"$cmd\" | grep -qE 'docker([- ]compose)?[[:space:]]+(container[[:space:]]+)?kill([[:space:]]|$)' && ask=1\n[ -n \"$ask\" ] && printf '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"ask\",\"permissionDecisionReason\":\"Destructive Docker action (volume/data loss, container/image removal, kill, or registry push) \u2014 confirm.\"}}'\nexit 0"
36
+ }
37
+ ]
38
+ }
39
+ ]
40
+ }
41
+ }
@@ -6,8 +6,9 @@ dist/
6
6
  .venv/
7
7
  .pytest_cache/
8
8
 
9
- # stray test artifacts from running the CLI in-repo
10
- /.claude/
9
+ # stray test artifacts from running the CLI in-repo, but keep committed settings
10
+ /.claude/*
11
+ !/.claude/settings.json
11
12
 
12
13
  # The .blitz/ working area is governed content, not an artifact: memory/ holds
13
14
  # the repo's playbook (seeded once, never overwritten even by `init --force`),
@@ -1,11 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blitz-cli
3
- Version: 0.16.1
3
+ Version: 0.17.0
4
4
  Summary: Developer CLI for Blitz locker-room: spec-driven playbooks, a codebase/huddle ontology graph with an MCP tool, and the composable watcher/trainer/graph containers.
5
5
  License: MIT
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.9
8
8
  Requires-Dist: rich>=13
9
+ Provides-Extra: secrets
10
+ Requires-Dist: boto3>=1.34; extra == 'secrets'
11
+ Provides-Extra: toml
12
+ Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'toml'
9
13
  Provides-Extra: train
10
14
  Requires-Dist: accelerate==1.2.1; extra == 'train'
11
15
  Requires-Dist: datasets==3.2.0; extra == 'train'
@@ -0,0 +1,182 @@
1
+ """AWS Secrets Manager <-> env-file sync.
2
+
3
+ Each service keeps its config as a single JSON blob (one secret per service,
4
+ e.g. `spareparts-api/env`), whose keys are env var names. ECS reads those keys
5
+ directly via the task definition; this module is for the paths ECS doesn't
6
+ cover -- editing the blob and pulling it down.
7
+
8
+ boto3 is the optional [secrets] extra: the core CLI is stdlib + rich, so the
9
+ import lives inside `_client()` and surfaces `_MISSING` rather than a traceback.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from pathlib import Path
16
+ from typing import Dict, Iterable, Optional
17
+
18
+ _MISSING = (
19
+ "`blitz secrets` needs the AWS SDK.\n"
20
+ "Install it with: pip install 'blitz-cli[secrets]'"
21
+ )
22
+
23
+
24
+ class SecretsError(Exception):
25
+ """A caller-facing failure: bad credentials, missing secret, malformed blob."""
26
+
27
+
28
+ def _client(region: str):
29
+ try:
30
+ import boto3
31
+ except ImportError:
32
+ raise SecretsError(_MISSING)
33
+ return boto3.client("secretsmanager", region_name=region)
34
+
35
+
36
+ def _fetch(client, secret_id: str) -> Dict[str, str]:
37
+ """Read `secret_id`'s JSON blob. Raises SecretsError unless absent (caller decides)."""
38
+ try:
39
+ response = client.get_secret_value(SecretId=secret_id)
40
+ except client.exceptions.ResourceNotFoundException:
41
+ raise
42
+ except Exception as err:
43
+ raise SecretsError(f"could not read {secret_id}: {err}")
44
+
45
+ body = response.get("SecretString")
46
+ if not body:
47
+ raise SecretsError(f"{secret_id} has no SecretString (binary secrets unsupported)")
48
+
49
+ try:
50
+ parsed = json.loads(body)
51
+ except json.JSONDecodeError:
52
+ raise SecretsError(f"{secret_id} is not JSON -- expected a flat object of env keys")
53
+
54
+ if not isinstance(parsed, dict):
55
+ raise SecretsError(f"{secret_id} is not a JSON object -- expected a flat object of env keys")
56
+
57
+ return {key: "" if value is None else str(value) for key, value in parsed.items()}
58
+
59
+
60
+ def pull(secret_id: str, region: str, exclude: Optional[Iterable[str]] = None) -> Dict[str, str]:
61
+ """Fetch the blob, dropping any `exclude` keys."""
62
+ client = _client(region)
63
+ try:
64
+ values = _fetch(client, secret_id)
65
+ except client.exceptions.ResourceNotFoundException:
66
+ raise SecretsError(f"secret not found: {secret_id}")
67
+
68
+ skip = set(exclude or ())
69
+ return {key: value for key, value in values.items() if key not in skip}
70
+
71
+
72
+ def push(
73
+ secret_id: str,
74
+ values: Dict[str, str],
75
+ region: str,
76
+ merge: bool = True,
77
+ create: bool = False,
78
+ ) -> Dict[str, int]:
79
+ """Write `values` to the blob.
80
+
81
+ Merging is the default and matters: a local env file is rarely a superset of
82
+ the blob. Replacing wholesale drops any key the file lacks, and ECS refuses
83
+ to start a task whose definition references a missing key -- so the damage
84
+ would surface at the next deploy, not here.
85
+ """
86
+ client = _client(region)
87
+
88
+ remote: Dict[str, str] = {}
89
+ exists = True
90
+ try:
91
+ remote = _fetch(client, secret_id)
92
+ except client.exceptions.ResourceNotFoundException:
93
+ exists = False
94
+
95
+ if not exists:
96
+ if not create:
97
+ raise SecretsError(f"secret not found: {secret_id} (pass --create to create it)")
98
+ payload = dict(values)
99
+ elif merge:
100
+ payload = {**remote, **values}
101
+ else:
102
+ payload = dict(values)
103
+
104
+ body = json.dumps(payload)
105
+ try:
106
+ if exists:
107
+ client.update_secret(SecretId=secret_id, SecretString=body)
108
+ else:
109
+ client.create_secret(
110
+ Name=secret_id,
111
+ SecretString=body,
112
+ Description="Created by blitz secrets push",
113
+ )
114
+ except Exception as err:
115
+ raise SecretsError(f"could not write {secret_id}: {err}")
116
+
117
+ return {
118
+ "written": len(payload),
119
+ "added": len(set(payload) - set(remote)),
120
+ "removed": len(set(remote) - set(payload)),
121
+ }
122
+
123
+
124
+ def parse_env(path: Path) -> Dict[str, str]:
125
+ """Parse a KEY=VALUE env file.
126
+
127
+ Deliberately stdlib rather than python-dotenv -- this is the whole grammar we
128
+ need: `export ` prefixes, `#` comments, and optional surrounding quotes. A
129
+ value is only unquoted when the quotes wrap it entirely, so `pass#word` and
130
+ `a"b` survive intact.
131
+ """
132
+ if not path.exists():
133
+ raise SecretsError(f"file not found: {path}")
134
+
135
+ values: Dict[str, str] = {}
136
+ for lineno, raw in enumerate(path.read_text().splitlines(), start=1):
137
+ line = raw.strip()
138
+ if not line or line.startswith("#"):
139
+ continue
140
+ if line.startswith("export "):
141
+ line = line[len("export "):].lstrip()
142
+
143
+ key, sep, value = line.partition("=")
144
+ if not sep:
145
+ raise SecretsError(f"{path}:{lineno}: expected KEY=VALUE, got {raw!r}")
146
+
147
+ key = key.strip()
148
+ if not key:
149
+ raise SecretsError(f"{path}:{lineno}: empty key")
150
+
151
+ value = value.strip()
152
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
153
+ quote = value[0]
154
+ value = value[1:-1]
155
+ if quote == "'":
156
+ # Undo write_env's shell-style escape: a single-quoted string
157
+ # can't contain a quote, so one is spliced in as '\''.
158
+ value = value.replace("'\\''", "'")
159
+ else:
160
+ # Only strip comments off bare values; a quoted value may contain '#'.
161
+ marker = value.find(" #")
162
+ if marker != -1:
163
+ value = value[:marker].rstrip()
164
+
165
+ values[key] = value
166
+
167
+ return values
168
+
169
+
170
+ def write_env(values: Dict[str, str], path: Path, prepend: str = "") -> None:
171
+ """Write KEY='VALUE' lines, sorted.
172
+
173
+ Every value is single-quoted: unquoted values break docker compose's env_file
174
+ parsing the moment one contains a '#' or a space. Embedded single quotes are
175
+ escaped shell-style, so the output is safe to `source` as well.
176
+ """
177
+ lines = []
178
+ for key in sorted(values):
179
+ escaped = str(values[key]).replace("'", "'\\''")
180
+ lines.append(f"{prepend}{key}='{escaped}'")
181
+
182
+ path.write_text("\n".join(lines) + "\n" if lines else "")