diff --git a/.env.sample b/.env.sample
index c20cd63..8b410f5 100644
--- a/.env.sample
+++ b/.env.sample
@@ -10,6 +10,12 @@ POSTGRES_PASSWORD=
POSTGRES_HOST=
POSTGRES_PORT=
+TEST_POSTGRES_DB=
+TEST_POSTGRES_USER=
+TEST_POSTGRES_PASSWORD=
+TEST_POSTGRES_HOST=
+TEST_POSTGRES_PORT=
+
CORS_ALLOWED_HOST=
CORS_ALLOW_ALL_ORIGINS=
diff --git a/README.md b/README.md
index 87d9aa9..5f71de8 100644
--- a/README.md
+++ b/README.md
@@ -7,13 +7,10 @@
+
-
-[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/tigran-saatchyan/UniMart_by_EvoQ)
-[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
-
## Table of Contents
* [Background / Overview](#background--overview)
@@ -25,12 +22,10 @@
* [Documentation](#documentation)
* [Browser Support](#browser-support)
* [Dependencies](#dependencies)
-* [Todo](#todo)
* [Release History](#release-history)
* [Changelog](#changelog)
* [Issues](#issues)
* [Bugs](#bugs)
-* [Deployment](#deployment)
* [Translations](#translations)
* [Authors](#authors)
* [Acknowledgments](#acknowledgments)
@@ -38,7 +33,12 @@
* [License](#license)
## Background / Overview
-
+**UniMart by EvoQ API** is a tool that allows
+developers to integrate e-commerce features
+like product management, inventory control,
+order processing, and customer data management
+into their applications or websites, making it
+easier to run online stores.
## Features
@@ -61,9 +61,29 @@ Open your terminal and type in.
### Setting up
-
### Structure / Scaffolding
```text
+ ========= Tigran Saatchyan ~ git version 2.34.1
+ =============== -------------------------------------
+ ================= Project: UniMart_by_EvoQ (2 branches)
+ === ==============
+ =================== Created: 5 days ago
+ ========== Language:
+ ========================== ======= ● Python (100.0 %)
+ ============================ ======== Authors: 100% Tigran Saatchyan 20
+============================= ========= URL: git@github.com:tigran-saatchyan/UniMart_by_EvoQ.git
+============================ ========== Commits: 20
+========== ============================
+========= ============================= Lines of code: 1369
+ ======== ============================ Size: 244.97 KiB (60 files)
+ ======= ========================== License: MIT
+ ==========
+ ===================
+ ============== ===
+ =================
+ ===============
+ =========
+
```
@@ -72,15 +92,58 @@ Open your terminal and type in.
```text
UniMart_by_EvoQ
app
+├── __init__.py
├── api
+│ ├── __init__.py
+│ └── v1
+│ ├── __init__.py
+│ ├── auth.py
+│ ├── cart.py
+│ ├── dependencies.py
+│ ├── products.py
+│ └── routers.py
├── db
+│ ├── __init__.py
+│ └── db.py
├── main.py
+├── migrations
+│ ├── env.py
+│ ├── README
+│ ├── script.py.mako
+│ └── versions
├── models
+│ ├── __init__.py
+│ ├── base_model.py
+│ ├── cart.py
+│ ├── products.py
+│ └── users.py
├── repositories
+│ ├── __init__.py
+│ ├── cart.py
+│ ├── products.py
+│ └── users.py
├── schemas
+│ ├── __init__.py
+│ ├── cart.py
+│ ├── products.py
+│ └── users.py
├── services
+│ ├── __init__.py
+│ ├── cart.py
+│ ├── managers.py
+│ ├── products.py
+│ └── validators.py
├── settings
+│ ├── __init__.py
+│ ├── auth.py
+│ └── config.py
└── utils
+ ├── __init__.py
+ ├── factories.py
+ ├── repository.py
+ ├── unitofwork.py
+ └── utils.py
+
```
@@ -90,36 +153,27 @@ app
Note: The scaffolding was generated with tree.
## Documentation
+- http://localhost:8000/
- http://localhost:8000/docs
-- http://localhost:8000/redoc
## Dependencies
List of dependencies used in the project
-[//]: # ()
-[//]: # (| **Main Libraries** | **Other Libraries** |)
-
-[//]: # (|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|)
-
-[//]: # (| ![Python Badge](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftigran-saatchyan%2FAtomicProgress_by_Evoq%2Fdevelop%2Fpyproject.toml&query=%24.tool.poetry.dependencies.python&style=flat&logo=python&label=Python) | ![Pillow Badge](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftigran-saatchyan%2FAtomicProgress_by_Evoq%2Fdevelop%2Fpyproject.toml&query=%24.tool.poetry.dependencies.Pillow&style=flat&label=Pillow) |)
-[//]: # (| ![Django Badge](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftigran-saatchyan%2FAtomicProgress_by_Evoq%2Fdevelop%2Fpyproject.toml&query=%24.tool.poetry.dependencies.Django&style=flat&logo=django&label=Django) | ![psycopg Badge](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftigran-saatchyan%2FAtomicProgress_by_Evoq%2Fdevelop%2Fpyproject.toml&query=%24.tool.poetry.dependencies.psycopg&style=flat&label=psycopg) |)
+| **Main Libraries** | **Other Libraries** |
+|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| ![Python Badge](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftigran-saatchyan%2FUniMart_by_EvoQ%2Fdevelop%2Fpyproject.toml&query=%24.tool.poetry.dependencies.python&style=flat&logo=python&label=Python) | ![Alembic](https://img.shields.io/badge/FastAPI--users-%5E2.8.2-blue?logo=FastAPI) |
+| ![FastAPI](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftigran-saatchyan%2FUniMart_by_EvoQ%2Fdevelop%2Fpyproject.toml&query=%24.tool.poetry.dependencies.fastapi.version&style=flat&logo=fastapi&label=FastAPI) | ![asyncpg](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftigran-saatchyan%2FUniMart_by_EvoQ%2Fdevelop%2Fpyproject.toml&query=%24.tool.poetry.dependencies.asyncpg&logo=PostgreSQL&style=flat&label=asyncpg) |
+| ![SQLAlchemy](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftigran-saatchyan%2FUniMart_by_EvoQ%2Fdevelop%2Fpyproject.toml&query=%24.tool.poetry.dependencies.sqlalchemy&style=flat&logo=sqlalchemy&label=SQLAlchemy) | ![pytest](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftigran-saatchyan%2FUniMart_by_EvoQ%2Fdevelop%2Fpyproject.toml&query=%24.tool.poetry.group.develop.dependencies.pytest&logo=pytest&style=flat&label=pytest) |
+| ![Alembic](https://img.shields.io/badge/Alembic-%5E1.12.1-blue?logo=Alembic) | ![pytest](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftigran-saatchyan%2FUniMart_by_EvoQ%2Fdevelop%2Fpyproject.toml&query=%24.tool.poetry.group.develop.dependencies.ruff&logo=ruff&style=flat&label=ruff) |
+| | ![pytest](https://img.shields.io/badge/pre--commit-%5E3.5.0-blue?logo=pre-commit) |
+| | ![pytest](https://img.shields.io/badge/pytest--postgresql-%5E5.0.0-blue?logo=pytest) |
+| | ![pytest](https://img.shields.io/badge/pytest--asyncio-%5E0.21.1-blue?logo=pytest) |
+| | ![pytest](https://img.shields.io/badge/pytest--cov-%5E4.1.0-blue?logo=pytest) |
-[//]: # (| ![Redis Badge](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftigran-saatchyan%2FAtomicProgress_by_Evoq%2Fdevelop%2Fpyproject.toml&query=%24.tool.poetry.dependencies.redis&style=flat&logo=redis&label=Redis) | ![Celery Badge](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftigran-saatchyan%2FAtomicProgress_by_Evoq%2Fdevelop%2Fpyproject.toml&query=%24.tool.poetry.dependencies.celery&style=flat&logo=Celery&label=Celery) |)
-[//]: # (| ![Django-Celery-Beat Badge](https://img.shields.io/badge/Django--Celery--Beat-%5E2.5.0-blue?logo=Django) | ![drf-yasg Badge](https://img.shields.io/badge/drf--yasg-%5E1.21.7-blue?logo=django) |)
-
-[//]: # (| ![DRF Badge](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftigran-saatchyan%2FAtomicProgress_by_Evoq%2Fdevelop%2Fpyproject.toml&query=%24.tool.poetry.dependencies.djangorestframework&style=flat&logo=django&label=DRF) | ![django-filter Badge](https://img.shields.io/badge/django--filter-%5E23.3-blue?logo=django) |)
-
-[//]: # (| | ![DRFSimpleJWT Badge](https://img.shields.io/badge/DRFSimpleJWT-%5E5.3.0-blue?logo=django) |)
-
-[//]: # (| | ![requests Badge](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftigran-saatchyan%2FAtomicProgress_by_Evoq%2Fdevelop%2Fpyproject.toml&query=%24.tool.poetry.dependencies.requests&style=flat&label=requests) |)
-
-[//]: # (| | ![pyTelegramBotAPI Badge](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftigran-saatchyan%2FAtomicProgress_by_Evoq%2Fdevelop%2Fpyproject.toml&query=%24.tool.poetry.dependencies.pytelegrambotapi&style=flat&label=pyTelegramBotAPI) |)
-
-
-## Todo
+[//]: # (## Todo)
## Release History
@@ -143,8 +197,7 @@ Please make sure to read the [Issue Reporting Checklist](https://github.com/tigr
If you have questions, feature requests or a bug you want to report, please click [here](https://github.com/tigran-saatchyan/UniMart_by_Evoq/issues) to file an issue.
-## Deployment
-
+[//]: # (## Deployment)
## Translations
diff --git a/app/api/v1/auth.py b/app/api/v1/auth.py
index 59eea1f..ca42ee6 100644
--- a/app/api/v1/auth.py
+++ b/app/api/v1/auth.py
@@ -1,3 +1,5 @@
+"""Authentication routes for the FastAPI application."""
+
from fastapi import FastAPI
from fastapi_users import FastAPIUsers
@@ -7,7 +9,7 @@
from app.settings.auth import auth_backend
PREFIX = "/api/v1"
-AUTH_TAG = "Authication"
+AUTH_TAG = "Authentication"
fastapi_users = FastAPIUsers[User, int](
@@ -17,6 +19,11 @@
def set_up_auth_routes(application: FastAPI):
+ """Set up authentication routes for the FastAPI application.
+
+ Args:
+ application (FastAPI): The FastAPI application instance.
+ """
application.include_router(
fastapi_users.get_auth_router(auth_backend),
prefix=f"{PREFIX}/jwt",
diff --git a/app/api/v1/cart.py b/app/api/v1/cart.py
index 0c412b9..b33f338 100644
--- a/app/api/v1/cart.py
+++ b/app/api/v1/cart.py
@@ -1,3 +1,5 @@
+"""Cart API endpoints."""
+
from typing import Annotated, List, Union
from fastapi import APIRouter, Depends
@@ -19,6 +21,17 @@ async def add_product_to_cart(
product: Union[CartCreate, List[CartCreate]],
uow: UOWDependency,
) -> Union[int, List[int]]:
+ """Add one or multiple products to the user's cart.
+
+ Args:
+ user (User): The authenticated user.
+ product (Union[CartCreate, List[CartCreate]]): The product(s)
+ to be added to the cart.
+ uow (UOWDependency): Unit of Work dependency.
+
+ Returns:
+ Union[int, List[int]]: The ID(s) of the added product(s) in the cart.
+ """
return await CartService().add(uow, product, user)
@@ -27,6 +40,15 @@ async def get_all_products_from_cart(
uow: UOWDependency,
user: Annotated[User, Depends(current_user)],
):
+ """Get all products from the user's cart.
+
+ Args:
+ uow (UOWDependency): Unit of Work dependency.
+ user (User): The authenticated user.
+
+ Returns:
+ List: List of products in the user's cart.
+ """
return await CartService().get_all(uow, user)
@@ -36,6 +58,16 @@ async def get_one_product_from_cart(
product_id: int,
user: Annotated[User, Depends(current_user)],
):
+ """Get details of a specific product in the user's cart.
+
+ Args:
+ uow (UOWDependency): Unit of Work dependency.
+ product_id (int): ID of the product to retrieve.
+ user (User): The authenticated user.
+
+ Returns:
+ dict: Details of the requested product in the cart.
+ """
return await CartService().get(uow, product_id, user)
@@ -46,6 +78,17 @@ async def update_product_quantity(
product: CartUpdate,
user: Annotated[User, Depends(current_user)],
):
+ """Update the quantity of a product in the user's cart.
+
+ Args:
+ uow (UOWDependency): Unit of Work dependency.
+ product_id (int): ID of the product to update.
+ product (CartUpdate): Updated product information.
+ user (User): The authenticated user.
+
+ Returns:
+ dict: Details of the updated product in the cart.
+ """
return await CartService().update(uow, product_id, product, user)
@@ -55,6 +98,16 @@ async def remove_product_from_cart(
product_id: int,
user: Annotated[User, Depends(current_user)],
):
+ """Remove a product from the user's cart.
+
+ Args:
+ uow (UOWDependency): Unit of Work dependency.
+ product_id (int): ID of the product to remove.
+ user (User): The authenticated user.
+
+ Returns:
+ bool: True if the product is successfully removed.
+ """
return await CartService().delete(uow, product_id, user)
@@ -63,6 +116,15 @@ async def clear_cart(
uow: UOWDependency,
user: Annotated[User, Depends(current_user)],
):
+ """Clear all products from the user's cart.
+
+ Args:
+ uow (UOWDependency): Unit of Work dependency.
+ user (User): The authenticated user.
+
+ Returns:
+ bool: True if the cart is successfully cleared.
+ """
return await CartService().delete_all(uow, user)
@@ -71,4 +133,13 @@ async def get_total_cart_price(
user: Annotated[User, Depends(current_user)],
uow: UOWDependency,
) -> float:
+ """Get the total price of all products in the user's cart.
+
+ Args:
+ user (User): The authenticated user.
+ uow (UOWDependency): Unit of Work dependency.
+
+ Returns:
+ float: The total price of all products in the cart.
+ """
return await CartService().get_total_price(uow, user)
diff --git a/app/api/v1/dependencies.py b/app/api/v1/dependencies.py
index 7241702..3806fc6 100644
--- a/app/api/v1/dependencies.py
+++ b/app/api/v1/dependencies.py
@@ -1,3 +1,5 @@
+"""Dependencies for FastAPI endpoints."""
+
from typing import Annotated
from fastapi import Depends
diff --git a/app/api/v1/products.py b/app/api/v1/products.py
index effa883..f37392a 100644
--- a/app/api/v1/products.py
+++ b/app/api/v1/products.py
@@ -1,3 +1,5 @@
+"""API endpoints for products."""
+
from typing import Annotated
from fastapi import APIRouter, Depends
@@ -19,6 +21,16 @@ async def add_product(
product: ProductsCreate,
uow: UOWDependency,
) -> int:
+ """Add a new product.
+
+ Args:
+ user (User): The authenticated user.
+ product (ProductsCreate): The product information to be added.
+ uow (UOWDependency): Unit of Work dependency.
+
+ Returns:
+ int: The ID of the added product.
+ """
return await ProductsService().add(uow, product, user)
@@ -27,6 +39,15 @@ async def get_products(
uow: UOWDependency,
user: Annotated[User, Depends(current_user)],
):
+ """Get all products.
+
+ Args:
+ uow (UOWDependency): Unit of Work dependency.
+ user (User): The authenticated user.
+
+ Returns:
+ List: List of products.
+ """
return await ProductsService().get_all(uow, user)
@@ -36,6 +57,16 @@ async def get_product(
uow: UOWDependency,
user: Annotated[User, Depends(current_user)],
):
+ """Get details of a specific product.
+
+ Args:
+ product_id (int): ID of the product to retrieve.
+ uow (UOWDependency): Unit of Work dependency.
+ user (User): The authenticated user.
+
+ Returns:
+ dict: Details of the requested product.
+ """
return await ProductsService().get(uow, product_id, user)
@@ -46,6 +77,17 @@ async def update_product(
uow: UOWDependency,
user: Annotated[User, Depends(current_user)],
):
+ """Update a product.
+
+ Args:
+ product_id (int): ID of the product to update.
+ product (ProductsUpdate): Updated product information.
+ uow (UOWDependency): Unit of Work dependency.
+ user (User): The authenticated user.
+
+ Returns:
+ dict: A message indicating the success of the update.
+ """
await ProductsService().update(uow, product_id, product, user)
return {"message": "Product updated successfully"}
@@ -56,5 +98,15 @@ async def delete_product(
uow: UOWDependency,
user: Annotated[User, Depends(current_user)],
):
+ """Delete a product.
+
+ Args:
+ product_id (int): ID of the product to delete.
+ uow (UOWDependency): Unit of Work dependency.
+ user (User): The authenticated user.
+
+ Returns:
+ dict: A message indicating the success of the deletion.
+ """
await ProductsService().delete(uow, product_id, user)
return {"message": "Product deleted successfully"}
diff --git a/app/api/v1/routers.py b/app/api/v1/routers.py
index e468328..d9d8efb 100644
--- a/app/api/v1/routers.py
+++ b/app/api/v1/routers.py
@@ -1,3 +1,5 @@
+"""Authentication routes for the FastAPI application."""
+
from app.api.v1.cart import router as cart_router
from app.api.v1.products import router as product_router
diff --git a/app/db/db.py b/app/db/db.py
index 39860f4..24d8410 100644
--- a/app/db/db.py
+++ b/app/db/db.py
@@ -1,7 +1,6 @@
-from sqlalchemy.ext.asyncio import (
- async_sessionmaker,
- create_async_engine,
-)
+"""Database module."""
+
+from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.orm import declarative_base
from app.settings import config
@@ -13,5 +12,10 @@
async def get_async_session():
+ """Get an asynchronous database session.
+
+ Yields:
+ Session: An asynchronous database session.
+ """
async with async_session_maker() as session:
yield session
diff --git a/app/main.py b/app/main.py
index 403dab6..65181aa 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,16 +1,16 @@
+"""Main module for the application."""
+
import uvicorn
from app.utils.factories import (
create_app,
custom_openapi,
setup_cors,
- setup_database,
setup_routes,
)
app = create_app()
-setup_database(app)
setup_cors(app)
setup_routes(app)
diff --git a/app/models/base_model.py b/app/models/base_model.py
index f402880..1f41361 100644
--- a/app/models/base_model.py
+++ b/app/models/base_model.py
@@ -1,3 +1,5 @@
+"""Base model class for all database models."""
+
from datetime import datetime
from sqlalchemy import TIMESTAMP, Integer, func
@@ -7,6 +9,14 @@
class BaseModel(Base):
+ """Base model class for all database models.
+
+ Attributes:
+ id (int): The primary key of the model.
+ created_at (datetime): The timestamp when the model was created.
+ updated_at (datetime): The timestamp when the model was last updated.
+ """
+
__abstract__ = True
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
diff --git a/app/models/cart.py b/app/models/cart.py
index ef1af25..f75a411 100644
--- a/app/models/cart.py
+++ b/app/models/cart.py
@@ -1,3 +1,5 @@
+"""Database model representing a shopping cart."""
+
from sqlalchemy import Boolean, Float, ForeignKey, Integer
from sqlalchemy.orm import Mapped, mapped_column
@@ -6,6 +8,22 @@
class Cart(BaseModel):
+ """Database model representing a shopping cart.
+
+ Attributes:
+ id (int): The primary key of the cart.
+ price (float): The total price of the products in the cart.
+ quantity (int): The quantity of the product in the cart.
+ product_id (int): The foreign key referencing the associated
+ product.
+ owner_id (int): The foreign key referencing the owner (user)
+ of the cart.
+ is_active (bool): Indicates whether the cart is active.
+ created_at (datetime): The timestamp when the cart was created.
+ updated_at (datetime): The timestamp when the cart was last
+ updated.
+ """
+
__tablename__ = "cart"
price: Mapped[float] = mapped_column(
@@ -23,6 +41,11 @@ class Cart(BaseModel):
)
def to_pydantic_model(self):
+ """Convert the database model to a Pydantic model (CartRead).
+
+ Returns:
+ CartRead: The Pydantic model representing the cart.
+ """
return CartRead(
id=self.id,
price=self.price,
diff --git a/app/models/products.py b/app/models/products.py
index 2d83613..be33197 100644
--- a/app/models/products.py
+++ b/app/models/products.py
@@ -1,13 +1,29 @@
+"""Database model representing a product."""
+
from sqlalchemy import Boolean, Float, ForeignKey, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
-from app.models.base_model import (
- BaseModel,
-)
+from app.models.base_model import BaseModel
from app.schemas.products import ProductRead
class Product(BaseModel):
+ """Database model representing a product.
+
+ Attributes:
+ id (int): The primary key of the product.
+ name (str): The name of the product.
+ description (str): The description of the product.
+ price (float): The price of the product.
+ owner_id (int): The foreign key referencing the owner (user)
+ of the product.
+ is_active (bool): Indicates whether the product is active.
+ created_at (datetime): The timestamp when the product was
+ created.
+ updated_at (datetime): The timestamp when the product was
+ last updated.
+ """
+
__tablename__ = "product"
name: Mapped[str] = mapped_column(String(150), nullable=False)
@@ -21,6 +37,11 @@ class Product(BaseModel):
)
def to_pydantic_model(self):
+ """Convert the database model to a Pydantic model (ProductRead).
+
+ Returns:
+ ProductRead: The Pydantic model representing the product.
+ """
return ProductRead(
id=self.id,
name=self.name,
diff --git a/app/models/users.py b/app/models/users.py
index 7535eec..7b7b11d 100644
--- a/app/models/users.py
+++ b/app/models/users.py
@@ -1,3 +1,5 @@
+"""Database model representing a user."""
+
from datetime import datetime
from fastapi_users_db_sqlalchemy import SQLAlchemyBaseUserTable
@@ -9,6 +11,28 @@
class User(SQLAlchemyBaseUserTable[int], BaseModel):
+ """Database model representing a user.
+
+ Attributes:
+ id (int): The primary key of the user.
+ email (str): The email address of the user.
+ hashed_password (str): The hashed password of the user.
+ telephone (str): The telephone number of the user.
+ first_name (str): The first name of the user.
+ last_name (str): The last name of the user.
+ last_login (datetime): The timestamp of the last login.
+ full_name (str): The full name of the user.
+ telegram_user_id (str): The Telegram user ID of the user.
+ role (str): The role of the user.
+ country (str): The country of the user.
+ city (str): The city of the user.
+ is_superuser (bool): Indicates whether the user is a superuser.
+ is_active (bool): Indicates whether the user is active.
+ is_verified (bool): Indicates whether the user is verified.
+ created_at (datetime): The timestamp when the user was created.
+ updated_at (datetime): The timestamp when the user was last updated.
+ """
+
telephone: Mapped[str] = mapped_column(
String(50), unique=True, nullable=False
)
@@ -19,6 +43,11 @@ class User(SQLAlchemyBaseUserTable[int], BaseModel):
)
def to_pydantic_model(self):
+ """Convert the database model to a Pydantic model (UserRead).
+
+ Returns:
+ UserRead: The Pydantic model representing the user.
+ """
return UserRead(
id=self.id,
email=self.email,
diff --git a/app/repositories/cart.py b/app/repositories/cart.py
index 00be27c..193afae 100644
--- a/app/repositories/cart.py
+++ b/app/repositories/cart.py
@@ -1,3 +1,5 @@
+"""Repository for interacting with the Cart model in the database."""
+
from sqlalchemy import and_, delete, func, select, update
from app.models import User
@@ -6,9 +8,24 @@
class CartRepository(BaseRepository):
+ """Repository for interacting with the Cart model in the database.
+
+ Attributes:
+ model: The Cart model.
+ session: The database session.
+ """
+
model = Cart
async def get_total_price(self, user: User):
+ """Get the total price of all products in the user's cart.
+
+ Args:
+ user (User): The user for whom to calculate the total price.
+
+ Returns:
+ float: The total price of all products in the user's cart.
+ """
statement = select(func.sum(Cart.price)).where(
Cart.owner_id == user.id
)
@@ -16,6 +33,16 @@ async def get_total_price(self, user: User):
return result.scalar_one_or_none()
async def get_by_product_id(self, product_id: int, user: User):
+ """Get a cart item by product ID for a specific user.
+
+ Args:
+ product_id (int): The ID of the product.
+ user (User): The user for whom to retrieve the cart item.
+
+ Returns:
+ CartRead: The Pydantic model representing the cart item,
+ or None if not found.
+ """
statement = select(self.model).where(
and_(
self.model.product_id == product_id,
@@ -29,6 +56,16 @@ async def get_by_product_id(self, product_id: int, user: User):
async def update_by_product_id(
self, product_id: int, data: dict, user: User
):
+ """Update a cart item by product ID for a specific user.
+
+ Args:
+ product_id (int): The ID of the product.
+ data (dict): The data to update in the cart item.
+ user (User): The user for whom to update the cart item.
+
+ Returns:
+ int: The product ID of the updated cart item.
+ """
statement = (
update(self.model)
.where(
@@ -47,6 +84,15 @@ async def update_by_product_id(
async def delete_by_owner_id_and_product_id(
self, product_id: int, user: User
):
+ """Delete a cart item by product ID for a specific user.
+
+ Args:
+ product_id (int): The ID of the product.
+ user (User): The user for whom to delete the cart item.
+
+ Returns:
+ None: The result of the deletion operation.
+ """
statement = (
delete(self.model)
.where(
@@ -60,6 +106,14 @@ async def delete_by_owner_id_and_product_id(
return await self.session.execute(statement)
async def delete_all_by_owner_id(self, user: User):
+ """Delete all cart items for a specific user.
+
+ Args:
+ user (User): The user for whom to delete all cart items.
+
+ Returns:
+ None: The result of the deletion operation.
+ """
statement = (
delete(self.model)
.where(
diff --git a/app/repositories/products.py b/app/repositories/products.py
index dfd6580..6c9684c 100644
--- a/app/repositories/products.py
+++ b/app/repositories/products.py
@@ -1,6 +1,15 @@
+"""Repository for interacting with the Product model in the database."""
+
from app.models import Product
from app.utils.repository import BaseRepository
class ProductsRepository(BaseRepository):
+ """Repository for interacting with the Product model in the database.
+
+ Attributes:
+ model: The Product model.
+ session: The database session.
+ """
+
model = Product
diff --git a/app/repositories/users.py b/app/repositories/users.py
index 3cdae60..1b7ece1 100644
--- a/app/repositories/users.py
+++ b/app/repositories/users.py
@@ -1,3 +1,5 @@
+"""Repository for interacting with the User model in the database."""
+
from sqlalchemy import select
from app.models import User
@@ -5,9 +7,25 @@
class UsersRepository(BaseRepository):
+ """Repository for interacting with the User model in the database.
+
+ Attributes:
+ model: The User model.
+ session: The database session.
+ """
+
model = User
async def get_by_telephone(self, telephone: str):
+ """Get a user by telephone number.
+
+ Args:
+ telephone (str): The telephone number to search for.
+
+ Returns:
+ User: The user with the specified telephone number,
+ or None if not found.
+ """
if telephone:
statement = select(self.model).where(
self.model.telephone == telephone
diff --git a/app/schemas/cart.py b/app/schemas/cart.py
index 66dc897..7c81928 100644
--- a/app/schemas/cart.py
+++ b/app/schemas/cart.py
@@ -1,9 +1,29 @@
+"""Pydantic models representing the shopping cart."""
+
from datetime import datetime
from pydantic import BaseModel, Field
class CartRead(BaseModel):
+ """Pydantic model representing a read-only view of a shopping cart item.
+
+ Attributes:
+ id (int): The unique identifier of the cart item.
+ quantity (int): The quantity of the product in the cart.
+ product_id (int): The ID of the associated product.
+ price (float): The price of the cart item.
+ owner_id (int): The ID of the owner (user) of the cart.
+ is_active (bool): Indicates whether the cart item is active.
+ created_at (datetime): The timestamp when the cart item was created.
+ updated_at (datetime): The timestamp when the cart item was
+ last updated.
+
+ Config:
+ from_attributes (bool): Enable attribute assignment from
+ class attributes.
+ """
+
id: int
quantity: int
product_id: int
@@ -18,9 +38,30 @@ class Config:
class CartCreate(BaseModel):
+ """Pydantic model representing the creation of a shopping cart item.
+
+ Attributes:
+ quantity (int): The quantity of the product in the cart
+ (default: 1).
+ product_id (int): The ID of the associated product.
+
+ Config:
+ ge (int): Quantity must be greater than or equal to 0.
+ """
+
quantity: int = Field(1, ge=0)
product_id: int
class CartUpdate(BaseModel):
+ """Pydantic model representing the update of a shopping cart item.
+
+ Attributes:
+ quantity (int): The new quantity of the product in the
+ cart (default: 1).
+
+ Config:
+ gt (int): Quantity must be greater than 0.
+ """
+
quantity: int = Field(1, gt=0)
diff --git a/app/schemas/products.py b/app/schemas/products.py
index eb3fd22..47fc5c0 100644
--- a/app/schemas/products.py
+++ b/app/schemas/products.py
@@ -1,9 +1,30 @@
+"""Pydantic models representing the products schema."""
+
from datetime import datetime
from pydantic import BaseModel
class ProductRead(BaseModel):
+ """Pydantic model representing a read-only view of a product.
+
+ Attributes:
+ id (int): The unique identifier of the product.
+ name (str): The name of the product.
+ description (str): The description of the product.
+ price (float): The price of the product.
+ owner_id (int): The ID of the owner (user) of the product.
+ is_active (bool): Indicates whether the product is active.
+ created_at (datetime): The timestamp when the product was
+ created.
+ updated_at (datetime): The timestamp when the product was
+ last updated.
+
+ Config:
+ from_attributes (bool): Enable attribute assignment from
+ class attributes.
+ """
+
id: int
name: str
description: str
@@ -18,12 +39,28 @@ class Config:
class ProductsCreate(BaseModel):
+ """Pydantic model representing the creation of a product.
+
+ Attributes:
+ name (str): The name of the product.
+ description (str): The description of the product.
+ price (float): The price of the product.
+ """
+
name: str
description: str
price: float
class ProductsUpdate(BaseModel):
+ """Pydantic model representing the update of a product.
+
+ Attributes:
+ name (str): The new name of the product.
+ description (str): The new description of the product.
+ price (float): The new price of the product.
+ """
+
name: str
description: str
price: float
diff --git a/app/schemas/users.py b/app/schemas/users.py
index 7df210e..c9e8356 100644
--- a/app/schemas/users.py
+++ b/app/schemas/users.py
@@ -1,3 +1,5 @@
+"""Pydantic models for the user schema."""
+
from datetime import datetime
from typing import Optional
@@ -6,6 +8,28 @@
class UserRead(schemas.BaseUser[int]):
+ """Pydantic model representing the read-only view of a user.
+
+ Attributes:
+ id (int): The unique identifier of the user.
+ email (str): The email address of the user.
+ hashed_password (str): The hashed password of the user.
+ first_name (str): The first name of the user.
+ last_name (str): The last name of the user.
+ telephone (str): The telephone number of the user.
+ last_login (Optional[datetime]): The timestamp of the last login.
+ is_superuser (bool): Indicates whether the user is a superuser.
+ is_active (bool): Indicates whether the user is active.
+ is_verified (bool): Indicates whether the user is verified.
+ created_at (datetime): The timestamp when the user was created.
+ updated_at (datetime): The timestamp when the user was last
+ updated.
+
+ Config:
+ from_attributes (bool): Enable attribute assignment from class
+ attributes.
+ """
+
id: int
first_name: str
last_name: str
@@ -19,6 +43,20 @@ class Config:
class UserCreate(schemas.BaseUserCreate):
+ """Pydantic model representing the creation of a user.
+
+ Attributes:
+ email (str): The email address of the user.
+ password (str): The password of the user.
+ confirm_password (str): The confirmation of the password.
+ first_name (Optional[str]): The first name of the user.
+ last_name (Optional[str]): The last name of the user.
+ telephone (Optional[str]): The telephone number of the user.
+
+ Config:
+ schema_extra (dict): Additional information for OpenAPI schema.
+ """
+
confirm_password: str
first_name: Optional[str] = Field(None)
last_name: Optional[str] = Field(None)
@@ -26,6 +64,19 @@ class UserCreate(schemas.BaseUserCreate):
class UserUpdate(schemas.BaseUserUpdate):
+ """Pydantic model representing the update of a user.
+
+ Attributes:
+ password (Optional[str]): The new password of the user.
+ confirm_password (str): The confirmation of the new password.
+ first_name (Optional[str]): The new first name of the user.
+ last_name (Optional[str]): The new last name of the user.
+ telephone (Optional[str]): The new telephone number of the user.
+
+ Config:
+ schema_extra (dict): Additional information for OpenAPI schema.
+ """
+
confirm_password: str
first_name: Optional[str] = Field(None)
last_name: Optional[str] = Field(None)
diff --git a/app/services/cart.py b/app/services/cart.py
index 7a4ba9d..5361d94 100644
--- a/app/services/cart.py
+++ b/app/services/cart.py
@@ -1,3 +1,5 @@
+"""Service class for managing user shopping carts."""
+
from typing import List, Union, overload
from fastapi import HTTPException, status
@@ -7,12 +9,26 @@
from app.models import User
from app.schemas.cart import CartCreate, CartUpdate
from app.services.products import ProductsService
-from app.services.validators import (
- ProductInCartValidator,
-)
+from app.services.validators import ProductInCartValidator
class CartService:
+ """Service class for managing user shopping carts.
+
+ Methods:
+ add: Add one or multiple products to the user's cart.
+ add_one: Add a single product to the user's cart.
+ add_many: Add multiple products to the user's cart.
+ get_all: Get all products from the user's cart.
+ get: Get a specific product from the user's cart.
+ update: Update the quantity of a product in the user's cart.
+ delete: Remove a product from the user's cart.
+ delete_all: Remove all products from the user's cart.
+ get_total_price: Get the total price of all products in the
+ user's cart.
+ is_in_cart: Check if a product is already in the user's cart.
+ """
+
@overload
def add(self, uow: UOWDependency, product: CartCreate, user: User) -> int:
pass
@@ -29,6 +45,18 @@ async def add(
products: Union[CartCreate, List[CartCreate]],
user: User,
) -> Union[int, List[int]]:
+ """Add one or multiple products to the user's cart.
+
+ Args:
+ uow (UOWDependency): The unit of work dependency.
+ products (Union[CartCreate, List[CartCreate]]): The product
+ or list of products to add.
+ user (User): The user for whom to add the products.
+
+ Returns:
+ Union[int, List[int]]: The product ID(s) that were added
+ to the cart.
+ """
if isinstance(products, list):
async with uow:
result = await self.add_many(uow, products, user)
@@ -40,6 +68,16 @@ async def add(
async def add_one(
self, uow: UOWDependency, product: CartCreate, user: User
):
+ """Add a single product to the user's cart.
+
+ Args:
+ uow (UOWDependency): The unit of work dependency.
+ product (CartCreate): The product to add.
+ user (User): The user for whom to add the product.
+
+ Returns:
+ int: The product ID that was added to the cart.
+ """
cart_dict = product.model_dump()
cart_dict["owner_id"] = user.id
is_product_in_cart, _product = await self.is_in_cart(
@@ -55,6 +93,16 @@ async def add_one(
async def add_many(
self, uow: UOWDependency, products: List[CartCreate], user: User
):
+ """Add multiple products to the user's cart.
+
+ Args:
+ uow (UOWDependency): The unit of work dependency.
+ products (List[CartCreate]): The list of products to add.
+ user (User): The user for whom to add the products.
+
+ Returns:
+ List[int]: The list of product IDs that were added to the cart.
+ """
result = []
for product in products:
product_id = await self.add_one(uow, product, user)
@@ -63,11 +111,30 @@ async def add_many(
@staticmethod
async def get_all(uow: UOWDependency, user: User):
+ """Get all products from the user's cart.
+
+ Args:
+ uow (UOWDependency): The unit of work dependency.
+ user (User): The user for whom to retrieve the cart items.
+
+ Returns:
+ List: The list of cart items.
+ """
async with uow:
return await uow.cart.get_all(user)
@staticmethod
async def get(uow: UOWDependency, product_id: int, user: User):
+ """Get a specific product from the user's cart.
+
+ Args:
+ uow (UOWDependency): The unit of work dependency.
+ product_id (int): The ID of the product to retrieve.
+ user (User): The user for whom to retrieve the cart item.
+
+ Returns:
+ dict: The cart item.
+ """
async with uow:
result = await uow.cart.get_by_product_id(product_id, user)
if result is not None:
@@ -85,6 +152,17 @@ async def update(
product: CartUpdate,
user: User,
):
+ """Update the quantity of a product in the user's cart.
+
+ Args:
+ uow (UOWDependency): The unit of work dependency.
+ product_id (int): The ID of the product to update.
+ product (CartUpdate): The updated product information.
+ user (User): The user for whom to update the cart item.
+
+ Returns:
+ int: The updated product ID.
+ """
product_dict = product.model_dump()
is_product_in_cart, _product = await self.is_in_cart(
uow, product_id, user
@@ -102,6 +180,16 @@ async def update(
@staticmethod
async def delete(uow: UOWDependency, product_id: int, user: User):
+ """Remove a product from the user's cart.
+
+ Args:
+ uow (UOWDependency): The unit of work dependency.
+ product_id (int): The ID of the product to remove.
+ user (User): The user for whom to remove the cart item.
+
+ Returns:
+ dict: The result of the deletion operation.
+ """
async with uow:
result = await uow.cart.delete_by_owner_id_and_product_id(
product_id, user
@@ -111,6 +199,15 @@ async def delete(uow: UOWDependency, product_id: int, user: User):
@staticmethod
async def delete_all(uow: UOWDependency, user: User):
+ """Remove all products from the user's cart.
+
+ Args:
+ uow (UOWDependency): The unit of work dependency.
+ user (User): The user for whom to remove all cart items.
+
+ Returns:
+ dict: The result of the deletion operation.
+ """
async with uow:
result = await uow.cart.delete_all_by_owner_id(user)
await uow.commit()
@@ -118,6 +215,15 @@ async def delete_all(uow: UOWDependency, user: User):
@staticmethod
async def get_total_price(uow: UOWDependency, user: User) -> float:
+ """Get the total price of all products in the user's cart.
+
+ Args:
+ uow (UOWDependency): The unit of work dependency.
+ user (User): The user for whom to calculate the total price.
+
+ Returns:
+ float: The total price of all products in the cart.
+ """
async with uow:
total_price = await uow.cart.get_total_price(user)
if total_price is not None:
@@ -130,6 +236,17 @@ async def get_total_price(uow: UOWDependency, user: User) -> float:
@staticmethod
async def is_in_cart(uow, product_id, user):
+ """Check if a product is already in the user's cart.
+
+ Args:
+ uow: The unit of work dependency.
+ product_id: The ID of the product to check.
+ user: The user for whom to check the cart.
+
+ Returns:
+ tuple: A tuple containing a boolean indicating if the
+ product is in the cart and the product information.
+ """
try:
_product = await ProductsService().get(uow, product_id, user)
except NoResultFound:
diff --git a/app/services/managers.py b/app/services/managers.py
index 2f0b914..bcff6e6 100644
--- a/app/services/managers.py
+++ b/app/services/managers.py
@@ -1,3 +1,5 @@
+"""User manager module."""
+
from typing import Any, Dict, Optional, Union
from fastapi import Depends, HTTPException, Request, status
@@ -18,12 +20,31 @@
class UserManager(IntegerIDMixin, BaseUserManager[User, int], UsersRepository):
+ """User manager class responsible for handling user-related operations.
+
+ Attributes:
+ reset_password_token_secret (str): The secret key for reset
+ password tokens.
+ verification_token_secret (str): The secret key for
+ verification tokens.
+ """
+
reset_password_token_secret = config.JWT_SECRET
verification_token_secret = config.JWT_SECRET
async def validate_password(
self, password: str, user: Union[schemas.UC, models.UP, Dict[str, Any]]
) -> None:
+ """Validate the password against the user's confirmation password.
+
+ Args:
+ password (str): The password to validate.
+ user (Union[schemas.UC, models.UP, Dict[str, Any]]): The
+ user information.
+
+ Raises:
+ HTTPException: If the password validation fails.
+ """
validator = PasswordValidator()
if isinstance(user, dict):
validator(password, user["confirm_password"])
@@ -34,6 +55,15 @@ async def validate_password(
async def validate_telephone(
user: Union[schemas.UC, models.UP, Dict[str, Any]]
) -> None:
+ """Validate the telephone number.
+
+ Args:
+ user (Union[schemas.UC, models.UP, Dict[str, Any]]): The
+ user information.
+
+ Raises:
+ HTTPException: If the telephone validation fails.
+ """
validator = TelephoneValidator()
if isinstance(user, dict):
validator(user["telephone"])
@@ -46,6 +76,20 @@ async def create(
safe: bool = False,
request: Optional[Request] = None,
) -> models.UP:
+ """Create a new user.
+
+ Args:
+ user_create (schemas.UC): The user creation schema.
+ safe (bool): Flag indicating if it's a safe creation
+ (default is False).
+ request (Optional[Request]): The request object.
+
+ Returns:
+ models.UP: The created user.
+
+ Raises:
+ HTTPException: If the user creation fails.
+ """
await self.validate_password(user_create.password, user_create)
await self.validate_telephone(user_create)
@@ -73,6 +117,19 @@ async def create(
async def _update(
self, user: models.UP, update_dict: Dict[str, Any]
) -> models.UP:
+ """Update user information.
+
+ Args:
+ user (models.UP): The user to update.
+ update_dict (Dict[str, Any]): The dictionary containing
+ the updated information.
+
+ Returns:
+ models.UP: The updated user.
+
+ Raises:
+ HTTPException: If the update fails.
+ """
validated_update_dict = {}
for field, value in update_dict.items():
if field == "email" and value != user.email:
@@ -90,11 +147,18 @@ async def _update(
elif field == "telephone" and value is not None:
await self.validate_telephone(update_dict)
validated_update_dict[field] = value
-
else:
validated_update_dict[field] = value
return await self.user_db.update(user, validated_update_dict)
async def get_user_manager(user_db=Depends(get_user_table)):
+ """Get an instance of the UserManager.
+
+ Args:
+ user_db: The user database.
+
+ Yields:
+ UserManager: The user manager instance.
+ """
yield UserManager(user_db)
diff --git a/app/services/products.py b/app/services/products.py
index af79d7c..8b29c14 100644
--- a/app/services/products.py
+++ b/app/services/products.py
@@ -1,13 +1,30 @@
+"""Service class for handling product-related operations."""
+
from app.api.v1.dependencies import UOWDependency
from app.models import User
from app.schemas.products import ProductsCreate, ProductsUpdate
class ProductsService:
+ """Service class for handling product-related operations."""
+
@staticmethod
async def add(
uow: UOWDependency, product: ProductsCreate, user: User
) -> int:
+ """Add a new product.
+
+ Args:
+ uow (UOWDependency): The unit of work dependency.
+ product (ProductsCreate): The product creation schema.
+ user (User): The user adding the product.
+
+ Returns:
+ int: The ID of the added product.
+
+ Raises:
+ HTTPException: If the product addition fails.
+ """
product_dict = product.model_dump()
product_dict["owner_id"] = user.id
async with uow:
@@ -17,11 +34,37 @@ async def add(
@staticmethod
async def get_all(uow: UOWDependency, user: User):
+ """Get all products for a given user.
+
+ Args:
+ uow (UOWDependency): The unit of work dependency.
+ user (User): The user for whom to retrieve products.
+
+ Returns:
+ List[Product]: The list of products.
+
+ Raises:
+ HTTPException: If the product retrieval fails.
+ """
async with uow:
return await uow.products.get_all(user)
@staticmethod
async def get(uow: UOWDependency, product_id: int, user: User):
+ """Get a specific product by ID for a given user.
+
+ Args:
+ uow (UOWDependency): The unit of work dependency.
+ product_id (int): The ID of the product to retrieve.
+ user (User): The user for whom to retrieve the product.
+
+ Returns:
+ Product: The retrieved product.
+
+ Raises:
+ HTTPException: If the product retrieval fails or the
+ product is not found.
+ """
async with uow:
return await uow.products.get(product_id, user)
@@ -32,6 +75,21 @@ async def update(
product: ProductsUpdate,
user: User,
):
+ """Update a specific product by ID for a given user.
+
+ Args:
+ uow (UOWDependency): The unit of work dependency.
+ product_id (int): The ID of the product to update.
+ product (ProductsUpdate): The updated product information.
+ user (User): The user performing the update.
+
+ Returns:
+ int: The ID of the updated product.
+
+ Raises:
+ HTTPException: If the product update fails or the
+ product is not found.
+ """
product_dict = product.model_dump()
async with uow:
await uow.products.update(product_id, product_dict, user)
@@ -40,6 +98,20 @@ async def update(
@staticmethod
async def delete(uow: UOWDependency, product_id: int, user: User):
+ """Delete a specific product by ID for a given user.
+
+ Args:
+ uow (UOWDependency): The unit of work dependency.
+ product_id (int): The ID of the product to delete.
+ user (User): The user performing the deletion.
+
+ Returns:
+ bool: True if the deletion is successful.
+
+ Raises:
+ HTTPException: If the product deletion fails or the
+ product is not found.
+ """
async with uow:
result = await uow.products.delete(product_id, user)
await uow.commit()
diff --git a/app/services/validators.py b/app/services/validators.py
index f2ff228..7dbd8e5 100644
--- a/app/services/validators.py
+++ b/app/services/validators.py
@@ -1,3 +1,5 @@
+"""Validators for checking the validity of data."""
+
import re
from fastapi import HTTPException
@@ -5,6 +7,16 @@
class PasswordValidator:
+ """Validator for checking the validity of passwords.
+
+ Args:
+ password (str): The password to validate.
+ confirm_password (str): The confirmation password.
+
+ Raises:
+ HTTPException: If the password is not valid or passwords do not match.
+ """
+
def __call__(self, password: str, confirm_password: str):
if not re.search(r"^(?=.*[A-Z])(?=.*[$%&!]).{8,}$", password):
raise HTTPException(
@@ -20,6 +32,15 @@ def __call__(self, password: str, confirm_password: str):
class TelephoneValidator:
+ """Validator for checking the validity of telephone numbers.
+
+ Args:
+ telephone (str): The telephone number to validate.
+
+ Raises:
+ HTTPException: If the telephone number is not valid.
+ """
+
def __call__(self, telephone: str):
if not re.search(r"^\+7\d{10}$", telephone):
raise HTTPException(
@@ -30,6 +51,15 @@ def __call__(self, telephone: str):
class ProductInCartValidator:
+ """Validator for checking if a product is already in the cart.
+
+ Args:
+ product: The product to check.
+
+ Raises:
+ HTTPException: If the product is already in the cart.
+ """
+
def __call__(self, product):
if product is not None:
raise HTTPException(
diff --git a/app/settings/auth.py b/app/settings/auth.py
index 453069f..32cccfb 100644
--- a/app/settings/auth.py
+++ b/app/settings/auth.py
@@ -1,3 +1,5 @@
+"""Authentication settings for the FastAPI Users package."""
+
from typing import Annotated
from fastapi import Depends
@@ -19,6 +21,11 @@
def get_jwt_strategy() -> JWTStrategy:
+ """Get the JWT (JSON Web Token) authentication strategy.
+
+ Returns:
+ JWTStrategy: The JWT authentication strategy.
+ """
return JWTStrategy(secret=config.JWT_SECRET, lifetime_seconds=3600)
@@ -32,4 +39,12 @@ def get_jwt_strategy() -> JWTStrategy:
async def get_user_table(
session: Annotated[AsyncSession, Depends(get_async_session)]
):
+ """Get the SQLAlchemyUserDatabase instance for the User model.
+
+ Args:
+ session (AsyncSession): The asynchronous database session.
+
+ Yields:
+ SQLAlchemyUserDatabase: The SQLAlchemyUserDatabase instance.
+ """
yield SQLAlchemyUserDatabase(session, User)
diff --git a/app/settings/config.py b/app/settings/config.py
index fd45993..6421e17 100644
--- a/app/settings/config.py
+++ b/app/settings/config.py
@@ -1,3 +1,5 @@
+"""Configuration classes for the application."""
+
import os
from pathlib import Path
from typing import ClassVar, Type
@@ -11,6 +13,20 @@
class Config:
+ """Base configuration class for the application.
+
+ Attributes:
+ DEBUG (bool): Indicates whether the application is in debug mode.
+ ACCESS_TOKEN_MINUTES (int): The expiration time for access
+ tokens in minutes.
+ REFRESH_TOKEN_DAYS (int): The expiration time for refresh
+ tokens in days.
+ FASTAPI_SETTINGS (ClassVar[dict]): FastAPI settings for the
+ application.
+ ... (Other attributes for email settings, database connection, etc.)
+
+ """
+
DEBUG = os.getenv("DEBUG", "False").lower() in ("true", "1", "True")
ACCESS_TOKEN_MINUTES = 60
@@ -98,23 +114,76 @@ class Config:
class DevelopmentConfig(Config):
+ """Development-specific configuration class.
+
+ Inherits from the base Config class.
+
+ Attributes:
+ DEBUG (bool): Indicates whether the application is in debug
+ mode (set to True in development).
+ """
+
DEBUG = True
class TestingConfig(Config):
+ """Testing-specific configuration class.
+
+ Inherits from the base Config class.
+
+ Attributes:
+ DEBUG (bool): Indicates whether the application is in debug
+ mode (set to True in testing).
+ TESTING (bool): Indicates that the application is in testing mode.
+ """
+
DEBUG = True
TESTING = True
class ProductionConfig(Config):
+ """Production-specific configuration class.
+
+ Inherits from the base Config class.
+
+ Attributes:
+ DEBUG (bool): Indicates whether the application is in debug
+ mode (set to False in production).
+ """
+
DEBUG = False
class ConfigFactory:
+ """Factory class for obtaining the appropriate configuration class
+ based on the environment.
+
+ Attributes:
+ fastapi_env (str): The environment variable indicating the
+ FastAPI environment.
+
+ Methods:
+ get_config(): Get the configuration class based on the
+ FastAPI environment.
+
+ Raises:
+ NotImplementedError: If the FastAPI environment is not recognized.
+
+ """
+
fastapi_env = os.getenv("FASTAPI_ENV")
@classmethod
def get_config(cls) -> Type[Config]:
+ """Get the configuration class based on the FastAPI environment.
+
+ Returns:
+ Type[Config]: The configuration class for the
+ current environment.
+
+ Raises:
+ NotImplementedError: If the FastAPI environment is not recognized.
+ """
if cls.fastapi_env == "development":
env_config = DevelopmentConfig
elif cls.fastapi_env == "production":
diff --git a/app/utils/factories.py b/app/utils/factories.py
index b020699..864d595 100644
--- a/app/utils/factories.py
+++ b/app/utils/factories.py
@@ -1,3 +1,5 @@
+"""Factories for creating FastAPI application instances."""
+
from typing import Dict
from fastapi import FastAPI
@@ -9,15 +11,21 @@
def create_app() -> FastAPI:
- return FastAPI(**config.FASTAPI_SETTINGS)
+ """Create a FastAPI application instance with configuration settings.
-
-def setup_database(application: FastAPI) -> None:
- # TODO @Tigran_Saatchyan: implement database setup
- ...
+ Returns:
+ FastAPI: The FastAPI application instance.
+ """
+ return FastAPI(**config.FASTAPI_SETTINGS)
def setup_cors(application: FastAPI) -> None:
+ """Set up Cross-Origin Resource Sharing (CORS) middleware for
+ the FastAPI application.
+
+ Args:
+ application (FastAPI): The FastAPI application instance.
+ """
application.add_middleware(
CORSMiddleware,
allow_origins=config.CORS_ALLOWED_ORIGINS,
@@ -28,6 +36,11 @@ def setup_cors(application: FastAPI) -> None:
def setup_routes(application: FastAPI) -> None:
+ """Set up API routes for the FastAPI application.
+
+ Args:
+ application (FastAPI): The FastAPI application instance.
+ """
from app.api.v1.routers import all_routers
set_up_auth_routes(application)
@@ -36,6 +49,14 @@ def setup_routes(application: FastAPI) -> None:
def custom_openapi(application: FastAPI) -> Dict[str, dict]:
+ """Customize the OpenAPI schema for the FastAPI application.
+
+ Args:
+ application (FastAPI): The FastAPI application instance.
+
+ Returns:
+ Dict[str, dict]: The customized OpenAPI schema.
+ """
if application.openapi_schema:
return application.openapi_schema
openapi_schema = get_openapi(
diff --git a/app/utils/repository.py b/app/utils/repository.py
index a08db2f..01a3cf6 100644
--- a/app/utils/repository.py
+++ b/app/utils/repository.py
@@ -1,3 +1,5 @@
+"""Repository module."""
+
from abc import ABC, abstractmethod
from sqlalchemy import and_, delete, insert, select, update
@@ -9,19 +11,65 @@
class AbstractRepository(ABC):
@abstractmethod
async def add(self, data: dict) -> int:
+ """Add a new record to the repository.
+
+ Args:
+ data (dict): The data to be added.
+
+ Returns:
+ int: The ID of the added record.
+ """
raise NotImplementedError
@abstractmethod
async def get_all(self, owner: User):
+ """Get all records from the repository for a specific owner.
+
+ Args:
+ owner (User): The owner of the records.
+
+ Returns:
+ list: A list of records.
+ """
raise NotImplementedError
async def get(self, id: int, owner: User):
+ """Get a specific record from the repository.
+
+ Args:
+ id (int): The ID of the record.
+ owner (User): The owner of the record.
+
+ Returns:
+ pydantic.Model: The retrieved record.
+ """
raise NotImplementedError
+ @abstractmethod
async def update(self, id: int, data: dict, owner: User):
+ """Update a specific record in the repository.
+
+ Args:
+ id (int): The ID of the record to be updated.
+ data (dict): The data to be updated.
+ owner (User): The owner of the record.
+
+ Returns:
+ int: The ID of the updated record.
+ """
raise NotImplementedError
+ @abstractmethod
async def delete(self, id: int, owner: User):
+ """Delete a specific record from the repository.
+
+ Args:
+ id (int): The ID of the record to be deleted.
+ owner (User): The owner of the record.
+
+ Returns:
+ None
+ """
raise NotImplementedError
@@ -31,21 +79,42 @@ class BaseRepository(AbstractRepository):
def __init__(self, session: AsyncSession):
self.session = session
- async def add(
- self,
- data: dict,
- ) -> int:
+ async def add(self, data: dict) -> int:
+ """Add a new record to the repository.
+
+ Args:
+ data (dict): The data to be added.
+
+ Returns:
+ int: The ID of the added record.
+ """
statement = insert(self.model).values(**data).returning(self.model.id)
result = await self.session.execute(statement)
-
return result.scalar_one()
async def get_all(self, owner: User):
+ """Get all records from the repository for a specific owner.
+
+ Args:
+ owner (User): The owner of the records.
+
+ Returns:
+ list: A list of records.
+ """
statement = select(self.model).where(self.model.owner_id == owner.id)
result = await self.session.execute(statement)
return [row[0].to_pydantic_model() for row in result.all()]
async def get(self, id: int, owner: User):
+ """Get a specific record from the repository.
+
+ Args:
+ id (int): The ID of the record.
+ owner (User): The owner of the record.
+
+ Returns:
+ pydantic.Model: The retrieved record.
+ """
statement = select(self.model).where(
and_(
self.model.id == id,
@@ -57,6 +126,16 @@ async def get(self, id: int, owner: User):
return result.to_pydantic_model()
async def update(self, id: int, data: dict, owner: User):
+ """Update a specific record in the repository.
+
+ Args:
+ id (int): The ID of the record to be updated.
+ data (dict): The data to be updated.
+ owner (User): The owner of the record.
+
+ Returns:
+ int: The ID of the updated record.
+ """
statement = (
update(self.model)
.where(
@@ -69,10 +148,18 @@ async def update(self, id: int, data: dict, owner: User):
.returning(self.model.product_id)
)
result = await self.session.execute(statement)
-
return result.scalar_one()
async def delete(self, id: int, owner: User):
+ """Delete a specific record from the repository.
+
+ Args:
+ id (int): The ID of the record to be deleted.
+ owner (User): The owner of the record.
+
+ Returns:
+ None
+ """
statement = (
delete(self.model)
.where(
@@ -83,4 +170,4 @@ async def delete(self, id: int, owner: User):
)
.returning()
)
- return await self.session.execute(statement)
+ await self.session.execute(statement)
diff --git a/app/utils/unitofwork.py b/app/utils/unitofwork.py
index ec13628..967a345 100644
--- a/app/utils/unitofwork.py
+++ b/app/utils/unitofwork.py
@@ -1,3 +1,5 @@
+"""Unit of Work module."""
+
from abc import ABC, abstractmethod
from typing import Type
@@ -13,20 +15,28 @@ class IUnitOfWork(ABC):
cart: Type[CartRepository]
@abstractmethod
- def __init__(self): ...
+ def __init__(self):
+ """Initialize the Unit of Work."""
+ raise NotImplementedError
@abstractmethod
- async def __aenter__(self): ...
+ async def __aenter__(self):
+ """Enter the asynchronous context."""
+ raise NotImplementedError
@abstractmethod
- async def __aexit__(self, *args): ...
+ async def __aexit__(self, *args):
+ """Exit the asynchronous context."""
+ raise NotImplementedError
@abstractmethod
async def commit(self):
+ """Commit changes made during the Unit of Work."""
raise NotImplementedError
@abstractmethod
async def rollback(self):
+ """Rollback changes made during the Unit of Work."""
raise NotImplementedError
@@ -35,17 +45,23 @@ def __init__(self):
self.session_factory = async_session_maker
async def __aenter__(self):
+ """Enter the asynchronous context, creating repositories."""
self.session = self.session_factory()
self.products = ProductsRepository(self.session)
self.users = UsersRepository(self.session)
self.cart = CartRepository(self.session)
async def __aexit__(self, *args):
+ """Exit the asynchronous context, rolling back changes and
+ closing the session.
+ """
await self.rollback()
await self.session.close()
async def commit(self):
+ """Commit changes made during the Unit of Work."""
await self.session.commit()
async def rollback(self):
+ """Rollback changes made during the Unit of Work."""
await self.session.rollback()
diff --git a/app/utils/utils.py b/app/utils/utils.py
index 4a79b24..0403c5a 100644
--- a/app/utils/utils.py
+++ b/app/utils/utils.py
@@ -1,3 +1,5 @@
+"""Utility functions for the application."""
+
import base64
import hashlib
import hmac
@@ -6,6 +8,16 @@
def hash_password(password, pwd_salt=config.PWD_HASH_SALT):
+ """Hash a password using PBKDF2-HMAC with a specified hash function,
+ salt, and number of iterations.
+
+ Args:
+ password (str): The password to be hashed.
+ pwd_salt (bytes): The salt to be used in the hashing process.
+
+ Returns:
+ dict: A dictionary containing the hashed password and salt.
+ """
hashed_password = hashlib.pbkdf2_hmac(
hash_name=config.CRYPTOGRAPHIC_HASH_FUNCTION,
password=password.encode("utf-8"),
@@ -19,5 +31,15 @@ def hash_password(password, pwd_salt=config.PWD_HASH_SALT):
async def compare_passwords(db_pwd, received_pwd, pwd_salt) -> bool:
+ """Compare a stored hashed password with a newly hashed password.
+
+ Args:
+ db_pwd (str): The stored hashed password.
+ received_pwd (str): The newly hashed password to be compared.
+ pwd_salt (bytes): The salt used in the original hashing process.
+
+ Returns:
+ bool: True if the passwords match, False otherwise.
+ """
received_pwd = hash_password(received_pwd, pwd_salt)["hashed_password"]
return hmac.compare_digest(db_pwd, received_pwd)
diff --git a/pyproject.toml b/pyproject.toml
index 7db88c4..594096e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -72,7 +72,11 @@ ignore = [
"D417", # Missing argument descriptions in the docstring
"D415", # First line should end with a period
"D205", # 1 blank line required between summary line and description
- "D1", # Missing docstring
+ "D104", # Missing docstring
+ "D101", # Missing docstring
+ "D102", # Missing docstring
+ "D106", # Missing docstring
+ "D107", # Missing docstring
"TD003", # Missing issue link on the line following this TODOs
"FIX002", # todos found
"FIX003", # XXX found
diff --git a/tests/conftest.py b/tests/conftest.py
index 320a7a8..5278252 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,3 +1,5 @@
+"""Pytest configuration file for the application."""
+
import asyncio
from typing import AsyncGenerator
@@ -15,15 +17,12 @@
from app.main import app
from app.settings import config
-# DATABASE
-
engine_test = create_async_engine(config.TEST_DATABASE_URI, poolclass=NullPool)
async_session_maker = async_sessionmaker(
engine_test, class_=AsyncSession, expire_on_commit=False
)
Base.metadata.bind = engine_test
-
USER_EMAIL = "user3@example.com"
USER_PHONE = "+79999999999"
USER_PASSWORD = "Q1!string"
@@ -42,6 +41,11 @@
async def override_get_async_session() -> AsyncGenerator[AsyncSession, None]:
+ """Override the get_async_session dependency to use a testing session.
+
+ Yields:
+ AsyncSession: An asynchronous session for testing purposes.
+ """
async with async_session_maker() as session:
yield session
@@ -50,10 +54,16 @@ async def override_get_async_session() -> AsyncGenerator[AsyncSession, None]:
async def login_user(ac: AsyncClient):
- await ac.post(
- "/api/v1/register",
- json=USER_DATA,
- )
+ """Simulate user registration and login, returning the
+ authentication cookies.
+
+ Args:
+ ac (AsyncClient): The asynchronous HTTP client.
+
+ Returns:
+ http.cookies: Authentication cookies.
+ """
+ await ac.post("/api/v1/register", json=USER_DATA)
response = await ac.post(
"/api/v1/jwt/login",
@@ -64,6 +74,7 @@ async def login_user(ac: AsyncClient):
@pytest.fixture(autouse=True, scope="class")
async def prepare_database():
+ """Fixture to set up and tear down the test database."""
async with engine_test.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
@@ -85,5 +96,10 @@ def event_loop(request):
@pytest.fixture(scope="session")
async def ac() -> AsyncGenerator[AsyncClient, None]:
+ """AsyncClient fixture for testing asynchronous endpoints.
+
+ Yields:
+ AsyncClient: An asynchronous HTTP client for testing.
+ """
async with AsyncClient(app=app, base_url="http://test") as ac:
yield ac
diff --git a/tests/test_auth.py b/tests/test_auth.py
index 99819cd..7b545fc 100644
--- a/tests/test_auth.py
+++ b/tests/test_auth.py
@@ -1,3 +1,5 @@
+"""Test authentication endpoints."""
+
from httpx import AsyncClient
from starlette import status
@@ -7,6 +9,14 @@
class TestAuth:
@staticmethod
async def test_register(ac: AsyncClient):
+ """Test user registration endpoint.
+
+ Args:
+ ac (AsyncClient): The asynchronous HTTP client.
+
+ Returns:
+ None
+ """
response = await ac.post(
"/api/v1/register",
json=USER_DATA,
@@ -17,6 +27,14 @@ async def test_register(ac: AsyncClient):
class TestLogin:
async def test_login_and_logout(self, ac: AsyncClient):
+ """Test user login and logout endpoints.
+
+ Args:
+ ac (AsyncClient): The asynchronous HTTP client.
+
+ Returns:
+ None
+ """
await ac.post(
"/api/v1/register",
json=USER_DATA,
diff --git a/tests/test_cart.py b/tests/test_cart.py
index 56885c3..5c26cf2 100644
--- a/tests/test_cart.py
+++ b/tests/test_cart.py
@@ -1,3 +1,5 @@
+"""Tests for the cart endpoints."""
+
from httpx import AsyncClient
from starlette import status
@@ -7,6 +9,14 @@
class TestCart:
@staticmethod
async def test_add_to_cart(ac: AsyncClient):
+ """Test adding a product to the user's cart.
+
+ Args:
+ ac (AsyncClient): The asynchronous HTTP client.
+
+ Returns:
+ None
+ """
auth_cookies = await login_user(ac)
test_product = {
"name": "Test product",
@@ -36,6 +46,14 @@ async def test_add_to_cart(ac: AsyncClient):
@staticmethod
async def test_get_all_from_cart(ac: AsyncClient):
+ """Test retrieving all products from the user's cart.
+
+ Args:
+ ac (AsyncClient): The asynchronous HTTP client.
+
+ Returns:
+ None
+ """
auth_cookies = await login_user(ac)
response = await ac.get(
diff --git a/tests/test_product.py b/tests/test_product.py
index 423cf2a..649028e 100644
--- a/tests/test_product.py
+++ b/tests/test_product.py
@@ -1,3 +1,5 @@
+"""Tests for the product endpoints."""
+
from httpx import AsyncClient
from starlette import status
@@ -7,6 +9,14 @@
class TestProducts:
@staticmethod
async def test_add_product(ac: AsyncClient):
+ """Test adding a new product.
+
+ Args:
+ ac (AsyncClient): The asynchronous HTTP client.
+
+ Returns:
+ None
+ """
auth_cookies = await login_user(ac)
test_product = {
"name": "Test product",
diff --git a/tests/test_users.py b/tests/test_users.py
index eae5b2f..9680cba 100644
--- a/tests/test_users.py
+++ b/tests/test_users.py
@@ -1,3 +1,5 @@
+"""Tests for the users endpoints."""
+
from httpx import AsyncClient
from starlette import status
@@ -7,6 +9,14 @@
class TestUsers:
@staticmethod
async def test_get_current_user(ac: AsyncClient):
+ """Test retrieving information about the current user.
+
+ Args:
+ ac (AsyncClient): The asynchronous HTTP client.
+
+ Returns:
+ None
+ """
auth_cookies = await login_user(ac)
response = await ac.get(
"/users/me",