Climbing Route Topos
Climbing-route topo registry API: photo uploads with thumbnails, grayscale copies, grade bands and index-backed crag lookups
One app.py with 1 table, 6 computed columns, 7 HTTP routes. Needs pixeltable[serve]>=0.7.16. Updated 2026-10-09.
app.py
"""Climbing Route Topo Registry API built with Pixeltable.Upload a route topo photo with crag, grade and style. Pixeltable stores the image,derives a thumbnail and grayscale copy as media computed columns, and tags aspect,dominant color and a combined topo label via UDFs. B-tree indexes on crag, grade andstyle back the lookup queries the API exposes.Layout: UDFs live in udfs.py; this file holds table models, @pxt.query functions and routes.Run locally:pxt schema update app.py climbingpxt service run app.py climbing"""import pixeltable as pxtimport pixeltable.functions as pxtffrom pixeltable.serving import FastAPIRouterfrom udfs import aspect_band, dominant_channel, grade_band, topo_tag# ---- tables ----TableModel = pxt.model_base()class Routes(TableModel, name='routes', has_default_idxs=False):route_id = pxt.Column(value=pxtf.uuid.uuid7(), primary_key=True)name: pxt.Stringcrag: pxt.Stringgrade: pxt.String # e.g. 5.10astyle: pxt.String # sport / trad / boulder / alpinebolts: pxt.Int | Nonelength_m: pxt.Float | Nonetopo: pxt.Image# generated mediathumb = topo.resize((96, 128))gray = topo.convert('L')# UDF chainaspect = aspect_band(topo)channel = dominant_channel(topo)band = grade_band(grade)tag = topo_tag(aspect, channel, style)__indexes__ = [pxt.BtreeIndex(crag),pxt.BtreeIndex(grade),pxt.BtreeIndex(style),pxt.BtreeIndex(band),]# ---- queries ----@pxt.querydef by_crag(crag: str):"""Routes at one crag (crag index), hardest grade first."""return (Routes.where(Routes.crag == crag).select(Routes.route_id, Routes.name, Routes.grade, Routes.style, Routes.tag, Routes.band).order_by(Routes.grade, asc=False))@pxt.querydef by_grade_band(band: str, style: str):"""Routes in a grade band and style (band + style indexes)."""return (Routes.where((Routes.band == band) & (Routes.style == style)).select(Routes.route_id, Routes.name, Routes.crag, Routes.grade, Routes.tag).order_by(Routes.name))@pxt.querydef route_thumb(name: str):"""Thumbnail for a route name (served as an image file)."""return (Routes.where(Routes.name == name).order_by(Routes.route_id, asc=False).limit(1).select(Routes.thumb))@pxt.querydef route_detail(name: str):"""One route's metadata and media tags."""return Routes.where(Routes.name == name).select(Routes.route_id, Routes.name, Routes.crag, Routes.grade, Routes.style,Routes.bolts, Routes.length_m, Routes.aspect, Routes.channel, Routes.band, Routes.tag,)# ---- routes ----climbing_api = FastAPIRouter(name='climbing_api')climbing_api.add_insert_route(Routes,path='/routes',inputs=[Routes.name, Routes.crag, Routes.grade, Routes.style, Routes.bolts, Routes.length_m],uploadfile_inputs=[Routes.topo],outputs=[Routes.route_id, Routes.aspect, Routes.channel, Routes.band, Routes.tag],)climbing_api.add_compute_route(Routes,path='/analyze',inputs=[Routes.grade, Routes.style],uploadfile_inputs=[Routes.topo],outputs=[Routes.aspect, Routes.channel, Routes.band, Routes.tag],)climbing_api.add_delete_route(Routes, path='/routes/delete')climbing_api.add_query_route(path='/routes/by-crag', query=by_crag, method='get')climbing_api.add_query_route(path='/routes/by-band', query=by_grade_band, method='get')climbing_api.add_query_route(path='/routes/thumb', query=route_thumb, method='get', one_row=True, return_fileresponse=True)climbing_api.add_query_route(path='/routes/detail', query=route_detail, method='get', one_row=True)
What app.py declares
Each row links to its line.
- runs on insert
- stored
- index
- route
- route_idnamecraggradestyleboltslength_mtopo
- thumb = topo.resize((96, 128))L35
- gray = topo.convert('L')L36
- aspect = aspect_band(topo)L39
- channel = dominant_channel(topo)L40
- band = grade_band(grade)L41
- tag = topo_tag(aspect, channel, style)L42
- B-tree index on cragL45
- B-tree index on gradeL46
- B-tree index on styleL47
- B-tree index on bandL48
serves climbing_api
- POST/routesinsert, routestakes name, crag, grade, style, bolts, length_m; returns route_id, aspect, channel, band, tagL96
- POST/analyzecompute, routestakes grade, style; returns aspect, channel, band, tagL103
- POST/routes/deletedelete, routesL110
- GET/routes/by-cragquery, by_cragtakes cragL111
- GET/routes/by-bandquery, by_grade_bandtakes band, styleL112
- GET/routes/thumbquery, route_thumbtakes nameL113
- GET/routes/detailquery, route_detailtakes nameL116
Run it
Needs uv. The routes then answer on port 8000.
terminal
git clone https://github.com/pierrebrunelle/pixeltable-climbing-routescd pixeltable-climbing-routesuv syncuv run pxt schema update app.py climbinguv run python seed.py climbinguv run pxt service run app.py climbing --port 8000
Related apps
- Mural RegistryPublic-art mural registry API: wall photo uploads with thumbnails, grayscale copies and color tagsShares: B-tree indexes, Images, video and audio, UDFs, File uploads
- Travel ScrapbookTravel photo scrapbook API: image uploads, thumbnails and place tags as computed columnsShares: Images, video and audio, UDFs, File uploads
- Research Agent MemoryDurable, searchable long-term memory for an AI research assistant: per-user semantic recall, retrieval_udf lookups, tool calling and memory consolidation with custom aggregatesShares: B-tree indexes, Images, video and audio, UDFs