Skip to content

Commit

Permalink
docs: add docstrings
Browse files Browse the repository at this point in the history
  • Loading branch information
montoyaobeso committed Jun 6, 2024
1 parent e4a456c commit ac1d9e2
Show file tree
Hide file tree
Showing 24 changed files with 119 additions and 286 deletions.
16 changes: 14 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# Environment
STAGE = "local"

# AWS
BUCKET_NAME = "transactions-email-sender-files-dev"

# SendGrid API KEY
SENDGRID_SENDER_EMAIL = "<SET YOUR VERIFIED SENDER>"
SENDGRID_API_KEY = "<SET YOUR SENDGRID API KEY>"
SENDGRID_SENDER_EMAIL = "storinoreply@gmail.com"
SENDGRID_API_KEY = "<SET API KEY>"

# DB Config
POSTGRES_USER="postgres"
POSTGRES_PASSWORD="postgres"
POSTGRES_DB="postgres"
POSTGRES_HOST="localhost"
22 changes: 11 additions & 11 deletions src/api/main.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
from dotenv import load_dotenv
from fastapi import FastAPI
from mangum import Mangum
from dotenv import load_dotenv

load_dotenv()


from src.app.db import models
from src.app.db.database import engine

models.Base.metadata.create_all(bind=engine)

from src.api.routers import root
from src.api.routers import presiged_url
from src.api.routers import load_transactions
from src.api.routers import load_transactions_s3
from src.api.routers import account
from src.api.routers import transaction
from src.api.routers import send_balance

from src.api.routers import (
account,
load_transactions,
load_transactions_s3,
presiged_url,
root,
send_balance,
transaction,
)

app = FastAPI(
title="Transactions Email Generator",
description="Generate a transactions summary email.",
description="Manage accounts, transactions and send balances to registered email.",
version="0.0.1",
contact={
"name": "Abraham Montoya",
Expand Down
2 changes: 2 additions & 0 deletions src/api/routers/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ async def create_account(
account: schemas.AccountCreate,
db: Session = Depends(get_db),
):
"""Create a new account."""
# Get account by email
db_account = crud.get_account_by_email(db, email=account.email)

Expand All @@ -41,6 +42,7 @@ async def create_account(

@router.get("/accounts", response_model=List[schemas.Accounts])
async def get_accounts(db: Session = Depends(get_db)):
"""Get all accounts."""

# Get account by email
db_account = crud.get_accounts(db)
Expand Down
2 changes: 1 addition & 1 deletion src/api/routers/load_transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async def load_transactions(
file: UploadFile = File(..., description="Binary CSV file data."),
db: Session = Depends(get_db),
):

"""Load and save transactions from provided file."""
db_account = crud.get_account(db, account_id=account_id)

if file.content_type == "text/csv":
Expand Down
4 changes: 1 addition & 3 deletions src/api/routers/load_transactions_s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ async def load_transactions_s3(
],
db: Session = Depends(get_db),
):
"""
Load transactions from a file uploaded to s3.
"""
"""Load transactions from a file uploaded to s3."""

# Create s3 client and get object
s3 = boto3.client("s3")
Expand Down
1 change: 1 addition & 0 deletions src/api/routers/presiged_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

@router.get("")
async def presigned_url():
"""Get a prsigned url and additional fields to upload a file to s3."""

response = create_presigned_post(
bucket_name=os.environ["BUCKET_NAME"],
Expand Down
1 change: 1 addition & 0 deletions src/api/routers/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

@router.get("/")
async def root():
"""Get a welcome messge from the API."""
return JSONResponse(
content={"message": "Welcome to transactions email generator API."},
status_code=status.HTTP_200_OK,
Expand Down
3 changes: 2 additions & 1 deletion src/api/routers/send_balance.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def get_db():


@router.post("")
async def load_transactions(
async def send_balance(
account_id: Annotated[
str,
Form(
Expand All @@ -48,6 +48,7 @@ async def load_transactions(
] = None,
db: Session = Depends(get_db),
):
"""Send balance to account's email."""

# Get account info
db_account = crud.get_account(db, account_id=account_id)
Expand Down
2 changes: 2 additions & 0 deletions src/api/routers/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ async def create_transaction(
account_id: int,
db: Session = Depends(get_db),
):
"""Register a single transaction."""
db_transaction = crud.get_transaction_by_ids(
db, account_id=account_id, transaction_id=transaction.transaction_id
)
Expand All @@ -45,6 +46,7 @@ async def get_transactions(
account_id: int,
db: Session = Depends(get_db),
):
"""Get all transactions."""
db_transaction = crud.get_transactions_by_account_id(db, account_id=account_id)

if db_transaction is None:
Expand Down
15 changes: 1 addition & 14 deletions src/app/aws/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,8 @@
def create_presigned_post(
bucket_name, object_name, fields=None, conditions=None, expiration=3600
):
"""Generate a presigned URL S3 POST request to upload a file
"""Generate a presigned URL S3 POST request to upload a file"""

:param bucket_name: string
:param object_name: string
:param fields: Dictionary of prefilled form fields
:param conditions: List of conditions to include in the policy
:param expiration: Time in seconds for the presigned URL to remain valid
:return: Dictionary with the following keys:
url: URL to post to
fields: Dictionary of form fields and values to submit with the POST
:return: None if error.
"""

# Generate a presigned S3 POST URL
s3_client = boto3.client("s3")
try:
response = s3_client.generate_presigned_post(
Expand All @@ -34,5 +22,4 @@ def create_presigned_post(
logging.error(e)
return None

# The response contains the presigned URL and required fields
return response
8 changes: 3 additions & 5 deletions src/app/aws/secrets.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import json

import boto3
from botocore.exceptions import ClientError
import json
import logging


def get_secret(secret_name: str, region: str = "us-west-2"):
"""Get a secret value from AWS SecretsManager service."""

# Create a Secrets Manager client
session = boto3.session.Session()
client = session.client(service_name="secretsmanager", region_name=region)

Expand All @@ -17,6 +17,4 @@ def get_secret(secret_name: str, region: str = "us-west-2"):

secret = get_secret_value_response["SecretString"]

logging.info("Secret retrieved successfully.")

return json.loads(secret)
4 changes: 3 additions & 1 deletion src/app/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def wrapper(*args, **kwargs):

@with_session
def create_account_flow(args, session):
print("Executing create_account flow...")
"""Create a new account."""

account = schemas.AccountCreate(name=args.name, email=args.email)

Expand All @@ -52,6 +52,7 @@ def create_account_flow(args, session):

@with_session
def load_transactions_flow(args, session):
"""Load transactions to database."""

db_account = crud.get_account(session, account_id=args.account_id)

Expand Down Expand Up @@ -89,6 +90,7 @@ def load_transactions_flow(args, session):

@with_session
def send_balance_flow(args, session):
"""Send balance to account's registered email."""
# Get account info
db_account = crud.get_account(session, account_id=args.account_id)

Expand Down
26 changes: 18 additions & 8 deletions src/app/db/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,33 @@


def get_account(db: Session, account_id: int):
"""Get account by id."""
return db.query(models.Account).filter(models.Account.id == account_id).first()


def get_account_by_email(db: Session, email: str):
"""Get account by eamil."""
return db.query(models.Account).filter(models.Account.email == email).first()


def get_accounts(db: Session, skip: int = 0, limit: int = 100):
"""Get all accounts."""
return db.query(models.Account).offset(skip).limit(limit).all()


def create_account(db: Session, account: schemas.AccountCreate):
"""Create a new account."""
db_item = models.Account(email=account.email, name=account.name)
db.add(db_item)
db.commit()
db.refresh(db_item)
return parse_obj_as(schemas.Account, db_item)


def get_transactions(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.Transaction).offset(skip).limit(limit).all()
def get_transactions(db: Session):
"""Get all transactions"""
# TODO: Support pagination with offset() and limit()
return db.query(models.Transaction).all()


def get_transactions_by_date(
Expand All @@ -41,6 +47,8 @@ def get_transactions_by_date(
from_date: date = None,
to_date: date = None,
) -> List[schemas.Transaction]:
"""Get all transactions filter by dates."""

if from_date is None and to_date is None:
return parse_obj_as(
List[schemas.Transaction],
Expand Down Expand Up @@ -86,9 +94,9 @@ def get_transactions_by_date(
)


def get_transaction_by_ids(
db: Session, account_id: int, transaction_id: int, skip: int = 0, limit: int = 100
):
def get_transaction_by_ids(db: Session, account_id: int, transaction_id: int):
"""Get a transaction by account id and transaction id"""
# TODO: Support pagination with offset() and limit()
return (
db.query(models.Transaction)
.filter(models.Transaction.transaction_id == transaction_id)
Expand All @@ -97,9 +105,9 @@ def get_transaction_by_ids(
)


def get_transactions_by_account_id(
db: Session, account_id: int, skip: int = 0, limit: int = 100
):
def get_transactions_by_account_id(db: Session, account_id: int):
"""Get all transactions by account id."""
# TODO: Support pagination with offset() and limit()
return (
db.query(models.Transaction)
.filter(models.Transaction.account_id == account_id)
Expand All @@ -110,6 +118,7 @@ def get_transactions_by_account_id(
def create_transaction(
db: Session, transaction: schemas.TransactionCreate, account_id: int
):
"""Create transaction by account id."""
db_item = models.Transaction(**transaction.model_dump(), account_id=account_id)
db.add(db_item)
db.commit()
Expand All @@ -120,6 +129,7 @@ def create_transaction(
def save_transactions_bulk(
db: Session, transactions: List[schemas.TransactionCreate], account_id: int
):
"""Save bulk transactions by account id. Existing transactions by unique(transaction_id and account_id) are ignored."""
data = [
{
"transaction_id": t.transaction_id,
Expand Down
1 change: 0 additions & 1 deletion src/app/db/database.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import logging
import os

from sqlalchemy import create_engine
Expand Down
2 changes: 2 additions & 0 deletions src/app/db/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pydantic import BaseModel, EmailStr, Field


# Transaction schemas
class TransactionBase(BaseModel):
transaction_id: int
date: date
Expand All @@ -25,6 +26,7 @@ class Transactions(TransactionBase):
id: int


# Account schemas
class AccountBase(BaseModel):
name: str
email: EmailStr
Expand Down
2 changes: 2 additions & 0 deletions src/app/email/content_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


class EmailBodyBuilder:
"""Class to build a custom HTMP email body."""

def __init__(
self,
Expand All @@ -19,6 +20,7 @@ def __init__(
self.transactions_history = transactions_history

def get_email_body(self) -> str:
"""Render HTML email body."""

file_loader = FileSystemLoader("src/app/templates")

Expand Down
4 changes: 4 additions & 0 deletions src/app/email/sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@


class EmailSender:
"""Class to send emails using SendGrid service."""

def __init__(self, subject: str, recipient: str, body_content: str) -> None:
self.subject = subject
self.recipient = recipient
self.body_content = body_content

def set_credentials_from_aws_secrets(self):
"""Set credentials from AWS SecretsManager."""
secret = get_secret(os.environ["SECRET_NAME"])
os.environ["SENDGRID_SENDER_EMAIL"] = secret["SENDGRID_SENDER_EMAIL"]
os.environ["SENDGRID_API_KEY"] = secret["SENDGRID_API_KEY"]

def send_email(self) -> None:
"""Send email to recipient."""
if (
"SENDGRID_SENDER_EMAIL" not in os.environ
or "SENDGRID_API_KEY" not in os.environ
Expand Down
4 changes: 2 additions & 2 deletions src/app/transactions/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ def get_avg_credit_amount(self):
"""
return self.txns[self.txns["value"] >= 0]["value"].mean()

def get_transactions_history(self) -> list:
def get_transactions_history(self) -> dict:
"""Return a sorted list of transactions per month and year
Returns:
list: sorted list of tuples with transactions per month.
dict: dictionary otuples with transactions per month.
"""
df = self.txns.copy()

Expand Down
1 change: 1 addition & 0 deletions src/app/validator/input_validator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pandera import Column, DataFrameSchema, Check, Index
from pandera.engines import pandas_engine

# Schema to validate CSV input file, ensure data format and non-null values.
schema = DataFrameSchema(
{
"Id": Column(
Expand Down
Loading

0 comments on commit ac1d9e2

Please sign in to comment.