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.
Files changed (148) hide show
  1. lakana_server-0.12.0/.gitignore +25 -0
  2. lakana_server-0.12.0/PKG-INFO +39 -0
  3. lakana_server-0.12.0/README.md +9 -0
  4. lakana_server-0.12.0/hatch_build.py +39 -0
  5. lakana_server-0.12.0/pyproject.toml +73 -0
  6. lakana_server-0.12.0/src/lakana_server/__init__.py +1 -0
  7. lakana_server-0.12.0/src/lakana_server/__main__.py +56 -0
  8. lakana_server-0.12.0/src/lakana_server/api/__init__.py +1 -0
  9. lakana_server-0.12.0/src/lakana_server/api/iceberg/__init__.py +1 -0
  10. lakana_server-0.12.0/src/lakana_server/api/iceberg/encoding.py +60 -0
  11. lakana_server-0.12.0/src/lakana_server/api/iceberg/route.py +203 -0
  12. lakana_server-0.12.0/src/lakana_server/api/iceberg/router.py +531 -0
  13. lakana_server-0.12.0/src/lakana_server/api/iceberg/schemas.py +245 -0
  14. lakana_server-0.12.0/src/lakana_server/api/platform/__init__.py +1 -0
  15. lakana_server-0.12.0/src/lakana_server/api/platform/artifacts.py +1520 -0
  16. lakana_server-0.12.0/src/lakana_server/api/platform/audit_events.py +211 -0
  17. lakana_server-0.12.0/src/lakana_server/api/platform/column_masks.py +219 -0
  18. lakana_server-0.12.0/src/lakana_server/api/platform/deploys.py +358 -0
  19. lakana_server-0.12.0/src/lakana_server/api/platform/environments.py +212 -0
  20. lakana_server-0.12.0/src/lakana_server/api/platform/grants.py +222 -0
  21. lakana_server-0.12.0/src/lakana_server/api/platform/groups.py +180 -0
  22. lakana_server-0.12.0/src/lakana_server/api/platform/info.py +65 -0
  23. lakana_server-0.12.0/src/lakana_server/api/platform/jobs.py +289 -0
  24. lakana_server-0.12.0/src/lakana_server/api/platform/namespaces.py +177 -0
  25. lakana_server-0.12.0/src/lakana_server/api/platform/package_indexes.py +175 -0
  26. lakana_server-0.12.0/src/lakana_server/api/platform/principals.py +127 -0
  27. lakana_server-0.12.0/src/lakana_server/api/platform/queries.py +343 -0
  28. lakana_server-0.12.0/src/lakana_server/api/platform/repositories.py +180 -0
  29. lakana_server-0.12.0/src/lakana_server/api/platform/row_filters.py +212 -0
  30. lakana_server-0.12.0/src/lakana_server/api/platform/runs.py +471 -0
  31. lakana_server-0.12.0/src/lakana_server/api/platform/schemas.py +2589 -0
  32. lakana_server-0.12.0/src/lakana_server/api/platform/secrets.py +228 -0
  33. lakana_server-0.12.0/src/lakana_server/api/platform/tables.py +1304 -0
  34. lakana_server-0.12.0/src/lakana_server/api/platform/task_tokens.py +77 -0
  35. lakana_server-0.12.0/src/lakana_server/api/platform/tokens.py +107 -0
  36. lakana_server-0.12.0/src/lakana_server/api/platform/whoami.py +43 -0
  37. lakana_server-0.12.0/src/lakana_server/api/routing.py +75 -0
  38. lakana_server-0.12.0/src/lakana_server/app.py +228 -0
  39. lakana_server-0.12.0/src/lakana_server/auth_rate_limit.py +119 -0
  40. lakana_server-0.12.0/src/lakana_server/body_limit.py +228 -0
  41. lakana_server-0.12.0/src/lakana_server/concurrency.py +50 -0
  42. lakana_server-0.12.0/src/lakana_server/deps.py +759 -0
  43. lakana_server-0.12.0/src/lakana_server/errors.py +220 -0
  44. lakana_server-0.12.0/src/lakana_server/log_config.py +45 -0
  45. lakana_server-0.12.0/src/lakana_server/settings.py +194 -0
  46. lakana_server-0.12.0/tests/__init__.py +1 -0
  47. lakana_server-0.12.0/tests/api/__init__.py +1 -0
  48. lakana_server-0.12.0/tests/api/iceberg/__init__.py +1 -0
  49. lakana_server-0.12.0/tests/api/iceberg/test_commit_route_has_no_action.py +63 -0
  50. lakana_server-0.12.0/tests/api/iceberg/test_encoding.py +33 -0
  51. lakana_server-0.12.0/tests/api/iceberg/test_namespaces.py +394 -0
  52. lakana_server-0.12.0/tests/api/iceberg/test_route.py +255 -0
  53. lakana_server-0.12.0/tests/api/iceberg/test_tables.py +1105 -0
  54. lakana_server-0.12.0/tests/api/iceberg/test_views_hidden.py +32 -0
  55. lakana_server-0.12.0/tests/api/platform/__init__.py +1 -0
  56. lakana_server-0.12.0/tests/api/platform/test_artifacts_delete.py +708 -0
  57. lakana_server-0.12.0/tests/api/platform/test_artifacts_read.py +1241 -0
  58. lakana_server-0.12.0/tests/api/platform/test_artifacts_upload.py +1332 -0
  59. lakana_server-0.12.0/tests/api/platform/test_artifacts_uploads_list.py +183 -0
  60. lakana_server-0.12.0/tests/api/platform/test_audit_events.py +268 -0
  61. lakana_server-0.12.0/tests/api/platform/test_bound_task_tokens.py +675 -0
  62. lakana_server-0.12.0/tests/api/platform/test_column_masks.py +316 -0
  63. lakana_server-0.12.0/tests/api/platform/test_deployments_run_as.py +148 -0
  64. lakana_server-0.12.0/tests/api/platform/test_deploys.py +398 -0
  65. lakana_server-0.12.0/tests/api/platform/test_environments.py +486 -0
  66. lakana_server-0.12.0/tests/api/platform/test_grants.py +308 -0
  67. lakana_server-0.12.0/tests/api/platform/test_groups.py +157 -0
  68. lakana_server-0.12.0/tests/api/platform/test_info.py +63 -0
  69. lakana_server-0.12.0/tests/api/platform/test_jobs.py +340 -0
  70. lakana_server-0.12.0/tests/api/platform/test_lockdown_api.py +239 -0
  71. lakana_server-0.12.0/tests/api/platform/test_namespaces.py +116 -0
  72. lakana_server-0.12.0/tests/api/platform/test_package_indexes.py +210 -0
  73. lakana_server-0.12.0/tests/api/platform/test_principals.py +74 -0
  74. lakana_server-0.12.0/tests/api/platform/test_queries.py +1157 -0
  75. lakana_server-0.12.0/tests/api/platform/test_queries_writes.py +209 -0
  76. lakana_server-0.12.0/tests/api/platform/test_repositories.py +254 -0
  77. lakana_server-0.12.0/tests/api/platform/test_row_filters.py +149 -0
  78. lakana_server-0.12.0/tests/api/platform/test_runs.py +685 -0
  79. lakana_server-0.12.0/tests/api/platform/test_secrets.py +289 -0
  80. lakana_server-0.12.0/tests/api/platform/test_tables.py +692 -0
  81. lakana_server-0.12.0/tests/api/platform/test_tables_append.py +1704 -0
  82. lakana_server-0.12.0/tests/api/platform/test_tables_lineage.py +100 -0
  83. lakana_server-0.12.0/tests/api/platform/test_tables_maintenance.py +351 -0
  84. lakana_server-0.12.0/tests/api/platform/test_tables_replace.py +594 -0
  85. lakana_server-0.12.0/tests/api/platform/test_task_tokens.py +512 -0
  86. lakana_server-0.12.0/tests/api/platform/test_tokens.py +226 -0
  87. lakana_server-0.12.0/tests/api/platform/test_whoami.py +44 -0
  88. lakana_server-0.12.0/tests/api/test_schemas_warnings.py +30 -0
  89. lakana_server-0.12.0/tests/cli/__init__.py +1 -0
  90. lakana_server-0.12.0/tests/cli/conftest.py +163 -0
  91. lakana_server-0.12.0/tests/cli/test_cli_deploy.py +158 -0
  92. lakana_server-0.12.0/tests/cli/test_cli_end_to_end.py +317 -0
  93. lakana_server-0.12.0/tests/cli/test_cli_integration.py +191 -0
  94. lakana_server-0.12.0/tests/cli/test_column_masks_howto.py +143 -0
  95. lakana_server-0.12.0/tests/cli/test_local_mode_integration.py +280 -0
  96. lakana_server-0.12.0/tests/cli/test_row_filters_howto.py +166 -0
  97. lakana_server-0.12.0/tests/conftest.py +173 -0
  98. lakana_server-0.12.0/tests/integration_helpers.py +417 -0
  99. lakana_server-0.12.0/tests/python_asset_helpers.py +684 -0
  100. lakana_server-0.12.0/tests/sdk/__init__.py +1 -0
  101. lakana_server-0.12.0/tests/sdk/conftest.py +34 -0
  102. lakana_server-0.12.0/tests/sdk/test_sdk_admin.py +173 -0
  103. lakana_server-0.12.0/tests/sdk/test_sdk_artifacts.py +243 -0
  104. lakana_server-0.12.0/tests/sdk/test_sdk_break_glass.py +50 -0
  105. lakana_server-0.12.0/tests/sdk/test_sdk_contract.py +72 -0
  106. lakana_server-0.12.0/tests/sdk/test_sdk_data_plane.py +428 -0
  107. lakana_server-0.12.0/tests/sdk/test_sdk_deploys.py +86 -0
  108. lakana_server-0.12.0/tests/sdk/test_sdk_integration.py +124 -0
  109. lakana_server-0.12.0/tests/sdk/test_sdk_package_indexes.py +58 -0
  110. lakana_server-0.12.0/tests/sdk/test_sdk_repositories.py +58 -0
  111. lakana_server-0.12.0/tests/sdk/test_sdk_runs.py +277 -0
  112. lakana_server-0.12.0/tests/server_test_helpers.py +288 -0
  113. lakana_server-0.12.0/tests/test_app.py +818 -0
  114. lakana_server-0.12.0/tests/test_artifacts_integration.py +1152 -0
  115. lakana_server-0.12.0/tests/test_auth_rate_limit.py +123 -0
  116. lakana_server-0.12.0/tests/test_authorization_lifecycle_integration.py +220 -0
  117. lakana_server-0.12.0/tests/test_body_limit.py +496 -0
  118. lakana_server-0.12.0/tests/test_bound_tokens_integration.py +584 -0
  119. lakana_server-0.12.0/tests/test_checks_races_integration.py +2894 -0
  120. lakana_server-0.12.0/tests/test_denial_audit_flood.py +136 -0
  121. lakana_server-0.12.0/tests/test_deps.py +403 -0
  122. lakana_server-0.12.0/tests/test_download_log.py +101 -0
  123. lakana_server-0.12.0/tests/test_errors.py +145 -0
  124. lakana_server-0.12.0/tests/test_hatch_build.py +58 -0
  125. lakana_server-0.12.0/tests/test_healthz.py +18 -0
  126. lakana_server-0.12.0/tests/test_iceberg_commit_answer_integration.py +267 -0
  127. lakana_server-0.12.0/tests/test_identity_lifecycle_integration.py +165 -0
  128. lakana_server-0.12.0/tests/test_jobs_integration.py +944 -0
  129. lakana_server-0.12.0/tests/test_jobs_schema.py +23 -0
  130. lakana_server-0.12.0/tests/test_lifecycle_integration.py +210 -0
  131. lakana_server-0.12.0/tests/test_lineage_visibility_integration.py +476 -0
  132. lakana_server-0.12.0/tests/test_log_config.py +85 -0
  133. lakana_server-0.12.0/tests/test_lost_worker_processes_integration.py +1058 -0
  134. lakana_server-0.12.0/tests/test_lost_workers_integration.py +1810 -0
  135. lakana_server-0.12.0/tests/test_main.py +96 -0
  136. lakana_server-0.12.0/tests/test_main_logging.py +139 -0
  137. lakana_server-0.12.0/tests/test_materialization_integration.py +404 -0
  138. lakana_server-0.12.0/tests/test_no_run_context_on_the_wire.py +211 -0
  139. lakana_server-0.12.0/tests/test_notifications_integration.py +578 -0
  140. lakana_server-0.12.0/tests/test_projects_deploy_integration.py +769 -0
  141. lakana_server-0.12.0/tests/test_python_assets_integration.py +2300 -0
  142. lakana_server-0.12.0/tests/test_query_process_integration.py +503 -0
  143. lakana_server-0.12.0/tests/test_run_engine_integration.py +3019 -0
  144. lakana_server-0.12.0/tests/test_running_cap_integration.py +468 -0
  145. lakana_server-0.12.0/tests/test_secrets_lifecycle_integration.py +204 -0
  146. lakana_server-0.12.0/tests/test_settings.py +84 -0
  147. lakana_server-0.12.0/tests/test_statement_limit.py +185 -0
  148. 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