FastAPI 11. Хранение и сокращение ссылок
Some checks failed
Lint project / lint (push) Has been cancelled
Some checks failed
Lint project / lint (push) Has been cancelled
This commit is contained in:
@ -19,7 +19,7 @@ repos:
|
||||
rev: v3.19.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [ --py312-plus ]
|
||||
args: [ --py313-plus ]
|
||||
|
||||
- repo: https://github.com/hhatto/autopep8
|
||||
rev: v2.3.1
|
||||
@ -39,7 +39,7 @@ repos:
|
||||
rev: 24.10.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.12
|
||||
language_version: python3.13
|
||||
args: [ --line-length=120 ]
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
|
@ -3,7 +3,7 @@
|
||||

|
||||
[](https://t.me/press_any_button)
|
||||
[](https://t.me/writeanynotes)
|
||||
[](https://t.me/+Li2vbxfWo0Q4ZDk6)
|
||||
[](https://t.me/+Li2vbxfWo0Q4ZDk6)
|
||||
|
||||
Lkeep — сервис сокращения ссылок, написанный на Python с использованием современных технологий, таких как FastAPI,
|
||||
PostgreSQL, Poetry, Pydantic и других.
|
||||
@ -59,10 +59,11 @@ PostgreSQL, Poetry, Pydantic и других.
|
||||
7. [FastAPI 7. Электронная почта, подтверждение регистрации, Celery и Redis](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-7-elektronnaya-pochta-podtverzhdenie-registracii-celery-i-redis/)
|
||||
8. [FastAPI 8. Маршрут авторизации и JWT](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-8-marshrut-avtorizacii-i-jwt/)
|
||||
9. [FastAPI 9. Logout и проверка авторизации](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-9-logout-i-proverka-avtorizacii/)
|
||||
10. [FastAPI 10. Изменение данных пользователя]()
|
||||
10. [FastAPI 10. Изменение данных пользователя](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-10-izmenenie-dannyh-polzovatelya/)
|
||||
11. [FastAPI 11. Хранение и сокращение ссылок](https://pressanybutton.ru/post/servis-na-fastapi/fastapi-11-hranenie-i-sokrashenie-ssylok/)
|
||||
|
||||
## Установка
|
||||
|
||||
h
|
||||
Для установки и запуска проекта на вашем локальном компьютере выполните следующие шаги.
|
||||
|
||||
1. **Клонируйте репозиторий:**
|
||||
|
2
poetry.toml
Normal file
2
poetry.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[virtualenvs]
|
||||
in-project = true
|
@ -9,9 +9,11 @@ https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
from fastapi import APIRouter
|
||||
|
||||
from lkeep.apps.auth.routes import auth_router
|
||||
from lkeep.apps.links.routes import links_router
|
||||
from lkeep.apps.profile.routes import profile_router
|
||||
|
||||
apps_router = APIRouter(prefix="/api/v1")
|
||||
|
||||
apps_router.include_router(router=auth_router)
|
||||
apps_router.include_router(router=profile_router)
|
||||
apps_router.include_router(router=links_router)
|
||||
|
@ -18,10 +18,10 @@ class GetUserByID(BaseModel):
|
||||
Класс для получения пользователя по его уникальному идентификатору (ID).
|
||||
|
||||
:ivar id: Уникальный идентификатор пользователя, может быть представлен как объект типа uuid.UUID или строкой.
|
||||
:type id: uuid.UUID | str
|
||||
:type id: uuid.UUID
|
||||
"""
|
||||
|
||||
id: uuid.UUID | str
|
||||
id: uuid.UUID
|
||||
|
||||
|
||||
class GetUserByEmail(BaseModel):
|
||||
|
7
src/lkeep/apps/links/__init__.py
Normal file
7
src/lkeep/apps/links/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
124
src/lkeep/apps/links/managers.py
Normal file
124
src/lkeep/apps/links/managers.py
Normal file
@ -0,0 +1,124 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy import delete, insert, select
|
||||
|
||||
from lkeep.apps.links.schemas import GetLinkSchema, LinkSchema
|
||||
from lkeep.core.core_dependency.db_dependency import DBDependency
|
||||
from lkeep.database.models import Link
|
||||
|
||||
|
||||
class LinksManager:
|
||||
"""
|
||||
Менеджер для выполнения операций над ссылками в базе данных.
|
||||
"""
|
||||
|
||||
def __init__(self, db: DBDependency = Depends(DBDependency)) -> None:
|
||||
"""
|
||||
Инициализирует менеджер с зависимостью доступа к базе данных.
|
||||
|
||||
:param db: Объект для получения асинхронных сессий с базой данных.
|
||||
:type db: DBDependency
|
||||
"""
|
||||
self.db = db
|
||||
self.link_model = Link
|
||||
|
||||
async def get_link(self, short_link: str) -> GetLinkSchema | None:
|
||||
"""
|
||||
Возвращает полную ссылку по короткому идентификатору.
|
||||
|
||||
:param short_link: Сокращенный идентификатор ссылки.
|
||||
:type short_link: str
|
||||
:returns: Найденная ссылка или None, если запись отсутствует.
|
||||
:rtype: GetLinkSchema | None
|
||||
"""
|
||||
async with self.db.db_session() as session:
|
||||
query = select(self.link_model.full_link).where(self.link_model.short_link == short_link)
|
||||
|
||||
result = await session.execute(query)
|
||||
link = result.scalar_one_or_none()
|
||||
|
||||
if link:
|
||||
return GetLinkSchema(full_link=link)
|
||||
|
||||
return None
|
||||
|
||||
async def get_links(self, user_id: uuid.UUID) -> list[LinkSchema]:
|
||||
"""
|
||||
Получает список ссылок, принадлежащих пользователю.
|
||||
|
||||
:param user_id: Идентификатор владельца ссылок.
|
||||
:type user_id: uuid.UUID
|
||||
:returns: Список ссылок пользователя.
|
||||
:rtype: list[LinkSchema]
|
||||
"""
|
||||
async with self.db.db_session() as session:
|
||||
query = select(self.link_model).where(self.link_model.owner_id == user_id)
|
||||
|
||||
result = await session.execute(query)
|
||||
links = result.scalars().all()
|
||||
|
||||
return [LinkSchema.model_validate(link, from_attributes=True) for link in links]
|
||||
|
||||
async def create_link(self, full_link: str, user_id: uuid.UUID, short_link: str) -> LinkSchema:
|
||||
"""
|
||||
Создает новую ссылку и возвращает сохраненную запись.
|
||||
|
||||
:param full_link: Полный адрес, который требуется сократить.
|
||||
:type full_link: str
|
||||
:param user_id: Идентификатор владельца ссылки.
|
||||
:type user_id: uuid.UUID
|
||||
:param short_link: Сгенерированное короткое представление ссылки.
|
||||
:type short_link: str
|
||||
:returns: Созданная ссылка с заполненными полями.
|
||||
:rtype: LinkSchema
|
||||
"""
|
||||
async with self.db.db_session() as session:
|
||||
query = (
|
||||
insert(self.link_model)
|
||||
.values(full_link=full_link, short_link=short_link, owner_id=user_id)
|
||||
.returning(self.link_model)
|
||||
)
|
||||
|
||||
result = await session.execute(query)
|
||||
await session.commit()
|
||||
link = result.scalar()
|
||||
|
||||
return LinkSchema.model_validate(link, from_attributes=True)
|
||||
|
||||
async def get_link_owner(self, link_id: uuid.UUID) -> uuid.UUID | None:
|
||||
"""
|
||||
Возвращает идентификатор владельца ссылки.
|
||||
|
||||
:param link_id: Идентификатор ссылки.
|
||||
:type link_id: uuid.UUID
|
||||
:returns: Идентификатор владельца или None, если ссылка не найдена.
|
||||
:rtype: uuid.UUID | None
|
||||
"""
|
||||
async with self.db.db_session() as session:
|
||||
query = select(self.link_model.owner_id).where(self.link_model.id == link_id)
|
||||
|
||||
result = await session.execute(query)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def delete_link(self, link_id: uuid.UUID) -> None:
|
||||
"""
|
||||
Удаляет ссылку по ее идентификатору.
|
||||
|
||||
:param link_id: Идентификатор ссылки, которую требуется удалить.
|
||||
:type link_id: uuid.UUID
|
||||
:returns: None
|
||||
"""
|
||||
async with self.db.db_session() as session:
|
||||
query = delete(self.link_model).where(self.link_model.id == link_id)
|
||||
|
||||
await session.execute(query)
|
||||
await session.commit()
|
97
src/lkeep/apps/links/routes.py
Normal file
97
src/lkeep/apps/links/routes.py
Normal file
@ -0,0 +1,97 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from starlette import status
|
||||
|
||||
from lkeep.apps.auth.depends import get_current_user
|
||||
from lkeep.apps.auth.schemas import UserVerifySchema
|
||||
from lkeep.apps.links.schemas import (
|
||||
CreateLinkSchema,
|
||||
DeleteLinkSchema,
|
||||
GetLinkSchema,
|
||||
LinkSchema,
|
||||
)
|
||||
from lkeep.apps.links.services import LinksService
|
||||
|
||||
links_router = APIRouter(prefix="/links", tags=["links"])
|
||||
|
||||
|
||||
@links_router.get("/get_link", response_model=GetLinkSchema | None, status_code=status.HTTP_200_OK)
|
||||
async def get_link(short_link: str, service: LinksService = Depends(LinksService)) -> GetLinkSchema | None:
|
||||
"""
|
||||
Возвращает полную ссылку по сокращенному идентификатору.
|
||||
|
||||
:param short_link: Короткий идентификатор ссылки.
|
||||
:type short_link: str
|
||||
:param service: Сервис ссылок, содержащий бизнес-логику.
|
||||
:type service: LinksService
|
||||
:returns: Полная ссылка либо None, если запись не найдена.
|
||||
:rtype: GetLinkSchema | None
|
||||
"""
|
||||
return await service.get_link(short_link=short_link)
|
||||
|
||||
|
||||
@links_router.get("/get_user_links", response_model=list[LinkSchema], status_code=status.HTTP_200_OK)
|
||||
async def get_user_links(
|
||||
user: Annotated[UserVerifySchema, Depends(get_current_user)], service: LinksService = Depends(LinksService)
|
||||
) -> list[LinkSchema]:
|
||||
"""
|
||||
Возвращает список ссылок текущего пользователя.
|
||||
|
||||
:param user: Авторизованный пользователь, для которого запрашиваются ссылки.
|
||||
:type user: UserVerifySchema
|
||||
:param service: Сервис ссылок, выполняющий выборку данных.
|
||||
:type service: LinksService
|
||||
:returns: Коллекция ссылок пользователя.
|
||||
:rtype: list[LinkSchema]
|
||||
"""
|
||||
return await service.get_links(user=user)
|
||||
|
||||
|
||||
@links_router.post("/create_link", response_model=LinkSchema, status_code=status.HTTP_201_CREATED)
|
||||
async def create_link(
|
||||
link_data: CreateLinkSchema,
|
||||
user: Annotated[UserVerifySchema, Depends(get_current_user)],
|
||||
service: LinksService = Depends(LinksService),
|
||||
) -> LinkSchema:
|
||||
"""
|
||||
Создает новую сокращенную ссылку для пользователя.
|
||||
|
||||
:param link_data: Данные с полным адресом ссылки.
|
||||
:type link_data: CreateLinkSchema
|
||||
:param user: Пользователь, для которого создается ссылка.
|
||||
:type user: UserVerifySchema
|
||||
:param service: Сервис ссылок, отвечающий за генерацию и сохранение записи.
|
||||
:type service: LinksService
|
||||
:returns: Созданная ссылка с коротким идентификатором.
|
||||
:rtype: LinkSchema
|
||||
"""
|
||||
return await service.create_link(link_data=link_data, user=user)
|
||||
|
||||
|
||||
@links_router.delete("/delete_link", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_link(
|
||||
link_data: DeleteLinkSchema,
|
||||
user: Annotated[UserVerifySchema, Depends(get_current_user)],
|
||||
service: LinksService = Depends(LinksService),
|
||||
) -> None:
|
||||
"""
|
||||
Удаляет ссылку, если она принадлежит текущему пользователю.
|
||||
|
||||
:param link_data: Данные с идентификатором ссылки для удаления.
|
||||
:type link_data: DeleteLinkSchema
|
||||
:param user: Авторизованный пользователь, запрашивающий удаление.
|
||||
:type user: UserVerifySchema
|
||||
:param service: Сервис ссылок, выполняющий проверку владельца и удаление.
|
||||
:type service: LinksService
|
||||
:returns: None
|
||||
"""
|
||||
await service.delete_link(link_data=link_data, user=user)
|
45
src/lkeep/apps/links/schemas.py
Normal file
45
src/lkeep/apps/links/schemas.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class BaseFullLink(BaseModel):
|
||||
"""
|
||||
Базовая схема, содержащая полный адрес ссылки.
|
||||
"""
|
||||
|
||||
full_link: str
|
||||
|
||||
|
||||
class DeleteLinkSchema(BaseModel):
|
||||
"""
|
||||
Схема для удаления ссылки по идентификатору.
|
||||
"""
|
||||
|
||||
id: uuid.UUID
|
||||
|
||||
|
||||
class LinkSchema(BaseFullLink, DeleteLinkSchema):
|
||||
"""
|
||||
Полная схема ссылки с коротким адресом и метаданными.
|
||||
"""
|
||||
|
||||
short_link: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class GetLinkSchema(BaseFullLink):
|
||||
"""Схема ответа при получении полной ссылки по сокращенному адресу."""
|
||||
|
||||
|
||||
class CreateLinkSchema(BaseFullLink):
|
||||
"""Схема запроса на создание новой ссылки."""
|
100
src/lkeep/apps/links/services.py
Normal file
100
src/lkeep/apps/links/services.py
Normal file
@ -0,0 +1,100 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
import secrets
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from starlette import status
|
||||
|
||||
from lkeep.apps.auth.schemas import UserVerifySchema
|
||||
from lkeep.apps.links.managers import LinksManager
|
||||
from lkeep.apps.links.schemas import (
|
||||
CreateLinkSchema,
|
||||
DeleteLinkSchema,
|
||||
GetLinkSchema,
|
||||
LinkSchema,
|
||||
)
|
||||
from lkeep.core.settings import settings
|
||||
|
||||
|
||||
class LinksService:
|
||||
"""
|
||||
Сервисный слой для работы с пользовательскими ссылками.
|
||||
"""
|
||||
|
||||
def __init__(self, manager: LinksManager = Depends(LinksManager)) -> None:
|
||||
"""
|
||||
Создает сервис со связанным менеджером ссылок.
|
||||
|
||||
:param manager: Менеджер, выполняющий операции с базой данных.
|
||||
:type manager: LinksManager
|
||||
"""
|
||||
self.manager = manager
|
||||
|
||||
async def get_link(self, short_link: str) -> GetLinkSchema | None:
|
||||
"""
|
||||
Получает полную ссылку по ее короткому представлению.
|
||||
|
||||
:param short_link: Сокращенный идентификатор ссылки.
|
||||
:type short_link: str
|
||||
:returns: Полная ссылка или None, если запись отсутствует.
|
||||
:rtype: GetLinkSchema | None
|
||||
"""
|
||||
return await self.manager.get_link(short_link=short_link)
|
||||
|
||||
async def get_links(self, user: UserVerifySchema) -> list[LinkSchema]:
|
||||
"""
|
||||
Возвращает все ссылки, принадлежащие пользователю.
|
||||
|
||||
:param user: Данные авторизованного пользователя.
|
||||
:type user: UserVerifySchema
|
||||
:returns: Список ссылок пользователя.
|
||||
:rtype: list[LinkSchema]
|
||||
"""
|
||||
return await self.manager.get_links(user_id=user.id)
|
||||
|
||||
async def create_link(self, link_data: CreateLinkSchema, user: UserVerifySchema) -> LinkSchema:
|
||||
"""
|
||||
Создает новую сокращенную ссылку для пользователя.
|
||||
|
||||
:param link_data: Данные запроса с полным адресом ссылки.
|
||||
:type link_data: CreateLinkSchema
|
||||
:param user: Пользователь, которому будет принадлежать ссылка.
|
||||
:type user: UserVerifySchema
|
||||
:returns: Созданная запись о ссылке.
|
||||
:rtype: LinkSchema
|
||||
"""
|
||||
link_length = settings.link_length
|
||||
|
||||
while True:
|
||||
short_link = secrets.token_urlsafe(link_length)[:link_length]
|
||||
try:
|
||||
return await self.manager.create_link(
|
||||
full_link=link_data.full_link, user_id=user.id, short_link=short_link
|
||||
)
|
||||
except IntegrityError:
|
||||
continue
|
||||
|
||||
async def delete_link(self, link_data: DeleteLinkSchema, user: UserVerifySchema) -> None:
|
||||
"""
|
||||
Удаляет ссылку пользователя после проверки владельца.
|
||||
|
||||
:param link_data: Данные с идентификатором ссылки для удаления.
|
||||
:type link_data: DeleteLinkSchema
|
||||
:param user: Пользователь, запрашивающий удаление.
|
||||
:type user: UserVerifySchema
|
||||
:raises HTTPException: Если пользователь не является владельцем ссылки.
|
||||
:returns: None
|
||||
"""
|
||||
link_owner = await self.manager.get_link_owner(link_id=link_data.id)
|
||||
|
||||
if link_owner and link_owner == user.id:
|
||||
await self.manager.delete_link(link_id=link_data.id)
|
||||
else:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Wrong link owner")
|
@ -0,0 +1,7 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
@ -1,3 +1,11 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
@ -9,11 +17,30 @@ from lkeep.database.models import User
|
||||
|
||||
|
||||
class ProfileManager:
|
||||
"""
|
||||
Менеджер для работы с данными профиля в базе данных.
|
||||
"""
|
||||
|
||||
def __init__(self, db: DBDependency = Depends(DBDependency)) -> None:
|
||||
"""
|
||||
Инициализирует менеджер с зависимостью доступа к базе данных.
|
||||
|
||||
:param db: Провайдер асинхронных сессий базы данных.
|
||||
:type db: DBDependency
|
||||
"""
|
||||
self.db = db
|
||||
self.user_model = User
|
||||
|
||||
async def update_user_fields(self, user_id: uuid.UUID | str, **kwargs: Any) -> None:
|
||||
"""
|
||||
Обновляет выбранные поля пользователя по его идентификатору.
|
||||
|
||||
:param user_id: Идентификатор пользователя, данные которого нужно изменить.
|
||||
:type user_id: uuid.UUID | str
|
||||
:param kwargs: Поля и значения, подлежащие обновлению.
|
||||
:type kwargs: Any
|
||||
:returns: None
|
||||
"""
|
||||
async with self.db.db_session() as session:
|
||||
query = update(self.user_model).where(self.user_model.id == user_id).values(**kwargs)
|
||||
|
||||
@ -22,6 +49,14 @@ class ProfileManager:
|
||||
await session.commit()
|
||||
|
||||
async def get_user_hashed_password(self, user_id: uuid.UUID | str) -> str:
|
||||
"""
|
||||
Возвращает хешированный пароль пользователя.
|
||||
|
||||
:param user_id: Идентификатор пользователя для поиска.
|
||||
:type user_id: uuid.UUID | str
|
||||
:returns: Хеш текущего пароля пользователя.
|
||||
:rtype: str
|
||||
"""
|
||||
async with self.db.db_session() as session:
|
||||
query = select(self.user_model.hashed_password).where(self.user_model.id == user_id)
|
||||
|
||||
|
@ -1,3 +1,11 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
@ -18,6 +26,17 @@ async def change_email(
|
||||
user: Annotated[UserVerifySchema, Depends(get_current_user)],
|
||||
service: ProfileService = Depends(ProfileService),
|
||||
) -> None:
|
||||
"""
|
||||
Изменяет адрес электронной почты текущего пользователя.
|
||||
|
||||
:param data: Данные с новым адресом электронной почты пользователя.
|
||||
:type data: ChangeEmailRequest
|
||||
:param user: Авторизованный пользователь, инициирующий изменение почты.
|
||||
:type user: UserVerifySchema
|
||||
:param service: Сервисный слой, выполняющий бизнес-логику профиля.
|
||||
:type service: ProfileService
|
||||
:returns: None
|
||||
"""
|
||||
return await service.change_email(data=data, user=user)
|
||||
|
||||
|
||||
@ -27,4 +46,16 @@ async def change_password(
|
||||
user: Annotated[UserVerifySchema, Depends(get_current_user)],
|
||||
service: ProfileService = Depends(ProfileService),
|
||||
) -> Response:
|
||||
"""
|
||||
Обновляет пароль авторизованного пользователя.
|
||||
|
||||
:param data: Данные с текущим и новым паролем.
|
||||
:type data: ChangePasswordRequest
|
||||
:param user: Пользователь, для которого выполняется смена пароля.
|
||||
:type user: UserVerifySchema
|
||||
:param service: Сервис профиля, реализующий проверку и обновление данных.
|
||||
:type service: ProfileService
|
||||
:returns: HTTP-ответ, подтверждающий успешную операцию либо ошибку.
|
||||
:rtype: Response
|
||||
"""
|
||||
return await service.change_password(data=data, user=user)
|
||||
|
@ -1,12 +1,28 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, EmailStr, StringConstraints
|
||||
|
||||
|
||||
class ChangeEmailRequest(BaseModel):
|
||||
"""
|
||||
Схема запроса на изменение электронной почты пользователя.
|
||||
"""
|
||||
|
||||
new_email: EmailStr
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
"""
|
||||
Схема запроса на обновление пароля пользователя.
|
||||
"""
|
||||
|
||||
old_password: Annotated[str, StringConstraints(min_length=8, max_length=128)]
|
||||
new_password: Annotated[str, StringConstraints(min_length=8, max_length=128)]
|
||||
|
@ -1,3 +1,11 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
from fastapi import Depends
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
@ -8,18 +16,49 @@ from lkeep.apps.profile.schemas import ChangeEmailRequest, ChangePasswordRequest
|
||||
|
||||
|
||||
class ProfileService:
|
||||
"""
|
||||
Сервис для работы с данными профиля пользователя.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
manager: ProfileManager = Depends(ProfileManager),
|
||||
handler: AuthHandler = Depends(AuthHandler),
|
||||
) -> None:
|
||||
"""
|
||||
Создает экземпляр сервиса профиля с необходимыми зависимостями.
|
||||
|
||||
:param manager: Менеджер для выполнения операций с моделью пользователя.
|
||||
:type manager: ProfileManager
|
||||
:param handler: Обработчик аутентификации, предоставляющий функции хеширования и проверки пароля.
|
||||
:type handler: AuthHandler
|
||||
"""
|
||||
self.manager = manager
|
||||
self.handler = handler
|
||||
|
||||
async def change_email(self, data: ChangeEmailRequest, user: UserVerifySchema) -> None:
|
||||
"""
|
||||
Обновляет адрес электронной почты пользователя.
|
||||
|
||||
:param data: Запрос с новым адресом электронной почты.
|
||||
:type data: ChangeEmailRequest
|
||||
:param user: Пользователь, для которого применяется изменение.
|
||||
:type user: UserVerifySchema
|
||||
:returns: None
|
||||
"""
|
||||
return await self.manager.update_user_fields(user_id=user.id, email=data.new_email)
|
||||
|
||||
async def change_password(self, data: ChangePasswordRequest, user: UserVerifySchema) -> None | JSONResponse:
|
||||
"""
|
||||
Изменяет пароль пользователя после проверки старого значения.
|
||||
|
||||
:param data: Запрос, содержащий старый и новый пароль.
|
||||
:type data: ChangePasswordRequest
|
||||
:param user: Пользователь, выполняющий смену пароля.
|
||||
:type user: UserVerifySchema
|
||||
:returns: None при успешном обновлении либо JSON-ответ с ошибкой.
|
||||
:rtype: None | JSONResponse
|
||||
"""
|
||||
current_password_hash = await self.manager.get_user_hashed_password(user_id=user.id)
|
||||
|
||||
if await self.handler.verify_password(raw_password=data.old_password, hashed_password=current_password_hash):
|
||||
|
@ -0,0 +1,7 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
@ -1,3 +1,11 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
from lkeep.core.settings import settings
|
||||
|
@ -41,7 +41,7 @@ class RedisDependency:
|
||||
return ConnectionPool.from_url(url=self._url, encoding="utf-8", decode_responses=True)
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_client(self) -> AsyncGenerator[Redis, None]:
|
||||
async def get_client(self) -> AsyncGenerator[Redis]:
|
||||
"""
|
||||
Получает клиентскую сессию Redis для взаимодействия с базой данных.
|
||||
|
||||
|
@ -119,6 +119,8 @@ class Settings(BaseSettings):
|
||||
:type frontend_url: str
|
||||
:ivar access_token_expire: Срок жизни JWT-токена
|
||||
:type access_token_expire: int
|
||||
:ivar link_length: Максимальная длина короткой ссылки
|
||||
:type link_length: int
|
||||
"""
|
||||
|
||||
db_settings: DBSettings = DBSettings()
|
||||
@ -129,6 +131,7 @@ class Settings(BaseSettings):
|
||||
frontend_url: str
|
||||
access_token_expire: int
|
||||
domain: str
|
||||
link_length: int = 12
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf8", extra="ignore")
|
||||
|
||||
|
@ -0,0 +1,41 @@
|
||||
"""Links model
|
||||
|
||||
Revision ID: d93cd9da97e5
|
||||
Revises: ccf7560dd457
|
||||
Create Date: 2025-09-24 14:30:04.603787
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "d93cd9da97e5"
|
||||
down_revision: str | None = "ccf7560dd457"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"link",
|
||||
sa.Column("full_link", sa.String(), nullable=False),
|
||||
sa.Column("short_link", sa.String(length=12), nullable=False),
|
||||
sa.Column("owner_id", sa.UUID(), nullable=False),
|
||||
sa.Column("id", sa.UUID(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.ForeignKeyConstraint(["owner_id"], ["user.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_link_short_link"), "link", ["short_link"], unique=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_link_short_link"), table_name="link")
|
||||
op.drop_table("link")
|
||||
# ### end Alembic commands ###
|
@ -7,7 +7,8 @@ https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
from lkeep.database.models.base import Base
|
||||
from lkeep.database.models.links import Link
|
||||
from lkeep.database.models.user import User
|
||||
|
||||
|
||||
__all__ = ("Base", "User")
|
||||
__all__ = ("Base", "User", "Link")
|
||||
|
31
src/lkeep/database/models/links.py
Normal file
31
src/lkeep/database/models/links.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""
|
||||
Проект: Lkeep
|
||||
Автор: Иван Ашихмин
|
||||
Год: 2025
|
||||
Специально для проекта "Код на салфетке"
|
||||
https://pressanybutton.ru/category/servis-na-fastapi/
|
||||
"""
|
||||
|
||||
from sqlalchemy import UUID, ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from lkeep.database.mixins.id_mixins import IDMixin
|
||||
from lkeep.database.mixins.timestamp_mixins import CreatedAtMixin
|
||||
from lkeep.database.models import Base
|
||||
|
||||
|
||||
class Link(IDMixin, CreatedAtMixin, Base):
|
||||
"""
|
||||
Модель сокращенной ссылки с указанием владельца.
|
||||
|
||||
:ivar full_link: Полная ссылка
|
||||
:type full_link: str
|
||||
:ivar short_link: Сокращённая ссылка
|
||||
:type short_link: str
|
||||
:ivar owner_id: Создатель ссылки
|
||||
:type owner_id: UUID
|
||||
"""
|
||||
|
||||
full_link: Mapped[str] = mapped_column(String)
|
||||
short_link: Mapped[str] = mapped_column(String(12), unique=True, index=True)
|
||||
owner_id: Mapped[UUID] = mapped_column(ForeignKey("user.id", ondelete="CASCADE"))
|
@ -26,4 +26,9 @@ app.add_middleware(
|
||||
|
||||
|
||||
def start():
|
||||
"""
|
||||
Запускает локальный сервер приложения с поддержкой автоматической перезагрузки.
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
uvicorn.run(app="lkeep.main:app", reload=True)
|
||||
|
Reference in New Issue
Block a user