Shared infra for the hasadna Open Bus hackathon. Everyone writes one function; they all show up on one dashboard for the final presentation.
POCs proven here get consolidated upstream into open-bus-map-search, open-bus-stride-api, and open-bus-pipelines.
You need Docker + VS Code. Everything else is inside the container — don't install Python or Node on your machine.
- Install Docker and the VS Code Dev Containers extension.
- Clone and open this repo in VS Code.
- Click Reopen in Container when prompted (or
F1→ Dev Containers: Reopen in Container). - Wait for the one-time setup, then in the container terminal:
./devThat's it. Three things start:
| URL | What | |
|---|---|---|
| Dashboard | http://localhost:5173 | ← the thing you look at |
| API | http://localhost:8000/docs | runs your analysis, feeds the dashboard |
| JupyterLab | http://localhost:8888 | notebooks, no token needed |
No Docker? Open the repo in a GitHub Codespace instead — same container, nothing to install.
Run claude in any container terminal. It sees the same Python env, the same data
client, and the same running services you do — so "why is my analysis returning an
empty frame?" is a question it can actually investigate.
First run asks you to sign in. That login is stored in a Docker volume scoped to this repo, so you only do it once — it survives container rebuilds.
./dev new delay-by-hourThat scaffolds analyses/delay_by_hour.py. Fill in the TODO and your card appears
on the dashboard — no wiring, no imports to register anywhere.
The whole contract:
from openbus_hack import AnalysisRequest, analysis, line_chart, stride
@analysis(
name="delay-by-hour",
title="Delay by hour of day",
description="Median delay per hour, to find the worst part of the day.",
author="your-name",
)
def run(req: AnalysisRequest):
df = stride.siri_rides(
lines=req.lines,
operators=req.operators,
date_from=req.date_from,
date_to=req.date_to,
)
hourly = df.groupby(df["scheduled_start_time"].dt.hour).size().reset_index(name="rides")
return line_chart(hourly, x="scheduled_start_time", y="rides")req.lines |
["480", "1"] — line short-names (may be empty) |
req.operators |
["אגד"] — agency names (may be empty) |
req.date_from / req.date_to |
date objects |
req.line / req.operator |
first one, for single-value analyses |
req.days |
number of days in the window |
req.opt("top_n", 5) |
your own declared options |
| Return this | Renders as |
|---|---|
metrics(("Total rides", 1234), ("On-time", 87.5)) |
stat tiles |
line_chart(df, x="day", y="rides", series="operator") |
line chart |
bar_chart(df, x="hour", y="count", stacked=True) |
bar chart |
table(df) |
scrollable table |
| a matplotlib figure | inline PNG |
| a DataFrame | table |
Add caveats with notes=["only 3 days of SIRI data"] — they render under the chart.
Set draft=True while you're still hacking; the card gets a DRAFT badge so
nobody demos a work-in-progress by accident.
from openbus_hack import stride
stride.agencies() # operator_ref ↔ agency_name
stride.routes(lines=["480"]) # GTFS routes
stride.siri_rides(lines=["480"]) # actual observed rides
stride.gtfs_rides_agg(d1, d2, group_by="operator_ref,gtfs_route_date") # fast aggregates
stride.siri_vehicle_locations(t1, t2) # raw GPS pings — pass a tight windowAll return DataFrames, all paged for you, all cached on disk — so re-running a
cell fifty times hits the cache, not the community's shared API.
stride.clear_cache() to reset.
Full API: https://open-bus-stride-api.hasadna.org.il/docs
gtfs_rides_agg'sgroup_byonly acceptsgtfs_route_date,gtfs_route_hour,operator_ref,day_of_week,line_ref. Anything else returns a 500. Direct link to that endpoint's docs: https://open-bus-stride-api.hasadna.org.il/docs#/aggregations/group_by__gtfs_rides_agg_group_by_get
The Stride API is backed by a Postgres read replica, reachable directly if you'd rather write SQL than page through REST:
host = open-bus-stride-db.hasadna.org.il
port = 5432
user = talpihack26
(Password shared separately — ask in Slack if you don't have it.) Prefer
openbus_hack.stride for anything the REST API already covers — it's cached on
disk and won't hammer the shared DB.
| Stride API source | https://github.com/hasadna/open-bus-stride-api |
| Stride DB schema / data model | https://github.com/hasadna/open-bus-stride-db/blob/main/DATA_MODEL.md |
| Stride Python client + notebook guide | https://github.com/hasadna/open-bus-stride-client/blob/main/README.md#using-the-interactive-jupyter-notebooks |
| Open Bus map search (consumes this data today) | https://github.com/hasadna/open-bus-map-search |
| Geo layer for roads / PT infrastructure | https://geo.mot.gov.il/ |
| Line info lookup (e.g. line 86001) | https://markav.net/line/86001/ |
| Hasadna Slack (#opendata / open-bus channels) | https://join.slack.com/t/hasadna/shared_invite/zt-458cp0v0n-GSnHzAq6F5aHeK43O4YeeA |
What each analysis actually does: algorithms/ — one file per
solution, with the author, the algorithm, the reasoning, findings (each with a
confidence level), and criticism. algorithms/upstream-issues.md
collects every upstream defect these solutions hit, as paste-ready issue drafts
routed to open-bus-map-search and its sibling repos.
Related work — read before trusting a number:
docs/busanalysis.md summarises
lihay7/BusAnalysis (private), a national
planned-vs-actual reconstruction on this same data. It documents five defects in
the upstream stride tables that constrain what our analyses can honestly claim —
most importantly that the stored SIRI→GTFS ride link has been empty since October
2024, and that first_vehicle_location_id is a processing-state flag, not evidence
a vehicle stopped transmitting.
workspace/<your-name>/ is created for you on first run:
workspace/noam/
notebooks/ drafts, exploration, dead ends
data/ intermediate results worth keeping (data/raw/ is gitignored)
out/ exported charts
NOTES.md running log
Be messy in there. Only analyses/*.py reaches the dashboard.
This repo ignores almost nothing on purpose — commit your drafts, notebooks and intermediate results. A half-finished notebook in git beats a perfect one on your laptop.
./dev save "explored delay distribution for 480"Stages everything, commits, pushes. nbdime is configured, so notebook diffs are
actually readable.
./dev start everything
./dev dash dashboard only
./dev api API only
./dev lab JupyterLab only
./dev new <name> scaffold a new analysis
./dev list list registered analyses
./dev check verify nothing is broken ← run before the demo
./dev save "msg" commit + push everything
.devcontainer/ the dev environment
openbus_hack/ shared library
contract.py AnalysisRequest / AnalysisResult + return helpers
registry.py the @analysis decorator and auto-discovery
stride.py Open Bus API client
theme.py shared matplotlib styling
server.py FastAPI bridge to the dashboard
analyses/ ← one file per person; this is the deliverable
frontend/ the dashboard (React + Vite)
notebooks/ shared notebooks
workspace/ per-person scratch space
One broken analysis can't break the demo: import failures and exceptions render as an error card, and everything else keeps working.
The dashboard has a Playwright end-to-end suite in frontend/e2e/. It never
talks to the real API — /api/** is intercepted with page.route() and
served from hand-written fixtures under frontend/e2e/fixtures/ (shaped to
match openbus_hack/contract.py), because the real analyses call a public
transit API and can take 60-90s per call. That means the suite only needs the
Vite dev server, not the Python backend.
cd frontend
npx playwright install --with-deps chromium # first time only
npm run test:e2e
GitHub Actions (.github/workflows/ci.yml) runs on every push/PR to main:
a python job that uv syncs and runs ./dev check (plus a non-blocking
ruff check), and a frontend job that type-checks with tsc and runs the
Playwright suite, uploading the HTML report as an artifact.
https://docs.google.com/spreadsheets/d/1uFikn1oFehRSQzr4VxS09NjVna7_gvvluq2YKs1pl5Y/edit?gid=0#gid=0