Skip to content

Commit 8a445cb

Browse files
authored
Merge pull request #14 from bjcoleman/database_storeage
Database storeage
2 parents 839d617 + 8e6ad86 commit 8a445cb

13 files changed

Lines changed: 465 additions & 166 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ __pycache__/
44
.DS_Store
55
*.egg-info/
66
.coverage
7-
7+
database/*.db

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,6 @@ The system is organized as a three-tier architecture:
107107
* [docs/usecases.md](docs/usecases.md) contains detailed use cases of the business logic for the system
108108
* [docs/dev.md](docs/dev.md) contains directions to set up dev/prod environments.
109109
* [docs/sample-dataset.md](docs/sample-dataset.md) contains data and discussion for some sample groups
110-
* [docs/schema-sqlite.sql](docs/schema-sqlite.sql) contains schema for all database tables
111-
* [docs/sample-data.sql](docs/sample-data.sql) contains the sample dataset in SQL format
110+
* [src/cost_sharing/sql/schema-sqlite.sql](src/cost_sharing/sql/schema-sqlite.sql) contains schema for all database tables
111+
* [src/cost_sharing/sql/sample-data.sql](src/cost_sharing/sql/sample-data.sql) contains the sample dataset in SQL format
112112
* [docs/api.yaml](docs/api.yaml) contains an OpenAPI specification of all API endpoints.

docs/dev.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,19 @@ LABEL=costsharing
138138
* Create a `.env` file as described in the [Configuration](#configuration) section.
139139
Use `http://localhost:8000` as the `BASE_URL`.
140140

141+
* Initialize the database:
142+
143+
```
144+
mkdir -p database
145+
sqlite3 database/costsharing.db < src/cost_sharing/sql/schema-sqlite.sql
146+
```
147+
148+
* (Optional) Load sample data:
149+
150+
```
151+
sqlite3 database/costsharing.db < src/cost_sharing/sql/sample-data.sql
152+
```
153+
141154

142155
## Running the Application in dev
143156

@@ -225,6 +238,13 @@ the team.
225238
* Create a `.env` file as described in the [Configuration](#configuration) section. On EC2 you need both the
226239
OAuth values **and** the AWS DNS Subdomain System values.
227240

241+
* Initialize the database:
242+
243+
```
244+
mkdir -p database
245+
sqlite3 database/costsharing.db < src/cost_sharing/sql/schema-sqlite.sql
246+
```
247+
228248
* Register your subdomain
229249

230250
```

docs/sample-dataset.md

Lines changed: 0 additions & 152 deletions
This file was deleted.

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[pytest]
22
testpaths = tests
33
junit_family=xunit1
4-
addopts = --cov=cost_sharing --cov-fail-under 90
4+
addopts = --cov=cost_sharing --cov-fail-under 90 --cov-report=html --cov-report=term-missing

src/cost_sharing/app.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import os
22
import sys
33
import functools
4+
import sqlite3
45
from flask import Flask, request, jsonify, g, render_template
56
from dotenv import load_dotenv
67
from cost_sharing.oauth_handler import (
78
OAuthHandler, OAuthCodeError, OAuthVerificationError,
89
TokenExpiredError, TokenInvalidError
910
)
10-
from cost_sharing.storage import InMemoryCostStorage
11+
from cost_sharing.db_storage import DatabaseCostStorage
1112
from cost_sharing.cost_sharing import CostSharing
1213
from cost_sharing.exceptions import UserNotFoundError
1314

@@ -189,7 +190,12 @@ def launch(): # pragma: no cover
189190
jwt_secret=os.getenv('JWT_SECRET')
190191
)
191192

192-
return create_app(oauth_handler, CostSharing(InMemoryCostStorage()))
193+
# check_same_thread=False is necessary to allow multiple
194+
# threads to access the database concurrently
195+
db_conn = sqlite3.connect('database/costsharing.db', check_same_thread=False)
196+
db_storage = DatabaseCostStorage(db_conn)
197+
198+
return create_app(oauth_handler, CostSharing(db_storage))
193199

194200

195201
# This is "main" for the local launch and can be tested directly

src/cost_sharing/db_storage.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""Database storage implementation using sqlite3"""
2+
3+
import sqlite3
4+
5+
from cost_sharing.models import User
6+
from cost_sharing.exceptions import (
7+
DuplicateEmailError,
8+
UserNotFoundError,
9+
StorageException
10+
)
11+
12+
13+
class DatabaseCostStorage:
14+
"""
15+
Database storage implementation using sqlite3.
16+
17+
Uses SQLite database (in-memory or file-based) for persistence.
18+
"""
19+
20+
def __init__(self, connection):
21+
"""
22+
Initialize database storage with a database connection.
23+
24+
Args:
25+
connection: A sqlite3.Connection object (e.g., sqlite3.connect(':memory:')
26+
or sqlite3.connect('costsharing.db'))
27+
"""
28+
self._conn = connection
29+
30+
# Use Row factory for dict-like access to rows
31+
self._conn.row_factory = sqlite3.Row
32+
# Enable foreign key constraints
33+
self._conn.execute('PRAGMA foreign_keys = ON')
34+
35+
def is_user(self, email):
36+
"""
37+
Check if a user exists with the given email.
38+
39+
Args:
40+
email: User's email address
41+
42+
Returns:
43+
bool: True if user exists, False otherwise
44+
45+
Raises:
46+
StorageException: If a database error occurs
47+
"""
48+
try:
49+
cursor = self._conn.execute(
50+
'SELECT 1 FROM users WHERE email = ?',
51+
(email,)
52+
)
53+
return cursor.fetchone() is not None
54+
except sqlite3.Error as e:
55+
raise StorageException(f"Database error checking user existence: {e}") from e
56+
57+
def get_user_by_email(self, email):
58+
"""
59+
Get user by email address.
60+
61+
Args:
62+
email: User's email address
63+
64+
Returns:
65+
User if found
66+
67+
Raises:
68+
UserNotFoundError: If user with the given email is not found
69+
StorageException: If a database error occurs
70+
"""
71+
try:
72+
cursor = self._conn.execute(
73+
'SELECT id, email, name FROM users WHERE email = ?',
74+
(email,)
75+
)
76+
row = cursor.fetchone()
77+
if row is None:
78+
raise UserNotFoundError(f"User with email '{email}' not found")
79+
return User(id=row['id'], email=row['email'], name=row['name'])
80+
except sqlite3.Error as e:
81+
raise StorageException(f"Database error retrieving user by email: {e}") from e
82+
83+
def create_user(self, email, name):
84+
"""
85+
Create a new user.
86+
87+
Args:
88+
email: User's email address
89+
name: User's name
90+
91+
Returns:
92+
Newly created User object
93+
94+
Raises:
95+
DuplicateEmailError: If email already exists
96+
StorageException: If a database error occurs
97+
"""
98+
try:
99+
cursor = self._conn.execute(
100+
'INSERT INTO users (email, name) VALUES (?, ?)',
101+
(email, name)
102+
)
103+
self._conn.commit()
104+
user_id = cursor.lastrowid
105+
return User(id=user_id, email=email, name=name)
106+
except sqlite3.IntegrityError as e:
107+
self._conn.rollback()
108+
# IntegrityError on users table insert is always a duplicate email
109+
raise DuplicateEmailError() from e
110+
except sqlite3.Error as e:
111+
self._conn.rollback()
112+
raise StorageException(f"Database error creating user: {e}") from e
113+
114+
def get_user_by_id(self, user_id):
115+
"""
116+
Get user by ID.
117+
118+
Args:
119+
user_id: User ID
120+
121+
Returns:
122+
User if found
123+
124+
Raises:
125+
UserNotFoundError: If user with the given ID is not found
126+
StorageException: If a database error occurs
127+
"""
128+
try:
129+
cursor = self._conn.execute(
130+
'SELECT id, email, name FROM users WHERE id = ?',
131+
(user_id,)
132+
)
133+
row = cursor.fetchone()
134+
if row is None:
135+
raise UserNotFoundError(f"User with ID {user_id} not found")
136+
return User(id=row['id'], email=row['email'], name=row['name'])
137+
except sqlite3.Error as e:
138+
raise StorageException(f"Database error retrieving user by ID: {e}") from e

src/cost_sharing/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ class DuplicateEmailError(Exception):
77

88
class UserNotFoundError(Exception):
99
"""Raised when a requested user cannot be found"""
10+
11+
12+
class StorageException(Exception):
13+
"""Raised when a database storage operation fails"""

src/cost_sharing/sql/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)