Compare commits

...

7 Commits

3 changed files with 284 additions and 53 deletions
+236
View File
@@ -0,0 +1,236 @@
<!DOCTYPE HTML>
<html>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Merriweather+Sans:ital,wght@0,300..800;1,300..800&display=swap">
<link rel="stylesheet" href="https://cdn.ballast-data.co.uk/theme.css">
<link rel="icon" type="image/x-icon" href="https://cdn.ballast-data.co.uk/Icon.svg">
<style>
html {
display: flex;
min-height: 100vh;
background-color: var(--A1);
}
body {
display: flex;
min-height: 100%;
width: 100%;"
background-color: var(--A2);
padding-left: 18px;
padding-right: 18px;
padding-top: 0px;
padding-bottom: 0px;
margin: 0px;
}
a {
color: var(--A0);
}
.wrapper {
display: flex;
flex-direction: column;
min-height: 100%;
width: 100%;
background-color: var(--A0);
padding: 0px;
margin: 0px;
}
.header {
position: sticky;
top: 0px;
width: fit-content;
min-width: 35em;
color: var(--A0);
background-color: var(--C2);
border: 6px solid var(--C1);
border-radius: 0 0 9px 9px;
padding: 12px;
margin-left: auto;
margin-right: auto;
margin-top: 0px;
}
.main {
width: fit-content;
background-color: white;
border: 9px solid var(--B2);
border-radius: 9px;
padding: 12px;
margin-left: auto;
margin-right: auto;
margin-bottom: 1em;
margin-top: 1em;
}
.branding {
width: fit-content;
border-bottom: 9px solid var(--C1);
margin-left: auto;
margin-right: auto;
}
.query {
width: fit-content;
margin-left: auto;
margin-right: auto;
}
.footer {
display: flex;
width: fit-content;
font-size: 0.8em;
color: var(--A0);
background-color: var(--D2);
border: 6px solid var(--D1);
border-radius: 9px 9px 0 0;
padding: 12px;
margin-left: auto;
margin-right: auto;
margin-top: auto;
}
.level0 {
text-align: right;
}
.col0,
.col1,
.col2 {
text-align: left;
}
input {
font-family: "Merriweather Sans";
font-size: 1em;
border: 3px solid var(--A1);
}
input:hover {
background-color: var(--C2);
}
</style>
<script>
function querySt(attribute) {
url = window.location.search.substring(1);
substrings = url.split('&');
for (i=0;i<substrings.length;i++) {
ft = substrings[i].split('=');
if (ft[0] == attribute) {
return ft[1];
}
}
}
function prefill(attribute) {
var value = querySt(attribute);
value = decodeURIComponent(value);
if(value !== 'undefined'){
document.getElementById(attribute).value = value;
} else {
document.getElementById(attribute).value = '';
}
}
</script>
</head>
<body>
<div class="wrapper">
<div class="header">
<table>
<tr>
<td>
<a href="/fares"><img src="https://cdn.ballast-data.co.uk/Icon-NB.svg"></a>
</td>
<td>
Ballast-Data Product <br>
<a href="/">Home</a> |
<a href="/fares">Fares</a> |
<a href="/stations">Stations</a> |
<a href="/about">About Us</a>
</td>
</tr>
</table>
</div>
<br>
<div class="main">
<div class="branding">
<br>
<a href="/fares">
<img height=80px src="https://cdn.ballast-data.co.uk/Logo.svg">
</a>
<b style="font-size: 4em; line-height: 1; font-weight: 600;"> Fares </b> <br>
</div>
<br>
<div class="query">
<form method="get" action="/fares">
<input
name="origin"
id="origin"
required
autocomplete="on"
placeholder="Origin*"
style="width: 10em;"
/>
<script>prefill('origin')</script>
<input
name="destination"
id="destination"
required
autocomplete="on"
placeholder="Destination*"
style="width: 10em;"
/>
<script>prefill('destination')</script>
<input
name="toc"
id="toc"
autocomplete="on"
placeholder="TOC"
style="width: 5em;"
/>
<script>prefill('toc')</script>
<input type="submit" value="Go!" style="width: 5em;">
</form>
</div>
<br>
<!-- Content -->
</div>
<br>
<div class="footer">
<span>
Powered by National Rail Enquiries. This site is not accredited by National Rail. <br>
We use information from
<a href="https://opendata.nationalrail.co.uk/" target="_blank">National Rail Open Data</a>
and
<a href="https://raildata.org.uk/" target="_blank">Rail Data Marketplace</a>. <br>
This service is not to be used for safety critical purposes. <br>
</span>
</div>
</div>
</body>
</html>
+10 -2
View File
@@ -12,12 +12,20 @@ import json
# Functions # Functions
def fares_query(origin: str, destination: str) -> list[dict[str, dict[str, str]]]: def fares_query(
origin: str,
destination: str,
toc: str | None = None,
) -> list[dict[str, dict[str, str]]]:
url = f"https://fares.ballast-data.co.uk/fares?origin={origin}&destination={destination}"
url += f"&toc={toc.upper()}" if toc is not None else ""
response = requests.get( response = requests.get(
url=f"https://fares.ballast-data.co.uk/fares?origin={origin}&destination={destination}", url=url,
auth=( auth=(
environ.get("BD_FARES_USER", ""), environ.get("BD_FARES_USER", ""),
environ.get("BD_FARES_PASS", ""), environ.get("BD_FARES_PASS", ""),
), ),
) )
if response.status_code != 200:
return []
return json.loads(response.content.decode()) # pyright: ignore[reportAny] return json.loads(response.content.decode()) # pyright: ignore[reportAny]
+38 -51
View File
@@ -7,7 +7,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from logging import INFO, Logger from logging import INFO, Logger
import pandas as pd import pandas as pd
from pandas.io.formats.style import Styler from pathlib import Path
from requests.exceptions import ConnectionError from requests.exceptions import ConnectionError
from fares_site.api_calling import fares_query from fares_site.api_calling import fares_query
@@ -35,72 +35,55 @@ COLUMN_RENAMES = {
"restriction_code": "Restriction", "restriction_code": "Restriction",
} }
HTML_CONTENT_HEADER = """ VALID_QUERY_TERMS = ["origin", "destination", "toc"]
<!DOCTYPE HTML>
<html>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Merriweather+Sans:ital,wght@0,300..800;1,300..800&display=swap">
<link rel="stylesheet" href="https://cdn.ballast-data.co.uk/theme.css">
<link rel="icon" type="image/x-icon" href="https://cdn.ballast-data.co.uk/Icon.svg">
</head>
<body style="padding: 0px; margin: 0px;">
<div style="width: 100%; padding: 0px; margin: 0px; background-color: var(--dark-color);"> DEFAULT_TEMPLATE_PATH = Path(__file__).parents[2] / "media/template.html"
<div style="margin-left: auto; margin-right: auto; width: fit-content; background-color: var(--secondary-accent); margin-top: 0px; padding: 12px; border-radius: 0 0 9px 9px;">
Ballast-Data Product <br>
Contact: Non-Doxxing Email Lorum Ipsum <br>
Some Header Content
</div>
<br>
<div style="width: fit-content; margin-left: auto; margin-right: auto; border: 9px solid var(--light-color); border-radius: 3px; padding: 12px; background-color: white; border-radius: 9px;">
<div style="width: fit-content; margin-left: auto; margin-right: auto; border-bottom: 9px solid var(--secondary-accent);">
<br>
<img height=80px src="https://cdn.ballast-data.co.uk/Logo.svg">
<b style="font-size: 60px; line-height: 1; font-weight: 400;"> RailFares </b> <br>
</div>
<br>
"""
HTML_CONTENT_FOOTER = """
</div>
<br>
<div style="margin-left: auto; margin-right: auto; width: fit-content; background-color: var(--primary-accent); margin-top: 0px; padding: 12px; border-radius: 9px 9px 0 0;">
Open Rail Data Disclaimer Etc. | No Guarantees or Whatever | We do freelance and dont want to be poor.
</div>
</div>
</body>
</html>
"""
logger: Logger = Logger(name=__name__, level=INFO) logger: Logger = Logger(name=__name__, level=INFO)
# Classes # Classes
class FaresHandler(BaseHTTPRequestHandler): class FaresHandler(BaseHTTPRequestHandler):
def parse_url_query(self) -> dict[str, str]: def parse_url_query(self) -> dict[str, str | None]:
text = self.requestline.split(" ")[1].split("?") text = self.requestline.split(" ")[1].split("?")
if len(text) == 1: if len(text) == 1:
return {} return {}
text = text[1] return {
return {s.split("=")[0]: s.split("=")[-1] for s in text.split("&")} _s[0]: _s[-1] if _s[-1] != "" else None
for s in text[1].split("&")
if (_s := s.split("="))[0] in VALID_QUERY_TERMS
}
def content_of_GET(self) -> str: def content_of_GET(self, template_path: Path = DEFAULT_TEMPLATE_PATH) -> str:
""" """
Generates the content of a GET request. Generates the content of a GET request.
Responsible for receiving the requests' details and constructing the output. Responsible for receiving the requests' details and constructing the output.
""" """
query_terms = self.parse_url_query()
origin = query_terms.get("origin")
destination = query_terms.get("destination")
if origin is None or destination is None:
text = "Search above including at least Origin and Destination."
with open(template_path, "r") as rf:
return rf.read().replace("<!-- Content -->", text)
assert origin is not None
assert destination is not None
data = fares_query( data = fares_query(
origin=(_d := self.parse_url_query()).get("origin", "NCL"), origin=origin,
destination=_d.get("destination", "NCL"), destination=destination,
toc=query_terms.get("toc"),
) )
text = "<table>" if len(data) == 0:
text = "No Fares Found."
with open(template_path, "r") as rf:
return rf.read().replace("<!-- Content -->", text)
text = '<table style="margin-left: auto; margin-right: auto;">'
text += ( text += (
'<tr style="font-size: 1.6em;"> <th> Flow </th> <th> Fares </th> </tr>\n' '<tr style="font-size: 1.6em;"> <th> Flow </th> <th> Fares </th> </tr>\n'
) )
for flow_fares in data: for flow_fares in sorted(data, key=lambda d: d["flow_id"]["flow_id"]):
flow_table = ( flow_table = (
pd.json_normalize(flow_fares["flow_id"]) pd.json_normalize(flow_fares["flow_id"])
.rename(COLUMN_RENAMES, axis=1) .rename(COLUMN_RENAMES, axis=1)
@@ -116,13 +99,14 @@ class FaresHandler(BaseHTTPRequestHandler):
) )
_ = fares_table.hide(axis=0) _ = fares_table.hide(axis=0)
text += '<tr> \n <td style="border-top: 6px solid var(--primary-accent); vertical-align: top;">' text += '<tr> \n <td style="border-top: 6px solid var(--D1); vertical-align: top;">'
text += flow_table.to_html() text += flow_table.to_html()
text += '</td> <td style="border-top: 6px solid var(--primary-accent); vertical-align: top;">' text += '</td> <td style="border-top: 6px solid var(--D1); vertical-align: top;">'
text += fares_table.to_html() text += fares_table.to_html()
text += "</td> \n </tr>" text += "</td> \n </tr>"
text += "</table>" text += "</table>"
return HTML_CONTENT_HEADER + text + HTML_CONTENT_FOOTER with open(template_path, "r") as rf:
return rf.read().replace("<!-- Content -->", text)
def do_GET(self) -> None: def do_GET(self) -> None:
""" """
@@ -138,9 +122,11 @@ class FaresHandler(BaseHTTPRequestHandler):
self.end_headers() self.end_headers()
_ = self.wfile.write(content.encode()) _ = self.wfile.write(content.encode())
return None return None
except ConnectionError as ex: except ConnectionError as ex:
logger.warning(f"{type(ex)} | {ex}") logger.warning(f"{type(ex)} | {ex}")
self.send_response(503, "Upstream API refused to respond.") self.send_response(503, "Our upstream API refused to respond.")
except Exception as ex: except Exception as ex:
logger.critical(f"{type(ex)} | {ex}") logger.critical(f"{type(ex)} | {ex}")
self.send_response( self.send_response(
@@ -148,6 +134,7 @@ class FaresHandler(BaseHTTPRequestHandler):
"Something abnormal occured, apologies. It has been logged with importance.", "Something abnormal occured, apologies. It has been logged with importance.",
) )
raise ex raise ex
self.end_headers() self.end_headers()