Initial Commit
slip slop
This commit is contained in:
commit
064e672ab7
61 changed files with 3504 additions and 0 deletions
53
app/__init__.py
Normal file
53
app/__init__.py
Normal 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
17
app/auth.py
Normal 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
3
app/extensions.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
175
app/models.py
Normal file
175
app/models.py
Normal 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
125
app/routes_instruments.py
Normal 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
43
app/routes_musicians.py
Normal 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
43
app/routes_sources.py
Normal 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
124
app/routes_sub.py
Normal 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
155
app/routes_tunes.py
Normal 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
43
app/routes_tunings.py
Normal 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})
|
||||
Loading…
Add table
Add a link
Reference in a new issue