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 %} {% 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)