Add websckets connection

This commit is contained in:
2026-01-20 17:06:06 +03:00
parent 8167c77a27
commit a690116399
19 changed files with 748 additions and 83 deletions
+4 -2
View File
@@ -46,13 +46,15 @@ async def login(response: Response, credentials: OAuth2PasswordRequestForm = Dep
'access_token',
token.access_token,
max_age=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
httponly=True
httponly=True,
samesite='lax'
)
response.set_cookie(
'refresh_token',
str(token.refresh_token),
max_age=settings.REFRESH_TOKEN_EXPIRE_DAYS * 30 * 24 * 60,
httponly=True
httponly=True,
samesite='lax'
)
return token
+100
View File
@@ -0,0 +1,100 @@
import uuid
from typing import Optional, List
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import func, select, and_
from app.core.dao import BaseDAO
from app.chats.models import ChatModel, MessageModel, ParticipantModel
from app.chats.schemas import ChatCreateDB, MessageCreateDB, ParticipantCreateDB
from app.chats.schemas import ChatUpdateDB, MessageUpdateDB, ParticipantUpdateDB
from app.users.models import UserModel
class ChatDAO(BaseDAO[ChatModel, ChatCreateDB, ChatUpdateDB]):
model = ChatModel
@classmethod
async def get_chat_id(cls, session: AsyncSession, user_a_id: int, user_b_id: int) -> Optional[uuid.UUID]:
stmt = (
select(ParticipantModel.chat_id)
.join(ChatModel)
.where(ChatModel.is_group == False)
.where(ParticipantModel.user_id.in_([user_a_id, user_b_id]))
.group_by(ParticipantModel.chat_id)
.having(func.count(ParticipantModel.chat_id) == 2)
)
result = await session.execute(stmt)
return result.scalar_one_or_none()
@classmethod
async def get_chats(cls, session: AsyncSession, user_id: int, offset: int = 0, limit: int = 10):
stmt = (
select(
ChatModel.id.label("chat_id"),
ChatModel.last_message,
UserModel.id.label("user_id"),
UserModel.avatar_url,
UserModel.display_name
)
.join(ParticipantModel, ParticipantModel.chat_id==ChatModel.id)
.join(UserModel, UserModel.id==ParticipantModel.user_id)
.where(
ChatModel.id.in_(
select(ParticipantModel.chat_id).where(ParticipantModel.user_id==user_id)
)
)
.where(UserModel.id!=user_id)
.where(ChatModel.is_group==False)
.order_by(ChatModel.updated_at.desc())
.limit(limit)
.offset(offset)
)
result = await session.execute(stmt)
return result.mappings().all()
@classmethod
async def get_chat_with_participant(cls, session: AsyncSession, chat_id: uuid.UUID, user_id: int):
stmt = (
select(ChatModel, ParticipantModel.id.label("participant_id"))
.outerjoin(
ParticipantModel,
and_(
ParticipantModel.chat_id==ChatModel.id,
ParticipantModel.user_id==user_id
)
)
.where(ChatModel.id==chat_id)
)
result = await session.execute(stmt)
return result.first()
class MessageDAO(BaseDAO[MessageModel, MessageCreateDB, MessageUpdateDB]):
model = MessageModel
@classmethod
async def find_all_asc(
cls,
session: AsyncSession,
offset: Optional[int],
limit: Optional[int],
*filter,
**filter_by
) -> List[MessageModel]:
stmt = select(MessageModel).filter(*filter).filter_by(**filter_by).order_by(MessageModel.created_at.asc())
if offset is not None:
stmt = stmt.offset(offset)
if limit is not None:
stmt = stmt.limit(limit)
result = await session.execute(stmt)
return result.scalars().all()
class ParticipantDAO(BaseDAO[ParticipantModel, ParticipantCreateDB, ParticipantUpdateDB]):
model = ParticipantModel
+28 -5
View File
@@ -1,5 +1,7 @@
import uuid
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import ForeignKey
from sqlalchemy import ForeignKey, UUID, UniqueConstraint
from app.core.database import Base
@@ -7,7 +9,28 @@ from app.core.database import Base
class MessageModel(Base):
__tablename__ = "message"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
sender_id: Mapped[int] = mapped_column(ForeignKey("user.id", ondelete="CASCADE"), index=True)
recipient_id: Mapped[int] = mapped_column(ForeignKey("user.id", ondelete="CASCADE"), index=True)
content: Mapped[str] = mapped_column()
id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True, index=True, default=uuid.uuid4)
sender_id: Mapped[int] = mapped_column(ForeignKey("user.id", ondelete="SET NULL"), index=True)
chat_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey("chat.id", ondelete="CASCADE"), index=True)
content: Mapped[str] = mapped_column()
is_read: Mapped[bool] = mapped_column(default=False)
class ChatModel(Base):
__tablename__ = "chat"
id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True, index=True, default=uuid.uuid4)
is_group: Mapped[bool] = mapped_column(default=False)
last_message: Mapped[str] = mapped_column(nullable=True)
class ParticipantModel(Base):
__tablename__ = "Participant"
id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True, index=True, default=uuid.uuid4)
chat_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey("chat.id", ondelete="CASCADE"), index=True)
user_id: Mapped[int] = mapped_column(ForeignKey("user.id", ondelete="CASCADE"), index=True)
__table_args__ = (
UniqueConstraint("chat_id", "user_id", name="uq_chat_user"),
)
+44
View File
@@ -0,0 +1,44 @@
import uuid
from typing import List
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect
from app.chats.service import ChatService
from app.auth.dependencies import get_current_verified_user
from app.users.models import UserModel
from app.chats.schemas import Chat, MessageCreate, Message
router = APIRouter(prefix="/chats", tags=["chats"])
@router.get("/")
async def get_chats(
offset: int = 0,
limit: int = 10,
user: UserModel = Depends(get_current_verified_user)
) -> List[Chat]:
return await ChatService.get_chats(user, offset, limit)
@router.get("/{chat_id}")
async def get_chat(
chat_id: uuid.UUID,
offset: int = 0,
limit: int = 10,
user: UserModel = Depends(get_current_verified_user)
) -> List[Message]:
return await ChatService.get_chat(chat_id, user, offset, limit)
@router.post("/message")
async def send_message(message: MessageCreate, user: UserModel = Depends(get_current_verified_user)) -> Message:
return await ChatService.send_message(user, message)
@router.websocket("/ws")
async def websocket_endpoint(ws: WebSocket, user: UserModel = Depends(get_current_verified_user)):
await ws.accept()
await ChatService.save_websocket(user, ws)
try:
while True:
await ws.receive_text()
except WebSocketDisconnect:
await ChatService.delete_websocket(user)
+67
View File
@@ -0,0 +1,67 @@
from datetime import datetime
from typing import Optional
import uuid
from pydantic import BaseModel
class MessageCreate(BaseModel):
recipient_id: Optional[int] = None
chat_id: Optional[uuid.UUID] = None
content: str
class MessageUpdate(BaseModel):
id: uuid.UUID
content: str
class MessageCreateDB(BaseModel):
sender_id: Optional[int]
chat_id: Optional[uuid.UUID]
content: Optional[str]
is_read: Optional[bool] = False
class MessageUpdateDB(BaseModel):
content: Optional[str]
class Message(BaseModel):
id: uuid.UUID
sender_id: int
chat_id: uuid.UUID
content: str
created_at: datetime
updated_at: datetime
class ChatBase(BaseModel):
is_group: Optional[bool] = False
last_message: Optional[str] = None
class ChatCreateDB(ChatBase):
pass
class ChatUpdateDB(ChatBase):
pass
class Chat(BaseModel):
chat_id: uuid.UUID
user_id: int
last_message: Optional[str]
avatar_url: Optional[str]
display_name: str
class ParticipantCreateDB(BaseModel):
chat_id: Optional[uuid.UUID]
user_id: Optional[int]
class ParticipantUpdateDB(BaseModel):
chat_id: Optional[uuid.UUID]
user_id: Optional[int]
+183
View File
@@ -0,0 +1,183 @@
import json
import uuid
from typing import List, Dict
import logging
from fastapi import HTTPException, status, WebSocket
from sqlalchemy import and_
from app.core.database import async_session_maker
from app.chats.dao import ChatDAO, MessageDAO, ParticipantDAO
from app.chats.models import ChatModel, MessageModel, ParticipantModel
from app.chats.schemas import Chat, MessageCreate, MessageCreateDB, ChatCreateDB, ParticipantCreateDB, Message
from app.users.models import UserModel
from app.core.redis import get_redis
log = logging.getLogger(__name__)
class ChatService:
active_connections: Dict[str, WebSocket] = {}
@classmethod
async def get_chats(cls, user: UserModel, offset: int, limit: int) -> List[Chat]:
log.debug("Getting chats", extra={"user_id": user.id, "offset": offset, "limit": limit})
async with async_session_maker() as session:
chats = await ChatDAO.get_chats(session, user.id, offset, limit)
log.debug("Retrieved chats", extra={"user_id": user.id, "count": len(chats)})
return chats
@classmethod
async def send_message(cls, sender: UserModel, message: MessageCreate) -> Message:
log.info("Sending message", extra={"sender_id": sender.id, "chat_id": message.chat_id, "recipient_id": message.recipient_id})
async with async_session_maker() as session:
target_chat_id = message.chat_id
if target_chat_id is None:
if message.recipient_id is None:
log.warning("Message send failed: missing chat_id and recipient_id", extra={"sender_id": sender.id})
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Need chat_id or user_id")
target_chat_id = await ChatDAO.get_chat_id(session, sender.id, message.recipient_id)
if target_chat_id is None:
log.info("Creating new chat", extra={"sender_id": sender.id, "recipient_id": message.recipient_id})
target_chat_db = await ChatDAO.add(
session,
obj_in=ChatCreateDB(
is_group=False,
last_message=message.content
)
)
target_chat_id: uuid.UUID = target_chat_db.id
await ParticipantDAO.add(
session,
obj_in=ParticipantCreateDB(
user_id=sender.id,
chat_id=target_chat_id
)
)
await ParticipantDAO.add(
session,
obj_in=ParticipantCreateDB(
user_id=message.recipient_id,
chat_id=target_chat_id
)
)
log.info("Created new chat", extra={"chat_id": target_chat_id, "sender_id": sender.id, "recipient_id": message.recipient_id})
members = await ParticipantDAO.find_all(
session,
None,
None,
ParticipantModel.chat_id==target_chat_id
)
members_ids = [member.user_id for member in members]
if not sender.id in members_ids :
log.warning("Access denied to chat", extra={"user_id": sender.id, "chat_id": message.chat_id})
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="Access denied")
message_db = await MessageDAO.add(
session,
obj_in=MessageCreateDB(
sender_id=sender.id,
chat_id=target_chat_id,
content=message.content
)
)
await cls._send_ws_message(members_ids, Message(
id=message_db.id,
sender_id=message_db.sender_id,
chat_id=message_db.chat_id,
content=message_db.content,
created_at=message_db.created_at,
updated_at=message_db.updated_at
))
await ChatDAO.update(
session,
ChatModel.id==target_chat_id,
obj_in={"last_message": message.content}
)
await session.commit()
log.info("Message sent", extra={"message_id": message_db.id, "sender_id": sender.id, "chat_id": target_chat_id})
return message_db
@classmethod
async def get_chat(cls, chat_id: uuid.UUID, user: UserModel, offset: int = 0, limit: int = 0) -> List[Message]:
log.debug("Getting chat messages", extra={"user_id": user.id, "chat_id": chat_id, "offset": offset, "limit": limit})
async with async_session_maker() as session:
chat_exist = await ChatDAO.get_chat_with_participant(session, chat_id, user.id)
if chat_exist is None:
log.warning("Chat not found", extra={"user_id": user.id, "chat_id": chat_id})
raise HTTPException(status.HTTP_404_NOT_FOUND, "Chat not found")
if chat_exist.participant_id is None:
log.warning("Access denied to chat", extra={"user_id": user.id, "chat_id": chat_id})
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="Access denied")
messages = await MessageDAO.find_all_asc(
session,
offset,
limit,
MessageModel.chat_id==chat_id
)
log.debug("Retrieved chat messages", extra={"user_id": user.id, "chat_id": chat_id, "count": len(messages)})
return messages
@classmethod
async def save_websocket(cls, user: UserModel, ws: WebSocket):
cls.active_connections[str(user.id)] = ws
log.info("WebSocket connection saved", extra={"user_id": user.id, "active_connections": len(cls.active_connections) + 1})
@classmethod
async def delete_websocket(cls, user: UserModel):
cls.active_connections.pop(str(user.id))
log.info("WebSocket connection deleted", extra={"user_id": user.id, "active_connections": len(cls.active_connections) - 1})
@classmethod
async def message_listener(cls):
redis_client = await get_redis()
pubsub = redis_client.pubsub()
await pubsub.subscribe("messenger_updates")
async for message in pubsub.listen():
log.debug(f"Received message from Redis: {message}")
if message["type"] == "message":
payload = json.loads(message["data"])
user_id = payload["user_id"]
if user_id in cls.active_connections:
ws = cls.active_connections[user_id]
await ws.send_json(payload["message"])
log.info(f"Message sent to user {user_id} via WebSocket")
else:
log.debug(f"User {user_id} not connected")
@classmethod
async def _send_ws_message(cls, user_ids: List[int], message: Message):
redis_client = await get_redis()
for user_id in user_ids:
payload = {
"user_id": str(user_id),
"message": message.model_dump(mode='json')
}
await redis_client.publish("messenger_updates", json.dumps(payload))
log.debug(f"Published message for user_id: {user_id}")
+8
View File
@@ -3,6 +3,7 @@ from contextlib import asynccontextmanager
import uvicorn
import logging
import uuid
import asyncio
from fastapi import FastAPI, APIRouter, Request, Response
from fastapi.middleware.cors import CORSMiddleware
@@ -10,6 +11,8 @@ from fastapi.middleware.cors import CORSMiddleware
from app.core.redis import close_redis, init_redis
from app.users.router import router as user_router
from app.auth.router import router as auth_router
from app.chats.router import router as chat_router
from app.chats.service import ChatService
from app.core.log_config import set_logging
from app.core.config import settings
@@ -21,14 +24,19 @@ log = logging.getLogger(__name__)
async def lifespan(app: FastAPI):
await init_redis()
log.info("Redis connected")
task_send_message = asyncio.create_task(ChatService.message_listener())
log.info("Message sender started")
yield
await close_redis()
log.info("Redis disconnected")
task_send_message.cancel()
log.info("Message sender stopped")
api_router = APIRouter(prefix="/api/v1")
api_router.include_router(user_router)
api_router.include_router(auth_router)
api_router.include_router(chat_router)
@api_router.get("/health")
async def test_health():
+2
View File
@@ -9,6 +9,8 @@ from alembic import context
from app.core.database import Base
from app.core.config import settings
from app.users.models import UserModel
from app.chats.models import MessageModel, ChatModel, ParticipantModel
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
@@ -0,0 +1,101 @@
"""Create tables
Revision ID: 0d3f7039ba77
Revises: 7ad624ae1699
Create Date: 2026-01-12 15:51:43.453822
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '0d3f7039ba77'
down_revision: Union[str, Sequence[str], None] = '7ad624ae1699'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('chat',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('is_group', sa.Boolean(), nullable=False),
sa.Column('last_message', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('chat_pkey'))
)
op.create_index(op.f('chat_id_idx'), 'chat', ['id'], unique=False)
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('display_name', sa.String(), nullable=False),
sa.Column('username', sa.String(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('birth_day', sa.DATE(), nullable=True),
sa.Column('description', sa.String(), nullable=True),
sa.Column('avatar_url', sa.String(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('is_verified', sa.Boolean(), nullable=False),
sa.Column('is_superuser', sa.Boolean(), nullable=False),
sa.Column('hashed_password', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('user_pkey'))
)
op.create_index(op.f('user_email_idx'), 'user', ['email'], unique=True)
op.create_index(op.f('user_id_idx'), 'user', ['id'], unique=False)
op.create_index(op.f('user_username_idx'), 'user', ['username'], unique=True)
op.create_table('Participant',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('chat_id', sa.UUID(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['chat_id'], ['chat.id'], name=op.f('Participant_chat_id_fkey'), ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('Participant_user_id_fkey'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('Participant_pkey')),
sa.UniqueConstraint('chat_id', 'user_id', name='uq_chat_user')
)
op.create_index(op.f('Participant_chat_id_idx'), 'Participant', ['chat_id'], unique=False)
op.create_index(op.f('Participant_id_idx'), 'Participant', ['id'], unique=False)
op.create_index(op.f('Participant_user_id_idx'), 'Participant', ['user_id'], unique=False)
op.create_table('message',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('sender_id', sa.Integer(), nullable=False),
sa.Column('chat_id', sa.UUID(), nullable=False),
sa.Column('content', sa.String(), nullable=False),
sa.Column('is_read', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['chat_id'], ['chat.id'], name=op.f('message_chat_id_fkey'), ondelete='CASCADE'),
sa.ForeignKeyConstraint(['sender_id'], ['user.id'], name=op.f('message_sender_id_fkey'), ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id', name=op.f('message_pkey'))
)
op.create_index(op.f('message_chat_id_idx'), 'message', ['chat_id'], unique=False)
op.create_index(op.f('message_id_idx'), 'message', ['id'], unique=False)
op.create_index(op.f('message_sender_id_idx'), 'message', ['sender_id'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('message_sender_id_idx'), table_name='message')
op.drop_index(op.f('message_id_idx'), table_name='message')
op.drop_index(op.f('message_chat_id_idx'), table_name='message')
op.drop_table('message')
op.drop_index(op.f('Participant_user_id_idx'), table_name='Participant')
op.drop_index(op.f('Participant_id_idx'), table_name='Participant')
op.drop_index(op.f('Participant_chat_id_idx'), table_name='Participant')
op.drop_table('Participant')
op.drop_index(op.f('user_username_idx'), table_name='user')
op.drop_index(op.f('user_id_idx'), table_name='user')
op.drop_index(op.f('user_email_idx'), table_name='user')
op.drop_table('user')
op.drop_index(op.f('chat_id_idx'), table_name='chat')
op.drop_table('chat')
# ### end Alembic commands ###
@@ -0,0 +1,32 @@
"""EDIT: chat tables
Revision ID: 7a5ccb6859fe
Revises: fd15ec3ae3fb
Create Date: 2026-01-12 14:51:11.514074
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '7a5ccb6859fe'
down_revision: Union[str, Sequence[str], None] = 'fd15ec3ae3fb'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
@@ -0,0 +1,32 @@
"""EDIT: chat tables
Revision ID: 7ad624ae1699
Revises: 7a5ccb6859fe
Create Date: 2026-01-12 14:54:04.459361
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '7ad624ae1699'
down_revision: Union[str, Sequence[str], None] = '7a5ccb6859fe'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
@@ -0,0 +1,53 @@
"""ADD: chat tables
Revision ID: fd15ec3ae3fb
Revises: 4d00c9b0516e
Create Date: 2026-01-11 21:54:51.418126
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = 'fd15ec3ae3fb'
down_revision: Union[str, Sequence[str], None] = '4d00c9b0516e'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('user_email_idx'), table_name='user')
op.drop_index(op.f('user_id_idx'), table_name='user')
op.drop_index(op.f('user_username_idx'), table_name='user')
op.drop_table('user')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('display_name', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('username', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('email', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('birth_day', sa.DATE(), autoincrement=False, nullable=True),
sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('avatar_url', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=False),
sa.Column('is_verified', sa.BOOLEAN(), autoincrement=False, nullable=False),
sa.Column('is_superuser', sa.BOOLEAN(), autoincrement=False, nullable=False),
sa.Column('created_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=False),
sa.Column('updated_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=False),
sa.Column('hashed_password', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('user_pkey'))
)
op.create_index(op.f('user_username_idx'), 'user', ['username'], unique=True)
op.create_index(op.f('user_id_idx'), 'user', ['id'], unique=False)
op.create_index(op.f('user_email_idx'), 'user', ['email'], unique=True)
# ### end Alembic commands ###
+4
View File
@@ -24,6 +24,10 @@ async def get_users(offset: int, limit: int, user: UserModel = Depends(get_curre
log.info("Getting users list", extra={"offset": offset, "limit": limit})
return await UserService.get_users_list(offset=offset, limit=limit)
@router.get("/{user_id}")
async def get_user(user_id: int, user: UserModel = Depends(get_current_verified_user)):
return await UserService.get_user(user_id)
@router.put("/me")
async def update_current_user(update_user: UserUpdate, user: UserModel = Depends(get_current_verified_user)) -> User:
return await UserService.update_user(user.id, update_user)
+3
View File
@@ -40,6 +40,7 @@ class User(UserBase):
is_verified: bool
is_superuser: bool
class UserCreateDB(UserBase):
email: Optional[str] = None
hashed_password: Optional[str] = None
@@ -47,6 +48,7 @@ class UserCreateDB(UserBase):
is_verified: Optional[bool] = None
is_superuser: Optional[bool] = None
class UserUpdateDB(UserBase):
email: Optional[str] = None
hashed_password: Optional[str] = None
@@ -56,6 +58,7 @@ class UserUpdateDB(UserBase):
is_verified: Optional[bool] = None
is_superuser: Optional[bool] = None
class ChangePassword(BaseModel):
old_password: str
new_password: str
+14 -3
View File
@@ -1,6 +1,6 @@
from typing import Dict, Optional
from fastapi import HTTPException, Request, status
from fastapi import HTTPException, Request, status, WebSocket
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
from fastapi.security import OAuth2
from fastapi.security.utils import get_authorization_scheme_param
@@ -20,8 +20,19 @@ class OAuth2PasswordBearerWithCookie(OAuth2):
password={"tokenUrl": tokenUrl, "scopes": scopes})
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
async def __call__(self, request: Request) -> Optional[str]:
authorization: str = request.cookies.get("access_token")
async def __call__(
self,
request: Request = None,
websocket: WebSocket = None
) -> Optional[str]:
connection = request or websocket
if connection is None:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="No connection found")
authorization: str = connection.cookies.get("access_token")
print(authorization)
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer":
-73
View File
@@ -1,73 +0,0 @@
services:
postgres-db:
image: postgres:latest
volumes:
- pgdata:/var/lib/postgresql
environment:
POSTGRES_DB: "${DB_NAME}"
POSTGRES_USER: "${DB_USER}"
POSTGRES_PASSWORD: "${DB_PASS}"
networks:
- aether-dev
ports:
- "${DB_PORT}:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 5s
retries: 5
restart: always
redis:
image: redis:latest
volumes:
- redis_data:/data
networks:
- aether-dev
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
retries: 5
restart: always
redis-insight:
image: redis/redisinsight:latest
container_name: redis_insight
ports:
- "5540:5540"
networks:
- aether-dev
depends_on:
- redis
maildev:
image: maildev/maildev
ports:
- "1080:1080"
- "${SMTP_PORT}:1025"
restart: unless-stopped
rabbitmq:
image: rabbitmq:3.8-management
hostname: rabbitmq
container_name: rabbitmq
ports:
- "${RMQ_PORT}:5672"
- "15672:15672"
networks:
- aether-dev
environment:
RABBITMQ_DEFAULT_USER: "${RMQ_USER}"
RABBITMQ_DEFAULT_PASS: "${RMQ_PASS}"
volumes:
- rabbitmq-data:/var/lib/rabbitmq
restart: unless-stopped
volumes:
pgdata:
redis_data:
rabbitmq-data:
networks:
aether-dev:
+64 -72
View File
@@ -3504,81 +3504,73 @@ files = [
[[package]]
name = "websockets"
version = "15.0.1"
version = "16.0"
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
optional = false
python-versions = ">=3.9"
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"},
{file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"},
{file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"},
{file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"},
{file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"},
{file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"},
{file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"},
{file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"},
{file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"},
{file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"},
{file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"},
{file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"},
{file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"},
{file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"},
{file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"},
{file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"},
{file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"},
{file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"},
{file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"},
{file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"},
{file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"},
{file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"},
{file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"},
{file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"},
{file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"},
{file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"},
{file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"},
{file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"},
{file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"},
{file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"},
{file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"},
{file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"},
{file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"},
{file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"},
{file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"},
{file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"},
{file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"},
{file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"},
{file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"},
{file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"},
{file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"},
{file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"},
{file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"},
{file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"},
{file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"},
{file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"},
{file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"},
{file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"},
{file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"},
{file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"},
{file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"},
{file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"},
{file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"},
{file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"},
{file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"},
{file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"},
{file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"},
{file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"},
{file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"},
{file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"},
{file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"},
{file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"},
{file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"},
{file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"},
{file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"},
{file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"},
{file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"},
{file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"},
{file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"},
{file = "websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a"},
{file = "websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0"},
{file = "websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957"},
{file = "websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72"},
{file = "websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde"},
{file = "websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3"},
{file = "websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3"},
{file = "websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9"},
{file = "websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35"},
{file = "websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8"},
{file = "websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad"},
{file = "websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d"},
{file = "websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe"},
{file = "websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b"},
{file = "websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5"},
{file = "websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64"},
{file = "websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6"},
{file = "websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac"},
{file = "websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00"},
{file = "websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79"},
{file = "websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39"},
{file = "websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c"},
{file = "websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f"},
{file = "websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1"},
{file = "websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2"},
{file = "websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89"},
{file = "websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea"},
{file = "websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9"},
{file = "websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230"},
{file = "websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c"},
{file = "websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5"},
{file = "websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82"},
{file = "websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8"},
{file = "websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f"},
{file = "websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a"},
{file = "websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156"},
{file = "websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0"},
{file = "websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904"},
{file = "websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4"},
{file = "websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e"},
{file = "websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4"},
{file = "websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1"},
{file = "websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3"},
{file = "websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8"},
{file = "websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d"},
{file = "websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244"},
{file = "websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e"},
{file = "websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641"},
{file = "websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8"},
{file = "websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e"},
{file = "websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944"},
{file = "websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206"},
{file = "websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6"},
{file = "websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd"},
{file = "websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d"},
{file = "websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03"},
{file = "websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da"},
{file = "websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c"},
{file = "websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767"},
{file = "websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec"},
{file = "websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5"},
]
[[package]]
@@ -3849,4 +3841,4 @@ propcache = ">=0.2.1"
[metadata]
lock-version = "2.1"
python-versions = ">=3.13"
content-hash = "0251ef60f483d739512c9ae09a00d85e239cc079667acae2be68da9a632a0ed5"
content-hash = "0bc3ba3c9bfed0b76ac6aa34c14882ea972d982a889c657d5c39d5c317df1775"
+9 -1
View File
@@ -21,10 +21,18 @@ dependencies = [
"bcrypt (<4.1)",
"pytest (>=9.0.2,<10.0.0)",
"aiobotocore (>=3.1.0,<4.0.0)",
"types-aiobotocore[essential] (>=3.1.0,<4.0.0)"
"types-aiobotocore[essential] (>=3.1.0,<4.0.0)",
"websockets (>=16.0,<17.0)"
]
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "aetherbackend"
version = "0.1.0"
description = ""
authors = ["stasstrochewskij@gmail.com"]
package-mode = false # <--- Добавь это