lakana-server 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_server-0.12.0/.gitignore +25 -0
- lakana_server-0.12.0/PKG-INFO +39 -0
- lakana_server-0.12.0/README.md +9 -0
- lakana_server-0.12.0/hatch_build.py +39 -0
- lakana_server-0.12.0/pyproject.toml +73 -0
- lakana_server-0.12.0/src/lakana_server/__init__.py +1 -0
- lakana_server-0.12.0/src/lakana_server/__main__.py +56 -0
- lakana_server-0.12.0/src/lakana_server/api/__init__.py +1 -0
- lakana_server-0.12.0/src/lakana_server/api/iceberg/__init__.py +1 -0
- lakana_server-0.12.0/src/lakana_server/api/iceberg/encoding.py +60 -0
- lakana_server-0.12.0/src/lakana_server/api/iceberg/route.py +203 -0
- lakana_server-0.12.0/src/lakana_server/api/iceberg/router.py +531 -0
- lakana_server-0.12.0/src/lakana_server/api/iceberg/schemas.py +245 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/__init__.py +1 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/artifacts.py +1520 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/audit_events.py +211 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/column_masks.py +219 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/deploys.py +358 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/environments.py +212 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/grants.py +222 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/groups.py +180 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/info.py +65 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/jobs.py +289 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/namespaces.py +177 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/package_indexes.py +175 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/principals.py +127 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/queries.py +343 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/repositories.py +180 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/row_filters.py +212 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/runs.py +471 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/schemas.py +2589 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/secrets.py +228 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/tables.py +1304 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/task_tokens.py +77 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/tokens.py +107 -0
- lakana_server-0.12.0/src/lakana_server/api/platform/whoami.py +43 -0
- lakana_server-0.12.0/src/lakana_server/api/routing.py +75 -0
- lakana_server-0.12.0/src/lakana_server/app.py +228 -0
- lakana_server-0.12.0/src/lakana_server/auth_rate_limit.py +119 -0
- lakana_server-0.12.0/src/lakana_server/body_limit.py +228 -0
- lakana_server-0.12.0/src/lakana_server/concurrency.py +50 -0
- lakana_server-0.12.0/src/lakana_server/deps.py +759 -0
- lakana_server-0.12.0/src/lakana_server/errors.py +220 -0
- lakana_server-0.12.0/src/lakana_server/log_config.py +45 -0
- lakana_server-0.12.0/src/lakana_server/settings.py +194 -0
- lakana_server-0.12.0/tests/__init__.py +1 -0
- lakana_server-0.12.0/tests/api/__init__.py +1 -0
- lakana_server-0.12.0/tests/api/iceberg/__init__.py +1 -0
- lakana_server-0.12.0/tests/api/iceberg/test_commit_route_has_no_action.py +63 -0
- lakana_server-0.12.0/tests/api/iceberg/test_encoding.py +33 -0
- lakana_server-0.12.0/tests/api/iceberg/test_namespaces.py +394 -0
- lakana_server-0.12.0/tests/api/iceberg/test_route.py +255 -0
- lakana_server-0.12.0/tests/api/iceberg/test_tables.py +1105 -0
- lakana_server-0.12.0/tests/api/iceberg/test_views_hidden.py +32 -0
- lakana_server-0.12.0/tests/api/platform/__init__.py +1 -0
- lakana_server-0.12.0/tests/api/platform/test_artifacts_delete.py +708 -0
- lakana_server-0.12.0/tests/api/platform/test_artifacts_read.py +1241 -0
- lakana_server-0.12.0/tests/api/platform/test_artifacts_upload.py +1332 -0
- lakana_server-0.12.0/tests/api/platform/test_artifacts_uploads_list.py +183 -0
- lakana_server-0.12.0/tests/api/platform/test_audit_events.py +268 -0
- lakana_server-0.12.0/tests/api/platform/test_bound_task_tokens.py +675 -0
- lakana_server-0.12.0/tests/api/platform/test_column_masks.py +316 -0
- lakana_server-0.12.0/tests/api/platform/test_deployments_run_as.py +148 -0
- lakana_server-0.12.0/tests/api/platform/test_deploys.py +398 -0
- lakana_server-0.12.0/tests/api/platform/test_environments.py +486 -0
- lakana_server-0.12.0/tests/api/platform/test_grants.py +308 -0
- lakana_server-0.12.0/tests/api/platform/test_groups.py +157 -0
- lakana_server-0.12.0/tests/api/platform/test_info.py +63 -0
- lakana_server-0.12.0/tests/api/platform/test_jobs.py +340 -0
- lakana_server-0.12.0/tests/api/platform/test_lockdown_api.py +239 -0
- lakana_server-0.12.0/tests/api/platform/test_namespaces.py +116 -0
- lakana_server-0.12.0/tests/api/platform/test_package_indexes.py +210 -0
- lakana_server-0.12.0/tests/api/platform/test_principals.py +74 -0
- lakana_server-0.12.0/tests/api/platform/test_queries.py +1157 -0
- lakana_server-0.12.0/tests/api/platform/test_queries_writes.py +209 -0
- lakana_server-0.12.0/tests/api/platform/test_repositories.py +254 -0
- lakana_server-0.12.0/tests/api/platform/test_row_filters.py +149 -0
- lakana_server-0.12.0/tests/api/platform/test_runs.py +685 -0
- lakana_server-0.12.0/tests/api/platform/test_secrets.py +289 -0
- lakana_server-0.12.0/tests/api/platform/test_tables.py +692 -0
- lakana_server-0.12.0/tests/api/platform/test_tables_append.py +1704 -0
- lakana_server-0.12.0/tests/api/platform/test_tables_lineage.py +100 -0
- lakana_server-0.12.0/tests/api/platform/test_tables_maintenance.py +351 -0
- lakana_server-0.12.0/tests/api/platform/test_tables_replace.py +594 -0
- lakana_server-0.12.0/tests/api/platform/test_task_tokens.py +512 -0
- lakana_server-0.12.0/tests/api/platform/test_tokens.py +226 -0
- lakana_server-0.12.0/tests/api/platform/test_whoami.py +44 -0
- lakana_server-0.12.0/tests/api/test_schemas_warnings.py +30 -0
- lakana_server-0.12.0/tests/cli/__init__.py +1 -0
- lakana_server-0.12.0/tests/cli/conftest.py +163 -0
- lakana_server-0.12.0/tests/cli/test_cli_deploy.py +158 -0
- lakana_server-0.12.0/tests/cli/test_cli_end_to_end.py +317 -0
- lakana_server-0.12.0/tests/cli/test_cli_integration.py +191 -0
- lakana_server-0.12.0/tests/cli/test_column_masks_howto.py +143 -0
- lakana_server-0.12.0/tests/cli/test_local_mode_integration.py +280 -0
- lakana_server-0.12.0/tests/cli/test_row_filters_howto.py +166 -0
- lakana_server-0.12.0/tests/conftest.py +173 -0
- lakana_server-0.12.0/tests/integration_helpers.py +417 -0
- lakana_server-0.12.0/tests/python_asset_helpers.py +684 -0
- lakana_server-0.12.0/tests/sdk/__init__.py +1 -0
- lakana_server-0.12.0/tests/sdk/conftest.py +34 -0
- lakana_server-0.12.0/tests/sdk/test_sdk_admin.py +173 -0
- lakana_server-0.12.0/tests/sdk/test_sdk_artifacts.py +243 -0
- lakana_server-0.12.0/tests/sdk/test_sdk_break_glass.py +50 -0
- lakana_server-0.12.0/tests/sdk/test_sdk_contract.py +72 -0
- lakana_server-0.12.0/tests/sdk/test_sdk_data_plane.py +428 -0
- lakana_server-0.12.0/tests/sdk/test_sdk_deploys.py +86 -0
- lakana_server-0.12.0/tests/sdk/test_sdk_integration.py +124 -0
- lakana_server-0.12.0/tests/sdk/test_sdk_package_indexes.py +58 -0
- lakana_server-0.12.0/tests/sdk/test_sdk_repositories.py +58 -0
- lakana_server-0.12.0/tests/sdk/test_sdk_runs.py +277 -0
- lakana_server-0.12.0/tests/server_test_helpers.py +288 -0
- lakana_server-0.12.0/tests/test_app.py +818 -0
- lakana_server-0.12.0/tests/test_artifacts_integration.py +1152 -0
- lakana_server-0.12.0/tests/test_auth_rate_limit.py +123 -0
- lakana_server-0.12.0/tests/test_authorization_lifecycle_integration.py +220 -0
- lakana_server-0.12.0/tests/test_body_limit.py +496 -0
- lakana_server-0.12.0/tests/test_bound_tokens_integration.py +584 -0
- lakana_server-0.12.0/tests/test_checks_races_integration.py +2894 -0
- lakana_server-0.12.0/tests/test_denial_audit_flood.py +136 -0
- lakana_server-0.12.0/tests/test_deps.py +403 -0
- lakana_server-0.12.0/tests/test_download_log.py +101 -0
- lakana_server-0.12.0/tests/test_errors.py +145 -0
- lakana_server-0.12.0/tests/test_hatch_build.py +58 -0
- lakana_server-0.12.0/tests/test_healthz.py +18 -0
- lakana_server-0.12.0/tests/test_iceberg_commit_answer_integration.py +267 -0
- lakana_server-0.12.0/tests/test_identity_lifecycle_integration.py +165 -0
- lakana_server-0.12.0/tests/test_jobs_integration.py +944 -0
- lakana_server-0.12.0/tests/test_jobs_schema.py +23 -0
- lakana_server-0.12.0/tests/test_lifecycle_integration.py +210 -0
- lakana_server-0.12.0/tests/test_lineage_visibility_integration.py +476 -0
- lakana_server-0.12.0/tests/test_log_config.py +85 -0
- lakana_server-0.12.0/tests/test_lost_worker_processes_integration.py +1058 -0
- lakana_server-0.12.0/tests/test_lost_workers_integration.py +1810 -0
- lakana_server-0.12.0/tests/test_main.py +96 -0
- lakana_server-0.12.0/tests/test_main_logging.py +139 -0
- lakana_server-0.12.0/tests/test_materialization_integration.py +404 -0
- lakana_server-0.12.0/tests/test_no_run_context_on_the_wire.py +211 -0
- lakana_server-0.12.0/tests/test_notifications_integration.py +578 -0
- lakana_server-0.12.0/tests/test_projects_deploy_integration.py +769 -0
- lakana_server-0.12.0/tests/test_python_assets_integration.py +2300 -0
- lakana_server-0.12.0/tests/test_query_process_integration.py +503 -0
- lakana_server-0.12.0/tests/test_run_engine_integration.py +3019 -0
- lakana_server-0.12.0/tests/test_running_cap_integration.py +468 -0
- lakana_server-0.12.0/tests/test_secrets_lifecycle_integration.py +204 -0
- lakana_server-0.12.0/tests/test_settings.py +84 -0
- lakana_server-0.12.0/tests/test_statement_limit.py +185 -0
- lakana_server-0.12.0/tests/test_test_hygiene.py +17 -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/
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: lakana-server
|
|
3
|
+
Version: 0.12.0
|
|
4
|
+
Summary: Lakana control plane: REST API, Iceberg REST catalog, worker.
|
|
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: fastapi<1,>=0.141
|
|
26
|
+
Requires-Dist: lakana-core==0.12.0
|
|
27
|
+
Requires-Dist: pyarrow>=18.0.0
|
|
28
|
+
Requires-Dist: uvicorn>=0.30
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# lakana-server
|
|
32
|
+
|
|
33
|
+
> **Pre-beta.** Lakana is published as a 0.x pre-beta: the API and the data layout may change
|
|
34
|
+
> between releases, and it is not ready for production data yet. See the
|
|
35
|
+
> [project README](https://github.com/chrschy/lakana#readme).
|
|
36
|
+
|
|
37
|
+
Lakana control plane: REST API, Iceberg REST catalog, worker.
|
|
38
|
+
|
|
39
|
+
Part of [Lakana](https://github.com/chrschy/lakana). Installed with `pip install "lakana[server]"`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# lakana-server
|
|
2
|
+
|
|
3
|
+
> **Pre-beta.** Lakana is published as a 0.x pre-beta: the API and the data layout may change
|
|
4
|
+
> between releases, and it is not ready for production data yet. See the
|
|
5
|
+
> [project README](https://github.com/chrschy/lakana#readme).
|
|
6
|
+
|
|
7
|
+
Lakana control plane: REST API, Iceberg REST catalog, worker.
|
|
8
|
+
|
|
9
|
+
Part of [Lakana](https://github.com/chrschy/lakana). Installed with `pip install "lakana[server]"`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Hatchling metadata hook: pin `lakana-core` to this exact release.
|
|
2
|
+
|
|
3
|
+
hatch-vcs resolves every workspace package's version from the same git tag
|
|
4
|
+
(`raw-options.root = "../.."` in each package's `[tool.hatch.version]`), so
|
|
5
|
+
`lakana-core` can be pinned to precisely this build's own version with no
|
|
6
|
+
hand-edited version anywhere (rule 3): hatchling resolves `project.version`
|
|
7
|
+
before any metadata hook's `update()` runs
|
|
8
|
+
(`hatchling.metadata.core.ProjectMetadata.core`), so `metadata["version"]`
|
|
9
|
+
below is already the real, resolved version. See
|
|
10
|
+
`packages/lakana/hatch_build.py`'s `ServerExtraMetadataHook`, which pins
|
|
11
|
+
`lakana-server`/`lakana-worker` inside `lakana`'s `server` extra the same way.
|
|
12
|
+
|
|
13
|
+
`dependencies` is dynamic in `pyproject.toml`, so hatchling requires this
|
|
14
|
+
hook to supply the *entire* list, not just the pinned addition. The other
|
|
15
|
+
dependencies, with their explanatory comments, live under this hook's own
|
|
16
|
+
config table (`[tool.hatch.metadata.hooks.custom]`) so they stay readable as
|
|
17
|
+
ordinary TOML instead of being hard-coded here.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from hatchling.metadata.plugin.interface import MetadataHookInterface
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CoreDependencyMetadataHook(MetadataHookInterface):
|
|
28
|
+
"""Appends a `lakana-core==<version>` pin to the configured dependencies."""
|
|
29
|
+
|
|
30
|
+
def update(self, metadata: dict[str, Any]) -> None:
|
|
31
|
+
"""Set `dependencies` to the configured list plus a `lakana-core` pin.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
metadata
|
|
36
|
+
The `project` table, mutated in place.
|
|
37
|
+
"""
|
|
38
|
+
version = metadata["version"]
|
|
39
|
+
metadata["dependencies"] = [*self.config["dependencies"], f"lakana-core=={version}"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lakana-server"
|
|
7
|
+
description = "Lakana control plane: REST API, Iceberg REST catalog, worker."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
authors = [{ name = "Christopher Schymura" }]
|
|
12
|
+
keywords = ["lakehouse", "duckdb", "iceberg", "data-platform"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
15
|
+
"License :: OSI Approved :: Apache Software License",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Topic :: Database",
|
|
25
|
+
]
|
|
26
|
+
urls = { Homepage = "https://github.com/chrschy/lakana", Documentation = "https://github.com/chrschy/lakana/tree/main/docs/site", Repository = "https://github.com/chrschy/lakana", Issues = "https://github.com/chrschy/lakana/issues", Changelog = "https://github.com/chrschy/lakana/blob/main/CHANGELOG.md" }
|
|
27
|
+
dynamic = ["version", "dependencies"]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
lakana-server = "lakana_server.__main__:main"
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
# The workspace `lakana` SDK, for the SDK contract test
|
|
34
|
+
# (tests/sdk/test_sdk_contract.py) and the SDK end-to-end tests
|
|
35
|
+
# (tests/sdk/), which drive the app only through `lakana.Client` -- never
|
|
36
|
+
# imported by lakana_server itself (see server-goes-through-core and
|
|
37
|
+
# sdk-is-independent in .importlinter; this is a dev-only, test-time
|
|
38
|
+
# dependency, unrelated to either contract).
|
|
39
|
+
dev = ["lakana"]
|
|
40
|
+
|
|
41
|
+
[tool.uv.sources]
|
|
42
|
+
lakana-core = { workspace = true }
|
|
43
|
+
lakana = { workspace = true }
|
|
44
|
+
|
|
45
|
+
[tool.hatch.version]
|
|
46
|
+
source = "vcs"
|
|
47
|
+
raw-options = { root = "../.." }
|
|
48
|
+
|
|
49
|
+
[tool.hatch.metadata.hooks.custom]
|
|
50
|
+
# Loads `hatch_build.py`'s `CoreDependencyMetadataHook`, which appends
|
|
51
|
+
# `lakana-core==<this build's version>` to the `dependencies` list below,
|
|
52
|
+
# pinning lakana-server to exactly this release's lakana-core -- the
|
|
53
|
+
# same hatch-vcs-resolved version pattern as `packages/lakana/hatch_build.py`'s
|
|
54
|
+
# `ServerExtraMetadataHook`. `dependencies` lives here, not under
|
|
55
|
+
# `[project]`, because hatchling requires the hook to supply the *entire*
|
|
56
|
+
# list once `dependencies` is dynamic; keeping the other pins and their
|
|
57
|
+
# comments in this table (rather than hard-coding them into hatch_build.py)
|
|
58
|
+
# keeps them visible and editable as ordinary TOML.
|
|
59
|
+
dependencies = [
|
|
60
|
+
"fastapi>=0.141,<1",
|
|
61
|
+
# Decodes a caller-submitted Arrow IPC stream for the governed append
|
|
62
|
+
# route (lakana_server.api.platform.tables.append_table_data). Already
|
|
63
|
+
# arrives transitively through lakana-core's own pyiceberg/pyarrow
|
|
64
|
+
# dependency, but this package imports it directly now, so it is
|
|
65
|
+
# declared directly too rather than relying on another package's
|
|
66
|
+
# transitive dependency graph. Apache-2.0, same as lakana-core's own copy.
|
|
67
|
+
"pyarrow>=18.0.0",
|
|
68
|
+
# ASGI server for the `lakana-server` console script. BSD-3-Clause.
|
|
69
|
+
"uvicorn>=0.30",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
[tool.hatch.build.targets.wheel]
|
|
73
|
+
packages = ["src/lakana_server"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Lakana control plane: REST API, Iceberg REST catalog, worker."""
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Entry point for the `lakana-server` console script.
|
|
2
|
+
|
|
3
|
+
`lakana-server` serves the API with uvicorn; `lakana-server migrate` applies the
|
|
4
|
+
metadata database migrations and exits. Both read `LAKANA_*` settings.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
|
|
11
|
+
import uvicorn
|
|
12
|
+
|
|
13
|
+
from lakana_core import migrate
|
|
14
|
+
from lakana_core.log_setup import configure_logging
|
|
15
|
+
from lakana_server.app import create_app
|
|
16
|
+
from lakana_server.log_config import uvicorn_log_config
|
|
17
|
+
from lakana_server.settings import Settings
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main(argv: list[str] | None = None) -> None:
|
|
21
|
+
"""Serve the API, or run a subcommand.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
argv
|
|
26
|
+
Command-line arguments without the program name; `sys.argv[1:]` when
|
|
27
|
+
`None`.
|
|
28
|
+
|
|
29
|
+
Notes
|
|
30
|
+
-----
|
|
31
|
+
Logging is set up once the settings are read (`lakana_core.log_setup.configure_logging`):
|
|
32
|
+
`LAKANA_LOG__LEVEL` (INFO by default) and above to stderr with time, level and logger
|
|
33
|
+
name, like the worker, so the INFO lines of `lakana_*` loggers reach
|
|
34
|
+
`docker compose logs server`. Uvicorn keeps its own handlers, with formatters that escape
|
|
35
|
+
control characters like the root handler's (`lakana_server.log_config`), and gets the same
|
|
36
|
+
level, so its access log (one line per request) is printed at INFO and DEBUG only.
|
|
37
|
+
"""
|
|
38
|
+
parser = argparse.ArgumentParser(prog="lakana-server", description="Run the Lakana API server.")
|
|
39
|
+
parser.add_argument("command", nargs="?", choices=["migrate"], help="migrate: apply migrations")
|
|
40
|
+
args = parser.parse_args(argv)
|
|
41
|
+
settings = Settings()
|
|
42
|
+
configure_logging(settings.log.level)
|
|
43
|
+
if args.command == "migrate":
|
|
44
|
+
migrate.upgrade(settings.database.url)
|
|
45
|
+
return
|
|
46
|
+
uvicorn.run(
|
|
47
|
+
create_app(settings),
|
|
48
|
+
host=settings.host,
|
|
49
|
+
port=settings.port,
|
|
50
|
+
log_level=settings.log.level.lower(),
|
|
51
|
+
log_config=uvicorn_log_config(),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__": # pragma: no cover — exercised only by `python -m lakana_server`
|
|
56
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The control plane's HTTP surface: the platform API and the Iceberg REST catalog."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The Iceberg REST Catalog API: config, namespaces, and tables."""
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
r"""Conversion between the Iceberg REST wire format and NamespacePath.
|
|
2
|
+
|
|
3
|
+
Two wire shapes exist for the same value: a URL path segment joins parts
|
|
4
|
+
with the raw unit separator character (`\x1f`), matching
|
|
5
|
+
`lakana_core.identifiers.NAMESPACE_SEPARATOR` exactly — this is not a
|
|
6
|
+
coincidence; the separator was chosen for this reason (see the architecture
|
|
7
|
+
doc's identity decisions). FastAPI/Starlette URL-decodes path parameters
|
|
8
|
+
before an endpoint ever sees them, so a namespace segment arrives here
|
|
9
|
+
already containing the literal separator character, not a percent-encoded
|
|
10
|
+
stand-in for it. A JSON response body instead uses a list of parts.
|
|
11
|
+
`decode_namespace` and `encode_namespace` are not a round-trip pair of each
|
|
12
|
+
other: `decode_namespace` only ever parses the joined URL-segment form, and
|
|
13
|
+
`encode_namespace` only ever produces the list form used in response
|
|
14
|
+
bodies — no call site passes one function's output into the other.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from lakana_core.identifiers import NamespacePath
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def decode_namespace(raw: str) -> NamespacePath:
|
|
23
|
+
"""Parse a namespace path segment from a request URL.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
raw
|
|
28
|
+
The `{namespace}` path parameter, already URL-decoded by Starlette,
|
|
29
|
+
so it already contains the literal separator character rather than
|
|
30
|
+
a percent-encoded stand-in for it.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
NamespacePath
|
|
35
|
+
The parsed path.
|
|
36
|
+
|
|
37
|
+
Raises
|
|
38
|
+
------
|
|
39
|
+
lakana_core.errors.ValidationError
|
|
40
|
+
If any component is invalid.
|
|
41
|
+
"""
|
|
42
|
+
return NamespacePath.parse(raw)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def encode_namespace(path: NamespacePath) -> list[str]:
|
|
46
|
+
"""Render a namespace as the Iceberg wire format's list of parts.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
path
|
|
51
|
+
The namespace to render.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
list of str
|
|
56
|
+
The namespace's parts, outermost first — the shape used in every
|
|
57
|
+
JSON response that names a namespace (`ListNamespacesResponse`,
|
|
58
|
+
`NamespaceResponse`, `ListTablesResponse`'s per-entry `namespace`).
|
|
59
|
+
"""
|
|
60
|
+
return list(path.parts)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""The error-shaping route class for the Iceberg REST router.
|
|
2
|
+
|
|
3
|
+
FastAPI's exception handlers are registered on the application as a whole,
|
|
4
|
+
but the Iceberg REST specification mandates a response body
|
|
5
|
+
(`{"error": {"message", "type", "code"}}`) different from the platform API's.
|
|
6
|
+
This route class wraps each endpoint's dependency resolution and handler
|
|
7
|
+
call, catching the same exception vocabulary and reshaping it before it
|
|
8
|
+
would ever reach the app-level handlers `install_exception_handlers` sets up
|
|
9
|
+
— those serve the platform API only.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from collections.abc import Callable, Coroutine
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import pydantic
|
|
19
|
+
from fastapi import Request, Response
|
|
20
|
+
from fastapi.exceptions import RequestValidationError
|
|
21
|
+
from fastapi.responses import JSONResponse
|
|
22
|
+
from starlette.exceptions import HTTPException
|
|
23
|
+
|
|
24
|
+
from lakana_core.errors import (
|
|
25
|
+
AlreadyExistsError,
|
|
26
|
+
AuthenticationError,
|
|
27
|
+
ConflictError,
|
|
28
|
+
KeptAfterCheckError,
|
|
29
|
+
LakanaError,
|
|
30
|
+
NotFoundError,
|
|
31
|
+
NotPublishedError,
|
|
32
|
+
PermissionDenied,
|
|
33
|
+
ValidationError,
|
|
34
|
+
)
|
|
35
|
+
from lakana_server.api.routing import AuthenticateFirstRoute
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
"""Logger for exceptions outside the caught vocabulary.
|
|
39
|
+
|
|
40
|
+
The terminal `except Exception` branch reshapes an unmodeled exception into
|
|
41
|
+
an opaque client response and must not let it propagate further, so this is
|
|
42
|
+
the only remaining way its traceback reaches the server side to be
|
|
43
|
+
correlated with the client-visible `type` (see `_OPAQUE_MESSAGE`).
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_STATUS_CODES: dict[type[Exception], int] = {
|
|
48
|
+
AuthenticationError: 401,
|
|
49
|
+
NotFoundError: 404,
|
|
50
|
+
NotPublishedError: 404,
|
|
51
|
+
KeptAfterCheckError: 404,
|
|
52
|
+
AlreadyExistsError: 409,
|
|
53
|
+
ConflictError: 409,
|
|
54
|
+
ValidationError: 422,
|
|
55
|
+
PermissionDenied: 403,
|
|
56
|
+
pydantic.ValidationError: 422,
|
|
57
|
+
RequestValidationError: 422,
|
|
58
|
+
}
|
|
59
|
+
"""Maps each caught exception type to its HTTP status code.
|
|
60
|
+
|
|
61
|
+
`NotPublishedError` is a 404 here, not the 409 of the platform API: an engine reads the
|
|
62
|
+
Iceberg REST catalog's 404 as "no such table", which is what a table with no published
|
|
63
|
+
snapshot is to it, and the message says why. `KeptAfterCheckError` (a task refused a table
|
|
64
|
+
that was kept after a failed check) is a 404 for the same reason: to the task's engine
|
|
65
|
+
the table is not there to read. The lookup is by exact type, so a subclass of
|
|
66
|
+
a mapped error needs its own entry.
|
|
67
|
+
|
|
68
|
+
`pydantic.ValidationError` is listed explicitly and is distinct from
|
|
69
|
+
`lakana_core.errors.ValidationError`, which is also in this table under its
|
|
70
|
+
own name — two different classes named `ValidationError` (a third,
|
|
71
|
+
`pyiceberg.exceptions.ValidationError`, is unrelated and not caught here).
|
|
72
|
+
`RequestValidationError` is a fourth, separate class: FastAPI raises it when
|
|
73
|
+
a request body fails to parse against its Pydantic model, before an
|
|
74
|
+
endpoint's own body ever runs — distinct from a `pydantic.ValidationError`
|
|
75
|
+
raised inside a handler doing its own parsing (for example `iceberg_json`).
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_OPAQUE_MESSAGE = "Internal server error"
|
|
80
|
+
"""Stands in for the message of an exception outside the caught vocabulary.
|
|
81
|
+
|
|
82
|
+
An error Lakana did not model is by definition one whose text was never
|
|
83
|
+
written for an external reader: it may name an internal path, a SQL
|
|
84
|
+
fragment, or a third-party library's internals. The exception's class name
|
|
85
|
+
is still reported in `type`, which is enough to correlate a client-side
|
|
86
|
+
report with the server-side traceback without spilling the message itself.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _error_response(exc: Exception, status_code: int, message: str) -> JSONResponse:
|
|
91
|
+
"""Build the Iceberg REST specification's error body.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
exc
|
|
96
|
+
The caught exception, whose class name becomes the body's `type`.
|
|
97
|
+
status_code
|
|
98
|
+
The HTTP status to answer with, echoed into the body's `code` as
|
|
99
|
+
the specification requires.
|
|
100
|
+
message
|
|
101
|
+
The human-readable message for the body.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
JSONResponse
|
|
106
|
+
A response carrying `{"error": {"message", "type", "code"}}`.
|
|
107
|
+
"""
|
|
108
|
+
return JSONResponse(
|
|
109
|
+
status_code=status_code,
|
|
110
|
+
content={"error": {"message": message, "type": type(exc).__name__, "code": status_code}},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _error_message(exc: Exception) -> str:
|
|
115
|
+
"""Render a caught exception's message for the Iceberg error body.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
exc
|
|
120
|
+
The caught exception.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
str
|
|
125
|
+
For a `RequestValidationError`, each error's dotted location and
|
|
126
|
+
message joined together — `str(exc)` on this class defaults to a
|
|
127
|
+
vague ``"N validation errors for Request"`` with no detail on what
|
|
128
|
+
was actually wrong. For every other caught exception, `str(exc)`.
|
|
129
|
+
"""
|
|
130
|
+
if isinstance(exc, RequestValidationError):
|
|
131
|
+
return "; ".join(
|
|
132
|
+
f"{'.'.join(str(part) for part in error['loc'])}: {error['msg']}"
|
|
133
|
+
for error in exc.errors()
|
|
134
|
+
)
|
|
135
|
+
return str(exc)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class IcebergRoute(AuthenticateFirstRoute):
|
|
139
|
+
"""An APIRoute that reshapes errors into the Iceberg REST error body.
|
|
140
|
+
|
|
141
|
+
Used as `route_class` for the Iceberg router only (`router.py`). The
|
|
142
|
+
platform API keeps FastAPI's default route class and relies on
|
|
143
|
+
`install_exception_handlers` instead.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:
|
|
147
|
+
"""Wrap the original handler with Iceberg-shaped error handling.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
Callable
|
|
152
|
+
A handler that catches `LakanaError`, `pydantic.ValidationError`,
|
|
153
|
+
and `RequestValidationError` — raised either inside a dependency
|
|
154
|
+
or inside the endpoint body, since both run inside the call this
|
|
155
|
+
wraps — and returns the spec-mandated JSON shape instead of
|
|
156
|
+
letting them propagate to FastAPI's default handling. A
|
|
157
|
+
`starlette.exceptions.HTTPException` raised by FastAPI's own
|
|
158
|
+
request-handling machinery (for example an invalid-UTF-8 body)
|
|
159
|
+
is reshaped into the same JSON body but keeps its own
|
|
160
|
+
`status_code` and `detail` rather than being folded into the
|
|
161
|
+
terminal 500 — it is a genuine client error, not an unmodeled
|
|
162
|
+
one. A terminal `except Exception` catches everything else as a
|
|
163
|
+
500 in the same shape, which is what the design's mapping
|
|
164
|
+
table means by its last row, "uncaught `LakanaError` / anything
|
|
165
|
+
else". Without it a stray `KeyError` or third-party exception
|
|
166
|
+
reaches Starlette's default handler and the client gets a
|
|
167
|
+
plain-text body on the one surface whose error shape is
|
|
168
|
+
externally specified. That branch also logs the exception,
|
|
169
|
+
traceback included, via `logger.exception` before responding —
|
|
170
|
+
otherwise nothing outside this process would ever record that
|
|
171
|
+
an unmodeled error happened, and `_OPAQUE_MESSAGE`'s promise
|
|
172
|
+
that the client-visible `type` can be correlated with a
|
|
173
|
+
server-side traceback would have no traceback to correlate
|
|
174
|
+
with. `KeyboardInterrupt` and `SystemExit` derive from
|
|
175
|
+
`BaseException`, not `Exception`, and so still propagate.
|
|
176
|
+
"""
|
|
177
|
+
original_route_handler = super().get_route_handler()
|
|
178
|
+
|
|
179
|
+
async def custom_route_handler(request: Request) -> Response:
|
|
180
|
+
"""Call the original handler, reshaping any caught error.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
request
|
|
185
|
+
The incoming request.
|
|
186
|
+
|
|
187
|
+
Returns
|
|
188
|
+
-------
|
|
189
|
+
Response
|
|
190
|
+
The original response on success, or a spec-shaped
|
|
191
|
+
`JSONResponse` on a caught error.
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
return await original_route_handler(request)
|
|
195
|
+
except (LakanaError, pydantic.ValidationError, RequestValidationError) as exc:
|
|
196
|
+
return _error_response(exc, _STATUS_CODES.get(type(exc), 500), _error_message(exc))
|
|
197
|
+
except HTTPException as exc:
|
|
198
|
+
return _error_response(exc, exc.status_code, str(exc.detail))
|
|
199
|
+
except Exception as exc:
|
|
200
|
+
logger.exception("Unhandled exception in Iceberg REST route: %s", exc)
|
|
201
|
+
return _error_response(exc, 500, _OPAQUE_MESSAGE)
|
|
202
|
+
|
|
203
|
+
return custom_route_handler
|