diff options
-rw-r--r-- | Pipfile | 5 | ||||
-rw-r--r-- | Pipfile.lock | 9 | ||||
-rw-r--r-- | backend/config.py | 2 | ||||
-rw-r--r-- | fatcat/__init__.py | 10 | ||||
-rw-r--r-- | fatcat/api.py | 13 | ||||
-rwxr-xr-x | fatcat/backend.py | 301 | ||||
-rw-r--r-- | fatcat/models.py | 158 | ||||
-rw-r--r--[-rwxr-xr-x] | fatcat/routes.py (renamed from fatcat/webface.py) | 38 | ||||
-rw-r--r-- | fatcat/sql.py | 32 | ||||
-rw-r--r-- | run.py | 27 | ||||
-rw-r--r-- | tests/test_backend.py (renamed from fatcat/test_backend.py) | 16 |
11 files changed, 266 insertions, 345 deletions
@@ -11,9 +11,10 @@ name = "pypi" [packages] -flask = "*" -sqlalchemy = "*" +Flask = "*" +SQLAlchemy = "*" requests = "*" +Flask-SQLAlchemy = "*" [requires] diff --git a/Pipfile.lock b/Pipfile.lock index bef64e85..1f82a0dc 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "98d21978b677ea4ad82a3dc1b0aee77ca0c6af87bc4928f0341f2a0a04f47084" + "sha256": "a011142ad79cd80853e0212f35f6de603c33a616965f635d91fc521bf110be13" }, "host-environment-markers": { "implementation_name": "cpython", @@ -57,6 +57,13 @@ ], "version": "==0.12.2" }, + "flask-sqlalchemy": { + "hashes": [ + "sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b", + "sha256:5971b9852b5888655f11db634e87725a9031e170f37c0ce7851cf83497f56e53" + ], + "version": "==2.3.2" + }, "idna": { "hashes": [ "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4", diff --git a/backend/config.py b/backend/config.py deleted file mode 100644 index d4196230..00000000 --- a/backend/config.py +++ /dev/null @@ -1,2 +0,0 @@ - -DATABASE_URI = "sqlite:///test.sqlite" diff --git a/fatcat/__init__.py b/fatcat/__init__.py new file mode 100644 index 00000000..230fd608 --- /dev/null +++ b/fatcat/__init__.py @@ -0,0 +1,10 @@ + +from flask import Flask +from config import Config +from flask_sqlalchemy import SQLAlchemy + +app = Flask(__name__) +app.config.from_object(Config) +db = SQLAlchemy(app) + +from fatcat import routes, models, api, sql diff --git a/fatcat/api.py b/fatcat/api.py new file mode 100644 index 00000000..d09eedd5 --- /dev/null +++ b/fatcat/api.py @@ -0,0 +1,13 @@ + +from flask import Flask, render_template, send_from_directory, request, \ + url_for, abort, g, redirect, jsonify +from fatcat import app, db +from fatcat.models import * + + +### Views ################################################################### + +@app.route('/work/<work_id>', methods=['GET']) +def work_get(): + work = WorkId.query.filter_by(id=work_id).first_or_404() + return jsonify(work) diff --git a/fatcat/backend.py b/fatcat/backend.py deleted file mode 100755 index a39ae790..00000000 --- a/fatcat/backend.py +++ /dev/null @@ -1,301 +0,0 @@ - -import argparse -from flask import Flask, render_template, send_from_directory, request, \ - url_for, abort, g, redirect, jsonify -from sqlalchemy import create_engine, MetaData, Table - -app = Flask(__name__) -app.config.from_object(__name__) - -# Load default config and override config from an environment variable -app.config.update(dict( - DATABASE_URI='sqlite://:memory:', - SECRET_KEY='development-key', - USERNAME='admin', - PASSWORD='admin' -)) -app.config.from_envvar('FATCAT_BACKEND_CONFIG', silent=True) - -metadata = MetaData() - - -## SQL Schema ############################################################### - -import enum -from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey, \ - Enum - -# TODO: http://docs.sqlalchemy.org/en/latest/orm/extensions/declarative/mixins.html - -class IdState(enum.Enum): - normal = 1 - redirect = 2 - removed = 3 - -work_id = Table('work_id', metadata, - Column('id', Integer, primary_key=True, autoincrement=False), - Column('revision', ForeignKey('work_revision.id')), - ) - -work_revision = Table('work_revision', metadata, - Column('id', Integer, primary_key=True, autoincrement=True), - Column('previous', ForeignKey('work_revision.id'), nullable=True), - Column('state', Enum(IdState)), - Column('redirect_id', ForeignKey('work_id.id'), nullable=True), - Column('edit_id', ForeignKey('edit.id')), - Column('extra_json', ForeignKey('extra_json.sha1'), nullable=True), - - Column('title', String), - Column('work_type', String), - Column('date', String), - ) - -release_id = Table('release_id', metadata, - Column('id', Integer, primary_key=True, autoincrement=False), - Column('revision', ForeignKey('release_revision.id')), - ) - -release_revision = Table('release_revision', metadata, - Column('id', Integer, primary_key=True, autoincrement=True), - Column('previous', ForeignKey('release_revision.id'), nullable=True), - Column('state', Enum(IdState)), - Column('redirect_id', ForeignKey('release_id.id'), nullable=True), - Column('edit_id', ForeignKey('edit.id')), - Column('extra_json', ForeignKey('extra_json.sha1'), nullable=True), - - #Column('work', ForeignKey('work_id.id')), - Column('container', ForeignKey('container_id.id')), - Column('title', String), - Column('license', String), # TODO: oa status foreign key - Column('release_type', String), # TODO: foreign key - Column('date', String), # TODO: datetime - Column('doi', String), # TODO: identifier table - ) - -creator_id = Table('creator_id', metadata, - Column('id', Integer, primary_key=True, autoincrement=False), - Column('revision', ForeignKey('creator_revision.id')), - ) - -creator_revision = Table('creator_revision', metadata, - Column('id', Integer, primary_key=True, autoincrement=True), - Column('previous', ForeignKey('creator_revision.id'), nullable=True), - Column('state', Enum(IdState)), - Column('redirect_id', ForeignKey('creator_id.id'), nullable=True), - Column('edit_id', ForeignKey('edit.id')), - Column('extra_json', ForeignKey('extra_json.sha1'), nullable=True), - - Column('name', String), - Column('sortname', String), - Column('orcid', String), # TODO: identifier table - ) - -work_contrib = Table('work_contrib', metadata, - Column('id', Integer, primary_key=True, autoincrement=True), - Column('work_rev', ForeignKey('work_revision.id'), nullable=False), - Column('creator_id', ForeignKey('creator_id.id'), nullable=False), - Column('stub', String, nullable=False), - ) - -release_contrib = Table('release_contrib', metadata, - Column('id', Integer, primary_key=True, autoincrement=True), - Column('release_rev', ForeignKey('release_revision.id'), nullable=False), - Column('creator_id', ForeignKey('creator_id.id'), nullable=False), - ) - -container_id = Table('container_id', metadata, - Column('id', Integer, primary_key=True, autoincrement=False), - Column('revision', ForeignKey('container_revision.id')), - ) - -container_revision = Table('container_revision', metadata, - Column('id', Integer, primary_key=True, autoincrement=True), - Column('previous', ForeignKey('container_revision.id'), nullable=True), - Column('state', Enum(IdState)), - Column('redirect_id', ForeignKey('container_id.id'), nullable=True), - Column('edit_id', ForeignKey('edit.id')), - Column('extra_json', ForeignKey('extra_json.sha1'), nullable=True), - - Column('name', String), - Column('container', ForeignKey('container_id.id')), - Column('publisher', String), # TODO: foreign key - Column('sortname', String), - Column('issn', String), # TODO: identifier table - ) - -file_id = Table('file_id', metadata, - Column('id', Integer, primary_key=True, autoincrement=False), - Column('revision', ForeignKey('container_revision.id')), - ) - -file_revision = Table('file_revision', metadata, - Column('id', Integer, primary_key=True, autoincrement=True), - Column('previous', ForeignKey('file_revision.id'), nullable=True), - Column('state', Enum(IdState)), - Column('redirect_id', ForeignKey('file_id.id'), nullable=True), - Column('edit_id', ForeignKey('edit.id')), - Column('extra_json', ForeignKey('extra_json.sha1'), nullable=True), - - Column('size', Integer), - Column('sha1', Integer), # TODO: hash table... only or in addition? - Column('url', Integer), # TODO: URL table - ) - -release_file= Table('release_file', metadata, - Column('id', Integer, primary_key=True, autoincrement=True), - Column('release_rev', ForeignKey('release_revision.id'), nullable=False), - Column('file_id', ForeignKey('file_id.id'), nullable=False), - ) - -edit = Table('edit', metadata, - Column('id', Integer, primary_key=True, autoincrement=True), - Column('edit_group', ForeignKey('edit_group.id')), - Column('editor', ForeignKey('editor.id')), - Column('description', String), - ) - -edit_group = Table('edit_group', metadata, - Column('id', Integer, primary_key=True, autoincrement=True), - Column('editor', ForeignKey('editor.id')), - Column('description', String), - ) - -editor = Table('editor', metadata, - Column('id', Integer, primary_key=True, autoincrement=True), - Column('username', String), - ) - -changelog = Table('changelog', metadata, - Column('id', Integer, primary_key=True, autoincrement=True), - Column('edit_id', ForeignKey('edit.id')), - Column('timestamp', Integer), - ) - -extra_json = Table('extra_json', metadata, - Column('sha1', String, primary_key=True, autoincrement=True), - Column('json', String), - ) - -## Helpers ################################################################## - -def is_fcid(s): - return len(s) == 26 and s.isalnum() - -# XXX: why isn't this running? -def test_is_fcid(): - - for s in ("rzga5b9cd7efgh04iljk", "RZGA5B9CD7Efgh04iljk"): - assert is_fcid() is True - - for s in ("rzga59cd7efgh04iljk", "rzga.b9cd7efgh04iljk", "", - "rzga5b9cd7efgh04iljkz"): - assert is_fcid() is False - -def release_list(id_list): - # XXX: MOCK - l = [] - for i in id_list: - l.append({ - "id": i, - "rev": "8fkj28fjhqkjdhkjkj9s", - "previous": "0021jdfjhqkjdhkjkj9s", - "state": "normal", - "redirect_id": None, - "edit_id": "932582iuhckjvssk", - "extra_json": None, - - "container_id": "0021jdfjhqkjdhkjkj9s", - "title": "Mocks are great", - "license": "CC-0", - "release_type": "publication", - "date": "2017-11-22", - "doi": "10.1000/953kj.sdfkj", - }) - return l - -def release_hydrate(release_id): - e = release_list([release_id])[0] - e['container'] = container_hydrate(d['container_id']) - e.pop('container_id') - e['creators'] = [creator_hydrate(c['id']) for c in e['creator_ids']] - return e - -def work_list(id_list): - """This is the fast/light version: populates entity-specific lists (eg, - identifiers), and any primaries, but doesn't transclude all other - entities""" - if len(id_list) == 0: - return [] - - l = [] - for i in id_list: - l.append({ - "id": "rzga5b9cd7efgh04iljk", - "rev": "8fkj28fjhqkjdhkjkj9s", - "previous": "0021jdfjhqkjdhkjkj9s", - "state": "normal", - "redirect_id": None, - "edit_id": "932582iuhckjvssk", - "extra_json": None, - - "title": "Mocks are great", - "contributors": [], - "work_type": "journal-article", - "date": None, - - "primary_release": release_list(["8fkj28fjhqkjdhkjkj9s"])[0], - }) - return l - -def work_hydrate(work_id): - """This is the heavy/slowversion: everything from get_works(), but also - recursively transcludes single-linked entities""" - # XXX: - return work_list([work_id])[0] - -## API Methods ############################################################## - -@app.route('/health', methods=['GET']) -def health(): - return jsonify({'ok': True}) - - -@app.route('/v0/work/<work_id>', methods=['GET']) -def work_get(work_id): - if not is_fcid(work_id): - print("not fcid: {}".format(work_id)) - return abort(404) - work = work_hydrate(work_id) - return jsonify(work) - -## Entry Point ############################################################## - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--debug', - action='store_true', - help="enable debugging interface") - parser.add_argument('--host', - default="127.0.0.1", - help="listen on this host/IP") - parser.add_argument('--port', - type=int, - default=8040, - help="listen on this port") - parser.add_argument('--database-uri', - default=app.config['DATABASE_URI'], - help="sqlalchemy database string") - args = parser.parse_args() - - app.config['DATABASE_URI'] = args.database_uri - app.conn = create_engine(app.config['DATABASE_URI'], convert_unicode=True) - metadata.create_all(bind=engine) - - # XXX: - db_test_data() - - app.run(debug=args.debug, host=args.host, port=args.port) - - -if __name__ == '__main__': - main() diff --git a/fatcat/models.py b/fatcat/models.py new file mode 100644 index 00000000..dd84070d --- /dev/null +++ b/fatcat/models.py @@ -0,0 +1,158 @@ + +import enum +from fatcat import db + + +# TODO: EntityMixin, EntityIdMixin + +work_contrib = db.Table("work_contrib", + db.Column("work_rev", db.ForeignKey('work_revision.id'), nullable=False, primary_key=True), + db.Column("creator_id", db.ForeignKey('creator_id.id'), nullable=False, primary_key=True), + db.Column("stub", db.String, nullable=True)) + +release_contrib = db.Table("release_contrib", + db.Column("release_rev", db.ForeignKey('release_revision.id'), nullable=False, primary_key=True), + db.Column("creator_id", db.ForeignKey('creator_id.id'), nullable=False, primary_key=True), + db.Column("stub", db.String, nullable=True)) + +class WorkId(db.Model): + __tablename__ = 'work_id' + id = db.Column(db.Integer, primary_key=True) + revision_id = db.Column(db.ForeignKey('work_revision.id')) + +class WorkRevision(db.Model): + __tablename__ = 'work_revision' + id = db.Column(db.Integer, primary_key=True) + previous = db.Column(db.ForeignKey('work_revision.id'), nullable=True) + state = db.Column(db.String) + redirect_id = db.Column(db.ForeignKey('work_id.id'), nullable=True) + edit_id = db.Column(db.ForeignKey('edit.id')) + extra_json = db.Column(db.ForeignKey('extra_json.sha1'), nullable=True) + #work_ids = db.relationship("WorkId", backref="revision", lazy=True) + + title = db.Column(db.String) + work_type = db.Column(db.String) + date = db.Column(db.String) + + creators = db.relationship('CreatorId', secondary=work_contrib, + lazy='subquery', backref=db.backref('works', lazy=True)) + +class ReleaseId(db.Model): + __tablename__ = 'release_id' + id = db.Column(db.Integer, primary_key=True) #autoincrement=False + revision_id = db.Column(db.ForeignKey('release_revision.id')) + +class ReleaseRevision(db.Model): + __tablename__ = 'release_revision' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + previous = db.Column(db.ForeignKey('release_revision.id'), nullable=True) + state = db.Column(db.String) # TODO: enum + redirect_id = db.Column(db.ForeignKey('release_id.id'), nullable=True) + edit_id = db.Column(db.ForeignKey('edit.id')) + extra_json = db.Column(db.ForeignKey('extra_json.sha1'), nullable=True) + #release_ids = db.relationship("ReleaseId", backref="revision", lazy=False) + + work_id = db.ForeignKey('work_id.id') + container = db.Column(db.ForeignKey('container_id.id')) + title = db.Column(db.String) + license = db.Column(db.String) # TODO: oa status foreign key + release_type = db.Column(db.String) # TODO: foreign key + date = db.Column(db.String) # TODO: datetime + doi = db.Column(db.String) # TODO: identifier table + + creators = db.relationship('CreatorId', secondary=release_contrib, + lazy='subquery') + #backref=db.backref('releases', lazy=True)) + +class CreatorId(db.Model): + __tablename__ = 'creator_id' + id = db.Column(db.Integer, primary_key=True) #autoincrement=False) + revision_id = db.Column(db.ForeignKey('creator_revision.id')) + +class CreatorRevision(db.Model): + __tablename__ = 'creator_revision' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + previous = db.Column(db.ForeignKey('creator_revision.id'), nullable=True) + state = db.Column(db.String) + redirect_id = db.Column(db.ForeignKey('creator_id.id'), nullable=True) + edit_id = db.Column(db.ForeignKey('edit.id')) + extra_json = db.Column(db.ForeignKey('extra_json.sha1'), nullable=True) + #creator_ids = db.relationship("CreatorId", backref="revision", lazy=False) + + name = db.Column(db.String) + sortname = db.Column(db.String) + orcid = db.Column(db.String) # TODO: identifier table + +class ContainerId(db.Model): + __tablename__ = 'container_id' + id = db.Column(db.Integer, primary_key=True) #autoincrement=False) + revision_id = db.Column(db.ForeignKey('container_revision.id')) + +class ContainerRevision(db.Model): + __tablename__ = 'container_revision' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + previous = db.Column(db.ForeignKey('container_revision.id'), nullable=True) + state = db.Column(db.String) + redirect_id = db.Column(db.ForeignKey('container_id.id'), nullable=True) + edit_id = db.Column(db.ForeignKey('edit.id')) + extra_json = db.Column(db.ForeignKey('extra_json.sha1'), nullable=True) + + name = db.Column(db.String) + container = db.Column(db.ForeignKey('container_id.id')) + publisher = db.Column(db.String) # TODO: foreign key + sortname = db.Column(db.String) + issn = db.Column(db.String) # TODO: identifier table + +class FileId(db.Model): + __tablename__ = 'file_id' + id = db.Column(db.Integer, primary_key=True, autoincrement=False) + revision_id = db.Column('revision', db.ForeignKey('container_revision.id')) + +class FileRevision(db.Model): + __tablename__ = 'file_revision' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + previous = db.Column(db.ForeignKey('file_revision.id'), nullable=True) + state = db.Column(db.String) + redirect_id = db.Column(db.ForeignKey('file_id.id'), nullable=True) + edit_id = db.Column(db.ForeignKey('edit.id')) + extra_json = db.Column(db.ForeignKey('extra_json.sha1'), nullable=True) + + size = db.Column(db.Integer) + sha1 = db.Column(db.Integer) # TODO: hash table... only or in addition? + url = db.Column(db.Integer) # TODO: URL table + +class ReleaseFil(db.Model): + __tablename__ = 'release_file' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + release_rev = db.Column(db.ForeignKey('release_revision.id'), nullable=False) + file_id = db.Column(db.ForeignKey('file_id.id'), nullable=False) + +class Edit(db.Model): + __tablename__ = 'edit' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + edit_group = db.Column(db.ForeignKey('edit_group.id')) + editor = db.Column(db.ForeignKey('editor.id')) + description = db.Column(db.String) + +class EditGroup(db.Model): + __tablename__ = 'edit_group' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + editor = db.Column(db.ForeignKey('editor.id')) + description = db.Column(db.String) + +class Editor(db.Model): + __tablename__ = 'editor' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + username = db.Column(db.String) + +class ChangelogEntry(db.Model): + __tablename__= 'changelog' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + edit_id = db.Column(db.ForeignKey('edit.id')) + timestamp = db.Column(db.Integer) + +class ExtraJson(db.Model): + __tablename__ = 'extra_json' + sha1 = db.Column(db.String, primary_key=True) + json = db.Column(db.String) + diff --git a/fatcat/webface.py b/fatcat/routes.py index 33833e25..61228638 100755..100644 --- a/fatcat/webface.py +++ b/fatcat/routes.py @@ -1,21 +1,12 @@ -#!/usr/bin/env python3 import os -import argparse -import requests from flask import Flask, render_template, send_from_directory, request, \ url_for, abort, g, redirect, jsonify - -app = Flask(__name__) -app.config.from_object(__name__) +from fatcat import app, db ### Views ################################################################### -@app.route('/health', methods=['GET']) -def health(): - return jsonify({'ok': True}) - @app.route('/work/create', methods=['GET']) def work_create(): return render_template('work_add.html') @@ -61,27 +52,6 @@ def robots(): 'robots.txt', mimetype='text/plain') - -### Entry Point ############################################################# - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--debug', - action='store_true', - help="enable debugging interface") - parser.add_argument('--host', - default="127.0.0.1", - help="listen on this host/IP") - parser.add_argument('--port', - type=int, - default=5050, - help="listen on this port") - parser.add_argument('--backend-api', - default="localhost:6060", - help="backend API to connect to") - args = parser.parse_args() - - app.run(debug=args.debug, host=args.host, port=args.port) - -if __name__ == '__main__': - main() +@app.route('/health', methods=['GET']) +def health(): + return jsonify({'ok': True}) diff --git a/fatcat/sql.py b/fatcat/sql.py new file mode 100644 index 00000000..01da5873 --- /dev/null +++ b/fatcat/sql.py @@ -0,0 +1,32 @@ + +from fatcat import app, db +from fatcat.models import * + +def populate_db(): + + # XXX: doesn't create an edit trail (yet) + n_elkies = CreatorRevision( + name="Noam D. Elkies", + sortname="Elkies, N", + orcid=None) + n_elkies_id = CreatorId(revision_id=n_elkies.id) + pi_work = WorkRevision( + title="Why is π 2so close to 10?") + pi_work_id = WorkId(revision_id=pi_work.id) + pi_release = ReleaseRevision( + title=pi_work.title, + creators=[n_elkies_id], + work_id=pi_work.id) + pi_release_id = ReleaseId(revision_id=pi_release.id) + pi_work.primary_release = pi_release.id + + #pi_file = File( + # sha1="efee52e46c86691e2b892dbeb212f3b92e92e9d3", + # url="http://www.math.harvard.edu/~elkies/Misc/pi10.pdf") + db.session.add_all([n_elkies, pi_work, pi_work_id, pi_release, pi_release_id]) + + #ligo_collab = CreatorRevision(name="LIGO Scientific Collaboration") + #ligo_paper = ReleaseRevision( + # title="Full Band All-sky Search for Periodic Gravitational Waves in the O1 LIGO Data") + db.session.commit() + @@ -0,0 +1,27 @@ + +import argparse +from fatcat import app, db + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--debug', + action='store_true', + help="enable debugging interface") + parser.add_argument('--host', + default="127.0.0.1", + help="listen on this host/IP") + parser.add_argument('--port', + type=int, + default=8040, + help="listen on this port") + parser.add_argument('--database-uri', + default=app.config['DATABASE_URI'], + help="sqlalchemy database string") + args = parser.parse_args() + + app.config['DATABASE_URI'] = args.database_uri + db.create_all() + app.run(debug=args.debug, host=args.host, port=args.port) + +if __name__ == '__main__': + main() diff --git a/fatcat/test_backend.py b/tests/test_backend.py index 429b5ae7..afc6215d 100644 --- a/fatcat/test_backend.py +++ b/tests/test_backend.py @@ -1,7 +1,8 @@ import os import json -import backend +import fatcat +import fatcat.sql import unittest import tempfile from nose.tools import * @@ -22,12 +23,13 @@ def check_entity_fields(e): ## API Tests ################################################################ -class BackendTestCase(unittest.TestCase): +class FatcatTestCase(unittest.TestCase): def setUp(self): - backend.app.config['DATABASE_URI'] = 'sqlite://:memory:' - backend.app.testing = True - self.app = backend.app.test_client() + fatcat.app.config['DATABASE_URI'] = 'sqlite://:memory:' + fatcat.app.testing = True + self.app = fatcat.app.test_client() + fatcat.db.create_all() def test_health(self): rv = self.app.get('/health') @@ -51,3 +53,7 @@ class BackendTestCase(unittest.TestCase): check_entity_fields(obj) assert obj['title'] assert_equal(obj['work_type'], "journal-article") + + def test_something(self): + + fatcat.sql.populate_db() |