Files and a poller

Do the Tigris setup first.

Gateways
CSV / Parquet
PUT
Landing bucket
every minute
Poller
table.append
commit
Lake bucket
Iceberg tables

Gateways upload files to the landing bucket with any S3 client, using a key that is unique per file, such as gateway-7/20260105T1000.csv. Every minute a script lists the landing bucket, appends each file, and deletes it. Nothing needs to be reachable from the internet.

ingest_landing.py:

"""Appends every CSV or Parquet file in the landing bucket to a Timelake table, then deletes it."""
 
import io
import os
 
import boto3
import pyarrow.csv as csv
import pyarrow.parquet as pq
import timelake
 
BUCKET = os.environ["LANDING_BUCKET"]
s3 = boto3.client("s3")  # endpoint and region from AWS_ENDPOINT_URL_S3 and AWS_REGION
table = timelake.Lake(os.environ["TIMELAKE_WAREHOUSE"]).table(os.environ["TIMELAKE_TABLE"])
 
 
def read(name, body):
    return pq.read_table(io.BytesIO(body)) if name.endswith(".parquet") else csv.read_csv(io.BytesIO(body))
 
 
for page in s3.get_paginator("list_objects_v2").paginate(Bucket=BUCKET):
    for obj in page.get("Contents", []):
        key = obj["Key"]
        body = s3.get_object(Bucket=BUCKET, Key=key)["Body"].read()
        # The object key is the batch id: if the script stops before the delete, the next run skips the file.
        print(key, table.append(read(key, body), batch_id=key))
        s3.delete_object(Bucket=BUCKET, Key=key)

Add the rest of its settings to timelake.env, so cron has them too:

AWS_ENDPOINT=https://t3.storage.dev
TIMELAKE_WAREHOUSE=s3://my-lake/warehouse
TIMELAKE_TABLE=iot.readings
LANDING_BUCKET=my-lake-landing

Run it by hand once, then from cron on any host with Python:

set -a; . ./timelake.env; set +a
uv run --with timelake --with boto3 ingest_landing.py
* * * * * cd $HOME/timelake && set -a && . ./timelake.env && set +a && $HOME/.local/bin/uv run --with timelake --with boto3 ingest_landing.py >> ingest.log 2>&1

Run one poller per table. It appends files one after another, so its commits never wait for each other. A file uploaded again with a key that was already loaded is skipped, so send corrections as new files.