Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1.1.0 #1

Merged
merged 6 commits into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
24 changes: 24 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
15 changes: 14 additions & 1 deletion sqlite2rest/app.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 9 additions & 3 deletions sqlite2rest/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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())
Expand All @@ -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()
13 changes: 5 additions & 8 deletions sqlite2rest/routes.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -26,15 +23,15 @@ 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

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
Expand Down
44 changes: 26 additions & 18 deletions tests/test_routes.py
Original file line number Diff line number Diff line change
@@ -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()
Loading