Metadata-Version: 2.4
Name: flordb
Version: 4.0.0
Summary: A hindsight logging database for Python developers
Home-page: https://github.com/ucbrise/flor
Author: Rolando Garcia
Author-email: rolando.garcia@asu.edu
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: GitPython
Requires-Dist: cloudpickle
Requires-Dist: astunparse
Requires-Dist: pandas
Requires-Dist: bidict==0.21.3
Requires-Dist: apted
Requires-Dist: matplotlib
Requires-Dist: scikit-learn
Requires-Dist: numpy
Requires-Dist: tqdm
Requires-Dist: sh
Requires-Dist: ipython
Requires-Dist: ipykernel
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# FlorDB: Log-First Context Management for ML Practitioners

[![PyPI](https://img.shields.io/pypi/v/flordb.svg?nocache=1)](https://pypi.org/project/flordb/)


FlorDB brings experiment tracking, provenance, and reproducibility to your ML workflow—using the one thing every engineer already writes: **logs**.

Unlike heavyweight MLOps platforms, FlorDB doesn’t ask you to adopt a new UI, schema, or service. Just import it, log as you normally would, and gain full history, lineage, and replay capabilities across your training runs.

## 🌻 Why FlorDB?

- **Log-Driven Experiment Tracking**  
  No dashboards to configure or schemas to design. `flor.log(...)` writes structured, queryable metadata; `flor.arg(...)` turns a constant into a CLI-settable hyperparameter that is recorded with the run.

- **Zero Code Changes to Start**  
  Already using `print` or `logging`? Import FlorDB and your existing output is captured, versioned, and indexed by loop iteration — no rewrite required.

- **Hindsight Logging & Replay**  
  Missed a metric? Add a log *after the fact* and replay past runs to capture it—no rerunning from scratch.

- **Reproducibility Without Friction**  
  Every run is versioned via Git, every hyperparameter is recorded, and PyTorch checkpoints are captured and addressable by loop iteration—automatically.

- **Works With Your Stack**  
  Makefiles, Airflow, Slurm, HuggingFace, PyTorch—you don’t change your workflow. FlorDB fits in.

## 📦 Installation

```bash
pip install flordb
```

For contributors or bleeding-edge features:

```bash
git clone https://github.com/ucbrise/flor.git
cd flor
pip install -e .
```

---

## 📝 First Log in 30 Seconds

> *Requires a Git repository for automatic versioning.*

```bash
mkdir flor_sandbox
cd flor_sandbox
git init
ipython
```

```python
import flordb as flor
flor.log("message", "Hello ML World!")
```
```
message: Hello ML World!

Run committed successfully.
```

Retrieve logs anytime:

```python
flor.dataframe("message")
```
```
         projid              tstamp filename   source          message
0  flor_sandbox 2025-10-13 18:13:48  ipython  forward  Hello ML World!

```

## 🪵 Already Using `print` and `logging`? Just Import

Add one import to a script you already have. Nothing else changes:

```python
import logging
logging.basicConfig(level=logging.INFO, format="%(message)s")

import flordb as flor          # <-- the only new line

for epoch in range(3):
    print(f"epoch {epoch} | loss: {1.0 / (epoch + 2):.4f}")
    logging.info("checkpoint saved")
```

Your terminal looks exactly the same. But the run is now versioned, committed,
and queryable:

```python
flor.io()
```

```
  projid                     tstamp  filename   source        channel                    line
0  zero2 2026-08-13 12:34:47.951515  train.py  forward     io::stdout  epoch 0 | loss: 0.5000
1  zero2 2026-08-13 12:34:47.951515  train.py  forward  io::log::info        checkpoint saved
2  zero2 2026-08-13 12:34:47.951515  train.py  forward     io::stdout  epoch 1 | loss: 0.3333
3  zero2 2026-08-13 12:34:47.951515  train.py  forward  io::log::info        checkpoint saved
4  zero2 2026-08-13 12:34:47.951515  train.py  forward     io::stdout  epoch 2 | loss: 0.2500
5  zero2 2026-08-13 12:34:47.951515  train.py  forward  io::log::info        checkpoint saved
```

Filter by channel with `flor.io("io::stdout")` or `flor.io("io::log::error")`.

**Then adopt as much as you want.** Every further step buys a specific thing.
Naming the loop is the first one — it tells FlorDB what an iteration is, so
your output lands at the right grain:

```python
for epoch in flor.loop("epoch", range(3)):   # was: for epoch in range(3):
```

```
  projid                     tstamp  filename   source  epoch epoch_value        channel                    line
0  zero2 2026-08-13 12:34:49.944234  train.py  forward      0           0     io::stdout  epoch 0 | loss: 0.5000
1  zero2 2026-08-13 12:34:49.944234  train.py  forward      0           0  io::log::info        checkpoint saved
2  zero2 2026-08-13 12:34:49.944234  train.py  forward      1           1     io::stdout  epoch 1 | loss: 0.3333
3  zero2 2026-08-13 12:34:49.944234  train.py  forward      1           1  io::log::info        checkpoint saved
4  zero2 2026-08-13 12:34:49.944234  train.py  forward      2           2     io::stdout  epoch 2 | loss: 0.2500
5  zero2 2026-08-13 12:34:49.944234  train.py  forward      2           2  io::log::info        checkpoint saved
```

That `epoch` column is what the rest of FlorDB is built on: checkpoints
addressable by iteration, and replay that can jump to one.

Captured text stays out of `flor.dataframe()` — it isn't a metric — but FlorDB
can pull metrics out of it if you ask. See what that would do to *your* logs
before turning it on:

```bash
python -m flordb capture --preview
```

```
Would extract 3 value(s) across 1 metric(s) from 6 captured line(s): loss

  loss                 0.5            <- epoch 0 | loss: 0.5000
  loss                 0.3333         <- epoch 1 | loss: 0.3333
  loss                 0.25           <- epoch 2 | loss: 0.2500

Nothing was written. Enable with flor.set_capture(extract=True) in your script.
```

With `flor.set_capture(extract=True)`, `loss` becomes a real column in
`flor.dataframe("loss")` while the raw line stays on record. It is off by
default because a bad guess would invent a column you didn't ask for.

Capture is tunable and easy to switch off:

```python
flor.set_capture(max_records=50_000)   # per-run ceiling (default 10,000)
flor.set_capture(False)                # or FLOR_CAPTURE=0 in the environment
```

## 🧪 Track Experiments with Zero Overhead

Drop FlorDB into your existing training script:

```python
import flordb as flor

# Hyperparameters
lr = flor.arg("lr", 1e-3)
batch_size = flor.arg("batch_size", 32)

for epoch in flor.loop("epoch", range(epochs)):
    for x, y in flor.loop("step", trainloader):
        ...
        flor.log("loss", loss.item())
    flor.log("val_acc", validate(net))

    # Already in your script? Then you're done: flor mirrors this save into
    # its object store, one snapshot per epoch, and replays from it later.
    torch.save({"model": net.state_dict()}, "ckpt.pth")
```

No `with flor.checkpointing(...):` block is required — an existing `torch.save`
inside a `flor.loop` is enough. Use `flor.checkpointing(model=..., optimizer=...)`
to enroll non-torch objects (scikit-learn estimators, plain dicts), and
`flor.set_ckpt_interval(seconds)` to bound how much disk the snapshots take
(default: at most one every 60s).

**Change hyperparameters from the CLI:**

```bash
python train.py --kwargs lr=5e-4 batch_size=64
```

View metrics across runs:

```python
flor.dataframe("lr", "batch_size", "val_acc")
```

```
        projid                     tstamp  filename   source  epoch epoch_value      lr batch_size val_acc
0  ml_tutorial 2026-08-13 11:27:06.417615  train.py  forward      0           0  0.0005         64      90
1  ml_tutorial 2026-08-13 11:27:06.417615  train.py  forward      1           1  0.0005         64      91
2  ml_tutorial 2026-08-13 11:27:06.417615  train.py  forward      2           2  0.0005         64      92
```

Every `flor.loop` you name becomes a column, so nested metrics land at the right
grain without a join table. `source` distinguishes values observed on the
original run (`forward`) from ones recovered later by replay (`replay`).
Raw SQL is available too:

```python
flor.query("SELECT * FROM logs WHERE value_name = 'val_acc' AND source = 'forward'")
```

## 🔍 Hindsight Logging: Fix It After You See It

Forgot to log gradient norms?

```python
flor.log("grad_norm", ...)
```

Add the logging statement to the script and run:

```bash
python -m flordb replay --apply grad_norm
```

FlorDB estimates the cost, asks for confirmation, then walks the historical
versions: it checks each run's commit out, splices your new statement into that
version of the script, restarts from the nearest checkpoint, and records the
recovered values. Narrow the work when you don't need every iteration:

```bash
python -m flordb replay --apply grad_norm --iter epoch=0,2 --iter step=all
```

Loops you don't mention default to their last iteration. To replay one run
directly (no orchestration, no git checkout), the same verbs are flags on the
script itself:

```bash
python train.py --replay_flor \
    --apply loss,val_acc \
    --iter epoch=0,2 \
    --iter step=all \
    --override device=cpu
```

`--override` exists for environment-shaped settings such as `device` — replaying
on a different machine is fine, but hyperparameters that defined the original
run are rejected, since changing those makes it a new experiment rather than a
replay.

## 📁 What FlorDB Writes

Everything is project-local; nothing lands in your home directory.

```
.flor/
  runs/<tstamp>.jsonl     tracked: one immutable record per forward run
  obj_store/<tstamp>/     ignored: checkpoints, addressable by loop iteration
  <projid>.db             ignored: sqlite query cache, rebuildable at any time
.flor.cmd                 tracked: the run's tstamp and command line
```

Each run makes one `FLOR::Auto-commit::<tstamp>` commit on a shadow branch whose
message body carries the run's hyperparameters — so a run stays reproducible
even if the logs are gone. Lost or moved the cache? Rebuild it from the JSONL:

```bash
python -m flordb unpack
```

### What syncs, and what doesn't

The three things under `.flor/` have very different economics, so flor treats
them differently in `.gitignore` (written on first run):

| | Recomputable? | In git? |
|---|---|---|
| `runs/*.jsonl` | No — the one irreplaceable observation | **Yes**, ~69KB packed per run |
| `obj_store/` | Yes, by replaying | No — ~19MB per run, and checkpoints don't dedup |
| `<projid>.db` | Yes, `flor unpack` in seconds | No |

So run history travels with the code that produced it. A teammate runs
`git fetch && python -m flordb unpack` and has everyone's metrics — no server,
no bucket, no bill.

Checkpoints don't travel, which means the first replay in a fresh clone has to
recompute from iteration 0. It does that automatically, and **shelves the
checkpoints it passes on the way**, so only the first replay of a given run pays
the cost. Two rules keep that from corrupting history: a recomputed checkpoint
never overwrites one the forward run wrote, and warming is disabled under an
`--override` that could move the numbers (`device=cpu`).

Replaying from iteration 0 assumes the script seeds deterministically. If your
script has a resume block (`torch.load("ckpt.pth")` at module scope), flor
neutralizes it for that first replay so it can't load end-of-run weights over
the fresh initialization — your checkpoint file is left where it is, untouched.

## 🏗 Real ML Systems Built on FlorDB

FlorDB powers full AI/ML lifecycle tooling:

- **Feature Stores & Model Registries**
- **Document Parsing & Feedback Loops**
- **Continuous Training Pipelines**

See our [Scan Studio](https://github.com/bwerick/scan_studio) and [Document Parser](https://github.com/rlnsanz/document_parser) examples for real-world integration.


## 📚 Publications

FlorDB is based on research from UC Berkeley’s RISE Lab and Arizona State University.

- *Flow with FlorDB: Incremental Context Maintenance for the Machine Learning Lifecycle* ([CIDR 2025](https://vldb.org/cidrdb/papers/2025/p33-garcia.pdf))  
- *The Management of Context in the ML Lifecycle* ([UCB Tech Report 2024](https://www2.eecs.berkeley.edu/Pubs/TechRpts/2024/EECS-2024-142.html))  
- *Hindsight Logging for Model Training* ([PVLDB 2021](http://www.vldb.org/pvldb/vol14/p682-garcia.pdf))  


## 🛠 License

[Apache v2 License](https://www.apache.org/licenses/LICENSE-2.0) — free to use, modify, and distribute.

## 💡 Get Involved

FlorDB is actively developed. Contributions, issues, and real-world use cases are welcome!

```bash
make test        # full suite, including real forward runs and replays
make test-fast   # unit tests only (~1s)
```

**GitHub:** https://github.com/ucbrise/flor  
**Tutorial Video:** https://youtu.be/mKENSkk3S4Y
