feat: ability to add and view blogs for a user

This commit is contained in:
2025-06-24 18:54:48 -04:00
parent 07c0977aa7
commit 7da38ddd8c
26 changed files with 1553 additions and 142 deletions
+46
View File
@@ -0,0 +1,46 @@
"""empty message
Revision ID: 1331953dbdf0
Revises: f6c8aa750e08
Create Date: 2025-06-22 14:23:39.931696
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '1331953dbdf0'
down_revision: Union[str, None] = 'f6c8aa750e08'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_index(op.f('ix_users_id'), table_name='users')
op.drop_index(op.f('ix_users_username'), table_name='users')
op.drop_table('users')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('users',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('username', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('email', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('hashed_password', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('permissions', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False),
sa.Column('subscriber', sa.BOOLEAN(), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('users_pkey'))
)
op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
# ### end Alembic commands ###
+32
View File
@@ -0,0 +1,32 @@
"""empty message
Revision ID: 273690fd257d
Revises: aaeb70eb4cdb
Create Date: 2025-06-21 09:08:04.102896
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '273690fd257d'
down_revision: Union[str, None] = 'aaeb70eb4cdb'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
+32
View File
@@ -0,0 +1,32 @@
"""empty message
Revision ID: 3b78fe6cf60f
Revises: 1331953dbdf0
Create Date: 2025-06-22 14:24:52.883175
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '3b78fe6cf60f'
down_revision: Union[str, None] = '1331953dbdf0'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
+58
View File
@@ -0,0 +1,58 @@
"""empty message
Revision ID: 4a1e42eba1cf
Revises: b9dcd098debd
Create Date: 2025-06-21 08:55:29.395985
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '4a1e42eba1cf'
down_revision: Union[str, None] = 'b9dcd098debd'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_index(op.f('ix_users_id'), table_name='users')
op.drop_index(op.f('ix_users_username'), table_name='users')
op.drop_table('users')
op.drop_index(op.f('ix_items_id'), table_name='items')
op.drop_index(op.f('ix_items_name'), table_name='items')
op.drop_table('items')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('items',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column('body', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('items_pkey'))
)
op.create_index(op.f('ix_items_name'), 'items', ['name'], unique=False)
op.create_index(op.f('ix_items_id'), 'items', ['id'], unique=False)
op.create_table('users',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('username', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('email', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('hashed_password', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('permissions', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False),
sa.Column('subscriber', sa.BOOLEAN(), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('users_pkey'))
)
op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
# ### end Alembic commands ###
+32
View File
@@ -0,0 +1,32 @@
"""empty message
Revision ID: aaeb70eb4cdb
Revises: d4894b788937
Create Date: 2025-06-21 09:06:10.983927
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'aaeb70eb4cdb'
down_revision: Union[str, None] = 'd4894b788937'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
+28
View File
@@ -0,0 +1,28 @@
"""empty message
Revision ID: d4894b788937
Revises: 4a1e42eba1cf
Create Date: 2025-06-21 08:56:04.467650
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'd4894b788937'
down_revision: Union[str, None] = '4a1e42eba1cf'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass
+104
View File
@@ -0,0 +1,104 @@
"""empty message
Revision ID: f6c8aa750e08
Revises: 273690fd257d
Create Date: 2025-06-22 14:14:03.560792
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = "f6c8aa750e08"
down_revision: Union[str, None] = "273690fd257d"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_blogs_author_id"), table_name="blogs")
op.drop_index(op.f("ix_blogs_id"), table_name="blogs")
op.drop_index(op.f("ix_blogs_title"), table_name="blogs")
op.drop_table("blogs")
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"blogs",
sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column("title", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("author_id", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("description", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column(
"body",
postgresql.JSON(astext_type=sa.Text()),
autoincrement=False,
nullable=False,
),
sa.Column(
"created_at",
postgresql.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
autoincrement=False,
nullable=False,
),
sa.Column(
"updated_at",
postgresql.TIMESTAMP(timezone=True),
autoincrement=False,
nullable=True,
),
sa.Column(
"published_at",
postgresql.TIMESTAMP(timezone=True),
autoincrement=False,
nullable=True,
),
sa.Column("word_count", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("version", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("read_time", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("language", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column(
"tags",
postgresql.JSON(astext_type=sa.Text()),
autoincrement=False,
nullable=True,
),
sa.Column("view_count", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("like_count", sa.INTEGER(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(
["author_id"], ["users.id"], name=op.f("blogs_author_id_fkey")
),
sa.PrimaryKeyConstraint("id", name=op.f("blogs_pkey")),
)
op.create_index(op.f("ix_blogs_title"), "blogs", ["title"], unique=False)
op.create_index(op.f("ix_blogs_id"), "blogs", ["id"], unique=False)
op.create_index(op.f("ix_blogs_author_id"), "blogs", ["author_id"], unique=False)
op.create_table(
"users",
sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column("username", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("email", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("hashed_password", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column(
"permissions",
postgresql.JSON(astext_type=sa.Text()),
autoincrement=False,
nullable=False,
),
sa.Column("subscriber", sa.BOOLEAN(), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("users_pkey")),
)
op.create_index(op.f("ix_users_username"), "users", ["username"], unique=True)
op.create_index(op.f("ix_users_id"), "users", ["id"], unique=False)
op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True)
# ### end Alembic commands ###
+60 -11
View File
@@ -1,26 +1,75 @@
from sqlalchemy.orm import Session
from app import models, schemas
from app.utils import hash_password, verify_password
from typing import List, Optional
def get_item(db: Session, item_id: int):
return db.query(models.Item).filter(models.Item.id == item_id).first()
def get_blog(db: Session, blog_id: int) -> Optional[models.Blog]:
return db.query(models.Blog).filter(models.Blog.id == blog_id).first()
def get_items(db: Session, skip: int = 0, limit: int = 10):
return db.query(models.Item).offset(skip).limit(limit).all()
def get_blogs(
db: Session,
skip: int = 0,
limit: int = 10,
author_id: Optional[int] = None,
visibility: Optional[str] = None,
) -> List[models.Blog]:
q = db.query(models.Blog)
if author_id is not None:
q = q.filter(models.Blog.author_id == author_id)
if visibility is not None:
q = q.filter(models.Blog.visibility == visibility)
return q.offset(skip).limit(limit).all()
def create_item(db: Session, item: schemas.Item):
db_item = models.Item(**item.model_dump())
db.add(db_item)
def create_blog(db: Session, blog_in: schemas.BlogCreate) -> models.Blog:
db_blog = models.Blog(**blog_in.model_dump())
db.add(db_blog)
db.commit()
db.refresh(db_item)
return db_item
db.refresh(db_blog)
return db_blog
def delete_item(db: Session, item_id: int):
item = db.query(models.Item).filter(models.Item.id == item_id).first()
def update_blog(
db: Session, blog_id: int, blog_in: schemas.BlogUpdate
) -> Optional[models.Blog]:
db_blog = get_blog(db, blog_id)
if not db_blog:
return None
update_data = blog_in.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(db_blog, field, value)
db.commit()
db.refresh(db_blog)
return db_blog
def delete_blog(db: Session, blog_id: int) -> Optional[models.Blog]:
db_blog = get_blog(db, blog_id)
if not db_blog:
return None
db.delete(db_blog)
db.commit()
return db_blog
def increment_view_count(db: Session, blog_id: int) -> None:
db.query(models.Blog).filter(models.Blog.id == blog_id).update(
{"view_count": models.Blog.view_count + 1}
)
db.commit()
def add_like(db: Session, blog_id: int) -> None:
db.query(models.Blog).filter(models.Blog.id == blog_id).update(
{"like_count": models.Blog.like_count + 1}
)
db.commit()
def delete_blogs(db: Session, item_id: int):
item = db.query(models.Blog).filter(models.Blog.id == item_id).first()
if item:
db.delete(item)
db.commit()
+51 -21
View File
@@ -2,6 +2,7 @@ from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from typing import Optional
from app.utils import create_access_token
from . import schemas, crud
@@ -34,35 +35,64 @@ def health_check():
return {"Health": "Super Healthy!"}
@app.post("/items/", response_model=schemas.Item)
def create_item(item: schemas.Item, db: Session = Depends(get_db)):
return crud.create_item(db, item)
@app.post("/blogs/", response_model=schemas.Blog)
def create_blog(
blog: schemas.BlogCreate,
db: Session = Depends(get_db),
):
return crud.create_blog(db, blog)
@app.get("/items/", response_model=list[schemas.Item])
def read_items(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
return crud.get_items(db, skip, limit)
@app.get("/blogs/", response_model=list[schemas.Blog])
def read_blogs(
skip: int = 0,
limit: int = 10,
author_id: Optional[int] = None,
db: Session = Depends(get_db),
):
return crud.get_blogs(
db,
skip=skip,
limit=limit,
author_id=author_id,
)
@app.get("/items/{item_id}", response_model=schemas.Item)
def read_item(item_id: int, db: Session = Depends(get_db)):
db_item = crud.get_item(db, item_id)
if db_item is None:
raise HTTPException(status_code=404, detail="Item not found")
return db_item
@app.get("/blogs/{blog_id}", response_model=schemas.Blog)
def read_blog(
blog_id: int,
db: Session = Depends(get_db),
):
db_blog = crud.get_blog(db, blog_id)
if not db_blog:
raise HTTPException(status_code=404, detail="Blog not found")
return db_blog
@app.delete("/items/{item_id}", response_model=schemas.Item)
def delete_item(item_id: int, db: Session = Depends(get_db)):
item = crud.delete_item(db, item_id)
if item is None:
raise HTTPException(status_code=404, detail="Item not found")
return item
@app.put("/blogs/{blog_id}", response_model=schemas.Blog)
def update_blog(
blog_id: int,
blog_in: schemas.BlogUpdate,
db: Session = Depends(get_db),
):
updated = crud.update_blog(db, blog_id, blog_in)
if not updated:
raise HTTPException(status_code=404, detail="Blog not found")
return updated
@app.delete("/blogs/{blog_id}", response_model=schemas.Blog)
def delete_blog(
blog_id: int,
db: Session = Depends(get_db),
):
deleted = crud.delete_blog(db, blog_id)
if not deleted:
raise HTTPException(status_code=404, detail="Blog not found")
return deleted
# Users
@app.post("/login", response_model=schemas.Token)
def user_login(
form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)
@@ -75,7 +105,7 @@ def user_login(
headers={"WWW-Authenticate": "Bearer"},
)
access_token = create_access_token(data={"sub": user.username})
return {"access_token": access_token, "token_type": "bearer"}
return {"access_token": access_token, "token_type": "bearer", "user_id": user.id}
@app.post("/register", response_model=schemas.UserOut)
+30 -5
View File
@@ -1,14 +1,39 @@
from sqlalchemy import JSON, Boolean, Column, Integer, String
from sqlalchemy import (
JSON,
Boolean,
Column,
DateTime,
Integer,
String,
func,
)
from .database import Base
class Item(Base):
__tablename__ = "items"
class Blog(Base):
__tablename__ = "blogs"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
title = Column(String, index=True)
author_id = Column(Integer, nullable=False, index=True)
description = Column(String, nullable=True)
body = Column(JSON, nullable=False)
body = Column(String, nullable=False)
created_at = Column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
published_at = Column(DateTime(timezone=True), nullable=True)
# Document Meta Data
word_count = Column(Integer, nullable=True)
version = Column(Integer, nullable=True)
read_time = Column(Integer, nullable=True)
language = Column(String, nullable=True)
tags = Column(JSON, nullable=True)
# User Meta Data
view_count = Column(Integer, default=0, nullable=False)
like_count = Column(Integer, default=0, nullable=False)
class User(Base):
+48 -8
View File
@@ -1,17 +1,56 @@
from pydantic import BaseModel, EmailStr
from typing import Optional
from pydantic import AwareDatetime, BaseModel, EmailStr
# DB Schemas
class ItemBase(BaseModel):
name: str
description: str | None = None
class BlogBase(BaseModel):
title: str
author_id: int
description: Optional[str] = None
body: str
created_at: AwareDatetime
updated_at: AwareDatetime
published_at: AwareDatetime
# Document meta
word_count: Optional[int] = None
version: Optional[int] = None
read_time: Optional[int] = None
language: Optional[str] = None
tags: Optional[list[str]] = None
class Item(ItemBase):
class BlogCreate(BlogBase):
"""All fields required to create a new blog post."""
pass
class BlogUpdate(BaseModel):
"""All fields are optional for partial updates."""
title: Optional[str] = None
description: Optional[str] = None
body: Optional[str] = None
created_at: Optional[str] = None
updated_at: Optional[str] = None
published_at: Optional[str] = None
word_count: Optional[int] = None
version: Optional[int] = None
read_time: Optional[int] = None
language: Optional[str] = None
tags: Optional[list[str]] = None
view_count: Optional[int] = None
like_count: Optional[int] = None
class Blog(BlogBase):
"""Whats returned in responses."""
id: int
view_count: int
like_count: int
class Config:
from_attributes = True
@@ -41,6 +80,7 @@ class UserOut(UserBase):
class Token(BaseModel):
access_token: str
token_type: str
user_id: int
class TokenData(BaseModel):
+5
View File
@@ -17,3 +17,8 @@ dependencies = [
[tool.pyrefly]
project-includes = ["**/*"]
project-excludes = ["**/*venv/**/*"]
[dependency-groups]
dev = [
"sqlalchemy-stubs>=0.4",
]
+71 -18
View File
@@ -508,6 +508,47 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "mypy"
version = "1.16.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557, upload-time = "2025-06-16T16:37:21.421Z" },
{ url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921, upload-time = "2025-06-16T16:51:28.659Z" },
{ url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887, upload-time = "2025-06-16T16:50:53.627Z" },
{ url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658, upload-time = "2025-06-16T16:33:55.002Z" },
{ url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486, upload-time = "2025-06-16T16:37:03.301Z" },
{ url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482, upload-time = "2025-06-16T16:47:37.48Z" },
{ url = "https://files.pythonhosted.org/packages/b4/d6/39482e5fcc724c15bf6280ff5806548c7185e0c090712a3736ed4d07e8b7/mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d", size = 11066493, upload-time = "2025-06-16T16:47:01.683Z" },
{ url = "https://files.pythonhosted.org/packages/e6/e5/26c347890efc6b757f4d5bb83f4a0cf5958b8cf49c938ac99b8b72b420a6/mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9", size = 10081687, upload-time = "2025-06-16T16:48:19.367Z" },
{ url = "https://files.pythonhosted.org/packages/44/c7/b5cb264c97b86914487d6a24bd8688c0172e37ec0f43e93b9691cae9468b/mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79", size = 11839723, upload-time = "2025-06-16T16:49:20.912Z" },
{ url = "https://files.pythonhosted.org/packages/15/f8/491997a9b8a554204f834ed4816bda813aefda31cf873bb099deee3c9a99/mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15", size = 12722980, upload-time = "2025-06-16T16:37:40.929Z" },
{ url = "https://files.pythonhosted.org/packages/df/f0/2bd41e174b5fd93bc9de9a28e4fb673113633b8a7f3a607fa4a73595e468/mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd", size = 12903328, upload-time = "2025-06-16T16:34:35.099Z" },
{ url = "https://files.pythonhosted.org/packages/61/81/5572108a7bec2c46b8aff7e9b524f371fe6ab5efb534d38d6b37b5490da8/mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b", size = 9562321, upload-time = "2025-06-16T16:48:58.823Z" },
{ url = "https://files.pythonhosted.org/packages/28/e3/96964af4a75a949e67df4b95318fe2b7427ac8189bbc3ef28f92a1c5bc56/mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438", size = 11063480, upload-time = "2025-06-16T16:47:56.205Z" },
{ url = "https://files.pythonhosted.org/packages/f5/4d/cd1a42b8e5be278fab7010fb289d9307a63e07153f0ae1510a3d7b703193/mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536", size = 10090538, upload-time = "2025-06-16T16:46:43.92Z" },
{ url = "https://files.pythonhosted.org/packages/c9/4f/c3c6b4b66374b5f68bab07c8cabd63a049ff69796b844bc759a0ca99bb2a/mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f", size = 11836839, upload-time = "2025-06-16T16:36:28.039Z" },
{ url = "https://files.pythonhosted.org/packages/b4/7e/81ca3b074021ad9775e5cb97ebe0089c0f13684b066a750b7dc208438403/mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359", size = 12715634, upload-time = "2025-06-16T16:50:34.441Z" },
{ url = "https://files.pythonhosted.org/packages/e9/95/bdd40c8be346fa4c70edb4081d727a54d0a05382d84966869738cfa8a497/mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be", size = 12895584, upload-time = "2025-06-16T16:34:54.857Z" },
{ url = "https://files.pythonhosted.org/packages/5a/fd/d486a0827a1c597b3b48b1bdef47228a6e9ee8102ab8c28f944cb83b65dc/mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee", size = 9573886, upload-time = "2025-06-16T16:36:43.589Z" },
{ url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "passlib"
version = "1.7.4"
@@ -522,6 +563,15 @@ bcrypt = [
{ name = "bcrypt" },
]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
]
[[package]]
name = "psycopg2-binary"
version = "2.9.10"
@@ -672,22 +722,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
]
[[package]]
name = "pyrefly"
version = "0.18.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c1/25/a01a7c1dbad97d9108f0445a75b971170b5f748781864e01e942b572161d/pyrefly-0.18.1.tar.gz", hash = "sha256:e192edc0a4916b56bc89362d569956a93f6fdd2ed60c63cbc14ea86a8d7c072b", size = 1000813, upload-time = "2025-06-07T00:19:35.917Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/21/53/ae13e448e4fab6f6106ab3b690ea36ec6d33a824f10507e5cf63515c4420/pyrefly-0.18.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:311cd1855fb991dd79aee550c26be88457ea2e547bc08568a6481dfcb8f35a4a", size = 5619666, upload-time = "2025-06-07T00:19:19.911Z" },
{ url = "https://files.pythonhosted.org/packages/0e/78/8551414e7300d4937afd74af474435d20b6e42d33962700b2c2e02c5cf85/pyrefly-0.18.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3abb545a427f105f5c743a0aa8a4bd59a8481df31c1feb6c886336804ed58395", size = 5210517, upload-time = "2025-06-07T00:19:22.192Z" },
{ url = "https://files.pythonhosted.org/packages/23/52/edf7bda660cf7e6a7a25a75a067d46708ecf61ffa423aeb727476dcc85e8/pyrefly-0.18.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:443b4376a6c6f38cdab521e3268e3844e88c3f465eccfd757387725245640e01", size = 5415912, upload-time = "2025-06-07T00:19:24.024Z" },
{ url = "https://files.pythonhosted.org/packages/1d/4a/1b1c626a4d076c3c63d6f2fe80d6317dee61bf957766353aded6abcfb75e/pyrefly-0.18.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c5c2a3d458eae1a293ba1b3acae679ae611fd22ec486e095c02f828f2c60f06", size = 6086858, upload-time = "2025-06-07T00:19:25.944Z" },
{ url = "https://files.pythonhosted.org/packages/0f/35/995862038f8e708cab59f5f45fe830044bd85bcdd2fba330b08934e9bc2f/pyrefly-0.18.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe5801cf26fafe0cfcc99d89a2c7632bf8b1edfa05b142ad1ac18621a042de68", size = 5849624, upload-time = "2025-06-07T00:19:27.988Z" },
{ url = "https://files.pythonhosted.org/packages/15/cb/181aada3e76fc740a6bc628b597c092042b880285c3a2fd2a465e6dfffa5/pyrefly-0.18.1-py3-none-win32.whl", hash = "sha256:31e663ca9208cfb4021026784936cd21c4350235d51bbc3cbad7c0dc97649421", size = 5243708, upload-time = "2025-06-07T00:19:29.932Z" },
{ url = "https://files.pythonhosted.org/packages/d8/3e/d1ca8927cdde89e2f202ac7b734d7cbe6970405e4c34a0329dfead0ef5a8/pyrefly-0.18.1-py3-none-win_amd64.whl", hash = "sha256:bb7da09aadd6a53bb210b8eaa4ce86471fb27222e7d2c785bb32d02254f17a66", size = 5668996, upload-time = "2025-06-07T00:19:31.88Z" },
{ url = "https://files.pythonhosted.org/packages/15/a8/1aff0e1d26cee78fdadf86777a1ad5b305a7dec7b61e27b51d84e255f4d8/pyrefly-0.18.1-py3-none-win_arm64.whl", hash = "sha256:88220a9ff947c9f2c9ab81e06a5e334b0104b9987317e961219d218b367d9192", size = 5321319, upload-time = "2025-06-07T00:19:33.657Z" },
]
[[package]]
name = "python-dotenv"
version = "1.1.0"
@@ -809,12 +843,16 @@ dependencies = [
{ name = "fastapi", extra = ["standard"] },
{ name = "passlib", extra = ["bcrypt"] },
{ name = "psycopg2-binary" },
{ name = "pyrefly" },
{ name = "python-jose", extra = ["cryptography"] },
{ name = "sqlalchemy" },
{ name = "uvicorn", extra = ["standard"] },
]
[package.dev-dependencies]
dev = [
{ name = "sqlalchemy-stubs" },
]
[package.metadata]
requires-dist = [
{ name = "alembic", specifier = ">=1.16.1" },
@@ -822,12 +860,14 @@ requires-dist = [
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" },
{ name = "psycopg2-binary", specifier = ">=2.9.10" },
{ name = "pyrefly", specifier = ">=0.18.1" },
{ name = "python-jose", extras = ["cryptography"], specifier = ">=3.5.0" },
{ name = "sqlalchemy", specifier = ">=2.0.41" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.34.2" },
]
[package.metadata.requires-dev]
dev = [{ name = "sqlalchemy-stubs", specifier = ">=0.4" }]
[[package]]
name = "shellingham"
version = "1.5.4"
@@ -892,6 +932,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" },
]
[[package]]
name = "sqlalchemy-stubs"
version = "0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/81/60/db082788267740b17eac2c00666bbea1c8c5a94b569e8b1ea76b0cf42d57/sqlalchemy-stubs-0.4.tar.gz", hash = "sha256:c665d6dd4482ef642f01027fa06c3d5e91befabb219dc71fc2a09e7d7695f7ae", size = 70682, upload-time = "2021-01-12T14:02:04.438Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/ae/cb215ab25b76228bc90c90444b87e323ffba58c212321a53d5bc92903098/sqlalchemy_stubs-0.4-py3-none-any.whl", hash = "sha256:5eec7aa110adf9b957b631799a72fef396b23ff99fe296df726645d01e312aa5", size = 116067, upload-time = "2021-01-12T14:02:02.723Z" },
]
[[package]]
name = "starlette"
version = "0.46.2"