diff options
| author | Alex Schofield <git@ajschof.me> | 2026-04-14 20:57:39 +0100 |
|---|---|---|
| committer | Alex Schofield <git@ajschof.me> | 2026-04-14 20:57:39 +0100 |
| commit | 4dc7f3441e8e434d38efc0e12d53a61a6dc8f540 (patch) | |
| tree | 8cab51aec8de496550ce9490c0687261ec524a48 | |
| download | fuelnearme-4dc7f3441e8e434d38efc0e12d53a61a6dc8f540.tar.gz fuelnearme-4dc7f3441e8e434d38efc0e12d53a61a6dc8f540.zip | |
initial commit
| -rw-r--r-- | .gitignore | 219 | ||||
| -rw-r--r-- | README.md | 37 | ||||
| -rw-r--r-- | main.py | 91 | ||||
| -rw-r--r-- | requirements.txt | 4 |
4 files changed, 351 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df1a6d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,219 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml + +# Jetbrains +.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..ecd964a --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# FuelNearMe + +FuelNearMe is a simple Python utility that retrieves data from the UK Government’s +solution to centralising fuel station prices across the country. You can read more +about it [here](https://www.fuel-finder.service.gov.uk/). + +It lacks useful features like crowdsourcing data, but it aims to be a quick and simple +way to find the cheapest fuel near you. + +## Usage + +[Geopy](https://github.com/geopy/geopy) is used to get the coordinates to search for +fuel stations in the surrounding area in miles. It is also used to calculate the +geodesic distance between the starting coordinates and the coordinates of a fuel +station - this is an estimate and may not represent the actual distance. + +It's relatively easy to use. For example, you can search for service stations +around LS11 (Leeds), within a 5 mile radius, and to sort the results by distance. + +``` +python3 main.py --address "LS11" --radius 5 --sort "distance" +``` + +### Sort Options + +You can sort by: `distance`, `e10`, `e5`, `b7s` + +If this parameter isn't used, it automatically defaults to e10 (standard petrol). + +## Installation + +For now, create a [virtual environment](https://docs.python.org/3/library/venv.html), +activate it, and then install the dependencies from the `requirements.txt`. + +``` +pip install -r requirements.txt +``` @@ -0,0 +1,91 @@ +import argparse +import sys +from io import StringIO +from textwrap import dedent + +import pandas as pd +import requests +from colorama import Back, Fore, Style, just_fix_windows_console +from geopy.distance import geodesic +from geopy.geocoders import Nominatim + +just_fix_windows_console() + + +ENDPOINT = "https://www.fuel-finder.service.gov.uk/internal/v1.0.2/csv/get-latest-fuel-prices-csv" +near_stations = [] + + +def get_location(address): + loc = Nominatim(user_agent="FuelNearMe") + getLoc = loc.geocode(address) + if not getLoc: + print("[*] Failed to get location. Please check if the address is valid.") + sys.exit(1) + latitude = getLoc.latitude + longitude = getLoc.longitude + return (latitude, longitude) + + +def get_latest_data(): + response = requests.get(ENDPOINT) + return pd.read_csv(StringIO(response.text)), response.headers.get("Last-Modified") + + +parser = argparse.ArgumentParser() +parser.add_argument("-a", "--address", type=str, required=True) +parser.add_argument("-r", "--radius", type=int, default=5) +parser.add_argument("-s", "--sort", type=str, default="e10") +args = parser.parse_args() + +location = get_location(args.address) + +df, last_modified = get_latest_data() + +print(f"Last modified: {last_modified}") + +price_cols = [c for c in df.columns if "fuel_price" in c] +df[price_cols] = df[price_cols].fillna(0.0) +df = df.fillna("N/A") + +print(f"\n{Fore.MAGENTA}Stations: " + Style.RESET_ALL + str(len(df))) + +for station, latitude, longitude, e5_price, e10_price, diesel_price in zip( + df["forecourts.trading_name"], + df["forecourts.location.latitude"], + df["forecourts.location.longitude"], + df["forecourts.fuel_price.E5"], + df["forecourts.fuel_price.E10"], + df["forecourts.fuel_price.B7S"], +): + distance_from_current_location = geodesic((latitude, longitude), location).miles + if distance_from_current_location < args.radius: + station_dict = { + "station_name": station, + "distance": round(distance_from_current_location, 1), + "e5_price": round(e5_price / 100, 2), + "e10_price": round(e10_price / 100, 2), + "diesel_price": round(diesel_price / 100, 2), + } + near_stations.append(station_dict) + +match args.sort: + case "e10": + sort_by = "e10_price" + case "e5": + sort_by = "e5_price" + case "b7s": + sort_by = "diesel_price" + case "distance": + sort_by = "distance" + +near_stations_sorted_by_price = sorted(near_stations, key=lambda d: d[sort_by]) + +for number, row in enumerate(near_stations_sorted_by_price): + output = dedent(f""" + {number + 1}. {row["station_name"]} + Distance: {row["distance"]} miles + E5 Price: £{row["e5_price"]:.2f}/L + E10 Price: £{row["e10_price"]:.2f}/L + B7S (Standard Diesel) Price: £{row["diesel_price"]:.2f}/L""") + print(output) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..355609e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +colorama==0.4.6 +geopy==2.4.1 +pandas==3.0.2 +Requests==2.33.1 |
