Spaces:
Running
Running
Ved Gupta commited on
Commit ·
15ea62b
0
Parent(s):
initial commit
Browse files- .gitignore +12 -0
- Dockerfile +11 -0
- Pipfile +20 -0
- README.md +70 -0
- app/__init__.py +13 -0
- app/api/__init__.py +10 -0
- app/api/endpoints/__init__.py +10 -0
- app/api/endpoints/items.py +13 -0
- app/api/endpoints/users.py +48 -0
- app/api/models/__init__.py +2 -0
- app/api/models/item.py +19 -0
- app/api/models/user.py +14 -0
- app/core/__init__.py +5 -0
- app/core/config.py +60 -0
- app/core/database.py +13 -0
- app/core/security.py +7 -0
- app/main.py +25 -0
- app/tests/__init__.py +34 -0
- app/tests/conftest.py +36 -0
- app/tests/test_api/__init__.py +11 -0
- app/tests/test_api/test_items.py +34 -0
- app/tests/test_api/test_users.py +63 -0
- app/tests/test_core/__init__.py +16 -0
- app/tests/test_core/test_config.py +8 -0
- app/tests/test_core/test_database.py +24 -0
- app/tests/test_core/test_security.py +22 -0
- requirements.txt +9 -0
.gitignore
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
*.pyo
|
| 4 |
+
*.egg-info/
|
| 5 |
+
dist/
|
| 6 |
+
build/
|
| 7 |
+
.env
|
| 8 |
+
.idea/
|
| 9 |
+
.vscode/
|
| 10 |
+
*.log
|
| 11 |
+
*.swp
|
| 12 |
+
.DS_Store
|
Dockerfile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8
|
| 2 |
+
|
| 3 |
+
COPY ./app /app/app
|
| 4 |
+
|
| 5 |
+
COPY ./requirements.txt /app
|
| 6 |
+
|
| 7 |
+
RUN pip install --no-cache-dir -r /app/requirements.txt
|
| 8 |
+
|
| 9 |
+
EXPOSE 80
|
| 10 |
+
|
| 11 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
|
Pipfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[[source]]
|
| 2 |
+
url = "https://pypi.org/simple"
|
| 3 |
+
verify_ssl = true
|
| 4 |
+
name = "pypi"
|
| 5 |
+
|
| 6 |
+
[packages]
|
| 7 |
+
fastapi = "*"
|
| 8 |
+
uvicorn = {extras = ["standard"], version = "*"}
|
| 9 |
+
python-dotenv = "*"
|
| 10 |
+
sqlalchemy = "*"
|
| 11 |
+
psycopg2-binary = "*"
|
| 12 |
+
pytest = "*"
|
| 13 |
+
pytest-cov = "*"
|
| 14 |
+
faker = "*"
|
| 15 |
+
requests-mock = "*"
|
| 16 |
+
|
| 17 |
+
[dev-packages]
|
| 18 |
+
|
| 19 |
+
[requires]
|
| 20 |
+
python_version = "3.9"
|
README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# my-fastapi-project
|
| 2 |
+
|
| 3 |
+
This is a production level project structure for a Python FastAPI project.
|
| 4 |
+
|
| 5 |
+
## Project Structure
|
| 6 |
+
|
| 7 |
+
```
|
| 8 |
+
my-fastapi-project
|
| 9 |
+
├── app
|
| 10 |
+
│ ├── __init__.py
|
| 11 |
+
│ ├── api
|
| 12 |
+
│ │ ├── __init__.py
|
| 13 |
+
│ │ ├── endpoints
|
| 14 |
+
│ │ │ ├── __init__.py
|
| 15 |
+
│ │ │ ├── items.py
|
| 16 |
+
│ │ │ └── users.py
|
| 17 |
+
│ │ └── models
|
| 18 |
+
│ │ ├── __init__.py
|
| 19 |
+
│ │ ├── item.py
|
| 20 |
+
│ │ └── user.py
|
| 21 |
+
│ ├── core
|
| 22 |
+
│ │ ├── __init__.py
|
| 23 |
+
│ │ ├── config.py
|
| 24 |
+
│ │ ├── security.py
|
| 25 |
+
│ │ └── database.py
|
| 26 |
+
│ ├── tests
|
| 27 |
+
│ │ ├── __init__.py
|
| 28 |
+
│ │ ├── conftest.py
|
| 29 |
+
│ │ ├── test_api
|
| 30 |
+
│ │ │ ├── __init__.py
|
| 31 |
+
│ │ │ ├── test_items.py
|
| 32 |
+
│ │ │ └── test_users.py
|
| 33 |
+
│ │ └── test_core
|
| 34 |
+
│ │ ├── __init__.py
|
| 35 |
+
│ │ ├── test_config.py
|
| 36 |
+
│ │ ├── test_security.py
|
| 37 |
+
│ │ └── test_database.py
|
| 38 |
+
│ └── main.py
|
| 39 |
+
├── .env
|
| 40 |
+
├── .gitignore
|
| 41 |
+
├── Dockerfile
|
| 42 |
+
├── requirements.txt
|
| 43 |
+
├── README.md
|
| 44 |
+
└── .vscode
|
| 45 |
+
├── settings.json
|
| 46 |
+
└── launch.json
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## Description
|
| 50 |
+
|
| 51 |
+
The project structure is organized as follows:
|
| 52 |
+
|
| 53 |
+
- `app`: contains the main application code.
|
| 54 |
+
- `app/api`: contains the API endpoints.
|
| 55 |
+
- `app/api/endpoints`: contains the endpoint functions.
|
| 56 |
+
- `app/api/models`: contains the data models.
|
| 57 |
+
- `app/core`: contains the core application code.
|
| 58 |
+
- `app/core/config.py`: contains the application configuration.
|
| 59 |
+
- `app/core/security.py`: contains the security functions.
|
| 60 |
+
- `app/core/database.py`: contains the database connection code.
|
| 61 |
+
- `app/tests`: contains the test code.
|
| 62 |
+
- `app/tests/test_api`: contains the API endpoint tests.
|
| 63 |
+
- `app/tests/test_core`: contains the core application tests.
|
| 64 |
+
- `app/main.py`: contains the main application entry point.
|
| 65 |
+
- `.env`: contains environment variables.
|
| 66 |
+
- `.gitignore`: specifies files and directories to ignore in Git.
|
| 67 |
+
- `Dockerfile`: specifies the Docker image configuration.
|
| 68 |
+
- `requirements.txt`: specifies the Python dependencies.
|
| 69 |
+
- `README.md`: this file.
|
| 70 |
+
- `.vscode`: contains Visual Studio Code configuration files.
|
app/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
|
| 3 |
+
from app.api.api import api_router
|
| 4 |
+
from app.core.config import settings
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def create_app() -> FastAPI:
|
| 8 |
+
app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION)
|
| 9 |
+
app.include_router(api_router, prefix=settings.API_V1_STR)
|
| 10 |
+
return app
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
app = create_app()
|
app/api/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# File: my-fastapi-project/app/api/__init__.py
|
| 2 |
+
|
| 3 |
+
from fastapi import FastAPI
|
| 4 |
+
from .endpoints import items, users
|
| 5 |
+
from .models import item, user
|
| 6 |
+
|
| 7 |
+
app = FastAPI()
|
| 8 |
+
|
| 9 |
+
app.include_router(items.router)
|
| 10 |
+
app.include_router(users.router)
|
app/api/endpoints/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# File: my-fastapi-project/app/api/endpoints/__init__.py
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter
|
| 4 |
+
|
| 5 |
+
from . import items, users
|
| 6 |
+
|
| 7 |
+
router = APIRouter()
|
| 8 |
+
|
| 9 |
+
router.include_router(items.router, prefix="/items", tags=["items"])
|
| 10 |
+
router.include_router(users.router, prefix="/users", tags=["users"])
|
app/api/endpoints/items.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
|
| 3 |
+
router = APIRouter()
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@router.get("/")
|
| 7 |
+
async def read_items():
|
| 8 |
+
return [{"name": "Item Foo"}, {"name": "item Bar"}]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@router.get("/{item_id}")
|
| 12 |
+
async def read_item(item_id: int, q: str = None):
|
| 13 |
+
return {"item_id": item_id, "q": q}
|
app/api/endpoints/users.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
|
| 3 |
+
from app.api.models.user import UserInDB
|
| 4 |
+
from app.api.db import database
|
| 5 |
+
|
| 6 |
+
users_router = r = APIRouter()
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@r.post("/", response_model=UserInDB, status_code=201)
|
| 10 |
+
async def create_user(user: UserInDB):
|
| 11 |
+
query = UserInDB.insert().values(
|
| 12 |
+
username=user.username,
|
| 13 |
+
email=user.email,
|
| 14 |
+
hashed_password=user.hashed_password
|
| 15 |
+
)
|
| 16 |
+
user_id = await database.execute(query)
|
| 17 |
+
return {**user.dict(), "id": user_id}
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@r.get("/{id}/", response_model=UserInDB)
|
| 21 |
+
async def read_user(id: int):
|
| 22 |
+
query = UserInDB.select().where(UserInDB.c.id == id)
|
| 23 |
+
user = await database.fetch_one(query)
|
| 24 |
+
return user
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@r.put("/{id}/", response_model=UserInDB)
|
| 28 |
+
async def update_user(id: int, user: UserInDB):
|
| 29 |
+
query = (
|
| 30 |
+
UserInDB
|
| 31 |
+
.update()
|
| 32 |
+
.where(UserInDB.c.id == id)
|
| 33 |
+
.values(
|
| 34 |
+
username=user.username,
|
| 35 |
+
email=user.email,
|
| 36 |
+
hashed_password=user.hashed_password
|
| 37 |
+
)
|
| 38 |
+
.returning(UserInDB.c.id)
|
| 39 |
+
)
|
| 40 |
+
user_id = await database.execute(query)
|
| 41 |
+
return {**user.dict(), "id": user_id}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@r.delete("/{id}/", response_model=int)
|
| 45 |
+
async def delete_user(id: int):
|
| 46 |
+
query = UserInDB.delete().where(UserInDB.c.id == id)
|
| 47 |
+
user_id = await database.execute(query)
|
| 48 |
+
return user_id
|
app/api/models/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .item import Item
|
| 2 |
+
from .user import User
|
app/api/models/item.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class ItemBase(BaseModel):
|
| 6 |
+
title: str
|
| 7 |
+
description: Optional[str] = None
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class ItemCreate(ItemBase):
|
| 11 |
+
pass
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class Item(ItemBase):
|
| 15 |
+
id: int
|
| 16 |
+
owner_id: int
|
| 17 |
+
|
| 18 |
+
class Config:
|
| 19 |
+
orm_mode = True
|
app/api/models/user.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
|
| 3 |
+
class UserBase(BaseModel):
|
| 4 |
+
email: str
|
| 5 |
+
|
| 6 |
+
class UserCreate(UserBase):
|
| 7 |
+
password: str
|
| 8 |
+
|
| 9 |
+
class User(UserBase):
|
| 10 |
+
id: int
|
| 11 |
+
is_active: bool
|
| 12 |
+
|
| 13 |
+
class Config:
|
| 14 |
+
orm_mode = True
|
app/core/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .config import settings
|
| 2 |
+
from .database import Base, engine, SessionLocal
|
| 3 |
+
from .security import get_password_hash, verify_password
|
| 4 |
+
|
| 5 |
+
__all__ = ["settings", "Base", "engine", "SessionLocal", "get_password_hash", "verify_password"]
|
app/core/config.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict, List, Optional, Union
|
| 2 |
+
from pydantic import AnyHttpUrl, BaseSettings, validator
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class Settings(BaseSettings):
|
| 6 |
+
API_V1_STR: str = "/api/v1"
|
| 7 |
+
PROJECT_NAME: str = "Whisper API"
|
| 8 |
+
SECRET_KEY: str
|
| 9 |
+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
|
| 10 |
+
SERVER_NAME: str
|
| 11 |
+
SERVER_HOST: AnyHttpUrl
|
| 12 |
+
POSTGRES_SERVER: str
|
| 13 |
+
POSTGRES_USER: str
|
| 14 |
+
POSTGRES_PASSWORD: str
|
| 15 |
+
POSTGRES_DB: str
|
| 16 |
+
|
| 17 |
+
@validator("SECRET_KEY", pre=True)
|
| 18 |
+
def secret_key_must_be_set(cls, v: Optional[str], values: Dict[str, Any]) -> str:
|
| 19 |
+
if not v:
|
| 20 |
+
raise ValueError("SECRET_KEY must be set")
|
| 21 |
+
return v
|
| 22 |
+
|
| 23 |
+
@validator("SERVER_NAME", pre=True)
|
| 24 |
+
def server_name_must_be_set(cls, v: Optional[str], values: Dict[str, Any]) -> str:
|
| 25 |
+
if not v:
|
| 26 |
+
raise ValueError("SERVER_NAME must be set")
|
| 27 |
+
return v
|
| 28 |
+
|
| 29 |
+
@validator("SERVER_HOST", pre=True)
|
| 30 |
+
def server_host_must_be_set(cls, v: Optional[str], values: Dict[str, Any]) -> AnyHttpUrl:
|
| 31 |
+
if not v:
|
| 32 |
+
raise ValueError("SERVER_HOST must be set")
|
| 33 |
+
return v
|
| 34 |
+
|
| 35 |
+
@validator("POSTGRES_SERVER", pre=True)
|
| 36 |
+
def postgres_server_must_be_set(cls, v: Optional[str], values: Dict[str, Any]) -> str:
|
| 37 |
+
if not v:
|
| 38 |
+
raise ValueError("POSTGRES_SERVER must be set")
|
| 39 |
+
return v
|
| 40 |
+
|
| 41 |
+
@validator("POSTGRES_USER", pre=True)
|
| 42 |
+
def postgres_user_must_be_set(cls, v: Optional[str], values: Dict[str, Any]) -> str:
|
| 43 |
+
if not v:
|
| 44 |
+
raise ValueError("POSTGRES_USER must be set")
|
| 45 |
+
return v
|
| 46 |
+
|
| 47 |
+
@validator("POSTGRES_PASSWORD", pre=True)
|
| 48 |
+
def postgres_password_must_be_set(cls, v: Optional[str], values: Dict[str, Any]) -> str:
|
| 49 |
+
if not v:
|
| 50 |
+
raise ValueError("POSTGRES_PASSWORD must be set")
|
| 51 |
+
return v
|
| 52 |
+
|
| 53 |
+
@validator("POSTGRES_DB", pre=True)
|
| 54 |
+
def postgres_db_must_be_set(cls, v: Optional[str], values: Dict[str, Any]) -> str:
|
| 55 |
+
if not v:
|
| 56 |
+
raise ValueError("POSTGRES_DB must be set")
|
| 57 |
+
return v
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
settings = Settings()
|
app/core/database.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import create_engine
|
| 2 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 3 |
+
from sqlalchemy.orm import sessionmaker
|
| 4 |
+
|
| 5 |
+
from app.core.config import settings
|
| 6 |
+
|
| 7 |
+
SQLALCHEMY_DATABASE_URL = settings.database_url
|
| 8 |
+
|
| 9 |
+
engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
| 10 |
+
|
| 11 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 12 |
+
|
| 13 |
+
Base = declarative_base()
|
app/core/security.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from passlib.context import CryptContext
|
| 2 |
+
|
| 3 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 4 |
+
|
| 5 |
+
ALGORITHM = "HS256"
|
| 6 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
| 7 |
+
SECRET_KEY = "mysecretkey"
|
app/main.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from app.api.api_v1.api import router as api_router
|
| 3 |
+
from app.core.config import settings
|
| 4 |
+
from app.core.errors import http_error_handler
|
| 5 |
+
from app.core.errors import http422_error_handler
|
| 6 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 7 |
+
|
| 8 |
+
app = FastAPI(title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json")
|
| 9 |
+
|
| 10 |
+
# Set all CORS enabled origins
|
| 11 |
+
if settings.BACKEND_CORS_ORIGINS:
|
| 12 |
+
app.add_middleware(
|
| 13 |
+
CORSMiddleware,
|
| 14 |
+
allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
|
| 15 |
+
allow_credentials=True,
|
| 16 |
+
allow_methods=["*"],
|
| 17 |
+
allow_headers=["*"],
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# Include routers
|
| 21 |
+
app.include_router(api_router, prefix=settings.API_V1_STR)
|
| 22 |
+
|
| 23 |
+
# Error handlers
|
| 24 |
+
app.add_exception_handler(422, http422_error_handler)
|
| 25 |
+
app.add_exception_handler(500, http_error_handler)
|
app/tests/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# File: my-fastapi-project/app/tests/__init__.py
|
| 2 |
+
|
| 3 |
+
# Import necessary modules
|
| 4 |
+
import pytest
|
| 5 |
+
from fastapi.testclient import TestClient
|
| 6 |
+
from sqlalchemy import create_engine
|
| 7 |
+
from sqlalchemy.orm import sessionmaker
|
| 8 |
+
|
| 9 |
+
from app.core.config import settings
|
| 10 |
+
from app.main import app
|
| 11 |
+
from app.tests.utils.utils import override_get_db
|
| 12 |
+
|
| 13 |
+
# Create test database
|
| 14 |
+
SQLALCHEMY_DATABASE_URL = settings.TEST_DATABASE_URL
|
| 15 |
+
engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
| 16 |
+
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 17 |
+
|
| 18 |
+
# Define test client
|
| 19 |
+
@pytest.fixture(scope="module")
|
| 20 |
+
def test_client():
|
| 21 |
+
with TestClient(app) as client:
|
| 22 |
+
yield client
|
| 23 |
+
|
| 24 |
+
# Define test database
|
| 25 |
+
@pytest.fixture(scope="module")
|
| 26 |
+
def test_db():
|
| 27 |
+
db = TestingSessionLocal()
|
| 28 |
+
yield db
|
| 29 |
+
db.close()
|
| 30 |
+
|
| 31 |
+
# Override get_db function for testing
|
| 32 |
+
@pytest.fixture(autouse=True)
|
| 33 |
+
def override_get_db(monkeypatch):
|
| 34 |
+
monkeypatch.setattr("app.api.dependencies.get_db", override_get_db)
|
app/tests/conftest.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from fastapi.testclient import TestClient
|
| 3 |
+
from sqlalchemy import create_engine
|
| 4 |
+
from sqlalchemy.orm import sessionmaker
|
| 5 |
+
|
| 6 |
+
from app.core.config import settings
|
| 7 |
+
from app.db.base import Base
|
| 8 |
+
from app.db.session import SessionLocal
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@pytest.fixture(scope="module")
|
| 12 |
+
def test_app():
|
| 13 |
+
# set up test app with client
|
| 14 |
+
from app.main import app
|
| 15 |
+
client = TestClient(app)
|
| 16 |
+
# set up test database
|
| 17 |
+
SQLALCHEMY_DATABASE_URL = settings.SQLALCHEMY_DATABASE_TEST_URL
|
| 18 |
+
engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
| 19 |
+
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 20 |
+
Base.metadata.create_all(bind=engine)
|
| 21 |
+
# yield the app and database session to the test
|
| 22 |
+
try:
|
| 23 |
+
yield client, TestingSessionLocal()
|
| 24 |
+
finally:
|
| 25 |
+
# clean up test database
|
| 26 |
+
Base.metadata.drop_all(bind=engine)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@pytest.fixture(scope="function")
|
| 30 |
+
def db_session():
|
| 31 |
+
# set up a new database session for each test
|
| 32 |
+
session = SessionLocal()
|
| 33 |
+
try:
|
| 34 |
+
yield session
|
| 35 |
+
finally:
|
| 36 |
+
session.close()
|
app/tests/test_api/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# File: my-fastapi-project/app/tests/test_api/__init__.py
|
| 2 |
+
|
| 3 |
+
from fastapi.testclient import TestClient
|
| 4 |
+
from my_fastapi_project.app.api import app
|
| 5 |
+
|
| 6 |
+
client = TestClient(app)
|
| 7 |
+
|
| 8 |
+
def test_read_main():
|
| 9 |
+
response = client.get("/")
|
| 10 |
+
assert response.status_code == 200
|
| 11 |
+
assert response.json() == {"msg": "Hello World"}
|
app/tests/test_api/test_items.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi.testclient import TestClient
|
| 2 |
+
from app.main import app
|
| 3 |
+
|
| 4 |
+
client = TestClient(app)
|
| 5 |
+
|
| 6 |
+
def test_create_item():
|
| 7 |
+
data = {"name": "test", "description": "test description"}
|
| 8 |
+
response = client.post("/items/", json=data)
|
| 9 |
+
assert response.status_code == 200
|
| 10 |
+
assert response.json()["name"] == "test"
|
| 11 |
+
assert response.json()["description"] == "test description"
|
| 12 |
+
|
| 13 |
+
def test_read_item():
|
| 14 |
+
response = client.get("/items/1")
|
| 15 |
+
assert response.status_code == 200
|
| 16 |
+
assert response.json()["name"] == "test"
|
| 17 |
+
assert response.json()["description"] == "test description"
|
| 18 |
+
|
| 19 |
+
def test_read_all_items():
|
| 20 |
+
response = client.get("/items/")
|
| 21 |
+
assert response.status_code == 200
|
| 22 |
+
assert len(response.json()) == 1
|
| 23 |
+
|
| 24 |
+
def test_update_item():
|
| 25 |
+
data = {"name": "updated test", "description": "updated test description"}
|
| 26 |
+
response = client.put("/items/1", json=data)
|
| 27 |
+
assert response.status_code == 200
|
| 28 |
+
assert response.json()["name"] == "updated test"
|
| 29 |
+
assert response.json()["description"] == "updated test description"
|
| 30 |
+
|
| 31 |
+
def test_delete_item():
|
| 32 |
+
response = client.delete("/items/1")
|
| 33 |
+
assert response.status_code == 200
|
| 34 |
+
assert response.json() == {"detail": "Item deleted successfully"}
|
app/tests/test_api/test_users.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi.testclient import TestClient
|
| 2 |
+
from app.core.config import settings
|
| 3 |
+
|
| 4 |
+
client = TestClient(settings.app)
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def test_create_user():
|
| 8 |
+
data = {"email": "test@example.com", "password": "password"}
|
| 9 |
+
response = client.post("/users/", json=data)
|
| 10 |
+
assert response.status_code == 200
|
| 11 |
+
assert response.json()["email"] == "test@example.com"
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def test_create_user_invalid_email():
|
| 15 |
+
data = {"email": "invalid_email", "password": "password"}
|
| 16 |
+
response = client.post("/users/", json=data)
|
| 17 |
+
assert response.status_code == 422
|
| 18 |
+
assert "value_error.email" in response.json()["detail"][0]["type"]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_create_user_invalid_password():
|
| 22 |
+
data = {"email": "test@example.com", "password": "short"}
|
| 23 |
+
response = client.post("/users/", json=data)
|
| 24 |
+
assert response.status_code == 422
|
| 25 |
+
assert "ensure this value has at least 6 characters" in response.json()["detail"][0]["msg"]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def test_read_user():
|
| 29 |
+
response = client.get("/users/1")
|
| 30 |
+
assert response.status_code == 200
|
| 31 |
+
assert response.json()["email"] == "test@example.com"
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_read_user_not_found():
|
| 35 |
+
response = client.get("/users/999")
|
| 36 |
+
assert response.status_code == 404
|
| 37 |
+
assert response.json()["detail"] == "User not found"
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def test_update_user():
|
| 41 |
+
data = {"email": "new_email@example.com", "password": "new_password"}
|
| 42 |
+
response = client.put("/users/1", json=data)
|
| 43 |
+
assert response.status_code == 200
|
| 44 |
+
assert response.json()["email"] == "new_email@example.com"
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def test_update_user_not_found():
|
| 48 |
+
data = {"email": "new_email@example.com", "password": "new_password"}
|
| 49 |
+
response = client.put("/users/999", json=data)
|
| 50 |
+
assert response.status_code == 404
|
| 51 |
+
assert response.json()["detail"] == "User not found"
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def test_delete_user():
|
| 55 |
+
response = client.delete("/users/1")
|
| 56 |
+
assert response.status_code == 200
|
| 57 |
+
assert response.json() == {"msg": "User deleted"}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def test_delete_user_not_found():
|
| 61 |
+
response = client.delete("/users/999")
|
| 62 |
+
assert response.status_code == 404
|
| 63 |
+
assert response.json()["detail"] == "User not found"
|
app/tests/test_core/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi.testclient import TestClient
|
| 2 |
+
from app.core.config import settings
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def test_app():
|
| 6 |
+
from app.main import app
|
| 7 |
+
client = TestClient(app)
|
| 8 |
+
response = client.get("/")
|
| 9 |
+
assert response.status_code == 200
|
| 10 |
+
assert response.json() == {"message": "Hello World"}
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_config():
|
| 14 |
+
assert settings.app_name == "My FastAPI Project"
|
| 15 |
+
assert settings.log_level == "debug"
|
| 16 |
+
assert settings.max_connection_count == 10
|
app/tests/test_core/test_config.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.core.config import settings
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def test_settings():
|
| 5 |
+
assert settings.API_V1_STR == "/api/v1"
|
| 6 |
+
assert settings.PROJECT_NAME == "My FastAPI Project"
|
| 7 |
+
assert settings.SQLALCHEMY_DATABASE_URI == "sqlite:///./test.db"
|
| 8 |
+
assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == 30
|
app/tests/test_core/test_database.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy.orm import Session
|
| 2 |
+
from app.core.database import Base
|
| 3 |
+
from app.models.user import User
|
| 4 |
+
from app.models.item import Item
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def test_create_user(db: Session):
|
| 8 |
+
user = User(email="test@example.com", password="password123")
|
| 9 |
+
db.add(user)
|
| 10 |
+
db.commit()
|
| 11 |
+
db.refresh(user)
|
| 12 |
+
assert user.id is not None
|
| 13 |
+
assert user.email == "test@example.com"
|
| 14 |
+
assert user.is_active is True
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def test_create_item(db: Session):
|
| 18 |
+
item = Item(name="test item", description="test description")
|
| 19 |
+
db.add(item)
|
| 20 |
+
db.commit()
|
| 21 |
+
db.refresh(item)
|
| 22 |
+
assert item.id is not None
|
| 23 |
+
assert item.name == "test item"
|
| 24 |
+
assert item.description == "test description"
|
app/tests/test_core/test_security.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import HTTPException
|
| 2 |
+
from app.core.security import verify_password, get_password_hash
|
| 3 |
+
|
| 4 |
+
def test_password_hashing():
|
| 5 |
+
password = "testpassword"
|
| 6 |
+
hashed_password = get_password_hash(password)
|
| 7 |
+
assert hashed_password != password
|
| 8 |
+
|
| 9 |
+
def test_password_verification():
|
| 10 |
+
password = "testpassword"
|
| 11 |
+
hashed_password = get_password_hash(password)
|
| 12 |
+
assert verify_password(password, hashed_password)
|
| 13 |
+
assert not verify_password("wrongpassword", hashed_password)
|
| 14 |
+
|
| 15 |
+
def test_password_verification_exception():
|
| 16 |
+
password = "testpassword"
|
| 17 |
+
hashed_password = get_password_hash(password)
|
| 18 |
+
try:
|
| 19 |
+
verify_password("wrongpassword", hashed_password)
|
| 20 |
+
except HTTPException as e:
|
| 21 |
+
assert e.status_code == 401
|
| 22 |
+
assert e.detail == "Incorrect email or password"
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
+
python-dotenv
|
| 4 |
+
sqlalchemy
|
| 5 |
+
psycopg2-binary
|
| 6 |
+
pytest
|
| 7 |
+
pytest-cov
|
| 8 |
+
faker
|
| 9 |
+
requests-mock
|