mirror of
https://github.com/lorsanstand/Aether.git
synced 2026-06-19 12:05:16 +03:00
Add websckets connection
This commit is contained in:
@@ -46,13 +46,15 @@ async def login(response: Response, credentials: OAuth2PasswordRequestForm = Dep
|
|||||||
'access_token',
|
'access_token',
|
||||||
token.access_token,
|
token.access_token,
|
||||||
max_age=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
max_age=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||||||
httponly=True
|
httponly=True,
|
||||||
|
samesite='lax'
|
||||||
)
|
)
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
'refresh_token',
|
'refresh_token',
|
||||||
str(token.refresh_token),
|
str(token.refresh_token),
|
||||||
max_age=settings.REFRESH_TOKEN_EXPIRE_DAYS * 30 * 24 * 60,
|
max_age=settings.REFRESH_TOKEN_EXPIRE_DAYS * 30 * 24 * 60,
|
||||||
httponly=True
|
httponly=True,
|
||||||
|
samesite='lax'
|
||||||
)
|
)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
from sqlalchemy import ForeignKey
|
from sqlalchemy import ForeignKey, UUID, UniqueConstraint
|
||||||
|
|
||||||
from app.core.database import Base
|
from app.core.database import Base
|
||||||
|
|
||||||
@@ -7,7 +9,28 @@ from app.core.database import Base
|
|||||||
class MessageModel(Base):
|
class MessageModel(Base):
|
||||||
__tablename__ = "message"
|
__tablename__ = "message"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
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="CASCADE"), index=True)
|
sender_id: Mapped[int] = mapped_column(ForeignKey("user.id", ondelete="SET NULL"), index=True)
|
||||||
recipient_id: Mapped[int] = mapped_column(ForeignKey("user.id", ondelete="CASCADE"), index=True)
|
chat_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey("chat.id", ondelete="CASCADE"), index=True)
|
||||||
content: Mapped[str] = mapped_column()
|
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"),
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
@@ -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]
|
||||||
@@ -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}")
|
||||||
@@ -3,6 +3,7 @@ from contextlib import asynccontextmanager
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from fastapi import FastAPI, APIRouter, Request, Response
|
from fastapi import FastAPI, APIRouter, Request, Response
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
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.core.redis import close_redis, init_redis
|
||||||
from app.users.router import router as user_router
|
from app.users.router import router as user_router
|
||||||
from app.auth.router import router as auth_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.log_config import set_logging
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
|
|
||||||
@@ -21,14 +24,19 @@ log = logging.getLogger(__name__)
|
|||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
await init_redis()
|
await init_redis()
|
||||||
log.info("Redis connected")
|
log.info("Redis connected")
|
||||||
|
task_send_message = asyncio.create_task(ChatService.message_listener())
|
||||||
|
log.info("Message sender started")
|
||||||
yield
|
yield
|
||||||
await close_redis()
|
await close_redis()
|
||||||
log.info("Redis disconnected")
|
log.info("Redis disconnected")
|
||||||
|
task_send_message.cancel()
|
||||||
|
log.info("Message sender stopped")
|
||||||
|
|
||||||
|
|
||||||
api_router = APIRouter(prefix="/api/v1")
|
api_router = APIRouter(prefix="/api/v1")
|
||||||
api_router.include_router(user_router)
|
api_router.include_router(user_router)
|
||||||
api_router.include_router(auth_router)
|
api_router.include_router(auth_router)
|
||||||
|
api_router.include_router(chat_router)
|
||||||
|
|
||||||
@api_router.get("/health")
|
@api_router.get("/health")
|
||||||
async def test_health():
|
async def test_health():
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from alembic import context
|
|||||||
|
|
||||||
from app.core.database import Base
|
from app.core.database import Base
|
||||||
from app.core.config import settings
|
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
|
# this is the Alembic Config object, which provides
|
||||||
# access to the values within the .ini file in use.
|
# 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 ###
|
||||||
@@ -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})
|
log.info("Getting users list", extra={"offset": offset, "limit": limit})
|
||||||
return await UserService.get_users_list(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")
|
@router.put("/me")
|
||||||
async def update_current_user(update_user: UserUpdate, user: UserModel = Depends(get_current_verified_user)) -> User:
|
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)
|
return await UserService.update_user(user.id, update_user)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class User(UserBase):
|
|||||||
is_verified: bool
|
is_verified: bool
|
||||||
is_superuser: bool
|
is_superuser: bool
|
||||||
|
|
||||||
|
|
||||||
class UserCreateDB(UserBase):
|
class UserCreateDB(UserBase):
|
||||||
email: Optional[str] = None
|
email: Optional[str] = None
|
||||||
hashed_password: Optional[str] = None
|
hashed_password: Optional[str] = None
|
||||||
@@ -47,6 +48,7 @@ class UserCreateDB(UserBase):
|
|||||||
is_verified: Optional[bool] = None
|
is_verified: Optional[bool] = None
|
||||||
is_superuser: Optional[bool] = None
|
is_superuser: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
class UserUpdateDB(UserBase):
|
class UserUpdateDB(UserBase):
|
||||||
email: Optional[str] = None
|
email: Optional[str] = None
|
||||||
hashed_password: Optional[str] = None
|
hashed_password: Optional[str] = None
|
||||||
@@ -56,6 +58,7 @@ class UserUpdateDB(UserBase):
|
|||||||
is_verified: Optional[bool] = None
|
is_verified: Optional[bool] = None
|
||||||
is_superuser: Optional[bool] = None
|
is_superuser: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
class ChangePassword(BaseModel):
|
class ChangePassword(BaseModel):
|
||||||
old_password: str
|
old_password: str
|
||||||
new_password: str
|
new_password: str
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Dict, Optional
|
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.openapi.models import OAuthFlows as OAuthFlowsModel
|
||||||
from fastapi.security import OAuth2
|
from fastapi.security import OAuth2
|
||||||
from fastapi.security.utils import get_authorization_scheme_param
|
from fastapi.security.utils import get_authorization_scheme_param
|
||||||
@@ -20,8 +20,19 @@ class OAuth2PasswordBearerWithCookie(OAuth2):
|
|||||||
password={"tokenUrl": tokenUrl, "scopes": scopes})
|
password={"tokenUrl": tokenUrl, "scopes": scopes})
|
||||||
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
|
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)
|
scheme, param = get_authorization_scheme_param(authorization)
|
||||||
if not authorization or scheme.lower() != "bearer":
|
if not authorization or scheme.lower() != "bearer":
|
||||||
|
|||||||
Generated
+64
-72
@@ -3504,81 +3504,73 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "websockets"
|
name = "websockets"
|
||||||
version = "15.0.1"
|
version = "16.0"
|
||||||
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
|
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.10"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"},
|
{file = "websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a"},
|
||||||
{file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"},
|
{file = "websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0"},
|
||||||
{file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"},
|
{file = "websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957"},
|
||||||
{file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"},
|
{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-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"},
|
{file = "websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde"},
|
||||||
{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-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3"},
|
||||||
{file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"},
|
{file = "websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3"},
|
||||||
{file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"},
|
{file = "websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9"},
|
||||||
{file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"},
|
{file = "websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35"},
|
||||||
{file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"},
|
{file = "websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8"},
|
||||||
{file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"},
|
{file = "websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad"},
|
||||||
{file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"},
|
{file = "websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d"},
|
||||||
{file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"},
|
{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-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"},
|
{file = "websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b"},
|
||||||
{file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"},
|
{file = "websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5"},
|
||||||
{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-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64"},
|
||||||
{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-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6"},
|
||||||
{file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"},
|
{file = "websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac"},
|
||||||
{file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"},
|
{file = "websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00"},
|
||||||
{file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"},
|
{file = "websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79"},
|
||||||
{file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"},
|
{file = "websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39"},
|
||||||
{file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"},
|
{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-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"},
|
{file = "websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f"},
|
||||||
{file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"},
|
{file = "websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1"},
|
||||||
{file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"},
|
{file = "websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2"},
|
||||||
{file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"},
|
{file = "websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89"},
|
||||||
{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-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea"},
|
||||||
{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-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9"},
|
||||||
{file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"},
|
{file = "websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230"},
|
||||||
{file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"},
|
{file = "websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c"},
|
||||||
{file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"},
|
{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-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"},
|
{file = "websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82"},
|
||||||
{file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"},
|
{file = "websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8"},
|
||||||
{file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"},
|
{file = "websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f"},
|
||||||
{file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"},
|
{file = "websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a"},
|
||||||
{file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"},
|
{file = "websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156"},
|
||||||
{file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"},
|
{file = "websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0"},
|
||||||
{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-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904"},
|
||||||
{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-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4"},
|
||||||
{file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"},
|
{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-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"},
|
{file = "websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4"},
|
||||||
{file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"},
|
{file = "websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1"},
|
||||||
{file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"},
|
{file = "websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3"},
|
||||||
{file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"},
|
{file = "websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8"},
|
||||||
{file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"},
|
{file = "websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d"},
|
||||||
{file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"},
|
{file = "websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244"},
|
||||||
{file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"},
|
{file = "websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e"},
|
||||||
{file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"},
|
{file = "websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641"},
|
||||||
{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-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8"},
|
||||||
{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-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e"},
|
||||||
{file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"},
|
{file = "websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944"},
|
||||||
{file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"},
|
{file = "websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206"},
|
||||||
{file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"},
|
{file = "websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6"},
|
||||||
{file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"},
|
{file = "websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd"},
|
||||||
{file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"},
|
{file = "websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d"},
|
||||||
{file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"},
|
{file = "websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03"},
|
||||||
{file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"},
|
{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-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"},
|
{file = "websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c"},
|
||||||
{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-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767"},
|
||||||
{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-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec"},
|
||||||
{file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"},
|
{file = "websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5"},
|
||||||
{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"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3849,4 +3841,4 @@ propcache = ">=0.2.1"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.13"
|
python-versions = ">=3.13"
|
||||||
content-hash = "0251ef60f483d739512c9ae09a00d85e239cc079667acae2be68da9a632a0ed5"
|
content-hash = "0bc3ba3c9bfed0b76ac6aa34c14882ea972d982a889c657d5c39d5c317df1775"
|
||||||
|
|||||||
@@ -21,10 +21,18 @@ dependencies = [
|
|||||||
"bcrypt (<4.1)",
|
"bcrypt (<4.1)",
|
||||||
"pytest (>=9.0.2,<10.0.0)",
|
"pytest (>=9.0.2,<10.0.0)",
|
||||||
"aiobotocore (>=3.1.0,<4.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]
|
[build-system]
|
||||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.poetry]
|
||||||
|
name = "aetherbackend"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = ["stasstrochewskij@gmail.com"]
|
||||||
|
package-mode = false # <--- Добавь это
|
||||||
|
|||||||
Reference in New Issue
Block a user