diff --git a/.github/workflows/wiki.yml b/.github/workflows/wiki.yml index fe736f2..15b135f 100644 --- a/.github/workflows/wiki.yml +++ b/.github/workflows/wiki.yml @@ -27,3 +27,12 @@ jobs: git add . git diff --cached --quiet || git commit -m "sync wiki from main (${GITHUB_SHA::7})" git push + + - name: Sync wiki to GitHub Pages + run: | + cp wiki/*.md docs/wiki/ + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add docs/wiki/ + git diff --cached --quiet || git commit -m "sync wiki to docs/wiki (${GITHUB_SHA::7})" + git push diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..48ae599 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,486 @@ + + + + + + python-snacks Wiki + + + + + + +
+ + +
+
Loading...
+
+
+ + + + + + + + + diff --git a/docs/wiki/Commands-Reference.md b/docs/wiki/Commands-Reference.md new file mode 100644 index 0000000..e4e83de --- /dev/null +++ b/docs/wiki/Commands-Reference.md @@ -0,0 +1,234 @@ +# Commands Reference + +## Summary + +| Command | Description | +|---|---| +| `snack list [category]` | List all snippets, optionally filtered by category | +| `snack search ` | Search snippet filenames for a keyword | +| `snack unpack [--flat] [--force]` | Copy a snippet from the stash into the current directory | +| `snack pack [--force]` | Copy a snippet from the current directory into the stash | +| `snack stash create ` | Register a new named stash directory | +| `snack stash list` | List all configured stashes | +| `snack stash move ` | Move a stash to a new location | +| `snack stash add-remote [--subdir] [--force]` | Copy snippets from a GitHub repo into the active stash | + +All commands accept `--help` for inline usage information. + +--- + +## Snippet commands + +These commands operate on the **active stash** (set via `snack stash create` or the `SNACK_STASH` env var). + +### `snack list` + +Lists all `.py` files in the stash, sorted alphabetically. + +```bash +snack list +``` + +Output: + +``` +auth/google_oauth_fastapi.py +auth/google_oauth_flask.py +auth/jwt_helpers.py +forms/contact_form.py +forms/newsletter_signup.py +email/smtp_sender.py +``` + +#### Filter by category + +Pass a category name (subdirectory) to narrow the output: + +```bash +snack list auth +``` + +Output: + +``` +auth/google_oauth_fastapi.py +auth/google_oauth_flask.py +auth/jwt_helpers.py +``` + +--- + +### `snack search` + +Searches snippet filenames (not file contents) for a keyword. Case-insensitive. + +```bash +snack search oauth +``` + +Output: + +``` +auth/google_oauth_fastapi.py +auth/google_oauth_flask.py +``` + +--- + +### `snack unpack` + +Copies a snippet **from the stash** into the **current working directory**. + +```bash +snack unpack auth/google_oauth_fastapi.py +``` + +The path is relative to the stash root. Use `snack list` or `snack search` to find the correct path. + +#### Flags + +| Flag | Description | +|---|---| +| `--flat` | Copy the file directly into the current directory, discarding subdirectory structure | +| `--force` | Overwrite an existing file without prompting | + +#### Default (preserves structure) + +```bash +cd ~/projects/my-app +snack unpack auth/google_oauth_fastapi.py +# Creates: ./auth/google_oauth_fastapi.py +``` + +#### `--flat` + +```bash +snack unpack auth/google_oauth_fastapi.py --flat +# Creates: ./google_oauth_fastapi.py +``` + +#### Overwrite prompt + +If the destination file already exists: + +``` +'./auth/google_oauth_fastapi.py' already exists. Overwrite? [y/N]: +``` + +Pass `--force` to skip the prompt. + +--- + +### `snack pack` + +Copies a file **from the current working directory** into the stash. Use this when you have improved a snippet during a project and want to update the canonical version. + +```bash +snack pack auth/google_oauth_fastapi.py +``` + +The path is relative to the current working directory and is used as-is inside the stash. + +#### Flags + +| Flag | Description | +|---|---| +| `--force` | Overwrite an existing stash file without prompting | + +--- + +## Stash management commands + +### `snack stash create` + +Registers a new named stash directory and creates it on disk if it doesn't already exist. + +```bash +snack stash create default ~/snack-stash +snack stash create work ~/work-stash +snack stash create work ~/work-stash --no-activate +``` + +The first stash created is automatically set as active. For subsequent stashes, use `--activate` (the default) to switch, or `--no-activate` to add without switching. + +#### Flags + +| Flag | Default | Description | +|---|---|---| +| `--activate` / `--no-activate` | `--activate` | Whether to set this as the active stash | + +--- + +### `snack stash list` + +Shows all configured stashes with their paths, marking the active one. + +```bash +snack stash list +``` + +Output: + +``` + default /Users/you/snack-stash ← active + work /Users/you/work-stash +``` + +If a stash path no longer exists on disk, it is flagged: + +``` + default /Users/you/snack-stash ← active [path missing!] +``` + +If `SNACK_STASH` env var is set, a note is shown at the bottom indicating it is overriding the active stash. + +--- + +### `snack stash move` + +Moves a stash directory to a new location on disk and updates the path in `~/.snackstashrc`. + +```bash +snack stash move default ~/new-location/snack-stash +``` + +- The old directory and all its contents are moved to the new path +- The config is updated automatically +- If the old path no longer exists on disk, the new directory is created instead +- Errors if the new path already exists (to prevent accidental overwrites) + +--- + +### `snack stash add-remote` + +Downloads a public GitHub repository as a tarball and copies all `.py` files into the active stash, preserving directory structure. + +```bash +snack stash add-remote owner/repo +snack stash add-remote https://github.com/owner/repo +``` + +#### Filter by subdirectory + +Only copy files from a specific subdirectory of the repo: + +```bash +snack stash add-remote owner/repo --subdir auth +``` + +This copies `auth/google_oauth.py` from the repo as `auth/google_oauth.py` in the stash (the full path is preserved). + +#### Flags + +| Flag | Description | +|---|---| +| `--subdir ` | Only copy files under this subdirectory of the repo | +| `--force` | Overwrite existing stash files without prompting | + +#### Accepted repo formats + +- `owner/repo` +- `https://github.com/owner/repo` +- `https://github.com/owner/repo.git` + +Non-Python files (`.md`, `.txt`, etc.) are never copied. diff --git a/docs/wiki/Configuration.md b/docs/wiki/Configuration.md new file mode 100644 index 0000000..b173697 --- /dev/null +++ b/docs/wiki/Configuration.md @@ -0,0 +1,99 @@ +# Configuration + +Snack Stash supports multiple named stashes. The active stash is used by all snippet commands (`list`, `search`, `unpack`, `pack`). + +## Priority Order + +1. `SNACK_STASH` environment variable (overrides everything) +2. Active stash in `~/.snackstashrc` +3. Error — nothing is configured + +--- + +## Method 1: Named stashes (recommended) + +Use `snack stash create` to register stashes. This is the standard workflow and requires no manual config editing. + +```bash +# Create your first stash (auto-activated) +snack stash create default ~/snack-stash + +# Add a second stash without switching to it +snack stash create work ~/work-stash --no-activate + +# See what's configured +snack stash list +# default /Users/you/snack-stash ← active +# work /Users/you/work-stash +``` + +This writes `~/.snackstashrc` in INI format: + +```ini +[config] +active = default + +[stash.default] +path = /Users/you/snack-stash + +[stash.work] +path = /Users/you/work-stash +``` + +You can edit this file by hand if needed. + +--- + +## Method 2: Environment variable + +Set `SNACK_STASH` to override the config file entirely. Useful for scripts, CI, or quickly switching context. + +```bash +export SNACK_STASH=~/snack-stash +``` + +Add to `~/.zshrc` or `~/.bashrc` to make it permanent: + +```bash +echo 'export SNACK_STASH=~/snack-stash' >> ~/.zshrc +``` + +When `SNACK_STASH` is set, named stashes in the config file are ignored for all snippet commands. `snack stash list` will still display configured stashes but will note the override. + +--- + +## Stash directory structure + +A stash is a plain directory of `.py` files, organized into subdirectories by category. No special files or metadata are required. + +``` +~/snack-stash/ +├── auth/ +│ ├── google_oauth_fastapi.py +│ ├── google_oauth_flask.py +│ └── jwt_helpers.py +├── forms/ +│ ├── contact_form.py +│ └── newsletter_signup.py +└── email/ + └── smtp_sender.py +``` + +The directory can be managed with Git, Dropbox, or any other sync mechanism independently of this tool. Syncing is outside the scope of `snack` — it only copies files in and out. + +--- + +## Switching the active stash + +To change which named stash is active, create a new one with `--activate` or edit `~/.snackstashrc` directly and update the `active` key under `[config]`. + +--- + +## Verifying your configuration + +```bash +snack stash list # see all stashes and which is active +snack list # confirm the active stash has snippets +``` + +See [[Error-Reference]] for help with configuration errors. diff --git a/docs/wiki/Contributing.md b/docs/wiki/Contributing.md new file mode 100644 index 0000000..71b1104 --- /dev/null +++ b/docs/wiki/Contributing.md @@ -0,0 +1,74 @@ +# Contributing + +## Dev Setup + +Clone the repo and install in editable mode with test dependencies: + +```bash +git clone https://github.com/kickash/python-snacks.git +cd python-snacks +pip install -e ".[test]" +``` + +Verify the CLI works: + +```bash +snack --help +``` + +## Running Tests + +```bash +pytest tests/ -v +``` + +The test suite has 33 tests across two files: + +- `tests/test_commands.py` — snippet commands (`list`, `search`, `unpack`, `pack`), config resolution, error cases +- `tests/test_stash_commands.py` — stash management commands (`create`, `list`, `move`, `add-remote`), including mocked HTTP for `add-remote` + +Tests never write to `~/.snackstashrc` — the config path is redirected to a temp file via `monkeypatch`. + +## CI + +Every push and pull request to `main` runs the test suite against Python 3.10, 3.11, and 3.12 via GitHub Actions (`.github/workflows/ci.yml`). Pull requests must pass CI before merging. + +## Release Process + +Releases are fully automated via `.github/workflows/publish.yml`. + +1. Update the version in `pyproject.toml` +2. Commit and push to `main` +3. Tag the commit with a `v`-prefixed version and push the tag: + +```bash +git tag v0.2.0 +git push origin v0.2.0 +``` + +The publish workflow will: +- Run the test suite +- Build the sdist and wheel +- Publish to PyPI (via OIDC trusted publishing — no API token required) +- Create a GitHub Release with the built artifacts attached + +## Project Structure + +``` +python-snacks/ +├── pyproject.toml # Package metadata and dependencies +├── snacks/ +│ ├── main.py # Typer app and all command definitions +│ ├── config.py # SnackConfig class, stash path resolution +│ └── ops.py # File ops: pack, unpack, add_remote +└── tests/ + ├── test_commands.py # Snippet command tests + └── test_stash_commands.py # Stash management tests +``` + +## Key design notes + +- **Config format:** `~/.snackstashrc` uses INI format (`[stash.]` sections). The old `stash=` sectionless format is still read for backwards compatibility but never written. +- **`SNACK_STASH` env var:** Always overrides the config file for all snippet commands. Useful for scripts and CI. +- **`add-remote`:** Uses only stdlib (`urllib`, `tarfile`) — no additional dependencies beyond Typer. +- **Test isolation:** `monkeypatch.setattr(config_module, "CONFIG_PATH", ...)` redirects the config file to a temp path so tests are hermetic. diff --git a/docs/wiki/Error-Reference.md b/docs/wiki/Error-Reference.md new file mode 100644 index 0000000..6b75f8f --- /dev/null +++ b/docs/wiki/Error-Reference.md @@ -0,0 +1,166 @@ +# Error Reference + +## Stash not configured + +**Message:** +``` +[error] Snack stash location is not configured. +Create a stash with: + snack stash create default ~/snack-stash +Or set the SNACK_STASH environment variable: + export SNACK_STASH=~/snack-stash +``` + +**Cause:** No stash has been created and `SNACK_STASH` is not set. + +**Fix:** Run `snack stash create default ~/snack-stash` to get started. See [[Configuration]] for full details. + +--- + +## Stash path does not exist + +**Message:** +``` +[error] SNACK_STASH is set to '/path/to/stash' but that path does not exist. +``` + +or + +``` +[error] Active stash 'default' path '/path/to/stash' does not exist. +``` + +**Cause:** The configured path is not present on disk. Common reasons: +- The directory was moved or deleted manually (use `snack stash move` instead) +- A typo in the path +- The drive or volume it lives on is not mounted + +**Fix:** Either recreate the directory, correct the path in `~/.snackstashrc`, or run `snack stash move` to point the stash to its new location. + +--- + +## Snippet not found in stash (`unpack`) + +**Message:** +``` +[error] 'auth/nope.py' not found in stash (/path/to/stash). +``` + +**Cause:** The path passed to `snack unpack` does not exist in the stash. + +**Fix:** Use `snack list` or `snack search` to find the correct path: + +```bash +snack list auth +snack search oauth +``` + +--- + +## Source file not found (`pack`) + +**Message:** +``` +[error] 'auth/google_oauth.py' not found in current directory. +``` + +**Cause:** The file passed to `snack pack` does not exist relative to the current working directory. + +**Fix:** Check your working directory and the path you provided: + +```bash +pwd +ls auth/ +``` + +--- + +## Overwrite conflict + +**Message:** +``` +'/path/to/file.py' already exists. Overwrite? [y/N]: +``` + +**Cause:** The destination file already exists. Applies to `snack unpack`, `snack pack`, and `snack stash add-remote`. + +**Fix:** +- Answer `y` at the prompt to overwrite +- Answer `n` (or press Enter) to abort +- Pass `--force` to skip the prompt: + +```bash +snack unpack auth/google_oauth.py --force +snack pack auth/google_oauth.py --force +snack stash add-remote owner/repo --force +``` + +--- + +## Stash name already exists (`stash create`) + +**Message:** +``` +[error] A stash named 'default' already exists. +``` + +**Cause:** You ran `snack stash create` with a name that is already registered in `~/.snackstashrc`. + +**Fix:** Choose a different name, or edit `~/.snackstashrc` directly to remove the existing entry first. + +--- + +## Unknown stash name (`stash move`) + +**Message:** +``` +[error] No stash named 'foo'. Run 'snack stash list' to see available stashes. +``` + +**Cause:** The name passed to `snack stash move` is not registered in `~/.snackstashrc`. + +**Fix:** Run `snack stash list` to see valid names. + +--- + +## Move target already exists (`stash move`) + +**Message:** +``` +[error] '/new/path' already exists. +``` + +**Cause:** The destination path for `snack stash move` is already present on disk. + +**Fix:** Choose a different target path, or remove the existing directory first if it is safe to do so. + +--- + +## GitHub repo not found or inaccessible (`stash add-remote`) + +**Message:** +``` +[error] HTTP 404: Not Found +``` + +**Cause:** The repository does not exist, is private, or the URL is incorrect. + +**Fix:** Check the repo name and ensure it is public. Private repos are not supported without a token (which is not currently a supported feature). + +--- + +## Invalid repo format (`stash add-remote`) + +**Message:** +``` +[error] Invalid repo 'not-a-repo'. Use 'owner/repo' or a full GitHub URL. +``` + +**Cause:** The argument passed to `snack stash add-remote` could not be parsed. + +**Fix:** Use one of the supported formats: + +```bash +snack stash add-remote owner/repo +snack stash add-remote https://github.com/owner/repo +``` diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md new file mode 100644 index 0000000..4195df1 --- /dev/null +++ b/docs/wiki/Home.md @@ -0,0 +1,39 @@ +# python-snacks + +A personal CLI tool for managing a local stash of reusable Python code snippets. Copy snippets in and out of projects with a single command, and manage multiple named stashes. + +## Quick Start + +```bash +# Install +pipx install python-snacks + +# Create your first stash +snack stash create default ~/snack-stash + +# Browse what you have +snack list + +# Pull snippets from a GitHub repo into your stash +snack stash add-remote owner/my-snippets + +# Copy a snippet into your project +snack unpack auth/google_oauth.py + +# Copy an improved snippet back into the stash +snack pack auth/google_oauth.py +``` + +## Pages + +- [[Installation]] — Install via pipx or pip +- [[Configuration]] — Named stashes, env var, and config file +- [[Commands-Reference]] — Full reference for all commands and flags +- [[Writing-Good-Snippets]] — How to write snippets that stay reusable +- [[Error-Reference]] — Diagnose and fix common errors +- [[Contributing]] — Dev setup, tests, and release process + +## Links + +- [PyPI — python-snacks](https://pypi.org/project/python-snacks/) +- [GitHub Repository](https://github.com/kickash/python-snacks) diff --git a/docs/wiki/Installation.md b/docs/wiki/Installation.md new file mode 100644 index 0000000..f07f075 --- /dev/null +++ b/docs/wiki/Installation.md @@ -0,0 +1,56 @@ +# Installation + +## Requirements + +- Python 3.10 or later + +## Recommended: pipx + +[pipx](https://pipx.pypa.io) installs CLI tools in isolated environments so they don't conflict with your project dependencies. + +```bash +pipx install python-snacks +``` + +## Alternative: pip + +```bash +pip install python-snacks +``` + +If you want it available globally, use `pip install --user python-snacks` or install inside a virtual environment you always activate. + +## Verify + +```bash +snack --help +``` + +You should see: + +``` +Usage: snack [OPTIONS] COMMAND [ARGS]... + + Manage your personal snack stash of reusable Python snippets. + +Commands: + unpack Copy a snippet FROM the stash INTO the current working directory. + pack Copy a snippet FROM the current working directory INTO the stash. + list List all snippets in the stash. + search Search snippet filenames for a keyword. + stash Manage stash directories. +``` + +## Upgrading + +```bash +pipx upgrade python-snacks +# or +pip install --upgrade python-snacks +``` + +New versions are published to PyPI automatically when a `v*` tag is pushed to the repository. + +## Next Step + +[[Configuration]] — create your first stash and tell the tool where it lives. diff --git a/docs/wiki/Writing-Good-Snippets.md b/docs/wiki/Writing-Good-Snippets.md new file mode 100644 index 0000000..8e69e25 --- /dev/null +++ b/docs/wiki/Writing-Good-Snippets.md @@ -0,0 +1,120 @@ +# Writing Good Snippets + +A snippet is only as useful as it is reusable. These guidelines help you write snippets that are easy to drop into any project and immediately understand. + +--- + +## The core principle: self-containment + +A snippet should work correctly after a single `cp` into a new project. That means: + +- All imports are explicit and at the top of the file +- All configuration comes from parameters or environment variables — never hardcoded values +- No assumptions about what else exists in the project +- No side effects at import time (wrap executable code in functions or `if __name__ == "__main__"`) + +If unpacking a snippet requires you to also copy another file, refactor until it doesn't. + +--- + +## Use a standard header + +The single most useful habit is a consistent docstring that captures three things: what the snippet does, what it depends on, and how to use it. Anyone (including future you) can read this without opening the file. + +```python +"""Send transactional email via SMTP. + +Dependencies: + stdlib only + +Usage: + from email.smtp_sender import send_email + send_email(to="user@example.com", subject="Hello", body="

World

") + +Config (env vars): + SMTP_HOST — mail server hostname + SMTP_PORT — defaults to 587 + SMTP_USER — sender login + SMTP_PASSWORD — sender password +""" +``` + +Keep it short. The goal is a five-second orientation, not full API documentation. + +--- + +## Name files to be searchable + +Filenames are the primary search surface — `snack search` matches against them. Choose names that are specific and predictable. + +**Good:** +``` +auth/google_oauth_fastapi.py +auth/google_oauth_flask.py +forms/contact_form_wtf.py +email/smtp_sender.py +storage/s3_upload.py +``` + +**Avoid:** +``` +auth/helpers.py # too vague +auth/utils.py # tells you nothing +auth/new_version.py # not a meaningful name +``` + +When a snippet has framework-specific variants, use a suffix: `_fastapi`, `_flask`, `_django`, `_sqlalchemy`. This makes `snack search fastapi` return only what's relevant. + +--- + +## One file, one job + +Resist the urge to bundle related utilities into a single file. A snippet called `auth_helpers.py` that contains OAuth, JWT, and session logic will only ever be partially useful. Three focused snippets are more valuable than one large one. + +The right size for a snippet is: "I need exactly this, and I copy exactly this." + +--- + +## Organise by what a project needs, not by technical category + +Think about how you reach for snippets when starting a project. Categories like `auth/`, `forms/`, `email/`, `storage/`, `payments/` map to features. Categories like `decorators/` or `metaclasses/` map to implementation details and are harder to browse purposefully. + +``` +~/snack-stash/ +├── auth/ # authentication and authorisation +├── email/ # sending and templating emails +├── forms/ # form handling and validation +├── payments/ # Stripe, etc. +├── storage/ # file uploads, S3, local disk +└── tasks/ # background jobs, queues +``` + +--- + +## Keep configuration out of the code + +Configuration that varies per-project (API keys, hostnames, feature flags) should be read from environment variables or passed as function arguments. Never commit a snippet with hardcoded credentials or URLs. + +```python +# Good — caller provides credentials +def create_oauth_client(client_id: str, client_secret: str) -> OAuth2Session: + ... + +# Good — reads from environment +STRIPE_KEY = os.environ["STRIPE_SECRET_KEY"] + +# Bad — hardcoded +STRIPE_KEY = "sk_live_abc123" +``` + +--- + +## Update the stash when you improve a snippet + +The stash is most valuable when it reflects your current best practice, not the version from two years ago. When you improve a snippet during a project, pack it back: + +```bash +snack pack auth/google_oauth_fastapi.py +``` + +If you manage your stash as a Git repo, commit the improvement there too. The stash stays useful only if it gets maintained like any other codebase.