diff --git a/app/api/routes/api.py b/app/api/routes/api.py index c3807f7..59a0e44 100644 --- a/app/api/routes/api.py +++ b/app/api/routes/api.py @@ -1,7 +1,9 @@ from fastapi import APIRouter from app.api.routes import users +from app.api.routes import markets router = APIRouter() router.include_router(users.router, tags=["users"], prefix="/user") +router.include_router(markets.router, tags=["markets"], prefix="/market") diff --git a/app/api/routes/markets.py b/app/api/routes/markets.py new file mode 100644 index 0000000..10733ba --- /dev/null +++ b/app/api/routes/markets.py @@ -0,0 +1,32 @@ +import logging +from typing import Any, List +from fastapi import APIRouter, Depends, HTTPException, UploadFile +from sqlalchemy.orm import Session +from app.api.dependencies import database +from app.models.domain import markets +from app.crud import crud_markets, crud_posts + +router = APIRouter() + + +@router.post("/create") +def create_market_posts( + *, + db: Session = Depends(database.get_db), + files: List[UploadFile], + market_in: markets.MarketCreate = Depends() +) -> Any: + """ + Create new user. + """ + # post data create + post_data = crud_posts.create(db=db, obj_in=market_in, files=files) + # room data create + market_data = crud_markets.create(db=db, obj_in=market_in, post_id=post_data.id) + if market_data and post_data: + return {"message": "create success"} + else: + raise HTTPException( + status_code=500, + detail="create failed", + ) diff --git a/app/core/config.py b/app/core/config.py index 011694b..611211f 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,6 +1,8 @@ import logging import sys from typing import List, Any + +import boto3 from pydantic_settings import BaseSettings from loguru import logger from starlette.config import Config @@ -11,7 +13,7 @@ class Settings(BaseSettings): config: Config = Config(".env") - API_PREFIX: str = "/api" + API_PREFIX: str = "/api/v1" JWT_TOKEN_PREFIX: str = "Token" VERSION: str = "1.0.0" @@ -29,6 +31,9 @@ class Settings(BaseSettings): ALLOWED_HOSTS: List[str] = config( "ALLOWED_HOSTS", cast=CommaSeparatedStrings, default="" ) + BUCKET_NAME: str = config("BUCKET_NAME", cast=str) + AWS_ACCESS_KEY: str = config("AWS_ACCESS_KEY", cast=str) + AWS_SECRET_KEY: str = config("AWS_SECRET_KEY", cast=str) # logging configuration diff --git a/app/crud/__init__.py b/app/crud/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/crud/crud_markets.py b/app/crud/crud_markets.py new file mode 100644 index 0000000..fbde1b3 --- /dev/null +++ b/app/crud/crud_markets.py @@ -0,0 +1,26 @@ +from sqlalchemy.orm import Session +from app.models.domain import markets +from app.models.schemas.markets import Market + + +def create(db: Session, *, obj_in: markets.MarketCreate, post_id: int) -> Market: + db_obj = None + if obj_in.starting_price: + db_obj = Market( + starting_price=obj_in.starting_price, + price=obj_in.price, + auction=obj_in.auction, + deadline=obj_in.deadline, + post_id=post_id, + ) + else: + db_obj = Market( + price=obj_in.price, + auction=obj_in.auction, + deadline=obj_in.deadline, + post_id=post_id, + ) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj diff --git a/app/crud/crud_posts.py b/app/crud/crud_posts.py new file mode 100644 index 0000000..1814c4e --- /dev/null +++ b/app/crud/crud_posts.py @@ -0,0 +1,34 @@ +from sqlalchemy.orm import Session +from app.models.domain import posts +from app.models.schemas.posts import Post +from app.services.aws import s3_upload +from fastapi import HTTPException +from fastapi import UploadFile +from typing import List +import datetime + + +def create(db: Session, *, obj_in: posts.PostCreate, files: List[UploadFile]) -> Post: + db_obj = Post( + title=obj_in.title, + content=obj_in.content, + status=obj_in.status, + category=obj_in.category, + created_at=datetime.datetime.today(), + ) + # image bool value insert + db_obj.image = True if files else False + db.add(db_obj) + db.commit() + db.refresh(db_obj) + # if image exits, do uploading in s3 + if files: + s3_result = s3_upload( + files=files, post_id=db_obj.id, user_email="sumink0903@gmail.com" + ) + if not s3_result: + raise HTTPException( + status_code=500, + detail="s3 upload failed", + ) + return db_obj diff --git a/app/db/base.py b/app/db/base.py index 61ae182..4ef39c0 100644 --- a/app/db/base.py +++ b/app/db/base.py @@ -2,3 +2,5 @@ # imported by Alembic from app.db.base_class import Base # noqa from app.models.schemas.users import User # noqa +from app.models.schemas.posts import Post # noqa +from app.models.schemas.markets import Market # noqa diff --git a/app/models/domain/markets.py b/app/models/domain/markets.py new file mode 100644 index 0000000..02c2e58 --- /dev/null +++ b/app/models/domain/markets.py @@ -0,0 +1,16 @@ +from typing import Optional +from pydantic import BaseModel +import datetime +from app.resources.status import Status + + +class MarketCreate(BaseModel): + post_id: Optional[int] = None + starting_price: Optional[int] = None + price: int + auction: bool + deadline: datetime.datetime + title: str + content: str + status: Status + category: str diff --git a/app/models/domain/posts.py b/app/models/domain/posts.py new file mode 100644 index 0000000..1030baa --- /dev/null +++ b/app/models/domain/posts.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel +from typing import Optional +from app.resources.status import Status + + +class PostCreate(BaseModel): + title: str + content: str + status: Status + category: str + user_id: Optional[int] = None diff --git a/app/models/schemas/markets.py b/app/models/schemas/markets.py new file mode 100644 index 0000000..466e712 --- /dev/null +++ b/app/models/schemas/markets.py @@ -0,0 +1,17 @@ +from sqlalchemy import Boolean, Column, Integer, DateTime, ForeignKey +from sqlalchemy.orm import relationship +from typing import TYPE_CHECKING +from app.db.base_class import Base + +if TYPE_CHECKING: + from .posts import Post # noqa: F401 + + +class Market(Base): + id = Column(Integer, primary_key=True, autoincrement=True, index=True) + starting_price = Column(Integer) + price = Column(Integer, nullable=False) + auction = Column(Boolean(), default=False) + deadline = Column(DateTime, nullable=False) + post_id = Column(Integer, ForeignKey("post.id")) + post = relationship("Post", back_populates="market") diff --git a/app/models/schemas/posts.py b/app/models/schemas/posts.py new file mode 100644 index 0000000..affb17b --- /dev/null +++ b/app/models/schemas/posts.py @@ -0,0 +1,23 @@ +from sqlalchemy import Boolean, Column, Integer, String, DateTime, ForeignKey, Enum +from sqlalchemy.orm import relationship +from app.db.base_class import Base +from typing import TYPE_CHECKING +from app.resources.status import Status + +if TYPE_CHECKING: + from .users import User # noqa: F401 + from .markets import Market # noqa: F401 + + +class Post(Base): + id = Column(Integer, primary_key=True, autoincrement=True, index=True) + title = Column(String(255), index=True) + content = Column(String(255), index=True) + user_id = Column(Integer) + user_id = Column(Integer, ForeignKey("user.id")) + user = relationship("User", back_populates="posts") + status = Column(Enum(Status), nullable=False, default=Status.PROCESSING) + created_at = Column(DateTime, nullable=False) + category = Column(String(255), index=True) + image = Column(Boolean(), default=False) + market = relationship("Market", back_populates="post") diff --git a/app/models/schemas/users.py b/app/models/schemas/users.py index 7da74be..9e85e7a 100644 --- a/app/models/schemas/users.py +++ b/app/models/schemas/users.py @@ -1,9 +1,14 @@ -from sqlalchemy import Boolean, Column, Integer, String - +from sqlalchemy import Column, Integer, String +from sqlalchemy.orm import relationship from app.db.base_class import Base +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .posts import Post # noqa: F401 class User(Base): id = Column(Integer, primary_key=True, autoincrement=True, index=True) username = Column(String(255), index=True) email = Column(String(255), unique=True, index=True, nullable=False) + posts = relationship("Post", back_populates="user") diff --git a/app/resources/status.py b/app/resources/status.py new file mode 100644 index 0000000..02e6f0e --- /dev/null +++ b/app/resources/status.py @@ -0,0 +1,6 @@ +import enum + + +class Status(enum.Enum): + COMPLETED = "COMPLETED" + PROCESSING = "PROCESSING" diff --git a/app/services/aws.py b/app/services/aws.py new file mode 100644 index 0000000..47d0dd3 --- /dev/null +++ b/app/services/aws.py @@ -0,0 +1,26 @@ +import boto3 +from botocore.exceptions import ClientError +from fastapi import UploadFile +from app.core.config import settings +from typing import List + + +def s3_upload(files: List[UploadFile], post_id: int, user_email: str) -> bool: + client = boto3.client( + "s3", + region_name="us-east-1", + aws_access_key_id=settings.AWS_ACCESS_KEY, + aws_secret_access_key=settings.AWS_SECRET_KEY, + ) + try: + for file in files: + new_filename = f"/{user_email}/{post_id}/{file.filename}" + client.upload_fileobj( + file.file, + settings.BUCKET_NAME, + new_filename, + ExtraArgs={"ContentType": file.content_type}, + ) + except ClientError as e: + return False + return True diff --git a/docker-compose.yml b/docker-compose.yml index cc589f2..d26a21a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,8 @@ services: app: - build: . + build: + no_cache: true + context: . ports: - "8000:8000" env_file: diff --git a/requirements.txt b/requirements.txt index e165500..081e7aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,8 @@ anyio==3.7.1 astroid==3.0.1 bcrypt==4.0.1 black==23.9.1 +boto3==1.28.64 +botocore==1.31.64 cffi==1.16.0 click==8.1.7 cryptography==41.0.4 @@ -15,6 +17,7 @@ fastapi==0.103.2 h11==0.14.0 idna==3.4 isort==5.12.0 +jmespath==1.0.1 jwt==1.3.1 lists==1.3.0 loguru==0.7.2 @@ -30,10 +33,15 @@ pydantic-settings==2.0.3 pydantic_core==2.10.1 pylint==3.0.1 PyMySQL==1.1.0 +python-dateutil==2.8.2 python-dotenv==1.0.0 +python-multipart==0.0.6 +s3transfer==0.7.0 +six==1.16.0 sniffio==1.3.0 SQLAlchemy==1.4.49 starlette==0.27.0 tomlkit==0.12.1 typing_extensions==4.8.0 +urllib3==2.0.6 uvicorn==0.23.2