From 60c5ba5e70ca6857efce62bed2a7b49bc93b1967 Mon Sep 17 00:00:00 2001 From: pptx704 Date: Thu, 3 Jul 2025 10:01:56 +0300 Subject: [PATCH] Added recruitment task --- .env.example | 2 +- README.md | 125 ++++++++++------ .../ffce71fc567e_initial_migrations.py | 65 ++++++++ app/database.py | 4 +- app/models.py | 78 +++++++++- app/repositories/auth.py | 52 ++++++- app/repositories/post.py | 128 ++++++++++++++++ app/routers/auth.py | 30 +++- app/routers/post.py | 61 ++++++++ app/schemas.py | 56 ++++++- app/security.py | 26 +++- docker-compose.yml | 8 +- main.py | 3 +- poetry.lock | 141 +++++++++++++++++- pyproject.toml | 1 + 15 files changed, 705 insertions(+), 75 deletions(-) create mode 100644 alembic/versions/ffce71fc567e_initial_migrations.py create mode 100644 app/repositories/post.py create mode 100644 app/routers/post.py diff --git a/.env.example b/.env.example index f75adc9..970e604 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/README.md b/README.md index 34fe2e8..13411eb 100644 --- a/README.md +++ b/README.md @@ -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//.git - cd - ``` +### 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. diff --git a/alembic/versions/ffce71fc567e_initial_migrations.py b/alembic/versions/ffce71fc567e_initial_migrations.py new file mode 100644 index 0000000..fe17909 --- /dev/null +++ b/alembic/versions/ffce71fc567e_initial_migrations.py @@ -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 ### diff --git a/app/database.py b/app/database.py index 0524a07..f05f222 100644 --- a/app/database.py +++ b/app/database.py @@ -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) diff --git a/app/models.py b/app/models.py index 261bfae..dde467a 100644 --- a/app/models.py +++ b/app/models.py @@ -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"" + + +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"" + + +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"" diff --git a/app/repositories/auth.py b/app/repositories/auth.py index aa01473..4a4914a 100644 --- a/app/repositories/auth.py +++ b/app/repositories/auth.py @@ -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) diff --git a/app/repositories/post.py b/app/repositories/post.py new file mode 100644 index 0000000..350ca2e --- /dev/null +++ b/app/repositories/post.py @@ -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") diff --git a/app/routers/auth.py b/app/routers/auth.py index e828cda..0eacf51 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -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 diff --git a/app/routers/post.py b/app/routers/post.py new file mode 100644 index 0000000..84c6adc --- /dev/null +++ b/app/routers/post.py @@ -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) diff --git a/app/schemas.py b/app/schemas.py index b7ab9b1..b185742 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -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 diff --git a/app/security.py b/app/security.py index 2ac7831..ca1a0a3 100644 --- a/app/security.py +++ b/app/security.py @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 6984f70..41359fa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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} \ No newline at end of file + - POSTGRES_DB=${POSTGRES_DB} + ports: + - "5432:5432" diff --git a/main.py b/main.py index 7b90e72..e3423e8 100644 --- a/main.py +++ b/main.py @@ -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) diff --git a/poetry.lock b/poetry.lock index bf3cef1..206535f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index 3c38a0f..b2150a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"