Skip to content

Commit

Permalink
Merge branch 'release/2.2.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
cohaolain committed Apr 18, 2023
2 parents 33daaa4 + d15de57 commit a621d84
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 42 deletions.
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
44 changes: 25 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -24,27 +28,27 @@ 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)
```python
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
Expand All @@ -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")
Expand All @@ -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 |
| ... | | | | | | | |


64 changes: 43 additions & 21 deletions ryanair/ryanair.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,31 @@
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
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
Expand All @@ -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"))
Expand All @@ -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:
Expand All @@ -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"))
Expand All @@ -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:
Expand Down Expand Up @@ -153,38 +164,48 @@ 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'])),
destination=flight['arrivalAirport']['iataCode'],
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):
Expand All @@ -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'],
Expand Down
2 changes: 1 addition & 1 deletion ryanair/types.py
Original file line number Diff line number Diff line change
@@ -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"))
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit a621d84

Please sign in to comment.