Skip to content

Commit 9db166f

Browse files
committed
initial commit
1 parent 03856ed commit 9db166f

18 files changed

Lines changed: 2002 additions & 1 deletion

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
name: Test (Python ${{ matrix.python-version }})
12+
runs-on: ubuntu-latest
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
python-version: ["3.10", "3.11", "3.12"]
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- uses: actions/setup-python@v5
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
cache: pip
25+
26+
- name: Install package and test dependencies
27+
run: pip install -e ".[test]"
28+
29+
- name: Run tests
30+
run: pytest tests/ -v

.github/workflows/publish.yml

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
name: Publish
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
jobs:
9+
# ── 1. Run tests before publishing ─────────────────────────────────────────
10+
test:
11+
name: Test before publish
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-python@v5
16+
with:
17+
python-version: "3.11"
18+
cache: pip
19+
- run: pip install -e ".[test]"
20+
- run: pytest tests/ -v
21+
22+
# ── 2. Build sdist + wheel ──────────────────────────────────────────────────
23+
build:
24+
name: Build distribution
25+
needs: test
26+
runs-on: ubuntu-latest
27+
steps:
28+
- uses: actions/checkout@v4
29+
30+
- uses: actions/setup-python@v5
31+
with:
32+
python-version: "3.11"
33+
cache: pip
34+
35+
- name: Install build frontend
36+
run: pip install build
37+
38+
- name: Build sdist and wheel
39+
run: python -m build
40+
41+
- name: Upload build artifacts
42+
uses: actions/upload-artifact@v4
43+
with:
44+
name: dist
45+
path: dist/
46+
if-no-files-found: error
47+
48+
# ── 3. Publish to PyPI (OIDC trusted publishing — no API token needed) ──────
49+
publish-pypi:
50+
name: Publish to PyPI
51+
needs: build
52+
runs-on: ubuntu-latest
53+
environment: pypi
54+
permissions:
55+
id-token: write # required for OIDC trusted publishing
56+
57+
steps:
58+
- name: Download build artifacts
59+
uses: actions/download-artifact@v4
60+
with:
61+
name: dist
62+
path: dist/
63+
64+
- name: Publish to PyPI
65+
uses: pypa/gh-action-pypi-publish@release/v1
66+
67+
# ── 4. Create GitHub Release and attach artifacts ───────────────────────────
68+
github-release:
69+
name: Create GitHub Release
70+
needs: build
71+
runs-on: ubuntu-latest
72+
permissions:
73+
contents: write # required to create releases
74+
75+
steps:
76+
- name: Download build artifacts
77+
uses: actions/download-artifact@v4
78+
with:
79+
name: dist
80+
path: dist/
81+
82+
- name: Create GitHub Release
83+
uses: softprops/action-gh-release@v2
84+
with:
85+
files: dist/*
86+
generate_release_notes: true

README.md

Lines changed: 218 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,218 @@
1-
# python-snacks
1+
# Snack Stash
2+
3+
A personal CLI tool for managing a local stash of reusable Python code snippets. Browse, copy, and curate snippets across projects with a single command.
4+
5+
```bash
6+
pipx install snack-stash
7+
snack stash create default ~/snack-stash
8+
snack unpack auth/google_oauth.py
9+
```
10+
11+
---
12+
13+
## Installation
14+
15+
**Recommended (pipx):**
16+
```bash
17+
pipx install snack-stash
18+
```
19+
20+
**Alternative (pip):**
21+
```bash
22+
pip install snack-stash
23+
```
24+
25+
Requires Python 3.10+.
26+
27+
---
28+
29+
## Configuration
30+
31+
### First-time setup
32+
33+
Create a stash directory and register it:
34+
35+
```bash
36+
snack stash create default ~/snack-stash
37+
```
38+
39+
This creates `~/snack-stash`, writes `~/.snackstashrc`, and sets `default` as the active stash.
40+
41+
### Environment variable (overrides config file)
42+
43+
```bash
44+
export SNACK_STASH=~/snack-stash
45+
```
46+
47+
Add to `~/.zshrc` or `~/.bashrc` to make it permanent. Takes priority over everything else.
48+
49+
### Config file (`~/.snackstashrc`)
50+
51+
Created automatically by `snack stash create`. You can also edit it by hand:
52+
53+
```ini
54+
[config]
55+
active = default
56+
57+
[stash.default]
58+
path = ~/snack-stash
59+
60+
[stash.work]
61+
path = ~/work-stash
62+
```
63+
64+
**Priority order:** `SNACK_STASH` env var → `~/.snackstashrc` active stash → error.
65+
66+
---
67+
68+
## Stash structure
69+
70+
A stash is a plain directory of `.py` files, organized into subdirectories by category:
71+
72+
```
73+
~/snack-stash/
74+
├── auth/
75+
│ ├── google_oauth_fastapi.py
76+
│ ├── google_oauth_flask.py
77+
│ └── jwt_helpers.py
78+
├── forms/
79+
│ ├── contact_form.py
80+
│ └── newsletter_signup.py
81+
└── email/
82+
└── smtp_sender.py
83+
```
84+
85+
You can manage the directory with Git, Dropbox, or any sync tool independently of this CLI.
86+
87+
---
88+
89+
## Commands
90+
91+
### `snack list [category]`
92+
93+
List all snippets in the active stash.
94+
95+
```bash
96+
snack list # all snippets
97+
snack list auth # filtered by category (subdirectory)
98+
```
99+
100+
### `snack search <keyword>`
101+
102+
Search snippet filenames for a keyword (case-insensitive).
103+
104+
```bash
105+
snack search oauth
106+
# auth/google_oauth_fastapi.py
107+
# auth/google_oauth_flask.py
108+
```
109+
110+
### `snack unpack <snippet>`
111+
112+
Copy a snippet **from the stash** into the current working directory.
113+
114+
```bash
115+
snack unpack auth/google_oauth_fastapi.py
116+
# → ./auth/google_oauth_fastapi.py
117+
118+
snack unpack auth/google_oauth_fastapi.py --flat
119+
# → ./google_oauth_fastapi.py (no subdirectory)
120+
121+
snack unpack auth/google_oauth_fastapi.py --force
122+
# Overwrites without prompting
123+
```
124+
125+
### `snack pack <snippet>`
126+
127+
Copy a file **from the current directory** back into the stash. Use this when you've improved a snippet on a project and want to update the canonical version.
128+
129+
```bash
130+
snack pack auth/google_oauth_fastapi.py
131+
snack pack auth/google_oauth_fastapi.py --force
132+
```
133+
134+
---
135+
136+
## Stash management
137+
138+
### `snack stash create <name> <path>`
139+
140+
Register a new named stash. Creates the directory if it doesn't exist.
141+
142+
```bash
143+
snack stash create default ~/snack-stash
144+
snack stash create work ~/work-stash --no-activate
145+
```
146+
147+
The first stash created is automatically set as active. Use `--no-activate` to add a stash without switching to it.
148+
149+
### `snack stash list`
150+
151+
Show all configured stashes and which one is active.
152+
153+
```bash
154+
snack stash list
155+
# default /Users/you/snack-stash ← active
156+
# work /Users/you/work-stash
157+
```
158+
159+
### `snack stash move <name> <new-path>`
160+
161+
Move a stash to a new location. Moves the directory on disk and updates the config.
162+
163+
```bash
164+
snack stash move default ~/new-location/snack-stash
165+
```
166+
167+
### `snack stash add-remote <repo>`
168+
169+
Copy Python snippets from a public GitHub repository into the active stash. Downloads the repo as a tarball and copies all `.py` files, preserving directory structure.
170+
171+
```bash
172+
snack stash add-remote owner/repo
173+
snack stash add-remote https://github.com/owner/repo
174+
snack stash add-remote owner/repo --subdir auth # only copy files under auth/
175+
snack stash add-remote owner/repo --force # overwrite without prompting
176+
```
177+
178+
---
179+
180+
## Error handling
181+
182+
| Situation | Behaviour |
183+
|---|---|
184+
| No stash configured | Error with setup instructions |
185+
| Stash path doesn't exist | Error with the attempted path |
186+
| Snippet not found in stash | Error — use `snack list` or `snack search` |
187+
| Source file not found | Error |
188+
| Destination file already exists | Prompt to confirm, or skip with `--force` |
189+
| Named stash already exists | Error |
190+
| Move target already exists | Error |
191+
| GitHub repo not found (HTTP 404) | Error with status code |
192+
193+
---
194+
195+
## Project structure
196+
197+
```
198+
python-snacks/
199+
├── pyproject.toml
200+
├── snacks/
201+
│ ├── main.py # Typer app, all command definitions
202+
│ ├── config.py # SnackConfig class, stash path resolution
203+
│ └── ops.py # File copy logic (pack, unpack, add_remote)
204+
└── tests/
205+
├── test_commands.py # snippet commands
206+
└── test_stash_commands.py # stash management commands
207+
```
208+
209+
---
210+
211+
## CI / CD
212+
213+
- **CI:** Tests run on every push and PR to `main` across Python 3.10, 3.11, and 3.12.
214+
- **Publish:** Push a `v*` tag to trigger a build, PyPI publish, and GitHub Release.
215+
216+
```bash
217+
git tag v0.2.0 && git push origin v0.2.0
218+
```

pyproject.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[build-system]
2+
requires = ["setuptools>=68", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "snack-stash"
7+
version = "0.1.0"
8+
description = "A CLI tool for managing a personal stash of reusable Python code snippets."
9+
requires-python = ">=3.10"
10+
dependencies = [
11+
"typer[all]>=0.9",
12+
]
13+
14+
[project.scripts]
15+
snack = "snacks.main:app"
16+
17+
[project.optional-dependencies]
18+
test = ["pytest>=8"]
19+
20+
[tool.setuptools.packages.find]
21+
where = ["."]
22+
include = ["snacks*"]

snacks/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)