
REST
GET, POST, PUT, PATCH, DELETE 의 5가지 동사CRUD 기능과 비슷특정 도시의 카페들에 대한 데이터를 수집하여 원격 근무에 적합한 카페를 찾는 API 구축
(런던의 카페 정보가 담긴 db 파일로 실습)
🔍 유의 사항
- 사용자에게 임의의 카페 정보를 제공하는
/random경로 생성
- GET 요청은 모든 라우트에서 기본으로 하는 요청
/random경로로 GET 요청을 하면 플라스크 서버가 데이터베이스에서 임의의 카페를 가져옴
- SQLAlchemy의 random 함수는 파이썬의 함수와 다름
(SQLAlchemy가 직접 Python 객체의 리스트를 다루는 것이 아니기 때문)- render_template()으로 HTML 템플릿을 반환하는 대신 필요한 데이터가 포함된 JSON을 리턴
- 서버가 API 역할을 하기 때문
- random_cafe SQL 알케미 객체를 JSON으로 변환해야 함 → 직렬화
- 플라스크에 직렬화를 돕는
jsonify()메소드 내장- 리턴할 JSON의 구조는 직접 제공해야 한다
/random경로 작동시키기
- id와 같은 일부 속성을 생략하거나, 일부 속성들을 하위 섹션으로 그룹화하여 반환 가능
- 데이터베이스 행 객체를 JSON으로 직렬화할 수 있음
(행 객체를 딕셔너리로 변환한 다음jsonify()를 사용하여 JSON으로 변환)
🔍 유의 사항
- 모든 카페 목록을 보여주는
/all경로 생성/all경로로 GET 요청을 하면 서버가 데이터베이스의 모든 카페를 JSON으로 반환
🔍 유의 사항
- 특정 지역의 모든 카페를 검색하는
/search경로 생성- 위치를 매개변수로 전달하여
/search경로로 GET 요청
- 매개변수는 ?와 함께 URL에 전달
- 매개변수로 올바른 위치를 전달하지 않으면 에러 메시지를 JSON 형식으로 전달하기
🔍 유의 사항
- Postman
- API 경로 테스트 시 사용하는 도구
- 매개변수를 브라우저 URL 표시줄에 입력할 필요 없음
- API 문서를 자동으로 생성
- 요청 매개변수에 대한 키-값 쌍을 추가할 수 있으며, URL 형식이 자동으로 지정됨
Create a Collection으로 컬렉션 생성 후Add request로 생성했던 경로들 모두 추가
🔍 유의 사항
- Postman으로 WTForm 또는 HTML 양식을 작성하지 않고 API POST 요청 테스트 가능
- 요청방식을 POST로 설정 후
/add경로 붙여넣기Body에서x-www-form-urlencoded옵션 선택- Postman의 본문 탭에 입력하는 키-값 쌍은
<input>요소와 동일
(파이썬 코드에서request.form.get()에 넣은 이름과 같아야 한다)- 파이썬에서 코드 작성 후 실행한 다음 포스트맨에서 POST 요청 테스트
🔍 유의 사항
- PUT
- 기존 정보의 전체를 새 것으로 교체하여 데이터베이스를 업데이트
- 🚲 앞바퀴만 망가진 자전거를 아예 새 자전거로 교체하는 것과 같음
- PATCH
- 데이터 일부만 새로 업데이트
- 🛞 망가진 앞바퀴 부품만 새로 교체하는 것과 같음
🔍 유의 사항
- 사용자가 어느 카페의 블랙커피 가격 변동 사항을 제출하려면
- GET 요청으로 해당 카페의
id를 알아낸 뒤,coffee_price필드를 업데이트할 수 있음- 나머지 데이터는 변경할 필요 없기 때문에 PUT 보다 PATCH 요청이 효율적
PATCH요청 경로 생성- API가 RESTful이 되기 위한 이상적인 경로 :
/update-price/<cafe_id>
- 요청을 매개변수로 전달하여 업데이트된 블랙 커피 한 잔 가격을 제공해야 한다
- 경로에
id가 존재하지 않을 경우 올바른 피드백 제공하기
id가 22인 카페의 커피 가격이 업데이트됨
🔍 유의 사항
- 폐업한 카페가 있을 경우 사용자가
DELETE요청으로 데이터베이스 업데이트- 아무나 데이터베이스에서 항목을 삭제하지 못하도록 보안 기능 추가하기
TopSecretAPIKey라는 API 키 값이 있으면 삭제 요청이 가능- 키가 없으면 해당 요청을 할 권한이 없다고 알려주기
DELETE요청 경로를/report-closed/<cafe_id>에 추가- 사용자가 잘못된 api키값을 갖고 있거나 해당 id의 카페가 존재하지 않을 경우 각각 다른 응답 필요
🔍 유의 사항
- Postman에서 API 사용 방법을 문서화하기
해당 콜렉션→View documentation→Publish→ 생성된 링크 이용- 링크에서 언어를
Python - Requests로 변경해서 코드 보기- index.html에서 앵커 태그로 API 문서 링크 포함시키기
⌨️ main.py
from flask import Flask, jsonify, render_template, request
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import Integer, String, Boolean
from sqlalchemy.sql.functions import random
app = Flask(__name__)
# CREATE DB
class Base(DeclarativeBase):
pass
# Connect to Database
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///cafes.db'
db = SQLAlchemy(model_class=Base)
db.init_app(app)
# Cafe TABLE Configuration
class Cafe(db.Model):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(250), unique=True, nullable=False)
map_url: Mapped[str] = mapped_column(String(500), nullable=False)
img_url: Mapped[str] = mapped_column(String(500), nullable=False)
location: Mapped[str] = mapped_column(String(250), nullable=False)
seats: Mapped[str] = mapped_column(String(250), nullable=False)
has_toilet: Mapped[bool] = mapped_column(Boolean, nullable=False)
has_wifi: Mapped[bool] = mapped_column(Boolean, nullable=False)
has_sockets: Mapped[bool] = mapped_column(Boolean, nullable=False)
can_take_calls: Mapped[bool] = mapped_column(Boolean, nullable=False)
coffee_price: Mapped[str] = mapped_column(String(250), nullable=True)
# 키(열 이름)와 값(해당 열의 값들) 쌍의 딕셔너리를 생성하는 함수
def to_dict(self):
# getattr(객체이름, 속성이름) : 해당 객체의 특정 속성 값을 동적으로 가져옴
return {column.name: getattr(self, column.name) for column in self.__table__.columns}
with app.app_context():
db.create_all()
@app.route("/")
def home():
return render_template("index.html")
# HTTP GET - Read Record
@app.route("/random")
def get_random_cafe():
cafes = db.session.query(Cafe).all()
# Cafe 테이블에서 임의의 레코드 하나 선택(데이터베이스에서 쿼리를 수행하는 방식)
random_cafe = db.session.query(Cafe).order_by(random()).first()
# jsonify() : Flask에서 리스트나 딕셔너리 형 데이터를 JSON 형식으로 변환하는 함수
return jsonify(cafe=random_cafe.to_dict())
@app.route("/all")
def get_all_cafes():
cafes = db.session.query(Cafe).all()
return jsonify(cafes=[cafe.to_dict() for cafe in cafes])
@app.route("/search")
def get_cafe_at_location():
query_location = request.args.get("loc")
cafes = db.session.query(Cafe).filter_by(location=query_location).all()
if cafes:
return jsonify(cafes=[cafe.to_dict() for cafe in cafes])
else:
return jsonify(error={"Not Found": "Sorry, we don't have a cafe at that location."})
# HTTP POST - Create Record
@app.route("/add", methods=["GET", "POST"])
def post_new_cafe():
new_cafe = Cafe(
name=request.form.get("name"),
map_url=request.form.get("map_url"),
img_url=request.form.get("img_url"),
location=request.form.get("loc"),
has_sockets=bool(int(request.form.get("sockets"))),
has_toilet=bool(int(request.form.get("toilet"))),
has_wifi=bool(int(request.form.get("wifi"))),
can_take_calls=bool(int(request.form.get("calls"))),
seats=request.form.get("seats"),
coffee_price=request.form.get("coffee_price"),
)
db.session.add(new_cafe)
db.session.commit()
return jsonify(response={"success": "Successfully added the new cafe."})
# HTTP PUT/PATCH - Update Record
@app.route("/update-price/<int:cafe_id>", methods=["GET", "PATCH"])
def patch_new_price(cafe_id):
new_price = request.args.get("new_price")
cafe = db.session.query(Cafe).get(cafe_id)
if cafe:
cafe.coffee_price = new_price
db.session.commit()
return jsonify(response={"success": "Successfully updated the price."}), 200
else:
return jsonify(error={"Not Found": "Sorry a cafe with that id was not found in the database."}), 404
# HTTP DELETE - Delete Record
@app.route("/report-closed/<cafe_id>", methods=["GET", "DELETE"])
def delete_closed_cafe(cafe_id):
api_key = request.args.get("api-key")
if api_key == "TopSecretAPIKey":
cafe = db.session.query(Cafe).get(cafe_id)
if cafe:
db.session.delete(cafe)
db.session.commit()
return jsonify(response={"success": "Successfully deleted the cafe from the database."}), 200
else:
return jsonify(error={"Not Found": "Sorry a cafe with that id was not found in the database."}), 404
else:
return jsonify(error={"Forbidden": "Sorry, that's not allowed. Make sure you have the correct api_key."}), 403
if __name__ == '__main__':
app.run(debug=True)
🏗️ index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Cafe&Wifi API</title>
</head>
<body>
<h1>Welcome to the Cafe & Wifi API</h1>
<a href="https://documenter.getpostman.com/view/39314524/2sAY4uC3fz">Read the Documentation</a>
</body>
</html>