├── .gitignore ├── README.md └── app ├── app.py └── functions.py /.gitignore: -------------------------------------------------------------------------------- 1 | # MacOS 2 | .DS_Store 3 | 4 | # Python 5 | *.pyc 6 | __pycache__/ 7 | dist/ 8 | build/ 9 | *.egg-info/ 10 | 11 | # Experiments 12 | tmp/ 13 | 14 | # Jupyter notebook 15 | .ipynb_checkpoints/ 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask 를 이용하여 API 만들기 2 | 3 | ## REST API 4 | 5 | REpresentational State Transfer (REST) API 의 약어로, Uniform Resource Identifier (URI) 를 통하여 자연을 명시하고, 처리된 결과를 보통 JSON 이나 XML 로 return 한다. 6 | 보통 Uniform Resource Locator (URL) 은 자원의 위치를 나타내지만, URI 는 그 안에 데이터 정보가 포함된다. 7 | 자원의 위치도 데이터 정보이기 때문에 URI 가 URL 을 포함하는 더 큰 단위의 개념이다. 8 | 9 | `GET` 은 URI 에 패러매터를 추가하여 요청하는 방식으로, query string 에 그 값을 추가한다. 10 | `https://127.0.0.1:5000/test?name=lovit&koname=%EA%B9%80%ED%98%84%EC%A4%91'` 에서 `?` 뒤의 부분이 query string 이다. 11 | 마치 데이터베이스에서 어떤 값을 조회하기 위한 attributes 를 입력하는 구조인데, 이는 `GET` 이 서버로부터 값을 조회하기 위해 만들어졌기 때문이다. 12 | 13 | ## URL encoding 14 | 15 | URL 은 ASCII 로만 기술할 수 있으며, ASCII 에 포함되지 않는 언어와 특수문자를 표현하기 위해 URL Encoding 을 이용한다. 16 | 즉 non-ASCII 글자들을 적절히 변형해야 하는데, `%EA` 처럼 `%` 다음의 16진수로 이 값들을 나타낸다. 17 | 18 | ```python 19 | import urllib.parse 20 | params = {'name': 'lovit', 'koname': '김현중'} 21 | data = urllib.parse.urlencode(params, doseq=True) 22 | print(data) 23 | ``` 24 | 25 | ``` 26 | 'name=lovit&koname=%EA%B9%80%ED%98%84%EC%A4%91' 27 | ``` 28 | 29 | non-ASCII 에 대한 인코딩 방식을 설정할 수 있는데, 기본값 `None` 은 `encoding='utf-8'` 이다. 30 | 만약 이 값을 `cp949` (처음 파이썬으로 한글을 접할 때 맨붕을 일으키는 그..) 를 이용하여 URL 로 변경한다면 다음과 같은 값이 된다. 31 | 그 외 더 많은 사용 가능한 인코딩은 [Python docs](https://docs.python.org/2.4/lib/standard-encodings.html) 에서 확인할 수 있다. 32 | 33 | ```python 34 | urllib.parse.urlencode(params, doseq=True, encoding='cp949') 35 | ``` 36 | 37 | ``` 38 | 'name=lovit&koname=%B1%E8%C7%F6%C1%DF' 39 | ``` 40 | 41 | ## URL decoding 42 | 43 | ASCII 로 기술된 값을 본래의 encoding 으로 되돌릴 수 있다. 44 | 앞서 ASCII 로 변경한 `data` 를 다시 `utf-8` 로 변경하면 다음의 값을 얻을 수 있다. 45 | 그러나 `urllib.parse.unquote` 함수는 인코딩만 변경할 뿐, query string 을 파싱을 하지는 않는다. 46 | 이를 위해서는 `urllib.parse.parse_qs` 함수를 이용한다. 47 | 단 주의할 점은 `str` 의 값도 query string parsing 을 거치면 `list of str` 으로 반환된다. 48 | 49 | ```python 50 | decoded_data = urllib.parse.unquote(data, encoding='utf-8') 51 | print(decoded_data) 52 | 53 | parsed_data = urllib.parse.parse_qs(decoded_data) 54 | print(parsed_data) 55 | ``` 56 | 57 | ``` 58 | 'name=lovit&koname=김현중' 59 | {'name': ['lovit'], 'koname': ['김현중']} 60 | ``` 61 | 62 | 사실 `utf-8` 에는 ASCII 가 포함되어 있기 때문에 위의 두 단계의 과정을 한번에 해결할 수도 있다. 63 | 하지만 `data` 를 `ASCII` 로 파싱하면 잘못된 결과가 출력된다. 64 | 65 | ```python 66 | print(urllib.parse.unquote(data)) 67 | print(urllib.parse.unquote(data, encoding='utf-8')) 68 | print(urllib.parse.unquote(data, encoding='ASCII')) 69 | ``` 70 | 71 | ``` 72 | {'name': ['lovit'], 'koname': ['김현중']} 73 | {'name': ['lovit'], 'koname': ['김현중']} 74 | {'name': ['lovit'], 'koname': ['���������']} 75 | ``` 76 | 77 | `GET` 으로 URL 을 받은 뒤, `?` 로 split 한 뒤, query string 을 값으로 복원하는 과정까지 살펴보았다. 78 | 79 | ## Flask app 80 | 81 | `app.py` 82 | 83 | ```python 84 | from flask import Flask 85 | from flask import request 86 | 87 | 88 | var_path = os.path.abspath(os.path.dirname(__file__)) + '../var/' 89 | app = Flask(__name__) 90 | 91 | @app.route('/') 92 | def hello_world(): 93 | return 'Hello, World!' 94 | 95 | if __name__ == "__main__": 96 | app.run() 97 | ``` 98 | 99 | ``` 100 | $ python app.py 101 | 102 | * Serving Flask app "app" (lazy loading) 103 | * Environment: production 104 | WARNING: This is a development server. Do not use it in a production deployment. 105 | Use a production WSGI server instead. 106 | * Debug mode: off 107 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 108 | ``` 109 | 110 | ## Data in URL 111 | 112 | `app.py` 113 | 114 | ```python 115 | @app.route('/user/') 116 | @app.route('/user/') 117 | def hello_user(user_name=None): 118 | # `user_name=None` 으로 초기화를 하면 위의 두 route 를 모두 이용 가능 119 | # user_name 의 초기값이 지정되지 않으면 `@app.route('/user/')` 만 이용 가능 120 | if (user_name is None): 121 | return 'Hello, annonymous' 122 | return f'Hello, {escape(user_name)}' 123 | 124 | 125 | @app.route('/add//') 126 | def add(a, b): 127 | a = int(a) 128 | b = int(b) 129 | # return type must be `str` 130 | return str(a + b) 131 | ``` 132 | 133 | IPython notebook 을 하나 켠다. 134 | 135 | `agent.ipynb` 136 | 137 | ```python 138 | import requests 139 | 140 | get_url = 'http://127.0.0.1:5000/add/3/5' 141 | response = requests.get(url=get_url) 142 | print(response.status_code) # 200 143 | print(response.text) # 8 144 | ``` 145 | 146 | ## Package import 147 | 148 | `functions.py` 파일에 다음의 함수를 구현해둔다. 149 | 150 | ```python 151 | def external_add(a: int, b: int): 152 | print('called external function') 153 | return a + b 154 | ``` 155 | 156 | ```python 157 | import functions as F 158 | 159 | @app.route('/external_add//') 160 | def external_add(a, b): 161 | a = int(a) 162 | b = int(b) 163 | # return type must be `str` 164 | return str(F.external_add(a, b)) 165 | ``` 166 | 167 | ## GET 168 | 169 | `app.py` 170 | 171 | ```python 172 | @app.route('/add_get/') 173 | def add_get(): 174 | print(type(request.args)) # 175 | print(request.args) # ImmutableMultiDict([('a', '3'), ('b', '5')]) 176 | a = int(request.args.get('a', '0')) 177 | b = int(request.args.get('b', '0')) 178 | return str(a + b) 179 | ``` 180 | 181 | `agent.ipynb` 182 | 183 | ```python 184 | import requests 185 | 186 | get_url = 'http://127.0.0.1:5000/add_get?a=3&b=5' 187 | response = requests.get(url=get_url) 188 | print(response.status_code) # 200 189 | print(response.text) # 8 190 | ``` 191 | 192 | 193 | ## POST 194 | 195 | `GET` 방법은 긴 데이터를 입력하는데 적절하지 않을 수도 있다. 196 | `POST` 는 HTTP Body 에 데이터를 추가하여 요청하는 방식으로, 길이의 제한이 없기 때문에 훨씬 많은 양의 데이터를 서버로 보낼 수 있다. 197 | 또한 URL 에 직접 데이터를 입력하지 않기 때문에 URL 에 민감한 값들이 보이지 않는 장점도 있다. 198 | 199 | `POST` 는 서버에서 리소스의 값을 생성/변경하기 위해 만들어진 방법이다. 200 | 그렇기 때문에 `GET` 은 항상 같은 결과값을 출력 (idempotent) 하는데 반하여, `POST` 는 실행 순서에 따라 다른 값이 출력될 수도 (non-idempotent) 있다. 201 | 예를 들어 특정 값을 데이터베이스에 추가한다면, 서버의 리소스가 변경되기 때문이다. 202 | 대표적인 예시로 게시판의 글을 웹서버 데이터베이스에 입력하는 행위는 `POST` 이다. 203 | 204 | `app.py` 205 | 206 | ```python 207 | @app.route('/hello_user_post/', methods=['POST']) 208 | def hello_user2(): 209 | # request.form # form value in HTML 210 | # request.files # attached files 211 | # requests.json # parsed JSON format data 212 | # requests.json == request.get_json(force=True) 213 | print(request.json) 214 | print(request.get_json(force=True)) 215 | 216 | json_data = request.get_json(force=True) 217 | name = json_data.get('name', 'annonymous') 218 | koname = json_data.get('ko_name', '익명자') 219 | return f'{name} ({koname})' 220 | ``` 221 | 222 | `agent.ipynb` 223 | 224 | ```python 225 | # data: list of tuple, bytes or file-like 226 | # json: json data to send in the body 227 | json_data = {'name': 'lovit', 'ko_name': '김현중'} 228 | post_url = 'http://127.0.0.1:5000/hello_user_post' 229 | response = requests.post(url=post_url, json=json_data) 230 | print(response.status_code) # 200 231 | print(response.text) # lovit (김현중) 232 | ``` 233 | 234 | ## Return as JSON 235 | 236 | `app.py` 237 | 238 | ```python 239 | @app.route('/return_json/', methods=['POST']) 240 | def return_json(): 241 | json_data = request.get_json(force=True) 242 | name = json_data.get('name', 'annonymous') 243 | koname = json_data.get('ko_name', '익명자') 244 | 245 | response = { 246 | 'concatenated_name': f'{name} ({koname})' 247 | } 248 | return response 249 | ``` 250 | 251 | `agent.ipynb` 252 | 253 | ```python 254 | import json 255 | 256 | json_data = {'name': 'lovit', 'ko_name': '김현중'} 257 | url = 'http://127.0.0.1:5000/return_json' 258 | response = requests.post(url=url, json=json_data) 259 | print(response.status_code) # 200 260 | print(response.text.strip()) # {"concatenated_name":"lovit (\uae40\ud604\uc911)"} 261 | print(json.loads(response.text)) # {'concatenated_name': 'lovit (김현중)'} 262 | ``` 263 | 264 | ## Run server with specific IP and port 265 | 266 | `app.py` 267 | 268 | ```python 269 | if __name__ == "__main__": 270 | parser = argparse.ArgumentParser(description='Flask option arguments') 271 | parser.add_argument('--host', type=str, default=None, help='Default is localhost') 272 | parser.add_argument('--port', type=int, default=None, help='Default is :5000') 273 | 274 | args = parser.parse_args() 275 | host = args.host 276 | port = args.port 277 | 278 | print('Flask practice') 279 | app.run(host=host, port=port) 280 | ``` 281 | 282 | ``` 283 | $ python app.py --port 5050 284 | 285 | Flask practice 286 | * Serving Flask app "app" (lazy loading) 287 | * Environment: production 288 | WARNING: This is a development server. Do not use it in a production deployment. 289 | Use a production WSGI server instead. 290 | * Debug mode: off 291 | * Running on http://127.0.0.1:5050/ (Press CTRL+C to quit) 292 | ``` 293 | 294 | Localhost 로 실행할 때는 `run(host=None)` 혹은 `run(host='0.0.0.0')` 으로 실행합니다. 295 | 할당된 고정 IP, 'abc.def.ghi.jkh' 로 실행할 때는 `run(host='abc.def.ghi.jkh')` 혹은 `run(host='0.0.0.0')` 으로 실행합니다. 296 | 297 | ``` 298 | $ python app.py --host 0.0.0.0 --port 5050 299 | ``` 300 | 301 | ## More 302 | 303 | - 웹페이지에서 데이터 받아 처리하는 걸로 만들기 304 | - https://medium.com/@mystar09070907/%EC%9B%B9-%ED%8E%98%EC%9D%B4%EC%A7%80-client-%EC%97%90%EC%84%9C-%EC%A0%95%EB%B3%B4-%EB%B3%B4%EB%82%B4%EA%B8%B0-bf3aff952d3d 305 | - file upload example 306 | - https://flask.palletsprojects.com/en/1.1.x/patterns/fileuploads/ 307 | -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | from flask import Flask 4 | from flask import request 5 | from markupsafe import escape 6 | import functions as F 7 | 8 | 9 | var_path = os.path.abspath(os.path.dirname(__file__)) + '../var/' 10 | app = Flask(__name__) 11 | 12 | 13 | @app.route('/') 14 | def hello_world(): 15 | return 'Hello, World!' 16 | 17 | 18 | @app.route('/user/') 19 | @app.route('/user/') 20 | def hello_user(user_name=None): 21 | # `user_name=None` 으로 초기화를 하면 위의 두 route 를 모두 이용 가능 22 | # user_name 의 초기값이 지정되지 않으면 `@app.route('/user/')` 만 이용 가능 23 | if (user_name is None): 24 | return 'Hello, annonymous' 25 | return f'Hello, {escape(user_name)}' 26 | 27 | 28 | @app.route('/add//') 29 | def add(a, b): 30 | a = int(a) 31 | b = int(b) 32 | # return type must be `str` 33 | return str(a + b) 34 | 35 | 36 | @app.route('/external_add//') 37 | def external_add(a, b): 38 | a = int(a) 39 | b = int(b) 40 | # return type must be `str` 41 | return str(F.external_add(a, b)) 42 | 43 | 44 | @app.route('/add_get/') 45 | def add_get(): 46 | print(type(request.args)) # 47 | print(request.args) # ImmutableMultiDict([('a', '3'), ('b', '5')]) 48 | a = int(request.args.get('a', '0')) 49 | b = int(request.args.get('b', '0')) 50 | return str(a + b) 51 | 52 | 53 | @app.route('/hello_user_post/', methods=['POST']) 54 | def hello_user2(): 55 | # request.form # form value in HTML 56 | # request.files # attached files 57 | # requests.json # parsed JSON format data 58 | # requests.json == request.get_json(force=True) 59 | print(request.json) 60 | print(request.get_json(force=True)) 61 | 62 | json_data = request.get_json(force=True) 63 | name = json_data.get('name', 'annonymous') 64 | koname = json_data.get('ko_name', '익명자') 65 | return f'{name} ({koname})' 66 | 67 | # def dynamic_method(): 68 | # if request.method == 'get': 69 | # if request.method == 'post': 70 | 71 | @app.route('/return_json/', methods=['POST']) 72 | def return_json(): 73 | json_data = request.get_json(force=True) 74 | name = json_data.get('name', 'annonymous') 75 | koname = json_data.get('ko_name', '익명자') 76 | 77 | response = { 78 | 'concatenated_name': f'{name} ({koname})' 79 | } 80 | return response 81 | 82 | 83 | if __name__ == "__main__": 84 | parser = argparse.ArgumentParser(description='Flask option arguments') 85 | parser.add_argument('--host', type=str, default=None, help='Default is localhost') 86 | parser.add_argument('--port', type=int, default=None, help='Default is :5000') 87 | 88 | args = parser.parse_args() 89 | host = args.host 90 | port = args.port 91 | 92 | print('Flask practice') 93 | app.run(host=host, port=port) 94 | -------------------------------------------------------------------------------- /app/functions.py: -------------------------------------------------------------------------------- 1 | def external_add(a: int, b: int): 2 | print('called external function') 3 | return a + b 4 | --------------------------------------------------------------------------------