Skip to content

Commit 602eb0c

Browse files
authored
Add files via upload
0 parents  commit 602eb0c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1545
-0
lines changed

README.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# FastAPI Template
2+
3+
### app - Backend FastAPI
4+
5+
# Настройка
6+
7+
### Настройка происходит в файле .env его нет в репозитории, т.к. он конфиденциален, но я предоставил файл .env-dist создайте на его основе файл .env и проведите все необходимые настройки.
8+
9+
# Установка зависимостей
10+
11+
### В основе проекта лежит пакетный менеджер poetry, но я так же предоставил файл зависимостей requirements.txt, что бы вы могли установить зависимости через пакетный менеджер pip.
12+
13+
`poetry install` - Вариант с использованием poetry.
14+
15+
`pip install -r requirements.txt` - Вариант с использованием pip.
16+
17+
# Запуск
18+
19+
### Для запуска воспользуйтесь ниже приведёнными командами.
20+
21+
`python -m app` - Команда для запуска backend'а.

alembic.ini

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
[alembic]
2+
script_location = migrations
3+
prepend_sys_path = . app
4+
version_path_separator = os
5+
sqlalchemy.url = driver://user:pass@localhost/dbname
6+
7+
[loggers]
8+
keys = root,sqlalchemy,alembic
9+
10+
[handlers]
11+
keys = console
12+
13+
[formatters]
14+
keys = generic
15+
16+
[logger_root]
17+
level = WARN
18+
handlers = console
19+
qualname =
20+
21+
[logger_sqlalchemy]
22+
level = WARN
23+
handlers =
24+
qualname = sqlalchemy.engine
25+
26+
[logger_alembic]
27+
level = INFO
28+
handlers =
29+
qualname = alembic
30+
31+
[handler_console]
32+
class = StreamHandler
33+
args = (sys.stderr,)
34+
level = NOTSET
35+
formatter = generic
36+
37+
[formatter_generic]
38+
format = %(levelname)-5.5s [%(name)s] %(message)s
39+
datefmt = %H:%M:%S

app/__main__.py

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import uvicorn
2+
from fastapi import FastAPI, Request
3+
from fastapi.middleware.cors import CORSMiddleware
4+
from sqlalchemy.ext.asyncio import AsyncSession
5+
6+
from app.api.api_v1 import api
7+
from app.core.config import settings
8+
from app.database import Database
9+
from app.database.database import engine
10+
11+
app = FastAPI(openapi_url=f"{settings.API_V1_STR}/openapi.json")
12+
13+
14+
@app.middleware("http")
15+
async def session_db(request: Request, call_next):
16+
async with AsyncSession(bind=engine, expire_on_commit=False) as session:
17+
request.state.db = Database(session)
18+
response = await call_next(request)
19+
return response
20+
21+
22+
app.add_middleware(
23+
CORSMiddleware,
24+
allow_credentials=True,
25+
allow_origins=[settings.APP_HOST],
26+
allow_methods=["*"],
27+
allow_headers=["*"],
28+
)
29+
30+
app.include_router(api.api_router, prefix=settings.API_V1_STR)
31+
32+
if __name__ == "__main__":
33+
uvicorn.run(app, host=settings.APP_HOST, port=settings.APP_PORT)
2.07 KB
Binary file not shown.

app/api/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
API Проекта
3+
"""
202 Bytes
Binary file not shown.
2.83 KB
Binary file not shown.

app/api/api_v1/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Первая версия API
3+
"""
220 Bytes
Binary file not shown.
564 Bytes
Binary file not shown.

app/api/api_v1/api.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
Модуль где подключаются все endpoints
3+
"""
4+
5+
from fastapi import APIRouter
6+
7+
from app.api.api_v1.endpoints import users
8+
9+
api_router = APIRouter()
10+
api_router.include_router(users.router, prefix="/users", tags=["users"])

app/api/api_v1/endpoints/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Endpoints module
3+
"""
Binary file not shown.
Binary file not shown.

app/api/api_v1/endpoints/users.py

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
User endpoints
3+
"""
4+
5+
from fastapi import APIRouter, Depends, HTTPException
6+
from starlette import status
7+
8+
from app.api import depends
9+
from app.core.oauth import OAuth2PasswordRequestForm
10+
from app.core.security import create_access_token, get_password_hash, verify_password
11+
from app.database import Database
12+
from app.schemas import UserScheme, UserSchemeAdd, UserTokenScheme
13+
14+
router = APIRouter()
15+
16+
17+
@router.get("/get", response_model=UserScheme)
18+
async def get(current_user=Depends(depends.get_current_user)):
19+
return UserScheme(**current_user.__dict__)
20+
21+
22+
@router.post("/new", response_model=UserTokenScheme)
23+
async def new(user: UserSchemeAdd, db: Database = Depends(depends.get_db)):
24+
user_exists = await db.user.get_by_email(user.email)
25+
if user_exists:
26+
raise HTTPException(
27+
status_code=status.HTTP_401_UNAUTHORIZED,
28+
detail="Такой пользователь уже зарегистрирован.",
29+
headers={"WWW-Authenticate": "Bearer"},
30+
)
31+
32+
hash_password = get_password_hash(user.password)
33+
await db.user.new(user.email, hash_password)
34+
await db.session.commit()
35+
access_token = create_access_token(user.email)
36+
return UserTokenScheme(access_token=access_token)
37+
38+
39+
@router.post("/token", response_model=UserTokenScheme)
40+
async def token(form_data: OAuth2PasswordRequestForm = Depends(), db: Database = Depends(depends.get_db)):
41+
user = await db.user.get_by_email(form_data.email)
42+
if not user or not verify_password(form_data.password, user.password):
43+
raise HTTPException(
44+
status_code=status.HTTP_401_UNAUTHORIZED,
45+
detail="Неверное имя пользователя или пароль",
46+
headers={"WWW-Authenticate": "Bearer"},
47+
)
48+
access_token = create_access_token(user.email)
49+
return UserTokenScheme(access_token=access_token)

app/api/depends.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""
2+
Зависимости
3+
"""
4+
5+
from fastapi import Depends, HTTPException, status, Request
6+
from fastapi.security import OAuth2PasswordBearer
7+
from jose import JWTError, jwt
8+
from sqlalchemy.ext.asyncio import AsyncSession
9+
10+
from app.core.config import settings
11+
from app.database import Database
12+
from app.database.database import engine
13+
14+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/users/token")
15+
16+
17+
async def get_db(request: Request = None) -> Database:
18+
if request:
19+
return request.state.db
20+
else:
21+
async with AsyncSession(bind=engine, expire_on_commit=False) as session:
22+
return Database(session)
23+
24+
25+
async def authorization(token: str, db: Database, credentials_exception):
26+
try:
27+
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
28+
email: str = payload.get("email")
29+
if not email:
30+
raise credentials_exception
31+
except JWTError:
32+
raise credentials_exception
33+
user = await db.user.get_by_email(email)
34+
if not user:
35+
raise credentials_exception
36+
return user
37+
38+
39+
async def get_current_user(token: str = Depends(oauth2_scheme), db: Database = Depends(get_db)):
40+
credentials_exception = HTTPException(
41+
status_code=status.HTTP_401_UNAUTHORIZED,
42+
detail="Не удалось проверить учетные данные",
43+
headers={"WWW-Authenticate": "Bearer"},
44+
)
45+
return await authorization(token, db, credentials_exception)

app/core/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Ядро FastAPI приложения.
3+
"""
223 Bytes
Binary file not shown.
1.43 KB
Binary file not shown.
1.52 KB
Binary file not shown.
1.47 KB
Binary file not shown.

app/core/config.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from pydantic_settings import BaseSettings, SettingsConfigDict
2+
3+
4+
class Settings(BaseSettings):
5+
model_config = SettingsConfigDict(
6+
env_file=".env", env_file_encoding="utf-8", case_sensitive=True
7+
)
8+
9+
# APP
10+
APP_HOST: str
11+
APP_PORT: int
12+
API_V1_STR: str
13+
14+
# Secret
15+
SECRET_KEY: str
16+
ALGORITHM: str
17+
18+
# DATABASE
19+
POSTGRES_HOST: str
20+
POSTGRES_PORT: int
21+
POSTGRES_USER: str
22+
POSTGRES_PASSWORD: str
23+
POSTGRES_DATABASE: str
24+
25+
@property
26+
def pg_dns(self):
27+
return f"postgresql+asyncpg://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DATABASE}"
28+
29+
30+
settings = Settings()

app/core/oauth.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import Annotated, Union
2+
3+
from fastapi import Form
4+
5+
6+
class OAuth2PasswordRequestForm:
7+
def __init__(
8+
self,
9+
*,
10+
grant_type: Annotated[Union[str, None], Form(pattern="password")] = None,
11+
email: Annotated[str, Form()] = None,
12+
password: Annotated[str, Form()],
13+
scope: Annotated[str, Form()] = "",
14+
client_id: Annotated[Union[str, None], Form()] = None,
15+
client_secret: Annotated[Union[str, None], Form()] = None,
16+
):
17+
self.grant_type = grant_type
18+
self.email = email
19+
self.password = password
20+
self.scopes = scope.split()
21+
self.client_id = client_id
22+
self.client_secret = client_secret

app/core/security.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from datetime import datetime, timedelta
2+
3+
from jose import jwt
4+
from passlib.context import CryptContext
5+
6+
from app.core.config import settings
7+
8+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
9+
10+
11+
def verify_password(plain_password, hashed_password):
12+
return pwd_context.verify(plain_password, hashed_password)
13+
14+
15+
def get_password_hash(password):
16+
return pwd_context.hash(password)
17+
18+
19+
def create_access_token(email: str):
20+
data = dict(
21+
email=email,
22+
exp=datetime.utcnow() + timedelta(minutes=120)
23+
)
24+
encoded_jwt = jwt.encode(data, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
25+
return encoded_jwt

app/database/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""
2+
Модуль для взаимодействия с базой данных.
3+
"""
4+
5+
from .database import Database
6+
from .models import Base
7+
8+
__all__ = ('Database', 'Base')
404 Bytes
Binary file not shown.
Binary file not shown.

app/database/database.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from sqlalchemy.engine.url import URL
2+
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
3+
from sqlalchemy.ext.asyncio import create_async_engine as _create_async_engine
4+
5+
from app.core.config import settings
6+
from .repositories import UserRepo
7+
8+
9+
def create_async_engine(url: URL | str) -> AsyncEngine:
10+
return _create_async_engine(url=url, echo=False, pool_pre_ping=True)
11+
12+
13+
engine = create_async_engine(settings.pg_dns)
14+
15+
16+
class Database:
17+
session: AsyncSession
18+
19+
user: UserRepo
20+
21+
def __init__(
22+
self,
23+
session: AsyncSession,
24+
user: UserRepo = None,
25+
):
26+
self.session = session
27+
self.user = user or UserRepo(session=session)

app/database/models/__init__.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""
2+
Модели базы данных.
3+
"""
4+
5+
from .base import Base
6+
from .user import User
7+
8+
__all__ = (
9+
'Base',
10+
'User',
11+
)
Binary file not shown.
Binary file not shown.
Binary file not shown.

app/database/models/base.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""
2+
Базовая модель от которой наследуются остальные модели.
3+
"""
4+
5+
from sqlalchemy import Integer, MetaData
6+
from sqlalchemy.ext.declarative import as_declarative
7+
from sqlalchemy.orm import Mapped, declared_attr, mapped_column
8+
9+
metadata = MetaData(
10+
naming_convention={
11+
'ix': 'ix_%(column_0_label)s',
12+
'uq': 'uq_%(table_name)s_%(column_0_name)s',
13+
'ck': 'ck_%(table_name)s_%(constraint_name)s',
14+
'fk': 'fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s',
15+
'pk': 'pk_%(table_name)s',
16+
}
17+
)
18+
19+
20+
@as_declarative(metadata=metadata)
21+
class Base:
22+
@classmethod
23+
@declared_attr
24+
def __tablename__(cls):
25+
return cls.__name__.lower()
26+
27+
__allow_unmapped__ = False
28+
29+
id: Mapped[int] = mapped_column(
30+
Integer, autoincrement=True, primary_key=True
31+
)

app/database/models/user.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""
2+
Модель пользователя.
3+
"""
4+
5+
import sqlalchemy as sa
6+
from sqlalchemy.orm import Mapped, mapped_column
7+
8+
from .base import Base
9+
10+
11+
class User(Base):
12+
email: Mapped[str] = mapped_column(sa.String, unique=False, nullable=False)
13+
password: Mapped[str] = mapped_column(sa.String, unique=False, nullable=False)
14+
15+
def __repr__(self):
16+
return f"User:{self.id=}"

0 commit comments

Comments
 (0)