/flight-finder

This project helps you and your friends, who are located in different parts of the world, to find the best airport and day to travel based on the lowest total price.

Primary LanguagePython

Flight Finder

This project helps you and your friends, who are located in different parts of the world, to find the best airport and day to travel based on the lowest total price.

Running with Docker

After cloning the repository, you can build a docker image using the following command:

make docker-build

Run the project running:

make docker-run

and a CLI will appear.

Running without Docker

After cloning the repository, you will need all the requirements:

  • Python 3.10.9 (You can use pyenv to install a specific Python version)
  • Poetry (Read how to install it by clicking here)

Install the project dependencies using Poetry:

poetry install

Execute the CLI using the command:

make cli

Usage

Here's an example of CLI interaction:

Enter a new airport (Enter an empty string to stop): JFK
Enter a new airport (Enter an empty string to stop): GRU
Enter a new airport (Enter an empty string to stop): PHX
Enter a new airport (Enter an empty string to stop): 
Enter the lower bound date: 2023-03-01
Enter the upper bound date: 2023-08-01
Enter the center airports limit: 15
Enter how much flight suggestions do you want: 10

Most of the arguments are self-explanatory except one: center airports limit. center_airports_limit: This is an argument that specifies the number of center airports to consider for the search. The search precision and running time will both decrease if this number decreases and vice-versa.

Context and Challenges

This project is part of the selection process that I'm attending right now. Here I'm going to talk about how I figured out how to solve the initial problem.

The initial problem was:

Given a list of cities where we live and a range of dates please use a flight pricing API to determine what would be the cheapest city to fly to a company offsite.

First of all, I need to choose an API to use as a data source. Wasting some time I found the Amadeus website. The free tier of it supports 2000 requests per month and I think it’s enough for what we are trying to do. So I registered, read the documentation, and started coding.

I created my project using Poetry, created a GitHub repository, and installed mypy to use static typing and pytest to make my tests. My first test was an integration test to see if everything connects as it should.

 alt text for screen readers

After writing enough code to make this test work we can make a mocked version of the API to use in unit tests.

I found on the internet an SQLite table describing all airports with coordinates. I’m going to use it to estimate the midpoint of all related airports and reduce API calls which allow us to stay on the free tier. I made an interface and tests for it too.

 alt text for screen readers

As before, now we can make a mocked version of it too.

I’m using the circular mean to calculate the midpoint of a collection of coordinates in the globe, which minimizes geodesic distance. The implementation of this pure function can be found at flight_finder/mathematics.py and this file is fully covered by tests too. Mocking the API and the airport's table interface allows us to implement and write tests to our main function: find_best_airports_and_days which is at flight_finder/find.py.

 alt text for screen readers

The idea is this function returns the best airports, on which date, and for which price. After implementing it, everything works nicely with mocked API and mocked airports table.

I got some errors and needed to start from the beginning a significant quantity of times so I thought a good idea could be to cache the API output locally. It can speed things up and save some API calls. So I implemented the SqliteCacher which implements AbstractCacher (It exists to allow us to change the caching strategy in the future). After it, I created the CachingWrapper which receives an API and some caching strategy and implements the same interface of an API. I wrote tests for everything I mentioned. Using this wrapper looks like this:

Before:

client = AmadeusApi(key, secret, url)

After:

cacher = SqliteCacher("cache.db")
client = CachingWrapper(AmadeusApi(key, secret, url), cacher)

In the end, I got 87% test coverage. Take a look:

---------- coverage: platform linux, python 3.8.10-final-0 -----------
Name                                                               Stmts   Miss  Cover
--------------------------------------------------------------------------------------
cli.py                                                                29     29     0%
flight_finder/__init__.py                                              0      0   100%
flight_finder/airports_table/airports_table_int.py                     9      2    78%
flight_finder/airports_table/airports_table_sqlite.py                 16      0   100%
flight_finder/api/amadeus_api.py                                      83     17    80%
flight_finder/api/api_int.py                                           7      1    86%
flight_finder/api/caching_wrapper.py                                  25      2    92%
flight_finder/cacher/cacher_int.py                                    11      2    82%
flight_finder/cacher/sqlite_cacher.py                                 25      1    96%
flight_finder/find.py                                                 33      2    94%
flight_finder/mathematics.py                                          24      0   100%
flight_finder/utils.py                                                 5      0   100%
tests/__init__.py                                                      0      0   100%
tests/flight_finder/__init__.py                                        0      0   100%
tests/flight_finder/airports_table/test_airports_table_sqlite.py      13      0   100%
tests/flight_finder/api/test_amadeus_api.py                           12      0   100%
tests/flight_finder/api/test_caching_crust.py                         40      0   100%
tests/flight_finder/cacher/test_sqlite_cacher.py                      17      0   100%
tests/flight_finder/test_find.py                                      28      0   100%
tests/flight_finder/test_mathematics.py                               26      0   100%
tests/flight_finder/test_utils.py                                     12      0   100%
--------------------------------------------------------------------------------------
TOTAL                                                                415     56    87%

With everything done and working, I documented every function, created the Dockerfile, created the Makefile, created the CI pipeline with GitHub Actions, wrote this README and adjusted every detail to work as I wanted. That's it.