Initial Commit

slip slop
This commit is contained in:
Ian Keane 2026-06-09 15:07:08 -04:00
commit 064e672ab7
61 changed files with 3504 additions and 0 deletions

53
app/__init__.py Normal file
View file

@ -0,0 +1,53 @@
"""Flask application factory."""
import os
from flask import Flask
from flask_cors import CORS
from app.extensions import db
from app.routes_tunes import tunes_bp
from app.routes_instruments import instruments_bp
from app.routes_sub import sub_bp
from app.routes_sources import sources_bp
from app.routes_musicians import musicians_bp
from app.routes_tunings import tunings_bp
def create_app():
app = Flask(__name__)
# --- Config ---
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ["DATABASE_URL"]
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
# --- CORS ---
# CORS_ORIGINS in .env is a comma-separated list of allowed origins.
# Defaults to * for local dev. Lock this down in production to your
# S3 bucket / CloudFront domain, e.g.:
# CORS_ORIGINS=https://yourdomain.com,https://www.yourdomain.com
raw = os.environ.get("CORS_ORIGINS", "*")
origins = [o.strip() for o in raw.split(",")] if raw != "*" else "*"
CORS(app, origins=origins, supports_credentials=False)
# --- Extensions ---
db.init_app(app)
# --- Blueprints ---
app.register_blueprint(tunes_bp)
app.register_blueprint(instruments_bp)
app.register_blueprint(sub_bp)
app.register_blueprint(sources_bp)
app.register_blueprint(musicians_bp)
app.register_blueprint(tunings_bp)
# --- Error handlers ---
from sqlalchemy.exc import IntegrityError
@app.errorhandler(IntegrityError)
def handle_integrity_error(e):
db.session.rollback()
return {"error": "Conflict — duplicate or constraint violation"}, 409
@app.get("/health")
def health():
return {"status": "ok"}
return app

17
app/auth.py Normal file
View file

@ -0,0 +1,17 @@
"""Simple API-key authentication decorator."""
import os
from functools import wraps
from flask import request, jsonify
def require_api_key(f):
@wraps(f)
def decorated(*args, **kwargs):
key = request.headers.get("X-API-Key") or request.args.get("api_key")
expected = os.environ.get("API_KEY", "")
if not expected:
return jsonify({"error": "API key not configured on server"}), 500
if key != expected:
return jsonify({"error": "Unauthorized"}), 401
return f(*args, **kwargs)
return decorated

3
app/extensions.py Normal file
View file

@ -0,0 +1,3 @@
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

175
app/models.py Normal file
View file

@ -0,0 +1,175 @@
from sqlalchemy import Boolean, Column, Date, ForeignKey, Integer, String, Table, UniqueConstraint
from app.extensions import db
# ── Many-to-many: references ↔ musicians ─────────────────────
reference_musicians = Table(
"reference_musicians",
db.metadata,
Column("reference_id", Integer, ForeignKey("references.id", ondelete="CASCADE"), primary_key=True),
Column("musician_id", Integer, ForeignKey("musicians.id", ondelete="CASCADE"), primary_key=True),
)
class Musician(db.Model):
__tablename__ = "musicians"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False)
references = db.relationship("Reference", secondary=reference_musicians, back_populates="musicians")
instrument_entries = db.relationship("TuneByInstrument", back_populates="learned_from_musician")
def to_dict(self):
return {"id": self.id, "name": self.name}
class Source(db.Model):
__tablename__ = "sources"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False)
tunes = db.relationship("Tune", back_populates="source")
def to_dict(self):
return {"id": self.id, "name": self.name}
class Instrument(db.Model):
__tablename__ = "instruments"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False, unique=True)
entries = db.relationship("TuneByInstrument", back_populates="instrument")
def to_dict(self):
return {"id": self.id, "name": self.name}
class Tuning(db.Model):
__tablename__ = "tunings"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False, unique=True)
entries = db.relationship("TuneByInstrument", back_populates="tuning")
def to_dict(self):
return {"id": self.id, "name": self.name}
class Tune(db.Model):
__tablename__ = "tunes"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=True)
key = Column(String, nullable=True)
modal = Column(Boolean, nullable=True)
source_id = Column(Integer, ForeignKey("sources.id", ondelete="SET NULL"), nullable=True)
source = db.relationship("Source", back_populates="tunes")
instruments = db.relationship("TuneByInstrument", back_populates="tune", cascade="all, delete-orphan")
notes = db.relationship("TuneNote", back_populates="tune", cascade="all, delete-orphan")
references = db.relationship("Reference", back_populates="tune", cascade="all, delete-orphan")
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"key": self.key,
"modal": self.modal,
"source_id": self.source_id,
"source": self.source.to_dict() if self.source else None,
"instruments": [i.to_dict() for i in self.instruments],
"notes": [n.to_dict() for n in self.notes],
"references": [r.to_dict() for r in self.references],
}
class TuneByInstrument(db.Model):
__tablename__ = "tune_by_instrument"
__table_args__ = (UniqueConstraint("tune_id", "instrument_id", name="uq_tune_instrument"),)
id = Column(Integer, primary_key=True, autoincrement=True)
tune_id = Column(Integer, ForeignKey("tunes.id", ondelete="CASCADE"), nullable=False)
instrument_id = Column(Integer, ForeignKey("instruments.id", ondelete="CASCADE"), nullable=False)
learned_from_id = Column(Integer, ForeignKey("musicians.id", ondelete="SET NULL"), nullable=True)
tuning_id = Column(Integer, ForeignKey("tunings.id", ondelete="SET NULL"), nullable=True)
date_learned = Column(Date, nullable=True)
callable = Column(Boolean, nullable=True)
review = Column(Boolean, nullable=True)
to_learn = Column(Boolean, nullable=True, default=False)
difficulty = Column(String, nullable=True)
tune = db.relationship("Tune", back_populates="instruments")
instrument = db.relationship("Instrument", back_populates="entries")
tuning = db.relationship("Tuning", back_populates="entries")
learned_from_musician = db.relationship("Musician", back_populates="instrument_entries")
notes = db.relationship("TuneByInstrumentNote", back_populates="entry", cascade="all, delete-orphan")
def to_dict(self):
return {
"id": self.id,
"tune_id": self.tune_id,
"instrument_id": self.instrument_id,
"instrument_name": self.instrument.name if self.instrument else None,
"learned_from_id": self.learned_from_id,
"learned_from": self.learned_from_musician.name if self.learned_from_musician else None,
"date_learned": self.date_learned.isoformat() if self.date_learned else None,
"tuning_id": self.tuning_id,
"tuning": self.tuning.name if self.tuning else None,
"callable": self.callable,
"review": self.review,
"to_learn": self.to_learn,
"difficulty": self.difficulty,
"notes": [n.to_dict() for n in self.notes],
}
class TuneNote(db.Model):
__tablename__ = "tune_notes"
id = Column(Integer, primary_key=True, autoincrement=True)
tune_id = Column(Integer, ForeignKey("tunes.id", ondelete="CASCADE"), nullable=False)
note = Column(String, nullable=True)
tune = db.relationship("Tune", back_populates="notes")
def to_dict(self):
return {"id": self.id, "tune_id": self.tune_id, "note": self.note}
class TuneByInstrumentNote(db.Model):
__tablename__ = "tune_by_instrument_notes"
id = Column(Integer, primary_key=True, autoincrement=True)
tune_by_instrument_id = Column(Integer, ForeignKey("tune_by_instrument.id", ondelete="CASCADE"), nullable=False)
note = Column(String, nullable=True)
entry = db.relationship("TuneByInstrument", back_populates="notes")
def to_dict(self):
return {"id": self.id, "tune_by_instrument_id": self.tune_by_instrument_id, "note": self.note}
class Reference(db.Model):
__tablename__ = "references"
id = Column(Integer, primary_key=True, autoincrement=True)
tune_id = Column(Integer, ForeignKey("tunes.id", ondelete="CASCADE"), nullable=True)
link = Column(String, nullable=True)
site = Column(String, nullable=True)
tune = db.relationship("Tune", back_populates="references")
musicians = db.relationship("Musician", secondary=reference_musicians, back_populates="references")
def to_dict(self):
return {
"id": self.id,
"tune_id": self.tune_id,
"link": self.link,
"site": self.site,
"musicians": [m.to_dict() for m in self.musicians],
}

125
app/routes_instruments.py Normal file
View file

@ -0,0 +1,125 @@
"""Routes for tune_by_instrument entries and their notes."""
from flask import Blueprint, jsonify, request
from app.extensions import db
from app.models import Tune, Instrument, TuneByInstrument, TuneByInstrumentNote
from app.auth import require_api_key
instruments_bp = Blueprint("instruments", __name__)
# ---------------------------------------------------------------------------
# Instruments lookup (public)
# ---------------------------------------------------------------------------
@instruments_bp.get("/instruments")
def list_instruments():
return jsonify([i.to_dict() for i in
db.session.query(Instrument).order_by(Instrument.name).all()])
# ---------------------------------------------------------------------------
# List all tune_by_instrument entries for a tune
# ---------------------------------------------------------------------------
@instruments_bp.get("/tunes/<int:tune_id>/instruments")
def list_tune_instruments(tune_id):
db.get_or_404(Tune, tune_id)
entries = db.session.query(TuneByInstrument).filter_by(tune_id=tune_id).all()
return jsonify([e.to_dict() for e in entries])
# ---------------------------------------------------------------------------
# Create a tune_by_instrument entry
# ---------------------------------------------------------------------------
@instruments_bp.post("/tunes/<int:tune_id>/instruments")
@require_api_key
def create_tune_instrument(tune_id):
db.get_or_404(Tune, tune_id)
data = request.get_json(force=True)
# Validate instrument exists
instrument_id = data.get("instrument_id")
db.get_or_404(Instrument, instrument_id)
entry = TuneByInstrument(
tune_id=tune_id,
instrument_id=instrument_id,
learned_from_id=data.get("learned_from_id"),
tuning_id=data.get("tuning_id"),
date_learned=data.get("date_learned"),
callable=data.get("callable"),
review=data.get("review"),
to_learn=data.get("to_learn", False),
difficulty=data.get("difficulty"),
)
db.session.add(entry)
db.session.commit()
return jsonify(entry.to_dict()), 201
# ---------------------------------------------------------------------------
# Update a tune_by_instrument entry
# ---------------------------------------------------------------------------
@instruments_bp.patch("/tunes/<int:tune_id>/instruments/<int:entry_id>")
@require_api_key
def update_tune_instrument(tune_id, entry_id):
entry = db.get_or_404(TuneByInstrument, entry_id)
data = request.get_json(force=True)
fields = ("instrument_id", "learned_from_id", "tuning_id", "date_learned",
"callable", "review", "to_learn", "difficulty")
for field in fields:
if field in data:
setattr(entry, field, data[field])
db.session.commit()
return jsonify(entry.to_dict())
# ---------------------------------------------------------------------------
# Delete a tune_by_instrument entry
# ---------------------------------------------------------------------------
@instruments_bp.delete("/tunes/<int:tune_id>/instruments/<int:entry_id>")
@require_api_key
def delete_tune_instrument(tune_id, entry_id):
entry = db.get_or_404(TuneByInstrument, entry_id)
db.session.delete(entry)
db.session.commit()
return jsonify({"deleted": entry_id})
# ---------------------------------------------------------------------------
# Notes on a tune_by_instrument entry
# ---------------------------------------------------------------------------
@instruments_bp.get("/tunes/<int:tune_id>/instruments/<int:entry_id>/notes")
def list_instrument_notes(tune_id, entry_id):
db.get_or_404(TuneByInstrument, entry_id)
notes = db.session.query(TuneByInstrumentNote).filter_by(tune_by_instrument_id=entry_id).all()
return jsonify([n.to_dict() for n in notes])
@instruments_bp.post("/tunes/<int:tune_id>/instruments/<int:entry_id>/notes")
@require_api_key
def add_instrument_note(tune_id, entry_id):
db.get_or_404(TuneByInstrument, entry_id)
data = request.get_json(force=True)
note = TuneByInstrumentNote(tune_by_instrument_id=entry_id, note=data.get("note"))
db.session.add(note)
db.session.commit()
return jsonify(note.to_dict()), 201
@instruments_bp.patch("/tunes/<int:tune_id>/instruments/<int:entry_id>/notes/<int:note_id>")
@require_api_key
def update_instrument_note(tune_id, entry_id, note_id):
note = db.get_or_404(TuneByInstrumentNote, note_id)
data = request.get_json(force=True)
if "note" in data:
note.note = data["note"]
db.session.commit()
return jsonify(note.to_dict())
@instruments_bp.delete("/tunes/<int:tune_id>/instruments/<int:entry_id>/notes/<int:note_id>")
@require_api_key
def delete_instrument_note(tune_id, entry_id, note_id):
note = db.get_or_404(TuneByInstrumentNote, note_id)
db.session.delete(note)
db.session.commit()
return jsonify({"deleted": note_id})

43
app/routes_musicians.py Normal file
View file

@ -0,0 +1,43 @@
"""Routes for the standalone musicians table."""
from flask import Blueprint, jsonify, request
from app.extensions import db
from app.models import Musician
from app.auth import require_api_key
musicians_bp = Blueprint("musicians", __name__, url_prefix="/musicians")
@musicians_bp.get("/")
def list_musicians():
musicians = db.session.query(Musician).order_by(Musician.name).all()
return jsonify([m.to_dict() for m in musicians])
@musicians_bp.post("/")
@require_api_key
def create_musician():
data = request.get_json(force=True)
musician = Musician(name=data["name"])
db.session.add(musician)
db.session.commit()
return jsonify(musician.to_dict()), 201
@musicians_bp.patch("/<int:musician_id>")
@require_api_key
def update_musician(musician_id):
musician = db.get_or_404(Musician, musician_id)
data = request.get_json(force=True)
if "name" in data:
musician.name = data["name"]
db.session.commit()
return jsonify(musician.to_dict())
@musicians_bp.delete("/<int:musician_id>")
@require_api_key
def delete_musician(musician_id):
musician = db.get_or_404(Musician, musician_id)
db.session.delete(musician)
db.session.commit()
return jsonify({"deleted": musician_id})

43
app/routes_sources.py Normal file
View file

@ -0,0 +1,43 @@
"""Routes for sources (musicians/artists whose version a tune is based on)."""
from flask import Blueprint, jsonify, request
from app.extensions import db
from app.models import Source
from app.auth import require_api_key
sources_bp = Blueprint("sources", __name__, url_prefix="/sources")
@sources_bp.get("/")
def list_sources():
sources = db.session.query(Source).order_by(Source.name).all()
return jsonify([s.to_dict() for s in sources])
@sources_bp.post("/")
@require_api_key
def create_source():
data = request.get_json(force=True)
source = Source(name=data["name"])
db.session.add(source)
db.session.commit()
return jsonify(source.to_dict()), 201
@sources_bp.patch("/<int:source_id>")
@require_api_key
def update_source(source_id):
source = db.get_or_404(Source, source_id)
data = request.get_json(force=True)
if "name" in data:
source.name = data["name"]
db.session.commit()
return jsonify(source.to_dict())
@sources_bp.delete("/<int:source_id>")
@require_api_key
def delete_source(source_id):
source = db.get_or_404(Source, source_id)
db.session.delete(source)
db.session.commit()
return jsonify({"deleted": source_id})

124
app/routes_sub.py Normal file
View file

@ -0,0 +1,124 @@
"""Routes for notes and references sub-resources."""
from flask import Blueprint, jsonify, request
from app.extensions import db
from app.models import Tune, TuneNote, Reference, Musician
from app.auth import require_api_key
sub_bp = Blueprint("sub", __name__)
# ===========================================================================
# Notes
# ===========================================================================
@sub_bp.get("/tunes/<int:tune_id>/notes")
def list_notes(tune_id):
db.get_or_404(Tune, tune_id)
notes = db.session.query(TuneNote).filter_by(tune_id=tune_id).all()
return jsonify([n.to_dict() for n in notes])
@sub_bp.post("/tunes/<int:tune_id>/notes")
@require_api_key
def add_note(tune_id):
db.get_or_404(Tune, tune_id)
data = request.get_json(force=True)
note = TuneNote(tune_id=tune_id, note=data.get("note"))
db.session.add(note)
db.session.commit()
return jsonify(note.to_dict()), 201
@sub_bp.patch("/tunes/<int:tune_id>/notes/<int:note_id>")
@require_api_key
def update_note(tune_id, note_id):
note = db.get_or_404(TuneNote, note_id)
data = request.get_json(force=True)
if "note" in data:
note.note = data["note"]
db.session.commit()
return jsonify(note.to_dict())
@sub_bp.delete("/tunes/<int:tune_id>/notes/<int:note_id>")
@require_api_key
def delete_note(tune_id, note_id):
note = db.get_or_404(TuneNote, note_id)
db.session.delete(note)
db.session.commit()
return jsonify({"deleted": note_id})
# ===========================================================================
# References
# ===========================================================================
@sub_bp.get("/tunes/<int:tune_id>/references")
def list_references(tune_id):
db.get_or_404(Tune, tune_id)
refs = db.session.query(Reference).filter_by(tune_id=tune_id).all()
return jsonify([r.to_dict() for r in refs])
@sub_bp.post("/tunes/<int:tune_id>/references")
@require_api_key
def add_reference(tune_id):
db.get_or_404(Tune, tune_id)
data = request.get_json(force=True)
ref = Reference(tune_id=tune_id, link=data.get("link"), site=data.get("site"))
db.session.add(ref)
db.session.flush()
for m in data.get("musicians", []):
musician = db.session.get(Musician, m["id"])
if musician:
ref.musicians.append(musician)
db.session.commit()
return jsonify(ref.to_dict()), 201
@sub_bp.patch("/tunes/<int:tune_id>/references/<int:ref_id>")
@require_api_key
def update_reference(tune_id, ref_id):
ref = db.get_or_404(Reference, ref_id)
data = request.get_json(force=True)
for field in ("link", "site"):
if field in data:
setattr(ref, field, data[field])
db.session.commit()
return jsonify(ref.to_dict())
@sub_bp.delete("/tunes/<int:tune_id>/references/<int:ref_id>")
@require_api_key
def delete_reference(tune_id, ref_id):
ref = db.get_or_404(Reference, ref_id)
db.session.delete(ref)
db.session.commit()
return jsonify({"deleted": ref_id})
# ===========================================================================
# Musicians on a reference (add/remove via join table)
# ===========================================================================
@sub_bp.post("/tunes/<int:tune_id>/references/<int:ref_id>/musicians")
@require_api_key
def add_musician_to_reference(tune_id, ref_id):
ref = db.get_or_404(Reference, ref_id)
data = request.get_json(force=True)
musician = db.get_or_404(Musician, data["musician_id"])
if musician not in ref.musicians:
ref.musicians.append(musician)
db.session.commit()
return jsonify(ref.to_dict())
@sub_bp.delete("/tunes/<int:tune_id>/references/<int:ref_id>/musicians/<int:musician_id>")
@require_api_key
def remove_musician_from_reference(tune_id, ref_id, musician_id):
ref = db.get_or_404(Reference, ref_id)
musician = db.get_or_404(Musician, musician_id)
if musician in ref.musicians:
ref.musicians.remove(musician)
db.session.commit()
return jsonify(ref.to_dict())

155
app/routes_tunes.py Normal file
View file

@ -0,0 +1,155 @@
"""Routes for the core tunes table."""
from flask import Blueprint, jsonify, request
from app.extensions import db
from app.models import Tune, TuneByInstrument, TuneNote, Reference, Musician
from app.auth import require_api_key
tunes_bp = Blueprint("tunes", __name__, url_prefix="/tunes")
# ---------------------------------------------------------------------------
# List / filter
# ---------------------------------------------------------------------------
@tunes_bp.get("/")
def list_tunes():
"""
Query params (all optional, combinable):
instrument_id=1
key=D
tuning_id=1
callable=true|false
review=true|false
to_learn=true|false
difficulty=easy|hard
modal=true|false
source_id=1
search=<substring match on name>
"""
q = db.session.query(Tune)
search = request.args.get("search")
if search:
q = q.filter(Tune.name.ilike(f"%{search}%"))
key = request.args.get("key")
if key:
q = q.filter(Tune.key.ilike(key))
modal = request.args.get("modal")
if modal is not None:
q = q.filter(Tune.modal == (modal.lower() in ("true", "1", "yes")))
source_id = request.args.get("source_id")
if source_id:
q = q.filter(Tune.source_id == int(source_id))
instrument_id = request.args.get("instrument_id")
tuning = request.args.get("tuning")
callable_ = request.args.get("callable")
review = request.args.get("review")
to_learn = request.args.get("to_learn")
difficulty = request.args.get("difficulty")
def _bool(val):
return val.lower() in ("true", "1", "yes")
if any([instrument_id, tuning, callable_, review, to_learn, difficulty]):
q = q.join(Tune.instruments)
if instrument_id:
q = q.filter(TuneByInstrument.instrument_id == int(instrument_id))
if tuning:
q = q.filter(TuneByInstrument.tuning_id == int(tuning))
if callable_ is not None:
q = q.filter(TuneByInstrument.callable == _bool(callable_))
if review is not None:
q = q.filter(TuneByInstrument.review == _bool(review))
if to_learn is not None:
q = q.filter(TuneByInstrument.to_learn == _bool(to_learn))
if difficulty:
q = q.filter(TuneByInstrument.difficulty.ilike(difficulty))
tunes = q.order_by(Tune.name).all()
return jsonify([t.to_dict() for t in tunes])
# ---------------------------------------------------------------------------
# Get single tune
# ---------------------------------------------------------------------------
@tunes_bp.get("/<int:tune_id>")
def get_tune(tune_id):
tune = db.get_or_404(Tune, tune_id)
return jsonify(tune.to_dict())
# ---------------------------------------------------------------------------
# Create tune (with optional nested instruments / notes / references)
# ---------------------------------------------------------------------------
@tunes_bp.post("/")
@require_api_key
def create_tune():
data = request.get_json(force=True)
tune = Tune(
name=data.get("name"),
key=data.get("key"),
modal=data.get("modal"),
source_id=data.get("source_id"),
)
db.session.add(tune)
db.session.flush()
for inst_data in data.get("instruments", []):
entry = TuneByInstrument(
tune_id=tune.id,
instrument_id=inst_data["instrument_id"],
learned_from_id=inst_data.get("learned_from_id"),
tuning_id=inst_data.get("tuning_id"),
date_learned=inst_data.get("date_learned"),
callable=inst_data.get("callable"),
review=inst_data.get("review"),
to_learn=inst_data.get("to_learn", False),
difficulty=inst_data.get("difficulty"),
)
db.session.add(entry)
for note_data in data.get("notes", []):
db.session.add(TuneNote(tune_id=tune.id, note=note_data.get("note")))
for ref_data in data.get("references", []):
ref = Reference(tune_id=tune.id, link=ref_data.get("link"), site=ref_data.get("site"))
db.session.add(ref)
db.session.flush()
for m in ref_data.get("musicians", []):
musician = db.session.get(Musician, m["id"])
if musician:
ref.musicians.append(musician)
db.session.commit()
return jsonify(tune.to_dict()), 201
# ---------------------------------------------------------------------------
# Update tune core fields
# ---------------------------------------------------------------------------
@tunes_bp.patch("/<int:tune_id>")
@require_api_key
def update_tune(tune_id):
tune = db.get_or_404(Tune, tune_id)
data = request.get_json(force=True)
for field in ("name", "key", "modal", "source_id"):
if field in data:
setattr(tune, field, data[field])
db.session.commit()
return jsonify(tune.to_dict())
# ---------------------------------------------------------------------------
# Delete tune
# ---------------------------------------------------------------------------
@tunes_bp.delete("/<int:tune_id>")
@require_api_key
def delete_tune(tune_id):
tune = db.get_or_404(Tune, tune_id)
db.session.delete(tune)
db.session.commit()
return jsonify({"deleted": tune_id})

43
app/routes_tunings.py Normal file
View file

@ -0,0 +1,43 @@
"""Routes for the tunings lookup table."""
from flask import Blueprint, jsonify, request
from app.extensions import db
from app.models import Tuning
from app.auth import require_api_key
tunings_bp = Blueprint("tunings", __name__, url_prefix="/tunings")
@tunings_bp.get("/")
def list_tunings():
return jsonify([t.to_dict() for t in
db.session.query(Tuning).order_by(Tuning.name).all()])
@tunings_bp.post("/")
@require_api_key
def create_tuning():
data = request.get_json(force=True)
tuning = Tuning(name=data["name"])
db.session.add(tuning)
db.session.commit()
return jsonify(tuning.to_dict()), 201
@tunings_bp.patch("/<int:tuning_id>")
@require_api_key
def update_tuning(tuning_id):
tuning = db.get_or_404(Tuning, tuning_id)
data = request.get_json(force=True)
if "name" in data:
tuning.name = data["name"]
db.session.commit()
return jsonify(tuning.to_dict())
@tunings_bp.delete("/<int:tuning_id>")
@require_api_key
def delete_tuning(tuning_id):
tuning = db.get_or_404(Tuning, tuning_id)
db.session.delete(tuning)
db.session.commit()
return jsonify({"deleted": tuning_id})