Skip to main content

lakana

Pre-beta. Lakana is published as a 0.x pre-beta: the API and the data layout may change between releases, and it is not ready for production data yet. See the project README.

A typed Python client for the Lakana platform API. lakana never imports lakana-core, lakana-server, or lakana-worker — it only speaks HTTP to a running Lakana server, over the same REST surface any other client would use.

Install

pip install lakana

The only runtime dependencies are httpx and pyarrow.

Credentials

Every request authenticates with a personal access token (PAT) or a task token, sent as a bearer credential. An administrator issues a PAT for your principal:

admin_client.tokens.issue("alice", "alice-laptop")  # -> starts with "lakana_pat_"

Pass the token to Client however is convenient for your environment — see "Configuration" below for the full resolution order.

Quickstart

import lakana
import pyarrow as pa

client = lakana.Client(url="https://lakana.example.com", token="lakana_pat_...")

client.environments.create(
    "dev",
    storage={"scheme": "s3", "root": "s3://my-bucket/dev", "options": {}},
)

schema = pa.schema([pa.field("id", pa.int64(), nullable=False), pa.field("region", pa.string())])
client.tables.create("dev", "bronze.sales", schema=schema)

data = pa.table({"id": [1, 2, 3], "region": ["eu", "us", "eu"]}, schema=schema)
client.tables.append("dev", "bronze.sales", data)

result = client.sql(
    "SELECT region, count(*) AS n FROM bronze.sales GROUP BY region", environment="dev"
)
print(result)

client.sql(...) submits a query, waits for it to finish, and returns the result as a pyarrow.Table in one call; client.queries.submit(...) returns a QueryRun handle for polling or waiting explicitly when you need more control. run.cancel() (or client.queries.cancel(query_id, environment)) cancels a queued query and asks the worker to stop a running one; it returns the QueryStatus after the request. A cancelled query has the status CANCELLED, and run.wait() then raises QueryFailed with cause == "cancelled". client.sql does not cancel when your code is interrupted.

DataFrames: polars by default

Arrow is the contract underneath, and polars is Lakana's default DataFrame tool. The SDK itself only depends on pyarrow (pip install polars separately); results convert zero-copy, and create/append accept polars schemas and DataFrames directly:

import polars as pl

frame = pl.DataFrame({"id": [1, 2, 3], "region": ["eu", "us", "eu"]})
client.tables.create("dev", "bronze.sales_pl", schema=frame.schema)
client.tables.append("dev", "bronze.sales_pl", frame)

df = pl.from_arrow(client.sql("SELECT * FROM bronze.sales_pl", environment="dev"))

polars exports text and bytes as Arrow string_view/binary_view; the SDK sends them as plain string/binary, since the append endpoint refuses view types. pandas works the same way (client.tables.append(..., pandas_df), result.to_pandas()).

Use client as a context manager (with lakana.Client(...) as client:) or call client.close() when done, to release the underlying HTTP connection pool.

Writing results into tables, views, lineage and maintenance

A query's result can go straight into a table instead of back to you. The write runs in a worker, under your own grants, and commits once: readers see the old table or the new one, never a mix.

client.sql("SELECT * FROM bronze.sales", environment="dev", into="silver.sales", mode="create")
client.sql(
    "SELECT region, count(*) AS n FROM silver.sales GROUP BY region",
    environment="dev",
    into="gold.daily",
    mode="replace",  # a full refresh, in one snapshot
)
status = client.sql(
    "SELECT * FROM bronze.events",
    environment="dev",
    into="silver.events",
    mode="append",
    incremental_key="ts",  # appends only rows past the table's max(ts)
)
print(status.rows_written, status.watermark)
  • mode is create, replace, append or view. With into, client.sql returns the finished run's status (rows_written, snapshot_id, watermark, schema_diff), not a table.
  • A result whose schema doesn't match the table fails before anything is written, with a diff of added, dropped, renamed-looking and retyped columns. evolve_schema=True lets the table adopt the result's schema instead. Extra nullable columns are always added.
  • Row filters and column masks on what you read apply to what you write: masked values land masked.
  • Late rows (ts at or below the watermark) are skipped by an incremental append, by design; merge and upsert come after 1.0.
  • An incremental append needs READ on the target, and is refused while a row filter or column mask on the target applies to you.
  • mode="view" stores the SELECT as a view without running it. Views use invoker's rights: reading one needs READ on the view and on every table beneath it, and those tables' row filters and masks still apply. A view's SQL and references are shown by tables.get only if you can READ every table it references; otherwise you get hidden_references (a count). A query expands at most 100 view references. Views are not visible over the Iceberg REST catalog, and can't be dropped through it.

Arrow data has the same two write shapes, and can declare where it came from:

client.tables.replace("dev", "gold.predictions", frame, sources=["gold.features"])
client.tables.append("dev", "gold.predictions", frame, sources=["gold.features"])

A read-modify-write job can make either write conditional on the table not having changed since it read it. Pass the snapshot_id you read (client.tables.get(...).snapshot_id), or "none" for "no snapshot yet":

current = client.tables.get("dev", "gold.predictions").snapshot_id
client.tables.replace("dev", "gold.predictions", frame, expected_snapshot_id=current)

If another writer committed in between, the write raises Conflict and nothing is retried; read again and redo it. On replace, "none" also creates a table that does not exist yet from the upload's schema (you need WRITE_TABLE and CREATE_TABLE). On append, a missing table is still NotFound.

Every write records table-level lineage, which you can walk:

graph = client.tables.lineage("dev", "gold.daily", direction="upstream", depth=3)

Tables you can't read are left out of the graph and counted in graph.hidden_nodes; the walk never goes through them. A graph has at most 500 nodes; graph.truncated is set when a table you can read was left out past that cap.

Maintenance expires old snapshots (older than keep_days, beyond the newest keep_last, never the current one or a branch or tag) and deletes files nothing references any more:

run = client.tables.maintain("dev", "gold.daily", keep_days=7, keep_last=5, dry_run=True)
print(run.wait().maintenance["report"])
  • Files are deleted only once they are at least older_than_hours old (default 72, minimum 1, maximum 87600). Set it longer than your longest external write to the table.
  • Expiry commits only against the metadata it selected from: if the table changed meanwhile, nothing is deleted and the report sets conflict: True (note "table changed during maintenance, retry"); the CLI exits 1. Check report["conflict"], not the note's wording.
  • If the table changes after the expiry but before orphans are deleted, orphan removal is skipped and the report's note says so.
  • If any file the table references lies outside its location, orphan removal is skipped and the report says why.
  • It needs the MAINTAIN_TABLE action, which only the built-in admin role carries.

Configuration

Client(url=None, token=None, *, profile=None, timeout=30.0, verify=True, http_client=None) resolves its URL and token from the first source below that names either one — sources are never merged, so a source that sets one of url/token must set both:

  1. Explicit constructor arguments — url and token.

  2. Environment variables — LAKANA_URL and LAKANA_TOKEN. LAKANA_PROFILE selects a named profile from the profile file if you set it alongside these (otherwise it has no effect at this step).

  3. The profile file — $XDG_CONFIG_HOME/lakana/config.toml, or ~/.config/lakana/config.toml when XDG_CONFIG_HOME is unset:

    default_profile = "dev"
    
    [profiles.dev]
    url = "https://lakana.example.com"
    token = "lakana_pat_..."
    

    profile= (or LAKANA_PROFILE) selects a profile by name, falling back to the file's own default_profile. The file must be readable only by its owner (mode 0600 or stricter) — the SDK refuses it otherwise, telling you the chmod to run. It is read through a single O_NOFOLLOW open, so a symlinked profile file is refused outright as a TOCTOU precaution; a symlinked parent directory is a known, unclosed limitation of that check (see docs/agent/security.md).

If none of these resolves a URL and token, Client(...) raises lakana.ConfigError.

http_client accepts any httpx.Client-compatible object in place of one the SDK builds itself — useful for tests (see lakana-server's own SDK end-to-end tests, which pass a Starlette TestClient this way).

A token is never written into a log line, an exception message, or a repr() — Client and its resolved config both mask everything but the token's key-id prefix, e.g. Client(url='...', token='lakana_pat_<key>_***').

What's covered

client.<resource> exposes every /api/v1 platform route: environments, namespaces, tables (including append, replace, lineage and maintain), queries, principals, groups, tokens, task_tokens, grants, row_filters, column_masks, secrets, repositories, deploys, deployments, project_versions, orphans, artifacts, runs, jobs, checks, and audit_events. A contract test (packages/lakana-server/tests/sdk/test_sdk_contract.py) checks in both directions that this SDK never calls a route the server doesn't serve, and that every platform route has an SDK method — so this list can't silently drift from what the server actually exposes.

The Iceberg REST Catalog routes (/v1/...) are a separate protocol for external Iceberg clients (e.g. pyiceberg) and are out of scope for this SDK.

Errors raise typed exceptions from lakana.errors (AuthenticationError, PermissionDenied, NotFound, Conflict, PayloadTooLarge, ValidationError, ServerError, LakanaConnectionError, QueryFailed, DeployFailed, ConfigError), all subclasses of lakana.LakanaError.

client.info().version is the platform version the instance runs (the same value lakana --version shows).

Command line

pip install lakana also installs the lakana command.

lakana login                       # prompts for URL and token; saves ~/.config/lakana/config.toml (0600)
lakana whoami
lakana --version                     # the CLI's version and the instance's (GET /api/v1/info; one request, no retry, gives up after 3 s in all)
lakana env create dev --storage-root file:///srv/lake
lakana env delete dev --yes        # 409 while a repository is deployed there: `lakana undeploy REPO --env dev` first
lakana table create bronze.sales --column id:long --column 'region:string?'  # quote: shells glob ?
lakana table append bronze.sales sales.csv      # Parquet, CSV or Arrow; a CSV is parsed with the table's types
lakana sql "SELECT region, count(*) FROM bronze.sales GROUP BY region"
lakana sql - --out report.parquet < report.sql
lakana sql "SELECT * FROM bronze.sales" --into silver.sales --mode create
lakana sql "SELECT region, count(*) AS n FROM silver.sales GROUP BY region" --into gold.daily --mode replace
lakana sql "SELECT * FROM bronze.events" --into silver.events --mode append --incremental-key ts
lakana query submit "SELECT ..." | lakana query status ID | lakana query wait ID | lakana query result ID
lakana query cancel ID                              # queued: cancelled at once; running: asked to stop
lakana query list --env dev [--all] [--limit N]     # queued and running; --all adds the last 24 h; no SQL
lakana table replace gold.predictions predictions.parquet --source gold.features
lakana table lineage gold.daily --depth 5          # --downstream for what it feeds; --output json
lakana table maintain gold.daily --dry-run          # --keep-days 7 --keep-last 5 --older-than-hours 72
  • Ctrl-C in lakana sql and lakana query wait cancels the query they wait for, says what happened on stderr and exits with 130 (a second Ctrl-C while the cancel is sent exits at once and names lakana query cancel ID). The submitter and a holder of MANAGE_ENVIRONMENT may cancel; anyone else gets a 404. query wait cancels someone else's query too if you hold MANAGE_ENVIRONMENT. See docs/site/how-to/stop-a-query.md.
  • lakana query list shows your queries (everyone's with MANAGE_ENVIRONMENT): running first, then queued in the order they are taken, with the reason each waits; never the SQL or the error text. SDK: client.queries.list(environment, include_ended=False, limit=100) returns QueryListEntry objects.
  • A query that is QUEUED says why: lakana query status ID shows waiting_for; lakana sql and lakana query wait print Waiting: <reason> on stderr after 3 seconds and again when the reason changes (nothing with --output json); lakana runs show has a waiting_for column for tasks. SDK: status.waiting_for is a WaitingFor (code, message, and the numbers workers, ahead, other_workers, running, limit, each None when the code does not use it) or None, and run.wait(on_update=f) calls f with every polled status. The codes include no_worker_of_kind (workers are running and none takes this kind of work: the operator starts one with LAKANA_WORKER__TAKES=interactive, pipeline or all) and user_limit (you already run as many ad-hoc queries as the limit per user allows: wait, or cancel one of them; running is your own number and a manager reading someone else's row gets limit only). Code that branches on code should treat an unknown one as "waiting". See docs/site/how-to/why-is-it-waiting.md.
  • In a protected environment, writes outside runs need --break-glass --reason "…" (SDK: break_glass_reason=) and MANAGE_ENVIRONMENT.
  • --env picks the environment; without it, the profile's default_environment is used (lakana login --env dev, lakana profile use NAME --env dev) — unless LAKANA_URL/LAKANA_TOKEN supply the credentials, in which case pass --env. There is no built-in default.
  • --output json (or LAKANA_OUTPUT=json) prints machine-readable JSON on stdout; errors go to stderr as {"error": {"type", "message"}} — usage errors included (type UsageError, exit 2), except a malformed global option, which Typer reports before the mode is known.
  • Concurrent lakana login / lakana profile runs are safe: each update of the profile file holds an exclusive lock (config.toml.lock, next to it) for its whole read-modify-write, and gives up after 10 s if another process holds it. Reads never wait.
  • There is no --token option. For CI, use LAKANA_URL/LAKANA_TOKEN, or lakana login --url URL --token-stdin.
  • Destructive commands ask first; --yes skips the prompt, and without a terminal they refuse unless --yes is given.
Exit code Meaning
0 Success
1 Unexpected error (--verbose for a traceback); a deploy or rollback that failed or outlived its --timeout; maintenance not applied because the table changed (retry); a downloaded artifact that did not match its manifest (ArtifactCorrupt, nothing of it is kept: download it again); or an answer from the server that is not the JSON the CLI expects (InvalidResponse: check the profile's URL)
2 Usage error: bad arguments, no environment, confirmation refused
3 Configuration: no credentials, or an unsafe profile file
4 Authentication failed (401)
5 Permission denied (403)
6 Not found (404)
7 Conflict (409)
8 Invalid input (422), or a local file/schema problem
9 Query failed or was cancelled by someone else, or a query's or maintenance run's --timeout elapsed
130 Ctrl-C in lakana sql or lakana query wait; the query was cancelled if the server could be reached
10 Payload too large (413)
11 Server error (5xx), connection failure, or a server too old for the command

Repositories (M5b)

An admin (MANAGE_REPOSITORY) registers each git repository once; deployers name it.

lakana repo add analytics --kind https --url https://github.com/acme/analytics.git \
    --credential-secret github-token --default-ref main
lakana repo add infra --kind ssh --url git@gitlab.internal:data/infra.git \
    --credential-secret deploy-key --known-hosts-file ./known_hosts
lakana repo add demo --kind local --url ~/projects/demo   # under LAKANA_REPO_ROOTS
lakana repo list | show NAME | update NAME [--clear default_ref] | remove NAME --yes

--credential-secret names a secret, resolved in each target environment's scope at deploy time: key token for https, private_key for ssh. Secret values never leave the server. repo remove is refused while any environment has a current deployment from the repository; lakana undeploy removes it first.

Deploying a project (M5b)

lakana init [DIR] scaffolds a project; see docs/site/how-to/projects.md for assets, lakana.yaml, deploys and CI setup.

lakana deploy [REPO] --env dev [--ref main] [--dry-run] [--no-wait]
lakana rollback [REPO] --env dev (--to SHA | --to-previous)
lakana deploys list --env dev | lakana deploys show ID --env dev
lakana deployments --env dev
lakana deployments set-run-as REPO --env prod (--principal P | --clear)
lakana undeploy REPO --env dev [--yes]
lakana project-version show ID --env dev
lakana orphans --env dev

REPO defaults to repo: in the lakana.yaml found by walking up from the current directory to the git root (outside a git repository only the current directory is checked). A deploy is fetched and validated by a worker, never by the CLI; lakana deploy waits and prints the resolved SHA and the diff, exiting 1 if the deploy fails (including losing a race to another deploy) or --timeout elapses (the run keeps going; the message names the id for lakana deploys show). After a successful deploy it prints a hint on stderr naming lakana run [REPO] --env ENV (REPO only when you named it on the command line); there is none for --dry-run, --no-wait, a failed deploy, a rollback or --output json. A rollback never contacts the git host. For a local repository, the CLI warns when the work tree has uncommitted changes: only committed state is deployed. Deploys record a project version; lakana run executes it (see "Running a project").

set-run-as binds the service account a protected environment's runs write as (needs MANAGE_ENVIRONMENT). undeploy removes the deployment pointer and binding (needs DEPLOY, refused while a run is active); tables and project versions stay.

Running a project (M5c1)

lakana run [REPO] --env dev [--select 'gold.*']... [--upstream] [--downstream] [--no-wait] [--timeout S]
lakana runs list --env dev [--repo R] [--limit N] | lakana runs show ID --env dev
lakana runs logs ID --env dev [--task ASSET] [--follow]
lakana runs cancel ID --env dev | lakana runs resume ID --env dev
lakana scheduler status
lakana worker list

lakana run prints the run id (stderr), then a line per task each time its state changes (a.b running attempt 1/3: the attempt in use out of the allowed attempts; no count before the first attempt, and retrying after attempt 1/3 while a failed task waits to try again), and waits. Exit codes: 0 success; 1 failed, cancelled or timed out; 7 a run is already active. --select globs match dotted table names and * crosses namespace dots (see docs/site/how-to/projects.md). SDK: client.runs.*, client.scheduler.status(). lakana worker list (admin) shows the registered workers, alive or lost, the work each takes (takes: all, interactive or pipeline) and what each is doing; SDK: client.workers.list() returns WorkerInfo objects, with WorkerInfo.takes.

Jobs (M5d-b)

lakana job list --env dev [--repo R]
lakana job run NAME --env dev [--repo R] [--no-wait] [--timeout S]
lakana job pause NAME --env dev [--repo R]
lakana job resume NAME --env dev [--repo R]
lakana job notify-test NAME --env dev [--repo R]
lakana job notification DELIVERY_ID --env dev

A job is a named selection in lakana.yaml (jobs:, see docs/site/how-to/projects.md). job list shows, per job, its state (scheduled, paused, waiting, no schedule or no run principal), the schedule with its zone, the next run in the job's zone and in UTC, and the last run with what started it; under the table, the sentence of each job that waits. job run starts the job with the selection from the manifest and follows the run exactly as lakana run does (same output, same exit codes, --no-wait, --timeout). It needs RUN on the environment and works also on a paused job. When the project already has an active run, job run does not fail with 7 as lakana run does: the start waits, begins when the project is free, and the command prints daily waits for run ID to finish; it starts then. (and how to follow that run) and exits 0: that means accepted, not run, so a script that needs the result follows the run with lakana runs show (--output json has waiting instead of run). Only one start waits per job; another start merges into it. The trigger of the run is api when the caller is a service account and manual otherwise. job pause and job resume need DEPLOY on the environment: resume computes the next run from now, so the times missed while paused do not fire; a pause keeps a waiting start. A paused job shows no next run in job list. The state waiting to start means a start waits and no run is active, so the scheduler begins it within seconds; a waiting start is checked again then, and one whose requester was deactivated or lost RUN, or that has no run principal any more, is dropped with an audit event (JOB_START_DROPPED). --repo defaults to lakana.yaml's repo:. lakana runs list|show have job and trigger columns (show also scheduled_for and missed). Exit codes as elsewhere: 5 without the permission, 6 for an unknown job (the message lists the jobs there are), 7 when a protected environment has no run principal, or when you pause a job that has no schedule. SDK: client.jobs.list|start|pause|resume, which return Job and JobStart (run or waiting).

job notify-test queues a test message (clearly fake run data) to the webhook the job's notify: names, then reads the delivery once a second for up to 30 seconds and prints the result: delivered (exit 0); failed: <reason> with the next step, for example "the address is private; allow it with LAKANA_WEBHOOK__ALLOW_HOSTS" (exit 1); or still trying (attempt N of 5); see 'lakana job notification ID --env dev' (exit 1: the result is not known, so a script must not treat it as success; --output json carries the state). It needs DEPLOY on the environment. A job without notify: gives exit 7 ("add or fix it in lakana.yaml and deploy"), an unknown job exit 6. job notification shows one delivery (needs DEPLOY, RUN or READ_ENVIRONMENT). lakana runs show lists the notifications of a run under its tasks, each with its delivery id: delivered, sending (attempt 1 of 5), retrying (attempt 2 of 5); last: ... or failed: the server answered 404. SDK: client.jobs.notify_test(env, repo, job) returns the delivery id, client.jobs.delivery(env, id) a Delivery, and Run.notifications the list (empty from an older server).

Python assets (M5c2a)

SQL transforms, Python at the edges: a Python asset receives its declared inputs as polars DataFrames and returns the DataFrame that replaces its table; see docs/site/how-to/projects.md, "Python assets", for the rules, what a deploy checks and what a task may touch. With output="artifact" it stores a file or a directory (a trained model, say) instead; see "Artifacts (M5c2b)" below. The project also needs a pyproject.toml that depends on lakana[assets] and a committed uv.lock.

# assets/gold/station_scores.py  ->  table gold.station_scores
import polars as pl
from lakana import asset


@asset(inputs={"readings": "bronze.readings", "stations": "bronze.stations"}, retries=2)
def station_scores(readings: pl.DataFrame, stations: pl.DataFrame) -> pl.DataFrame:
    """Temperature spread per station over the last week of readings."""
    recent = readings.filter(pl.col("ts") > pl.col("ts").max() - pl.duration(days=7))
    scored = recent.group_by("station_id").agg(score=pl.col("temp_c").std())
    return scored.join(stations, on="station_id")

The inputs are tables other assets build (here two SQL assets, bronze.readings and bronze.stations). The full walk-through, with those two files, the pyproject.toml and the deploy steps, is in docs/site/how-to/projects.md, "Python assets".

Python assets run as processes of the worker's user, not in a sandbox: one instance is one trust domain, so deploy only code you would run on the worker yourself.

Artifacts (M5c2b)

An artifact is a named file or directory, such as a trained model, stored in numbered versions and governed like a table. An asset declared @asset(output="artifact", keep_versions=5) writes into lakana.output_dir() and returns a pathlib.Path inside it; a consuming asset gets a Path to a read-only copy. Everything is in docs/site/reference/artifacts.md; the SDK is client.artifacts.

lakana artifact list [--namespace NS] | upload NAME PATH | download NAME DIR [--version N]
lakana artifact versions list NAME | versions delete NAME N [--yes]
lakana artifact drop NAME [--yes] | uploads list [NAME] | uploads abort NAME UPLOAD_ID

upload, versions delete and drop take --break-glass --reason "…" in a protected environment. Loading a pickled model runs code: see docs/site/explanation/artifacts.md, "A pickled model is code".

Data quality checks (M5d-a)

A project keeps checks under checks/<namespace>/<table>/<name>.sql: a SELECT that returns the rows that are wrong. A run counts them after it writes the table; a failing blocking check sets the table back and stops what reads it. A guide is docs/site/how-to/check-your-data.md, the rules are in docs/site/reference/checks.md.

lakana check list --env dev [--run RUN_ID] [--table NS.TABLE] [--limit N]
lakana runs show RUN_ID --env dev        # the checks of each task, after the task table
lakana deploy --env dev --dry-run        # lists the checks under "Checks:"

SDK: client.checks.for_run(env, run_id), client.checks.list(env, table=..., cursor=...) and client.checks.iter(env, table=...) return CheckResult objects (a count of rows, never a row). RunTask.checks holds a task's results and RunTask.kind is asset or source_check (a table the project only reads). Deploy.checks lists a project's checks by table. Table.state is published or not_published, with Table.state_message saying why; a read of a table that is not published is refused.

Local mode

pip install "lakana[server]" also installs lakana-server and lakana-worker, so lakana server start --local can run a complete Lakana on this machine: SQLite for the catalog, the local filesystem for storage, no Docker and no Postgres.

pip install "lakana[server]"
lakana server start --local          # runs in the foreground; Ctrl-C stops it
lakana sql "SELECT 42"               # in another terminal, once it's ready
lakana server status                 # exit 0 if running, 1 otherwise

A --kind local repository must lie under one of LAKANA_REPO_ROOTS; local mode defaults that to your home directory (resolved), so lakana repo add demo --kind local --url "$PWD" works anywhere under $HOME and is refused elsewhere (/tmp, another mount); the error says to move the project under your home directory, or, on a server, that the operator sets LAKANA_REPO_ROOTS (it never prints the configured roots). To deploy from another place, start with LAKANA_REPO_ROOTS='["/srv/projects"]' lakana server start --local.

lakana[assets] adds polars for projects with Python assets: python -m lakana.runner, the harness that runs one, needs it. The SDK and CLI work without it.

The server extra pins lakana-server and lakana-worker to lakana's own exact version: a hatch_build.py metadata hook ([tool.hatch.metadata.hooks.custom]) sets optional-dependencies.server from lakana's already-resolved hatch-vcs version at build time, so pip install "lakana[server]" always installs matching versions of all three, never a mix. A dev build off an unreleased commit (lakana==X.Y.Z.devN+gHASH) pins the same local version string, which only resolves against a matching local wheel or an index that carries it, not a plain PyPI release. lakana-server and lakana-worker each carry their own hatch_build.py metadata hook that pins their lakana-core dependency to the same build's version too, so a lakana[server] install always resolves one matching lakana-core rather than whatever the latest release happens to be.

The data directory defaults to $XDG_DATA_HOME/lakana/local (~/.local/share/lakana/local), overridable with --data-dir; --port overrides the default port 8765. If the port is in use, another program or a Lakana instance started with a different --data-dir has it (lakana server status --data-dir DIR checks an instance); pass another --port: start points the local profile at it, so later commands find the instance without --url (add --profile local if another profile is your default). It holds lakana.db (SQLite, WAL mode), lake/ (storage root), master.key (mode 0600) and the running instance's pidfiles. On first start, a local profile is saved automatically (default_environment: dev) and becomes the default profile if none exists yet.

lakana server start --local binds only to 127.0.0.1 — it is never reachable from another machine, whatever --port you pick.

There is no reset command: stop the instance (Ctrl-C, SIGTERM, or — on POSIX — closing the terminal, which sends SIGHUP) and delete the data directory to start completely over, including a fresh master.key and a fresh bootstrap credential on the next start.

If a first run is interrupted after the local profile is saved but before its dev environment is created, the next start notices dev is missing and creates it itself (if that check fails, e.g. the profile's token is stale, start warns and keeps running); to do it by hand instead, lakana env create dev --storage-root file://<data-dir>/lake/dev (not lakana environment create, which isn't a command).

start exit code Meaning
0 Shutdown completed (Ctrl-C/SIGTERM/SIGHUP), whether or not a straggler needed SIGKILL after the 10 s grace period
1 (or the child's own code) Migration failed or the server didn't answer /healthz in time (1); a child exited after startup (its exit code, at least 1)
2 --local is missing, an instance is already running from this data directory, the port is in use, or the data directory/master.key is unsafe (a symlink, another user's, or group/other-accessible)
3 The lakana[server] extra isn't installed, or the data directory/master.key/a pidfile couldn't be prepared, read or written
status exit code Meaning
0 An instance is running
1 No instance is running from this data directory

Team mode: lakana bundle init

For a small team sharing one instance instead of running locally, lakana bundle init generates a Docker Compose bundle — Postgres for the catalog, SeaweedFS for object storage, the server, one or more workers, and a one-shot setup job:

lakana bundle init
cd lakana-deploy && docker compose up -d --wait
lakana sql "SELECT 42"

It writes compose.yaml and .env (mode 0600, holding every generated secret) into ./lakana-deploy by default, and refuses to overwrite an existing bundle rather than risk rotating the secrets master key out from under one. It also saves a profile — named team by default, from the global --profile option — so the CLI can talk to the new instance immediately. By default the server binds to 127.0.0.1 only; --bind 0.0.0.0 (or a specific address) exposes it on the network over plain HTTP, with a warning, and --public-url points the saved profile at a reverse proxy instead.

See docs/site/how-to/deploy-with-compose.md for the full reference: every .env key, exposing it with TLS, scaling workers, upgrading, backups, and revoking the bootstrap credential.

Metadata

Release files for lakana 0.12.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for lakana 0.12.0
File Size Uploaded
lakana-0.12.0.tar.gz 480.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for lakana 0.12.0
File Interpreter ABI Platform
lakana-0.12.0-py3-none-any.whl Python 3 none any Details

Total release size: 762.1 kB

Release files / lakana-0.12.0.tar.gz

Download URL lakana-0.12.0.tar.gz
Size 480.7 kB
Tags Source
SHA-256 checksum
How to use checksums
8985fc58ffe75ae3dfd6b22ccffc998a972d43eec8b9c5cf41fe815baf060e4f
BLAKE2b-256 checksum
How to use checksums
8b69ac35a3536140ba809d38320972b92ecff83c7653a15f4d428d5d71bc876a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release files / lakana-0.12.0-py3-none-any.whl

Download URL lakana-0.12.0-py3-none-any.whl
Size 281.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b3b2ceed8c65723b3381e1028713b2d8cbe4d8f9752c68a26ad4020a73b1883b
BLAKE2b-256 checksum
How to use checksums
cfdbebce2a579ad0421bb879dba849c9aebf8ebf8971fb1e89dd7850e0f6dcf7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.12.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page