Added recruitment task

This commit is contained in:
pptx704
2025-07-03 10:01:56 +03:00
parent bcb64faf74
commit 60c5ba5e70
15 changed files with 705 additions and 75 deletions

View File

@ -6,4 +6,4 @@ JWT_SECRET=
JWT_ALGORITHM=
JWT_EXPIRE_MINUTES=
SQLALCHEMY_DATABASE_URL=
SQLALCHEMY_DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB} # Update this

125
README.md
View File

@ -1,56 +1,89 @@
# Omukk FastAPI Template
# Omukk Python Software Engineer Recruitment Task
Template repository for all FastAPI backend projects developed by Omukk.
Omukk Backend Developer Assessment
## Components
- Basic FastAPI dependencies
- Docker and Docker Compose for easier testing
- Alembic configuration for database migrations
- Initial codebase to start from
Congratulations on progressing to the technical screening stage. This evaluation is designed to assess your proficiency in Python backend development with a focus on FastAPI implementation.
## Using this template
## Task Overview:
**Expected Duration**: 60-80 minutes using LLMs (maximum 2 hours)
1. **Create a new repository from this template**:
- In Omukk Repos, navigate to [this repository](https://git.omukk.dev/pptx704/fastapi-template)
- Click the "Use this template" button at the top of the page
- Choose "Create a new repository"
- Fill in your new repository details and click "Create Repository"
### Focus Areas
- FastAPI application development
- Python best practices
- SDK integration
- Using containers
- Problem-solving methodology
- Understanding REST principle
2. **Clone your new repository**:
```bash
git clone git@git.omukk.dev/<username>/<repo>.git
cd <repo>
```
### Evaluation Criteria:
- Code structure and maintainability
- Implementation efficiency
- Debugging capability
- Adherence to REST API conventions
- Documentation clarity
## Development and Testing
### Guidelines:
- You may reference documentation or LLMs as needed
- Containerization via Docker is required
- Include clear setup instructions in your submission
1. **Install dependencies**:
```
poetry install
poetry run pre-commit install
```
2. **Run database server**:
```bash
docker compose up db -d
```
3. **Run Dev Server**:
```bash
poetry run fastapi dev
```
## Introduction
Omukk is developing a social media platform. Currently, we have features for users to create/edit/delete posts and like/unlike posts. However, if you look into the API docs, you will see that the endpoints do not follow the REST principles.
4. **Stop Database Server**:
```bash
docker compose down db
```
After login, you can try to post something but you will see that you are not allowed to do so as you are not verified. But there is no verification process implemented. You can tinker `security.py` a bit to bypass this rule. Then you will see that users are allowed to post emptry strings.
## Development Rules
- Create a separate branch from `dev` and create a PR to `dev` after making changes
- Branch names must be meaningful. Check [docs](https://docs.omukk.dev/doc/repos-bvFEDvytPz) for more details
- Always run `black` and `isort` to maintain code consistency (this is done automatically using pre-commit hooks)-
```bash
poetry run isort app main.py
poetry run black app main.py
# Make sure to run isort first
```
As a Software Engineer at Omukk, your task is to fix all these issues and make the social media platform work as expected.
- Use static type checking using `mypy` if you feel like it (It is recommended but not mandatory). Static type checking might help you to identify critical bugs.
## Project Setup
- Clone this repository using `git clone https://git.omukk.dev/pptx704/python-hiring-task.git`.
- Copy the content of `.env.example` to `.env` and fill in the values.
You can then run `docker compose up` to start the project. The docs will be available at `http://localhost:8000/docs`.
Or you can run `docker compose up db` to start the database only and use poetry to run the backend on a non-containerized environment. In that case, you can reference the following commands-
```bash
docker compose up db -d
poetry install
poetry run alembic upgrade head
poetry run fastapi dev
```
Then you can access the API docs at `http://localhost:8000/docs`.
## Tasks
Your task is to fix all these issues and make the social media platform work as expected.
### Fix the endpoints
The current endpoints are not following the REST principle. For example, a user should be able to fetch all posts using `GET /posts/` and get a specific post using `GET /posts/{post_id}`. Similarly, editing and deleting a post should be done using `PUT /posts/{post_id}`.
Fix all the endpoints and add a new endpoint to fetch a specific post.
To toggle like for a post, you can use `POST /posts/{post_id}/like`.
### Fix post creation and editing
This task is fairly simple. You need to add a check so that users cannot post empty strings. Return a 400 error if the post is empty. This shall be done for both creation and editing.
### Implement the verification process
For this task, you need to implement two endpoints- one for sending a verification code to the user and another for verifying the code. For simplicity, let's assume that the verification code is a 6-digit number and you can directly go to the browser to verify. So the endpoints should be `POST /auth/verify` and `GET /auth/verify/{code}`.
To implement the verification mechanism, we will use Redis to store the verification code. When an unverified user makes a request to `POST /auth/verify`, you should generate a 6-digit code and store it in Redis with an expiration time of 10 minutes. You do not need to send an email to the user, rather just print the code to the console or send it as the API response. Use a key `verify:{user_id}` for the verification code. User can send as many post requests as needed and the code will be updated every time.
User then should be able to verify the code using `GET /auth/verify/{code}`. If the code is valid, you should set the user's `is_verified` field to `True` and return a success message. Upon successful verification, the redis entry should be deleted.
Since, we are using Redis, you need to have a container running for this. Update the `docker-compose.yml` file to include a Redis container. Additionally, update the `settings.py` and `.env.example` files to include the Redis connection details.
### Version Bump
When you are all done, go to `main.py` and bump the version to `0.1.0`.
## Submission
Create a repository on GitHub and push your changes to it. Fill in [this form](https://tally.so/r/w2Y9zD). Make sure that your repository is public so that we can review your changes.
## Additional Notes
- The tasks are kept simple and straightforward. Do not overcomplicate them (i.e. writing tests, optimizing DB queries, implementing rate limits etc.)
- Make sure that your code is formatted properly. We have included a pre-commit hook to format the code. You can run `poetry run pre-commit install` to install it. Or you can simply run `isort` and `black` to format your code. Totally upto you.
- Take advantage of LLMs for your own sake.
- All necessary dependencies are already installed for you. You can still install more if you want to. However, the `poetry.lock` file should be updated to reflect the changes (At Omukk, we use Poetry to manage dependencies. Plain`requirements.txt` or other dependency managers will not be accepted)
- If running your project needs some additional steps, update the `Dockerfile` to reflect the changes.
**Good Luck!**
Note: In case you need to contact us, please reach out to `rafeed@omukk.dev` via email.

View File

@ -0,0 +1,65 @@
"""Initial migrations
Revision ID: ffce71fc567e
Revises:
Create Date: 2025-07-03 08:51:38.752525
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ffce71fc567e'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('users',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('password_hash', sa.String(length=60), nullable=False),
sa.Column('is_verified', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
op.create_table('posts',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('user_id', sa.UUID(), nullable=False),
sa.Column('content', sa.String(length=512), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_posts_created_at'), 'posts', ['created_at'], unique=False)
op.create_index(op.f('ix_posts_id'), 'posts', ['id'], unique=False)
op.create_table('post_likes',
sa.Column('user_id', sa.UUID(), nullable=False),
sa.Column('post_id', sa.UUID(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('user_id', 'post_id')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('post_likes')
op.drop_index(op.f('ix_posts_id'), table_name='posts')
op.drop_index(op.f('ix_posts_created_at'), table_name='posts')
op.drop_table('posts')
op.drop_index(op.f('ix_users_id'), table_name='users')
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_table('users')
# ### end Alembic commands ###

View File

@ -2,9 +2,9 @@ from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from .const import SQLALCHEMY_DATABASE_URL
from app.settings import settings
engine = create_engine(SQLALCHEMY_DATABASE_URL)
engine = create_engine(settings.SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

View File

@ -5,13 +5,87 @@ from sqlalchemy import (
Column,
DateTime,
ForeignKey,
Integer,
PrimaryKeyConstraint,
String,
)
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from sqlalchemy.schema import Index
from sqlalchemy.sql import func
from app.database import Base
class User(Base):
__tablename__ = "users"
id = Column(
UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4
)
name = Column(String, nullable=False)
email = Column(String, unique=True, index=True, nullable=False)
password_hash = Column(String(60), nullable=False)
is_verified = Column(Boolean, default=False)
posts = relationship("Post", backref="user")
likes = relationship("Like", backref="user")
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
def __repr__(self):
return f"<User(id={self.id}, name={self.name}, email={self.email}, is_active={self.is_verified}>"
class Post(Base):
__tablename__ = "posts"
id = Column(
UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4
)
user_id = Column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
)
content = Column(String(512), nullable=False)
likes = relationship("Like", backref="post")
created_at = Column(
DateTime(timezone=True), server_default=func.now(), index=True
)
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
@property
def time(self):
if self.updated_at:
return self.updated_at
return self.created_at
def __repr__(self):
return f"<Post(id={self.id}, content={self.content[:10]}>"
class Like(Base):
__tablename__ = "post_likes"
user_id = Column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
)
post_id = Column(
UUID(as_uuid=True),
ForeignKey("posts.id", ondelete="CASCADE"),
nullable=False,
)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
__table_args__ = (PrimaryKeyConstraint("user_id", "post_id"),)
def __repr__(self):
return f"<Like(user_id={self.user_id}, post_id={self.post_id}>"

View File

@ -1,6 +1,52 @@
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from .. import schemas
from ..models import User
from ..security import create_jwt_token, get_password_hash, verify_password
from app import schemas
from app.models import User
from app.security import create_jwt_token, get_password_hash, verify_password
def register(
request: schemas.RegistrationRequest, db: Session
) -> schemas.BaseResponse:
if db.query(User).filter(User.email == request.email).first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered",
)
if request.password != request.confirm_password:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Passwords do not match",
)
user = User(
name=request.name,
email=request.email,
password_hash=get_password_hash(request.password),
)
db.add(user)
db.commit()
return schemas.BaseResponse(message="Registration successful")
def login(request: schemas.LoginRequest, db: Session):
user = db.query(User).filter(User.email == request.email).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
)
if not verify_password(request.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect password",
)
token = create_jwt_token({"user_id": str(user.id)})
return schemas.LoginResponse(token=token, verified=user.is_verified)

128
app/repositories/post.py Normal file
View File

@ -0,0 +1,128 @@
import uuid
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from app import schemas
from app.models import Like, Post, User
from app.security import create_jwt_token
def get_all_posts(user: schemas.User, db: Session) -> list[schemas.PostView]:
_posts = db.query(Post).order_by(Post.created_at.desc()).all()
posts = []
for post in _posts:
author = db.query(User).filter_by(id=post.user_id).first()
likes = db.query(Like).filter_by(post_id=post.id).count()
liked = (
db.query(Like).filter_by(post_id=post.id, user_id=user.id).first()
is not None
)
_post = schemas.PostView(
id=post.id,
content=post.content,
author=schemas.User.model_validate(author),
likes=likes,
liked=liked,
time=post.time,
)
posts.append(_post)
return posts
def create_post(
user: schemas.User, content: schemas.PostCreate, db: Session
) -> schemas.PostView:
new_post = Post(user_id=user.id, content=content.content)
db.add(new_post)
db.commit()
db.refresh(new_post)
post = schemas.PostView(
id=new_post.id,
content=new_post.content,
author=schemas.User.model_validate(user),
likes=0,
liked=False,
time=new_post.time,
)
return post
def edit_post(
user: schemas.User, info: schemas.PostEdit, db: Session
) -> schemas.PostView:
post = db.query(Post).filter_by(id=info.id).first()
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Post not found"
)
if post.user_id != user.id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized"
)
post.content = info.content
db.commit()
db.refresh(post)
return schemas.PostView(
id=post.id,
content=post.content,
author=schemas.User.mode(user),
likes=db.query(Like).filter_by(post_id=post.id).count(),
liked=False,
time=post.time,
)
def delete_post(
user: schemas.User, info: schemas.PostAction, db: Session
) -> schemas.BaseResponse:
post = db.query(Post).filter_by(id=info.id).first()
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Post not found"
)
if post.user_id != user.id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized"
)
db.delete(post)
db.commit()
return schemas.BaseResponse(message="Post deleted")
def toggle_like(
user: schemas.User, info: schemas.PostAction, db: Session
) -> schemas.BaseResponse:
post = db.query(Post).filter_by(id=info.id).first()
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Post not found"
)
if post.user_id == user.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Can't like own post",
)
like = db.query(Like).filter_by(post_id=post.id, user_id=user.id).first()
if like:
db.delete(like)
db.commit()
return schemas.BaseResponse(message="Like removed")
like = Like(post_id=post.id, user_id=user.id)
db.add(like)
db.commit()
return schemas.BaseResponse(message="Liked")

View File

@ -2,10 +2,30 @@ from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from app import database, schemas
from .. import schemas
from ..database import get_db
from ..repositories import auth
from ..security import get_user
from app.repositories import auth
from app.security import get_user
router = APIRouter(prefix="", tags=["auth"])
get_db = database.get_db
@router.post(
"/register",
response_model=schemas.BaseResponse,
status_code=status.HTTP_201_CREATED,
)
def register(
request: schemas.RegistrationRequest, db: Session = Depends(get_db)
):
return auth.register(request, db)
@router.post("/login", response_model=schemas.LoginResponse)
def login(request: schemas.LoginRequest, db: Session = Depends(get_db)):
return auth.login(request, db)
@router.get("/me", response_model=schemas.User)
def current_user(user: schemas.User = Depends(get_user)):
return user

61
app/routers/post.py Normal file
View File

@ -0,0 +1,61 @@
import uuid
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app import database, schemas
from app.repositories import auth, post
from app.security import get_user, get_user_strict
router = APIRouter(prefix="/post", tags=["posts"])
get_db = database.get_db
@router.get("/all", response_model=list[schemas.PostView])
def get_all_posts(
user: schemas.User = Depends(get_user), db: Session = Depends(get_db)
):
return post.get_all_posts(user, db)
@router.post(
"/create",
response_model=schemas.PostView,
status_code=status.HTTP_201_CREATED,
)
def create_post(
content: schemas.PostCreate,
user: schemas.User = Depends(get_user_strict),
db: Session = Depends(get_db),
):
return post.create_post(user, content, db)
@router.post("/edit", response_model=schemas.PostView)
def edit_post(
info: schemas.PostEdit,
user: schemas.User = Depends(get_user_strict),
db: Session = Depends(get_db),
):
return post.edit_post(user, info, db)
@router.post("/delete", response_model=schemas.BaseResponse)
def delete_post(
info: schemas.PostAction,
user: schemas.User = Depends(get_user_strict),
db: Session = Depends(get_db),
):
return post.delete_post(user, info, db)
@router.post(
"/like", response_model=schemas.BaseResponse, name="Like or unlike post"
)
def like_post(
info: schemas.PostAction,
user: schemas.User = Depends(get_user_strict),
db: Session = Depends(get_db),
):
return post.toggle_like(user, info, db)

View File

@ -1,17 +1,63 @@
import datetime
import enum
import uuid
from typing import List, Optional
from pydantic import BaseModel, EmailStr
from pydantic import BaseModel, ConfigDict, EmailStr
class BaseResponse(BaseModel):
message: str
# Auth schemas
class RegistrationRequest(BaseModel):
name: str
email: EmailStr
password: str
confirm_password: str
class LoginRequest(BaseModel):
email: EmailStr
password: str
class LoginResponse(BaseModel):
token: str
verified: bool
class UserUnverified(BaseModel):
id: uuid.UUID
name: str
class User(BaseModel):
id: uuid.UUID
name: str
email: EmailStr
is_verified: bool
class Config:
orm_mode = True
model_config = ConfigDict(from_attributes=True)
# Post schemas
class PostView(BaseModel):
id: uuid.UUID
content: str
author: User
likes: int
liked: bool
time: datetime.datetime
class PostCreate(BaseModel):
content: str
class PostEdit(BaseModel):
id: uuid.UUID
content: str
class PostAction(BaseModel):
id: uuid.UUID

View File

@ -1,9 +1,8 @@
import os
from datetime import UTC, datetime, timedelta
import jwt
from bcrypt import checkpw, gensalt, hashpw
from fastapi import HTTPException, Security
from fastapi import Depends, HTTPException, Security
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from app import schemas
@ -25,7 +24,7 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
def create_jwt_token(data: dict) -> str:
_ed = timedelta(minutes=settings.JWT_EXPIRE_MINUTES)
iat = datetime.now(UTC)
exp = datetime.now(UTC) + _ed
exp = iat + _ed
token_payload = data
token_payload.update({"iat": iat, "exp": exp})
@ -50,8 +49,18 @@ def get_user_from_token(token: str) -> schemas.User:
user_id = payload.get("user_id")
# Return user from database
...
if user_id is None:
raise HTTPException(
status_code=401, detail="Invalid authentication credentials"
)
with SessionLocal() as db:
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return schemas.User.model_validate(user)
def get_user(
@ -64,3 +73,10 @@ def get_user(
token = authorization.credentials
return get_user_from_token(token)
def get_user_strict(user: schemas.User = Depends(get_user)) -> schemas.User:
if not user.is_verified:
raise HTTPException(status_code=401, detail="User not verified")
return user

View File

@ -1,5 +1,3 @@
version: '3.1'
services:
backend:
image: fastapi
@ -12,7 +10,7 @@ services:
- JWT_SECRET=${JWT_SECRET}
- JWT_ALGORITHM=${JWT_ALGORITHM:-HS256}
- JWT_EXPIRE_MINUTES=${JWT_EXPIRE_MINUTES:-60}
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
- SQLALCHEMY_DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
- WAIT_HOSTS=db:5432
depends_on:
- db
@ -21,4 +19,6 @@ services:
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_DB=${POSTGRES_DB}
ports:
- "5432:5432"

View File

@ -1,7 +1,7 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.routers import auth
from app.routers import auth, post
app = FastAPI()
@ -19,3 +19,4 @@ async def index() -> str:
app.include_router(auth.router)
app.include_router(post.router)

141
poetry.lock generated
View File

@ -410,6 +410,125 @@ files = [
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
]
[[package]]
name = "hiredis"
version = "3.2.1"
description = "Python wrapper for hiredis"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "hiredis-3.2.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:add17efcbae46c5a6a13b244ff0b4a8fa079602ceb62290095c941b42e9d5dec"},
{file = "hiredis-3.2.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:5fe955cc4f66c57df1ae8e5caf4de2925d43b5efab4e40859662311d1bcc5f54"},
{file = "hiredis-3.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f9ad63cd9065820a43fb1efb8ed5ae85bb78f03ef5eb53f6bde47914708f5718"},
{file = "hiredis-3.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e7f9e5fdba08841d78d4e1450cae03a4dbed2eda8a4084673cafa5615ce24a"},
{file = "hiredis-3.2.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1dce2508eca5d4e47ef38bc7c0724cb45abcdb0089f95a2ef49baf52882979a8"},
{file = "hiredis-3.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:186428bf353e4819abae15aa2ad64c3f40499d596ede280fe328abb9e98e72ce"},
{file = "hiredis-3.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74f2500d90a0494843aba7abcdc3e77f859c502e0892112d708c02e1dcae8f90"},
{file = "hiredis-3.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32822a94d2fdd1da96c05b22fdeef6d145d8fdbd865ba2f273f45eb949e4a805"},
{file = "hiredis-3.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ead809fb08dd4fdb5b4b6e2999c834e78c3b0c450a07c3ed88983964432d0c64"},
{file = "hiredis-3.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b90fada20301c3a257e868dd6a4694febc089b2b6d893fa96a3fc6c1f9ab4340"},
{file = "hiredis-3.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6d8bff53f526da3d9db86c8668011e4f7ca2958ee3a46c648edab6fe2cd1e709"},
{file = "hiredis-3.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:043d929ae262d03e1db0f08616e14504a9119c1ff3de13d66f857d85cd45caff"},
{file = "hiredis-3.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8d470fef39d02dbe5c541ec345cc4ffd7d2baec7d6e59c92bd9d9545dc221829"},
{file = "hiredis-3.2.1-cp310-cp310-win32.whl", hash = "sha256:efa4c76c45cc8c42228c7989b279fa974580e053b5e6a4a834098b5324b9eafa"},
{file = "hiredis-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbac5ec3a620b095c46ef3a8f1f06da9c86c1cdc411d44a5f538876c39a2b321"},
{file = "hiredis-3.2.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:e4ae0be44cab5e74e6e4c4a93d04784629a45e781ff483b136cc9e1b9c23975c"},
{file = "hiredis-3.2.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:24647e84c9f552934eb60b7f3d2116f8b64a7020361da9369e558935ca45914d"},
{file = "hiredis-3.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6fb3e92d1172da8decc5f836bf8b528c0fc9b6d449f1353e79ceeb9dc1801132"},
{file = "hiredis-3.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38ba7a32e51e518b6b3e470142e52ed2674558e04d7d73d86eb19ebcb37d7d40"},
{file = "hiredis-3.2.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4fc632be73174891d6bb71480247e57b2fd8f572059f0a1153e4d0339e919779"},
{file = "hiredis-3.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f03e6839ff21379ad3c195e0700fc9c209e7f344946dea0f8a6d7b5137a2a141"},
{file = "hiredis-3.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99983873e37c71bb71deb544670ff4f9d6920dab272aaf52365606d87a4d6c73"},
{file = "hiredis-3.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffd982c419f48e3a57f592678c72474429465bb4bfc96472ec805f5d836523f0"},
{file = "hiredis-3.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc993f4aa4abc029347f309e722f122e05a3b8a0c279ae612849b5cc9dc69f2d"},
{file = "hiredis-3.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dde790d420081f18b5949227649ccb3ed991459df33279419a25fcae7f97cd92"},
{file = "hiredis-3.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b0c8cae7edbef860afcf3177b705aef43e10b5628f14d5baf0ec69668247d08d"},
{file = "hiredis-3.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e8a90eaca7e1ce7f175584f07a2cdbbcab13f4863f9f355d7895c4d28805f65b"},
{file = "hiredis-3.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:476031958fa44e245e803827e0787d49740daa4de708fe514370293ce519893a"},
{file = "hiredis-3.2.1-cp311-cp311-win32.whl", hash = "sha256:eb3f5df2a9593b4b4b676dce3cea53b9c6969fc372875188589ddf2bafc7f624"},
{file = "hiredis-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:1402e763d8a9fdfcc103bbf8b2913971c0a3f7b8a73deacbda3dfe5f3a9d1e0b"},
{file = "hiredis-3.2.1-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:3742d8b17e73c198cabeab11da35f2e2a81999d406f52c6275234592256bf8e8"},
{file = "hiredis-3.2.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9c2f3176fb617a79f6cccf22cb7d2715e590acb534af6a82b41f8196ad59375d"},
{file = "hiredis-3.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a8bd46189c7fa46174e02670dc44dfecb60f5bd4b67ed88cb050d8f1fd842f09"},
{file = "hiredis-3.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f86ee4488c8575b58139cdfdddeae17f91e9a893ffee20260822add443592e2f"},
{file = "hiredis-3.2.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3717832f4a557b2fe7060b9d4a7900e5de287a15595e398c3f04df69019ca69d"},
{file = "hiredis-3.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5cb12c21fb9e2403d28c4e6a38120164973342d34d08120f2d7009b66785644"},
{file = "hiredis-3.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:080fda1510bbd389af91f919c11a4f2aa4d92f0684afa4709236faa084a42cac"},
{file = "hiredis-3.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1252e10a1f3273d1c6bf2021e461652c2e11b05b83e0915d6eb540ec7539afe2"},
{file = "hiredis-3.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d9e320e99ab7d2a30dc91ff6f745ba38d39b23f43d345cdee9881329d7b511d6"},
{file = "hiredis-3.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:641668f385f16550fdd6fdc109b0af6988b94ba2acc06770a5e06a16e88f320c"},
{file = "hiredis-3.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1e1f44208c39d6c345ff451f82f21e9eeda6fe9af4ac65972cc3eeb58d41f7cb"},
{file = "hiredis-3.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f882a0d6415fffe1ffcb09e6281d0ba8b1ece470e866612bbb24425bf76cf397"},
{file = "hiredis-3.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4e78719a0730ebffe335528531d154bc8867a246418f74ecd88adbc4d938c49"},
{file = "hiredis-3.2.1-cp312-cp312-win32.whl", hash = "sha256:33c4604d9f79a13b84da79950a8255433fca7edaf292bbd3364fd620864ed7b2"},
{file = "hiredis-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7b9749375bf9d171aab8813694f379f2cff0330d7424000f5e92890ad4932dc9"},
{file = "hiredis-3.2.1-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:7cabf7f1f06be221e1cbed1f34f00891a7bdfad05b23e4d315007dd42148f3d4"},
{file = "hiredis-3.2.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:db85cb86f8114c314d0ec6d8de25b060a2590b4713135240d568da4f7dea97ac"},
{file = "hiredis-3.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c9a592a49b7b8497e4e62c3ff40700d0c7f1a42d145b71e3e23c385df573c964"},
{file = "hiredis-3.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0079ef1e03930b364556b78548e67236ab3def4e07e674f6adfc52944aa972dd"},
{file = "hiredis-3.2.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d6a290ed45d9c14f4c50b6bda07afb60f270c69b5cb626fd23a4c2fde9e3da1"},
{file = "hiredis-3.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79dd5fe8c0892769f82949adeb021342ca46871af26e26945eb55d044fcdf0d0"},
{file = "hiredis-3.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:998a82281a159f4aebbfd4fb45cfe24eb111145206df2951d95bc75327983b58"},
{file = "hiredis-3.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41fc3cd52368ffe7c8e489fb83af5e99f86008ed7f9d9ba33b35fec54f215c0a"},
{file = "hiredis-3.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8d10df3575ce09b0fa54b8582f57039dcbdafde5de698923a33f601d2e2a246c"},
{file = "hiredis-3.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1ab010d04be33735ad8e643a40af0d68a21d70a57b1d0bff9b6a66b28cca9dbf"},
{file = "hiredis-3.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ec3b5f9ea34f70aaba3e061cbe1fa3556fea401d41f5af321b13e326792f3017"},
{file = "hiredis-3.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:158dfb505fff6bffd17f823a56effc0c2a7a8bc4fb659d79a52782f22eefc697"},
{file = "hiredis-3.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d632cd0ddd7895081be76748e6fb9286f81d2a51c371b516541c6324f2fdac9"},
{file = "hiredis-3.2.1-cp313-cp313-win32.whl", hash = "sha256:e9726d03e7df068bf755f6d1ecc61f7fc35c6b20363c7b1b96f39a14083df940"},
{file = "hiredis-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:b5b1653ad7263a001f2e907e81a957d6087625f9700fa404f1a2268c0a4f9059"},
{file = "hiredis-3.2.1-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:ef27728a8ceaa038ef4b6efc0e4473b7643b5c873c2fff5475e2c8b9c8d2e0d5"},
{file = "hiredis-3.2.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:1039d8d2e1d2a1528ad9f9e289e8aa8eec9bf4b4759be4d453a2ab406a70a800"},
{file = "hiredis-3.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83a8cd0eb6e535c93aad9c21e3e85bcb7dd26d3ff9b8ab095287be86e8af2f59"},
{file = "hiredis-3.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6fc1e8f78bcdc7e25651b7d96d19b983b843b575904d96642f97ae157797ae4"},
{file = "hiredis-3.2.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ddfa9a10fda3bea985a3b371a64553731141aaa0a20cbcc62a0e659f05e6c01"},
{file = "hiredis-3.2.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e789ee008752b9be82a7bed82e36b62053c7cc06a0179a5a403ba5b2acba5bd8"},
{file = "hiredis-3.2.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bf271877947a0f3eb9dc331688404a2e4cc246bca61bc5a1e2d62da9a1caad8"},
{file = "hiredis-3.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9ad404fd0fdbdfe74e55ebb0592ab4169eecfe70ccf0db80eedc1d9943dd6d7"},
{file = "hiredis-3.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:979572c602bdea0c3df255545c8c257f2163dd6c10d1f172268ffa7a6e1287d6"},
{file = "hiredis-3.2.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f74e3d899be057fb00444ea5f7ae1d7389d393bddf0f3ed698997aa05563483b"},
{file = "hiredis-3.2.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:a015666d5fdc3ca704f68db9850d0272ddcfb27e9f26a593013383f565ed2ad7"},
{file = "hiredis-3.2.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:259a3389dfe3390e356c2796b6bc96a778695e9d7d40c82121096a6b8a2dd3c6"},
{file = "hiredis-3.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:39f469891d29f0522712265de76018ab83a64b85ac4b4f67e1f692cbd42a03f9"},
{file = "hiredis-3.2.1-cp38-cp38-win32.whl", hash = "sha256:73aa0508f26cd6cb4dfdbe189b28fb3162fd171532e526e90a802363b88027f8"},
{file = "hiredis-3.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:2b910f12d7bcaf5ffc056087fc7b2d23e688f166462c31b73a0799d12891378d"},
{file = "hiredis-3.2.1-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:523a241d9f268bc0c7306792f58f9c633185f939a19abc0356c55f078d3901c5"},
{file = "hiredis-3.2.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:fec453a038c262e18d7de4919220b2916e0b17d1eadd12e7a800f09f78f84f39"},
{file = "hiredis-3.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e75a49c5927453c316665cfa39f4274081d00ce69b137b393823eb90c66a8371"},
{file = "hiredis-3.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd974cbe8b3ae8d3e7f60675e6da10383da69f029147c2c93d1a7e44b36d1290"},
{file = "hiredis-3.2.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12d3b8fff9905e44f357417159d64138a32500dbd0d5cffaddbb2600d3ce33b1"},
{file = "hiredis-3.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e21985804a40cb91e69e35ae321eb4e3610cd61a2cbc0328ab73a245f608fa1c"},
{file = "hiredis-3.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e26e2b49a9569f44a2a2d743464ff0786b46fb1124ed33d2a1bd8b1c660c25b"},
{file = "hiredis-3.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ef1ebf9ee8e0b4a895b86a02a8b7e184b964c43758393532966ecb8a256f37c"},
{file = "hiredis-3.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c936b690dd31d7af74f707fc9003c500315b4c9ad70fa564aff73d1283b3b37a"},
{file = "hiredis-3.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4909666bcb73270bb806aa00d0eee9e81f7a1aca388aafb4ba7dfcf5d344d23a"},
{file = "hiredis-3.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d74a2ad25bc91ca9639e4485099852e6263b360b2c3650fdd3cc47762c5db3fa"},
{file = "hiredis-3.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:e99910088df446ee64d64b160835f592fb4d36189fcc948dd204e903d91fffa3"},
{file = "hiredis-3.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:54423bd7af93a773edc6f166341cfb0e5f35ef42ca07b93f568f672a6f445e40"},
{file = "hiredis-3.2.1-cp39-cp39-win32.whl", hash = "sha256:4a5365cb6d7be82d3c6d523b369bc0bc1a64987e88ed6ecfabadda2aa1cf4fa4"},
{file = "hiredis-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a2eb02b6aaf4f1425a408e892c0378ba6cb6b45b1412c30dd258df1322d88c0"},
{file = "hiredis-3.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:73913d2fa379e722d17ba52f21ce12dd578140941a08efd73e73b6fab1dea4d8"},
{file = "hiredis-3.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:15a3dff3eca31ecbf3d7d6d104cf1b318dc2b013bad3f4bdb2839cb9ea2e1584"},
{file = "hiredis-3.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c78258032c2f9fc6f39fee7b07882ce26de281e09178266ce535992572132d95"},
{file = "hiredis-3.2.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:578d6a881e64e46db065256355594e680202c3bacf3270be3140057171d2c23e"},
{file = "hiredis-3.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b7f34b170093c077c972b8cc0ceb15d8ff88ad0079751a8ae9733e94d77e733"},
{file = "hiredis-3.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:291a18b228fc90f6720d178de2fac46522082c96330b4cc2d3dd8cb2c1cb2815"},
{file = "hiredis-3.2.1-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f53d2af5a7cd33a4b4d7ba632dce80c17823df6814ef5a8d328ed44c815a68e7"},
{file = "hiredis-3.2.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:20bdf6dbdf77eb43b98bc53950f7711983042472199245d4c36448e6b4cb460f"},
{file = "hiredis-3.2.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f43e5c50d76da15118c72b757216cf26c643d55bb1b3c86cad1ae49173971780"},
{file = "hiredis-3.2.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e5bb5fe9834851d56c8543e52dcd2ac5275fb6772ebc97876e18c2e05a3300b"},
{file = "hiredis-3.2.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e348438b6452e3d14dddb95d071fe8eaf6f264f641cba999c10bf6359cf1d2"},
{file = "hiredis-3.2.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e305f6c63a2abcbde6ce28958de2bb4dd0fd34c6ab3bde5a4410befd5df8c6b2"},
{file = "hiredis-3.2.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:33f24b1152f684b54d6b9d09135d849a6df64b6982675e8cf972f8adfa2de9aa"},
{file = "hiredis-3.2.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:01dd8ea88bf8363751857ca2eb8f13faad0c7d57a6369663d4d1160f225ab449"},
{file = "hiredis-3.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b16946533535cbb5cc7d4b6fc009d32d22b0f9ac58e8eb6f144637b64f9a61d"},
{file = "hiredis-3.2.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9a03886cad1076e9f7e9e411c402826a8eac6f56ba426ee84b88e6515574b7b"},
{file = "hiredis-3.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a4f6340f1c378bce17c195d46288a796fcf213dd3e2a008c2c942b33ab58993"},
{file = "hiredis-3.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9d64ddf29016d34e7e3bc4b3d36ca9ac8a94f9b2c13ac4b9d8a486862d91b95c"},
{file = "hiredis-3.2.1.tar.gz", hash = "sha256:5a5f64479bf04dd829fe7029fad0ea043eac4023abc6e946668cbbec3493a78d"},
]
[[package]]
name = "httpcore"
version = "1.0.9"
@ -1217,6 +1336,26 @@ files = [
{file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
]
[[package]]
name = "redis"
version = "6.2.0"
description = "Python client for Redis database and key-value store"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "redis-6.2.0-py3-none-any.whl", hash = "sha256:c8ddf316ee0aab65f04a11229e94a64b2618451dab7a67cb2f77eb799d872d5e"},
{file = "redis-6.2.0.tar.gz", hash = "sha256:e821f129b75dde6cb99dd35e5c76e8c49512a5a0d8dfdc560b2fbd44b85ca977"},
]
[package.dependencies]
hiredis = {version = ">=3.2.0", optional = true, markers = "extra == \"hiredis\""}
[package.extras]
hiredis = ["hiredis (>=3.2.0)"]
jwt = ["pyjwt (>=2.9.0)"]
ocsp = ["cryptography (>=36.0.1)", "pyopenssl (>=20.0.1)", "requests (>=2.31.0)"]
[[package]]
name = "rich"
version = "14.0.0"
@ -1738,4 +1877,4 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = "^3.12"
content-hash = "a4aed51b84bde98c9b1f9d16fdd3402b4f85be562168a6cf4ad15e4cabe53b78"
content-hash = "23fabd3c47115e08dd261535a7335d9bd5b82e0c4568d83803ea5bf5e3833ad9"

View File

@ -22,6 +22,7 @@ pyjwt = "^2.10.1"
alembic = "^1.16.2"
python-dotenv = "^1.1.0"
psycopg2-binary = "^2.9.10"
redis = {version = "^6.2.0", extras = ["hiredis"]}
[tool.poetry.group.dev.dependencies]
pytest = "^8.4.1"