Bottle 프레임워크로 Rest API 구축하기
게시 됨: 2022-03-11REST API는 웹 백엔드와 프론트엔드, 서로 다른 웹 서비스 간에 인터페이스를 설정하는 일반적인 방법이 되었습니다. 이러한 종류의 인터페이스의 단순성과 다양한 네트워크 및 프레임워크 전반에 걸친 HTTP 및 HTTPS 프로토콜의 유비쿼터스 지원으로 인해 상호 운용성 문제를 고려할 때 쉽게 선택할 수 있습니다.
Bottle은 미니멀리스트 Python 웹 프레임워크입니다. 가볍고 빠르며 사용하기 쉬우며 RESTful 서비스 구축에 적합합니다. Andriy Kornatskyy가 수행한 기본 비교에 따르면 응답 시간 및 처리량(초당 요청 수) 측면에서 상위 3개 프레임워크 중 하나입니다. DigitalOcean에서 사용할 수 있는 가상 서버에 대한 자체 테스트에서 uWSGI 서버 스택과 Bottle의 조합이 요청당 140μs의 낮은 오버헤드를 달성할 수 있음을 발견했습니다.
이 기사에서는 Bottle을 사용하여 RESTful API 서비스를 구축하는 방법에 대한 안내를 제공합니다.
설치 및 구성
Bottle 프레임워크는 부분적으로 가벼운 무게 덕분에 인상적인 성능을 달성합니다. 사실 전체 라이브러리는 하나의 파일 모듈로 배포됩니다. 즉, 다른 프레임워크만큼 손을 잡지는 않지만 더 유연하고 다양한 기술 스택에 맞게 조정할 수 있습니다. 따라서 Bottle은 성능과 사용자 정의 가능성이 가장 중요하고 보다 견고한 프레임워크의 시간 절약 이점이 덜 고려되는 프로젝트에 가장 적합합니다.
Bottle의 유연성은 자신의 스택을 반영하지 않을 수 있으므로 플랫폼 설정에 대한 자세한 설명을 약간 무익하게 만듭니다. 그러나 옵션에 대한 간략한 개요와 설정 방법에 대해 자세히 알아볼 수 있는 위치는 다음과 같습니다.
설치
Bottle 설치는 다른 Python 패키지를 설치하는 것만큼 쉽습니다. 옵션은 다음과 같습니다.
- 시스템의 패키지 관리자를 사용하여 시스템에 설치합니다. Debian Jessie(현재 안정)는 버전 0.12를 python-bottle 로 패키징합니다.
-
pip install bottle
과 함께 Python 패키지 색인을 사용하여 시스템에 설치합니다. - 가상 환경에 설치합니다(권장).
가상 환경에 Bottle을 설치하려면 virtualenv 및 pip 도구가 필요합니다. 시스템에 이미 설치되어 있을 수 있지만 설치하려면 virtualenv 및 pip 설명서를 참조하십시오.
Bash에서 Python 3으로 환경을 만듭니다.
$ virtualenv -p `which python3` env
-p `which python3`
매개변수를 억제하면 시스템에 있는 기본 Python 인터프리터(일반적으로 Python 2.7)가 설치됩니다. Python 2.7이 지원되지만 이 자습서에서는 Python 3.4를 가정합니다.
이제 환경을 활성화하고 Bottle을 설치합니다.
$ . env/bin/activate $ pip install bottle
그게 다야 병이 설치되어 사용할 준비가 되었습니다. virtualenv 또는 pip 에 익숙하지 않은 경우 해당 설명서가 최고입니다. 구경하다! 그들은 그만한 가치가 있습니다.
섬기는 사람
Bottle은 Python의 표준 WSGI(Web Server Gateway Interface)를 준수하므로 모든 WSGI 호환 서버와 함께 사용할 수 있습니다. 여기에는 uWSGI, Tornado, Gunicorn, Apache, Amazon Beanstalk, Google App Engine 등이 포함됩니다.
올바른 설정 방법은 환경에 따라 조금씩 다릅니다. Bottle은 WSGI 인터페이스를 준수하는 개체를 노출하며 이 개체와 상호 작용하도록 서버를 구성해야 합니다.
서버를 설정하는 방법에 대한 자세한 내용은 여기에서 서버의 문서와 Bottle의 문서를 참조하십시오.
데이터 베이스
Bottle은 데이터베이스에 구애받지 않으며 데이터가 어디에서 오는지 상관하지 않습니다. 앱에서 데이터베이스를 사용하려는 경우 Python 패키지 색인에는 SQLAlchemy, PyMongo, MongoEngine, CouchDB 및 DynamoDB용 Boto와 같은 몇 가지 흥미로운 옵션이 있습니다. 선택한 데이터베이스에서 작동하도록 하려면 적절한 어댑터만 있으면 됩니다.
병 프레임워크 기본 사항
이제 Bottle에서 기본 앱을 만드는 방법을 알아보겠습니다. 코드 예제의 경우 Python >= 3.4라고 가정하겠습니다. 그러나 내가 여기에 쓸 대부분의 내용은 Python 2.7에서도 작동합니다.
Bottle의 기본 앱은 다음과 같습니다.
import bottle app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)
내가 기본이라고 말할 때, 이 프로그램은 "Hello World"조차 하지 않는다는 것을 의미합니다. ("Hello World"라고 응답한 REST 인터페이스에 마지막으로 액세스한 것이 언제였습니까?) 127.0.0.1:8000
에 대한 모든 HTTP 요청은 404 Not Found 응답 상태를 수신합니다.
병에 담긴 앱
Bottle에는 여러 개의 앱 인스턴스가 생성될 수 있지만 편의를 위해 첫 번째 인스턴스가 생성됩니다. 기본 앱입니다. Bottle은 이러한 인스턴스를 모듈 내부 스택에 보관합니다. Bottle로 무언가를 할 때마다(예: 앱 실행 또는 경로 연결) 말하는 앱을 지정하지 않으면 기본 앱을 참조합니다. 사실, app = application = bottle.default_app()
라인은 이 기본 앱에 존재할 필요조차 없지만 Gunicorn, uWSGI 또는 일부 일반 WSGI 서버로 기본 앱을 쉽게 호출할 수 있도록 존재합니다.
여러 앱의 가능성은 처음에는 혼란스러워 보일 수 있지만 Bottle에 유연성을 추가합니다. 애플리케이션의 다른 모듈에 대해 다른 Bottle 클래스를 인스턴스화하고 필요에 따라 다른 구성으로 설정하여 특수 Bottle 앱을 만들 수 있습니다. 이러한 다른 앱은 Bottle의 URL 라우터를 통해 다른 URL에서 액세스할 수 있습니다. 이 튜토리얼에서는 이에 대해 자세히 다루지 않겠지만 여기와 여기에서 Bottle의 문서를 살펴보는 것이 좋습니다.
서버 호출
스크립트의 마지막 줄은 표시된 서버를 사용하여 Bottle을 실행합니다. 서버가 지정되지 않은 경우 여기의 경우와 같이 기본 서버는 개발 목적으로만 적합한 Python의 내장 WSGI 참조 서버입니다. 다음과 같이 다른 서버를 사용할 수 있습니다.
bottle.run(server='gunicorn', host = '127.0.0.1', port = 8000)
이것은 이 스크립트를 실행하여 앱을 시작할 수 있는 구문 설탕입니다. 예를 들어 이 파일의 이름이 main.py
인 경우 python main.py
를 실행하여 앱을 시작할 수 있습니다. Bottle은 이러한 방식으로 사용할 수 있는 매우 광범위한 서버 어댑터 목록을 제공합니다.
일부 WSGI 서버에는 Bottle 어댑터가 없습니다. 이들은 서버의 자체 실행 명령으로 시작할 수 있습니다. 예를 들어 uWSGI에서는 다음과 같이 uwsgi
를 호출하기만 하면 됩니다.
$ uwsgi --http :8000 --wsgi-file main.py
파일 구조에 대한 참고 사항
Bottle은 앱의 파일 구조를 전적으로 귀하에게 맡깁니다. 내 파일 구조 정책은 프로젝트마다 발전하지만 MVC 철학을 기반으로 하는 경향이 있습니다.
REST API 빌드
물론 요청된 모든 URI에 대해 404만 반환하는 서버가 필요한 사람은 없습니다. REST API를 구축하겠다고 약속했으므로 실행해 보겠습니다.
이름 집합을 조작하는 인터페이스를 구축한다고 가정합니다. 실제 앱에서는 이를 위해 데이터베이스를 사용할 수 있지만 이 예에서는 메모리 내 set
데이터 구조만 사용합니다.
API의 골격은 다음과 같습니다. 이 코드는 프로젝트의 아무 곳에나 배치할 수 있지만 api/names.py
와 같은 별도의 API 파일을 권장합니다.
from bottle import request, response from bottle import post, get, put, delete _names = set() # the set of names @post('/names') def creation_handler(): '''Handles name creation''' pass @get('/names') def listing_handler(): '''Handles name listing''' pass @put('/names/<name>') def update_handler(name): '''Handles name updates''' pass @delete('/names/<name>') def delete_handler(name): '''Handles name deletions''' pass
라우팅
보다시피 Bottle의 라우팅은 데코레이터를 사용하여 수행됩니다. 가져온 데코레이터는 이 네 가지 작업에 대한 레지스터 처리기를 post
, get
, put
및 delete
합니다. 이러한 작업을 다음과 같이 분류할 수 있는 방법을 이해합니다.
- 위의 모든 데코레이터는
default_app
라우팅 데코레이터의 바로 가기입니다. 예를 들어@get()
데코레이터는bottle.default_app().get()
을 핸들러에 적용합니다. -
default_app
의 라우팅 메서드는 모두route()
의 바로 가기입니다. 따라서default_app().get('/')
은default_app().route(method='GET', '/')
와 동일합니다.
따라서 @get('/')
은 @route(method='GET', '/')
와 동일하며 @bottle.default_app().route(method='GET', '/')
와 동일합니다. , 이들은 서로 바꿔서 사용할 수 있습니다.
@route
데코레이터에 대한 한 가지 유용한 점은 예를 들어 동일한 핸들러를 사용하여 객체 업데이트와 삭제를 모두 처리하려는 경우 다음과 같이 처리하는 메서드 목록을 전달할 수 있다는 것입니다.
@route('/names/<name>', method=['PUT', 'DELETE']) def update_delete_handler(name): '''Handles name updates and deletions''' pass
자, 이제 이러한 핸들러 중 일부를 구현해 보겠습니다.
POST: 리소스 생성
POST 핸들러는 다음과 같을 수 있습니다.
import re, json namepattern = re.compile(r'^[a-zA-Z\d]{1,64}$') @post('/names') def creation_handler(): '''Handles name creation''' try: # parse input data try: data = request.json() except: raise ValueError if data is None: raise ValueError # extract and validate name try: if namepattern.match(data['name']) is None: raise ValueError name = data['name'] except (TypeError, KeyError): raise ValueError # check for existence if name in _names: raise KeyError except ValueError: # if bad request data, return 400 Bad Request response.status = 400 return except KeyError: # if name already exists, return 409 Conflict response.status = 409 return # add name _names.add(name) # return 200 Success response.headers['Content-Type'] = 'application/json' return json.dumps({'name': name})
글쎄, 그것은 꽤 많다. 이러한 단계를 부분적으로 검토해 보겠습니다.
바디 파싱
이 API를 사용하려면 사용자가 "name"이라는 속성을 사용하여 본문에 JSON 문자열을 POST해야 합니다.
이전에 bottle
에서 가져온 request
객체는 항상 현재 요청을 가리키고 모든 요청 데이터를 보유합니다. 그 body
속성은 요청 본문의 바이트 스트림을 포함하며, 스트림 객체를 읽을 수 있는 모든 함수(예: 파일 읽기)에서 액세스할 수 있습니다.
request.json()
메서드는 "application/json" 콘텐츠 유형에 대한 요청 헤더를 확인하고 올바른 경우 본문을 구문 분석합니다. Bottle이 잘못된 본문(예: 비어 있거나 잘못된 콘텐츠 유형 포함)을 감지하면 이 메서드는 None
을 반환하므로 ValueError
가 발생합니다. JSON 파서에서 형식이 잘못된 JSON 콘텐츠를 감지한 경우 다시 ValueError
로 catch하고 다시 발생시키는 예외를 발생시킵니다.

개체 구문 분석 및 유효성 검사
오류가 없으면 요청 본문을 data
변수가 참조하는 Python 객체로 변환했습니다. "name" 키가 있는 사전을 받은 경우 data['name']
을 통해 사전에 액세스할 수 있습니다. 이 키가 없는 사전을 받은 경우 액세스를 시도하면 KeyError
예외가 발생합니다. 사전이 아닌 다른 것을 받으면 TypeError
예외가 발생합니다. 이러한 오류가 발생하면 다시 한 번 잘못된 입력을 나타내는 ValueError
로 다시 발생시킵니다.
이름 키의 형식이 올바른지 확인하려면 여기에서 만든 이름 패턴 마스크와 같은 namepattern
마스크에 대해 테스트해야 합니다. 키 name
이 문자열이 namepattern.match()
는 TypeError
를 발생시키고 일치하지 않으면 None
을 반환합니다.
이 예의 마스크에서 이름은 1~64자의 공백이 없는 ASCII 영숫자여야 합니다. 이것은 간단한 유효성 검사이며 예를 들어 가비지 데이터가 있는 개체를 테스트하지 않습니다. FormEncode와 같은 도구를 사용하여 더 복잡하고 완전한 유효성 검사를 수행할 수 있습니다.
존재 테스트
요청을 이행하기 전 마지막 테스트는 주어진 이름이 세트에 이미 존재하는지 여부입니다. 보다 구조화된 앱에서 해당 테스트는 전용 모듈에서 수행되어야 하고 특수 예외를 통해 API에 신호를 보내야 하지만 세트를 직접 조작하기 때문에 여기에서 수행해야 합니다.
KeyError
를 발생시켜 이름의 존재를 알립니다.
오류 응답
요청 객체가 모든 요청 데이터를 보유하는 것처럼 응답 객체는 응답 데이터에 대해서도 동일한 작업을 수행합니다. 응답 상태를 설정하는 방법에는 두 가지가 있습니다.
response.status = 400
그리고:
response.status = '400 Bad Request'
이 예에서는 더 간단한 형식을 선택했지만 두 번째 형식을 사용하여 오류의 텍스트 설명을 지정할 수 있습니다. 내부적으로 Bottle은 두 번째 문자열을 분할하고 숫자 코드를 적절하게 설정합니다.
성공 응답
모든 단계가 성공하면 _names
집합에 이름을 추가하고 Content-Type
응답 헤더를 설정하고 응답을 반환하여 요청을 이행합니다. 함수에서 반환된 모든 문자열은 200 Success
응답의 응답 본문으로 처리되므로 json.dumps
를 사용하여 간단히 생성합니다.
GET: 리소스 목록
이름 생성에서 계속해서 이름 목록 처리기를 구현합니다.
@get('/names') def listing_handler(): '''Handles name listing''' response.headers['Content-Type'] = 'application/json' response.headers['Cache-Control'] = 'no-cache' return json.dumps({'names': list(_names)})
이름을 나열하는 것이 훨씬 쉬웠죠? 이름 생성과 비교할 때 여기에서 할 일이별로 없습니다. 일부 응답 헤더를 설정하고 모든 이름의 JSON 표현을 반환하기만 하면 됩니다.
PUT: 리소스 업데이트
이제 업데이트 방법을 구현하는 방법을 살펴보겠습니다. create 메소드와 크게 다르지 않지만 이 예제를 사용하여 URI 매개변수를 소개합니다.
@put('/names/<oldname>') def update_handler(name): '''Handles name updates''' try: # parse input data try: data = json.load(utf8reader(request.body)) except: raise ValueError # extract and validate new name try: if namepattern.match(data['name']) is None: raise ValueError newname = data['name'] except (TypeError, KeyError): raise ValueError # check if updated name exists if oldname not in _names: raise KeyError(404) # check if new name exists if name in _names: raise KeyError(409) except ValueError: response.status = 400 return except KeyError as e: response.status = e.args[0] return # add new name and remove old name _names.remove(oldname) _names.add(newname) # return 200 Success response.headers['Content-Type'] = 'application/json' return json.dumps({'name': newname})
업데이트 작업의 본문 스키마는 생성 작업의 경우와 동일하지만 이제 @put('/names/<oldname>')
경로에 정의된 대로 URI에 새로운 oldname
매개변수도 있습니다.
URI 매개변수
보시다시피 URI 매개변수에 대한 Bottle의 표기법은 매우 간단합니다. 원하는 만큼 많은 매개변수를 사용하여 URI를 빌드할 수 있습니다. Bottle은 URI에서 자동으로 추출하여 요청 핸들러에 전달합니다.
@get('/<param1>/<param2>') def handler(param1, param2): pass
계단식 경로 데코레이터를 사용하여 선택적 매개변수로 URI를 빌드할 수 있습니다.
@get('/<param1>') @get('/<param1>/<param2>') def handler(param1, param2 = None) pass
또한 Bottle은 URI에서 다음 라우팅 필터를 허용합니다.
-
int
int
로 변환될 수 있는 매개변수만 일치시키고 변환된 값을 핸들러에 전달합니다.@get('/<param:int>') def handler(param): pass
-
float
int
와 같지만 부동 소수점 값을 사용합니다.@get('/<param:float>') def handler(param): pass
-
re
(정규 표현식)
주어진 정규식과 일치하는 매개변수만 일치:
@get('/<param:re:^[az]+$>') def handler(param): pass
-
path
유연한 방식으로 URI 경로의 하위 세그먼트를 찾습니다.
@get('/<param:path>/id>') def handler(param): pass
성냥:
/x/id
,x
를param
으로 전달합니다./x/y/id
,x/y
를param
으로 전달합니다.
DELETE: 리소스 삭제
GET 메소드와 마찬가지로 DELETE 메소드는 우리에게 거의 뉴스를 제공하지 않습니다. 상태를 설정하지 않고 None
을 반환하면 빈 본문과 200 상태 코드가 포함된 응답이 반환된다는 점에 유의하세요.
@delete('/names/<name>') def delete_handler(name): '''Handles name updates''' try: # Check if name exists if name not in _names: raise KeyError except KeyError: response.status = 404 return # Remove name _names.remove(name) return
최종 단계: API 활성화
이름 API를 api/names.py
로 저장했다고 가정하면 이제 기본 애플리케이션 파일 main.py
에서 이러한 경로를 활성화할 수 있습니다.
import bottle from api import names app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)
names
모듈만 가져왔습니다. 기본 앱에 연결된 URI로 모든 메서드를 장식했으므로 추가 설정을 수행할 필요가 없습니다. 우리의 방법은 이미 준비되어 있으며 액세스할 준비가 되어 있습니다.
Curl 또는 Postman과 같은 도구를 사용하여 API를 사용하고 수동으로 테스트할 수 있습니다. (Curl을 사용하는 경우 JSON 포맷터를 사용하여 응답을 덜 복잡하게 만들 수 있습니다.)
보너스: CORS(Cross Origin Resource Sharing)
REST API를 빌드하는 일반적인 이유 중 하나는 AJAX를 통해 JavaScript 프론트 엔드와 통신하는 것입니다. 일부 애플리케이션의 경우 이러한 요청은 API의 홈 도메인뿐만 아니라 모든 도메인에서 허용되어야 합니다. 기본적으로 대부분의 브라우저는 이 동작을 허용하지 않으므로 이를 허용하도록 Bottle에서 CORS(교차 출처 리소스 공유)를 설정하는 방법을 보여드리겠습니다.
from bottle import hook, route, response _allow_origin = '*' _allow_methods = 'PUT, GET, POST, DELETE, OPTIONS' _allow_headers = 'Authorization, Origin, Accept, Content-Type, X-Requested-With' @hook('after_request') def enable_cors(): '''Add headers to enable CORS''' response.headers['Access-Control-Allow-Origin'] = _allow_origin response.headers['Access-Control-Allow-Methods'] = _allow_methods response.headers['Access-Control-Allow-Headers'] = _allow_headers @route('/', method = 'OPTIONS') @route('/<path:path>', method = 'OPTIONS') def options_handler(path = None): return
hook
데코레이터를 사용하면 각 요청 전후에 함수를 호출할 수 있습니다. 우리의 경우 CORS를 활성화하려면 각 응답에 대해 Access-Control-Allow-Origin
, -Allow-Methods
및 -Allow-Headers
헤더를 설정해야 합니다. 이는 요청자에게 표시된 요청을 처리할 것임을 나타냅니다.
또한 클라이언트는 서버에 OPTIONS HTTP 요청을 보내 실제로 다른 방법으로 요청할 수 있는지 확인할 수 있습니다. 이 포괄적인 샘플 예제를 사용하여 모든 OPTIONS 요청에 200 상태 코드와 빈 본문으로 응답합니다.
이를 활성화하려면 저장하고 기본 모듈에서 가져오기만 하면 됩니다.
마무리
그게 다야!
이 자습서에서는 Bottle 웹 프레임워크를 사용하여 Python 앱용 REST API를 만드는 기본 단계를 다루려고 했습니다.
자습서 및 API 참조 문서를 방문하여 이 작지만 강력한 프레임워크에 대한 지식을 심화할 수 있습니다.