본문 바로가기
기록/Python

[Python] 파이썬 FastAPI 서버 구축 삽질 기록

by 자임 2022. 9. 22.

 

알게된 거 두서없이 기록.

 

*
10 popular REST frameworks For your MicroService https://vishnuch.tech/10-popular-rest-frameworks-for-your-microservice
Full Stack FastAPI and PostgreSQL - Base Project Generator : https://github.com/tiangolo/full-stack-fastapi-postgresql

 


*개념 정리

ASGI(Asynchronous Server Gateway Interface)란 :
WSGI의 상위 호환. 대용량 트래픽을 처리하기 위한 비동기 처리 지원. 대표 예는 uvicorn

 


Swagger UI란, Swagger 제품군 중 API Documentation과 관련된 기능을 제공하는 제품이다.
출처: https://sharplee7.tistory.com/48

 

 

RESTful API에 대해서 http://www.incodom.kr/RestFul_API
- “Representational State Transfer” 의 약자로, 자원을 이름으로 구분하여 해당 자원의 상태(정보)를 주고 받는 모든 것을 의미한다. 즉, 자원(resource)의 표현(representation)에 의한 상태 전달을 의미한다.
- REST는 기본적으로 웹의 기존 기술과 HTTP 프로토콜을 그대로 활용하기 때문에 웹의 장점을 최대한 활용할 수 있는 아키텍처 스타일이다.
- HTTP URI(Uniform Resource Identifier)를 통해 자원(Resource)을 명시하고, HTTP Method(POST, GET, PUT, DELETE)를 통해 해당 자원에 대한 CRUD Operation을 적용하는 것을 의미한다.

출처 : https://juna-dev.tistory.com/16

 

 

ENDPOINT란,

API가 두 시스템(어플리케이션)이 상호작용 할 수 있게 하는 프로토콜의 총 집합이라면,
ENDPOINT는 API가 서버에서 리소스에 접근할 수 있도록 가능하게 하는 URL이라 할 수 있겠다.

출처 : https://blog.naver.com/PostView.naver?blogId=ghdalswl77&logNo=222401162545&parentCategoryNo=&categoryNo=90&viewDate=&isShowPopularPosts=true&from=search



도움된 블로그
fastAPI 이해하기 https://jybaek.tistory.com/890
fastAPI 시리즈 https://blog.neonkid.xyz/252?category=656103
fastAPI 배우기 https://lucky516.tistory.com/category/Fast%20API/fastapi%EB%B0%B0%EC%9A%B0%EA%B8%B0




아래는 소스코드 짜면서 부딪힌 기록



main.py

import uvicorn
from fastapi import FastAPI
from app.api.v1 import api_router
from app.core import settings

app = FastAPI(title=settings.PROJECT_NAME)

app.include_router(api_router, prefix=settings.API_V1_STR)

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)


*app.include_router(api_router, prefix=settings.API_V1_STR) :
따로 만든 라우터를 메인에 합쳐주는 코드





config.py

import secrets
from pydantic import BaseSettings


class Settings(BaseSettings):
    API_V1_STR: str = ""
    PROJECT_NAME: str = ""
    SECRET_KEY: str = secrets.token_urlsafe(32)
    SQLALCHEMY_DATABASE_URI: str = ""

    class Config:
        env_file = ".env"

settings = Settings()

 


*secrets : 비밀 관리를 위한 안전한 난수 생성
nbytes의 무작위 바이트를 포함한, URL 안전한 무작위 텍스트 문자열을 돌려줍니다. 텍스트는 Base64로 인코딩되어 있으므로, 평균적으로 각 바이트는 약 1.3 문자가 됩니다. nbytes가 None이거나 제공되지 않으면, 적절한 기본값이 사용됩니다.

ex) secrets.token_urlsafe(16)  
=> 'Drmhze6EPcv0fN_81Bj-nA'


 



schemas\product.py

from typing import Optional
from pydantic import BaseModel

class ProductBase(BaseModel):
    required1: Optional[int]
    required2: Optional[str]
    etc: Optional[float]

class ProductCreate(ProductBase):
    required2: str
    etc: float

class ProductUpdate(ProductBase):
    required1: int
    pass

class ProductResponse(ProductBase):
    class Config:
        orm_mode = True



*BaseModel이 기본틀이고 그걸 각자 상속받아서 속성 오버라이드 Optional이었던 값을 필수값으로 재정의 할 수 있다.
Optional은 해당 속성에 값이 필수적으로 들어가지 않아도 되는 경우에 추가한다.
출처 : https://phsun102.tistory.com/m/65




*pydantic 라이브러리
파이단틱 모델

데이터 유효성 검사

pydantic은 type annotation을 사용해서 데이터를 검증하고 설정들을 관리하는 library이다.
runtime에서 type을 강제하고, type이 유효하지 않을 때 에러를 발생시킵니다.

출처: https://ks1171-park.tistory.com/83


 

*
class Config:
orm_mode = True

만약 가져온 데이터를 JSON 형태로 관리하고자 한다면, orm_mode를 이용해 쉽게 적용이 가능하다.
기본적으로 데이터 구조를 클래스르 지정하고 Config에서 orm_mode = True를 선언하여 반환되는 모델을 자동으로 JSON으로 가져올 수 있게 된다.

출처 : https://asecurity.dev/entry/Python-Josn-%EB%B3%80%ED%99%98-pydantic-ormmode



 



초기 버젼 products.py

@router.get("", response_model=List[schemas.ProductResponse])
def read_products(db: Session = Depends(get_db), skip: int = 0, limit: int = 100) -> Any:
    """
    Retrieve all products.
    """
    products = crud.product.get_multi(db, skip=skip, limit=limit)
    return products



*response_model
응답을 줄 때 response_model로 지정한 스키마 형태로 준다

출처 : https://lucky516.tistory.com/96


* ->
python3 함수 정의 시 나타나는 화살표(->)는 함수 리턴 값의 주석 역할을 한다.
말 그대로 주석이기 때문에 있으면 좋지만 없다고 문제가 되지는 않는다.
Any 모든 타입 허용한다는 뜻이다.

schemas에서 pydantic으로 타입을 체크해주기 때문에 아마 타입이 다른 데이터가 들어오면 그쪽에서 오류가 날 것 같긴 함.


* python3 함수 정의 시 : (콜론)
비슷한 역할로 콜론이 있다. 위 코드를 보면 x: int 라는 표현이 있는데, 이는 매개변수 x 타입에 대한 주석이다.
즉 x 값은 int로 들어올 것이다. 라는 의미다.

출처 : https://devpouch.tistory.com/189



 



base.py 최종 코드

from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union

from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from sqlalchemy.orm import Session

from app.database.base_class import Base


ModelType = TypeVar("ModelType", bound=Base)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)


class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
    def __init__(self, model: Type[ModelType]):
        """
        CRUD object with default methods to Create, Read, Update, Delete (CRUD).
        **Parameters**
        * `model`: A SQLAlchemy model class
        * `schema`: A Pydantic model (schema) class
        """
        self.model = model

    def get(self, db: Session, required1: Any) -> Optional[ModelType]:
        return db.query(self.model).get(required1)

    def get_multi(self, db: Session, required1:int, required2:str, etc:float, page:int, display_count:int) -> List[ModelType]:

        print("get multi", page, display_count)

        # 필수값이 아닌 파라미터 값 체크 (값이 있을 경우만 쿼리에 포함)
        queries = [self.model.required1 == required1]
        queries.append(self.model.required2 == required2)
        if etc is not None:
            queries.append(self.model.etc == etc)

        query = db.query(self.model).filter(*queries)
        query = query.offset((page-1)*display_count).limit(display_count) #페이지 처리
        product = query.all()
        print(query)

        return product


    def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType:
        obj_in_data = jsonable_encoder(obj_in)
        db_obj = self.model(**obj_in_data)
        db.add(db_obj)
        db.commit()
        db.refresh(db_obj)
        return db_obj




CRUD utility class
1.CRUDBase Class 정의
SQLAlchemy model로부터 Base 1개 import, pydantic으로부터 BaseModel 2개 import 해서 총 3개 input으로 넣음(ModelType, CreateSchemaType, UpdateSchemnaType)
2.Modeltype이 인스턴스 생성 시 input으로 들어감
3.get method : 하나의 database row를 가져온다.
Session : sqlalchemy로부터 import된 모듈로 db 를 입력받음
.query : 다른 DB query들을 하나로 묶는 방법
4.get_multi : 여러 database row 가져오기
.offset 과 .limit 이용, all() 로 마무리
5. .commit()
db object를 만들어내고자 한다면, row 삽입을 위해 commit() 필요

출처 : https://velog.io/@crosstar1228/FastapiSQLAlchemy-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-DB%EC%99%80-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0




*ModelType = TypeVar("ModelType", bound=Base)

Typing 라이브러리 
TypeVar로 제네릭을 구현할 수 있다

제네릭이란
자바에서 제네릭(generic)이란 데이터의 타입(data type)을 일반화한다(generalize)는 것을 의미합니다.
제네릭은 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법입니다.

데이터 형식에 의존하지 않고 인자, 변수 또는 반환값 등이 여러 다른 데이터 타입들을 가질 수 있는 방식을 제네릭이라고 한다.

어떤 함수의 파라미터에 대한 타입을 지정하지 않고 상황에 따라 다양한 타입을 파라미터로 사용하는 기법이다.

출처 : 
https://sjquant.tistory.com/68
https://velog.io/@sawol/%EC%A0%9C%EB%84%A4%EB%A6%AD-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-with-Python
https://movefast.tistory.com/74





최종 products.py 코드 조회 부분

#조회
@router.get("/search", response_model=List[schemas.ProductResponse])
def read_products(
        *,
        db: Session = Depends(get_db),
        required1: int,
        required2: str,
        etc: float = None,
        page: int = 1,
        display_count: int = 5
    ) -> Any:
    """
    Retrieve all products.
    """
    product = crud.product.get_multi(db,
                                     required1=required1,
                                     required2=required2,
                                     etc=etc,
                                     page=page,
                                     display_count=display_count)
    print(product)

    #검색 결과 없을 때
    if not product:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="검색 결과가 존재하지 않습니다.",
        )

    return product


* @router.get("/search", response_model=List[schemas.ProductResponse])

http://localhost:8000/products/search?id=123
이런식으로 쿼리 파라미터 사용 가능

쿼리 매개변수 : ?뒤에 붙어오는 key:value 값
https://fastapi.tiangolo.com/ko/tutorial/query-params/

생명의 은인 : https://cotak.tistory.com/28


 



*get_multi 에서 select 구현 방식 고민한 기록
원하는 동작 : get_multi 시 쿼리 파라미터로 들어온 값이 조건으로 들어가서 해당하는 데이터를 모두 출력


첫번째 시도: 
https://otrodevym.tistory.com/entry/Python-SQLAlchemy-CRUD-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95?category=956479

오류 : filter_by() takes 1 positional argument but 2 were given

=> 실패. 내가 원하는 방식으로는 활용할 수 없었음 




두번째 시도:
https://edykim.com/ko/post/getting-started-with-sqlalchemy-part-2/ 이거 보면서 쿼리문 바꿔봄

product = db.query(self.model).from_statement("SELECT * FROM self.model WHERE id=:id").params(id=id).all()
return product

오류
sqlalchemy.exc.ArgumentError: Textual SQL expression 'SELECT * FROM product WHE...' should be explicitly declared as text('SELECT * FROM product WHE...')

=> 실패

 



세번째 시도:

product = db.query(self.model).filter(self.model.id == id).all()
로 하면 값이 null로 찍히고

product = db.query(self.model).filter(self.model.id == id).first()로 하면 값이 잘 나오지만 당연히 하나밖에 안 나옴

둘 차이점은.. all 은 list 타입이고 first는 product 객체 타입이라는 거임..
<class 'app.models.product.Product'>





*선택적 매개변수
같은 방법으로 기본값을 None으로 설정하여 선택적 매개변수를 선언할 수 있습니다

@app.get("/items/{item_id}")
async def read_item(item_id: str, q: Union[str, None] = None):
    if q:
        return {"item_id": item_id, "q": q}
    return {"item_id": item_id}



 



models\product.py

from sqlalchemy import Column, Integer, String, Float
from app.database.base_class import Base

class Product(Base):
    required1 = Column(Integer, primary_key=True, index=True)
    required2 = Column(String, nullable=False)
    etc = Column(Float, nullable=True)



*nullable = False
nullable : 비어있을 수 있는
null 허용이면 nullable=True

테이블에 etc는 null허용이고 모델에는 nullable=False로 되어있길래 테이블 속성 변경 해줬더니 값이 base로 넘어가는 것까지는 성공.
근데 결과값 항상 None 나옴.



*response_model=schemas.ProductResponse 식제하니까 여러개 가져오는 거 되긴 함

[
  {
    "price": 1.1,
    "id": 2,
    "name": "test"
  },
  {
    "price": 300,
    "id": 3,
    "name": "test"
  },
  {
    "price": 111100,
    "id": 4,
    "name": "test"
  }
]

근데 출력이 순서가 무시되어서 나옴. 아무래도..
그럼 return 값 문제가 아니라 response_model이 문제라는 건데..

*response_model 관련 좋은 글 
https://juna-dev.tistory.com/14



 



base.py

def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType:
obj_in_data = jsonable_encoder(obj_in)
db_obj = self.model(**obj_in_data)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj



*jsonable_encoder()
데이터베이스에 넣기 위해 json 형식이 필요해서 해당 엔코더를 쓴다.

출처 : https://fastapi.tiangolo.com/ko/tutorial/encoder/

 

좋은 글 참고: https://davi06000.tistory.com/147?category=943704



 



네 번째 시도 => 성공!

바꾸기 전 base query 문: 
query = db.query(self.model).filter(self.model.name == name, self.model.id == id, self.model.price == price)

 



변경 후:

product.py

@router.get("/search")
def read_products(*, db: Session = Depends(get_db), id: int = None, name: str, price: float = None) -> Any:
    """
    Retrieve all products.
    """
    print("name", name)
    product = crud.product.get_multi(db, id=id, name=name, price=price)
    print(product)
    if not product:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="The product with this ID does not exist in the system.",
        )
    return product



base.py

def get_multi(self, db: Session, id:int, name:str, price:float) -> List[ModelType]:

print("get multi", id)
print("get multi", name)
print("get multi", price)
# query = db.query(self.model).filter(self.model.name == name, self.model.id == id, self.model.price == price)

queries = [self.model.name == name]
if id is not None:
queries.append(self.model.id == id)
if price is not None:
queries.append(self.model.price == price)

query= db.query(self.model).filter(*queries)
print(query)
product = query.all()
print("get_multi product", product)

return product



생명의 은인 : https://stackoverflow.com/questions/31063860/conditionally-filtering-in-sqlalchemy




*json 순서대로 안 나오는 것도 고침

@router.get("/search", response_model=List[schemas.ProductResponse])
List로 output 값 설정해주니까 됨.

참고 : https://velog.io/@kjh03160/Fast-API-Response-Model-Output-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%98%95%EC%8B%9D





*limit offset 처리
limit과 offset은 보통 쿼리의 pagination을 개발할 때 주로 사용됩니다. (pagination 페이지 처리)

-- 처음 10개의 Row를 반환
SELECT * FROM test LIMIT 10;

-- 위 SQL과 아래의 SQL은 같은 결과
SELECT * FROM test LIMIT 10 OFFSET 0;
 
-- 11번째 부터 10개의 Row를 반환.
SELECT * FROM test LIMIT 10 OFFSET 10;

출처 : https://brownbears.tistory.com/234


offset 설정 크기만큼 앞에서 부터 생략후 반환
offset 부터 limit개까지


offset과 limit을 이용해서 페이징 처리

if page_size:
query = query.limit(page_size)
if page: 
query = query.offset(page*page_size)

출처 : https://cotak.tistory.com/28



최종 코드:
query = query.offset((page-1)*display_count).limit(display_count)
product = query.all()

.all() 까지 한번에 써주면 오류 났었음
따로 해줘야 함