Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from config import settings
from endpoints import auth, health, user
from endpoints import article, auth, health, user
from fastapi import APIRouter, Depends, FastAPI
from fastapi.requests import Request
from fastapi.responses import Response
Expand All @@ -16,6 +16,7 @@
ROUTER.include_router(health.router, prefix="/health", tags=["health"])
ROUTER.include_router(user.router, prefix="/user", tags=["user"])
ROUTER.include_router(auth.router, prefix="/auth", tags=["auth"])
ROUTER.include_router(article.router, prefix="/article", tags=["article"])


# Startup event
Expand Down
3 changes: 2 additions & 1 deletion api/config.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import sys
from typing import Any, Dict, Optional

from dotenv import find_dotenv
from pydantic import BaseSettings, PostgresDsn, validator


class Settings(BaseSettings):
class Config:
env_file = ".env"
env_file = find_dotenv(usecwd=True)
env_file_encoding = "utf-8"

"""Application configuration"""
Expand Down
35 changes: 35 additions & 0 deletions api/endpoints/article.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import schemas
from deps import get_current_user, get_db
from fastapi import APIRouter, Depends, status
from models.article import Article
from repositories import article_repo
from sqlalchemy.ext.asyncio import AsyncSession

router = APIRouter()

CREATE_ARTICLE = {
201: {
"description": "Article created successfully",
"content": {"json": {"detail": "OK"}},
},
500: {
"description": "Internal server error",
"content": {"json": {"detail": "Internal server error"}},
},
}


@router.post(
"/",
status_code=status.HTTP_201_CREATED,
responses=CREATE_ARTICLE,
response_model=schemas.Article,
name="article:create_article",
)
async def create_article(
payload: schemas.ArticleCreate,
db: AsyncSession = Depends(get_db),
current_user: schemas.User = Depends(get_current_user),
):
article: Article = await article_repo.create(db, payload, owner_id=current_user.id)
return article
4 changes: 3 additions & 1 deletion api/migrate/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from sqlalchemy import engine_from_config, pool

from api.config import settings
from api.models.article import Article
from api.models.base import Base
from api.models.user import User

# this is the Alembic Config object, which provides
Expand All @@ -20,7 +22,7 @@
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = [User.__table__.metadata]
target_metadata = Base.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
Expand Down
39 changes: 39 additions & 0 deletions api/migrate/versions/5ed609dfd923_create_article_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""create article model

Revision ID: 5ed609dfd923
Revises: 8b70fd3f48fa
Create Date: 2023-03-30 17:44:18.720196

"""
import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "5ed609dfd923"
down_revision = "8b70fd3f48fa"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"article",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("deleted", sa.Boolean(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("update_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("title", sa.String(), nullable=False),
sa.Column("description", sa.String(), nullable=False),
sa.Column("body", sa.String(), nullable=False),
sa.Column("owner_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(["owner_id"], ["user.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("article")
# ### end Alembic commands ###
12 changes: 12 additions & 0 deletions api/models/article.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship

from .base import Base


class Article(Base):
title = Column(String, nullable=False)
description = Column(String, nullable=False)
body = Column(String, nullable=False)
owner_id = Column(Integer, ForeignKey("user.id", ondelete="CASCADE"))
owner = relationship("User", back_populates="articles")
2 changes: 2 additions & 0 deletions api/models/user.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from sqlalchemy import Column, String
from sqlalchemy.orm import relationship

from .base import Base

Expand All @@ -7,3 +8,4 @@ class User(Base):
name = Column(String, nullable=False)
email = Column(String, nullable=False, unique=True)
password = Column(String, nullable=False)
articles = relationship("Article", back_populates="owner")
3 changes: 3 additions & 0 deletions api/repositories/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from models.article import Article
from models.user import User

from .article import ArticleRepository
from .user import UserRepository

user_repo = UserRepository(User)
article_repo = ArticleRepository(Article)
15 changes: 15 additions & 0 deletions api/repositories/article.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from schemas.article import Article, ArticleCreate, ArticleUpdate
from sqlalchemy.ext.asyncio import AsyncSession

from .base import BaseRepository


class ArticleRepository(BaseRepository[Article, ArticleCreate, ArticleUpdate]):
async def create(
self, db: AsyncSession, article: ArticleCreate, owner_id: int
) -> Article:
db_obj = self.model(**article.dict(), owner_id=owner_id)
db.add(db_obj)
await db.commit()
await db.refresh(db_obj)
return db_obj
1 change: 1 addition & 0 deletions api/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .article import Article, ArticleCreate
from .auth import AuthToken, AuthTokenData
from .msg import Msg
from .user import User, UserCreate, UserUpdate, UserWithoutPassword
42 changes: 42 additions & 0 deletions api/schemas/article.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from pydantic import BaseModel


# Shared properties
class ArticleBase(BaseModel):
title: str
description: str
body: str


"""
CRUD schemas for Article
"""


# Properties to receive on article creation
class ArticleCreate(ArticleBase):
pass


# Properties to receive on article update
class ArticleUpdate(ArticleBase):
pass


"""
Database schemas for Article
"""


# Shared properties
class ArticleDB(ArticleBase):
id: int
owner_id: int

class Config:
orm_mode = True


# Properties to return to client
class Article(ArticleDB):
pass
11 changes: 8 additions & 3 deletions api/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from copy import deepcopy
from dataclasses import dataclass
from typing import Any, Callable, Dict, Mapping, Optional

Expand Down Expand Up @@ -41,12 +42,16 @@ async def __call__(
claims: Optional[Dict[str, Any]] = None,
assert_func: Callable = None,
data: Optional[Mapping[str, Any]] = None,
header: Optional[Mapping[str, str]] = None,
*args,
**kwargs,
):
header = self.header
if claims is not None:
header = {"Authorization": f"Bearer {create_access_token(claims=claims)}"}
if header is None:
header = deepcopy(self.header)
if claims is not None:
header = {
"Authorization": f"Bearer {create_access_token(claims=claims)}"
}

async with AsyncClient(app=app, base_url="https://localhost") as ac:
resp: Response = await ac.request(
Expand Down
40 changes: 40 additions & 0 deletions api/tests/test_article.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from app import APP
from fastapi import FastAPI
from tests import DEFAULT_USER, RequestBody, ResponseBody, assert_request

""" Test create article endpoint
@router post /article/
@status_code 201
@response_model schemas.Article
@name article:create_article
@dependencies get_current_user, get_db
"""


async def test_create_article_success(app: FastAPI):
req = RequestBody(
url=app.url_path_for(name="article:create_article"),
body={"title": "test", "description": "test", "body": "test"},
)
resp = ResponseBody(
status_code=201,
body={
"title": "test",
"description": "test",
"body": "test",
"id": 1,
"owner_id": DEFAULT_USER.id,
},
)
await assert_request(app=APP, method="POST", req_body=req, resp_body=resp)


async def test_create_article_auth_failed(app: FastAPI):
req = RequestBody(
url=app.url_path_for(name="article:create_article"),
body={"title": "test", "description": "test", "body": "test"},
)
resp = ResponseBody(status_code=401, body={"detail": "Not authenticated"})
await assert_request(
app=APP, method="POST", req_body=req, resp_body=resp, header={}
)