diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d69d26..c5c7976 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +# [v2.2.0] - 2023.04.18 +### Added +- Ability to constrain the max price of retrieved flights/trips from the API when +using `get_cheapest_flights` or `get_cheapest_return_flights` via the `max_price` kwarg. +- Added `currency` field to the `Flight` object. + - Availability API method `get_all_flights` does not support specifying currency (it is always the currency of the + departure country), but this was not documented. `Flight` will now allow the user to see what currency has been +returned. +- Log a warning when returned currency doesn't match the configured value. + - Primarily to warn users using the `get_all_flights` API that the response isn't as configured. + - Might also be useful if the `get_cheapest_flights` and `get_cheapest_return_flights` APIs ever stop respecting +requests for results in specific currencies. + +### Fixed +- Incorrect date format used for logs. + +### Changed +- It is now _optional_ to specify `currency` when creating an instance of the library. + - If not specified, the API decides the return currency (normally the currency of the departure country). + - If an API, such as the availability / `get_all_flights` API, doesn't support it anyway, this will be ignored, +except for the purposes of deciding whether a warning should be shown, where the currencies mismatch. + # [v2.1.0] - 2023.03.12 ### Added - Added flight departure time filter keyword arguments to `get_cheapest_flights` and `get_cheapest_return_flights`. diff --git a/README.md b/README.md index 22fdf49..a97c49e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,11 @@ pip install ryanair-py To create an instance: ```python from ryanair import Ryanair -api = Ryanair("EUR") # Euro currency, so could also be GBP etc. also + +# You can set a currency at the API instance level, so could also be GBP etc. also. +# Note that this may not *always* be respected by the API, so always check the currency returned matches +# your expectation. For example, the underlying API for get_all_flights does not support this. +api = Ryanair("EUR") ``` ### Get the cheapest one-way flights Get the cheapest flights from a given origin airport (returns at most 1 flight to each destination). @@ -24,14 +28,14 @@ from datetime import datetime, timedelta from ryanair import Ryanair from ryanair.types import Flight -api = Ryanair("EUR") # Euro currency, so could also be GBP etc. also +api = Ryanair(currency="EUR") # Euro currency, so could also be GBP etc. also tomorrow = datetime.today().date() + timedelta(days=1) flights = api.get_cheapest_flights("DUB", tomorrow, tomorrow + timedelta(days=1)) # Returns a list of Flight namedtuples flight: Flight = flights[0] -print(flight) # Flight(departureTime=datetime.datetime(2023, 3, 12, 17, 0), flightNumber='FR9717', price=31.99, origin='DUB', originFull='Dublin, Ireland', destination='GOA', destinationFull='Genoa, Italy') +print(flight) # Flight(departureTime=datetime.datetime(2023, 3, 12, 17, 0), flightNumber='FR9717', price=31.99, currency='EUR' origin='DUB', originFull='Dublin, Ireland', destination='GOA', destinationFull='Genoa, Italy') print(flight.price) # 9.78 ``` ### Get the cheapest return trips (outbound and inbound) @@ -39,12 +43,12 @@ print(flight.price) # 9.78 from datetime import datetime, timedelta from ryanair import Ryanair -api = Ryanair("EUR") # Euro currency, so could also be GBP etc. also +api = Ryanair(currency="EUR") # Euro currency, so could also be GBP etc. also tomorrow = datetime.today().date() + timedelta(days=1) tomorrow_1 = tomorrow + timedelta(days=1) trips = api.get_cheapest_return_flights("DUB", tomorrow, tomorrow, tomorrow_1, tomorrow_1) -print(trips[0]) # Trip(totalPrice=85.31, outbound=Flight(departureTime=datetime.datetime(2023, 3, 12, 7, 30), flightNumber='FR5437', price=49.84, origin='DUB', originFull='Dublin, Ireland', destination='EMA', destinationFull='East Midlands, United Kingdom'), inbound=Flight(departureTime=datetime.datetime(2023, 3, 13, 7, 45), flightNumber='FR5438', price=35.47, origin='EMA', originFull='East Midlands, United Kingdom', destination='DUB', destinationFull='Dublin, Ireland')) +print(trips[0]) # Trip(totalPrice=85.31, outbound=Flight(departureTime=datetime.datetime(2023, 3, 12, 7, 30), flightNumber='FR5437', price=49.84, currency='EUR', origin='DUB', originFull='Dublin, Ireland', destination='EMA', destinationFull='East Midlands, United Kingdom'), inbound=Flight(departureTime=datetime.datetime(2023, 3, 13, 7, 45), flightNumber='FR5438', price=35.47, origin='EMA', originFull='East Midlands, United Kingdom', destination='DUB', destinationFull='Dublin, Ireland')) ``` ### Get all available flights between two airports @@ -55,7 +59,9 @@ from datetime import datetime, timedelta from ryanair import Ryanair from tabulate import tabulate -api = Ryanair("EUR") +# We don't need to specify a currency if we're only using `get_all_flights`, as this API doesn't support currency +# conversion. It will always return dares denominated in the currency of the departure country. +api = Ryanair() tomorrow = datetime.today().date() + timedelta(days=1) flights = api.get_all_flights("DUB", tomorrow, "LGW") @@ -68,22 +74,22 @@ print(tabulate(flights, headers="keys", tablefmt="github")) This prints the following: -| departureTime | flightNumber | price | origin | originFull | destination | destinationFull | -|---------------------|----------------|---------|----------|--------------|---------------|-------------------| -| 2023-03-12 06:25:00 | FR 114 | 61.99 | DUB | Dublin | LGW | London (Gatwick) | -| 2023-03-12 09:20:00 | FR 112 | 88.12 | DUB | Dublin | LGW | London (Gatwick) | -| 2023-03-12 11:30:00 | FR 122 | 120.37 | DUB | Dublin | LGW | London (Gatwick) | -| ... | | | | | | | +| departureTime | flightNumber | price | currency | origin | originFull | destination | destinationFull | +|---------------------|----------------|---------|----------|----------|--------------|---------------|-------------------| +| 2023-03-12 06:25:00 | FR 114 | 61.99 | EUR | DUB | Dublin | LGW | London (Gatwick) | +| 2023-03-12 09:20:00 | FR 112 | 88.12 | EUR | DUB | Dublin | LGW | London (Gatwick) | +| 2023-03-12 11:30:00 | FR 122 | 120.37 | EUR | DUB | Dublin | LGW | London (Gatwick) | +| ... | | | EUR | | | | | and -| departureTime | flightNumber | price | origin | originFull | destination | destinationFull | -|---------------------|----------------|---------|----------|--------------|---------------|-------------------| -| 2023-03-12 06:25:00 | FR 114 | 61.99 | DUB | Dublin | LGW | LON | -| 2023-03-12 06:35:00 | FR 202 | 65.09 | DUB | Dublin | STN | LON | -| 2023-03-12 07:10:00 | FR 342 | 65.09 | DUB | Dublin | LTN | LON | -| 2023-03-12 08:20:00 | FR 206 | 102.09 | DUB | Dublin | STN | LON | -| ... | | | | | | | +| departureTime | flightNumber | price | currency | origin | originFull | destination | destinationFull | +|---------------------|----------------|---------|----------|----------|--------------|---------------|-------------------| +| 2023-03-12 06:25:00 | FR 114 | 61.99 | EUR | DUB | Dublin | LGW | LON | +| 2023-03-12 06:35:00 | FR 202 | 65.09 | EUR | DUB | Dublin | STN | LON | +| 2023-03-12 07:10:00 | FR 342 | 65.09 | EUR | DUB | Dublin | LTN | LON | +| 2023-03-12 08:20:00 | FR 206 | 102.09 | EUR | DUB | Dublin | STN | LON | +| ... | | | | | | | | diff --git a/ryanair/ryanair.py b/ryanair/ryanair.py index f687198..306316f 100644 --- a/ryanair/ryanair.py +++ b/ryanair/ryanair.py @@ -6,19 +6,23 @@ This is done directly through Ryanair's API, and does not require an API key. """ import logging -from typing import Union +from datetime import datetime, date, time +from typing import Union, Optional import backoff import requests -from datetime import datetime, date, time -from time import sleep - from deprecated import deprecated from ryanair.types import Flight, Trip -logging.basicConfig(level=logging.INFO, - format='%(asctime)s.%(msecs)03d %(levelname)s:%(message)s', datefmt="%m/%d/%Y %I:%M:%S") +logger = logging.getLogger("ryanair") +logger.setLevel(logging.INFO) + +console_handler = logging.StreamHandler() +formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(levelname)s:%(message)s', datefmt="%Y-%m-%d %I:%M:%S") + +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) # noinspection PyBroadException @@ -26,7 +30,7 @@ class Ryanair: BASE_SERVICES_API_URL = "https://services-api.ryanair.com/farfnd/v4/" BASE_AVAILABILITY_API_URL = "https://www.ryanair.com/api/booking/v4/" - def __init__(self, currency): + def __init__(self, currency: Optional[str] = None): self.currency = currency self._num_queries = 0 @@ -46,6 +50,7 @@ def get_cheapest_flights(self, airport, date_from, date_to, destination_country= custom_params=None, departure_time_from: Union[str, time] = "00:00", departure_time_to: Union[str, time] = "23:59", + max_price: int = None ): query_url = ''.join((Ryanair.BASE_SERVICES_API_URL, "oneWayFares")) @@ -54,19 +59,22 @@ def get_cheapest_flights(self, airport, date_from, date_to, destination_country= "departureAirportIataCode": airport, "outboundDepartureDateFrom": self._format_date_for_api(date_from), "outboundDepartureDateTo": self._format_date_for_api(date_to), - "currency": self.currency, "outboundDepartureTimeFrom": self._format_time_for_api(departure_time_from), "outboundDepartureTimeTo": self._format_time_for_api(departure_time_to) } + if self.currency: + params['currency'] = self.currency if destination_country: params['arrivalCountryCode'] = destination_country + if max_price: + params['priceValueTo'] = max_price if custom_params: params.update(custom_params) try: response = self._retryable_query(query_url, params)["fares"] except Exception: - logging.exception(f"Failed to parse response when querying {query_url}") + logger.exception(f"Failed to parse response when querying {query_url}") return [] if response: @@ -82,6 +90,7 @@ def get_cheapest_return_flights(self, source_airport, date_from, date_to, outbound_departure_time_to: Union[str, time] = "23:59", inbound_departure_time_from: Union[str, time] = "00:00", inbound_departure_time_to: Union[str, time] = "23:59", + max_price: int = None ): query_url = ''.join((Ryanair.BASE_SERVICES_API_URL, "roundTripFares")) @@ -100,13 +109,15 @@ def get_cheapest_return_flights(self, source_airport, date_from, date_to, } if destination_country: params['arrivalCountryCode'] = destination_country + if max_price: + params['priceValueTo'] = max_price if custom_params: params.update(custom_params) try: response = self._retryable_query(query_url, params)["fares"] except Exception as e: - logging.exception(f"Failed to parse response when querying {query_url}") + logger.exception(f"Failed to parse response when querying {query_url}") return [] if response: @@ -153,30 +164,39 @@ def get_all_flights(self, origin_airport, date_out, destination, params.update(custom_params) try: - response = self._retryable_query(query_url, params)["trips"][0] - flights = response['dates'][0]['flights'] + response = self._retryable_query(query_url, params) + currency = response["currency"] + trip = response["trips"][0] + flights = trip['dates'][0]['flights'] if flights: + if self.currency and self.currency != currency: + logger.warning(f"Configured to fetch fares in {self.currency} but availability API doesn't support" + f" specifying the currency, so it responded with fares in {currency}") + return [self._parse_all_flights_availability_result_as_flight(flight, - response['originName'], - response['destinationName']) + trip['originName'], + trip['destinationName'], + currency) for flight in flights] except Exception: - logging.exception(f"Failed to parse response when querying {query_url}") + logger.exception(f"Failed to parse response when querying {query_url}") return [] @staticmethod def _on_query_error(e): - logging.exception(f"Gave up retrying query, last exception was {e}") + logger.exception(f"Gave up retrying query, last exception was {e}") - @backoff.on_exception(backoff.expo, Exception, max_tries=5, logger=logging.getLogger(), on_giveup=_on_query_error, + @backoff.on_exception(backoff.expo, Exception, max_tries=5, logger=logger, on_giveup=_on_query_error, raise_on_giveup=False) def _retryable_query(self, url, params): self._num_queries += 1 return requests.get(url, params=params).json() - @staticmethod - def _parse_cheapest_flight(flight): + def _parse_cheapest_flight(self, flight): + currency = flight['price']['currencyCode'] + if self.currency and self.currency != currency: + logger.warning(f"Requested cheapest flights in {self.currency} but API responded with fares in {currency}") return Flight( origin=flight['departureAirport']['iataCode'], originFull=', '.join((flight['departureAirport']['name'], flight['departureAirport']['countryName'])), @@ -184,7 +204,8 @@ def _parse_cheapest_flight(flight): destinationFull=', '.join((flight['arrivalAirport']['name'], flight['arrivalAirport']['countryName'])), departureTime=datetime.fromisoformat(flight['departureDate']), flightNumber=f"{flight['flightNumber'][:2]} {flight['flightNumber'][2:]}", - price=flight['price']['value'] + price=flight['price']['value'], + currency=currency ) def _parse_cheapest_return_flights_as_trip(self, outbound, inbound): @@ -198,11 +219,12 @@ def _parse_cheapest_return_flights_as_trip(self, outbound, inbound): ) @staticmethod - def _parse_all_flights_availability_result_as_flight(response, origin_full, destination_full): + def _parse_all_flights_availability_result_as_flight(response, origin_full, destination_full, currency): return Flight(departureTime=datetime.fromisoformat(response['time'][0]), flightNumber=response['flightNumber'], price=response['regularFare']['fares'][0]['amount'] if response['faresLeft'] != 0 else float( 'inf'), + currency=currency, origin=response['segments'][0]['origin'], originFull=origin_full, destination=response['segments'][0]['destination'], diff --git a/ryanair/types.py b/ryanair/types.py index 5b7a42e..e30ad73 100644 --- a/ryanair/types.py +++ b/ryanair/types.py @@ -1,5 +1,5 @@ from collections import namedtuple -Flight = namedtuple("Flight", ("departureTime", "flightNumber", "price", "origin", "originFull", +Flight = namedtuple("Flight", ("departureTime", "flightNumber", "price", "currency", "origin", "originFull", "destination", "destinationFull")) Trip = namedtuple("Trip", ("totalPrice", "outbound", "inbound")) diff --git a/setup.py b/setup.py index 75dfcb2..67ab14e 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ long_description = f.read() setup(name='ryanair-py', - version='2.1.0', + version='2.2.0', description='A module which allows you to retrieve data about the cheapest one-way and return flights ' 'in a date range, or all available flights on a given day for a given route.', long_description=long_description,