aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlex Schofield <git@ajschof.me>2026-04-14 20:57:39 +0100
committerAlex Schofield <git@ajschof.me>2026-04-14 20:57:39 +0100
commit4dc7f3441e8e434d38efc0e12d53a61a6dc8f540 (patch)
tree8cab51aec8de496550ce9490c0687261ec524a48
downloadfuelnearme-4dc7f3441e8e434d38efc0e12d53a61a6dc8f540.tar.gz
fuelnearme-4dc7f3441e8e434d38efc0e12d53a61a6dc8f540.zip
initial commit
-rw-r--r--.gitignore219
-rw-r--r--README.md37
-rw-r--r--main.py91
-rw-r--r--requirements.txt4
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
+```
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..5c9b0cb
--- /dev/null
+++ b/main.py
@@ -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
git.ajschof.me — hosted by ajschofield — powered by cgit