Initial Commit
slip slop
This commit is contained in:
commit
064e672ab7
61 changed files with 3504 additions and 0 deletions
216
README.md
Normal file
216
README.md
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
# 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`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue