First scrappy version, no API.
This commit is contained in:
@@ -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"
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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 = """
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Merriweather+Sans:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
|
||||
<link rel="icon" type="image/x-icon" href="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d2/Tag_with_pound_Pinhead_icon.svg/250px-Tag_with_pound_Pinhead_icon.svg.png">
|
||||
</head>
|
||||
<body style="font-family: 'Merriweather Sans';">
|
||||
<img height=30px src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d2/Tag_with_pound_Pinhead_icon.svg/250px-Tag_with_pound_Pinhead_icon.svg.png">
|
||||
BALLAST DATA | Fares <br>
|
||||
<br>
|
||||
"""
|
||||
|
||||
HTML_CONTENT_FOOTER = """
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user