
가고 싶은 장소들과 상한가를 정해두면, 더 저렴한 항공권이 나왔을 때 알리는 프로그램
🔍 유의 사항
- Sheety API의 무료 멤버십 이용 시 불필요한 요청 줄이기
- pprint.pprint( object, ... )
- 구조가 복잡한 데이터를 pprint 모듈로 보기 좋게 출력 가능
- pprint = pretty print
- pprint()로 출력한 결과
- 사용자의 환경변수를 📄.env에 저장
📄.env
SHEETY_ENDPOINT="https://api.sheety.co/<유저이름>/citiesToFly/prices"
BEARER_TOKEN="개인 토큰"
⌨️ data_manager.py
import os
import requests
from dotenv import load_dotenv
# 환경변수 불러오기
load_dotenv()
class DataManager:
def __init__(self):
self.sheety_endpoint = os.getenv("SHEETY_ENDPOINT")
self.bearer_token = os.getenv("BEARER_TOKEN")
self.bearer_headers = {"Authorization": f"Bearer {self.bearer_token}"}
self.destination_data = {}
def get_destination_data(self):
response = requests.get(url=self.sheety_endpoint,
headers=self.bearer_headers)
data = response.json()
self.destination_data = data["prices"]
return self.destination_data
def update_destination_code(self):
for city in self.destination_data:
new_data = {
"price": {"iataCode": city["iataCode"]}
}
response = requests.put(url=f"{self.sheety_endpoint}/{city['id']}",
json=new_data,
headers=self.bearer_headers)
⌨️ flight_search.py
class FlightSearch:
def get_destination_code(self, city_name):
# 우선 시드에 코드가 제대로 입력되는지 보기 위해 테스트 문구 넣기
code = "TESTING"
return code
⌨️ main.py
from data_manager import DataManager
from flight_search import FlightSearch
data_manager = DataManager()
sheet_data = data_manager.get_destination_data()
# 구글 시트의 공항 코드가 비어 있는지 확인
if sheet_data[0]["iataCode"] == "":
from flight_search import FlightSearch
flight_search = FlightSearch()
for row in sheet_data:
row["iataCode"] = flight_search.get_destination_code(row["city"])
data_manager.destination_data = sheet_data
data_manager.update_destination_code()
출력되는 것을 체크한 뒤에는 TESTING을 모두 삭제하기
🔍 유의 사항
- Amadeus Flight Search API (무료 가입, 신용카드 정보 불필요)
My Self-Service Workspace→My apps→ 새 앱 생성 후 API 받기- Amadeus API 키&토큰 사용법
- API id와 secret으로 토큰을 요청해야 한다
- 응답 결과
{ 'type': 'amadeusOAuth2Token', 'username': '개인 이메일 주소', 'application_name': 'Flight Deals', 'client_id': '개인 API key', 'token_type': 'Bearer', 'access_token': '발급받은 토큰', 'expires_in': 1799, 'state': 'approved', 'scope': '' }
- 키
expires_in는 1799초(약 30분) 후 토큰이 만료된다는 뜻- 도시별 IATA(국제항공운송협회) 코드를 구글 시트에 붙이기
- Amadeus Search for Airport Codes by City name
- 여러 개의 공항이 있는 도시는 공항 코드와는 별도로 도시 IATA 코드 존재
(Cities with multiple commercial airports)- 공항 코드가 출력되지 않는 상황을 예외처리하기
- 하루에 제한된 요청 수를 넘었을 때
- 아마데우스 테스트 API에 없는 공항 코드를 요청했을 때
(인기 있는 도시들 위주로 넣는 것이 좋음)- 무료 API로 너무 빠르게 여러 번 요청할 경우 차단되거나 오류가 발생할 수 있음
- time.sleep()으로 각 요청 사이에 간격 두기
⌨️ flight_search.py
import os
import requests
from dotenv import load_dotenv
load_dotenv()
IATA_ENDPOINT = "https://test.api.amadeus.com/v1/reference-data/locations/cities"
TOKEN_ENDPOINT = "https://test.api.amadeus.com/v1/security/oauth2/token"
class FlightSearch:
def __init__(self):
self.api_key = os.getenv("API_KEY")
self.api_secret = os.getenv("API_SECRET")
# 프로그램을 시작할 때마다 토큰을 새로 발급
self.token = self.get_new_token()
def get_new_token(self):
header = {'Content-Type': 'application/x-www-form-urlencoded'}
body = {
'grant_type': 'client_credentials',
'client_id': self.api_key,
'client_secret': self.api_secret
}
response = requests.post(url=TOKEN_ENDPOINT, headers=header, data=body)
print(f"Your token is {response.json()['access_token']}")
return response.json()["access_token"]
def get_destination_code(self, city_name):
headers = {"Authorization": f"Bearer {self.token}"}
query = {
"keyword": city_name,
"max": "2",
"include": "AIRPORTS"
}
response = requests.get(url=IATA_ENDPOINT, headers=headers, params=query)
try:
code = response.json()["data"][0]['iataCode']
except IndexError:
print(f"IndexError: No airport code found for {city_name}.")
return "N/A"
except KeyError:
print(f"KeyError: No airport code found for {city_name}.")
return "Not Found"
return code
⌨️ main.py
import time
from data_manager import DataManager
from flight_search import FlightSearch
# ==================== Set up the Flight Search ====================
data_manager = DataManager()
sheet_data = data_manager.get_destination_data()
flight_search = FlightSearch()
# ==================== Update the Airport Codes in Google Sheet ====================
for row in sheet_data:
if row["iataCode"] == "":
row["iataCode"] = flight_search.get_destination_code(row["city"])
# 각 요청 간 2초 정도 텀 주기
time.sleep(2)
data_manager.destination_data = sheet_data
data_manager.update_destination_codes()
🔍 유의 사항
- Amadeus Flight Offer
- ❗️강의에서는 내일부터 6개월 이내에 출발하는 저렴한 항공권을 검색 가능했으나,
현재는 설정한 출발일과 리턴일(설정할 경우 왕복, 미설정할 경우 편도)의 티켓만 검색하므로
내일 출발하는 편도 항공권 중 가장 저렴한 것을 찾는 방식으로 변경- 조건
- 출발 도시 : 런던(LON)
- 도착 도시 : 구글 시트에 있는 도시들
- 출발일 : 내일
- datetime.timedelta() 사용 (Day32 ❖ datetime 모듈)
- split() 함수로 json 데이터에서 날짜의 첫 부분만 가져오기(T 이전)
- 성인 1명의 편도 항공권
- 통화 : GBP
- 🐞 디버깅 → 특정 목적지로 향하는 항공권이 없을 때
- 출발 도시를 큰 공항이 있는 곳으로 변경
- 도착 도시를 큰 공항이 있는 곳으로 변경
- 통화를 USD로 변경
시트의 첫 번째 도시에 대해 check_flight를 한 결과
→ 이 데이터를 파싱해서 가장 저렴한 항공편을 추출해야 한다
⌨️ flight_data.py
class FlightData:
def __init__(self, price, origin_airport, destination_airport, out_date):
self.price = price
self.origin_airport = origin_airport
self.destination_airport = destination_airport
self.out_date = out_date
def find_cheapest_flight(data):
# 오류 처리
if data is None or not data["data"]:
print("No flight data")
return FlightData("N/A", "N/A", "N/A", "N/A")
# 먼저 data["data"]의 1번째 원소를 가장 싼 항공편으로 지정
first_flight = data["data"][0]
lowest_price = float(first_flight["price"]["grandTotal"])
origin = first_flight["itineraries"][0]["segments"][0]["departure"]["iataCode"]
destination = first_flight["itineraries"][0]["segments"][0]["arrival"]["iataCode"]
out_date = first_flight["itineraries"][0]["segments"][0]["departure"]["at"].split("T")[0]
cheapest_flight = FlightData(lowest_price, origin, destination, out_date)
for flight in data["data"]:
price = float(flight["price"]["grandTotal"])
if price < lowest_price:
lowest_price = price
origin = flight["itineraries"][0]["segments"][0]["departure"]["iataCode"]
destination = flight["itineraries"][0]["segments"][0]["arrival"]["iataCode"]
out_date = flight["itineraries"][0]["segments"][0]["departure"]["at"].split("T")[0]
cheapest_flight = FlightData(lowest_price, origin, destination, out_date)
return cheapest_flight
⌨️ flight_search.py
import os
import requests
from dotenv import load_dotenv
from datetime import datetime
load_dotenv()
IATA_ENDPOINT = "https://test.api.amadeus.com/v1/reference-data/locations/cities"
FLIGHT_ENDPOINT = "https://test.api.amadeus.com/v2/shopping/flight-offers"
TOKEN_ENDPOINT = "https://test.api.amadeus.com/v1/security/oauth2/token"
class FlightSearch:
def __init__(self):
…
def get_new_token(self):
…
def get_destination_code(self, city_name):
…
def check_flight(self, origin_city_code, destination_city_code, from_time):
headers = {"Authorization": f"Bearer {self.token}"}
query = {
"originLocationCode": origin_city_code,
"destinationLocationCode": destination_city_code,
"departureDate": from_time.strftime("%Y-%m-%d"),
"adults": "1",
"nonStop": "true",
"currencyCode": "GBP",
"max": "5"
}
response = requests.get(url=FLIGHT_ENDPOINT, headers=headers, params=query)
if response.status_code != 200:
print(f"check_flights() response code: {response.status_code}")
print("Response body:", response.text)
return None
return response.json()
⌨️ main.py
import time
from datetime import datetime, timedelta
from data_manager import DataManager
from flight_search import FlightSearch
from flight_data import FlightData
# ==================== Set up the Flight Search ====================
…
ORIGIN_CITY_IATA = "LON"
# ==================== Update the Airport Codes in Google Sheet ====================
…
# ==================== Search for Flights ====================
tomorrow = datetime.now() + timedelta(days=1)
for destination in sheet_data:
print(f"Getting flights for {destination['city']}...")
flights = flight_search.check_flight(
origin_city_code=ORIGIN_CITY_IATA,
destination_city_code=destination["iataCode"],
from_time=tomorrow
)
cheapest_flight = FlightData.find_cheapest_flight(flights)
print(f"Lowest price to {destination['city']} is £{cheapest_flight.price}")
time.sleep(2)
🔍 유의 사항
- Programmable Messaging for WhatsApp and Python Quickstart
- 체험 계정의 요금 한도를 넘지 않기 위해 시트에 목적지 공항은 최대 10개 이내로 제한
- 검색한 최저가가 구글 시트의 최저가보다 저렴하다면, WhatsApp 메시지 전송하기
- 메시지에 포함될 정보
Low price alert! Only "항공권 가격" to fly from "출발 공항(도시) IATA 코드" to "도착 공항(도시) IATA 코드", on "출발 날짜" until "리턴 날짜"
- 출력을 위해 시트에서 파리의 최저가를 변경
📄.env
SHEETY_ENDPOINT="https://api.sheety.co/<유저이름>/citiesToFly/prices"
BEARER_TOKEN="개인 토큰"
AMADEUS_API_KEY="개인 키"
AMADEUS_API_SECRET="개인 토큰"
TWILIO_SID="개인 시드"
TWILIO_AUTH_TOKEN="개인 토큰"
TWILIO_WHATSAPP_NUMBER="개인 가상 왓츠앱 전화번호"
TWILIO_VERIFIED_NUMBER="메시지를 받을 전화번호"
⌨️ data_manager.py
import os
import requests
from dotenv import load_dotenv
load_dotenv()
class DataManager:
def __init__(self):
self.sheety_endpoint = os.getenv("SHEETY_ENDPOINT")
self.bearer_token = os.getenv("BEARER_TOKEN")
self.bearer_headers = {"Authorization": f"Bearer {self.bearer_token}"}
self.destination_data = {}
def get_destination_data(self):
response = requests.get(url=self.sheety_endpoint,
headers=self.bearer_headers)
data = response.json()
self.destination_data = data["prices"]
return self.destination_data
def update_destination_codes(self):
for city in self.destination_data:
new_data = {
"price": {"iataCode": city["iataCode"]}
}
response = requests.put(url=f"{self.sheety_endpoint}/{city['id']}",
json=new_data,
headers=self.bearer_headers)
⌨️ flight_data.py
class FlightData:
def __init__(self, price, origin_airport, destination_airport, out_date):
self.price = price
self.origin_airport = origin_airport
self.destination_airport = destination_airport
self.out_date = out_date
def find_cheapest_flight(data):
# 오류 처리
if data is None or not data["data"]:
print("No flight data")
return FlightData("N/A", "N/A", "N/A", "N/A")
# 먼저 data["data"]의 1번째 원소를 가장 싼 항공편으로 지정
first_flight = data["data"][0]
lowest_price = float(first_flight["price"]["grandTotal"])
origin = first_flight["itineraries"][0]["segments"][0]["departure"]["iataCode"]
destination = first_flight["itineraries"][0]["segments"][0]["arrival"]["iataCode"]
out_date = first_flight["itineraries"][0]["segments"][0]["departure"]["at"].split("T")[0]
cheapest_flight = FlightData(lowest_price, origin, destination, out_date)
for flight in data["data"]:
price = float(flight["price"]["grandTotal"])
if price < lowest_price:
lowest_price = price
origin = flight["itineraries"][0]["segments"][0]["departure"]["iataCode"]
destination = flight["itineraries"][0]["segments"][0]["arrival"]["iataCode"]
out_date = flight["itineraries"][0]["segments"][0]["departure"]["at"].split("T")[0]
cheapest_flight = FlightData(lowest_price, origin, destination, out_date)
return cheapest_flight
⌨️ flight_search.py
import os
import requests
from dotenv import load_dotenv
from datetime import datetime
load_dotenv()
IATA_ENDPOINT = "https://test.api.amadeus.com/v1/reference-data/locations/cities"
FLIGHT_ENDPOINT = "https://test.api.amadeus.com/v2/shopping/flight-offers"
TOKEN_ENDPOINT = "https://test.api.amadeus.com/v1/security/oauth2/token"
class FlightSearch:
def __init__(self):
self.api_key = os.getenv("AMADEUS_API_KEY")
self.api_secret = os.getenv("AMADEUS_API_SECRET")
# 프로그램을 시작할 때마다 토큰을 새로 발급
self.token = self.get_new_token()
def get_new_token(self):
header = {'Content-Type': 'application/x-www-form-urlencoded'}
body = {
'grant_type': 'client_credentials',
'client_id': self.api_key,
'client_secret': self.api_secret
}
response = requests.post(url=TOKEN_ENDPOINT, headers=header, data=body)
print(f"Your token is {response.json()['access_token']}")
return response.json()["access_token"]
def get_destination_code(self, city_name):
headers = {"Authorization": f"Bearer {self.token}"}
query = {
"keyword": city_name,
"max": "2",
"include": "AIRPORTS"
}
response = requests.get(url=IATA_ENDPOINT, headers=headers, params=query)
try:
code = response.json()["data"][0]['iataCode']
except IndexError:
print(f"IndexError: No airport code found for {city_name}.")
return "N/A"
except KeyError:
print(f"KeyError: No airport code found for {city_name}.")
return "Not Found"
return code
def check_flight(self, origin_city_code, destination_city_code, from_time):
headers = {"Authorization": f"Bearer {self.token}"}
query = {
"originLocationCode": origin_city_code,
"destinationLocationCode": destination_city_code,
"departureDate": from_time.strftime("%Y-%m-%d"),
"adults": "1",
"nonStop": "true",
"currencyCode": "GBP",
"max": "5"
}
response = requests.get(url=FLIGHT_ENDPOINT, headers=headers, params=query)
if response.status_code != 200:
print(f"check_flights() response code: {response.status_code}")
print("Response body:", response.text)
return None
return response.json()
⌨️ notification_manager.py
import os
from twilio.rest import Client
from dotenv import load_dotenv
load_dotenv()
class NotificationManager:
def __init__(self):
self.client = client = Client(os.getenv("TWILIO_SID"), os.getenv("TWILIO_AUTH_TOKEN"))
def send_whatsapp(self, message_body):
message = self.client.messages.create(
body=message_body,
from_=f"whatsapp:{os.getenv('TWILIO_WHATSAPP_NUMBER')}",
to=f"whatsapp:{os.getenv('TWILIO_VERIFIED_NUMBER')}"
)
⌨️ main.py
import time
from datetime import datetime, timedelta
from data_manager import DataManager
from flight_search import FlightSearch
from flight_data import FlightData
from notification_manager import NotificationManager
# ==================== Set up the Flight Search ====================
data_manager = DataManager()
sheet_data = data_manager.get_destination_data()
flight_search = FlightSearch()
notification_manager = NotificationManager()
ORIGIN_CITY_IATA = "LON"
# ==================== Update the Airport Codes in Google Sheet ====================
for row in sheet_data:
if row["iataCode"] == "":
row["iataCode"] = flight_search.get_destination_code(row["city"])
time.sleep(2)
data_manager.destination_data = sheet_data
data_manager.update_destination_codes()
# ==================== Search for Flights ====================
tomorrow = datetime.now() + timedelta(days=1)
for destination in sheet_data:
print(f"Getting flights for {destination['city']}...")
flights = flight_search.check_flight(
origin_city_code=ORIGIN_CITY_IATA,
destination_city_code=destination["iataCode"],
from_time=tomorrow
)
cheapest_flight = FlightData.find_cheapest_flight(flights)
print(f"Lowest price to {destination['city']} is £{cheapest_flight.price}")
time.sleep(2)
if cheapest_flight.price != "N/A" and cheapest_flight.price < destination["lowestPrice"]:
print(f"Lower price flight found to {destination['city']}!")
notification_manager.send_whatsapp(
message_body=f"Low price alert! Only £{cheapest_flight.price} to fly "
f"from {cheapest_flight.origin_airport} "
f"to {cheapest_flight.destination_airport}, "
f"on {cheapest_flight.out_date}."
)