[Python] Flask-RestPlus 모듈 제대로 사용해 보기 Develop Tip

지난번에 [Python] Flask & flask-restplus && swagger ui 라는 블로그를 정리했었습니다.
그런데 이것을 기존에 작업되어 있던 것에 적용을 하는데
이틀정도 시행착오를 겪었습니다. 
아직 0.10.1 이라는 버전 때문에 그럴 수도 있지만 뭐 현실에 적응해서 앞으로 나아가는게
우선이라...
(나중에라도 버전업이 되어 원하는 데로 되어 간다면 내용을 수정해 놓겠습니다)

다음은 작업되어 있는 것을 수정한 내용입니다.

우선 기존 Blueprint 로 되어 있던 부분을 Namespace로 변경합니다.

기존,
bp_aaa = Blueprint('bp_aaa', __name__, url_prefix='/api/aaa')

변경,
api = Namespace('ns_aaa', description='User AAA for RESTful API')

(Bludprint에서는 prefix를 미리 주었는데, Namespace 는 나중에 주어도 됩니다)

flask-restplus는 function을 route 시킬 수 없고 Resource에서 파생된 클래스만 이용할 수 있습니다.
따라서, 다음과 같이 기존에 function은 class로 변경합니다.

################################################################################
@bp_aaa.route('/ping', methods=['GET', 'PUT', 'POST'])
def rapi_ping():
    r = None
    atlog = AuditLog('[AAA] Accessing API ping')
    try:
        r = {'success': True, 'http_method': request.method}
    except Exception as exp:
        atlog.set_except(exp)
        r = {'success': False, 'message': str(exp)}
    finally:
        atlog.set_user(current_user)
        atlog.save()
        return jsonify(r)

대신

################################################################################
# noinspection PyMethodMayBeStatic
@api.route('/ping')
class Ping(Resource):
    # ==========================================================================
    def get(self):
        """
        API ping to check alive or not

        # Input Arguments
        None
        """
        r = None
        atlog = AuditLog('[AAA] Accessing API ping')
        try:
            r = {'success': True, 'http_method': request.method}
        except Exception as exp:
            atlog.set_except(exp)
            r = {'success': False, 'message': str(exp)}
        finally:
            atlog.set_user(current_user)
            atlog.save()
            return r

또한 만약  @api.route(...) 다음에 @xxyyzz 데코레이터가 있다면 이 역시 function으로 인식해 오류가 발생하므로
해당 데코레이터를 @api.route 위에 두던가 해야 합니다.

그 다음 swagger를 위한 작업입니다.
(기존 Flask 및 Flask-Restful 에서 지원하지 않던 부분입니다)

우선 다음과 같은 코드를 추가합니다.

json_parser = api.parser()
json_parser.add_argument('json', type=str, required=True, location='json',
                         help='JSON BODY argument')
arg_parser = api.parser()
arg_parser.add_argument('json', type=str, required=True,
                        help='URL JSON argument')

Model과도 적용을 해 보았지만 swagger 에서 보내는 등의 시행착으로 겪었습니다.

다음과 같은 식으로 정리하면 되었습니다.

################################################################################
# noinspection PyMethodMayBeStatic
@api.route('/login')
class Login(Resource):
    # ==========================================================================
    res_model = api.model('Model', {
        'success': fields.Boolean(description='API Success/Failure',
                                  required=True),
        'message': fields.String(description='Success/Failure message',
                                 required=True),
    })

    # ==========================================================================
    @api.doc(parser=json_parser)
    @api.response(200, 'API Success/Failure', res_model)
    @api.response(400, 'Failure')
    @api.response(500, 'Error')
    def post(self):
        """
        checking User login

        *** If login required API is called without authentication raise
        401 HTTP Error ***

        # Input Arguments

        * user_id : str : required : user ID
        * password : str : required : user password

        # Example
        ``` json
        {
            "user_id": "user01",
            "password": "passwd"
        }
        ```
        """
        r = None
        atlog = AuditLog('[AAA] Login checking')
        try:
            # args = json_parser.parse_args()
            args = parse_req_data(request)
            user_id = args['user_id']
            password = args['password']
            atlog.add_where('for userid "%s"' % user_id)
            if not current_app.userMgr.can_login(user_id, password):
                r = {
                    'success': False,
                    'message': 'Invalid user_id or password'
                }
            else:
                _user = current_app.userMgr.get(user_id)
                _user.authenticated = True
                login_user(_user, remember=True)
                r = {
                    'success': True,
                    'message': 'user <%s> logined' % user_id
                }
        except Exception as exp:
            atlog.set_except(exp)
            r = {'success': False, 'message': str(exp)}
        finally:
            atlog.set_user(current_user)
            atlog.save()
            return r

################################################################################
def parse_req_data(request):
    """
    flask의 request에서 사용자 데이터를 가져옴
        주의: flask-restplus 모듈을 이용하여 swagger ui를 이용하는 패러미터
            패싱이 제대로 안되어 본 함수 이용 (0.10.1)
    :param request: Flask의 request
    :return: parameter dict
    """
    if not hasattr(request, 'method'):
        return None
    if request.method.upper() != 'GET':
        if request.data:
            return json.loads(request.data)
    if 'json' in request.args:
        return json.loads(request.args['json'])
    if request.args:
        return request.args     # note: type is ImmutableMultiDict
    return {}


위와 같은 식으로 정리하면 됩니다.
아래에 swagger 예시를 보여드리겠습니다.

그러면 마지막으로 서버에서,

from vivans.rest.bp_mysql import bp_mysql
from vivans.rest.bp_mongo import bp_mongo
from vivans.rest.bp_aaa import bp_aaa
...
app = Flask(__name__)
app.register_blueprint(bp_mysql)
app.register_blueprint(bp_mongo)
app.register_blueprint(bp_aaa)
app.run()

대신

from vivans.rest.ns_mysql import api as ns_mysql, on_load as on_load_mysql
from vivans.rest.ns_mongo import api as ns_mongo, on_load as on_load_mongo
from vivans.rest.ns_aaa import api as ns_aaa, on_load as on_load_aaa
...
on_load_mysql(app)
api.add_namespace(ns_mysql, path='...')
on_load_mongo(app)
api.add_namespace(ns_mongo, path='...')
on_load_aaa(app)
api.add_namespace(ns_aaa, path='...')
api.init_app(app)
app.run()


와 같은 식으로 수정하였습니다.

위에서 on_load_mysql(app) 와 같이 호출한 것은,

Blueprint 같은 경우,

################################################################################
@bp_aaa.record_once  # 처음 한번만 호출됨
def on_load(state):
    state.app.mymgr = MyMgr()

와 같은 record_once 이벤트 핸들러가 있다면 restplus에서는 존재하지 않습니다.

따라서, Namespace 에서

################################################################################
def on_load(app):
    app.mymgr = MyMgr()

와 같이 만들어 놓고 호출한 것입니다.

current_app 연결은 동일합니다.

이제 실행하면, 

해당 Namespace 별로 API 가 보이며, 특정 API (위에서는 login)를 확장해 보면,
잘 나옵니다.

get, post, put, delete 등의 Resouce에서 파생한 클래스의 메서드의 doc string에 Marddown으로 기술하면 그대로 나옵니다.

입력 및 출력 형식은 @api.doc 및, @api.response 를 이용하면 되구요.

위와 같이 예시로 넣고,

"Try it out!" 단추를 누르면 해당 결과를 아래 확인 할 수 있습니다.


이것 때문에 거의 이틀 고생을 했습니다만, 다른 분들은 두시간 만에 끝내시기를...

어느분께는 도움이 되셨기를...

덧글

  • 2017/10/18 21:54 # 삭제 답글 비공개

    비공개 덧글입니다.
  • 지훈현서아빠 2017/10/19 08:55 #

    일단 제가 가지고 있는 생각만 말씀드리겠습니다. 그리고 제가 별도 설계해 드릴 시간은 없어 그렇게 해 드리기는 힘들지만
    작업을 하시면서 걸리는 문제를 그때 그때 물어봐 주시면 제가 알려드리도록 하겠습니다.
    제 메일은 mcchae@gmail.com 입니다.
    우선 아두이노 자체는 리눅스처럼 자체 snmp 서비스나 cron 등의 다양한 프로세스가 돌 수 없기 때문에 쉽지 않습니다.
    대신 http://www.meccanismocomplesso.org/en/controlling-arduino-raspberry-pi/ 와 같이 아두이노와
    라즈베리파이가 하나씩 붙어 있다면 이야기는 쉬워집니다.
    일반 리눅스 관리 + 알파가 되기 때문이지요. 제 생각은 이렇습니다...
  • 2017/10/19 09:37 # 삭제 답글

    네~ 답변 너무 감사드립니다.
    한줄기 희망이 생긴것 같아 기분좋은 아침입니다.
    제가 하다가 안되는 부분은 여쭈어 보겠습니다.
    감사합니다~
  • 지훈현서아빠 2017/10/19 09:44 #

    도움이 되신다니 보람을 느낍니다.
  • 질문있습니다 2018/06/22 15:35 # 삭제 답글

    안녕하세요. flask에서 restapi 문서 관리를 위해 이것저것 알아보는 가운데 검색하다가 오게 방문하게 되었습니다.
    혹시 질문 드려도 될까요?
    restplus를 import해서 사용할 경우에 default port로 5000번이 잡히는데
    1) 웹서비스 포트는 유지를 하고 swagger-ui 포트를 변경할 수 있을까요?
    2) 그리고 특정 url로 swagger-ui를 확인 할 수 있게 설정할 수 있을까요?
    홈페이지에서 api등을 뒤져보는데 위 2가지에 대한 정보를 확인 할 수가 없어서요.
    확인하시면 답변 부탁 드리겠습니다.
  • 지훈현서아빠 2018/06/25 11:30 #

    제 경험을 간단히 말씀 드리겠습니다.

    우선 port는 flask app를 처음 동작시킬 때 주는 것이구요,
    API는 해당 URL에서 prefix를 주고 그 아래에 특정 URL routing을 하게 되는데,
    swaggers는 그 대표 URL로 들어가기만 하면 나오는 것으로 이해하고 있습니다.
댓글 입력 영역

구글애드텍스트