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.
- blitz_cli-0.17.0/.claude/settings.json +41 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/.gitignore +3 -2
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/PKG-INFO +5 -1
- blitz_cli-0.17.0/blitz_cli/_secrets.py +182 -0
- blitz_cli-0.17.0/blitz_cli/_secretscan.py +676 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/cli.py +155 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/pyproject.toml +7 -1
- blitz_cli-0.17.0/specs/008-secrets-scan/checklists/requirements.md +35 -0
- blitz_cli-0.17.0/specs/008-secrets-scan/contracts/cli-secrets-install-hook.md +53 -0
- blitz_cli-0.17.0/specs/008-secrets-scan/contracts/cli-secrets-scan.md +72 -0
- blitz_cli-0.17.0/specs/008-secrets-scan/data-model.md +100 -0
- blitz_cli-0.17.0/specs/008-secrets-scan/plan.md +129 -0
- blitz_cli-0.17.0/specs/008-secrets-scan/quickstart.md +100 -0
- blitz_cli-0.17.0/specs/008-secrets-scan/research.md +167 -0
- blitz_cli-0.17.0/specs/008-secrets-scan/spec.md +147 -0
- blitz_cli-0.17.0/specs/008-secrets-scan/tasks.md +459 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_cli.py +35 -0
- blitz_cli-0.17.0/tests/test_secrets.py +199 -0
- blitz_cli-0.17.0/tests/test_secretscan.py +436 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/.blitz/memory/playbook.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/.github/workflows/homebrew.yml +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/.github/workflows/publish.yml +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/.mcp.json +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/LICENSE +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/README.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/__init__.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_activity.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_code_graph.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_collect.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_collect_http.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_collect_server.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_corpus.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_dataset.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_execute.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_git.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_gitignore.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_make.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_ontology.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_playbooks.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_redact.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_scaffold.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_tool_schemas.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/_train.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/__init__.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/commands/__init__.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/commands/analyze.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/commands/clarify.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/commands/implement.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/commands/plan.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/commands/playbook.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/commands/specify.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/commands/tasks.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/memory/playbook.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/check-prerequisites.sh +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/common.sh +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/create-new-feature.sh +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/setup-plan.sh +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/setup-tasks.sh +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/templates/plan-template.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/templates/spec-template.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/scaffold/.blitz/templates/tasks-template.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/workspace/__init__.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/playbooks/workspace/huddle.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/templates/__init__.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/templates/locker_room_Makefile.tmpl +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/templates/locker_room_README.md.tmpl +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/templates/locker_room_compose.yml.tmpl +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/blitz_cli/templates/workspace_playbook.md.tmpl +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/001-cli-model-namespace/checklists/requirements.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/001-cli-model-namespace/contracts/cli-commands.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/001-cli-model-namespace/plan.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/001-cli-model-namespace/quickstart.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/001-cli-model-namespace/research.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/001-cli-model-namespace/spec.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/001-cli-model-namespace/tasks.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/002-label-experimental-surfaces/checklists/requirements.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/002-label-experimental-surfaces/contracts/experimental-labels.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/002-label-experimental-surfaces/data-model.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/002-label-experimental-surfaces/plan.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/002-label-experimental-surfaces/quickstart.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/002-label-experimental-surfaces/research.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/002-label-experimental-surfaces/spec.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/002-label-experimental-surfaces/tasks.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/checklists/requirements.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/contracts/cli-output.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/contracts/ensure-rules.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/contracts/gitignore-block.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/data-model.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/plan.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/quickstart.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/research.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/spec.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/003-init-gitignore-rules/tasks.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/checklists/requirements.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/contracts/cli.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/contracts/export-format.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/data-model.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/plan.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/quickstart.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/research.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/spec.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/004-dpo-collect-pairs/tasks.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/checklists/requirements.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/contracts/client-sink.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/contracts/ingest-api.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/data-model.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/plan.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/quickstart.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/research.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/spec.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/005-collect-ingest-endpoint/tasks.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/006-init-ignore-generated/checklists/requirements.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/006-init-ignore-generated/contracts/gitignore-block.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/006-init-ignore-generated/data-model.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/006-init-ignore-generated/plan.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/006-init-ignore-generated/quickstart.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/006-init-ignore-generated/research.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/006-init-ignore-generated/spec.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/006-init-ignore-generated/tasks.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/007-usage-data-layer/checklists/requirements.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/007-usage-data-layer/contracts/activity-db-v2.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/007-usage-data-layer/data-model.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/007-usage-data-layer/plan.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/007-usage-data-layer/quickstart.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/007-usage-data-layer/research.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/007-usage-data-layer/spec.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/specs/007-usage-data-layer/tasks.md +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_activity.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_collect.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_collect_http.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_collect_server.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_corpus.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_execute.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_gitignore.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_make.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_ontology.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_playbooks.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_redact.py +0 -0
- {blitz_cli-0.16.1 → blitz_cli-0.17.0}/tests/test_scaffold.py +0 -0
- {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.
|
|
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 "")
|