diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..7b765fb --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,30 @@ +name: Publish Python 🐍 distributions 📦 to PyPI + +on: + release: + types: [published] + +jobs: + build-n-publish: + name: Build and publish Python 🐍 distributions 📦 to PyPI + runs-on: ubuntu-18.04 + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install pypa/build + run: python -m pip install build + + - name: Build a binary wheel and a source tarball + run: python -m build --sdist --wheel --outdir dist/ + + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..82cfcf9 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,24 @@ +name: Run Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install package + run: | + python -m pip install --upgrade pip + pip install -e . + + - name: Run tests + run: | + python -m unittest discover tests diff --git a/setup.py b/setup.py index d62d9b9..3310422 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='sqlite2rest', - version='1.0.0', + version='1.1.0', description='A Python library for creating a RESTful API from an SQLite database using Flask.', author='Denis Laprise', author_email='git@2ni.net', diff --git a/sqlite2rest/app.py b/sqlite2rest/app.py index 1ae0b8a..85ad157 100644 --- a/sqlite2rest/app.py +++ b/sqlite2rest/app.py @@ -1,17 +1,30 @@ import logging import sys from flask import Flask, g +from .database import Database from .routes import setup_routes def create_app(database_uri): # Initialize Flask app app = Flask(__name__) - setup_routes(app, database_uri) + def get_db(): + if 'db' not in g: + g.db = Database(database_uri) + return g.db + tables = Database(database_uri).get_tables() + setup_routes(app, tables, get_db) # Configure logging logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) app.logger.addHandler(logging.StreamHandler(sys.stderr)) app.logger.setLevel(logging.DEBUG) + @app.teardown_appcontext + def teardown_db(exception): + db = g.pop('db', None) + + if db is not None: + db.close() + return app diff --git a/sqlite2rest/database.py b/sqlite2rest/database.py index 684086d..569cb9a 100644 --- a/sqlite2rest/database.py +++ b/sqlite2rest/database.py @@ -2,7 +2,7 @@ class Database: def __init__(self, db_uri): - self.conn = sqlite3.connect(db_uri, check_same_thread=False) + self.conn = sqlite3.connect(db_uri) self.cursor = self.conn.cursor() def get_tables(self): @@ -19,12 +19,16 @@ def get_primary_key(self, table_name): def get_records(self, table_name): self.cursor.execute(f"SELECT * FROM {table_name};") - return self.cursor.fetchall() + col_names = [description[0] for description in self.cursor.description] + records = [dict(zip(col_names, record)) for record in self.cursor.fetchall()] + return records def get_record(self, table_name, key): primary_key = self.get_primary_key(table_name) self.cursor.execute(f"SELECT * FROM {table_name} WHERE {primary_key} = ?;", (key,)) - return self.cursor.fetchone() + col_names = [description[0] for description in self.cursor.description] + record = dict(zip(col_names, self.cursor.fetchone())) + return record def create_record(self, table_name, data): columns = ', '.join(data.keys()) @@ -43,3 +47,5 @@ def delete_record(self, table_name, key): self.cursor.execute(f"DELETE FROM {table_name} WHERE {primary_key} = ?;", (key,)) self.conn.commit() + def close(self): + self.conn.close() diff --git a/sqlite2rest/routes.py b/sqlite2rest/routes.py index 49139f1..3bca100 100644 --- a/sqlite2rest/routes.py +++ b/sqlite2rest/routes.py @@ -1,14 +1,11 @@ from flask import jsonify, request -from .database import Database from .openapi import get_openapi_spec -def setup_routes(app, database_uri): - tables = Database(database_uri).get_tables() - +def setup_routes(app, tables, get_database): def create_get_records_fn(table_name): def get_records(): app.logger.info(f'Getting records for table {table_name}') - records = Database(database_uri).get_records(table_name) + records = get_database().get_records(table_name) return jsonify(records), 200, {'Content-Type': 'application/json'} get_records.__name__ = f'get_records_{table_name}' return get_records @@ -17,7 +14,7 @@ def create_create_record_fn(table_name): def create_record(): data = request.get_json() app.logger.info(f'Creating record in table {table_name} with data {data}') - Database(database_uri).create_record(table_name, data) + get_database().create_record(table_name, data) return jsonify({'message': 'Record created.'}), 201, {'Content-Type': 'application/json'} create_record.__name__ = f'create_record_{table_name}' return create_record @@ -26,7 +23,7 @@ def create_update_record_fn(table_name): def update_record(id): data = request.get_json() app.logger.info(f'Updating record with id {id} in table {table_name} with data {data}') - Database(database_uri).update_record(table_name, id, data) + get_database().update_record(table_name, id, data) return jsonify({'message': 'Record updated.'}), 200, {'Content-Type': 'application/json'} update_record.__name__ = f'update_record_{table_name}' return update_record @@ -34,7 +31,7 @@ def update_record(id): def create_delete_record_fn(table_name): def delete_record(id): app.logger.info(f'Deleting record with id {id} from table {table_name}') - Database(database_uri).delete_record(table_name, id) + get_database().delete_record(table_name, id) return jsonify({'message': 'Record deleted.'}), 200, {'Content-Type': 'application/json'} delete_record.__name__ = f'delete_record_{table_name}' return delete_record diff --git a/tests/test_routes.py b/tests/test_routes.py index dedd9be..686bb59 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -1,53 +1,61 @@ -import unittest import json import os -import shutil -from sqlite2rest import create_app +from sqlite2rest import Database, create_app +import unittest class TestRoutes(unittest.TestCase): @classmethod def setUpClass(cls): - # Copy the database file before running the tests - shutil.copyfile('data/chinook.db', 'test_chinook.db') + # Use a database file for testing + cls.db_uri = 'test.db' + cls.db = Database(cls.db_uri) + + # Create a test table + cls.db.cursor.execute('CREATE TABLE Artist (ArtistId INTEGER PRIMARY KEY, Name TEXT);') + cls.db.conn.commit() @classmethod def tearDownClass(cls): - # Delete the copied database file after running the tests - os.remove('test_chinook.db') + # Delete the database file after running the tests + os.remove('test.db') def setUp(self): - # Use the copied database file for testing - self.app = create_app('test_chinook.db') + # Create the Flask app + self.app = create_app(self.db_uri) self.client = self.app.test_client() - def test_get(self): + def test_0get(self): response = self.client.get('/Artist') self.assertEqual(response.status_code, 200) artists = json.loads(response.data) self.assertIsInstance(artists, list) + self.assertEqual(artists, []) def test_create(self): - response = self.client.post('/Artist', json={'Name': 'Test Artist'}) + response = self.client.post('/Artist', json={'ArtistId': 1, 'Name': 'Test Artist'}) self.assertEqual(response.status_code, 201) self.assertEqual(json.loads(response.data), {'message': 'Record created.'}) + # Verify the creation by reading it back + response = self.client.get('/Artist') + self.assertEqual(response.status_code, 200) + artists = json.loads(response.data) + self.assertEqual(artists, [{'ArtistId': 1, 'Name': 'Test Artist'}]) + def test_update(self): # First, create a record to update - self.client.post('/Artist', json={'Name': 'Test Artist'}) + self.client.post('/Artist', json={'ArtistId': 2, 'Name': 'Test Artist'}) # Then, update the record - response = self.client.put('/Artist/1', json={'Name': 'Updated Artist'}) + response = self.client.put('/Artist/2', json={'Name': 'Updated Artist'}) self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.data), {'message': 'Record updated.'}) def test_delete(self): # First, create a record to delete - self.client.post('/Artist', json={'Name': 'Test Artist'}) + self.client.post('/Artist', json={'ArtistId': 3, 'Name': 'Test Artist'}) # Then, delete the record - response = self.client.delete('/Artist/1') + response = self.client.delete('/Artist/3') self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.data), {'message': 'Record deleted.'}) - -if __name__ == '__main__': - unittest.main()