FastAPI 11. Хранение и сокращение ссылок
Some checks failed
Lint project / lint (push) Has been cancelled

This commit is contained in:
2025-09-25 12:00:27 +04:00
parent 5a299cf47d
commit be4e939f39
23 changed files with 611 additions and 9 deletions

View File

@ -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

View File

@ -3,7 +3,7 @@
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/proDreams/lkeep/lint.yaml)
[![Код на салфетке](https://img.shields.io/badge/Telegram-Код_на_салфетке-blue)](https://t.me/press_any_button)
[![Заметки на салфетке](https://img.shields.io/badge/Telegram-Заметки_на_салфетке-blue)](https://t.me/writeanynotes)
[![Кот на салфетке чат](https://img.shields.io/badge/Telegram-Кот_на_салфеткеат-blue)](https://t.me/+Li2vbxfWo0Q4ZDk6)
[![Кот на салфетке](https://img.shields.io/badge/Telegram-Кот_на_салфетке-blue)](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
View File

@ -0,0 +1,2 @@
[virtualenvs]
in-project = true

View File

@ -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)

View File

@ -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):

View File

@ -0,0 +1,7 @@
"""
Проект: Lkeep
Автор: Иван Ашихмин
Год: 2025
Специально для проекта "Код на салфетке"
https://pressanybutton.ru/category/servis-na-fastapi/
"""

View 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()

View 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)

View 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):
"""Схема запроса на создание новой ссылки."""

View 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")

View File

@ -0,0 +1,7 @@
"""
Проект: Lkeep
Автор: Иван Ашихмин
Год: 2025
Специально для проекта "Код на салфетке"
https://pressanybutton.ru/category/servis-na-fastapi/
"""

View File

@ -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)

View File

@ -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)

View File

@ -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)]

View File

@ -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):

View File

@ -0,0 +1,7 @@
"""
Проект: Lkeep
Автор: Иван Ашихмин
Год: 2025
Специально для проекта "Код на салфетке"
https://pressanybutton.ru/category/servis-na-fastapi/
"""

View File

@ -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

View File

@ -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 для взаимодействия с базой данных.

View File

@ -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")

View File

@ -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 ###

View File

@ -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")

View 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"))

View File

@ -26,4 +26,9 @@ app.add_middleware(
def start():
"""
Запускает локальный сервер приложения с поддержкой автоматической перезагрузки.
:returns: None
"""
uvicorn.run(app="lkeep.main:app", reload=True)