From 8742ea43f1668206cf28451cea769fe052ab7ae5 Mon Sep 17 00:00:00 2001 From: Samuel Jones Date: Thu, 4 Jun 2026 14:16:38 +0100 Subject: [PATCH] First scrappy version, no API. --- README.md | 0 pyproject.toml | 19 +++++++ src/fares_site/__init__.py | 0 src/fares_site/__main__.py | 16 ++++++ src/fares_site/api_calling.py | 24 +++++++++ src/fares_site/serve.py | 98 +++++++++++++++++++++++++++++++++++ 6 files changed, 157 insertions(+) create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/fares_site/__init__.py create mode 100644 src/fares_site/__main__.py create mode 100644 src/fares_site/api_calling.py create mode 100644 src/fares_site/serve.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d0c0036 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "fares-site" +version = "0.1.0" +description = "" +authors = [ + {name = "Samuel Jones",email = "samuel@williamjones.me"} +] +requires-python = ">=3.14" +dependencies = [ + "requests (>=2.34.2,<3.0.0)", + "pandas (>=3.0.3,<4.0.0)", + "pandas-stubs (>=3.0.3.260530,<4.0.0.0)", + "jinja2 (>=3.1.6,<4.0.0)" +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/src/fares_site/__init__.py b/src/fares_site/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fares_site/__main__.py b/src/fares_site/__main__.py new file mode 100644 index 0000000..b3daf09 --- /dev/null +++ b/src/fares_site/__main__.py @@ -0,0 +1,16 @@ +""" +CLI for starting the fares server. +""" + +# Imports +from fares_site.serve import create_fares_server, FaresServer + + +# Script +def main(): + server: FaresServer = create_fares_server() + server.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/src/fares_site/api_calling.py b/src/fares_site/api_calling.py new file mode 100644 index 0000000..cb809ec --- /dev/null +++ b/src/fares_site/api_calling.py @@ -0,0 +1,24 @@ +""" +Standard functions to call for fares information. +""" + +# Imports +import requests +import pandas as pd +import numpy as np + +# Init. + + +# Functions +def fares_query(origin: str, dest: str, date: str) -> pd.DataFrame: + _ = requests # .get(url=url, headers=headers) + df = pd.DataFrame( + { + "origin": [origin for _ in range(10)], + "dest": [dest for _ in range(10)], + "date": [date for _ in range(10)], + "fare": np.random.rand(10) * 20, + } + ) + return df diff --git a/src/fares_site/serve.py b/src/fares_site/serve.py new file mode 100644 index 0000000..b75dbd4 --- /dev/null +++ b/src/fares_site/serve.py @@ -0,0 +1,98 @@ +""" +Web server core for the fares site. +""" + +# Import +from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler +from logging import Logger, INFO +from requests.exceptions import ConnectionError + +from fares_site.api_calling import fares_query + +# Init. +DEFAULT_ADDRESS = "127.0.0.1" +DEFAULT_PORT = 12_000 + +HTML_CONTENT_HEADER = """ + + + + + + + + + + +BALLAST DATA | Fares
+
+""" + +HTML_CONTENT_FOOTER = """ + + +""" + +logger: Logger = Logger(name=__name__, level=INFO) + + +# Classes +class FaresHandler(BaseHTTPRequestHandler): + def parse_url_query(self) -> dict[str, str]: + return { + s.split("=")[0]: s.split("=")[-1] + for s in self.requestline.split(" ")[1].split("/")[1:] + } + + def content_of_GET(self) -> str: + """ + Generates the content of a GET request. + Responsible for receiving the requests' details and constructing the output. + """ + table = fares_query( + origin=(_d := self.parse_url_query()).get("origin", "NCL"), + dest=_d.get("dest", "NCL"), + date=_d.get("date", "2026-04-06"), + ).style + table = table.format({"fare": "{:.2f}"}) + text = table.to_html() + return HTML_CONTENT_HEADER + text + HTML_CONTENT_FOOTER + + def do_GET(self) -> None: + """ + Handles GET requests - built-in method of BaseHTTPRequestHandler. + Has been abstracted so only responsibility is exception handling. + Currently, any exception is responded with '418: I am a teapot'. + Depending on what can go wrong with the future fares API we should update this. + """ + try: + content = self.content_of_GET() + self.send_response(200) + self.send_header("Content", "text/html") + self.end_headers() + _ = self.wfile.write(content.encode()) + return None + except ConnectionError as ex: + logger.warning(f"{type(ex)} | {ex}") + self.send_response(503, "Upstream API refused to respond.") + except Exception as ex: + logger.critical(f"{type(ex)} | {ex}") + self.send_response( + 418, + "Something abnormal occured, apologies. It has been logged with importance.", + ) + raise ex + self.end_headers() + + +class FaresServer(ThreadingHTTPServer): ... # Inherited just for typing. + + +# Functions +def create_fares_server( + address: str = DEFAULT_ADDRESS, port: int = DEFAULT_PORT +) -> FaresServer: + return FaresServer( + server_address=(address, port), + RequestHandlerClass=FaresHandler, + )