lakana 0.12.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. lakana-0.12.0/.gitignore +25 -0
  2. lakana-0.12.0/PKG-INFO +615 -0
  3. lakana-0.12.0/README.md +579 -0
  4. lakana-0.12.0/hatch_build.py +37 -0
  5. lakana-0.12.0/pyproject.toml +52 -0
  6. lakana-0.12.0/src/lakana/__init__.py +48 -0
  7. lakana-0.12.0/src/lakana/_arrow.py +265 -0
  8. lakana-0.12.0/src/lakana/_artifact_manifest.py +886 -0
  9. lakana-0.12.0/src/lakana/_artifact_tree.py +1105 -0
  10. lakana-0.12.0/src/lakana/_config.py +262 -0
  11. lakana-0.12.0/src/lakana/_http.py +426 -0
  12. lakana-0.12.0/src/lakana/_models.py +1844 -0
  13. lakana-0.12.0/src/lakana/_routes.py +178 -0
  14. lakana-0.12.0/src/lakana/_tokens.py +43 -0
  15. lakana-0.12.0/src/lakana/_version.py +21 -0
  16. lakana-0.12.0/src/lakana/assets.py +292 -0
  17. lakana-0.12.0/src/lakana/cli/__init__.py +8 -0
  18. lakana-0.12.0/src/lakana/cli/_break_glass.py +57 -0
  19. lakana-0.12.0/src/lakana/cli/_commands.py +127 -0
  20. lakana-0.12.0/src/lakana/cli/_filelock.py +192 -0
  21. lakana-0.12.0/src/lakana/cli/_files.py +488 -0
  22. lakana-0.12.0/src/lakana/cli/_output.py +510 -0
  23. lakana-0.12.0/src/lakana/cli/_profiles.py +435 -0
  24. lakana-0.12.0/src/lakana/cli/_project.py +183 -0
  25. lakana-0.12.0/src/lakana/cli/_state.py +352 -0
  26. lakana-0.12.0/src/lakana/cli/app.py +456 -0
  27. lakana-0.12.0/src/lakana/cli/artifacts.py +727 -0
  28. lakana-0.12.0/src/lakana/cli/audit.py +148 -0
  29. lakana-0.12.0/src/lakana/cli/auth.py +209 -0
  30. lakana-0.12.0/src/lakana/cli/checks.py +178 -0
  31. lakana-0.12.0/src/lakana/cli/deploy/__init__.py +12 -0
  32. lakana-0.12.0/src/lakana/cli/deploy/_cli.py +277 -0
  33. lakana-0.12.0/src/lakana/cli/deploy/_init.py +314 -0
  34. lakana-0.12.0/src/lakana/cli/deploy/compose.yaml +193 -0
  35. lakana-0.12.0/src/lakana/cli/deploy/setup.py +236 -0
  36. lakana-0.12.0/src/lakana/cli/deploys.py +636 -0
  37. lakana-0.12.0/src/lakana/cli/environments.py +220 -0
  38. lakana-0.12.0/src/lakana/cli/grants.py +282 -0
  39. lakana-0.12.0/src/lakana/cli/groups.py +126 -0
  40. lakana-0.12.0/src/lakana/cli/indexes.py +194 -0
  41. lakana-0.12.0/src/lakana/cli/jobs.py +323 -0
  42. lakana-0.12.0/src/lakana/cli/local/__init__.py +1 -0
  43. lakana-0.12.0/src/lakana/cli/local/_datadir.py +398 -0
  44. lakana-0.12.0/src/lakana/cli/local/_pidfile.py +118 -0
  45. lakana-0.12.0/src/lakana/cli/local/_scripts.py +100 -0
  46. lakana-0.12.0/src/lakana/cli/local/_supervisor.py +545 -0
  47. lakana-0.12.0/src/lakana/cli/namespaces.py +139 -0
  48. lakana-0.12.0/src/lakana/cli/policies.py +246 -0
  49. lakana-0.12.0/src/lakana/cli/principals.py +107 -0
  50. lakana-0.12.0/src/lakana/cli/profile.py +86 -0
  51. lakana-0.12.0/src/lakana/cli/project_init.py +384 -0
  52. lakana-0.12.0/src/lakana/cli/queries.py +933 -0
  53. lakana-0.12.0/src/lakana/cli/repos.py +231 -0
  54. lakana-0.12.0/src/lakana/cli/runs.py +947 -0
  55. lakana-0.12.0/src/lakana/cli/secrets.py +187 -0
  56. lakana-0.12.0/src/lakana/cli/server.py +916 -0
  57. lakana-0.12.0/src/lakana/cli/tables.py +711 -0
  58. lakana-0.12.0/src/lakana/cli/tokens.py +152 -0
  59. lakana-0.12.0/src/lakana/client.py +291 -0
  60. lakana-0.12.0/src/lakana/errors.py +222 -0
  61. lakana-0.12.0/src/lakana/resources/__init__.py +9 -0
  62. lakana-0.12.0/src/lakana/resources/artifacts.py +1804 -0
  63. lakana-0.12.0/src/lakana/resources/audit_events.py +169 -0
  64. lakana-0.12.0/src/lakana/resources/checks.py +213 -0
  65. lakana-0.12.0/src/lakana/resources/column_masks.py +194 -0
  66. lakana-0.12.0/src/lakana/resources/deploys.py +413 -0
  67. lakana-0.12.0/src/lakana/resources/environments.py +185 -0
  68. lakana-0.12.0/src/lakana/resources/grants.py +199 -0
  69. lakana-0.12.0/src/lakana/resources/groups.py +164 -0
  70. lakana-0.12.0/src/lakana/resources/jobs.py +290 -0
  71. lakana-0.12.0/src/lakana/resources/namespaces.py +173 -0
  72. lakana-0.12.0/src/lakana/resources/package_indexes.py +169 -0
  73. lakana-0.12.0/src/lakana/resources/principals.py +122 -0
  74. lakana-0.12.0/src/lakana/resources/queries.py +493 -0
  75. lakana-0.12.0/src/lakana/resources/repositories.py +192 -0
  76. lakana-0.12.0/src/lakana/resources/row_filters.py +188 -0
  77. lakana-0.12.0/src/lakana/resources/runs.py +454 -0
  78. lakana-0.12.0/src/lakana/resources/secrets.py +170 -0
  79. lakana-0.12.0/src/lakana/resources/tables.py +878 -0
  80. lakana-0.12.0/src/lakana/resources/task_tokens.py +82 -0
  81. lakana-0.12.0/src/lakana/resources/tokens.py +119 -0
  82. lakana-0.12.0/src/lakana/runner.py +1823 -0
  83. lakana-0.12.0/tests/cli/conftest.py +195 -0
  84. lakana-0.12.0/tests/cli/test_app.py +604 -0
  85. lakana-0.12.0/tests/cli/test_artifacts_cmd.py +1372 -0
  86. lakana-0.12.0/tests/cli/test_auth_cmd.py +464 -0
  87. lakana-0.12.0/tests/cli/test_break_glass_cmd.py +216 -0
  88. lakana-0.12.0/tests/cli/test_checks_cmd.py +387 -0
  89. lakana-0.12.0/tests/cli/test_cli_contract.py +137 -0
  90. lakana-0.12.0/tests/cli/test_deploy_init.py +765 -0
  91. lakana-0.12.0/tests/cli/test_deploy_setup.py +544 -0
  92. lakana-0.12.0/tests/cli/test_deploys_cmd.py +1105 -0
  93. lakana-0.12.0/tests/cli/test_environments_cmd.py +218 -0
  94. lakana-0.12.0/tests/cli/test_filelock.py +157 -0
  95. lakana-0.12.0/tests/cli/test_files.py +592 -0
  96. lakana-0.12.0/tests/cli/test_governance.py +463 -0
  97. lakana-0.12.0/tests/cli/test_help_text.py +68 -0
  98. lakana-0.12.0/tests/cli/test_identity.py +204 -0
  99. lakana-0.12.0/tests/cli/test_indexes_cmd.py +256 -0
  100. lakana-0.12.0/tests/cli/test_jobs_cmd.py +551 -0
  101. lakana-0.12.0/tests/cli/test_listings_layout.py +704 -0
  102. lakana-0.12.0/tests/cli/test_local_datadir.py +516 -0
  103. lakana-0.12.0/tests/cli/test_local_pidfile.py +157 -0
  104. lakana-0.12.0/tests/cli/test_local_scripts.py +163 -0
  105. lakana-0.12.0/tests/cli/test_local_supervisor.py +731 -0
  106. lakana-0.12.0/tests/cli/test_namespaces_cmd.py +94 -0
  107. lakana-0.12.0/tests/cli/test_output.py +743 -0
  108. lakana-0.12.0/tests/cli/test_profile_cmd.py +112 -0
  109. lakana-0.12.0/tests/cli/test_profiles.py +451 -0
  110. lakana-0.12.0/tests/cli/test_project.py +176 -0
  111. lakana-0.12.0/tests/cli/test_project_init.py +484 -0
  112. lakana-0.12.0/tests/cli/test_queries.py +1916 -0
  113. lakana-0.12.0/tests/cli/test_repos_cmd.py +205 -0
  114. lakana-0.12.0/tests/cli/test_runs_cmd.py +1578 -0
  115. lakana-0.12.0/tests/cli/test_server_cmd.py +1900 -0
  116. lakana-0.12.0/tests/cli/test_tables_cmd.py +1229 -0
  117. lakana-0.12.0/tests/cli/test_version_option.py +239 -0
  118. lakana-0.12.0/tests/test_artifact_manifest.py +183 -0
  119. lakana-0.12.0/tests/test_artifact_scan.py +645 -0
  120. lakana-0.12.0/tests/test_artifact_tree.py +1194 -0
  121. lakana-0.12.0/tests/test_assets.py +250 -0
  122. lakana-0.12.0/tests/test_client.py +163 -0
  123. lakana-0.12.0/tests/test_config_resolution.py +277 -0
  124. lakana-0.12.0/tests/test_dockerfile.py +140 -0
  125. lakana-0.12.0/tests/test_errors.py +144 -0
  126. lakana-0.12.0/tests/test_http.py +377 -0
  127. lakana-0.12.0/tests/test_packaging.py +73 -0
  128. lakana-0.12.0/tests/test_resources_admin.py +839 -0
  129. lakana-0.12.0/tests/test_resources_artifacts.py +2522 -0
  130. lakana-0.12.0/tests/test_resources_data.py +2094 -0
  131. lakana-0.12.0/tests/test_resources_jobs.py +215 -0
  132. lakana-0.12.0/tests/test_resources_package_indexes.py +79 -0
  133. lakana-0.12.0/tests/test_runner.py +3561 -0
  134. lakana-0.12.0/tests/test_sdk_version.py +43 -0
  135. lakana-0.12.0/tests/test_tokens.py +40 -0
@@ -0,0 +1,25 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ .env
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+ .mypy_cache/
12
+ .import_linter_cache/
13
+ .coverage
14
+ coverage.json
15
+ mutants/
16
+ htmlcov/
17
+ .DS_Store
18
+ node_modules/
19
+ .idea/
20
+ .vscode/
21
+ .superpowers/
22
+ docs/superpowers/
23
+ graphify-out/
24
+ .worktrees/
25
+ site-build/
lakana-0.12.0/PKG-INFO ADDED
@@ -0,0 +1,615 @@
1
+ Metadata-Version: 2.5
2
+ Name: lakana
3
+ Version: 0.12.0
4
+ Summary: Lakana SDK and command-line interface.
5
+ Project-URL: Homepage, https://github.com/chrschy/lakana
6
+ Project-URL: Documentation, https://github.com/chrschy/lakana/tree/main/docs/site
7
+ Project-URL: Repository, https://github.com/chrschy/lakana
8
+ Project-URL: Issues, https://github.com/chrschy/lakana/issues
9
+ Project-URL: Changelog, https://github.com/chrschy/lakana/blob/main/CHANGELOG.md
10
+ Author: Christopher Schymura
11
+ License-Expression: Apache-2.0
12
+ Keywords: data-platform,duckdb,iceberg,lakehouse
13
+ Classifier: Development Status :: 2 - Pre-Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Database
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: httpx<1,>=0.28
26
+ Requires-Dist: pyarrow>=26.0.0
27
+ Requires-Dist: pyyaml>=6.0.3
28
+ Requires-Dist: rich>=13.8
29
+ Requires-Dist: typer<1,>=0.27
30
+ Provides-Extra: assets
31
+ Requires-Dist: polars>=1.30; extra == 'assets'
32
+ Provides-Extra: server
33
+ Requires-Dist: lakana-server==0.12.0; extra == 'server'
34
+ Requires-Dist: lakana-worker==0.12.0; extra == 'server'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # lakana
38
+
39
+ > **Pre-beta.** Lakana is published as a 0.x pre-beta: the API and the data layout may change
40
+ > between releases, and it is not ready for production data yet. See the
41
+ > [project README](https://github.com/chrschy/lakana#readme).
42
+
43
+ A typed Python client for the Lakana platform API. `lakana` never imports `lakana-core`,
44
+ `lakana-server`, or `lakana-worker` — it only speaks HTTP to a running Lakana server, over the
45
+ same REST surface any other client would use.
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install lakana
51
+ ```
52
+
53
+ The only runtime dependencies are `httpx` and `pyarrow`.
54
+
55
+ ## Credentials
56
+
57
+ Every request authenticates with a personal access token (PAT) or a task token, sent as a
58
+ bearer credential. An administrator issues a PAT for your principal:
59
+
60
+ ```python
61
+ admin_client.tokens.issue("alice", "alice-laptop") # -> starts with "lakana_pat_"
62
+ ```
63
+
64
+ Pass the token to `Client` however is convenient for your environment — see "Configuration"
65
+ below for the full resolution order.
66
+
67
+ ## Quickstart
68
+
69
+ ```python
70
+ import lakana
71
+ import pyarrow as pa
72
+
73
+ client = lakana.Client(url="https://lakana.example.com", token="lakana_pat_...")
74
+
75
+ client.environments.create(
76
+ "dev",
77
+ storage={"scheme": "s3", "root": "s3://my-bucket/dev", "options": {}},
78
+ )
79
+
80
+ schema = pa.schema([pa.field("id", pa.int64(), nullable=False), pa.field("region", pa.string())])
81
+ client.tables.create("dev", "bronze.sales", schema=schema)
82
+
83
+ data = pa.table({"id": [1, 2, 3], "region": ["eu", "us", "eu"]}, schema=schema)
84
+ client.tables.append("dev", "bronze.sales", data)
85
+
86
+ result = client.sql(
87
+ "SELECT region, count(*) AS n FROM bronze.sales GROUP BY region", environment="dev"
88
+ )
89
+ print(result)
90
+ ```
91
+
92
+ `client.sql(...)` submits a query, waits for it to finish, and returns the result as a
93
+ `pyarrow.Table` in one call; `client.queries.submit(...)` returns a `QueryRun` handle for
94
+ polling or waiting explicitly when you need more control. `run.cancel()` (or
95
+ `client.queries.cancel(query_id, environment)`) cancels a queued query and asks the worker to stop a
96
+ running one; it returns the `QueryStatus` after the request. A cancelled query has the status
97
+ `CANCELLED`, and `run.wait()` then raises `QueryFailed` with `cause == "cancelled"`. `client.sql`
98
+ does not cancel when your code is interrupted.
99
+
100
+ ### DataFrames: polars by default
101
+
102
+ Arrow is the contract underneath, and **polars** is Lakana's default DataFrame tool. The SDK
103
+ itself only depends on pyarrow (`pip install polars` separately); results convert
104
+ zero-copy, and `create`/`append` accept polars schemas and DataFrames directly:
105
+
106
+ ```python
107
+ import polars as pl
108
+
109
+ frame = pl.DataFrame({"id": [1, 2, 3], "region": ["eu", "us", "eu"]})
110
+ client.tables.create("dev", "bronze.sales_pl", schema=frame.schema)
111
+ client.tables.append("dev", "bronze.sales_pl", frame)
112
+
113
+ df = pl.from_arrow(client.sql("SELECT * FROM bronze.sales_pl", environment="dev"))
114
+ ```
115
+
116
+ polars exports text and bytes as Arrow `string_view`/`binary_view`; the SDK sends them as
117
+ plain `string`/`binary`, since the append endpoint refuses view types. pandas works the
118
+ same way (`client.tables.append(..., pandas_df)`, `result.to_pandas()`).
119
+
120
+ Use `client` as a context manager (`with lakana.Client(...) as client:`) or call
121
+ `client.close()` when done, to release the underlying HTTP connection pool.
122
+
123
+ ### Writing results into tables, views, lineage and maintenance
124
+
125
+ A query's result can go straight into a table instead of back to you. The write runs in a
126
+ worker, under your own grants, and commits once: readers see the old table or the new one,
127
+ never a mix.
128
+
129
+ ```python
130
+ client.sql("SELECT * FROM bronze.sales", environment="dev", into="silver.sales", mode="create")
131
+ client.sql(
132
+ "SELECT region, count(*) AS n FROM silver.sales GROUP BY region",
133
+ environment="dev",
134
+ into="gold.daily",
135
+ mode="replace", # a full refresh, in one snapshot
136
+ )
137
+ status = client.sql(
138
+ "SELECT * FROM bronze.events",
139
+ environment="dev",
140
+ into="silver.events",
141
+ mode="append",
142
+ incremental_key="ts", # appends only rows past the table's max(ts)
143
+ )
144
+ print(status.rows_written, status.watermark)
145
+ ```
146
+
147
+ - `mode` is `create`, `replace`, `append` or `view`. With `into`, `client.sql` returns the
148
+ finished run's status (`rows_written`, `snapshot_id`, `watermark`, `schema_diff`), not a
149
+ table.
150
+ - A result whose schema doesn't match the table fails before anything is written, with a
151
+ diff of added, dropped, renamed-looking and retyped columns. `evolve_schema=True` lets the
152
+ table adopt the result's schema instead. Extra nullable columns are always added.
153
+ - Row filters and column masks on what you read apply to what you write: masked values land
154
+ masked.
155
+ - Late rows (`ts` at or below the watermark) are skipped by an incremental append, by
156
+ design; merge and upsert come after 1.0.
157
+ - An incremental append needs READ on the target, and is refused while a row filter or column
158
+ mask on the target applies to you.
159
+ - `mode="view"` stores the SELECT as a view without running it. Views use invoker's rights:
160
+ reading one needs READ on the view and on every table beneath it, and those tables' row
161
+ filters and masks still apply. A view's SQL and references are shown by `tables.get` only if
162
+ you can READ every table it references; otherwise you get `hidden_references` (a count). A
163
+ query expands at most 100 view references. Views are not visible over the Iceberg REST
164
+ catalog, and can't be dropped through it.
165
+
166
+ Arrow data has the same two write shapes, and can declare where it came from:
167
+
168
+ ```python
169
+ client.tables.replace("dev", "gold.predictions", frame, sources=["gold.features"])
170
+ client.tables.append("dev", "gold.predictions", frame, sources=["gold.features"])
171
+ ```
172
+
173
+ A read-modify-write job can make either write conditional on the table not having changed since
174
+ it read it. Pass the `snapshot_id` you read (`client.tables.get(...).snapshot_id`), or `"none"`
175
+ for "no snapshot yet":
176
+
177
+ ```python
178
+ current = client.tables.get("dev", "gold.predictions").snapshot_id
179
+ client.tables.replace("dev", "gold.predictions", frame, expected_snapshot_id=current)
180
+ ```
181
+
182
+ If another writer committed in between, the write raises `Conflict` and nothing is retried; read
183
+ again and redo it. On `replace`, `"none"` also creates a table that does not exist yet from the
184
+ upload's schema (you need `WRITE_TABLE` and `CREATE_TABLE`). On `append`, a missing table is
185
+ still `NotFound`.
186
+
187
+ Every write records table-level lineage, which you can walk:
188
+
189
+ ```python
190
+ graph = client.tables.lineage("dev", "gold.daily", direction="upstream", depth=3)
191
+ ```
192
+
193
+ Tables you can't read are left out of the graph and counted in `graph.hidden_nodes`; the walk
194
+ never goes through them. A graph has at most 500 nodes; `graph.truncated` is set when a table
195
+ you can read was left out past that cap.
196
+
197
+ Maintenance expires old snapshots (older than `keep_days`, beyond the newest `keep_last`,
198
+ never the current one or a branch or tag) and deletes files nothing references any more:
199
+
200
+ ```python
201
+ run = client.tables.maintain("dev", "gold.daily", keep_days=7, keep_last=5, dry_run=True)
202
+ print(run.wait().maintenance["report"])
203
+ ```
204
+
205
+ - Files are deleted only once they are at least `older_than_hours` old (default 72, minimum 1,
206
+ maximum 87600). Set it longer than your longest external write to the table.
207
+ - Expiry commits only against the metadata it selected from: if the table changed meanwhile,
208
+ nothing is deleted and the report sets `conflict: True` (note "table changed during
209
+ maintenance, retry"); the CLI exits 1. Check `report["conflict"]`, not the note's wording.
210
+ - If the table changes after the expiry but before orphans are deleted, orphan removal is
211
+ skipped and the report's note says so.
212
+ - If any file the table references lies outside its location, orphan removal is skipped and
213
+ the report says why.
214
+ - It needs the `MAINTAIN_TABLE` action, which only the built-in admin role carries.
215
+
216
+ ## Configuration
217
+
218
+ `Client(url=None, token=None, *, profile=None, timeout=30.0, verify=True, http_client=None)`
219
+ resolves its URL and token from the first source below that names either one — sources are
220
+ never merged, so a source that sets one of `url`/`token` must set both:
221
+
222
+ 1. **Explicit constructor arguments** — `url` and `token`.
223
+ 2. **Environment variables** — `LAKANA_URL` and `LAKANA_TOKEN`. `LAKANA_PROFILE` selects a named
224
+ profile from the profile file if you set it alongside these (otherwise it has no effect
225
+ at this step).
226
+ 3. **The profile file** — `$XDG_CONFIG_HOME/lakana/config.toml`, or
227
+ `~/.config/lakana/config.toml` when `XDG_CONFIG_HOME` is unset:
228
+
229
+ ```toml
230
+ default_profile = "dev"
231
+
232
+ [profiles.dev]
233
+ url = "https://lakana.example.com"
234
+ token = "lakana_pat_..."
235
+ ```
236
+
237
+ `profile=` (or `LAKANA_PROFILE`) selects a profile by name, falling back to the file's own
238
+ `default_profile`. The file must be readable only by its owner (mode `0600` or
239
+ stricter) — the SDK refuses it otherwise, telling you the `chmod` to run. It is read
240
+ through a single `O_NOFOLLOW` open, so a symlinked profile file is refused outright as a
241
+ TOCTOU precaution; a symlinked *parent directory* is a known, unclosed limitation of that
242
+ check (see `docs/agent/security.md`).
243
+
244
+ If none of these resolves a URL and token, `Client(...)` raises `lakana.ConfigError`.
245
+
246
+ `http_client` accepts any `httpx.Client`-compatible object in place of one the SDK builds
247
+ itself — useful for tests (see `lakana-server`'s own SDK end-to-end tests, which pass a
248
+ Starlette `TestClient` this way).
249
+
250
+ A token is never written into a log line, an exception message, or a `repr()` — `Client`
251
+ and its resolved config both mask everything but the token's key-id prefix, e.g.
252
+ `Client(url='...', token='lakana_pat_<key>_***')`.
253
+
254
+ ## What's covered
255
+
256
+ `client.<resource>` exposes every `/api/v1` platform route: `environments`, `namespaces`,
257
+ `tables` (including `append`, `replace`, `lineage` and `maintain`), `queries`,
258
+ `principals`, `groups`, `tokens`, `task_tokens`,
259
+ `grants`, `row_filters`, `column_masks`, `secrets`, `repositories`, `deploys`,
260
+ `deployments`, `project_versions`, `orphans`, `artifacts`, `runs`, `jobs`, `checks`, and `audit_events`. A contract test
261
+ (`packages/lakana-server/tests/sdk/test_sdk_contract.py`) checks in both directions that this
262
+ SDK never calls a route the server doesn't serve, and that every platform route has an SDK
263
+ method — so this list can't silently drift from what the server actually exposes.
264
+
265
+ The Iceberg REST Catalog routes (`/v1/...`) are a separate protocol for external Iceberg
266
+ clients (e.g. `pyiceberg`) and are out of scope for this SDK.
267
+
268
+ Errors raise typed exceptions from `lakana.errors` (`AuthenticationError`,
269
+ `PermissionDenied`, `NotFound`, `Conflict`, `PayloadTooLarge`, `ValidationError`,
270
+ `ServerError`, `LakanaConnectionError`, `QueryFailed`, `DeployFailed`, `ConfigError`), all subclasses of
271
+ `lakana.LakanaError`.
272
+
273
+ `client.info().version` is the platform version the instance runs (the same value `lakana --version` shows).
274
+
275
+ ## Command line
276
+
277
+ `pip install lakana` also installs the `lakana` command.
278
+
279
+ ```bash
280
+ lakana login # prompts for URL and token; saves ~/.config/lakana/config.toml (0600)
281
+ lakana whoami
282
+ 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)
283
+ lakana env create dev --storage-root file:///srv/lake
284
+ lakana env delete dev --yes # 409 while a repository is deployed there: `lakana undeploy REPO --env dev` first
285
+ lakana table create bronze.sales --column id:long --column 'region:string?' # quote: shells glob ?
286
+ lakana table append bronze.sales sales.csv # Parquet, CSV or Arrow; a CSV is parsed with the table's types
287
+ lakana sql "SELECT region, count(*) FROM bronze.sales GROUP BY region"
288
+ lakana sql - --out report.parquet < report.sql
289
+ lakana sql "SELECT * FROM bronze.sales" --into silver.sales --mode create
290
+ lakana sql "SELECT region, count(*) AS n FROM silver.sales GROUP BY region" --into gold.daily --mode replace
291
+ lakana sql "SELECT * FROM bronze.events" --into silver.events --mode append --incremental-key ts
292
+ lakana query submit "SELECT ..." | lakana query status ID | lakana query wait ID | lakana query result ID
293
+ lakana query cancel ID # queued: cancelled at once; running: asked to stop
294
+ lakana query list --env dev [--all] [--limit N] # queued and running; --all adds the last 24 h; no SQL
295
+ lakana table replace gold.predictions predictions.parquet --source gold.features
296
+ lakana table lineage gold.daily --depth 5 # --downstream for what it feeds; --output json
297
+ lakana table maintain gold.daily --dry-run # --keep-days 7 --keep-last 5 --older-than-hours 72
298
+ ```
299
+
300
+ - Ctrl-C in `lakana sql` and `lakana query wait` cancels the query they wait for, says what
301
+ happened on stderr and exits with 130 (a second Ctrl-C while the cancel is sent exits at once and
302
+ names `lakana query cancel ID`). The submitter and a holder of `MANAGE_ENVIRONMENT` may cancel;
303
+ 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`.
304
+ - `lakana query list` shows your queries (everyone's with `MANAGE_ENVIRONMENT`): running first, then
305
+ queued in the order they are taken, with the reason each waits; never the SQL or the error text.
306
+ SDK: `client.queries.list(environment, include_ended=False, limit=100)` returns `QueryListEntry`
307
+ objects.
308
+ - A query that is `QUEUED` says why: `lakana query status ID` shows `waiting_for`; `lakana sql` and
309
+ `lakana query wait` print `Waiting: <reason>` on stderr after 3 seconds and again when the
310
+ reason changes (nothing with `--output json`); `lakana runs show` has a `waiting_for` column
311
+ for tasks. SDK: `status.waiting_for` is a `WaitingFor` (`code`, `message`, and the numbers
312
+ `workers`, `ahead`, `other_workers`, `running`, `limit`, each `None` when the code does not use
313
+ it) or `None`, and `run.wait(on_update=f)` calls `f` with every polled status. The codes include
314
+ `no_worker_of_kind` (workers are running and none takes this kind of work: the operator starts
315
+ one with `LAKANA_WORKER__TAKES=interactive`, `pipeline` or `all`) and `user_limit` (you already
316
+ run as many ad-hoc queries as the limit per user allows: wait, or cancel one of them; `running`
317
+ is your own number and a manager reading someone else's row gets `limit` only). Code that
318
+ branches on `code` should treat an unknown one as "waiting". See
319
+ `docs/site/how-to/why-is-it-waiting.md`.
320
+ - In a protected environment, writes outside runs need `--break-glass --reason "…"` (SDK:
321
+ `break_glass_reason=`) and `MANAGE_ENVIRONMENT`.
322
+ - `--env` picks the environment; without it, the profile's `default_environment` is used
323
+ (`lakana login --env dev`, `lakana profile use NAME --env dev`) — unless `LAKANA_URL`/`LAKANA_TOKEN`
324
+ supply the credentials, in which case pass `--env`. There is no built-in default.
325
+ - `--output json` (or `LAKANA_OUTPUT=json`) prints machine-readable JSON on stdout; errors go
326
+ to stderr as `{"error": {"type", "message"}}` — usage errors included (type `UsageError`,
327
+ exit 2), except a malformed global option, which Typer reports before the mode is known.
328
+ - Concurrent `lakana login` / `lakana profile` runs are safe: each update of the profile file
329
+ holds an exclusive lock (`config.toml.lock`, next to it) for its whole read-modify-write, and
330
+ gives up after 10 s if another process holds it. Reads never wait.
331
+ - There is no `--token` option. For CI, use `LAKANA_URL`/`LAKANA_TOKEN`, or
332
+ `lakana login --url URL --token-stdin`.
333
+ - Destructive commands ask first; `--yes` skips the prompt, and without a terminal they
334
+ refuse unless `--yes` is given.
335
+
336
+ | Exit code | Meaning |
337
+ |---|---|
338
+ | 0 | Success |
339
+ | 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) |
340
+ | 2 | Usage error: bad arguments, no environment, confirmation refused |
341
+ | 3 | Configuration: no credentials, or an unsafe profile file |
342
+ | 4 | Authentication failed (401) |
343
+ | 5 | Permission denied (403) |
344
+ | 6 | Not found (404) |
345
+ | 7 | Conflict (409) |
346
+ | 8 | Invalid input (422), or a local file/schema problem |
347
+ | 9 | Query failed or was cancelled by someone else, or a query's or maintenance run's `--timeout` elapsed |
348
+ | 130 | Ctrl-C in `lakana sql` or `lakana query wait`; the query was cancelled if the server could be reached |
349
+ | 10 | Payload too large (413) |
350
+ | 11 | Server error (5xx), connection failure, or a server too old for the command |
351
+
352
+ ### Repositories (M5b)
353
+
354
+ An admin (`MANAGE_REPOSITORY`) registers each git repository once; deployers name it.
355
+
356
+ ```bash
357
+ lakana repo add analytics --kind https --url https://github.com/acme/analytics.git \
358
+ --credential-secret github-token --default-ref main
359
+ lakana repo add infra --kind ssh --url git@gitlab.internal:data/infra.git \
360
+ --credential-secret deploy-key --known-hosts-file ./known_hosts
361
+ lakana repo add demo --kind local --url ~/projects/demo # under LAKANA_REPO_ROOTS
362
+ lakana repo list | show NAME | update NAME [--clear default_ref] | remove NAME --yes
363
+ ```
364
+
365
+ `--credential-secret` names a secret, resolved in each target environment's scope at deploy
366
+ time: key `token` for `https`, `private_key` for `ssh`. Secret values never leave the server.
367
+ `repo remove` is refused while any environment has a current deployment from the repository;
368
+ `lakana undeploy` removes it first.
369
+
370
+ ### Deploying a project (M5b)
371
+
372
+ `lakana init [DIR]` scaffolds a project; see `docs/site/how-to/projects.md` for assets, `lakana.yaml`, deploys and CI setup.
373
+
374
+ lakana deploy [REPO] --env dev [--ref main] [--dry-run] [--no-wait]
375
+ lakana rollback [REPO] --env dev (--to SHA | --to-previous)
376
+ lakana deploys list --env dev | lakana deploys show ID --env dev
377
+ lakana deployments --env dev
378
+ lakana deployments set-run-as REPO --env prod (--principal P | --clear)
379
+ lakana undeploy REPO --env dev [--yes]
380
+ lakana project-version show ID --env dev
381
+ lakana orphans --env dev
382
+
383
+ `REPO` defaults to `repo:` in the `lakana.yaml` found by walking up from the current directory
384
+ 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;
385
+ `lakana deploy` waits and prints the resolved SHA and the diff, exiting 1 if the deploy fails
386
+ (including losing a race to another deploy) or `--timeout` elapses (the run keeps going; the
387
+ message names the id for `lakana deploys show`). After a successful deploy it prints a hint on
388
+ stderr naming `lakana run [REPO] --env ENV` (REPO only when you named it on the command
389
+ line); there is none for `--dry-run`, `--no-wait`, a failed deploy,
390
+ a rollback or `--output json`. A rollback never contacts the git host. For a
391
+ `local` repository, the CLI warns when the work tree has uncommitted changes: only committed
392
+ state is deployed. Deploys record a project version; `lakana run` executes it (see "Running a project").
393
+
394
+ `set-run-as` binds the service account a protected environment's runs write as (needs
395
+ `MANAGE_ENVIRONMENT`). `undeploy` removes the deployment pointer and binding (needs `DEPLOY`,
396
+ refused while a run is active); tables and project versions stay.
397
+
398
+ ### Running a project (M5c1)
399
+
400
+ lakana run [REPO] --env dev [--select 'gold.*']... [--upstream] [--downstream] [--no-wait] [--timeout S]
401
+ lakana runs list --env dev [--repo R] [--limit N] | lakana runs show ID --env dev
402
+ lakana runs logs ID --env dev [--task ASSET] [--follow]
403
+ lakana runs cancel ID --env dev | lakana runs resume ID --env dev
404
+ lakana scheduler status
405
+ lakana worker list
406
+
407
+ `lakana run` prints the run id (stderr), then a line per task each time its state changes
408
+ (`a.b running attempt 1/3`: the attempt in use out of the allowed attempts; no count before the
409
+ first attempt, and `retrying after attempt 1/3` while a failed task waits to try again), and
410
+ waits. Exit codes: 0 success; 1 failed,
411
+ cancelled or timed out; 7 a run is already active. `--select` globs match dotted table names and
412
+ `*` crosses namespace dots (see `docs/site/how-to/projects.md`). SDK: `client.runs.*`, `client.scheduler.status()`. `lakana worker list` (admin) shows the registered
413
+ workers, `alive` or `lost`, the work each takes (`takes`: `all`, `interactive` or `pipeline`) and what
414
+ each is doing; SDK: `client.workers.list()` returns `WorkerInfo` objects, with `WorkerInfo.takes`.
415
+
416
+ ### Jobs (M5d-b)
417
+
418
+ lakana job list --env dev [--repo R]
419
+ lakana job run NAME --env dev [--repo R] [--no-wait] [--timeout S]
420
+ lakana job pause NAME --env dev [--repo R]
421
+ lakana job resume NAME --env dev [--repo R]
422
+ lakana job notify-test NAME --env dev [--repo R]
423
+ lakana job notification DELIVERY_ID --env dev
424
+
425
+ A job is a named selection in `lakana.yaml` (`jobs:`, see `docs/site/how-to/projects.md`).
426
+ `job list` shows, per job, its state (`scheduled`, `paused`, `waiting`, `no schedule` or `no run
427
+ principal`), the schedule with its zone, the next run in the job's zone and in UTC, and the last
428
+ run with what started it; under the table, the sentence of each job that waits. `job run` starts the
429
+ job with the selection from the manifest and follows the run exactly as `lakana run` does (same
430
+ output, same exit codes, `--no-wait`, `--timeout`). It needs `RUN` on the environment and works also
431
+ on a paused job. When the project already has an active run, `job run` does not fail with 7 as
432
+ `lakana run` does: the start waits, begins when the project is free, and the command prints
433
+ `daily waits for run ID to finish; it starts then.` (and how to follow that run) and exits 0:
434
+ that means accepted, not run, so a script that needs the result follows the run with
435
+ `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
436
+ run is `api` when the caller is a service account and `manual` otherwise. `job pause` and `job
437
+ resume` need `DEPLOY` on the environment: resume computes the next run from now, so the times
438
+ missed while paused do not fire; a pause keeps a waiting start. A paused job shows no next run in `job list`. The state
439
+ `waiting to start` means a start waits and no run is active, so the scheduler begins it within
440
+ seconds; a waiting start is checked again then, and one whose requester was deactivated or lost
441
+ `RUN`, or that has no run principal any more, is dropped with an audit event
442
+ (`JOB_START_DROPPED`). `--repo` defaults to `lakana.yaml`'s
443
+ `repo:`. `lakana runs list|show` have `job` and `trigger` columns (`show` also `scheduled_for` and
444
+ `missed`). Exit codes as elsewhere: 5 without the permission, 6 for an unknown job (the message lists
445
+ the jobs there are), 7 when a protected environment has no run principal, or when you pause a job that has no
446
+ schedule. SDK: `client.jobs.list|start|pause|resume`, which return `Job` and
447
+ `JobStart` (`run` or `waiting`).
448
+
449
+ `job notify-test` queues a test message (clearly fake run data) to the webhook the job's `notify:`
450
+ names, then reads the delivery once a second for up to 30 seconds and prints the result:
451
+ `delivered` (exit 0); `failed: <reason>` with the next step, for example "the address is private;
452
+ allow it with LAKANA_WEBHOOK__ALLOW_HOSTS" (exit 1); or `still trying (attempt N of 5); see
453
+ 'lakana job notification ID --env dev'` (exit 1: the result is not known, so a script must not
454
+ treat it as success; `--output json` carries the `state`). It needs `DEPLOY` on the environment.
455
+ A job without `notify:` gives exit 7 ("add or fix it in lakana.yaml and deploy"), an unknown job exit 6.
456
+ `job notification` shows one delivery (needs `DEPLOY`, `RUN` or `READ_ENVIRONMENT`). `lakana runs
457
+ show` lists the notifications of a run under its tasks, each with its delivery id: `delivered`,
458
+ `sending (attempt 1 of 5)`, `retrying (attempt 2 of 5); last: ...` or `failed: the server answered
459
+ 404`. SDK: `client.jobs.notify_test(env, repo, job)` returns the
460
+ delivery id, `client.jobs.delivery(env, id)` a `Delivery`, and `Run.notifications` the list (empty
461
+ from an older server).
462
+
463
+ ### Python assets (M5c2a)
464
+
465
+ SQL transforms, Python at the edges: a Python asset receives its declared inputs as polars
466
+ DataFrames and returns the DataFrame that replaces its table; see `docs/site/how-to/projects.md`, "Python
467
+ assets", for the rules, what a deploy checks and what a task may touch. With `output="artifact"` it
468
+ stores a file or a directory (a trained model, say) instead; see "Artifacts (M5c2b)" below. The project also needs a
469
+ `pyproject.toml` that depends on `lakana[assets]` and a committed `uv.lock`.
470
+
471
+ ```python
472
+ # assets/gold/station_scores.py -> table gold.station_scores
473
+ import polars as pl
474
+ from lakana import asset
475
+
476
+
477
+ @asset(inputs={"readings": "bronze.readings", "stations": "bronze.stations"}, retries=2)
478
+ def station_scores(readings: pl.DataFrame, stations: pl.DataFrame) -> pl.DataFrame:
479
+ """Temperature spread per station over the last week of readings."""
480
+ recent = readings.filter(pl.col("ts") > pl.col("ts").max() - pl.duration(days=7))
481
+ scored = recent.group_by("station_id").agg(score=pl.col("temp_c").std())
482
+ return scored.join(stations, on="station_id")
483
+ ```
484
+
485
+ The inputs are tables other assets build (here two SQL assets, `bronze.readings` and
486
+ `bronze.stations`). The full walk-through, with those two files, the `pyproject.toml` and the
487
+ deploy steps, is in `docs/site/how-to/projects.md`, "Python assets".
488
+
489
+ Python assets run as processes of the worker's user, not in a sandbox: one instance is one trust
490
+ domain, so deploy only code you would run on the worker yourself.
491
+
492
+ ### Artifacts (M5c2b)
493
+
494
+ An artifact is a named file or directory, such as a trained model, stored in numbered versions and
495
+ governed like a table. An asset declared `@asset(output="artifact", keep_versions=5)` writes into
496
+ `lakana.output_dir()` and returns a `pathlib.Path` inside it; a consuming asset gets a `Path` to a
497
+ read-only copy. Everything is in `docs/site/reference/artifacts.md`; the SDK is `client.artifacts`.
498
+
499
+ lakana artifact list [--namespace NS] | upload NAME PATH | download NAME DIR [--version N]
500
+ lakana artifact versions list NAME | versions delete NAME N [--yes]
501
+ lakana artifact drop NAME [--yes] | uploads list [NAME] | uploads abort NAME UPLOAD_ID
502
+
503
+ `upload`, `versions delete` and `drop` take `--break-glass --reason "…"` in a protected
504
+ environment. Loading a pickled model runs code: see
505
+ `docs/site/explanation/artifacts.md`, "A pickled model is code".
506
+
507
+ ### Data quality checks (M5d-a)
508
+
509
+ A project keeps checks under `checks/<namespace>/<table>/<name>.sql`: a `SELECT` that returns the
510
+ rows that are wrong. A run counts them after it writes the table; a failing blocking check sets
511
+ the table back and stops what reads it. A guide is `docs/site/how-to/check-your-data.md`, the
512
+ rules are in `docs/site/reference/checks.md`.
513
+
514
+ lakana check list --env dev [--run RUN_ID] [--table NS.TABLE] [--limit N]
515
+ lakana runs show RUN_ID --env dev # the checks of each task, after the task table
516
+ lakana deploy --env dev --dry-run # lists the checks under "Checks:"
517
+
518
+ SDK: `client.checks.for_run(env, run_id)`, `client.checks.list(env, table=..., cursor=...)` and
519
+ `client.checks.iter(env, table=...)` return `CheckResult` objects (a count of rows, never a row).
520
+ `RunTask.checks` holds a task's results and `RunTask.kind` is `asset` or `source_check` (a table
521
+ the project only reads). `Deploy.checks` lists a project's checks by table. `Table.state` is
522
+ `published` or `not_published`, with `Table.state_message` saying why; a read of a table that is
523
+ not published is refused.
524
+
525
+ ## Local mode
526
+
527
+ `pip install "lakana[server]"` also installs `lakana-server` and `lakana-worker`, so `lakana server
528
+ start --local` can run a complete Lakana on this machine: SQLite for the catalog, the local
529
+ filesystem for storage, no Docker and no Postgres.
530
+
531
+ ```bash
532
+ pip install "lakana[server]"
533
+ lakana server start --local # runs in the foreground; Ctrl-C stops it
534
+ lakana sql "SELECT 42" # in another terminal, once it's ready
535
+ lakana server status # exit 0 if running, 1 otherwise
536
+ ```
537
+
538
+ A `--kind local` repository must lie under one of `LAKANA_REPO_ROOTS`; local mode defaults that to
539
+ your home directory (resolved), so `lakana repo add demo --kind local --url "$PWD"` works anywhere
540
+ under `$HOME` and is refused elsewhere (`/tmp`, another mount); the error says to move the
541
+ project under your home directory, or, on a server, that the operator sets `LAKANA_REPO_ROOTS`
542
+ (it never prints the configured roots). To deploy from another place, start with `LAKANA_REPO_ROOTS='["/srv/projects"]' lakana server start --local`.
543
+
544
+ `lakana[assets]` adds polars for projects with Python assets: `python -m lakana.runner`, the
545
+ harness that runs one, needs it. The SDK and CLI work without it.
546
+
547
+ The `server` extra pins `lakana-server` and `lakana-worker` to `lakana`'s own exact version: a
548
+ `hatch_build.py` metadata hook (`[tool.hatch.metadata.hooks.custom]`) sets
549
+ `optional-dependencies.server` from `lakana`'s already-resolved `hatch-vcs` version at build
550
+ time, so `pip install "lakana[server]"` always installs matching versions of all three, never
551
+ a mix. A dev build off an unreleased commit (`lakana==X.Y.Z.devN+gHASH`) pins the same local
552
+ version string, which only resolves against a matching local wheel or an index that carries
553
+ it, not a plain PyPI release. `lakana-server` and `lakana-worker` each carry their own
554
+ `hatch_build.py` metadata hook that pins their `lakana-core` dependency to the same
555
+ build's version too, so a `lakana[server]` install always resolves one matching
556
+ `lakana-core` rather than whatever the latest release happens to be.
557
+
558
+ The data directory defaults to `$XDG_DATA_HOME/lakana/local` (`~/.local/share/lakana/local`),
559
+ overridable with `--data-dir`; `--port` overrides the default port `8765`. If the port is in use,
560
+ another program or a Lakana instance started with a different `--data-dir` has it
561
+ (`lakana server status --data-dir DIR` checks an instance); pass another `--port`: `start` points
562
+ the `local` profile at it, so later commands find the instance without `--url` (add
563
+ `--profile local` if another profile is your default). It holds
564
+ `lakana.db` (SQLite, WAL mode), `lake/` (storage root), `master.key` (mode `0600`) and the
565
+ running instance's pidfiles. On first start, a `local` profile is saved automatically
566
+ (`default_environment: dev`) and becomes the default profile if none exists yet.
567
+
568
+ `lakana server start --local` binds only to `127.0.0.1` — it is never reachable from another
569
+ machine, whatever `--port` you pick.
570
+
571
+ There is no `reset` command: stop the instance (Ctrl-C, SIGTERM, or — on POSIX — closing the
572
+ terminal, which sends SIGHUP) and delete the data directory to start completely over,
573
+ including a fresh `master.key` and a fresh bootstrap credential on the next `start`.
574
+
575
+ If a first run is interrupted after the `local` profile is saved but before its `dev`
576
+ environment is created, the next `start` notices `dev` is missing and creates it itself (if that
577
+ check fails, e.g. the profile's token is stale, `start` warns and keeps running); to
578
+ do it by hand instead, `lakana env create dev --storage-root file://<data-dir>/lake/dev` (not
579
+ `lakana environment create`, which isn't a command).
580
+
581
+ | `start` exit code | Meaning |
582
+ |---|---|
583
+ | 0 | Shutdown completed (Ctrl-C/SIGTERM/SIGHUP), whether or not a straggler needed `SIGKILL` after the 10 s grace period |
584
+ | 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) |
585
+ | 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) |
586
+ | 3 | The `lakana[server]` extra isn't installed, or the data directory/`master.key`/a pidfile couldn't be prepared, read or written |
587
+
588
+ | `status` exit code | Meaning |
589
+ |---|---|
590
+ | 0 | An instance is running |
591
+ | 1 | No instance is running from this data directory |
592
+
593
+ ## Team mode: `lakana bundle init`
594
+
595
+ For a small team sharing one instance instead of running locally, `lakana bundle init`
596
+ generates a Docker Compose bundle — Postgres for the catalog, SeaweedFS for object storage,
597
+ the server, one or more workers, and a one-shot setup job:
598
+
599
+ ```bash
600
+ lakana bundle init
601
+ cd lakana-deploy && docker compose up -d --wait
602
+ lakana sql "SELECT 42"
603
+ ```
604
+
605
+ It writes `compose.yaml` and `.env` (mode `0600`, holding every generated secret) into
606
+ `./lakana-deploy` by default, and refuses to overwrite an existing bundle rather than risk
607
+ rotating the secrets master key out from under one. It also saves a profile — named `team`
608
+ by default, from the global `--profile` option — so the CLI can talk to the new instance
609
+ immediately. By default the server binds to `127.0.0.1` only; `--bind 0.0.0.0` (or a
610
+ specific address) exposes it on the network over plain HTTP, with a warning, and
611
+ `--public-url` points the saved profile at a reverse proxy instead.
612
+
613
+ See [`docs/site/how-to/deploy-with-compose.md`](../../docs/site/how-to/deploy-with-compose.md) for the full reference:
614
+ every `.env` key, exposing it with TLS, scaling workers, upgrading, backups, and revoking
615
+ the bootstrap credential.