-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
some useful boilerplate for the application (#3)
* boilerplate * more boilerplate * more boilerplate * more boilerplate * use built-in types * docker stuff * change tests * small fixes * stuff * missing settings * change env var location
- Loading branch information
1 parent
f504430
commit b1f0664
Showing
19 changed files
with
542 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
"""Defines CRUD interface for user API.""" | ||
|
||
import asyncio | ||
|
||
from types_aiobotocore_dynamodb.service_resource import DynamoDBServiceResource | ||
|
||
from store.app.api.db import get_aio_db | ||
from store.app.api.model import Token, User | ||
|
||
|
||
async def add_user(user: User, db: DynamoDBServiceResource) -> None: | ||
"""Adds a user to the database. | ||
Args: | ||
user: The user to add. | ||
db: The DynamoDB database. | ||
""" | ||
table = await db.Table("Users") | ||
await table.put_item(Item=user.model_dump()) | ||
|
||
|
||
async def get_user(user_id: str, db: DynamoDBServiceResource) -> User: | ||
"""Gets a user from the database. | ||
Args: | ||
user_id: The ID of the user to retrieve. | ||
db: The DynamoDB database. | ||
""" | ||
table = await db.Table("Users") | ||
user_dict = await table.get_item(Key={"user_id": user_id}) | ||
user = User.model_validate(user_dict["Item"]) | ||
return user | ||
|
||
|
||
async def get_user_count(db: DynamoDBServiceResource) -> int: | ||
"""Counts the users in the database. | ||
Args: | ||
db: The DynamoDB database. | ||
""" | ||
table = await db.Table("Users") | ||
return await table.item_count | ||
|
||
|
||
async def add_token(token: Token, db: DynamoDBServiceResource) -> None: | ||
"""Adds a token to the database. | ||
Args: | ||
token: The token to add. | ||
db: The DynamoDB database. | ||
""" | ||
table = await db.Table("UserTokens") | ||
await table.put_item(Item=token.model_dump()) | ||
|
||
|
||
async def get_token(token_id: str, db: DynamoDBServiceResource) -> Token: | ||
"""Gets a token from the database. | ||
Args: | ||
token_id: The ID of the token to retrieve. | ||
db: The DynamoDB database. | ||
""" | ||
table = await db.Table("UserTokens") | ||
token_dict = await table.get_item(Key={"token_id": token_id}) | ||
token = Token.model_validate(token_dict["Item"]) | ||
return token | ||
|
||
|
||
async def test_adhoc() -> None: | ||
async with get_aio_db() as db: | ||
await add_user(User(user_id="ben", email="[email protected]"), db) | ||
# print(await get_user("ben", db)) | ||
# print(await get_user_count(db)) | ||
# await get_token("ben", db) | ||
|
||
|
||
if __name__ == "__main__": | ||
# python -m store.app.api.crud.users | ||
asyncio.run(test_adhoc()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
# mypy: disable-error-code="empty-body" | ||
"""Defines base tools for interacting with the database.""" | ||
|
||
import asyncio | ||
from contextlib import asynccontextmanager | ||
from typing import AsyncGenerator, Literal | ||
|
||
import aioboto3 | ||
from types_aiobotocore_dynamodb.service_resource import DynamoDBServiceResource | ||
|
||
from store.settings import settings | ||
|
||
|
||
@asynccontextmanager | ||
async def get_aio_db() -> AsyncGenerator[DynamoDBServiceResource, None]: | ||
session = aioboto3.Session() | ||
async with session.resource( | ||
"dynamodb", | ||
endpoint_url=settings.database.endpoint_url, | ||
region_name=settings.database.region_name, | ||
aws_access_key_id=settings.database.aws_access_key_id, | ||
aws_secret_access_key=settings.database.aws_secret_access_key, | ||
) as db: | ||
yield db | ||
|
||
|
||
async def _create_dynamodb_table( | ||
db: DynamoDBServiceResource, | ||
name: str, | ||
columns: list[tuple[str, Literal["S", "N", "B"]]], | ||
pks: list[tuple[str, Literal["HASH", "RANGE"]]], | ||
deletion_protection: bool = False, | ||
read_capacity_units: int = 2, | ||
write_capacity_units: int = 2, | ||
billing_mode: Literal["PROVISIONED", "PAY_PER_REQUEST"] = "PAY_PER_REQUEST", | ||
) -> None: | ||
table = await db.create_table( | ||
AttributeDefinitions=[{"AttributeName": n, "AttributeType": t} for n, t in columns], | ||
TableName=name, | ||
KeySchema=[{"AttributeName": pk[0], "KeyType": pk[1]} for pk in pks], | ||
ProvisionedThroughput={"ReadCapacityUnits": read_capacity_units, "WriteCapacityUnits": write_capacity_units}, | ||
OnDemandThroughput={"MaxReadRequestUnits": read_capacity_units, "MaxWriteRequestUnits": write_capacity_units}, | ||
DeletionProtectionEnabled=deletion_protection, | ||
BillingMode=billing_mode, | ||
) | ||
await table.wait_until_exists() | ||
|
||
|
||
async def create_tables(db: DynamoDBServiceResource | None = None) -> None: | ||
"""Initializes all of the database tables. | ||
Args: | ||
db: The DynamoDB database. | ||
""" | ||
if db is None: | ||
async with get_aio_db() as db: | ||
await create_tables(db) | ||
else: | ||
await _create_dynamodb_table( | ||
db=db, | ||
name="Users", | ||
columns=[ | ||
("user_id", "S"), | ||
], | ||
pks=[ | ||
("user_id", "HASH"), | ||
], | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
# python -m store.app.api.db | ||
asyncio.run(create_tables()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
"""Utility functions for sending emails to users.""" | ||
|
||
import argparse | ||
import asyncio | ||
import datetime | ||
import logging | ||
import textwrap | ||
from dataclasses import dataclass | ||
from email.mime.multipart import MIMEMultipart | ||
from email.mime.text import MIMEText | ||
|
||
import aiosmtplib | ||
|
||
from store.app.api.token import create_token, load_token | ||
from store.settings import settings | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
async def send_email(subject: str, body: str, to: str) -> None: | ||
msg = MIMEMultipart("alternative") | ||
msg["Subject"] = subject | ||
msg["From"] = f"{settings.email.name} <{settings.email.email}>" | ||
msg["To"] = to | ||
|
||
msg.attach(MIMEText(body, "html")) | ||
|
||
smtp_client = aiosmtplib.SMTP(hostname=settings.email.host, port=settings.email.port) | ||
|
||
await smtp_client.connect() | ||
await smtp_client.login(settings.email.email, settings.email.password) | ||
await smtp_client.sendmail(settings.email.email, to, msg.as_string()) | ||
await smtp_client.quit() | ||
|
||
|
||
@dataclass | ||
class OneTimePassPayload: | ||
email: str | ||
|
||
def encode(self) -> str: | ||
expire_minutes = settings.crypto.expire_otp_minutes | ||
expire_after = datetime.timedelta(minutes=expire_minutes) | ||
return create_token({"email": self.email}, expire_after=expire_after) | ||
|
||
@classmethod | ||
def decode(cls, payload: str) -> "OneTimePassPayload": | ||
data = load_token(payload) | ||
return cls(email=data["email"]) | ||
|
||
|
||
async def send_otp_email(payload: OneTimePassPayload, login_url: str) -> None: | ||
url = f"{login_url}?otp={payload.encode()}" | ||
|
||
body = textwrap.dedent( | ||
f""" | ||
<h1><code>don't panic</code><br/><code>stay human</code></h1> | ||
<h2><code><a href="{url}">log in</a></code></h2> | ||
<p>Or copy-paste this link: {url}</p> | ||
""" | ||
) | ||
|
||
await send_email(subject="One-Time Password", body=body, to=payload.email) | ||
|
||
|
||
async def send_delete_email(email: str) -> None: | ||
body = textwrap.dedent( | ||
""" | ||
<h1><code>don't panic</code><br/><code>stay human</code></h1> | ||
<h2><code>your account has been deleted</code></h2> | ||
""" | ||
) | ||
|
||
await send_email(subject="Account Deleted", body=body, to=email) | ||
|
||
|
||
async def send_waitlist_email(email: str) -> None: | ||
body = textwrap.dedent( | ||
""" | ||
<h1><code>don't panic</code><br/><code>stay human</code></h1> | ||
<h2><code>you're on the waitlist!</code></h2> | ||
<p>Thanks for signing up! We'll let you know when you can log in.</p> | ||
""" | ||
) | ||
|
||
await send_email(subject="Waitlist", body=body, to=email) | ||
|
||
|
||
def test_email_adhoc() -> None: | ||
parser = argparse.ArgumentParser(description="Test sending an email.") | ||
parser.add_argument("subject", help="The subject of the email.") | ||
parser.add_argument("body", help="The body of the email.") | ||
parser.add_argument("to", help="The recipient of the email.") | ||
args = parser.parse_args() | ||
|
||
asyncio.run(send_email(args.subject, args.body, args.to)) | ||
|
||
|
||
if __name__ == "__main__": | ||
# python -m bot.api.email | ||
test_email_adhoc() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# mypy: disable-error-code="var-annotated" | ||
"""Defines the table models for the API.""" | ||
|
||
import datetime | ||
from dataclasses import field | ||
|
||
from pydantic import BaseModel | ||
|
||
|
||
class User(BaseModel): | ||
user_id: str | ||
email: str | ||
banned: bool = field(default=False) | ||
deleted: bool = field(default=False) | ||
|
||
|
||
class Token(BaseModel): | ||
token_id: str | ||
user_id: str | ||
issued: datetime.datetime = field(default_factory=datetime.datetime.now) | ||
disabled: bool = field(default=False) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
"""Defines the API endpoint for creating, deleting and updating user information.""" | ||
|
||
import logging | ||
|
||
from fastapi import APIRouter | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
api_router = APIRouter() |
Oops, something went wrong.