---
title: "Travel Scrapbook"
description: "Travel photo scrapbook API: image uploads, thumbnails and place tags as computed columns"
url: "https://www.pixeltable.com/recipes/travel-scrapbook"
source: "https://github.com/pierrebrunelle/pixeltable-travel-scrapbook"
---

# Travel Scrapbook

Travel photo scrapbook API: image uploads, thumbnails and place tags as computed columns

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

## Tables

### scrapbook (table)

| Column | Kind | Type or expression |
| --- | --- | --- |
| id | primary key | `pxtf.uuid.uuid7()` |
| city | stored | String |
| country | stored | String |
| taken_on | stored | String (optional) |
| caption | stored | String (optional) |
| photo | stored | Image |
| thumb | computed on insert | `photo.resize((64, 48))` |
| place | computed on insert | `trip_tag(city, country)` |
| shape | computed on insert | `orientation(photo)` |
| blurb | computed on insert | `caption_blurb(caption)` |

## Routes (service `scrapbook_api`)

- `POST /photos`: inserts into `scrapbook` (takes city, country, taken_on, caption; returns id, place, shape, blurb)
- `POST /photos/caption`: updates `scrapbook` (takes caption; returns id, blurb)
- `GET /photos/by-country`: runs the query `photos_in` (takes country)
- `GET /photos/thumb`: runs the query `latest_thumb` (takes city)

## Run it locally

```sh
git clone https://github.com/pierrebrunelle/pixeltable-travel-scrapbook
cd pixeltable-travel-scrapbook
uv sync
uv run pxt schema update app.py scrapbook
uv run python seed.py scrapbook
uv run pxt service run app.py scrapbook --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 `0e47ab91` (https://github.com/pierrebrunelle/pixeltable-travel-scrapbook/blob/0e47ab9189b456265ca36826961d9d07ff3f6c34/app.py):

```python
"""Travel Scrapbook API built with Pixeltable.

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

from udfs import caption_blurb, orientation, trip_tag

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


class Scrapbook(TableModel, name='scrapbook'):
    id = pxt.Column(value=pxtf.uuid.uuid7(), primary_key=True)
    city: pxt.String
    country: pxt.String
    taken_on: pxt.String | None
    caption: pxt.String | None
    photo: pxt.Image

    thumb = photo.resize((64, 48))          # stored media, generated once per photo
    place = trip_tag(city, country)
    shape = orientation(photo)
    blurb = caption_blurb(caption)


# ---- queries ----
@pxt.query
def photos_in(country: str):
    """Photos from one country, by date."""
    return Scrapbook.where(Scrapbook.country == country).select(
        Scrapbook.id, Scrapbook.place, Scrapbook.taken_on, Scrapbook.shape, Scrapbook.blurb
    ).order_by(Scrapbook.taken_on)


@pxt.query
def latest_thumb(city: str):
    """The most recent thumbnail for a city (served as an image file)."""
    return Scrapbook.where(Scrapbook.city == city).order_by(Scrapbook.id, asc=False).limit(1).select(Scrapbook.thumb)


# ---- routes ----
scrapbook_api = FastAPIRouter(name='scrapbook_api')
scrapbook_api.add_insert_route(
    Scrapbook, path='/photos',
    inputs=[Scrapbook.city, Scrapbook.country, Scrapbook.taken_on, Scrapbook.caption],
    uploadfile_inputs=[Scrapbook.photo],
    outputs=[Scrapbook.id, Scrapbook.place, Scrapbook.shape, Scrapbook.blurb],
)
scrapbook_api.add_update_route(Scrapbook, path='/photos/caption', inputs=[Scrapbook.caption],
                               outputs=[Scrapbook.id, Scrapbook.blurb])
scrapbook_api.add_query_route(path='/photos/by-country', query=photos_in, method='get')
scrapbook_api.add_query_route(path='/photos/thumb', query=latest_thumb, method='get', one_row=True,
                              return_fileresponse=True)
```
