Metadata-Version: 2.4
Name: lazy-budget
Version: 2.1.0
Summary: A local-first personal budgeting application
Author: Oleksandr Zelentsov
Author-email: Oleksandr Zelentsov <saze1997@gmail.com>
License-Expression: MIT
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Requires-Dist: alembic>=1.18.5
Requires-Dist: argon2-cffi>=25.1.0
Requires-Dist: babel>=2.18.0
Requires-Dist: fastapi>=0.141.1
Requires-Dist: pydantic-settings>=2.14.2
Requires-Dist: psycopg[binary]>=3.3.3
Requires-Dist: sqlalchemy>=2.0.51
Requires-Dist: tzdata>=2026.3
Requires-Dist: uvicorn>=0.52.0
Requires-Python: >=3.14
Project-URL: Homepage, https://gitlab.com/oleksandr.zelentsov/lazy-finance
Project-URL: Repository, https://gitlab.com/oleksandr.zelentsov/lazy-finance
Description-Content-Type: text/markdown

# Full Budget

[![PyPI version](https://badge.fury.io/py/lazy-budget.svg)](https://badge.fury.io/py/lazy-budget)

Full Budget is a local-first personal finance application with a database-backed
CLI and web interface. It tracks accounts, immutable ledger transactions,
transfers, budgets, spending funds, savings goals, recurring plans, forecasts, and
budget pacing.
SQLite and PostgreSQL use the same accounting services and Alembic history.

Version 2.0 replaces the legacy YAML/CSV CLI. The distribution remains
`lazy-budget` and the executable remains `lbud`, but commands now operate on a
SQLite or PostgreSQL database. Legacy plotting and file-editing commands were
removed.

Python 3.14 or newer is required.

## Install

Install the published application as an isolated tool:

```bash
uv tool install lazy-budget
lbud --version
```

Upgrade an existing installation:

```bash
uv tool upgrade lazy-budget
```

Install the current checkout for development:

```bash
uv sync
uv run lbud --version
```

## Direct Setup

### SQLite

Copy the public settings, choose a timezone, and keep the default SQLite URL:

```bash
cp .env.example .env
```

Relevant local settings:

```dotenv
FULL_BUDGET_DATABASE_URL=sqlite:///~/.local/share/full-budget/full-budget.sqlite3
FULL_BUDGET_TIMEZONE=Europe/Warsaw
FULL_BUDGET_HOST=127.0.0.1
FULL_BUDGET_PORT=8000
FULL_BUDGET_LOG_LEVEL=INFO
FULL_BUDGET_ACCESS_LOG=true
FULL_BUDGET_PUBLIC_ORIGIN=http://127.0.0.1:8000
FULL_BUDGET_TRUSTED_HOSTS=127.0.0.1,localhost,[::1]
FULL_BUDGET_SECURE_COOKIE=false
FULL_BUDGET_COOKIE_NAME=full_budget_session
FULL_BUDGET_ENABLE_DOCS=true
```

Initialize the schema and sole administrator, then run the web application:

```bash
lbud database migrate
lbud admin bootstrap
lbud web
```

When running from a checkout, prefix commands with `uv run`. Open
`http://127.0.0.1:8000`.

SQLite databases are backed up automatically before an online schema upgrade.
Create an additional physical backup with:

```bash
lbud database backup
```

### PostgreSQL Without Docker

Create an empty PostgreSQL database and set a SQLAlchemy URL using the Psycopg 3
driver:

```dotenv
FULL_BUDGET_DATABASE_URL=postgresql+psycopg://full_budget:password@127.0.0.1/full_budget
```

Then use the same application workflow:

```bash
lbud database migrate
lbud admin bootstrap
lbud web
```

Use PostgreSQL-native dumps or snapshots for backups. The `lbud database backup`
command intentionally supports only file-backed SQLite databases.

## CLI

Global options must appear before the command:

```text
lbud [--profile NAME] [--env-file PATH] [--database-url URL]
     [--timezone ZONE] [--format table|json] [--no-color] [--yes]
     COMMAND ...
```

Examples:

```bash
lbud --profile local --format json account list
lbud --profile production --yes transaction delete 42
```

Money values are decimal strings and JSON output keeps them as strings. Dates use
`YYYY-MM-DD`; instants accept ISO 8601 values. Destructive commands identify the
resolved profile and require interactive confirmation unless global `--yes` is
provided.

### Command Groups

| Group | Commands |
| --- | --- |
| `dashboard` | Show balances and the active budget summary |
| `account` | `create`, `list`, `show`, `balance`, `history`, `rename`, `archive`, `unarchive` |
| `transaction` | `add`, `list`, `show`, `activity`, `breakdown`, `categories`, `delete` |
| `transfer` | `add`, `list`, `show`, `delete` |
| `budget` | `create`, `list`, `show`, `update`, `stats`, `delete`, account membership and target management |
| `fund` | `create`, `list`, `show`, `fund`, `spend`, `return`, `release`, `close`, `reopen`, `unlink` |
| `plan` | `create`, `list`, `update`, `archive`, `unarchive`, `forecast`, `delete` |
| `occurrence` | `list`, `post`, `skip` |
| `savings-goal` | `create`, `list`, `show`, `update`, `archive`, `unarchive` |
| `database` | `status`, `migrate`, `backup`, `export`, `import` |
| `profile` | `create`, `list`, `show`, `delete` |
| `admin` | `bootstrap`, `reset` |
| `web` | Run the web application |

`add`, `ls`, `cats`, and `bd` are short aliases for transaction entry, the
dashboard, category listing, and active-budget burndown data.

Use `lbud COMMAND --help` and `lbud COMMAND SUBCOMMAND --help` for every argument.

### Common Workflow

Create accounts and record signed activity:

```bash
lbud account create Checking PLN --opening-balance 2500.00 --opened-on 2026-08-01
lbud account create Savings PLN --opening-balance 500.00 --opened-on 2026-08-01
lbud transaction add -18.50 --account 1 --category Food --description Lunch
lbud transaction add 4000.00 --account 1 --category Salary --description Payroll
lbud transfer add 300.00 --source 1 --destination 2 --description "Monthly saving"
lbud transaction activity
```

Create and inspect a budget:

```bash
lbud budget create August PLN 2026-08-01 2026-08-31 \
  --account 1 --available 2000.00
lbud budget create "From payday" PLN 2026-08-31 \
  --opening-at 2026-08-01T14:30:00+02:00 --account 1 --available 2000.00
lbud budget stats 1 --as-of 2026-08-15
lbud bd 1 --as-of 2026-08-15
```

The legacy `START_DATE END_DATE` form opens at local midnight. `--opening-at` accepts
an ISO date-time and takes only `END_DATE`; omitted opening values in the API default
to the current instant. Budget end dates and statistics `--as-of` values remain
inclusive, date-only controls.

Create a savings goal and recurring expense, then explicitly post an occurrence:

```bash
lbud savings-goal create "Emergency fund" PLN --target 12000.00
lbud plan create expense Rent 1800.00 2026-08-01 \
  --account 1 --recurrence monthly --budget-treatment committed
lbud occurrence list --through 2026-09-01
lbud occurrence post 1 --amount 1800.00
```

Plans are forecasts until an occurrence is explicitly posted. Posting creates the
authoritative ledger transaction atomically.

Use a spending fund when an expense is paid from savings rather than ordinary
day-to-day money:

```bash
lbud fund create 1 Conference
lbud fund fund 1 1 500.00 --source 2 --destination 1
lbud fund spend 1 1 -320.00 --account 1 --category Travel
lbud fund return 1 1 180.00 --source 1 --destination 2
lbud --yes fund close 1 1
```

Funding and returns are atomic ledger transfers. Funded expense portions are reported
separately and do not reduce ordinary budget pacing. `fund release` instead makes
unused reserve available as ordinary budget money without creating a ledger transfer.
Unlinking keeps ledger transactions but removes their fund classification.

### Profiles

Profiles are permission-restricted environment files under
`~/.config/full-budget/profiles`. They can hold database, timezone, server, and
security settings:

```bash
lbud profile create local \
  --database-path ~/.local/share/full-budget/local.sqlite3 \
  --timezone Europe/Warsaw
lbud profile create server \
  --database-url postgresql+psycopg://full_budget:password@db/full_budget \
  --timezone UTC
lbud profile list
lbud profile show server
lbud --profile local account list
```

`profile show` redacts database passwords. Profile switching happens between
processes; the application does not hot-swap database engines.

Configuration precedence, highest first, is:

1. Explicit CLI overrides.
2. Process environment.
3. `--env-file` or `FULL_BUDGET_ENV_FILE`.
4. `--profile` or `FULL_BUDGET_PROFILE`.
5. Working-directory `.env`.
6. `~/.config/full-budget/.env`.
7. Built-in defaults.

### Export And Import

Portable exports include authoritative financial records and immutable UUIDs,
but exclude administrator credentials, sessions, database URLs, environment
values, derived balances, and Alembic internals. Format version 2 preserves spending
fund definitions, transaction links, releases, and closed state; historical version 1
exports remain importable but cannot contain those relationships.

```bash
lbud database export full-budget-export.json
lbud database import --dry-run full-budget-export.json
lbud --yes database import full-budget-export.json
```

Merge imports are atomic. New portable IDs are inserted, identical records are
unchanged, and conflicting records abort the entire import.

### Upgrade A Pre-2.0 SQLite Database To PostgreSQL

This applies to an earlier SQL/Alembic `full_budget` database, not the removed
legacy YAML/CSV application.

1. Stop writers and create a consistent SQLite copy.
2. Run 2.0 migrations against that copy; the command creates another backup.
3. Verify accounts and budgets.
4. Export portable data.
5. Start an empty PostgreSQL database and dry-run the import.
6. Import, verify, and retain all migration artifacts until restore is rehearsed.

```bash
sqlite3 /path/to/original.sqlite3 \
  ".backup '/path/to/migration.sqlite3'"

lbud --database-url sqlite:////path/to/migration.sqlite3 database migrate
lbud --database-url sqlite:////path/to/migration.sqlite3 account list --all
lbud --database-url sqlite:////path/to/migration.sqlite3 budget list
lbud --database-url sqlite:////path/to/migration.sqlite3 \
  database export /path/to/full-budget-export.json
```

Migration `0010` assigns portable UUIDs. Preserve the migrated copy: repeatedly
migrating fresh copies of the same pre-0010 database creates different UUIDs.

## Docker Compose

The Compose stack builds a non-root Python 3.14 application image and uses the
official digest-pinned PostgreSQL 18 Alpine image. PostgreSQL is private to the
Compose network. Only the application is published, on host loopback by default.

### Local Docker

```bash
cp .env.example .env
```

Replace `POSTGRES_PASSWORD` with a long URL-safe random value. Ensure the browser
origin exactly matches `FULL_BUDGET_DOCKER_PUBLIC_ORIGIN`. Also replace
`FULL_BUDGET_POSTGRES_PASSWORD` with a different random value; the preparation
service creates a non-superuser application role. `localhost` and `127.0.0.1` are
different origins.

```bash
docker compose build --pull
docker compose up -d db
docker compose run --rm app lbud admin bootstrap
docker compose up -d
docker compose ps
```

Migrations run automatically at application startup. The default endpoint is
`http://127.0.0.1:8000`. PostgreSQL data persists in the
`full-budget_postgres_data` named volume across restarts, image updates,
container recreation, and `docker compose down`.

Application and access records are labeled `APP` and `ACCESS`. Successful
`/api/v1/health` requests are omitted from access logs, while failed health checks
remain visible. Compose rotates application and database container logs at `10m`
with three files by default.

Do not run `docker compose down --volumes` unless you intend to permanently remove
the PostgreSQL data.

Common operations:

```bash
docker compose logs -f app db
docker compose up -d --build
docker compose down
```

Import an export without mounting it into the container:

```bash
docker compose run --rm -T app \
  lbud --format json database import --dry-run - \
  < /path/to/full-budget-export.json
docker compose run --rm -T app \
  lbud --yes --format json database import - \
  < /path/to/full-budget-export.json
```

### Public HTTPS Deployment

Keep `FULL_BUDGET_BIND_ADDRESS=127.0.0.1` and terminate HTTPS at an existing NGINX
server. Configure production security before starting the app:

```dotenv
FULL_BUDGET_DOCKER_PUBLIC_ORIGIN=https://finance.example.com
FULL_BUDGET_DOCKER_TRUSTED_HOSTS=finance.example.com
FULL_BUDGET_DOCKER_SECURE_COOKIE=true
FULL_BUDGET_DOCKER_COOKIE_NAME=__Host-full_budget_session
FULL_BUDGET_ENABLE_DOCS=false
```

The application rejects insecure cookie settings for a non-loopback origin and
rejects API documentation with secure cookies.

Example NGINX configuration:

```nginx
limit_req_zone $binary_remote_addr zone=full_budget_login:10m rate=5r/m;

server {
    listen 443 ssl http2;
    server_name finance.example.com;

    location = /api/v1/auth/login {
        limit_req zone=full_budget_login burst=3 nodelay;
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
```

NGINX terminates TLS. The application deliberately does not derive security
decisions from forwarded headers: it validates the browser `Origin` and `Host`
against explicit settings and independently sets secure cookies.

Before enabling public DNS:

- Bootstrap the administrator offline.
- Confirm `/docs`, `/redoc`, and `/openapi.json` return `404`.
- Confirm the cookie uses `__Host-full_budget_session`, `Secure`, `HttpOnly`,
  `SameSite=Lax`, and `Path=/`.
- Run the PostgreSQL test and rehearse export/import and backup/restore.
- Keep the application port bound only to host loopback.

### PostgreSQL Backup And Restore

Create a dump and copy it off the Docker host:

```bash
docker compose exec -T db \
  sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \
  > full-budget.sql
```

Restore into an empty database:

```bash
docker compose exec -T db \
  sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \
  < full-budget.sql
```

## Environment Variables

Boolean values accept common forms such as `true` and `false`. Path values expand
`~`. `FULL_BUDGET_DATABASE_URL` takes precedence over the path fallback.

### Application

| Variable | Default | Description |
| --- | --- | --- |
| `FULL_BUDGET_DATABASE_URL` | unset | Preferred SQLAlchemy URL. Supports `sqlite` and `postgresql+psycopg`. |
| `FULL_BUDGET_DATABASE_PATH` | `~/.local/share/full-budget/full-budget.sqlite3` | SQLite fallback used when the URL is unset. |
| `FULL_BUDGET_TIMEZONE` | `UTC` | IANA timezone used for local dates and recurrence boundaries. |
| `FULL_BUDGET_HOST` | `127.0.0.1` | Server bind host. Non-loopback hosting only accepts `0.0.0.0` with the explicit gate. |
| `FULL_BUDGET_PORT` | `8000` | Server port; in Compose this is also the host-published port. |
| `FULL_BUDGET_RELOAD` | `false` | Uvicorn reload setting for direct server runs. |
| `FULL_BUDGET_LOG_LEVEL` | `INFO` | Application and Uvicorn log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, or `CRITICAL`. |
| `FULL_BUDGET_ACCESS_LOG` | `true` | Enable HTTP access logs. Successful health-check access records are suppressed. |
| `FULL_BUDGET_ALLOW_NON_LOOPBACK` | `false` | Must be `true` with `FULL_BUDGET_HOST=0.0.0.0`. |
| `FULL_BUDGET_PUBLIC_ORIGIN` | `http://127.0.0.1:8000` | Exact browser origin: scheme, hostname, and optional port, without path or trailing slash. |
| `FULL_BUDGET_TRUSTED_HOSTS` | `127.0.0.1,localhost,[::1],testserver` | Comma-separated accepted `Host` values. Wildcards are rejected. |
| `FULL_BUDGET_SECURE_COOKIE` | `false` | Require HTTPS and the secure cookie name. Insecure cookies are loopback-only. |
| `FULL_BUDGET_COOKIE_NAME` | `full_budget_session` | Session cookie name. Secure mode requires `__Host-full_budget_session`. |
| `FULL_BUDGET_SESSION_IDLE_SECONDS` | `1800` | Sliding idle expiry, from 300 to 86400 seconds. |
| `FULL_BUDGET_SESSION_ABSOLUTE_SECONDS` | `86400` | Absolute expiry, from 900 to 2592000 seconds; cannot be shorter than idle expiry. |
| `FULL_BUDGET_ENABLE_DOCS` | `false` | Enable `/docs`, `/redoc`, and `/openapi.json`. Rejected with secure cookies. |
| `FULL_BUDGET_PROFILE` | unset | Select a named profile when `--profile` is absent. |
| `FULL_BUDGET_ENV_FILE` | unset | Load an additional explicit env file when `--env-file` is absent. |

### Docker Compose

Compose maps these values into application settings where noted. The application
ignores Docker-only names when run directly.

| Variable | Default | Description |
| --- | --- | --- |
| `POSTGRES_PASSWORD` | required | PostgreSQL password. Keep it URL-safe because Compose interpolates it into the application URL. |
| `POSTGRES_DB` | `full_budget` | PostgreSQL database name. |
| `POSTGRES_USER` | `full_budget` | PostgreSQL role name. |
| `FULL_BUDGET_POSTGRES_USER` | `full_budget_app` | Non-superuser role used by the web application and migrations. |
| `FULL_BUDGET_POSTGRES_PASSWORD` | required | Password for the application role; use a value different from `POSTGRES_PASSWORD`. |
| `FULL_BUDGET_BIND_ADDRESS` | `127.0.0.1` | Host address that publishes the app container port. Keep loopback in production. |
| `FULL_BUDGET_IMAGE` | `full-budget:local` | Built application image name/tag. |
| `FULL_BUDGET_DOCKER_LOG_MAX_SIZE` | `10m` | Maximum Docker `json-file` log size before rotation for app and database containers. |
| `FULL_BUDGET_DOCKER_LOG_MAX_FILE` | `3` | Number of rotated Docker log files retained for app and database containers. |
| `FULL_BUDGET_DOCKER_PUBLIC_ORIGIN` | `http://127.0.0.1:8000` | Becomes `FULL_BUDGET_PUBLIC_ORIGIN` inside the app container. |
| `FULL_BUDGET_DOCKER_TRUSTED_HOSTS` | `127.0.0.1,localhost` | Becomes the container trusted-host list; health-check loopback hosts are appended. |
| `FULL_BUDGET_DOCKER_SECURE_COOKIE` | `false` | Becomes `FULL_BUDGET_SECURE_COOKIE` inside the app container. |
| `FULL_BUDGET_DOCKER_COOKIE_NAME` | `full_budget_session` | Becomes `FULL_BUDGET_COOKIE_NAME` inside the app container. |

Compose also passes `FULL_BUDGET_TIMEZONE`, `FULL_BUDGET_PORT`,
`FULL_BUDGET_LOG_LEVEL`, `FULL_BUDGET_ACCESS_LOG`,
`FULL_BUDGET_SESSION_IDLE_SECONDS`, `FULL_BUDGET_SESSION_ABSOLUTE_SECONDS`, and
`FULL_BUDGET_ENABLE_DOCS`. It overrides the container database URL, host,
non-loopback gate, and reload mode for the private container network.

## Authentication And Security

- There is exactly one administrator per database.
- Bootstrap and password recovery are offline CLI operations; no registration API
  exists.
- Passwords use Argon2id.
- Browser sessions are opaque, stored server-side by token hash, and have idle and
  absolute expiry.
- Unsafe API methods require an exact configured origin and per-session CSRF token.
- Login attempts are throttled and production API documentation is disabled.
- Password change and `lbud admin reset` revoke existing sessions.

Savings-goal rename, lifetime-target update, and deletion are not currently
implemented. Administrative session listing and selective revocation are also not
available; offline password reset revokes all sessions.

## Development

```bash
uv sync
uv run ruff format --check .
uv run ruff check .
uv run pytest
uv build --clear
```

Run the optional PostgreSQL smoke test with:

```bash
FULL_BUDGET_TEST_POSTGRES_URL=postgresql+psycopg://user:password@host/database \
  uv run pytest tests_web/test_postgresql.py
```

Accounting rules are documented in `specs/ACCOUNTING_PLAN.md`; architecture and
2.0 milestones are in `specs/WEB_APP_PLAN.md` and `specs/ROADMAP_V2.md`.
