Adding database support, basic reads

This commit is contained in:
evilchili 2022-11-20 01:00:54 -08:00
parent 5f874c7431
commit d67eee9121
7 changed files with 219 additions and 10 deletions

View File

@ -3,12 +3,28 @@ import os
import typer import typer
from dotenv import load_dotenv from dotenv import load_dotenv
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from groove import ondemand from groove import ondemand
from groove.db import metadata
app = typer.Typer() app = typer.Typer()
@app.command()
def initialize():
load_dotenv()
# todo: abstract this and replace in_memory_db fixture
engine = create_engine(f"sqlite:///{os.environ.get('DATABASE_PATH')}", future=True)
Session = sessionmaker(bind=engine, future=True)
session = Session()
metadata.create_all(bind=engine)
session.close()
@app.command() @app.command()
def server( def server(
host: str = typer.Argument( host: str = typer.Argument(
@ -29,6 +45,8 @@ def server(
""" """
load_dotenv() load_dotenv()
ondemand.initialize()
print("Starting Groove On Demand...") print("Starting Groove On Demand...")
debug = os.getenv('DEBUG', None) debug = os.getenv('DEBUG', None)

29
groove/db.py Normal file
View File

@ -0,0 +1,29 @@
from sqlalchemy import MetaData
from sqlalchemy import Table, Column, Integer, String, UnicodeText, ForeignKey, PrimaryKeyConstraint
metadata = MetaData()
track = Table(
"track",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("relpath", UnicodeText, index=True, unique=True),
)
playlist = Table(
"playlist",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("name", String),
Column("description", UnicodeText),
Column("slug", String, index=True, unique=True),
)
entry = Table(
"entry",
metadata,
Column("track", Integer),
Column("playlist_id", Integer, ForeignKey("playlist.id")),
Column("track_id", Integer, ForeignKey("track.id")),
PrimaryKeyConstraint("playlist_id", "track"),
)

49
groove/helper.py Normal file
View File

@ -0,0 +1,49 @@
import json
import logging
from bottle import HTTPResponse
from sqlalchemy import bindparam
from groove import db
class PlaylistDatabaseHelper:
"""
Convenience class for database interactions.
"""
def __init__(self, connection):
self._conn = connection
@property
def conn(self):
return self._conn
def playlist(self, slug: str) -> dict:
"""
Retrieve a playlist and its entries by its slug.
"""
playlist = {}
query = db.playlist.select(db.playlist.c.slug==bindparam('slug'))
logging.debug(f"playlist: '{slug}' requested. Query: {query}")
results = self.conn.execute(str(query), {'slug': slug}).fetchone()
if not results:
return playlist
playlist = results
query = db.entry.select(db.entry.c.playlist_id == bindparam('playlist_id'))
logging.debug(f"Retrieving playlist entries. Query: {query}")
entries = self.conn.execute(str(query), {'playlist_id': playlist['id']}).fetchall()
playlist = dict(playlist)
playlist['entries'] = [dict(entry) for entry in entries]
return playlist
def json_response(self, playlist: dict, status: int = 200) -> HTTPResponse:
"""
Create an application/json HTTPResponse object out of a playlist and its entries.
"""
response = json.dumps(playlist)
logging.debug(response)
return HTTPResponse(status=status, content_type='application/json', body=response)

View File

@ -1,7 +1,21 @@
from bottle import Bottle, auth_basic import logging
from groove.auth import is_authenticated import os
server = Bottle() import bottle
from bottle import HTTPResponse
from bottle.ext import sqlite
from groove.auth import is_authenticated
from groove.helper import PlaylistDatabaseHelper
server = bottle.Bottle()
def initialize():
"""
Configure the sqlite database.
"""
server.install(sqlite.Plugin(dbfile=os.environ.get('DATABASE_PATH')))
@server.route('/') @server.route('/')
@ -9,7 +23,20 @@ def index():
return "Groovy." return "Groovy."
@server.route('/admin') @server.route('/build')
@auth_basic(is_authenticated) @bottle.auth_basic(is_authenticated)
def admin(): def build():
return "Authenticated. Groovy." return "Authenticated. Groovy."
@server.route('/playlist/<slug>')
def get_playlist(slug, db):
"""
Retrieve a playlist and its entries by a slug.
"""
logging.debug(f"Looking up playlist: {slug}...")
pldb = PlaylistDatabaseHelper(connection=db)
playlist = pldb.playlist(slug)
if not playlist:
return HTTPResponse(status=404, body="Not found")
return pldb.json_response(playlist)

View File

@ -14,6 +14,8 @@ bottle = "^0.12.23"
typer = "^0.7.0" typer = "^0.7.0"
python-dotenv = "^0.21.0" python-dotenv = "^0.21.0"
Paste = "^3.5.2" Paste = "^3.5.2"
bottle-sqlite = "^0.2.0"
SQLAlchemy = "^1.4.44"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^7.2.0" pytest = "^7.2.0"

56
test/conftest.py Normal file
View File

@ -0,0 +1,56 @@
import pytest
import groove.db
from sqlalchemy import create_engine, insert
from sqlalchemy.orm import sessionmaker
@pytest.fixture(scope='function')
def in_memory_db():
"""
An (empty) in-memory SQLite3 database
"""
engine = create_engine('sqlite:///:memory:', future=True)
Session = sessionmaker(bind=engine, future=True)
session = Session()
groove.db.metadata.create_all(bind=engine)
yield session
session.close()
@pytest.fixture(scope='function')
def db(in_memory_db):
"""
Populate the in-memory sqlite database with fixture data.
"""
# create tracks
query = insert(groove.db.track)
in_memory_db.execute(query, [
{'id': 1, 'relpath': '/UNKLE/Psyence Fiction/01 Guns Blazing (Drums of Death, Part 1).flac'},
{'id': 2, 'relpath': '/UNKLE/Psyence Fiction/02 UNKLE (Main Title Theme).flac'},
{'id': 3, 'relpath': '/UNKLE/Psyence Fiction/03 Bloodstain.flac'}
])
# create playlists
query = insert(groove.db.playlist)
in_memory_db.execute(query, [
{'id': 1, 'name': 'playlist one', 'description': 'the first one', 'slug': 'playlist-one'},
{'id': 2, 'name': 'playlist two', 'description': 'the second one', 'slug': 'playlist-two'},
{'id': 3, 'name': 'playlist three', 'description': 'the threerd one', 'slug': 'playlist-three'}
])
# populate the playlists
query = insert(groove.db.entry)
in_memory_db.execute(query, [
{'playlist_id': '1', 'track': '1', 'track_id': '1'},
{'playlist_id': '1', 'track': '2', 'track_id': '2'},
{'playlist_id': '1', 'track': '3', 'track_id': '3'},
{'playlist_id': '2', 'track': '1', 'track_id': '1'},
{'playlist_id': '3', 'track': '6', 'track_id': '2'},
{'playlist_id': '3', 'track': '2', 'track_id': '3'},
])
yield in_memory_db

View File

@ -1,31 +1,59 @@
import pytest
import os import os
import sys import sys
import atheris import atheris
import bottle
from boddle import boddle from boddle import boddle
from groove import ondemand from groove import ondemand
@pytest.fixture(autouse=True, scope='session')
def init_db():
ondemand.initialize()
def test_server(): def test_server():
with boddle(): with boddle():
assert ondemand.index() == 'Groovy.' ondemand.index()
assert bottle.response.status_code == 200
def test_auth_with_valid_credentials(): def test_auth_with_valid_credentials():
with boddle(auth=(os.environ.get('USERNAME'), os.environ.get('PASSWORD'))): with boddle(auth=(os.environ.get('USERNAME'), os.environ.get('PASSWORD'))):
assert ondemand.admin() == 'Authenticated. Groovy.' ondemand.build()
assert bottle.response.status_code == 200
def test_auth_random_input(): def test_auth_random_input():
def auth(fuzzed_input): def auth(fuzzed_input):
with boddle(auth=(fuzzed_input, fuzzed_input)): with boddle(auth=(fuzzed_input, fuzzed_input)):
result = ondemand.admin() response = ondemand.build()
assert result.body == 'Access denied' assert response.status_code == 403
atheris.Setup([sys.argv[0], "-atheris_runs=100000"], auth) atheris.Setup([sys.argv[0], "-atheris_runs=100000"], auth)
try: try:
atheris.Fuzz() atheris.Fuzz()
except SystemExit: except SystemExit:
pass pass
@pytest.mark.parametrize('slug, expected', [
('playlist-one', 200),
('playlist-two', 200),
('playlist-three', 200),
('no such playlist', 404),
])
def test_playlist(db, slug, expected):
with boddle():
response = ondemand.get_playlist(slug, db)
assert response.status_code == expected
def test_playlist_on_empty_db(in_memory_db):
with boddle():
response = ondemand.get_playlist('some-slug', in_memory_db)
assert response.status_code == 404