Metadata-Version: 2.5
Name: flatland-client
Version: 0.2.0
Summary: Durable, dependency-free Python client for the hosted Flatland engine. Your model is a file you own; your compile runs on Flatland's servers.
Project-URL: Homepage, https://flatlandfi.com
Project-URL: Repository, https://github.com/Flatlandfi/flatland
Author-email: Flatland <info@flatlandfi.com>
License-Expression: MIT
License-File: LICENSE
Keywords: decision-modeling,financial-modeling,flatland,fpa,local-first
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Office/Business :: Financial
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# flatland-client

The durable, dependency-free Python client for the hosted [Flatland](https://flatlandfi.com) engine.

**Your model is a file you own.** It lives at `~/.flatland/models/<name>.flatland.json`
on *your* machine — versioned in your own git, diffable, re-runnable anywhere.
Compute is a round trip to the hosted `/api/v2/*` surface: the client ships the
model IR up, **your compile runs on Flatland's servers**, and the result comes
back.

This package is `pip install`-light: stdlib `urllib` only, no engine, no
fastapi/uvicorn/stripe. It is the supported durable, metered hosted path for
Python users.

## Install

```bash
pip install flatland-client
```

## Quickstart

```python
from flatland_client import create, load, http_transport

t = http_transport(api_key="fl_live_...")        # HTTPS to api.flatlandfi.com

h = create("acme_plan", t)                         # writes ~/.flatland/models/acme_plan.flatland.json
h.bulk_add([
    {"name": "revenue", "type": "Currency", "value": 1000},
    {"name": "cost", "type": "Currency", "value": 400},
    {"name": "profit", "type": "Currency", "formula": "revenue - cost"},
])
print(h.compile()["values"]["profit"]["value"])    # metered compute on the hosted engine

h.export_excel("acme_plan.xlsx")                    # .xlsx with LIVE Excel formulas
h.export_excel("downside.xlsx", scenario="downside")

# --- new session / a new machine that has the file ---
h = load("acme_plan", t)                            # durable across restart
```

## Surface

Compute: `compile`, `sensitivity`, `solve`, `diff`, `explain`, `impact_preview`,
`export_excel`. Mutations (each persists the new IR locally): `add_driver`,
`add_computed`, `add_alias`, `bulk_add`, `update_driver`, `remove_driver`,
`create_scenario`. Lifecycle: `create`, `load`, `save`, `ir`.

`solve`, `add_alias` and `export_excel` landed in 0.2.0 — before that this client
trailed the npm `flatland-client`, and the Excel deliverable named on every
Flatland tier was not reachable from it at all.

## Durability + safety contract

- **Atomic writes.** Every mutation persists the new IR with a tmp + `os.replace`
  rename, so a crash never leaves a half-written model file.
- **Fail-loud (F2).** A failed mutation (a `5xx`, or a rejected `4xx`) raises and
  **leaves the local file untouched** — a failure is never silently mistaken for
  success.
- **No server fallback.** `load()` of a missing file raises. There is no server
  copy to fall back to: every call ships the IR up from your disk, so if you lose
  the file you lose the model. Back up `~/.flatland/models/`.
- **Envelope versioning.** The file is `{ "flatland_ir_version": 1, "ir": {...} }`.
  A file written by an older client keeps loading; a too-new envelope is refused
  before it is persisted.
- **Path-traversal-safe** model names.

Get a key at https://flatlandfi.com/install.

## Error tracking (opt-out Sentry, dependency-free)

The client can report uncaught exceptions to [Sentry](https://sentry.io) with a
**stdlib-only envelope sender** — a `urllib.request` POST to the Sentry ingest
API. There is no Sentry SDK dependency; the package stays `dependencies = []`.

```python
import flatland_client

flatland_client.configure_sentry()  # arms capture when SENTRY_DSN is set
```

- **`SENTRY_DSN`** — set to enable capture. Unset, blank, or malformed →
  `configure_sentry()` is a no-op returning `False`: fail-open, no sender is
  armed, and no network request is attempted.
- **`SENTRY_DISABLED=1`** — kill-switch, checked before any DSN handling. Only
  the exact value `1` disables; the default is **ON** (opt-out, not opt-in).
- **`FLATLAND_ENV`** / **`FLATLAND_RELEASE`** — stamped on every event as
  `environment` (default `production`) and `release` (when set).

**Privacy posture.** Every event is scrubbed before it leaves your process with
the same `redact_pii()` rules the client's error boundary uses: engine-IP keys
(formulas, bindings, ASTs, eval order, …) are dropped, sensitive-named keys
(`api_key`, `token`, `password`, `email`, …) are replaced with `[REDACTED]`,
and secret/PII value patterns (`fl_live_…`, `Bearer …`, emails, …) are masked
in every string. Stack-frame locals are never captured. And the capture path
itself is fail-open: an unreachable ingest host or an ingest 500 changes
nothing about how your program runs or fails.

## Structured logging (stdlib-only JSON)

The client's loggers (`flatland_client`, and `flatland_client.analytics` for
the GA4 side-channel) are plain stdlib loggers with no handlers attached —
your application owns logging configuration. Call `configure_logging()` to
render every record as a single-line JSON object on stderr:

```python
import flatland_client

flatland_client.configure_logging()  # one JSON line per record on stderr
```

Each line carries `service` (`"flatland-client-py"`), `level`, an ISO-8601
`timestamp`, the `logger` name, the `message`, and any extra fields (the
transport logs a per-request `request_id`, so one id greps across client and
server logs). The rendered payload passes through the same `redact_pii()`
rules as the error boundary and the Sentry sender: sensitive-named keys
(`api_key`, `token`, `password`, `email`, …) are replaced with `[REDACTED]`,
and secret/PII value patterns (`fl_live_…`, `Bearer …`, emails, …) are masked
in every string.

## Interactive QA (dev run)

The client is a stdlib-only Python library (`urllib`, no engine, no
fastapi/uvicorn). You can poke it locally with **no private credentials** — the
import smoke and the test suite run without an API key (they never call the
hosted engine).

```bash
# 1. Install editable from the repo root:
pip install -e packages/flatland-client-py

# 2. Credential-free import smoke:
python -c "from flatland_client import create, load, http_transport; print('flatland_client ok')"
#   -> flatland_client ok
```

Operations that compile/export (`h.compile()`, `h.export_excel()`) need a
Flatland API key — those are private credentials and are NOT required for the QA
smoke above.

## Test

```bash
pytest packages/flatland-client-py/tests     # (run from the repo root; it is in the root testpaths)
```
