Skip to content
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ __pycache__/
.DS_Store
*.egg-info/
.coverage

database/*.db
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,6 @@ The system is organized as a three-tier architecture:
* [docs/usecases.md](docs/usecases.md) contains detailed use cases of the business logic for the system
* [docs/dev.md](docs/dev.md) contains directions to set up dev/prod environments.
* [docs/sample-dataset.md](docs/sample-dataset.md) contains data and discussion for some sample groups
* [docs/schema-sqlite.sql](docs/schema-sqlite.sql) contains schema for all database tables
* [docs/sample-data.sql](docs/sample-data.sql) contains the sample dataset in SQL format
* [src/cost_sharing/sql/schema-sqlite.sql](src/cost_sharing/sql/schema-sqlite.sql) contains schema for all database tables
* [src/cost_sharing/sql/sample-data.sql](src/cost_sharing/sql/sample-data.sql) contains the sample dataset in SQL format
* [docs/api.yaml](docs/api.yaml) contains an OpenAPI specification of all API endpoints.
20 changes: 20 additions & 0 deletions docs/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,19 @@ LABEL=costsharing
* Create a `.env` file as described in the [Configuration](#configuration) section.
Use `http://localhost:8000` as the `BASE_URL`.

* Initialize the database:

```
mkdir -p database
sqlite3 database/costsharing.db < src/cost_sharing/sql/schema-sqlite.sql
```

* (Optional) Load sample data:

```
sqlite3 database/costsharing.db < src/cost_sharing/sql/sample-data.sql
```


## Running the Application in dev

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

* Initialize the database:

```
mkdir -p database
sqlite3 database/costsharing.db < src/cost_sharing/sql/schema-sqlite.sql
```

* Register your subdomain

```
Expand Down
152 changes: 0 additions & 152 deletions docs/sample-dataset.md

This file was deleted.

2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[pytest]
testpaths = tests
junit_family=xunit1
addopts = --cov=cost_sharing --cov-fail-under 90
addopts = --cov=cost_sharing --cov-fail-under 90 --cov-report=html --cov-report=term-missing
10 changes: 8 additions & 2 deletions src/cost_sharing/app.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import os
import sys
import functools
import sqlite3
from flask import Flask, request, jsonify, g, render_template
from dotenv import load_dotenv
from cost_sharing.oauth_handler import (
OAuthHandler, OAuthCodeError, OAuthVerificationError,
TokenExpiredError, TokenInvalidError
)
from cost_sharing.storage import InMemoryCostStorage
from cost_sharing.db_storage import DatabaseCostStorage
from cost_sharing.cost_sharing import CostSharing
from cost_sharing.exceptions import UserNotFoundError

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

return create_app(oauth_handler, CostSharing(InMemoryCostStorage()))
# check_same_thread=False is necessary to allow multiple
# threads to access the database concurrently
db_conn = sqlite3.connect('database/costsharing.db', check_same_thread=False)
db_storage = DatabaseCostStorage(db_conn)

return create_app(oauth_handler, CostSharing(db_storage))


# This is "main" for the local launch and can be tested directly
Expand Down
138 changes: 138 additions & 0 deletions src/cost_sharing/db_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""Database storage implementation using sqlite3"""

import sqlite3

from cost_sharing.models import User
from cost_sharing.exceptions import (
DuplicateEmailError,
UserNotFoundError,
StorageException
)


class DatabaseCostStorage:
"""
Database storage implementation using sqlite3.

Uses SQLite database (in-memory or file-based) for persistence.
"""

def __init__(self, connection):
"""
Initialize database storage with a database connection.

Args:
connection: A sqlite3.Connection object (e.g., sqlite3.connect(':memory:')
or sqlite3.connect('costsharing.db'))
"""
self._conn = connection

# Use Row factory for dict-like access to rows
self._conn.row_factory = sqlite3.Row
# Enable foreign key constraints
self._conn.execute('PRAGMA foreign_keys = ON')

def is_user(self, email):
"""
Check if a user exists with the given email.

Args:
email: User's email address

Returns:
bool: True if user exists, False otherwise

Raises:
StorageException: If a database error occurs
"""
try:
cursor = self._conn.execute(
'SELECT 1 FROM users WHERE email = ?',
(email,)
)
return cursor.fetchone() is not None
except sqlite3.Error as e:
raise StorageException(f"Database error checking user existence: {e}") from e

def get_user_by_email(self, email):
"""
Get user by email address.

Args:
email: User's email address

Returns:
User if found

Raises:
UserNotFoundError: If user with the given email is not found
StorageException: If a database error occurs
"""
try:
cursor = self._conn.execute(
'SELECT id, email, name FROM users WHERE email = ?',
(email,)
)
row = cursor.fetchone()
if row is None:
raise UserNotFoundError(f"User with email '{email}' not found")
return User(id=row['id'], email=row['email'], name=row['name'])
except sqlite3.Error as e:
raise StorageException(f"Database error retrieving user by email: {e}") from e

def create_user(self, email, name):
"""
Create a new user.

Args:
email: User's email address
name: User's name

Returns:
Newly created User object

Raises:
DuplicateEmailError: If email already exists
StorageException: If a database error occurs
"""
try:
cursor = self._conn.execute(
'INSERT INTO users (email, name) VALUES (?, ?)',
(email, name)
)
self._conn.commit()
user_id = cursor.lastrowid
return User(id=user_id, email=email, name=name)
except sqlite3.IntegrityError as e:
self._conn.rollback()
# IntegrityError on users table insert is always a duplicate email
raise DuplicateEmailError() from e
except sqlite3.Error as e:
self._conn.rollback()
raise StorageException(f"Database error creating user: {e}") from e

def get_user_by_id(self, user_id):
"""
Get user by ID.

Args:
user_id: User ID

Returns:
User if found

Raises:
UserNotFoundError: If user with the given ID is not found
StorageException: If a database error occurs
"""
try:
cursor = self._conn.execute(
'SELECT id, email, name FROM users WHERE id = ?',
(user_id,)
)
row = cursor.fetchone()
if row is None:
raise UserNotFoundError(f"User with ID {user_id} not found")
return User(id=row['id'], email=row['email'], name=row['name'])
except sqlite3.Error as e:
raise StorageException(f"Database error retrieving user by ID: {e}") from e
4 changes: 4 additions & 0 deletions src/cost_sharing/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ class DuplicateEmailError(Exception):

class UserNotFoundError(Exception):
"""Raised when a requested user cannot be found"""


class StorageException(Exception):
"""Raised when a database storage operation fails"""
Empty file.
File renamed without changes.
File renamed without changes.
Loading