semblance-databricks 0.1.0__py3-none-any.whl
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.
- semblance_databricks/__init__.py +19 -0
- semblance_databricks/app.py +104 -0
- semblance_databricks/auth.py +145 -0
- semblance_databricks/cli.py +112 -0
- semblance_databricks/compatibility.py +30 -0
- semblance_databricks/compatibility.yaml +269 -0
- semblance_databricks/config.py +52 -0
- semblance_databricks/errors.py +72 -0
- semblance_databricks/fixtures/__init__.py +5 -0
- semblance_databricks/fixtures/defaults/__init__.py +0 -0
- semblance_databricks/fixtures/defaults/acme.yaml +47 -0
- semblance_databricks/fixtures/loaders.py +196 -0
- semblance_databricks/http.py +34 -0
- semblance_databricks/ids.py +88 -0
- semblance_databricks/models/__init__.py +1 -0
- semblance_databricks/pagination.py +48 -0
- semblance_databricks/py.typed +0 -0
- semblance_databricks/registry.py +29 -0
- semblance_databricks/services/__init__.py +1 -0
- semblance_databricks/services/clusters/__init__.py +3 -0
- semblance_databricks/services/clusters/routes.py +191 -0
- semblance_databricks/services/dbfs/__init__.py +3 -0
- semblance_databricks/services/dbfs/routes.py +126 -0
- semblance_databricks/services/dbsql/__init__.py +3 -0
- semblance_databricks/services/dbsql/routes.py +163 -0
- semblance_databricks/services/deps.py +37 -0
- semblance_databricks/services/jobs/__init__.py +3 -0
- semblance_databricks/services/jobs/routes.py +168 -0
- semblance_databricks/services/permissions/__init__.py +5 -0
- semblance_databricks/services/permissions/routes.py +50 -0
- semblance_databricks/services/secrets/__init__.py +3 -0
- semblance_databricks/services/secrets/routes.py +53 -0
- semblance_databricks/services/workspace/__init__.py +3 -0
- semblance_databricks/services/workspace/routes.py +39 -0
- semblance_databricks/state.py +253 -0
- semblance_databricks/testing.py +66 -0
- semblance_databricks-0.1.0.dist-info/METADATA +126 -0
- semblance_databricks-0.1.0.dist-info/RECORD +42 -0
- semblance_databricks-0.1.0.dist-info/WHEEL +5 -0
- semblance_databricks-0.1.0.dist-info/entry_points.txt +2 -0
- semblance_databricks-0.1.0.dist-info/licenses/LICENSE.md +21 -0
- semblance_databricks-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Clusters list/get/create/edit/delete/restart/events/libraries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Request
|
|
8
|
+
|
|
9
|
+
from semblance_databricks.errors import DatabricksError
|
|
10
|
+
from semblance_databricks.http import json_object, require_int
|
|
11
|
+
from semblance_databricks.pagination import paginate
|
|
12
|
+
from semblance_databricks.services.deps import (
|
|
13
|
+
mock_from,
|
|
14
|
+
require_fail_stage,
|
|
15
|
+
stub_unimplemented,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_clusters_router() -> APIRouter:
|
|
20
|
+
router = APIRouter()
|
|
21
|
+
|
|
22
|
+
def _list(
|
|
23
|
+
request: Request, page_size: int | None, page_token: str | None
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
mock = mock_from(request)
|
|
26
|
+
mock.state.maybe_tick_real(mock.config.clock)
|
|
27
|
+
items = [
|
|
28
|
+
mock.state.cluster_json(c)
|
|
29
|
+
for c in sorted(mock.state.clusters.values(), key=lambda r: r.cluster_id)
|
|
30
|
+
if not c.deleted
|
|
31
|
+
]
|
|
32
|
+
page, nxt = paginate(
|
|
33
|
+
items,
|
|
34
|
+
page_size=page_size,
|
|
35
|
+
page_token=page_token,
|
|
36
|
+
resource="clusters",
|
|
37
|
+
codec=mock.page_token_codec,
|
|
38
|
+
revision=mock.state.revision,
|
|
39
|
+
default_page_size=mock.config.default_page_size,
|
|
40
|
+
max_page_size=mock.config.max_page_size,
|
|
41
|
+
)
|
|
42
|
+
body: dict[str, Any] = {"clusters": page}
|
|
43
|
+
if nxt:
|
|
44
|
+
body["next_page_token"] = nxt
|
|
45
|
+
return body
|
|
46
|
+
|
|
47
|
+
def _get(request: Request, cluster_id: str | None) -> dict[str, Any]:
|
|
48
|
+
mock = mock_from(request)
|
|
49
|
+
mock.state.maybe_tick_real(mock.config.clock)
|
|
50
|
+
if not cluster_id:
|
|
51
|
+
raise DatabricksError(400, "INVALID_PARAMETER_VALUE", "cluster_id required")
|
|
52
|
+
rec = mock.state.clusters.get(cluster_id)
|
|
53
|
+
if rec is None or rec.deleted:
|
|
54
|
+
raise DatabricksError(
|
|
55
|
+
404, "RESOURCE_DOES_NOT_EXIST", f"Cluster {cluster_id} not found"
|
|
56
|
+
)
|
|
57
|
+
return mock.state.cluster_json(rec)
|
|
58
|
+
|
|
59
|
+
@router.get("/api/2.1/clusters/list")
|
|
60
|
+
def list_21(
|
|
61
|
+
request: Request,
|
|
62
|
+
page_size: int | None = None,
|
|
63
|
+
page_token: str | None = None,
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
return _list(request, page_size, page_token)
|
|
66
|
+
|
|
67
|
+
@router.get("/api/2.1/clusters/get")
|
|
68
|
+
def get_21(request: Request, cluster_id: str | None = None) -> dict[str, Any]:
|
|
69
|
+
return _get(request, cluster_id)
|
|
70
|
+
|
|
71
|
+
@router.get("/api/2.0/clusters/get")
|
|
72
|
+
def get_20(request: Request, cluster_id: str | None = None) -> dict[str, Any]:
|
|
73
|
+
return _get(request, cluster_id)
|
|
74
|
+
|
|
75
|
+
@router.get("/api/2.0/clusters/list")
|
|
76
|
+
def list_20(
|
|
77
|
+
request: Request,
|
|
78
|
+
page_size: int | None = None,
|
|
79
|
+
page_token: str | None = None,
|
|
80
|
+
) -> dict[str, Any]:
|
|
81
|
+
return _list(request, page_size, page_token)
|
|
82
|
+
|
|
83
|
+
@router.post("/api/2.1/clusters/create")
|
|
84
|
+
@router.post("/api/2.0/clusters/create")
|
|
85
|
+
async def create(request: Request) -> dict[str, Any]:
|
|
86
|
+
require_fail_stage(request, "before_validate")
|
|
87
|
+
mock = mock_from(request)
|
|
88
|
+
body = await json_object(request)
|
|
89
|
+
require_fail_stage(request, "before_write")
|
|
90
|
+
rec = mock.state.add_cluster(
|
|
91
|
+
str(body.get("cluster_name", "cluster")),
|
|
92
|
+
spark_version=str(body.get("spark_version", "13.3.x-scala2.12")),
|
|
93
|
+
node_type_id=str(body.get("node_type_id", "i3.xlarge")),
|
|
94
|
+
state="PENDING",
|
|
95
|
+
ticks_remaining=require_int(
|
|
96
|
+
body.get("startup_delay_ticks"), 1, "startup_delay_ticks"
|
|
97
|
+
),
|
|
98
|
+
fail_after=bool(body.get("startup_failure", False)),
|
|
99
|
+
num_workers=require_int(body.get("num_workers"), 1, "num_workers"),
|
|
100
|
+
)
|
|
101
|
+
require_fail_stage(request, "after_write")
|
|
102
|
+
return {"cluster_id": rec.cluster_id}
|
|
103
|
+
|
|
104
|
+
@router.post("/api/2.1/clusters/edit")
|
|
105
|
+
@router.post("/api/2.0/clusters/edit")
|
|
106
|
+
async def edit(request: Request) -> dict[str, Any]:
|
|
107
|
+
mock = mock_from(request)
|
|
108
|
+
body = await json_object(request)
|
|
109
|
+
cid = str(body.get("cluster_id", ""))
|
|
110
|
+
rec = mock.state.clusters.get(cid)
|
|
111
|
+
if rec is None or rec.deleted:
|
|
112
|
+
raise DatabricksError(404, "RESOURCE_DOES_NOT_EXIST", "Cluster not found")
|
|
113
|
+
if "cluster_name" in body:
|
|
114
|
+
rec.cluster_name = str(body["cluster_name"])
|
|
115
|
+
if "num_workers" in body:
|
|
116
|
+
rec.num_workers = require_int(body["num_workers"], 1, "num_workers")
|
|
117
|
+
mock.state.bump()
|
|
118
|
+
return {}
|
|
119
|
+
|
|
120
|
+
@router.post("/api/2.1/clusters/delete")
|
|
121
|
+
@router.post("/api/2.0/clusters/delete")
|
|
122
|
+
async def delete(request: Request) -> dict[str, Any]:
|
|
123
|
+
mock = mock_from(request)
|
|
124
|
+
body = await json_object(request)
|
|
125
|
+
cid = str(body.get("cluster_id", ""))
|
|
126
|
+
rec = mock.state.clusters.get(cid)
|
|
127
|
+
if rec is None or rec.deleted:
|
|
128
|
+
raise DatabricksError(404, "RESOURCE_DOES_NOT_EXIST", "Cluster not found")
|
|
129
|
+
rec.state = "TERMINATING"
|
|
130
|
+
mock.state.bump()
|
|
131
|
+
return {}
|
|
132
|
+
|
|
133
|
+
@router.post("/api/2.1/clusters/restart")
|
|
134
|
+
@router.post("/api/2.0/clusters/restart")
|
|
135
|
+
async def restart(request: Request) -> dict[str, Any]:
|
|
136
|
+
mock = mock_from(request)
|
|
137
|
+
body = await json_object(request)
|
|
138
|
+
cid = str(body.get("cluster_id", ""))
|
|
139
|
+
rec = mock.state.clusters.get(cid)
|
|
140
|
+
if rec is None or rec.deleted:
|
|
141
|
+
raise DatabricksError(404, "RESOURCE_DOES_NOT_EXIST", "Cluster not found")
|
|
142
|
+
rec.state = "RESTARTING"
|
|
143
|
+
rec.ticks_remaining = 1
|
|
144
|
+
rec.fail_after = False
|
|
145
|
+
mock.state.bump()
|
|
146
|
+
return {}
|
|
147
|
+
|
|
148
|
+
@router.post("/api/2.1/clusters/permanent-delete")
|
|
149
|
+
@router.post("/api/2.0/clusters/permanent-delete")
|
|
150
|
+
async def permanent_delete(request: Request) -> dict[str, Any]:
|
|
151
|
+
stub_unimplemented(request, "clusters/permanent-delete")
|
|
152
|
+
return {}
|
|
153
|
+
|
|
154
|
+
@router.post("/api/2.1/clusters/events")
|
|
155
|
+
@router.post("/api/2.0/clusters/events")
|
|
156
|
+
async def events(request: Request) -> dict[str, Any]:
|
|
157
|
+
mock = mock_from(request)
|
|
158
|
+
body = await json_object(request)
|
|
159
|
+
cid = str(body.get("cluster_id", ""))
|
|
160
|
+
rec = mock.state.clusters.get(cid)
|
|
161
|
+
if rec is None or rec.deleted:
|
|
162
|
+
raise DatabricksError(404, "RESOURCE_DOES_NOT_EXIST", "Cluster not found")
|
|
163
|
+
return {"events": rec.events, "total_count": len(rec.events)}
|
|
164
|
+
|
|
165
|
+
@router.post("/api/2.0/libraries/install")
|
|
166
|
+
async def install(request: Request) -> dict[str, Any]:
|
|
167
|
+
mock = mock_from(request)
|
|
168
|
+
body = await json_object(request)
|
|
169
|
+
cid = str(body.get("cluster_id", ""))
|
|
170
|
+
rec = mock.state.clusters.get(cid)
|
|
171
|
+
if rec is None or rec.deleted:
|
|
172
|
+
raise DatabricksError(404, "RESOURCE_DOES_NOT_EXIST", "Cluster not found")
|
|
173
|
+
rec.libraries.extend(list(body.get("libraries") or []))
|
|
174
|
+
return {}
|
|
175
|
+
|
|
176
|
+
@router.post("/api/2.0/libraries/uninstall")
|
|
177
|
+
async def uninstall(request: Request) -> dict[str, Any]:
|
|
178
|
+
mock = mock_from(request)
|
|
179
|
+
body = await json_object(request)
|
|
180
|
+
cid = str(body.get("cluster_id", ""))
|
|
181
|
+
rec = mock.state.clusters.get(cid)
|
|
182
|
+
if rec is None or rec.deleted:
|
|
183
|
+
raise DatabricksError(404, "RESOURCE_DOES_NOT_EXIST", "Cluster not found")
|
|
184
|
+
requested = list(body.get("libraries") or [])
|
|
185
|
+
if requested:
|
|
186
|
+
rec.libraries = [lib for lib in rec.libraries if lib not in requested]
|
|
187
|
+
else:
|
|
188
|
+
rec.libraries = []
|
|
189
|
+
return {}
|
|
190
|
+
|
|
191
|
+
return router
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""DBFS list/read, put, and add-block stub."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, Request
|
|
10
|
+
|
|
11
|
+
from semblance_databricks.errors import DatabricksError
|
|
12
|
+
from semblance_databricks.http import json_object
|
|
13
|
+
from semblance_databricks.services.deps import mock_from, stub_unimplemented
|
|
14
|
+
from semblance_databricks.state import DbfsNode
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _normalize(path: str) -> str:
|
|
18
|
+
raw = path.replace("\\", "/")
|
|
19
|
+
if not raw.startswith("/"):
|
|
20
|
+
raw = "/" + raw
|
|
21
|
+
parts: list[str] = []
|
|
22
|
+
for seg in raw.split("/"):
|
|
23
|
+
if seg in {"", "."}:
|
|
24
|
+
continue
|
|
25
|
+
if seg == ".." or ":" in seg:
|
|
26
|
+
raise DatabricksError(400, "INVALID_PARAMETER_VALUE", "Invalid path")
|
|
27
|
+
parts.append(seg)
|
|
28
|
+
return "/" + "/".join(parts) if parts else "/"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parent(path: str) -> str:
|
|
32
|
+
if path in {"", "/"}:
|
|
33
|
+
return "/"
|
|
34
|
+
stripped = path.rstrip("/")
|
|
35
|
+
idx = stripped.rfind("/")
|
|
36
|
+
return stripped[:idx] or "/"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _write_temp(root: str, dbfs_path: str, data: bytes) -> None:
|
|
40
|
+
base = Path(root).resolve()
|
|
41
|
+
rel = dbfs_path.lstrip("/")
|
|
42
|
+
dest = base.joinpath(*rel.split("/")).resolve()
|
|
43
|
+
try:
|
|
44
|
+
dest.relative_to(base)
|
|
45
|
+
except ValueError as exc:
|
|
46
|
+
raise DatabricksError(400, "INVALID_PARAMETER_VALUE", "Invalid path") from exc
|
|
47
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
dest.write_bytes(data)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def create_dbfs_router() -> APIRouter:
|
|
52
|
+
router = APIRouter()
|
|
53
|
+
|
|
54
|
+
@router.get("/api/2.0/dbfs/list")
|
|
55
|
+
def list_dbfs(request: Request, path: str | None = None) -> dict[str, Any]:
|
|
56
|
+
mock = mock_from(request)
|
|
57
|
+
if not path:
|
|
58
|
+
raise DatabricksError(400, "INVALID_PARAMETER_VALUE", "path required")
|
|
59
|
+
prefix = _normalize(path)
|
|
60
|
+
files: list[dict[str, Any]] = []
|
|
61
|
+
for node in mock.state.dbfs.values():
|
|
62
|
+
parent = _parent(node.path)
|
|
63
|
+
if parent == prefix:
|
|
64
|
+
if node.path == prefix:
|
|
65
|
+
continue
|
|
66
|
+
files.append(
|
|
67
|
+
{
|
|
68
|
+
"path": node.path,
|
|
69
|
+
"is_dir": node.is_dir,
|
|
70
|
+
"file_size": 0 if node.is_dir else len(node.data),
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
if prefix not in mock.state.dbfs and not files:
|
|
74
|
+
raise DatabricksError(404, "RESOURCE_DOES_NOT_EXIST", "Path not found")
|
|
75
|
+
return {"files": files}
|
|
76
|
+
|
|
77
|
+
@router.get("/api/2.0/dbfs/read")
|
|
78
|
+
def read_dbfs(
|
|
79
|
+
request: Request,
|
|
80
|
+
path: str | None = None,
|
|
81
|
+
offset: int = 0,
|
|
82
|
+
length: int = 1048576,
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
mock = mock_from(request)
|
|
85
|
+
if not path:
|
|
86
|
+
raise DatabricksError(400, "INVALID_PARAMETER_VALUE", "path required")
|
|
87
|
+
node = mock.state.dbfs.get(_normalize(path))
|
|
88
|
+
if node is None or node.is_dir:
|
|
89
|
+
raise DatabricksError(404, "RESOURCE_DOES_NOT_EXIST", "File not found")
|
|
90
|
+
data = node.data[offset : offset + length]
|
|
91
|
+
return {
|
|
92
|
+
"bytes_read": len(data),
|
|
93
|
+
"data": base64.b64encode(data).decode(),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@router.post("/api/2.0/dbfs/add-block")
|
|
97
|
+
async def add_block(request: Request) -> dict[str, Any]:
|
|
98
|
+
stub_unimplemented(request, "dbfs/add-block")
|
|
99
|
+
return {}
|
|
100
|
+
|
|
101
|
+
@router.post("/api/2.0/dbfs/put")
|
|
102
|
+
async def put_file(request: Request) -> dict[str, Any]:
|
|
103
|
+
mock = mock_from(request)
|
|
104
|
+
body = await json_object(request)
|
|
105
|
+
path = _normalize(str(body.get("path", "")))
|
|
106
|
+
if not path or path == "/":
|
|
107
|
+
raise DatabricksError(400, "INVALID_PARAMETER_VALUE", "path required")
|
|
108
|
+
if len(mock.state.dbfs) >= mock.config.dbfs_max_files:
|
|
109
|
+
raise DatabricksError(400, "MAX_BLOCK_SIZE_EXCEEDED", "DBFS file cap")
|
|
110
|
+
raw = str(body.get("contents", ""))
|
|
111
|
+
try:
|
|
112
|
+
content = base64.b64decode(raw, validate=True)
|
|
113
|
+
except Exception as exc:
|
|
114
|
+
raise DatabricksError(
|
|
115
|
+
400, "INVALID_PARAMETER_VALUE", "contents must be base64"
|
|
116
|
+
) from exc
|
|
117
|
+
total = sum(len(n.data) for n in mock.state.dbfs.values()) + len(content)
|
|
118
|
+
if total > mock.config.dbfs_max_bytes:
|
|
119
|
+
raise DatabricksError(400, "MAX_BLOCK_SIZE_EXCEEDED", "DBFS size cap")
|
|
120
|
+
mock.state.dbfs[path] = DbfsNode(path=path, is_dir=False, data=content)
|
|
121
|
+
if mock.config.dbfs_temp_dir:
|
|
122
|
+
_write_temp(mock.config.dbfs_temp_dir, path, content)
|
|
123
|
+
mock.state.bump()
|
|
124
|
+
return {}
|
|
125
|
+
|
|
126
|
+
return router
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""SQL warehouses and statement execution (no Spark)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Request
|
|
8
|
+
|
|
9
|
+
from semblance_databricks.errors import DatabricksError
|
|
10
|
+
from semblance_databricks.http import json_object
|
|
11
|
+
from semblance_databricks.ids import make_id
|
|
12
|
+
from semblance_databricks.pagination import paginate
|
|
13
|
+
from semblance_databricks.services.deps import mock_from
|
|
14
|
+
from semblance_databricks.state import WarehouseRecord
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _statement_payload(statement_id: str, rec: dict[str, Any]) -> dict[str, Any]:
|
|
18
|
+
chunks: list[Any] = rec.get("chunks") or []
|
|
19
|
+
index = int(rec.get("chunk_index", 0))
|
|
20
|
+
chunk = chunks[index] if index < len(chunks) else []
|
|
21
|
+
next_chunk = None
|
|
22
|
+
if index + 1 < len(chunks):
|
|
23
|
+
next_chunk = index + 1
|
|
24
|
+
result: dict[str, Any] = {
|
|
25
|
+
"chunk_index": index,
|
|
26
|
+
"row_count": len(chunk) if isinstance(chunk, list) else 0,
|
|
27
|
+
"data_array": chunk,
|
|
28
|
+
}
|
|
29
|
+
if next_chunk is not None:
|
|
30
|
+
result["next_chunk_internal"] = next_chunk
|
|
31
|
+
return {
|
|
32
|
+
"statement_id": statement_id,
|
|
33
|
+
"status": {"state": rec.get("state", "SUCCEEDED")},
|
|
34
|
+
"manifest": {
|
|
35
|
+
"format": "JSON_ARRAY",
|
|
36
|
+
"total_chunk_count": len(chunks),
|
|
37
|
+
"total_row_count": sum(
|
|
38
|
+
len(c) if isinstance(c, list) else 0 for c in chunks
|
|
39
|
+
),
|
|
40
|
+
},
|
|
41
|
+
"result": result,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create_dbsql_router() -> APIRouter:
|
|
46
|
+
router = APIRouter()
|
|
47
|
+
|
|
48
|
+
@router.get("/api/2.0/sql/warehouses")
|
|
49
|
+
def list_warehouses(
|
|
50
|
+
request: Request,
|
|
51
|
+
page_size: int | None = None,
|
|
52
|
+
page_token: str | None = None,
|
|
53
|
+
) -> dict[str, Any]:
|
|
54
|
+
mock = mock_from(request)
|
|
55
|
+
items = [
|
|
56
|
+
{
|
|
57
|
+
"id": w.id,
|
|
58
|
+
"name": w.name,
|
|
59
|
+
"state": w.state,
|
|
60
|
+
"cluster_size": w.cluster_size,
|
|
61
|
+
}
|
|
62
|
+
for w in mock.state.warehouses.values()
|
|
63
|
+
]
|
|
64
|
+
page, nxt = paginate(
|
|
65
|
+
items,
|
|
66
|
+
page_size=page_size,
|
|
67
|
+
page_token=page_token,
|
|
68
|
+
resource="warehouses",
|
|
69
|
+
codec=mock.page_token_codec,
|
|
70
|
+
revision=mock.state.revision,
|
|
71
|
+
default_page_size=mock.config.default_page_size,
|
|
72
|
+
max_page_size=mock.config.max_page_size,
|
|
73
|
+
)
|
|
74
|
+
body: dict[str, Any] = {"warehouses": page}
|
|
75
|
+
if nxt:
|
|
76
|
+
body["next_page_token"] = nxt
|
|
77
|
+
return body
|
|
78
|
+
|
|
79
|
+
@router.post("/api/2.0/sql/warehouses")
|
|
80
|
+
async def create_warehouse(request: Request) -> dict[str, Any]:
|
|
81
|
+
mock = mock_from(request)
|
|
82
|
+
body = await json_object(request)
|
|
83
|
+
name = str(body.get("name", "warehouse"))
|
|
84
|
+
wid = make_id("warehouse", f"{name}:{mock.state.next_seq()}", mock.config.seed)
|
|
85
|
+
rec = WarehouseRecord(
|
|
86
|
+
id=wid,
|
|
87
|
+
name=name,
|
|
88
|
+
cluster_size=str(body.get("cluster_size", "X-Small")),
|
|
89
|
+
)
|
|
90
|
+
mock.state.warehouses[wid] = rec
|
|
91
|
+
mock.state.permissions[("warehouses", wid)] = {
|
|
92
|
+
"object_id": wid,
|
|
93
|
+
"object_type": "warehouses",
|
|
94
|
+
"access_control_list": [],
|
|
95
|
+
}
|
|
96
|
+
mock.state.bump()
|
|
97
|
+
return {"id": wid}
|
|
98
|
+
|
|
99
|
+
@router.get("/api/2.0/sql/warehouses/{warehouse_id}")
|
|
100
|
+
def get_warehouse(request: Request, warehouse_id: str) -> dict[str, Any]:
|
|
101
|
+
mock = mock_from(request)
|
|
102
|
+
rec = mock.state.warehouses.get(warehouse_id)
|
|
103
|
+
if rec is None:
|
|
104
|
+
raise DatabricksError(404, "RESOURCE_DOES_NOT_EXIST", "Warehouse not found")
|
|
105
|
+
return {
|
|
106
|
+
"id": rec.id,
|
|
107
|
+
"name": rec.name,
|
|
108
|
+
"state": rec.state,
|
|
109
|
+
"cluster_size": rec.cluster_size,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@router.delete("/api/2.0/sql/warehouses/{warehouse_id}")
|
|
113
|
+
def delete_warehouse(request: Request, warehouse_id: str) -> dict[str, Any]:
|
|
114
|
+
mock = mock_from(request)
|
|
115
|
+
if warehouse_id not in mock.state.warehouses:
|
|
116
|
+
raise DatabricksError(404, "RESOURCE_DOES_NOT_EXIST", "Warehouse not found")
|
|
117
|
+
del mock.state.warehouses[warehouse_id]
|
|
118
|
+
mock.state.bump()
|
|
119
|
+
return {}
|
|
120
|
+
|
|
121
|
+
@router.post("/api/2.0/sql/statements")
|
|
122
|
+
async def execute_statement(request: Request) -> dict[str, Any]:
|
|
123
|
+
mock = mock_from(request)
|
|
124
|
+
body = await json_object(request)
|
|
125
|
+
warehouse_id = str(body.get("warehouse_id") or "")
|
|
126
|
+
sql = str(body.get("statement", ""))
|
|
127
|
+
if not warehouse_id:
|
|
128
|
+
raise DatabricksError(
|
|
129
|
+
400, "INVALID_PARAMETER_VALUE", "warehouse_id required"
|
|
130
|
+
)
|
|
131
|
+
if warehouse_id not in mock.state.warehouses:
|
|
132
|
+
raise DatabricksError(404, "RESOURCE_DOES_NOT_EXIST", "Warehouse not found")
|
|
133
|
+
sid = make_id("statement", f"{sql}:{mock.state.next_seq()}", mock.config.seed)
|
|
134
|
+
callback = mock.state.statement_callbacks.get(
|
|
135
|
+
sql
|
|
136
|
+
) or mock.state.statement_callbacks.get(warehouse_id)
|
|
137
|
+
if callback is not None:
|
|
138
|
+
payload = callback(body, mock.state)
|
|
139
|
+
chunks = payload.get("chunks", payload.get("data_array", [["1"]]))
|
|
140
|
+
if chunks and not isinstance(chunks[0], list):
|
|
141
|
+
chunks = [chunks]
|
|
142
|
+
else:
|
|
143
|
+
chunks = [[["ok"]]]
|
|
144
|
+
rec = {"state": "SUCCEEDED", "chunks": chunks, "chunk_index": 0}
|
|
145
|
+
mock.state.statements[sid] = rec
|
|
146
|
+
mock.state.bump()
|
|
147
|
+
return _statement_payload(sid, rec)
|
|
148
|
+
|
|
149
|
+
@router.get("/api/2.0/sql/statements/{statement_id}")
|
|
150
|
+
def get_statement(
|
|
151
|
+
request: Request,
|
|
152
|
+
statement_id: str,
|
|
153
|
+
chunk_index: int | None = None,
|
|
154
|
+
) -> dict[str, Any]:
|
|
155
|
+
mock = mock_from(request)
|
|
156
|
+
rec = mock.state.statements.get(statement_id)
|
|
157
|
+
if rec is None:
|
|
158
|
+
raise DatabricksError(404, "RESOURCE_DOES_NOT_EXIST", "Statement not found")
|
|
159
|
+
if chunk_index is not None:
|
|
160
|
+
rec = {**rec, "chunk_index": chunk_index}
|
|
161
|
+
return _statement_payload(statement_id, rec)
|
|
162
|
+
|
|
163
|
+
return router
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Request helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol, cast
|
|
6
|
+
|
|
7
|
+
from fastapi import Request
|
|
8
|
+
|
|
9
|
+
from semblance_databricks.config import DatabricksMockConfig
|
|
10
|
+
from semblance_databricks.errors import DatabricksError
|
|
11
|
+
from semblance_databricks.ids import PageTokenCodec
|
|
12
|
+
from semblance_databricks.state import DatabricksState
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DatabricksMockLike(Protocol):
|
|
16
|
+
config: DatabricksMockConfig
|
|
17
|
+
state: DatabricksState
|
|
18
|
+
page_token_codec: PageTokenCodec
|
|
19
|
+
|
|
20
|
+
def tick(self) -> None: ...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def mock_from(request: Request) -> DatabricksMockLike:
|
|
24
|
+
return cast(DatabricksMockLike, request.app.state.databricks_mock)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def require_fail_stage(request: Request, stage: str) -> None:
|
|
28
|
+
mock = mock_from(request)
|
|
29
|
+
if mock.config.fail_stage == stage:
|
|
30
|
+
raise DatabricksError(500, "INTERNAL_ERROR", f"fail_stage={stage}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def stub_unimplemented(request: Request, name: str) -> None:
|
|
34
|
+
mock = mock_from(request)
|
|
35
|
+
if mock.config.auth == "strict":
|
|
36
|
+
raise DatabricksError(501, "FEATURE_DISABLED", f"{name} is not implemented")
|
|
37
|
+
raise DatabricksError(404, "RESOURCE_DOES_NOT_EXIST", f"{name} not found")
|