repertory-api/README.md
2026-06-09 15:07:08 -04:00

216 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# repertory-api
Flask + SQLAlchemy REST API for the Repertory tune-tracking database.
## Stack
- **Flask 3** HTTP API
- **SQLAlchemy 2 / Flask-SQLAlchemy** ORM
- **PostgreSQL** database (`tunes`)
- **Alembic** migrations
- **uv** dependency management
- **Docker Compose** local development
- **NixOS systemd module** production deployment
---
## Local development
### First time setup
```bash
make local
```
This will:
1. Copy `env.example``.env` (skipped if `.env` already exists)
2. Start Postgres via Docker Compose
3. Wait for Postgres to be ready
4. Run Alembic migrations
5. Start the API container
At the end it prints the API URL and your local API key:
```
✅ Local environment is ready.
API: http://localhost:5000
API key: dev-secret-key-change-me
```
The API key is set in `.env` as `API_KEY`. Change it there if you want — it only affects your local dev environment. Production keys are managed via SOPS (see below).
### Subsequent runs
```bash
make local # idempotent — safe to re-run, skips what's already done
make stop # stop containers (data preserved)
make reset # stop containers AND wipe the database volume (fresh slate)
```
### Other useful commands
```bash
make test # run the test suite (SQLite in-memory, no Docker required)
make migrate # run pending Alembic migrations against the local DB
make logs # tail Docker Compose logs
make shell # open a psql shell in the running Postgres container
```
Run `make` or `make help` to see all available targets.
### Adding a schema migration
After changing a model in `app/models.py`:
```bash
# With the local DB running:
uv run alembic revision --autogenerate -m "describe your change"
make migrate
```
Commit both the model change and the new file in `migrations/versions/`.
---
## API Endpoints
### Health
| Method | Path | Description |
|--------|-----------|--------------|
| GET | `/health` | Health check |
### Tunes
| Method | Path | Description |
|--------|---------------|----------------------------------------------------------|
| GET | `/tunes/` | List all tunes (supports query filters — see below) |
| GET | `/tunes/<id>` | Get a single tune with all sub-records |
| POST | `/tunes/` | Create a tune (optionally with nested banjo/fiddle/etc.) |
| PATCH | `/tunes/<id>` | Update tune core fields |
| DELETE | `/tunes/<id>` | Delete a tune (cascades to all sub-records) |
**GET `/tunes/` query params** (all optional, combinable):
| Param | Example | Notes |
|--------------|----------------------|------------------------------------|
| `search` | `?search=cluck` | Substring match on name |
| `key` | `?key=D` | Exact key match (case-insensitive) |
| `instrument` | `?instrument=fiddle` | Filter by instrument |
| `tuning` | `?tuning=cross` | Substring match on tuning |
| `callable` | `?callable=true` | Boolean |
| `review` | `?review=false` | Boolean |
| `to_learn` | `?to_learn=true` | Boolean |
| `difficulty` | `?difficulty=easy` | "easy" or "hard" |
`instrument` must be set for `tuning`, `callable`, `review`, `to_learn`, and `difficulty` filters to apply.
### Instrument sub-records (banjo / fiddle)
| Method | Path | Description |
|--------|-----------------------|-------------------------------------|
| PUT | `/tunes/<id>/banjo` | Create or fully replace banjo entry |
| PUT | `/tunes/<id>/fiddle` | Create or fully replace fiddle entry|
| PATCH | `/tunes/<id>/banjo` | Partially update banjo entry |
| PATCH | `/tunes/<id>/fiddle` | Partially update fiddle entry |
| DELETE | `/tunes/<id>/banjo` | Remove banjo entry |
| DELETE | `/tunes/<id>/fiddle` | Remove fiddle entry |
Instrument fields: `learned_from`, `date_learned` (ISO date string), `tuning`, `callable` (bool), `review` (bool), `to_learn` (bool), `difficulty` ("easy"/"hard").
### Notes
| Method | Path | Description |
|--------|-----------------------------------|---------------|
| GET | `/tunes/<id>/notes` | List notes |
| POST | `/tunes/<id>/notes` | Add a note |
| PATCH | `/tunes/<id>/notes/<note_id>` | Update a note |
| DELETE | `/tunes/<id>/notes/<note_id>` | Delete a note |
### References
| Method | Path | Description |
|--------|----------------------------------------------------|--------------------|
| GET | `/tunes/<id>/references` | List references |
| POST | `/tunes/<id>/references` | Add a reference |
| PATCH | `/tunes/<id>/references/<ref_id>` | Update a reference |
| DELETE | `/tunes/<id>/references/<ref_id>` | Delete a reference |
| POST | `/tunes/<id>/references/<ref_id>/musicians` | Add a musician tag |
| DELETE | `/tunes/<id>/references/<ref_id>/musicians/<name>` | Remove a musician |
---
## Production deployment (NixOS + SOPS + age)
The NixOS module in [`nixos/module.nix`](nixos/module.nix) fetches the application source directly from your Forgejo instance at a pinned tag, builds the Python environment, and manages the service under systemd — no manual file copying required.
### 1. Add secrets to your SOPS file
```yaml
repertory-api-key: "your-api-key-here"
repertory-db-url: "postgresql://user:pass@host:5432/tunes"
```
### 2. Wire up the module in `configuration.nix`
```nix
imports = [ (fetchTarball {
url = "https://git.yourdomain.com/yourusername/repertory-api/archive/v1.0.0.tar.gz";
sha256 = "0000000000000000000000000000000000000000000000000000";
} + "/nixos/module.nix") ];
sops.secrets."repertory-api-key" = {};
sops.secrets."repertory-db-url" = {};
services.repertory-api = {
enable = true;
domain = "git.yourdomain.com";
owner = "yourusername";
version = "v1.0.0";
rev = "abc123..."; # git commit SHA for the tag
sha256 = "sha256-..."; # see below
};
```
### 3. Deploy
```bash
nixos-rebuild switch
```
On start, the service automatically runs `alembic upgrade head` before Flask comes up — so migrations are applied on every deploy with no manual step.
### Updating to a new version
1. Push a new tag to Forgejo (e.g. `v1.1.0`)
2. Get the new sha256:
```bash
nix-prefetch-url --unpack \
https://git.yourdomain.com/yourusername/repertory-api/archive/v1.1.0.tar.gz
```
3. Update `version`, `rev`, and `sha256` in `configuration.nix`
4. `nixos-rebuild switch` — Nix fetches the new source, rebuilds the Python env, restarts the service, and runs any new migrations
### How secrets are handled
The `preStart` script reads the SOPS-decrypted secret files (written to `/run/secrets/` by sops-nix) and assembles them into a mode-600 env file at `/run/repertory-api/env` on tmpfs. The Flask process receives `API_KEY` and `DATABASE_URL` from that file. Nothing is written to disk in plaintext.
---
## Database schema
```
musicians (id, name)
sources (id, name)
tunings (id, name)
instruments (id, name)
tunes (id, name, key, modal, source_id → sources.id)
├── tune_by_instrument (id, tune_id, instrument_id, learned_from_id, tuning_id, date_learned, callable, review, to_learn, difficulty)
│ └── tune_by_instrument_notes (id, tune_by_instrument_id, note)
├── tune_notes (id, tune_id, note)
└── references (id, tune_id, link, site)
└── reference_musicians (reference_id, musician_id) ← join table
```
To add a new instrument (e.g. harmonica): add a model in `app/models.py` mirroring `Banjo`/`Fiddle`, register the name in `INSTRUMENT_MODELS` in `app/routes_instruments.py`, then `uv run alembic revision --autogenerate -m "add harmonica"` and `make migrate`.