generated from pptx704/fastapi-template
Added recruitment task
This commit is contained in:
@ -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
125
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/<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.
|
||||
|
||||
65
alembic/versions/ffce71fc567e_initial_migrations.py
Normal file
65
alembic/versions/ffce71fc567e_initial_migrations.py
Normal 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 ###
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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}>"
|
||||
|
||||
@ -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
128
app/repositories/post.py
Normal 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")
|
||||
@ -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
61
app/routers/post.py
Normal 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)
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
3
main.py
3
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)
|
||||
|
||||
141
poetry.lock
generated
141
poetry.lock
generated
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
Reference in New Issue
Block a user