Skip to content

Commit

Permalink
Modularize DB Date Fields' Logic and Denormalize Item Models (#11)
Browse files Browse the repository at this point in the history
* feat: modularize date fields and update logic for db models

* refactor: denormalize ItemPrice model into Item model

- fix inheritance flow for User models and schema
- add necessary UserSession schema field

* fix: setup valid user response schema

- add UserBaseResponse that has datetime fields
- replace UserBase in api responses with UserBaseResponse
  • Loading branch information
dhruv-ahuja authored May 30, 2024
1 parent 4f1f360 commit c4dca9b
Show file tree
Hide file tree
Showing 9 changed files with 66 additions and 60 deletions.
4 changes: 2 additions & 2 deletions src/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

from .app import AppConfig
from .users import User, BlacklistedToken
from .poe import Item, ItemCategory, ItemPrice
from .poe import Item, ItemCategory


# group and export models for initializing Beanie connection
document_models: list[Type[beanie.Document]] = [User, BlacklistedToken, AppConfig, Item, ItemCategory, ItemPrice]
document_models: list[Type[beanie.Document]] = [User, BlacklistedToken, AppConfig, Item, ItemCategory]
19 changes: 19 additions & 0 deletions src/models/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import datetime as dt

from beanie import Document, after_event, Replace, SaveChanges, Update, ValidateOnSave
from pydantic import Field


class DateMetadataDocument(Document):
"""DateMetadataDocument provides created and updated time fields, and sets the correct `updated_time` each time the
model instance is modified."""

created_time: dt.datetime = Field(default_factory=dt.datetime.now)
updated_time: dt.datetime = Field(default_factory=dt.datetime.now)

@after_event(Update, Replace, SaveChanges, ValidateOnSave)
def update_document_time(self) -> None:
self.updated_time = dt.datetime.now()

class Settings:
is_root = True
52 changes: 8 additions & 44 deletions src/models/poe.py
Original file line number Diff line number Diff line change
@@ -1,82 +1,46 @@
import datetime as dt
from decimal import Decimal
from enum import Enum

from beanie import Document, Link, Replace, SaveChanges, Update, ValidateOnSave, after_event
from beanie import Link
from pydantic import Field

from src.schemas.poe import Currency
from src.models.common import DateMetadataDocument
from src.schemas.poe import ItemPrice


class ItemIdType(str, Enum):
pay = "pay"
receive = "receive"


# TODO: see if we can modularize the created-updated time and update-time aspects
class ItemCategory(Document):
class ItemCategory(DateMetadataDocument):
"""ItemCategory represents a major category that items belong to. A category is the primary form of classifying
items. Each category belongs to a category group (model not defined for category groups)."""

name: str
internal_name: str
group: str
created_time: dt.datetime = Field(default_factory=dt.datetime.now)
updated_time: dt.datetime = Field(default_factory=dt.datetime.now)

@after_event(Replace, SaveChanges, Update, ValidateOnSave)
def update_time(self):
self.updated_time = dt.datetime.now()

class Settings:
"""Defines the settings for the collection."""

name = "poe_item_categories"


class Item(Document):
"""Item represents a Path of Exile in-game item. Each item belongs to a category."""
class Item(DateMetadataDocument):
"""Item represents a Path of Exile in-game item. Each item belongs to a category. It contains information such as
item type and the current, past and predicted pricing, encapsulated in the `ItemPrice` schema."""

poe_ninja_id: int
id_type: ItemIdType | None = None
name: str
category: Link[ItemCategory]
price: ItemPrice | None
type_: str | None = Field(None, serialization_alias="type")
variant: str | None = None
icon_url: str | None = None
enabled: bool = True
created_time: dt.datetime = Field(default_factory=dt.datetime.now)
updated_time: dt.datetime = Field(default_factory=dt.datetime.now)

@after_event(Replace, SaveChanges, Update, ValidateOnSave)
def update_time(self):
self.updated_time = dt.datetime.now()

class Settings:
"""Defines the settings for the collection."""

name = "poe_items"


class ItemPrice(Document):
"""ItemPrice holds information regarding the current, past and future price of an item.
It stores the recent and predicted prices in a dictionary, with the date as the key."""

item: Link[Item]
price: Decimal
currency: Currency
price_history: dict[dt.datetime, Decimal]
price_history_currency: Currency
price_prediction: dict[dt.datetime, Decimal]
price_prediction_currency: Currency
created_time: dt.datetime = Field(default_factory=dt.datetime.now)
updated_time: dt.datetime = Field(default_factory=dt.datetime.now)

@after_event(Replace, SaveChanges, Update, ValidateOnSave)
def update_time(self):
self.updated_time = dt.datetime.now()

class Settings:
"""Defines the settings for the collection."""

name = "poe_item_prices"
3 changes: 2 additions & 1 deletion src/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
from pydantic import Field, SecretStr
from pymongo import IndexModel

from src.models.common import DateMetadataDocument
from src.schemas.users import UserBase, UserSession


# TODO: add user status and login attempts columns
class User(UserBase, Document):
class User(UserBase, DateMetadataDocument):
"""User represents a User of the application."""

password: SecretStr = Field(min_length=8)
Expand Down
18 changes: 17 additions & 1 deletion src/schemas/poe.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import datetime as dt
from decimal import Decimal
from enum import Enum

from pydantic import BaseModel


class Currency(str, Enum):
divines = "divines"
chaos = "chaos"
divines = "divines"


class ItemPrice(BaseModel):
"""ItemPrice holds information regarding the current, past and future price of an item.
It stores the recent and predicted prices in a dictionary, with the date as the key."""

price: Decimal
currency: Currency
price_history: dict[dt.datetime, Decimal]
price_history_currency: Currency
price_prediction: dict[dt.datetime, Decimal]
price_prediction_currency: Currency
16 changes: 11 additions & 5 deletions src/schemas/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,26 @@ class UserUpdateInput(BaseModel):


class UserBase(BaseModel):
"""UserBase is the base user model, representing User instances for API responses. Omits the
password field for security."""
"""UserBase is the base user model, encapsulating core-User instance data. Omits the password field for
security."""

id: PydanticObjectId | None = None
name: str = Field(min_length=3, max_length=255)
email: EmailStr
role: Role
created_time: dt.datetime = Field(default_factory=dt.datetime.now)
updated_time: dt.datetime = Field(default_factory=dt.datetime.now)


class UserBaseResponse(UserBase):
"""UserBaseResponse extends UserBase and includes the created and updated datetime fields, for User API
responses."""

created_time: dt.datetime
updated_time: dt.datetime


class UserSession(BaseModel):
"""UserSession encapsulates the user's session logic."""

refresh_token: str | None
expiration_time: dt.datetime | None
updated_time: dt.datetime = Field(default_factory=dt.datetime.now)
updated_time: dt.datetime = Field(default_factory=dt.datetime.utcnow)
4 changes: 2 additions & 2 deletions src/services/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from starlette import status

from src.models.users import User, BlacklistedToken
from src.schemas.users import UserBase, UserSession
from src.schemas.users import UserBase, UserBaseResponse, UserSession
from src.services import users as users_service
from src.utils.auth_utils import compare_values

Expand All @@ -30,7 +30,7 @@ async def check_users_credentials(form_data: OAuth2PasswordRequestForm) -> UserB
if not valid_password:
raise invalid_credentials_error

user_base = user = UserBase(
user_base = user = UserBaseResponse(
id=user.id,
name=user.name,
email=user.email,
Expand Down
6 changes: 3 additions & 3 deletions src/services/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from starlette.status import HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND

from src.models.users import User
from src.schemas.users import Role, UserBase, UserInput, UserUpdateInput
from src.schemas.users import Role, UserBase, UserBaseResponse, UserInput, UserUpdateInput
from src.utils import auth_utils


Expand Down Expand Up @@ -46,7 +46,7 @@ async def get_users() -> list[UserBase]:

# parsing User to UserBase using parse_obj to avoid `id` loss -- pydantic V2 bug
for user_record in user_records:
user = UserBase(
user = UserBaseResponse(
id=user_record.id,
name=user_record.name,
email=user_record.email,
Expand Down Expand Up @@ -98,7 +98,7 @@ async def get_user(
user_record = await get_user_from_database(None, user_email, missing_user_error=missing_user_error)

user_record = cast(User, user_record)
user = UserBase(
user = UserBaseResponse(
id=user_record.id,
name=user_record.name,
email=user_record.email,
Expand Down
4 changes: 2 additions & 2 deletions src/tests/routers/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from src import main
from src.models.users import BlacklistedToken, User
from src.schemas.users import Role, UserBase
from src.schemas.users import Role, UserBase, UserBaseResponse
from src.utils.auth_utils import hash_value


Expand Down Expand Up @@ -46,7 +46,7 @@ async def test_user() -> AsyncGenerator[User | UserBase, Any]:
except (beanie.exceptions.RevisionIdWasChanged, beanie.exceptions.DocumentAlreadyCreated):
pass

user_base = UserBase(
user_base = UserBaseResponse(
id=user.id,
name="backend_burger_test",
email=EMAIL,
Expand Down

0 comments on commit c4dca9b

Please sign in to comment.