Skip to content

Commit d862d5a

Browse files
committed
feature: author models and routes added & book category and genre routes updated
1 parent 3309d5c commit d862d5a

File tree

11 files changed

+494
-8
lines changed

11 files changed

+494
-8
lines changed

migrations/env.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from pkg.config import Config
88
from src.auth.models import PasswordResetLog, TokenBlacklist, User
9+
from src.authors.models import Author
910
from src.books.models import BookCategory, BookGenre
1011
from src.profile.models import UserProfile
1112

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""authors
2+
3+
Revision ID: 4dbd54902428
4+
Revises: 80993e702d0c
5+
Create Date: 2024-10-11 06:08:03.193956
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
import sqlalchemy as sa
12+
from alembic import op
13+
from sqlalchemy.dialects import postgresql
14+
15+
revision: str = "4dbd54902428"
16+
down_revision: Union[str, None] = "80993e702d0c"
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
op.create_table(
23+
"authors",
24+
sa.Column("uid", sa.UUID(), nullable=False),
25+
sa.Column("first_name", sa.VARCHAR(), nullable=False),
26+
sa.Column("last_name", sa.VARCHAR(), nullable=False),
27+
sa.Column("pen_name", sa.VARCHAR(), nullable=True),
28+
sa.Column("nationality", sa.VARCHAR(), nullable=False),
29+
sa.Column("biography", sa.TEXT(), nullable=False),
30+
sa.Column(
31+
"profile_image",
32+
sa.VARCHAR(),
33+
server_default="https://api.dicebear.com/9.x/adventurer-neutral/png?seed=Adrian",
34+
nullable=True,
35+
),
36+
sa.Column("created_at", postgresql.TIMESTAMP(), nullable=False),
37+
sa.Column("updated_at", postgresql.TIMESTAMP(), nullable=False),
38+
sa.PrimaryKeyConstraint("uid"),
39+
)
40+
41+
42+
def downgrade() -> None:
43+
op.drop_table("authors")

pkg/tasks/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from .send_mail import send_email_task
2+
from .upload_image import upload_image_task

src/profile/tasks.py renamed to pkg/tasks/upload_image.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66

77

88
@celery_app.task
9-
def upload_user_avatar_image_task(
10-
image_content: bytes, filename: str, content_type: str
11-
):
9+
def upload_image_task(image_content: bytes, filename: str, content_type: str):
1210
file_data = BytesIO(image_content)
1311

1412
minio_client.put_object(

src/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from pkg.middleware import register_middleware
44
from src.auth.routes import auth_router
5+
from src.authors.routes import author_router
56
from src.books.routes import book_category_router, book_genre_router
67
from src.profile.routes import profile_router
78

@@ -47,3 +48,4 @@
4748
prefix=f"{version_prefix}/books/genre",
4849
tags=["book_genre"],
4950
)
51+
app.include_router(author_router, prefix=f"{version_prefix}/authors", tags=["authors"])

src/authors/models.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import uuid
2+
from datetime import datetime
3+
4+
import sqlalchemy.dialects.postgresql as pg
5+
from sqlmodel import Column, Field, SQLModel
6+
7+
8+
class Author(SQLModel, table=True):
9+
__tablename__ = "authors"
10+
11+
uid: uuid.UUID = Field(
12+
sa_column=Column(pg.UUID, nullable=False, primary_key=True, default=uuid.uuid4)
13+
)
14+
first_name: str = Field(sa_column=Column(pg.VARCHAR, nullable=False))
15+
last_name: str = Field(sa_column=Column(pg.VARCHAR, nullable=False))
16+
pen_name: str = Field(sa_column=Column(pg.VARCHAR, nullable=True))
17+
nationality: str = Field(sa_column=Column(pg.VARCHAR, nullable=False))
18+
biography: str = Field(sa_column=Column(pg.TEXT, nullable=False))
19+
profile_image: str = Field(
20+
sa_column=Column(
21+
pg.VARCHAR,
22+
nullable=True,
23+
server_default="https://api.dicebear.com/9.x/adventurer-neutral/png?seed=Adrian",
24+
)
25+
)
26+
created_at: datetime = Field(
27+
sa_column=Column(pg.TIMESTAMP, nullable=False, default=datetime.now)
28+
)
29+
updated_at: datetime = Field(
30+
sa_column=Column(
31+
pg.TIMESTAMP, nullable=False, default=datetime.now, onupdate=datetime.now
32+
)
33+
)

src/authors/routes.py

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import json
2+
import os
3+
4+
from fastapi import APIRouter, Depends, Request, status
5+
from fastapi.responses import JSONResponse
6+
from sqlmodel.ext.asyncio.session import AsyncSession
7+
8+
from pkg.config import Config
9+
from pkg.db import get_session
10+
from pkg.tasks import upload_image_task
11+
from pkg.utils import get_current_user_uid
12+
13+
from .schemas import AuthorCreateSchema, AuthorResponseSchema
14+
from .service import AuthorService
15+
16+
author_router = APIRouter()
17+
18+
author_service = AuthorService()
19+
20+
21+
@author_router.post("/create", status_code=status.HTTP_201_CREATED)
22+
async def create_author(
23+
author_data: AuthorCreateSchema,
24+
session: AsyncSession = Depends(get_session),
25+
user_uid: str = Depends(get_current_user_uid),
26+
):
27+
author = await author_service.get_author_by_name(
28+
author_data.first_name, author_data.last_name, session
29+
)
30+
if author:
31+
return JSONResponse(
32+
status_code=status.HTTP_400_BAD_REQUEST,
33+
content={"message": "Author already exists"},
34+
)
35+
36+
author = await author_service.get_author_by_pen_name(author_data.pen_name, session)
37+
if author:
38+
return JSONResponse(
39+
status_code=status.HTTP_400_BAD_REQUEST,
40+
content={"message": "Author already exists"},
41+
)
42+
43+
author = await author_service.create_author(author_data.model_dump(), session)
44+
45+
return JSONResponse(
46+
status_code=status.HTTP_201_CREATED,
47+
content={
48+
"message": "Author created successfully",
49+
"author": AuthorResponseSchema(
50+
**json.loads(author.model_dump_json())
51+
).model_dump(),
52+
},
53+
)
54+
55+
56+
@author_router.patch("/update/{author_uid}", status_code=status.HTTP_200_OK)
57+
async def update_author(
58+
author_uid: str,
59+
author_data: AuthorCreateSchema,
60+
session: AsyncSession = Depends(get_session),
61+
user_uid: str = Depends(get_current_user_uid),
62+
):
63+
author = await author_service.get_author_by_uid(author_uid, session)
64+
if not author:
65+
return JSONResponse(
66+
status_code=status.HTTP_404_NOT_FOUND,
67+
content={"message": "Author not found"},
68+
)
69+
70+
author = await author_service.update_author(
71+
author, author_data.model_dump(), session
72+
)
73+
74+
return JSONResponse(
75+
status_code=status.HTTP_200_OK,
76+
content={
77+
"message": "Author updated successfully",
78+
"author": AuthorResponseSchema(
79+
**json.loads(author.model_dump_json())
80+
).model_dump(),
81+
},
82+
)
83+
84+
85+
@author_router.patch(
86+
"/update/profile_image/{author_uid}", status_code=status.HTTP_200_OK
87+
)
88+
async def update_author_profile_image(
89+
author_uid: str,
90+
request: Request,
91+
session: AsyncSession = Depends(get_session),
92+
user_uid: str = Depends(get_current_user_uid),
93+
):
94+
author = await author_service.get_author_by_uid(author_uid, session)
95+
if not author:
96+
return JSONResponse(
97+
status_code=status.HTTP_404_NOT_FOUND,
98+
content={"message": "Author not found"},
99+
)
100+
101+
form = await request.form()
102+
profile_image = form.get("profile_image")
103+
104+
if not profile_image:
105+
return JSONResponse(
106+
status_code=status.HTTP_400_BAD_REQUEST,
107+
content={"message": "Profile image is required"},
108+
)
109+
110+
profile_image_content = await profile_image.read()
111+
profile_image_file_extension = os.path.splitext(profile_image.filename)[1]
112+
profile_image_file_name = (
113+
f"author_profile_images/{author_uid}{profile_image_file_extension}"
114+
)
115+
116+
upload_image_task.delay(
117+
profile_image_content,
118+
profile_image_file_name,
119+
profile_image.content_type,
120+
)
121+
122+
file_url = f"http://{Config.DOMAIN}/minio/storage/{Config.MINIO_STORAGE_BUCKET}/{profile_image_file_name}"
123+
await author_service.update_author_profile_image(author, file_url, session)
124+
125+
return JSONResponse(
126+
status_code=status.HTTP_200_OK,
127+
content={
128+
"message": "Author profile image updated successfully",
129+
"author": AuthorResponseSchema(
130+
**json.loads(author.model_dump_json())
131+
).model_dump(),
132+
},
133+
)
134+
135+
136+
@author_router.get("/list", status_code=status.HTTP_200_OK)
137+
async def list_authors(session: AsyncSession = Depends(get_session)):
138+
authors = await author_service.list_authors(session)
139+
140+
return JSONResponse(
141+
status_code=status.HTTP_200_OK,
142+
content={
143+
"message": "Authors retrieved successfully",
144+
"authors": [
145+
AuthorResponseSchema(
146+
**json.loads(author.model_dump_json())
147+
).model_dump()
148+
for author in authors
149+
],
150+
},
151+
)
152+
153+
154+
@author_router.get("/list/{nationality}", status_code=status.HTTP_200_OK)
155+
async def list_authors_by_nationality(
156+
nationality: str, session: AsyncSession = Depends(get_session)
157+
):
158+
authors = await author_service.list_authors_by_nationality(nationality, session)
159+
160+
return JSONResponse(
161+
status_code=status.HTTP_200_OK,
162+
content={
163+
"message": "Authors retrieved successfully",
164+
"authors": [
165+
AuthorResponseSchema(
166+
**json.loads(author.model_dump_json())
167+
).model_dump()
168+
for author in authors
169+
],
170+
},
171+
)
172+
173+
174+
@author_router.get("/get/pen_name/{pen_name}", status_code=status.HTTP_200_OK)
175+
async def get_author_by_pen_name(
176+
pen_name: str, session: AsyncSession = Depends(get_session)
177+
):
178+
author = await author_service.get_author_by_pen_name(pen_name, session)
179+
180+
if not author:
181+
return JSONResponse(
182+
status_code=status.HTTP_404_NOT_FOUND,
183+
content={"message": "Author not found"},
184+
)
185+
186+
return JSONResponse(
187+
status_code=status.HTTP_200_OK,
188+
content={
189+
"message": "Author found",
190+
"author": AuthorResponseSchema(
191+
**json.loads(author.model_dump_json())
192+
).model_dump(),
193+
},
194+
)
195+
196+
197+
@author_router.get("/get/uid/{author_uid}", status_code=status.HTTP_200_OK)
198+
async def get_author_by_uid(
199+
author_uid: str, session: AsyncSession = Depends(get_session)
200+
):
201+
author = await author_service.get_author_by_uid(author_uid, session)
202+
203+
if not author:
204+
return JSONResponse(
205+
status_code=status.HTTP_404_NOT_FOUND,
206+
content={"message": "Author not found"},
207+
)
208+
209+
return JSONResponse(
210+
status_code=status.HTTP_200_OK,
211+
content={
212+
"message": "Author found",
213+
"author": AuthorResponseSchema(
214+
**json.loads(author.model_dump_json())
215+
).model_dump(),
216+
},
217+
)

src/authors/schemas.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from pydantic import BaseModel, Field
2+
3+
4+
class AuthorCreateSchema(BaseModel):
5+
first_name: str = Field(max_length=50, description="The first name of the author.")
6+
last_name: str = Field(max_length=50, description="The last name of the author.")
7+
pen_name: str = Field(
8+
max_length=50,
9+
description="The pen name of the author, if they have one.",
10+
)
11+
nationality: str = Field(
12+
max_length=50, description="The country to which the author belongs."
13+
)
14+
biography: str = Field(description="A brief biography of the author.")
15+
16+
model_config = {
17+
"json_schema_extra": {
18+
"example": {
19+
"first_name": "Isaac",
20+
"last_name": "Asimov",
21+
"pen_name": "Paul French",
22+
"nationality": "Russian",
23+
"biography": "Isaac Asimov was a Russian-born American author, professor, and biochemist, best known for his works of science fiction and popular science.",
24+
}
25+
}
26+
}
27+
28+
29+
class AuthorResponseSchema(BaseModel):
30+
uid: str
31+
first_name: str
32+
last_name: str
33+
pen_name: str
34+
nationality: str
35+
biography: str
36+
profile_image: str
37+
created_at: str
38+
updated_at: str
39+
40+
model_config = {
41+
"json_schema_extra": {
42+
"example": {
43+
"uid": "123e4567-e89b-12d3-a456-426614174000",
44+
"first_name": "Isaac",
45+
"last_name": "Asimov",
46+
"pen_name": "Paul French",
47+
"nationality": "Russian",
48+
"biography": "Isaac Asimov was a Russian-born American author, professor, and biochemist, best known for his works of science fiction and popular science.",
49+
"profile_image": "https://example.com/profile-image.jpg",
50+
"created_at": "2021-07-01T12:00:00",
51+
"updated_at": "2021-07-01T12:00:00",
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)