diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bf21e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,267 @@ +# Project level files +0.0.2 +alembic.ini +changes_note.txt +migrations/ +.dbdata/ +app.dp + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Backup files # +*.bak + +# C extensions +*.so + +# MacOS stuff +**/.DS_Store + +# Docker ignore +.dockerignore + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +flask_monitoringdashboard.db + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +*.pot +*.pyc +__pycache__ +db.sqlite3 +media + +#FastAPI stuff: +.pytest_cache +htmlcov +dist +site +.coverage +coverage.xml +.netlify +test.db +log.txt +docs_build +docs.zip +archive.zip + + +# Flask stuff: +# instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# Python # +*.py[cod] +*$py.class + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv/ +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# If you are using PyCharm # +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# File-based project format +*.iws + +# JIRA plugin +atlassian-ide-plugin.xml + +# Distribution / packaging +.Python build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# .key +SECRET.key \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2dbbbe6 --- /dev/null +++ b/README.md @@ -0,0 +1,180 @@ + +
+
+ Logo + +

Python Fast API boilerplate

+ +

+ Fast API boiler plate project +

+
+ + + + +
+ Table of Contents +
    +
  1. + About The Project + +
  2. +
  3. + Getting Started + +
  4. +
  5. License
  6. +
+
+ + +## About The Project + +FastAPI boilerplate provides a simple basic structure for project creation with mysql database. + + +### Built With + +* [![Python][Python]][Python-url] +* [![FastAPI][FastAPI]][FastAPI-url] + + +## Getting Started + +Instructions for setting up project locally. +To get a local copy up and running follow these simple steps. + +## Install + configure the project + +### 1. Linux +### Prerequisites + +Requirement of Project +* Install Python + ```sh + Python-Version : 3.11.0 + ``` +* Create python virtual environment + ```sh + python3 -m venv venv + ``` +* Activate the python virtual environment + ```sh + source venv/bin/activate + ``` + +### Installation + +1. Clone the repo + ```sh + git clone https://github.com/viitoradmin/python-fastapi-boilerplate/tree/feature/fastapi + ``` +2. Upgrade pip version + ```sh + python -m pip install --upgrade pip==22.1.2 + ``` +3. Install the requirements for the project into the virtual environment + ```sh + pip install -r requirements.txt + ``` +4. Install the dependencies of Fast API + ```sh + pip install "fastapi[all]" + ``` + +### 2. Windows + +1. Create python virtual environment + ``` + conda create --name venv python=3.11 + ``` + +2. Activate the python virtual environment + ``` + conda activate venv + ``` + +3. Install the requirements for the project into the virtual environment in the following sequence: + ``` + pip install -r requirements.txt + ``` + +4. Install the dependencies of Fast API + ``` + pip install "fastapi[all]" + ``` + +5. Upgrade pip version + ``` + python -m pip install --upgrade pip==22.1.2 + ``` + +### Use the alembic to Upgrade/Downgrade the database in the FastAPI + Note: Because by default Fastapi is provide only initial migrations. + It doesn't support the upgrade and downgrade the database. + so,to perform automatic migrations follow the following steps: + + +1. To create Migration folder + ``` + python -m alembic init migrations + ``` +2. Update the sqlalchemy.url into alembic.ini file + ``` + sqlalchemy.url = mysql+pymysql://user:pass@host/db_name + ``` + +3. update the Migrations>>env.py file o auto migrate the database. + ``` + from models import Base + target_database = Base.metadata + ``` + +4. Perform the initial migrations + ``` + alembic revision --autogenerate -m 'initials' + ``` + +5. Apply the changes into the database (upgrade the database) + ``` + alembic upgrade head + ``` + +6. To downgrade the database if required + ``` + alembic downgrade -1 + ``` + +## Run the server in development mode + +Add environment variables (given in .env) by running following command in cmd/terminal: + +Run the server + ``` + python asgi.py + ``` +Browse Swagger API Doc at: http://localhost:8000/docs + +Browse Redoc at: http://localhost:8000/redoc + +Browse Swagger API Doc for version v1 at: http://localhost:8000/v1/docs + +Browse Swagger API Doc for version v2 at: http://localhost:8000/v2/docs + +## Release History + +* 0.1 + * Work in progress + + + +[Python]: https://img.shields.io/badge/Python-000000?style=for-the-badge&logo=python&logoColor=Blue +[Python-url]: https://docs.python.org/3.10/ +[FastAPI]: https://img.shields.io/badge/FastAPI-20232A?style=for-the-badge&logo=fastapi&logoColor=009485 +[FastAPI-url]: https://fastapi.tiangolo.com/ \ No newline at end of file diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 0000000..b003693 --- /dev/null +++ b/apps/__init__.py @@ -0,0 +1,36 @@ +"""This module is include API's route.""" +from fastapi import FastAPI + +from apps.api.auth.models import Base as authbase +from apps.api.auth.view import router +from apps.constant import constant +from config import cors, database +from apps.middleware.custom_middleware import CustomMiddleware +from apps.middleware.request_size_middleware import LimitRequestSizeMiddleware +from apps.middleware.logging_middleware import LogRequestsMiddleware +from apps.middleware.authentication_middleware import AuthMiddleware +from apps.middleware.session_middleware import SessionMiddleware +from apps.middleware.rate_limiting_middleware import RateLimitMiddleware +from apps.middleware.error_handling_middleware import ErrorHandlingMiddleware + +# Bind with the database, whenever new models find it's create it. +authbase.metadata.create_all(bind=database.engine) + +# Create app object and add routes +app = FastAPI(title="Python FastAPI boilerplate", middleware=cors.middleware) + +# add middlewares +app.add_middleware(CustomMiddleware) +app.add_middleware(LimitRequestSizeMiddleware, max_body_size=10 * 1024 * 1024) +app.add_middleware(LogRequestsMiddleware) +app.add_middleware(AuthMiddleware, secure_token="secure_token") +app.add_middleware(SessionMiddleware, secure_token="new_session_token") +app.add_middleware(RateLimitMiddleware, rate_limit=5, time_window=60) +app.add_middleware(ErrorHandlingMiddleware) + +# define router for different version +# router for API's +app.include_router( + router, + prefix=constant.API_V1 + ) diff --git a/apps/api/auth/method.py b/apps/api/auth/method.py new file mode 100644 index 0000000..96c0fb8 --- /dev/null +++ b/apps/api/auth/method.py @@ -0,0 +1,23 @@ +"""This module contains database operations methods.""" +from sqlalchemy.orm import Session + +from apps.constant import constant + + +class UserAuthMethod(): + """This class defines methods to authenticate users.""" + + def __init__(self, model) -> constant.STATUS_NULL: + self.model = model + + def find_by_email(self, db: Session, email: str): + """This funtion will return the email object""" + return db.query(self.model).filter( + self.model.email == email + ).first() + + def find_by_username(self, db: Session, username: str): + """This function will return the username object""" + return db.query(self.model).filter( + self.model.username == username + ).first() diff --git a/apps/api/auth/models.py b/apps/api/auth/models.py new file mode 100644 index 0000000..8e2af2e --- /dev/null +++ b/apps/api/auth/models.py @@ -0,0 +1,27 @@ +"""This module contains database model implementations.""" +from datetime import datetime + +from sqlalchemy import Column, DateTime, Integer, String + +from config.database import Base + + +class Users(Base): + """ + Table used for stored the users information + """ + __tablename__ = "users" + + id = Column(Integer, primary_key=True, nullable=False) + uuid = Column(String(100), nullable=False, unique=True, doc='unique_id') + first_name = Column(String(255), doc='First name of user', nullable=True) + last_name = Column(String(255), doc='Last name of user', nullable=True) + email = Column(String(100), doc='Email ID of the user', nullable=True) + username = Column(String(100), doc='Username of the user', nullable=True) + password = Column(String(100), doc='Password of the user', nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False, + doc='its generate automatically when data create') + updated_at = Column(DateTime, nullable=True, + onupdate=datetime.utcnow, doc='its generate automatically when data update') + deleted_at = Column(DateTime, nullable=True, + doc='its generate automatically when data deleted') diff --git a/apps/api/auth/response.py b/apps/api/auth/response.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/auth/schema.py b/apps/api/auth/schema.py new file mode 100644 index 0000000..8b2e550 --- /dev/null +++ b/apps/api/auth/schema.py @@ -0,0 +1,24 @@ +"""This module is for swager and request parameter schema""" +from pydantic import BaseModel + + +class UserAuth(BaseModel): + """This class is for user schema.""" + first_name: str + last_name: str + email: str + password: str + username: str + + class Config: + """This class is the schema for user configuration.""" + from_attributes = True + json_schema_extra = { + "example": { + "first_name": "John", + "last_name": "Smith", + "email": "jhohnsmith@example.com", + "password": "Abc@123", + "username": "Jhon123" + } + } diff --git a/apps/api/auth/service.py b/apps/api/auth/service.py new file mode 100644 index 0000000..c9b2443 --- /dev/null +++ b/apps/api/auth/service.py @@ -0,0 +1,105 @@ +"""This module contains API's specific functionality.""" +import uuid + +from fastapi import status +from fastapi.encoders import jsonable_encoder +from sqlalchemy.orm import Session + +from apps.api.auth.method import UserAuthMethod +from apps.api.auth.models import Users +from apps.api.core import db_methods +from apps.api.core.validation import ValidationMethods +from apps.constant import constant +from apps.utils.message import ErrorMessage, InfoMessage +from apps.utils.standard_response import StandardResponse + + +class UserAuthService: + """This class represents the user creation service""" + def create_user_service(self, db: Session, body: dict): + """This function is used to create user + Args: + db (Session): database connection + body (dict): dictionary to user information data + + Returns: + response (dict): user object representing the user + """ + username = body['username'] + email = body['email'] or None + password = body['password'] + + # check Email exists in body or not + if "email" not in body or not email: + return StandardResponse( + False, status.HTTP_400_BAD_REQUEST, None, ErrorMessage.emailRequired + ) + + # check Email exists in Db or not + if (user_object := UserAuthMethod(Users).find_by_email(db, email)): + return StandardResponse( + False, status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, ErrorMessage.emailAlreadyExist + ).make + + # For password validation + if not ValidationMethods().password_validator(password): + return StandardResponse( + False, status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, ErrorMessage.invalidPasswordFormat + ).make + + user_object = Users( + uuid = uuid.uuid4(), + first_name=body['first_name'], + last_name=body['last_name'], + username=username, + email=email, + password=password + ) + + # Store user object in database + if not(db_methods.BaseMethods(Users).save(user_object, db)): + return StandardResponse( + False, status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, ErrorMessage.userNotSaved + ) + + # check email exists or not + if user_object := UserAuthMethod(Users).find_by_email(db, email): + data = { + "username": user_object.username, + "first_name": user_object.first_name, + "last_name": user_object.last_name, + "email": user_object.email, + "password": user_object.password, + } + + else: + return StandardResponse( + False, status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, ErrorMessage.userInvalid + ).make + + return StandardResponse( + True, status.HTTP_200_OK, data, InfoMessage.userCreated + ).make + + def get_user_service(self, db): + """This function returns the user service list.""" + if not (user_object := db_methods.BaseMethods(Users).find_by_uuid(db)): + return StandardResponse( + False, + status.HTTP_400_BAD_REQUEST, + None, + ErrorMessage.userNotFound + ) + # convert the object data into json + user_data = jsonable_encoder(user_object) + + return StandardResponse( + True, + status.HTTP_200_OK, + user_data, + InfoMessage.retriveInfoSuccessfully + ) diff --git a/apps/api/auth/test.py b/apps/api/auth/test.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/auth/view.py b/apps/api/auth/view.py new file mode 100644 index 0000000..f89df38 --- /dev/null +++ b/apps/api/auth/view.py @@ -0,0 +1,44 @@ +"""This module is responsible to contain API's endpoint""" +from fastapi import APIRouter, Depends, status +from sqlalchemy.orm import Session + +from apps.api.auth import schema +from apps.api.auth.service import UserAuthService +from apps.constant import constant +from apps.utils.standard_response import StandardResponse +from config import database + +## Load API's +router = APIRouter() +getdb = database.get_db + +## Define verison 1 API's here +class UserCrudApi(): + """This class is for user's CRUD operation with version 1 API's""" + @router.get('/users') + async def list_user(db: Session = Depends(getdb)): + """This API is for list user. + Args: None + Returns: + response: will return users list.""" + try: + response = UserAuthService().get_user_service(db) + return response + except Exception: + return StandardResponse(False, status.HTTP_400_BAD_REQUEST, None, constant.ERROR_MSG) + + @router.post('/create/user') + async def create_user(body: schema.UserAuth, + db: Session = Depends(getdb)): + """This API is for create user. + Args: + body(dict) : user's data + Returns: + response: will return the user's data""" + try: + # as per pydantic version 2. + body = body.model_dump() + response = UserAuthService().create_user_service(db, body) + return response + except Exception: + return StandardResponse(False, status.HTTP_400_BAD_REQUEST, None, constant.ERROR_MSG) \ No newline at end of file diff --git a/apps/api/core/db_methods.py b/apps/api/core/db_methods.py new file mode 100644 index 0000000..c11388d --- /dev/null +++ b/apps/api/core/db_methods.py @@ -0,0 +1,39 @@ +"""This module contains databse methods.""" +from fastapi import Depends +from sqlalchemy.orm import Session + +from apps.constant import constant +from config import database + +getdb = database.get_db + +class BaseMethods(): + """This class provides basic DB methods""" + + def __init__(self, model): + self.model = model + + def save(self, validate_data, db: Session = Depends(getdb)): + """this function saves the object to the database for the given model + Args: + validate_data (dict): validate data + db (Session): database session. + Returns: + returns the created object + """ + try: + db.add(validate_data) + db.commit() + db.refresh(validate_data) + return constant.STATUS_TRUE + except Exception as err: + print(err) + db.rollback() + db.close() + return constant.STATUS_FALSE + + def find_by_uuid(self, db: Session = Depends(getdb)): + """This function is used to find users.""" + return db.query(self.model).filter(self.model.deleted_at == constant.STATUS_NULL).all() + + \ No newline at end of file diff --git a/apps/api/core/validation.py b/apps/api/core/validation.py new file mode 100644 index 0000000..e005097 --- /dev/null +++ b/apps/api/core/validation.py @@ -0,0 +1,35 @@ +"""This module contains validations functionality.""" +import re + +class ValidationMethods: + """This class contains not null validation""" + def not_null_validator(self, v, field): + """This function is used to check whether a field is not null""" + if v == '': + raise ValueError(f'{field} must be required') + return v + + def password_validator(self, v): + """This function is used to validate the password.""" + reg = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!#%*?&]{6,20}$" + pattern = re.compile(reg) + + if not(re.search(pattern, v)): + # Length should be at least 6 + # Length should be not be greater than 20 + # Password should have at least one numeral + # Password should have at least one uppercase letter + # Password should have at least one lowercase letter + # Password should have at least one of the symbols $@# + return False + return True + + def email_validator(self, v): + """This function checks if the email address is valid or not.""" + # regular expression for email validation + reg = "([-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\"([]!#-[^-~ \t]|(\\[\t -~]))+\")@([-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\[[\t -Z^-~]*])" + + pattern = re.compile(reg) + if not(re.search(pattern, v)): + raise ValueError("Invalid Email format") + return v diff --git a/apps/constant/constant.py b/apps/constant/constant.py new file mode 100644 index 0000000..87fe12d --- /dev/null +++ b/apps/constant/constant.py @@ -0,0 +1,21 @@ +"""This module contains constant messages.""" + +STATUS_SUCCESS = "success" +STATUS_FAIL = "fail" +STATUS_ERROR = "error" +STATUS_NULL = None +STATUS_TRUE = True +STATUS_FALSE = False +API_V1 = "/v1" +API_V2 = "/v2" + +## success message ## +USER_CREATED = "User created successfully!" +USER_UPDATED = "User updated sucessfully!" +USER_DELETED = "User deleted successfully!" +USER_LIST = "List of all users!" + + +## error message ## +ERROR_MSG = "Error while creating user!" +LIST_ERROR_MSG = "There is no list to retrieve!!" diff --git a/apps/middleware/__init__.py b/apps/middleware/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/apps/middleware/__init__.py @@ -0,0 +1 @@ + diff --git a/apps/middleware/authentication_middleware.py b/apps/middleware/authentication_middleware.py new file mode 100644 index 0000000..7067ec5 --- /dev/null +++ b/apps/middleware/authentication_middleware.py @@ -0,0 +1,21 @@ +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response, JSONResponse + + + +class AuthMiddleware(BaseHTTPMiddleware): + def __init__(self, app, secure_token: str): + super().__init__(app) + self.secure_token = secure_token + + async def dispatch(self, request: Request, call_next): + token = request.headers.get("Authorization") + + # Validate the Authorization token + if not token or token != f"Bearer {self.secure_token}": + return JSONResponse(content={"error": "Unauthorized"}, status_code=401) + + # Proceed to the next middleware or endpoint + response = await call_next(request) + return response diff --git a/apps/middleware/cors_middleware.py b/apps/middleware/cors_middleware.py new file mode 100644 index 0000000..c194b0d --- /dev/null +++ b/apps/middleware/cors_middleware.py @@ -0,0 +1,10 @@ +from starlette.middleware.cors import CORSMiddleware + + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Replace "*" with specific origins for better security + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) diff --git a/apps/middleware/custom_middleware.py b/apps/middleware/custom_middleware.py new file mode 100644 index 0000000..e092b0b --- /dev/null +++ b/apps/middleware/custom_middleware.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, Request +from starlette.middleware.base import BaseHTTPMiddleware + +app = FastAPI() + +class CustomMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + # Logic before processing the request + print("Before Request") + + response = await call_next(request) + + # Logic after processing the request + print("After Request") + return response + + diff --git a/apps/middleware/error_handling_middleware.py b/apps/middleware/error_handling_middleware.py new file mode 100644 index 0000000..a3c7050 --- /dev/null +++ b/apps/middleware/error_handling_middleware.py @@ -0,0 +1,13 @@ +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import JSONResponse + +class ErrorHandlingMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + try: + # Proceed to the next middleware or endpoint + response = await call_next(request) + return response + except Exception as exc: + # Catch any exception and return a JSON response with status 500 + return JSONResponse(content={"error": str(exc)}, status_code=500) diff --git a/apps/middleware/logging_middleware.py b/apps/middleware/logging_middleware.py new file mode 100644 index 0000000..487f8c0 --- /dev/null +++ b/apps/middleware/logging_middleware.py @@ -0,0 +1,15 @@ +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response + +class LogRequestsMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + # Log incoming request details + print(f"Incoming request: {request.method} {request.url}") + + # Call the next middleware or endpoint + response = await call_next(request) + + # Log response details + print(f"Response status: {response.status_code}") + return response diff --git a/apps/middleware/rate_limiting_middleware.py b/apps/middleware/rate_limiting_middleware.py new file mode 100644 index 0000000..4885130 --- /dev/null +++ b/apps/middleware/rate_limiting_middleware.py @@ -0,0 +1,38 @@ +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response, JSONResponse +from time import time + + +class RateLimitMiddleware(BaseHTTPMiddleware): + def __init__(self, app, rate_limit: int, time_window: int = 60): + """ + :param app: The FastAPI app + :param rate_limit: Maximum number of requests allowed + :param time_window: Time window in seconds for rate limiting (default is 60 seconds) + """ + super().__init__(app) + self.rate_limit = rate_limit + self.time_window = time_window + self.request_count = 0 + self.window_start_time = time() + + async def dispatch(self, request: Request, call_next): + current_time = time() + + # Check if the time window has expired + if current_time - self.window_start_time > self.time_window: + # Reset the window + self.window_start_time = current_time + self.request_count = 0 + + # Enforce rate limiting + if self.request_count >= self.rate_limit: + return JSONResponse(content={"error": "Rate limit exceeded"}, status_code=429) + + # Increment request count + self.request_count += 1 + + # Process the request + response = await call_next(request) + return response diff --git a/apps/middleware/request_size_middleware.py b/apps/middleware/request_size_middleware.py new file mode 100644 index 0000000..9ead88e --- /dev/null +++ b/apps/middleware/request_size_middleware.py @@ -0,0 +1,17 @@ +from fastapi import status, HTTPException +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request + + +class LimitRequestSizeMiddleware(BaseHTTPMiddleware): + def __init__(self, app, max_body_size: int): + super().__init__(app) + self.max_body_size = max_body_size + + async def dispatch(self, request: Request, call_next): + if int(request.headers.get("content-length", 0)) > self.max_body_size: + raise HTTPException( + detail="Request entity too large", + status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + ) + return await call_next(request) diff --git a/apps/middleware/response_time_middleware.py b/apps/middleware/response_time_middleware.py new file mode 100644 index 0000000..58ebe5f --- /dev/null +++ b/apps/middleware/response_time_middleware.py @@ -0,0 +1,13 @@ +import time +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response + + +class TimingMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + start_time = time.time() + response: Response = await call_next(request) + process_time = time.time() - start_time + print(f"Request processing time: {process_time:.2f} seconds") + return response diff --git a/apps/middleware/session_middleware.py b/apps/middleware/session_middleware.py new file mode 100644 index 0000000..c5f990c --- /dev/null +++ b/apps/middleware/session_middleware.py @@ -0,0 +1,22 @@ +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response, JSONResponse + +class SessionMiddleware(BaseHTTPMiddleware): + def __init__(self, app, new_session_token: str): + super().__init__(app) + self.new_session_token = new_session_token + + async def dispatch(self, request: Request, call_next): + # Check for the session token in cookies + session_token = request.cookies.get("session_token") + if not session_token: + # Return a response indicating the session has expired + response = JSONResponse(content={"error": "Session expired"}, status_code=403) + # Set a new session token in the cookies + response.set_cookie(key="session_token", value=self.new_session_token) + return response + + # Proceed to the next middleware or endpoint + response = await call_next(request) + return response diff --git a/apps/utils/helper.py b/apps/utils/helper.py new file mode 100644 index 0000000..f9507ae --- /dev/null +++ b/apps/utils/helper.py @@ -0,0 +1,19 @@ +"""This moduel is used to convert password into hash string.""" +from passlib.context import CryptContext + +class PasswordUtils(): + """This class is used to manage password management""" + + def __init__(self): + self.pwd_context = CryptContext(schemes=['bcrypt'], deprecated="auto") + + def hash_password(self, password: str): + """ + This function is used to hash password + Arguments: + password(str) : password argument of string format. + + Returns: + Hash of the password + """ + return self.pwd_context.hash(password) diff --git a/apps/utils/message.py b/apps/utils/message.py new file mode 100644 index 0000000..180d3b6 --- /dev/null +++ b/apps/utils/message.py @@ -0,0 +1,18 @@ +"""This module contains predefined messages.""" + +class InfoMessage: + """This class contains information messages.""" + userCreated = "User created successfully" + retriveInfoSuccessfully = "Retrive user info successfully" + +class ErrorMessage: + """This class contains error messages.""" + emailRequired = "Email is required" + usernameRequired = "Username is required" + usernameAlreadyExists = "Username already exists" + userNotSaved = "User is not saved" + emailRequired = "E-Mail is required" + emailAlreadyExist = "E-Mail already exists." + invalidPasswordFormat = "Password format is invalid" + userNotFound = "User is not found" + userInvalid = "user is invalid" diff --git a/apps/utils/standard_response.py b/apps/utils/standard_response.py new file mode 100644 index 0000000..af044f2 --- /dev/null +++ b/apps/utils/standard_response.py @@ -0,0 +1,37 @@ +"""This module contains standard response class.""" +from fastapi.responses import JSONResponse + +from apps.constant import constant + + +class StandardResponse: + """This class is universal to return standard API responses + + Attributes: + status (int): The http status response from API + data (dict/list): The Data from API + message (str): The message from the API + """ + + def __init__(self, status, status_code: int, data: dict, message: str) -> None: + """This function defines arguments that are used in the class + + Arguments: + status (str): The success/failure status. + status_code (int): The http status response from API + data (dict/list): The Data from API + message (str): The message from the API + + Returns: + Returns the API standard response + """ + self.status = status + self.status_code = status_code + self.data = data + self.message = message + + @property + def make(self) -> dict: + self.status = constant.STATUS_SUCCESS if self.status_code in [201, 200] else constant.STATUS_FAIL + response = {'status': self.status, 'data': self.data, 'message': self.message} + return JSONResponse(content=response, status_code=self.status_code) \ No newline at end of file diff --git a/asgi.py b/asgi.py index e69de29..0b0d6b4 100644 --- a/asgi.py +++ b/asgi.py @@ -0,0 +1,15 @@ +"""This module contains entry point for an application.""" +import os + +import uvicorn + +from apps.__init__ import app +from config.env_config import load_dotenv + +load_dotenv() + +if __name__ == "__main__": + uvicorn.run("asgi:app", + host=os.environ.get('SERVER_HOST'), + port=int(os.environ.get('SERVER_PORT')), + reload=True) diff --git a/config/cors.py b/config/cors.py new file mode 100644 index 0000000..866126e --- /dev/null +++ b/config/cors.py @@ -0,0 +1,21 @@ +"""This module contains CORS headers.""" +from starlette.middleware import Middleware +from fastapi.middleware.cors import CORSMiddleware + + +##### CORS configuration ##### +CORS_ALLOW_ORIGINS = ["*"] + +CORS_ALLOW_METHODS = ["*"] + +CORS_ALLOW_HEADERS = ["*"] + +middleware = [ + Middleware( + CORSMiddleware, + allow_origins=CORS_ALLOW_ORIGINS, + allow_credentials=True, + allow_methods=CORS_ALLOW_METHODS, + allow_headers=CORS_ALLOW_HEADERS + ) +] diff --git a/config/database.py b/config/database.py new file mode 100644 index 0000000..2f2b6bc --- /dev/null +++ b/config/database.py @@ -0,0 +1,23 @@ +"""This config file will define Database Url and configurations""" +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +from config import env_config + +# ##### DATABASE CONFIGURATION ############################ +SQLALCHEMY_DATABASE_URL = f'mysql+pymysql://{env_config.DATABASE_USER}:{env_config.DATABASE_PASSWORD}@{env_config.DATABASE_HOST}:{env_config.DATABASE_PORT}/{env_config.DATABASE_NAME}' + +# Create engine by sqlalchemy db url +engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_recycle=3600, pool_pre_ping=True) +SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False) + +Base = declarative_base() + +def get_db(): + """This function returns the database object.""" + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/config/env_config.py b/config/env_config.py new file mode 100644 index 0000000..579adf0 --- /dev/null +++ b/config/env_config.py @@ -0,0 +1,19 @@ +"""This module contains configuration information.""" +import os +from os.path import join + +from dotenv import load_dotenv + +from .project_path import BASE_DIR + +##### ENV configuration ##### +dotenv_path = join(BASE_DIR, ".env") +load_dotenv(dotenv_path) + + +## Database deatils ## +DATABASE_NAME = os.environ.get("DATABASE_NAME") +DATABASE_USER = os.environ.get("DATABASE_USER") +DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD") +DATABASE_HOST = os.environ.get("DATABASE_HOST") +DATABASE_PORT = os.environ.get("DATABASE_PORT") diff --git a/config/project_path.py b/config/project_path.py new file mode 100644 index 0000000..6820465 --- /dev/null +++ b/config/project_path.py @@ -0,0 +1,19 @@ +"""This module contains project path information.""" +from os.path import abspath, basename, dirname, join + + +# ##### PATH CONFIGURATION ################################ + +# fetch FastAPI's project directory +BASE_DIR = dirname(dirname(abspath(__file__))) + +# the name of the whole site +SITE_NAME = basename(BASE_DIR) + +# look for templates here +# This is an internal setting, used in the TEMPLATES directive +PROJECT_TEMPLATES_ROOT = join(BASE_DIR, 'assets', 'templates') + +PROJECT_STATIC_ROOT = join(BASE_DIR, 'assets', 'static') + +MEDIA_ROOT = join(BASE_DIR, 'media') diff --git a/images/python_logo.png b/images/python_logo.png new file mode 100644 index 0000000..5a7c311 Binary files /dev/null and b/images/python_logo.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..24df1dc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.111.0 +PyMySQL==1.1.1 +SQLAlchemy==2.0.30 +uvicorn==0.30.0