---
title: "Habit Tracker"
description: "Habit tracker API with streak labels as computed columns, deployable to Pixeltable Cloud"
url: "https://www.pixeltable.com/recipes/habit-tracker"
source: "https://github.com/pierrebrunelle/pixeltable-habit-tracker"
---

# Habit Tracker

Habit tracker API with streak labels as computed columns, deployable to Pixeltable Cloud

One `app.py` with 1 table, 3 computed columns, 4 HTTP routes. Needs `pixeltable[serve]>=0.7.14`.

## Tables

### habits (table)

| Column | Kind | Type or expression |
| --- | --- | --- |
| id | primary key | `pxtf.uuid.uuid7()` |
| name | stored | String |
| category | stored | String |
| streak_days | stored | Int (optional) |
| notes | stored | String (optional) |
| label | computed on insert | `streak_label(streak_days)` |
| name_upper | computed on insert | `pxtf.string.upper(name)` |
| name_blurb | computed on insert | `habit_blurb(name, category)` |

## Routes (service `habits_api`)

- `POST /habits`: inserts into `habits` (takes name, category, streak_days, notes; returns id, label, name_upper, name_blurb)
- `POST /habits/update`: updates `habits` (takes streak_days; returns id, streak_days, label)
- `POST /streak-label`: computes `habits` columns without storing (takes streak_days; returns label)
- `GET /habits/by-category`: runs the query `by_category` (takes category)

## Run it locally

```sh
git clone https://github.com/pierrebrunelle/pixeltable-habit-tracker
cd pixeltable-habit-tracker
uv sync
uv run pxt schema update app.py habits
uv run python seed.py habits
uv run pxt service run app.py habits --port 8000
```

## 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
uv run pxt service update app.py pxt://<org>:main -f
```

## app.py

At commit `5b5e7225` (https://github.com/pierrebrunelle/pixeltable-habit-tracker/blob/5b5e7225b8e1dce9639e545cfcec821ce20e3d8e/app.py):

```python
"""Habit Tracker API built with Pixeltable.

    pxt schema update app.py habits
    pxt service run app.py habits
"""
import pixeltable as pxt
import pixeltable.functions as pxtf
from pixeltable.serving import FastAPIRouter

from udfs import habit_blurb, streak_label

# ---- tables ----
TableModel = pxt.model_base()


class Habits(TableModel, name='habits'):
    id = pxt.Column(value=pxtf.uuid.uuid7(), primary_key=True)
    name: pxt.String
    category: pxt.String
    streak_days: pxt.Int | None
    notes: pxt.String | None

    label = streak_label(streak_days)
    name_upper = pxtf.string.upper(name)
    name_blurb = habit_blurb(name, category)


# ---- queries ----
@pxt.query
def by_category(category: str):
    """Habits in a category, longest streak first."""
    return Habits.where(Habits.category == category).select(
        Habits.id, Habits.name, Habits.streak_days, Habits.label
    ).order_by(Habits.streak_days, asc=False)


# ---- routes ----
habits_api = FastAPIRouter(name='habits_api')
habits_api.add_insert_route(
    Habits, path='/habits',
    inputs=[Habits.name, Habits.category, Habits.streak_days, Habits.notes],
    outputs=[Habits.id, Habits.label, Habits.name_upper, Habits.name_blurb],
)
habits_api.add_update_route(Habits, path='/habits/update', inputs=[Habits.streak_days],
                            outputs=[Habits.id, Habits.streak_days, Habits.label])
habits_api.add_compute_route(Habits, path='/streak-label', inputs=[Habits.streak_days], outputs=[Habits.label])
habits_api.add_query_route(path='/habits/by-category', query=by_category, method='get')
```
