248 lines
7.7 KiB
Python
248 lines
7.7 KiB
Python
import sys
|
||
from collections import defaultdict
|
||
from typing_extensions import cast
|
||
from yattag import Doc, AsIs
|
||
import re
|
||
from functools import reduce
|
||
import json
|
||
import time
|
||
import datetime
|
||
from pathlib import Path
|
||
|
||
SLUG_PATTERN = re.compile(r"[\W_]+")
|
||
|
||
|
||
def slugify(value):
|
||
value = value.encode("ascii", errors="ignore").decode()
|
||
value = SLUG_PATTERN.sub("-", value)
|
||
return value.strip("-")
|
||
|
||
|
||
JSONFILE = sys.argv[1]
|
||
OUTPUTFILE = sys.argv[2]
|
||
|
||
|
||
col_titles = {
|
||
# included in the CSV
|
||
"lab": "Lab",
|
||
"chemistry": "Chemistry",
|
||
"format": "Format",
|
||
"subformat": "Subformat",
|
||
"includesSendShipping": "Includes outbound shipping?",
|
||
"sendShippingType": "Outbound shipping type",
|
||
"returnShippingCost": "Return shipping cost",
|
||
"returnShippingType": "Return shipping provider",
|
||
"cost": "Development cost",
|
||
"resolution": "Scan resolution",
|
||
"resolutionName": "Scan resolution name",
|
||
"url": "Product URL",
|
||
# render only
|
||
"outboundShipping": "Outbound shipping",
|
||
"returnShipping": "Return shipping",
|
||
"renderResolution": "Scan resolution",
|
||
"pricePerPixel": "Price per pixel",
|
||
"link": "Order page",
|
||
"calculatedPrice": "Calculated price",
|
||
}
|
||
|
||
entries_by_type = defaultdict(lambda: [])
|
||
notes_by_type = {}
|
||
|
||
|
||
def _render_line(*args, **kwargs):
|
||
d = Doc()
|
||
d.line(*args, **kwargs)
|
||
return d.getvalue()
|
||
|
||
|
||
def _format_price(price):
|
||
return "£{:.2f}".format(price)
|
||
|
||
|
||
raw_data_object = None
|
||
|
||
with open(JSONFILE) as f:
|
||
raw_data_object = json.load(f)
|
||
|
||
for row in raw_data_object["data"]:
|
||
entries_by_type[(row["chemistry"], row["format"], row["subformat"])].append(row)
|
||
|
||
for row in raw_data_object["notes"]:
|
||
notes_by_type[(row["chemistry"], row["format"], row["subformat"])] = row["note"]
|
||
|
||
acc = """---
|
||
title: "Film Development Price Comparison"
|
||
hideAside: true
|
||
asDirectory: false
|
||
---
|
||
{% extends "_layouts/base.html" %}
|
||
{% block head %}
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/simple-datatables@latest/dist/style.css" />
|
||
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@latest"></script>
|
||
<style>
|
||
nav.datatable-pagination {
|
||
all: unset !important;
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block main %}
|
||
"""
|
||
|
||
doc, tag, text, line = Doc().ttl()
|
||
|
||
with tag("div", klass="container pt-3"):
|
||
|
||
line("a", "Back to photography", href="https://www.akpain.net/photography/")
|
||
|
||
line("h1", "Film Development Price Comparison")
|
||
|
||
line("p", "This is my attempt to work out the best value for money film developing and scanning service that's available in the UK. Labs are compared as like-for-like as possible, but some variation (especially in scan size) is inevitable.")
|
||
with tag("p"):
|
||
text("If your favourite/local/whatever lab isn't listed here, ")
|
||
line("a", "let me know", href="https://www.akpain.net#contact")
|
||
text(" and I'll add it! Likewise, if you want to see E6, ECN2, half frame, 120 or anything else here, please do tell me.")
|
||
|
||
with tag("p"):
|
||
text(
|
||
"Development costs last updated "
|
||
+ datetime.datetime.utcfromtimestamp(raw_data_object["time"]).strftime(
|
||
"%Y-%m-%d %H:%M:%S"
|
||
)
|
||
+ ". Price per pixel figures do not include estimates for outbound or return shipping. "
|
||
)
|
||
line("a", "Raw data available here", href="rawdata.json")
|
||
text(". ")
|
||
line("a", "Git repo", href="https://git.tdpain.net/codemicro/film-dev-cost-scraper")
|
||
text(".")
|
||
|
||
|
||
with tag("div", klass="toc-container", style="width: 18rem;"):
|
||
with tag("div", klass="header"):
|
||
line("span", "On this page", klass="h4 font--monospace-bold")
|
||
with tag("div", klass="content"):
|
||
with tag("ul"):
|
||
for key in entries_by_type:
|
||
chemistry, format, subformat = key
|
||
slug = slugify(chemistry + format + subformat)
|
||
with tag("li"):
|
||
line("a", f"{chemistry} {format} ({subformat})", href=f"#{slug}-title")
|
||
|
||
slugs = []
|
||
|
||
for key in entries_by_type:
|
||
chemistry, format, subformat = key
|
||
|
||
slug = slugify(chemistry + format + subformat)
|
||
slugs.append(slug)
|
||
|
||
line(
|
||
"h2",
|
||
f"{chemistry} {format} ({subformat})",
|
||
id=slug + "-title",
|
||
)
|
||
|
||
if key in notes_by_type:
|
||
line("p", notes_by_type[key])
|
||
|
||
cols = [
|
||
("lab", lambda x: x["lab"]),
|
||
(
|
||
"outboundShipping",
|
||
lambda x: "×"
|
||
if x["includesSendShipping"].lower() == "no"
|
||
else x["sendShippingType"],
|
||
),
|
||
(
|
||
"returnShipping",
|
||
lambda x: (
|
||
"Free"
|
||
if (c := float(x["returnShippingCost"])) == 0
|
||
else _format_price(c)
|
||
)
|
||
+ f" ({x['returnShippingType']})",
|
||
),
|
||
("cost", lambda x: _format_price(float(x["cost"]))),
|
||
(
|
||
"renderResolution",
|
||
lambda x: f"{x['resolution']} ({repr(x['resolutionName'])})",
|
||
),
|
||
(
|
||
"pricePerPixel",
|
||
lambda x: "{:.5f}p".format(
|
||
float(x["cost"])
|
||
* 100
|
||
/ reduce(
|
||
lambda y, z: y * z,
|
||
map(int, x["resolution"].split("x")),
|
||
1,
|
||
)
|
||
),
|
||
),
|
||
("link", lambda x: _render_line("a", "Link", href=x["url"])),
|
||
]
|
||
|
||
# begin working out price per pixel colour scales
|
||
pppfn = None
|
||
for i, item in enumerate(cols):
|
||
if item[0] == "pricePerPixel":
|
||
pppfn = item[1]
|
||
break
|
||
assert pppfn is not None
|
||
pppcolours = {pppfn(data): "" for data in entries_by_type[key]}
|
||
coldiff = (
|
||
int(120 / (len(pppcolours) - 1)) if len(pppcolours) - 1 != 0 else 0
|
||
)
|
||
for i, (val, rawval) in enumerate(
|
||
sorted(
|
||
map(lambda x: (float(x[:-1]), x), pppcolours.keys()),
|
||
key=lambda y: y[0],
|
||
)
|
||
):
|
||
pppcolours[rawval] = f"hsl({120 - (i * coldiff)}, 71%, 73%)"
|
||
# end
|
||
|
||
with tag("table", klass="table table-hover", id=slug):
|
||
with tag("thead"):
|
||
with tag("tr"):
|
||
for t, _ in cols:
|
||
line("th", col_titles[t], scope="col")
|
||
|
||
with tag("tbody"):
|
||
for data in sorted(
|
||
entries_by_type[key], key=lambda x: x["lab"]
|
||
):
|
||
with tag("tr"):
|
||
for i, (key, fn) in enumerate(cols):
|
||
if i == 0:
|
||
line("th", fn(data), scope="row")
|
||
else:
|
||
with tag("td"):
|
||
val = fn(data)
|
||
doc.asis(val)
|
||
|
||
if key == "pricePerPixel":
|
||
doc.attr(
|
||
style="background-color: "
|
||
+ pppcolours[val]
|
||
)
|
||
|
||
with tag("script"):
|
||
doc.asis("const slugs = ")
|
||
doc.asis(json.dumps(slugs))
|
||
doc.asis(";\n")
|
||
with open(Path(__file__).resolve().parent / "page.js") as f:
|
||
doc.asis(f.read())
|
||
|
||
with tag(
|
||
"script",
|
||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js",
|
||
):
|
||
doc.asis()
|
||
|
||
acc += doc.getvalue()
|
||
acc += """
|
||
{% endblock %}"""
|
||
|
||
with open(OUTPUTFILE, "w") as f:
|
||
f.write(acc)
|