linewalks / be-common

Linewalks Back-end Guide
0 stars 0 forks source link

[Flask 2.0] Flask 2.0에 대응하기 #43

Open Jaymel opened 2 years ago

Jaymel commented 2 years ago

내용

Flask 2.0.x에서의 변경 사항을 간략히 설명합니다. flask_apispec은 현재 Flask 2.0.x에서 사용할 수 없습니다. 대안책 혹은 해결방안을 탐색합니다.

Flask 2.0.0의 변경사항

주요 변경사항

changes document

  1. 더이상 Python2와 Python3.5를 지원하지 않습니다.

  2. Werkzeug >= 2, Jinja2 >= 3, MarkupSafe >= 2, ItsDangerous >= 2, Click >= 8로 의존성이 변경되었습니다. 단 Celery 등 Click에 의존성이 높은 applications들을 위해 2.0.x는 Click7 역시 지원합니다. 추후 Flask 2.1.x부터 Click8이 강제될 수 있습니다.

  3. simplejson은 더이상 사용하지 않습니다. built-in json module의 성능이 더 좋아졌으므로 built-in json module을 사용합니다. 단 JSONMixin은 override가 가능하므로 다음과 같은 방법으로 simplejson을 다시 사용할 수 있습니다. 참조

    
    from simplejson import JSONEncoder

app.json_encoder = JSONEncoder


4. config.from_file 메소드가 추가됐습니다.

app.config.from_file(filename: str, loads_function)

다른 유형의 자료에서 config을 불러올 수 있습니다.(eg. toml)
config.from_json 메소드는 더이상 사용할 수 없습니다.

5. send_file 메소드와 send_from_directory 메소드가 werkzeug.utils의 wrappers가 됐습니다. 또 send_file의 일부 parameter 변수명이 아래와 같이 변경됐습니다. 

- attachment_filename -> download_name
- cache_timeout -> max_age
- add_etags -> etag

기존의 변수명은 더이상 사용하지 않습니다. 
max_age(cache_timeout) default는 기존 12시간에서 None으로 바꼈으며 이는 브라우저가 검증이 필요한 request에 대해 timed cache를 사용하지 않게 합니다.

6. helpers.safejoin은 더이상 사용할 수 없으며 werkzeug.utils.safejoin을 사용해야합니다.

7. **route decorator가 이제 기본 http method를 사용할 수 있습니다.**
예를 들어 다음과 같은 방식으로 rest api를 작성할 수 있습니다.

@app.post("/blah")

이는 아래와 동작이 동일합니다.

@app.route("/blah", methods=[POST])

기본 지원하는 http method는 [get, post, delete, put, patch] 입니다. 또한 기존의 app.route 방식 역시 여전히 사용할 수 있습니다.

8. helpers.total_seconds()는 제거됐습니다. timedelta.total_seconds()를 사용해야합니다.

### 의존성 문제
Click8에 뿐 아니라 flask_restful과 flask_jwt_extended에 대해 의존성 문제가 있습니다.

단 flask_jwt_extended의 경우 Flask 2.0.x으로의 전환이 breaking change 급 변화는 [아니라고 보며](https://github.com/vimalloc/flask-jwt-extended/issues/426) 4.2.1 버전부터 정식 지원을 하고 있습니다. 물론 강한 의존성 관계가 아니라 기존에 사용하던 4.1.0 버전에서도 단순 warning만 출력할 뿐 사용하는데 문제는 없습니다.

flask_restful의 경우 0.3.8 버전은 helpers 문제가 발생하며 0.3.9 버전업시 Flask2.0.x와 함께 사용할 수 있습니다. 단 현재 CLUE-Login과 CLUE-API는 flask_restful을 호출만 할 뿐 사용하지 않고 있으며 해당 내용을 지워도 문제없이 동작합니다. flask_restful 자체를 사용하지 않는 것도 방법이 될 수 있습니다.

Click8의 경우 아직 Flask2.1.x로 버전업이 되지 않았고 Flask2.0.x에선 Click7을 사용할 수 있으므로 추후 대응을 준비합니다.

## Flask 2.0.0 사용하기
Flask 2.0.0으로 업그레이드하려면 flask_apispec의 error를 해결해야합니다. 또 flask_jwt_extended >= 4.2.1, flask_restful >= 0.3.8의 의존성을 요구합니다.

### Flasgger를 사용하기
flask_apispec 대신 Flasgger를 사용할 수 있는지 확인합니다.
Flassger는 기존에 사용하던 flask_apispec과 사용법이 너무 다릅니다. 특히 @docs로 편하게 api 정보를 작성할 수 있는 flask_apispec과는 다르게 반드시 api function에 아래와 같이 api 정보를 작성해야합니다.

""" This examples uses FlaskRESTful Resource It works also with swag_from, schemas and spec_dict

parameters:

  1. validation 검증을 따로 해줘야함
  2. response에 대한 customize가 힘듦.

상기 등의 이유로 flask_apispec에 비하면 불만족스럽습니다. 사용하기 힘들 것 같습니다.

flask_apispec으로 Flask 2.0.x에 대비하기

docs.register_existing_resources()

현재 상기와 같이 swagger docs에 api를 등록하고 있는 과정을

import types

for name, rule in app.view_functions.items():
    blueprint_name = name.split(".")[0]

    if isinstance(rule, types.FunctionType) and rule.__name__ != "<lambda>":
      docs.register(rule, blueprint=blueprint_name)

위의 for 구문으로 변경합니다.

Flask 2.0.x 사용시 기존에 flask_apispec에서 문제가 되던 부분은 다음과 같습니다.

flask_apispec > extension.py > FlaskApiSpec

class FlaskApiSpec:
    def register_existing_resources(self):
        for name, rule in self.app.view_functions.items():
            try:
                blueprint_name, _ = name.split('.')
            except ValueError:
                blueprint_name = None

            try:
                self.register(rule, blueprint=blueprint_name)
            except TypeError:
                pass

    def register(
        self,
        target,
        endpoint=None,
        blueprint=None,
        resource_class_args=None,
        resource_class_kwargs=None
    ):

        self._defer(self._register, target, endpoint, blueprint, resource_class_args, resource_class_kwargs)

    def _register(
        self,
        target,
        endpoint=None,
        blueprint=None,
        resource_class_args=None,
        resource_class_kwargs=None
    ):
        if isinstance(target, types.FunctionType):
            paths = self.view_converter.convert(target, endpoint, blueprint)
        elif isinstance(target, ResourceMeta):
            paths = self.resource_converter.convert(
                target,
                endpoint,
                blueprint,
                resource_class_args=resource_class_args,
                resource_class_kwargs=resource_class_kwargs,
            )
        else:
            raise TypeError()
        for path in paths:
            self.spec.path(**path)

register_exsiting_resources는 blueprint rule들을 swagger에 등록할 때 app.view_functions.items()를 통해 모든 view_function들을 가져와 등록합니다. 이때 flask_apispec.extension.FlaskApiSpec object의 bound method들과 Flask의 send_static_file object의 bound method인 static이 view_function에 포함되는데 apispec에서 swagger ui를 생성할 때 사용하지 않으므로 Type check를 통해 blueprint들만 register합니다. 다만 Flask 2.0.x부터 Flask send_static_file의 static이 lambda func로 생성되며 문제가 발생했습니다.

실제 Flask app의 source를 보면 image view_func가 lambda인 걸 볼 수 있습니다. 참조 lambda 역시 types.FunctionType이므로 기존에 bound mehtod들을 예외처리하던 조건문을 통과하고 lambda를 register할 수 없던 flask_apispec은 알 수 없는 에러를 호출하며 사용할 수 없게 됐습니다!

따라서 view_func의 name이 "\<lambda>"인 경우도 예외처리를 해주어야 Flask 2.0.x에서 flask_apispec을 무사히 사용할 수 있습니다!

Flask 2.0.0을 사용 가능한 Repository

현재 CLUE-CDN, CLUE-API, CLUE-Login, punchbox-api 모두 Flask 2.0.0을 사용할 수 있음을 확인했습니다.

참조

Flask 2.0.0 Document post - Everything new in Flask 2.0 flasgger flask_apispec

liza0525 commented 2 years ago

자세한 정리 감사합니다👍 Flask2.0에선 flask_apispec 쓰려면 패키지의 내용을 변경해야 사용 가능한 걸로 이해했는데 그렇다면 Flasgger을 쓰는 걸 권장하는 바인 건가요~?

Jaymel commented 2 years ago

자세한 정리 감사합니다+1 Flask2.0에선 flask_apispec 쓰려면 패키지의 내용을 변경해야 사용 가능한 걸로 이해했는데 그렇다면 Flasgger을 쓰는 걸 권장하는 바인 건가요~?

패키지 수정 없이 기존에 main.__init__.py에서 docs.register_existing_resources() 로 쓰고 있는 부분을 본문 코드로 바꿔도 동작합니다! 일괄적으로 register하는 부분에 문제가 있던 거라 수동으로 하나하나 register한다고 생각하시면 됩니다.

liza0525 commented 2 years ago

패키지 수정 없이 기존에 main.init.py에서 docs.register_existing_resources() 로 쓰고 있는 부분을 본문 코드로 바꿔도 동작합니다! 일괄적으로 register하는 부분에 문제가 있던 거라 수동으로 하나하나 register한다고 생각하시면 됩니다.

오호 그런 것이군요! 설명 감사합니다 :)