Skip to content

Commit

Permalink
Get oneway flights working / Flight, ReturnFlight, Airport & more cla…
Browse files Browse the repository at this point in the history
…sses, parse RyanAir API output
  • Loading branch information
victorbmlabs committed Oct 23, 2024
1 parent 5182354 commit 96fbe25
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 39 deletions.
File renamed without changes.
97 changes: 65 additions & 32 deletions flyan/misc.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,52 @@
import json
from pydantic import BaseModel, field_validator
import time
from datetime import datetime
from typing import Optional

currencies: dict = json.load(open("currencies.json"))
stations: dict = json.load(open("stations.json"))
from pydantic import BaseModel, field_validator

currencies: dict = json.load(open("./currencies.json", encoding="utf-8"))
stations: dict = json.load(open("./stations.json", encoding="utf-8"))


class Airport(BaseModel):
country_name: str
iata_code: str
name: str
seo_name: str
city_name: str
city_code: str
city_country_code: str

class Flight(BaseModel):
pass
departure_airport: Airport
arrival_airport: Airport
departure_date: datetime
arrival_date: datetime
price: float
currency: str
flight_key: str
flight_number: str
previous_price: Optional[str | float]


class ReturnFlight(BaseModel):
outbound: Flight
inbound: Flight
summary_price: float
summary_currency: str
previous_price: str | float


class FlightSearchParams(BaseModel):
"""Parameters for flight searches"""

from_airport: str
from_date: datetime
date_to: datetime
destination_country: Optional[str]
max_price: Optional[int]
to_airport: Optional[str]
to_date: datetime
destination_country: Optional[str] = None
max_price: Optional[int] = None
to_airport: Optional[str] = None
departure_time_from: Optional[str] = "00:00"
departure_time_to: Optional[str] = "23:59"

Expand All @@ -28,7 +57,7 @@ def validate_airport(cls, v):

raise ValueError("Airport code must be a 3-letter IATA code")

@field_validator("date_from", "date_to")
@field_validator("from_date", "to_date")
def validate_dates(cls, v):
if v < datetime.now():
raise ValueError("Date from or to cannot be in the past")
Expand All @@ -37,55 +66,59 @@ def validate_dates(cls, v):

@field_validator("max_price")
def validate_price(cls, v):
if v is not None and v <= 0:
if v is None:
return v

if v <= 0:
raise ValueError("Price can't be negative")

return v

def to_api_params(self) -> dict:
"""Convert the parameters to the format expected by the Ryanair API"""
params = {
"departureAirportIataCode": self.departure_airport,
"outboundDepartureDateFrom": self.date_from.date().isoformat(),
"outboundDepartureDateTo": self.date_to.date().isoformat(),
"departureAirportIataCode": self.from_airport,
"outboundDepartureDateFrom": self.from_date.date().isoformat(),
"outboundDepartureDateTo": self.to_date.date().isoformat(),
"outboundDepartureTimeFrom": self.departure_time_from,
"outboundDepartureTimeTo": self.departure_time_to,
}

if self.destination_country:
params["arrivalCountryCode"] = self.destination_country

if self.max_price:
params["priceValueTo"] = self.max_price

if self.destination_airport:
params["arrivalAirportIataCode"] = self.destination_airport

if self.custom_params:
params.update(self.custom_params)

if self.to_airport:
params["arrivalAirportIataCode"] = self.to_airport

return params


class ReturnFlightSearchParams(FlightSearchParams):
"""Parameters for return flight searches"""

return_date_from: datetime
return_date_to: datetime
inbound_departure_time_from: Optional[str] = "00:00"
inbound_departure_time_to: Optional[str] = "23:59"

@field_validator('return_date_from', 'return_date_to')
@field_validator("return_date_from", "return_date_to")
def validate_return_dates(cls, v, values):
if 'date_from' in values and v < values['date_from']:
raise ValueError('Return date cannot be before departure date')
if "date_from" in values and v < values["date_from"]:
raise ValueError("Return date cannot be before departure date")
return v

def to_api_params(self) -> dict:
"""Convert the parameters to the format expected by the Ryanair API"""
params = super().to_api_params()
params.update({
"inboundDepartureDateFrom": self.return_date_from.date().isoformat(),
"inboundDepartureDateTo": self.return_date_to.date().isoformat(),
"inboundDepartureTimeFrom": self.inbound_departure_time_from,
"inboundDepartureTimeTo": self.inbound_departure_time_to,
})
return params
params.update(
{
"inboundDepartureDateFrom": self.return_date_from.date().isoformat(),
"inboundDepartureDateTo": self.return_date_to.date().isoformat(),
"inboundDepartureTimeFrom": self.inbound_departure_time_from,
"inboundDepartureTimeTo": self.inbound_departure_time_to,
}
)
return params
88 changes: 81 additions & 7 deletions flyan/ryanair.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import logging
from datetime import datetime

import httpx
from misc import currencies, FlightSearchParams, ReturnFlightSearchParams
from fake_useragent import UserAgent
from tenacity import (
retry,
retry_if_exception_type,
stop_after_attempt,
wait_exponential,
retry_if_exception_type,
)

from flyan.misc import (
Airport,
Flight,
FlightSearchParams,
ReturnFlight,
ReturnFlightSearchParams,
currencies,
)

ua = UserAgent()
logger = logging.getLogger("Flyan")
if not logger.handlers:
logger.setLevel(logging.INFO)
Expand All @@ -35,12 +45,25 @@ class RyanAir:
:param str currency: Preferred currency
"""

BASE_SERVICES_API_URL = "https://services-api.ryanair.com/farfnd/v4/"
BASE_SERVICES_API_URL = "https://services-api.ryanair.com/farfnd/v4"
AGGREGATE_URL = "https://www.ryanair.com/api/views/locate/3/aggregate/all/en"

def __init__(self, currency: str = "EUR"):
self.client = httpx.Client()
self.__get("https://ryanair.com")
self.client = httpx.Client(
headers={
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "en-GB,en;q=0.9",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"Priority": "u=0, i",
"Upgrade-Insecure-Requests": "1",
"User-Agent": ua.random,
},
follow_redirects=True,
)

self.__get("https://www.ryanair.com")

if currency in currencies.keys():
self.currency = currency
Expand Down Expand Up @@ -69,8 +92,59 @@ def __get(self, url: str, params: dict = {}) -> httpx.Response:
response = self.client.get(url, params=params)
response.raise_for_status()
return response

def get_oneways(self, params: FlightSearchParams):

def __parse_airport(self, airport: dict) -> Airport:
return Airport(
country_name=airport['countryName'],
iata_code=airport['iataCode'],
name=airport['name'],
seo_name=airport['seoName'],
city_name=airport['city']['name'],
city_code=airport['city']['code'],
city_country_code=airport['city']['countryCode'],

)

def __parse_fare(self, fare: dict, k: str = "outbound") -> Flight:
dep_date = datetime.fromisoformat(fare[k]["departureDate"])
arr_date = datetime.fromisoformat(fare[k]["arrivalDate"])


return Flight(
departure_airport=self.__parse_airport(fare[k]['departureAirport']),
arrival_airport=self.__parse_airport(fare[k]['arrivalAirport']),
departure_date=dep_date,
arrival_date=arr_date,
price=fare[k]["price"]["value"],
currency=fare[k]["price"]["currencyCode"],
flight_key=fare[k]["flightKey"],
flight_number=fare[k]["flightNumber"],
previous_price=fare[k].get("previousPrice", None),
)

def __parse_return_fare(self, fare: dict) -> ReturnFlight:
pass

def get_oneways(self, params: FlightSearchParams) -> list[Flight]:
"""
Get oneways
"""
print("Getting oneways")
url = f"{self.BASE_SERVICES_API_URL}/oneWayFares"

try:
r = self.__get(url, params.to_api_params())
if not r.is_success:
pass

fares = r.json()["fares"]
flights = [self.__parse_fare(f) for f in fares]

return flights

except httpx.HTTPError as HttpE:
logger.error(f"A HTTP Error occured trying to get {url}", exc_info=True)

except KeyError as KE:
print(KE)

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ description = "Open source unofficial API wrappper to get flight data from RyanA
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
"fake-useragent>=1.5.1",
"httpx>=0.27.2",
"pydantic>=2.9.2",
"tenacity>=9.0.0",
Expand Down
File renamed without changes.

0 comments on commit 96fbe25

Please sign in to comment.