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.
- lakana-0.12.0/.gitignore +25 -0
- lakana-0.12.0/PKG-INFO +615 -0
- lakana-0.12.0/README.md +579 -0
- lakana-0.12.0/hatch_build.py +37 -0
- lakana-0.12.0/pyproject.toml +52 -0
- lakana-0.12.0/src/lakana/__init__.py +48 -0
- lakana-0.12.0/src/lakana/_arrow.py +265 -0
- lakana-0.12.0/src/lakana/_artifact_manifest.py +886 -0
- lakana-0.12.0/src/lakana/_artifact_tree.py +1105 -0
- lakana-0.12.0/src/lakana/_config.py +262 -0
- lakana-0.12.0/src/lakana/_http.py +426 -0
- lakana-0.12.0/src/lakana/_models.py +1844 -0
- lakana-0.12.0/src/lakana/_routes.py +178 -0
- lakana-0.12.0/src/lakana/_tokens.py +43 -0
- lakana-0.12.0/src/lakana/_version.py +21 -0
- lakana-0.12.0/src/lakana/assets.py +292 -0
- lakana-0.12.0/src/lakana/cli/__init__.py +8 -0
- lakana-0.12.0/src/lakana/cli/_break_glass.py +57 -0
- lakana-0.12.0/src/lakana/cli/_commands.py +127 -0
- lakana-0.12.0/src/lakana/cli/_filelock.py +192 -0
- lakana-0.12.0/src/lakana/cli/_files.py +488 -0
- lakana-0.12.0/src/lakana/cli/_output.py +510 -0
- lakana-0.12.0/src/lakana/cli/_profiles.py +435 -0
- lakana-0.12.0/src/lakana/cli/_project.py +183 -0
- lakana-0.12.0/src/lakana/cli/_state.py +352 -0
- lakana-0.12.0/src/lakana/cli/app.py +456 -0
- lakana-0.12.0/src/lakana/cli/artifacts.py +727 -0
- lakana-0.12.0/src/lakana/cli/audit.py +148 -0
- lakana-0.12.0/src/lakana/cli/auth.py +209 -0
- lakana-0.12.0/src/lakana/cli/checks.py +178 -0
- lakana-0.12.0/src/lakana/cli/deploy/__init__.py +12 -0
- lakana-0.12.0/src/lakana/cli/deploy/_cli.py +277 -0
- lakana-0.12.0/src/lakana/cli/deploy/_init.py +314 -0
- lakana-0.12.0/src/lakana/cli/deploy/compose.yaml +193 -0
- lakana-0.12.0/src/lakana/cli/deploy/setup.py +236 -0
- lakana-0.12.0/src/lakana/cli/deploys.py +636 -0
- lakana-0.12.0/src/lakana/cli/environments.py +220 -0
- lakana-0.12.0/src/lakana/cli/grants.py +282 -0
- lakana-0.12.0/src/lakana/cli/groups.py +126 -0
- lakana-0.12.0/src/lakana/cli/indexes.py +194 -0
- lakana-0.12.0/src/lakana/cli/jobs.py +323 -0
- lakana-0.12.0/src/lakana/cli/local/__init__.py +1 -0
- lakana-0.12.0/src/lakana/cli/local/_datadir.py +398 -0
- lakana-0.12.0/src/lakana/cli/local/_pidfile.py +118 -0
- lakana-0.12.0/src/lakana/cli/local/_scripts.py +100 -0
- lakana-0.12.0/src/lakana/cli/local/_supervisor.py +545 -0
- lakana-0.12.0/src/lakana/cli/namespaces.py +139 -0
- lakana-0.12.0/src/lakana/cli/policies.py +246 -0
- lakana-0.12.0/src/lakana/cli/principals.py +107 -0
- lakana-0.12.0/src/lakana/cli/profile.py +86 -0
- lakana-0.12.0/src/lakana/cli/project_init.py +384 -0
- lakana-0.12.0/src/lakana/cli/queries.py +933 -0
- lakana-0.12.0/src/lakana/cli/repos.py +231 -0
- lakana-0.12.0/src/lakana/cli/runs.py +947 -0
- lakana-0.12.0/src/lakana/cli/secrets.py +187 -0
- lakana-0.12.0/src/lakana/cli/server.py +916 -0
- lakana-0.12.0/src/lakana/cli/tables.py +711 -0
- lakana-0.12.0/src/lakana/cli/tokens.py +152 -0
- lakana-0.12.0/src/lakana/client.py +291 -0
- lakana-0.12.0/src/lakana/errors.py +222 -0
- lakana-0.12.0/src/lakana/resources/__init__.py +9 -0
- lakana-0.12.0/src/lakana/resources/artifacts.py +1804 -0
- lakana-0.12.0/src/lakana/resources/audit_events.py +169 -0
- lakana-0.12.0/src/lakana/resources/checks.py +213 -0
- lakana-0.12.0/src/lakana/resources/column_masks.py +194 -0
- lakana-0.12.0/src/lakana/resources/deploys.py +413 -0
- lakana-0.12.0/src/lakana/resources/environments.py +185 -0
- lakana-0.12.0/src/lakana/resources/grants.py +199 -0
- lakana-0.12.0/src/lakana/resources/groups.py +164 -0
- lakana-0.12.0/src/lakana/resources/jobs.py +290 -0
- lakana-0.12.0/src/lakana/resources/namespaces.py +173 -0
- lakana-0.12.0/src/lakana/resources/package_indexes.py +169 -0
- lakana-0.12.0/src/lakana/resources/principals.py +122 -0
- lakana-0.12.0/src/lakana/resources/queries.py +493 -0
- lakana-0.12.0/src/lakana/resources/repositories.py +192 -0
- lakana-0.12.0/src/lakana/resources/row_filters.py +188 -0
- lakana-0.12.0/src/lakana/resources/runs.py +454 -0
- lakana-0.12.0/src/lakana/resources/secrets.py +170 -0
- lakana-0.12.0/src/lakana/resources/tables.py +878 -0
- lakana-0.12.0/src/lakana/resources/task_tokens.py +82 -0
- lakana-0.12.0/src/lakana/resources/tokens.py +119 -0
- lakana-0.12.0/src/lakana/runner.py +1823 -0
- lakana-0.12.0/tests/cli/conftest.py +195 -0
- lakana-0.12.0/tests/cli/test_app.py +604 -0
- lakana-0.12.0/tests/cli/test_artifacts_cmd.py +1372 -0
- lakana-0.12.0/tests/cli/test_auth_cmd.py +464 -0
- lakana-0.12.0/tests/cli/test_break_glass_cmd.py +216 -0
- lakana-0.12.0/tests/cli/test_checks_cmd.py +387 -0
- lakana-0.12.0/tests/cli/test_cli_contract.py +137 -0
- lakana-0.12.0/tests/cli/test_deploy_init.py +765 -0
- lakana-0.12.0/tests/cli/test_deploy_setup.py +544 -0
- lakana-0.12.0/tests/cli/test_deploys_cmd.py +1105 -0
- lakana-0.12.0/tests/cli/test_environments_cmd.py +218 -0
- lakana-0.12.0/tests/cli/test_filelock.py +157 -0
- lakana-0.12.0/tests/cli/test_files.py +592 -0
- lakana-0.12.0/tests/cli/test_governance.py +463 -0
- lakana-0.12.0/tests/cli/test_help_text.py +68 -0
- lakana-0.12.0/tests/cli/test_identity.py +204 -0
- lakana-0.12.0/tests/cli/test_indexes_cmd.py +256 -0
- lakana-0.12.0/tests/cli/test_jobs_cmd.py +551 -0
- lakana-0.12.0/tests/cli/test_listings_layout.py +704 -0
- lakana-0.12.0/tests/cli/test_local_datadir.py +516 -0
- lakana-0.12.0/tests/cli/test_local_pidfile.py +157 -0
- lakana-0.12.0/tests/cli/test_local_scripts.py +163 -0
- lakana-0.12.0/tests/cli/test_local_supervisor.py +731 -0
- lakana-0.12.0/tests/cli/test_namespaces_cmd.py +94 -0
- lakana-0.12.0/tests/cli/test_output.py +743 -0
- lakana-0.12.0/tests/cli/test_profile_cmd.py +112 -0
- lakana-0.12.0/tests/cli/test_profiles.py +451 -0
- lakana-0.12.0/tests/cli/test_project.py +176 -0
- lakana-0.12.0/tests/cli/test_project_init.py +484 -0
- lakana-0.12.0/tests/cli/test_queries.py +1916 -0
- lakana-0.12.0/tests/cli/test_repos_cmd.py +205 -0
- lakana-0.12.0/tests/cli/test_runs_cmd.py +1578 -0
- lakana-0.12.0/tests/cli/test_server_cmd.py +1900 -0
- lakana-0.12.0/tests/cli/test_tables_cmd.py +1229 -0
- lakana-0.12.0/tests/cli/test_version_option.py +239 -0
- lakana-0.12.0/tests/test_artifact_manifest.py +183 -0
- lakana-0.12.0/tests/test_artifact_scan.py +645 -0
- lakana-0.12.0/tests/test_artifact_tree.py +1194 -0
- lakana-0.12.0/tests/test_assets.py +250 -0
- lakana-0.12.0/tests/test_client.py +163 -0
- lakana-0.12.0/tests/test_config_resolution.py +277 -0
- lakana-0.12.0/tests/test_dockerfile.py +140 -0
- lakana-0.12.0/tests/test_errors.py +144 -0
- lakana-0.12.0/tests/test_http.py +377 -0
- lakana-0.12.0/tests/test_packaging.py +73 -0
- lakana-0.12.0/tests/test_resources_admin.py +839 -0
- lakana-0.12.0/tests/test_resources_artifacts.py +2522 -0
- lakana-0.12.0/tests/test_resources_data.py +2094 -0
- lakana-0.12.0/tests/test_resources_jobs.py +215 -0
- lakana-0.12.0/tests/test_resources_package_indexes.py +79 -0
- lakana-0.12.0/tests/test_runner.py +3561 -0
- lakana-0.12.0/tests/test_sdk_version.py +43 -0
- lakana-0.12.0/tests/test_tokens.py +40 -0
lakana-0.12.0/.gitignore
ADDED
|
@@ -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.
|