---
title: "Archery Range"
description: "Migrate archery-range scoring sessions from legacy JSON into Pixeltable, score with date-aware computed columns, and run as an incremental scheduled pipeline"
url: "https://www.pixeltable.com/recipes/archery-lanes"
source: "https://github.com/pierrebrunelle/pixeltable-archery-lanes"
---

# Archery Range

Migrate archery-range scoring sessions from legacy JSON into Pixeltable, score with date-aware computed columns, and run as an incremental scheduled pipeline

One `app.py` with 1 table, 13 computed columns. Needs `pixeltable>=0.7.16`.

## Tables

### sessions (table)

| Column | Kind | Type or expression |
| --- | --- | --- |
| session_id | primary key | `pxtf.uuid.uuid7()` |
| session_code | stored | String |
| archer | stored | String |
| lane | stored | Int |
| session_date | stored | Date |
| arrows | stored | Int |
| hits | stored | Int |
| x_rings | stored | Int |
| notes | stored | String |
| session_year | computed on insert | `pxtd.year(session_date)` |
| session_month | computed on insert | `pxtd.month(session_date)` |
| session_day | computed on insert | `pxtd.day(session_date)` |
| session_weekday | computed on insert | `pxtd.weekday(session_date)` |
| session_iso_weekday | computed on insert | `pxtd.isoweekday(session_date)` |
| session_iso | computed on insert | `pxtd.isoformat(session_date)` |
| next_practice | computed on insert | `pxtd.add_days(session_date, 7)` |
| calendar_label | computed on insert | `pxtd.strftime(session_date, '%a %b %d, %Y')` |
| hit_pct | computed on insert | `hits * 100.0 / arrows` |
| score | computed on insert | `hits * 100.0 / arrows + x_rings * 2.0` |
| band | computed on insert | `score_band(hit_pct)` |
| tag | computed on insert | `archer_tag(archer, lane)` |
| boto3_ok | computed on insert | `boto3_core_probe()` |

- B-tree index on `archer`
- B-tree index on `lane`
- B-tree index on `session_date`
- B-tree index on `band`
- B-tree index on `session_code`

No HTTP routes: insert into and query the tables from Python.

## Run it locally

```sh
git clone https://github.com/pierrebrunelle/pixeltable-archery-lanes
cd pixeltable-archery-lanes
uv sync
uv run pxt schema update app.py archery
```

## Run it on Pixeltable Cloud

```sh
uv run pxt new    # a free database, no account
# put the pxt:// address it prints in pixeltable.toml
uv run pxt db update pxt://<org>:main -f
uv run pxt schema update app.py pxt://<org>:main -f
```

## app.py

At commit `66f99c38` (https://github.com/pierrebrunelle/pixeltable-archery-lanes/blob/66f99c38c2a45067631d95d59e5caa4baa885993/app.py):

```python
"""Archery Lanes scheduled pipeline: migrate legacy JSON sessions, date helpers, alter_computed_column scoring. Tables and @pxt.query live here; UDFs in udfs.py.
"""
from __future__ import annotations


import pixeltable as pxt
import pixeltable.functions as pxtf
from pixeltable.functions import date as pxtd

from udfs import archer_tag, boto3_core_probe, score_band

TableModel = pxt.model_base()


class Sessions(TableModel, name='sessions', has_default_idxs=False):
    """One archery-range scoring session migrated from legacy JSON."""

    session_id = pxt.Column(value=pxtf.uuid.uuid7(), primary_key=True)
    session_code: pxt.String
    archer: pxt.String
    lane: pxt.Int
    session_date: pxt.Date
    arrows: pxt.Int
    hits: pxt.Int
    x_rings: pxt.Int
    notes: pxt.String

    # --- fn.date secondary ---
    session_year = pxtd.year(session_date)
    session_month = pxtd.month(session_date)
    session_day = pxtd.day(session_date)
    session_weekday = pxtd.weekday(session_date)  # Mon=0 .. Sun=6
    session_iso_weekday = pxtd.isoweekday(session_date)
    session_iso = pxtd.isoformat(session_date)
    next_practice = pxtd.add_days(session_date, 7)
    calendar_label = pxtd.strftime(session_date, '%a %b %d, %Y')

    # --- scoring (v1: hit%; later altered to include X-ring bonus) ---
    hit_pct = (hits * 100.0) / arrows
    # Club formula (v2): hit% + 2 per X-ring. pipeline.py alters from v1 hit_pct -> this.
    score = (hits * 100.0) / arrows + (x_rings * 2.0)
    band = score_band(hit_pct)
    tag = archer_tag(archer, lane)
    boto3_ok = boto3_core_probe()

    __indexes__ = [
        pxt.BtreeIndex(archer),
        pxt.BtreeIndex(lane),
        pxt.BtreeIndex(session_date),
        pxt.BtreeIndex(band),
        pxt.BtreeIndex(session_code),
    ]


@pxt.query
def by_archer(archer: str):
    """Sessions for one archer, newest first."""
    return (
        Sessions.where(Sessions.archer == archer)
        .select(
            Sessions.session_code,
            Sessions.session_date,
            Sessions.lane,
            Sessions.hit_pct,
            Sessions.score,
            Sessions.band,
            Sessions.calendar_label,
            Sessions.next_practice,
        )
        .order_by(Sessions.session_date, asc=False)
    )


@pxt.query
def weekend_club(min_pct: float = 70.0):
    """Weekend sessions (Sat/Sun) at or above a hit percentage."""
    return (
        Sessions.where(
            (Sessions.session_weekday >= 5) & (Sessions.hit_pct >= min_pct)
        )
        .select(
            Sessions.session_code,
            Sessions.archer,
            Sessions.calendar_label,
            Sessions.hit_pct,
            Sessions.score,
            Sessions.band,
            Sessions.tag,
        )
        .order_by(Sessions.hit_pct, asc=False)
    )


@pxt.query
def lane_day(lane: int, day_iso: str):
    """Sessions on one lane for a given ISO date string (YYYY-MM-DD)."""
    return (
        Sessions.where((Sessions.lane == lane) & (Sessions.session_iso == day_iso))
        .select(
            Sessions.session_code,
            Sessions.archer,
            Sessions.arrows,
            Sessions.hits,
            Sessions.score,
            Sessions.boto3_ok,
        )
        .order_by(Sessions.session_code)
    )
```
