clickzetta-studio-utils 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.
@@ -0,0 +1,5 @@
1
+ from clickzetta_studio_utils.dbutils import Dbutils, TaskValueNotFoundError, WidgetNotFoundError, init_dbutils
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["Dbutils", "TaskValueNotFoundError", "WidgetNotFoundError", "init_dbutils", "__version__"]
@@ -0,0 +1,11 @@
1
+ from clickzetta.connector.v0.connection import Connection
2
+ from clickzetta.connector.v0._fsutil import FsUtil
3
+
4
+
5
+ class _Utils:
6
+ def __init__(self, connection: Connection):
7
+ self._fs = FsUtil(connection)
8
+
9
+ @property
10
+ def fs(self) -> FsUtil:
11
+ return self._fs
@@ -0,0 +1,358 @@
1
+ from __future__ import annotations
2
+
3
+ import builtins
4
+ import json
5
+ import logging
6
+ import time
7
+ import urllib.error
8
+ import urllib.request
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+ LOGGER = logging.getLogger(__name__)
13
+
14
+ _DEFAULT_TIMEOUT_SECONDS = 3
15
+
16
+
17
+ class WidgetNotFoundError(KeyError):
18
+ pass
19
+
20
+
21
+ class TaskValueNotFoundError(KeyError):
22
+ pass
23
+
24
+
25
+ @dataclass
26
+ class DbutilsRuntimeConfig:
27
+ runtime: dict[str, Any] = field(default_factory=dict)
28
+ widget_values: dict[str, str] = field(default_factory=dict)
29
+ dbutils_api: dict[str, Any] = field(default_factory=dict)
30
+ task_values: dict[str, Any] = field(default_factory=dict)
31
+
32
+ @classmethod
33
+ def from_mapping(cls, config: dict[str, Any] | None) -> "DbutilsRuntimeConfig":
34
+ if isinstance(config, list):
35
+ config = {"connections": config}
36
+ config = config or {}
37
+ return cls(
38
+ runtime=dict(config.get("runtime") or {}),
39
+ widget_values=_string_map(config.get("widgetValues") or {}),
40
+ dbutils_api=dict(config.get("dbutilsApi") or {}),
41
+ task_values=dict(config.get("taskValues") or {}),
42
+ )
43
+
44
+
45
+ class _HttpClient:
46
+ def __init__(self, base_url: str | None, timeout_seconds: int = _DEFAULT_TIMEOUT_SECONDS):
47
+ self._base_url = (base_url or "").rstrip("/")
48
+ self._timeout_seconds = timeout_seconds
49
+
50
+ def post_json(self, path: str, payload: dict[str, Any]) -> Any:
51
+ if not self._base_url:
52
+ LOGGER.debug("Skip dbutils HTTP call because dbutilsApi.baseUrl is not configured")
53
+ return None
54
+ url = f"{self._base_url}{path}"
55
+ data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
56
+ request = urllib.request.Request(
57
+ url,
58
+ data=data,
59
+ headers={"Content-Type": "application/json"},
60
+ method="POST",
61
+ )
62
+ with urllib.request.urlopen(request, timeout=self._timeout_seconds) as response:
63
+ body = response.read()
64
+ if not body:
65
+ return None
66
+ return json.loads(body.decode("utf-8"))
67
+
68
+ def post_widget_event(self, payload: dict[str, Any]) -> None:
69
+ if not self._base_url:
70
+ LOGGER.debug("Skip widget event because dbutilsApi.baseUrl is not configured")
71
+ return
72
+ try:
73
+ self.post_json("/v1/dbutils/widgets/events", payload)
74
+ except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as exc:
75
+ LOGGER.warning("Failed to write dbutils widget event: %s", exc)
76
+
77
+ def set_task_value(self, payload: dict[str, Any]) -> None:
78
+ if not self._base_url:
79
+ LOGGER.debug("Skip task value set because dbutilsApi.baseUrl is not configured")
80
+ return
81
+ try:
82
+ self.post_json("/v1/dbutils/task-values/set", payload)
83
+ except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as exc:
84
+ LOGGER.warning("Failed to write dbutils task value: %s", exc)
85
+
86
+ def get_task_value(self, payload: dict[str, Any]) -> Any:
87
+ if not self._base_url:
88
+ return None
89
+ return self.post_json("/v1/dbutils/task-values/get", payload)
90
+
91
+
92
+ class Widgets:
93
+ def __init__(self, runtime_config: DbutilsRuntimeConfig):
94
+ self._runtime = runtime_config.runtime
95
+ self._values = dict(runtime_config.widget_values)
96
+ self._client = _HttpClient(runtime_config.dbutils_api.get("baseUrl"))
97
+ self._event_order = 0
98
+
99
+ def text(self, name: str, defaultValue: str = "", label: str | None = None) -> None:
100
+ self._register("text", name, defaultValue, None, label)
101
+
102
+ def dropdown(
103
+ self,
104
+ name: str,
105
+ defaultValue: str,
106
+ choices: list[str] | tuple[str, ...],
107
+ label: str | None = None,
108
+ ) -> None:
109
+ self._register("dropdown", name, defaultValue, choices, label)
110
+
111
+ def combobox(
112
+ self,
113
+ name: str,
114
+ defaultValue: str,
115
+ choices: list[str] | tuple[str, ...],
116
+ label: str | None = None,
117
+ ) -> None:
118
+ self._register("combobox", name, defaultValue, choices, label)
119
+
120
+ def multiselect(
121
+ self,
122
+ name: str,
123
+ defaultValue: str,
124
+ choices: list[str] | tuple[str, ...],
125
+ label: str | None = None,
126
+ ) -> None:
127
+ self._register("multiselect", name, defaultValue, choices, label)
128
+
129
+ def get(self, name: str) -> str:
130
+ if name not in self._values:
131
+ raise WidgetNotFoundError(f"Widget not found: {name}")
132
+ return self._values[name]
133
+
134
+ def getAll(self) -> dict[str, str]:
135
+ return dict(self._values)
136
+
137
+ def remove(self, name: str) -> None:
138
+ self._values.pop(name, None)
139
+ self._emit(
140
+ {
141
+ "eventType": "REMOVE",
142
+ "widgetKey": name,
143
+ }
144
+ )
145
+
146
+ def removeAll(self) -> None:
147
+ self._values.clear()
148
+ self._emit({"eventType": "REMOVE_ALL"})
149
+
150
+ def _register(
151
+ self,
152
+ widget_type: str,
153
+ name: str,
154
+ default_value: Any,
155
+ choices: list[str] | tuple[str, ...] | None,
156
+ label: str | None,
157
+ ) -> None:
158
+ key = _require_name(name)
159
+ default_str = _to_widget_value(default_value)
160
+ if key not in self._values:
161
+ self._values[key] = default_str
162
+ self._emit(
163
+ {
164
+ "eventType": "CREATE",
165
+ "widgetKey": key,
166
+ "widgetType": widget_type,
167
+ "label": label,
168
+ "defaultValue": default_str,
169
+ "choices": [_to_widget_value(choice) for choice in choices] if choices is not None else [],
170
+ }
171
+ )
172
+
173
+ def _emit(self, payload: dict[str, Any]) -> None:
174
+ self._event_order += 1
175
+ event = {
176
+ **self._runtime_fields(),
177
+ **payload,
178
+ "eventOrder": self._event_order,
179
+ "timestamp": int(time.time() * 1000),
180
+ }
181
+ self._client.post_widget_event(event)
182
+
183
+ def _runtime_fields(self) -> dict[str, Any]:
184
+ return {
185
+ "tenantId": self._runtime.get("tenantId"),
186
+ "projectId": self._runtime.get("projectId"),
187
+ "scheduleTaskId": self._runtime.get("scheduleTaskId"),
188
+ "scheduleInstanceId": self._runtime.get("scheduleInstanceId"),
189
+ "executeLogId": self._runtime.get("executeLogId"),
190
+ "paragraphId": self._runtime.get("paragraphId"),
191
+ }
192
+
193
+ def _update_runtime(self, values: dict[str, Any]) -> None:
194
+ self._runtime.update({key: value for key, value in values.items() if value is not None})
195
+
196
+
197
+ class Dbutils:
198
+ def __init__(self, runtime_config: DbutilsRuntimeConfig | dict[str, Any] | None = None):
199
+ if not isinstance(runtime_config, DbutilsRuntimeConfig):
200
+ runtime_config = DbutilsRuntimeConfig.from_mapping(runtime_config)
201
+ self.widgets = Widgets(runtime_config)
202
+ self.jobs = Jobs(runtime_config)
203
+
204
+ def set_runtime_context(self, **values: Any) -> None:
205
+ self.widgets._update_runtime(values)
206
+ self.jobs.taskValues._update_runtime(values)
207
+
208
+
209
+ class Jobs:
210
+ def __init__(self, runtime_config: DbutilsRuntimeConfig):
211
+ self.taskValues = TaskValues(runtime_config)
212
+
213
+
214
+ class TaskValues:
215
+ _NO_DEFAULT = object()
216
+
217
+ def __init__(self, runtime_config: DbutilsRuntimeConfig):
218
+ self._runtime = runtime_config.runtime
219
+ self._client = _HttpClient(runtime_config.dbutils_api.get("baseUrl"))
220
+ task_values_config = runtime_config.task_values
221
+ self._current_producer = dict(task_values_config.get("currentProducer") or {})
222
+ self._producers = dict(task_values_config.get("producers") or {})
223
+ self._local_values: dict[str, Any] = {}
224
+
225
+ def set(self, key: str | None = None, value: Any = None, **kwargs: Any) -> None:
226
+ if key is None:
227
+ key = kwargs.get("key")
228
+ if "value" in kwargs:
229
+ value = kwargs["value"]
230
+ value_key = _require_name(key)
231
+ value_json, value_type = _serialize_task_value(value)
232
+ self._local_values[value_key] = value
233
+ payload = {
234
+ **self._runtime_fields(),
235
+ "producerInstanceType": self._current_producer.get("producerInstanceType"),
236
+ "producerInstanceId": self._current_producer.get("producerInstanceId"),
237
+ "key": value_key,
238
+ "valueJson": value_json,
239
+ "valueType": value_type,
240
+ }
241
+ self._client.set_task_value(payload)
242
+
243
+ def get(
244
+ self,
245
+ taskKey: str | None = None,
246
+ key: str | None = None,
247
+ default: Any = _NO_DEFAULT,
248
+ **kwargs: Any,
249
+ ) -> Any:
250
+ if taskKey is None:
251
+ taskKey = kwargs.get("taskKey")
252
+ if key is None:
253
+ key = kwargs.get("key")
254
+ if key is None and taskKey is not None:
255
+ key = taskKey
256
+ taskKey = None
257
+ value_key = _require_name(key)
258
+ if taskKey is None or taskKey == self._current_producer.get("taskKey"):
259
+ return self._get_local(value_key, default)
260
+ producer = self._producers.get(str(taskKey))
261
+ if not producer:
262
+ return self._missing(value_key, default)
263
+ payload = {
264
+ "tenantId": self._runtime.get("tenantId"),
265
+ "producerInstanceType": producer.get("producerInstanceType"),
266
+ "producerInstanceId": producer.get("producerInstanceId"),
267
+ "key": value_key,
268
+ }
269
+ try:
270
+ response = self._client.get_task_value(payload)
271
+ except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as exc:
272
+ LOGGER.warning("Failed to read dbutils task value: %s", exc)
273
+ return self._missing(value_key, default)
274
+ data = _extract_common_result_data(response)
275
+ if not data:
276
+ return self._missing(value_key, default)
277
+ if isinstance(data, dict) and "valueJson" in data:
278
+ return json.loads(data["valueJson"])
279
+ return self._missing(value_key, default)
280
+
281
+ def _get_local(self, key: str, default: Any) -> Any:
282
+ if key in self._local_values:
283
+ return self._local_values[key]
284
+ return self._missing(key, default)
285
+
286
+ def _missing(self, key: str, default: Any) -> Any:
287
+ if default is not self._NO_DEFAULT:
288
+ return default
289
+ raise TaskValueNotFoundError(f"Task value not found: {key}")
290
+
291
+ def _runtime_fields(self) -> dict[str, Any]:
292
+ return {
293
+ "tenantId": self._runtime.get("tenantId"),
294
+ "projectId": self._runtime.get("projectId"),
295
+ "scheduleTaskId": self._runtime.get("scheduleTaskId"),
296
+ "scheduleInstanceId": self._runtime.get("scheduleInstanceId"),
297
+ "executeLogId": self._runtime.get("executeLogId"),
298
+ }
299
+
300
+ def _update_runtime(self, values: dict[str, Any]) -> None:
301
+ self._runtime.update({key: value for key, value in values.items() if value is not None})
302
+
303
+
304
+ def init_dbutils(config: dict[str, Any] | None = None, *, bind_builtins: bool = True) -> Dbutils:
305
+ existing = getattr(builtins, "dbutils", None)
306
+ if config is None and isinstance(existing, Dbutils):
307
+ if bind_builtins:
308
+ builtins.czutils = existing
309
+ return existing
310
+ runtime_config = DbutilsRuntimeConfig.from_mapping(config or {})
311
+ dbutils = Dbutils(runtime_config)
312
+ if bind_builtins:
313
+ builtins.dbutils = dbutils
314
+ builtins.czutils = dbutils
315
+ return dbutils
316
+
317
+
318
+ def _require_name(name: str) -> str:
319
+ if name is None or str(name).strip() == "":
320
+ raise ValueError("Widget name is required")
321
+ return str(name)
322
+
323
+
324
+ def _to_widget_value(value: Any) -> str:
325
+ if value is None:
326
+ return ""
327
+ return str(value)
328
+
329
+
330
+ def _string_map(values: dict[str, Any]) -> dict[str, str]:
331
+ return {str(key): _to_widget_value(value) for key, value in values.items()}
332
+
333
+
334
+ def _serialize_task_value(value: Any) -> tuple[str, str]:
335
+ value_json = json.dumps(value, ensure_ascii=False)
336
+ if len(value_json.encode("utf-8")) > 48 * 1024:
337
+ raise ValueError("Task value JSON size must be <= 48 KiB")
338
+ if value is None:
339
+ value_type = "NULL"
340
+ elif isinstance(value, bool):
341
+ value_type = "BOOLEAN"
342
+ elif isinstance(value, (int, float)):
343
+ value_type = "NUMBER"
344
+ elif isinstance(value, str):
345
+ value_type = "STRING"
346
+ else:
347
+ value_type = "JSON"
348
+ return value_json, value_type
349
+
350
+
351
+ def _extract_common_result_data(response: Any) -> Any:
352
+ if response is None:
353
+ return None
354
+ if not isinstance(response, dict):
355
+ return response
356
+ if "data" in response:
357
+ return response.get("data")
358
+ return response
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.1
2
+ Name: clickzetta-studio-utils
3
+ Version: 0.1.0
4
+ Summary: clickzetta-studio-utils
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: clickzetta-connector>=1.0.28
7
+ Description-Content-Type: text/markdown
8
+
9
+ # clickzetta-studio-utils
10
+
11
+ `clickzetta-studio-utils` is a Python utility package built on top of `clickzetta-connector`. It currently provides a lightweight wrapper around ClickZetta's `FsUtil`, making it convenient to work with Volume files from Studio or scripts.
12
+
13
+ ## Installation
14
+
15
+ This project requires Python `>=3.10`.
16
+
17
+ ```bash
18
+ pip install clickzetta-studio-utils
19
+ ```
20
+
21
+ For local development, use `uv` to install dependencies:
22
+
23
+ ```bash
24
+ uv sync --dev
25
+ ```
26
+
27
+ ## Basic Usage
28
+
29
+ ```python
30
+ from clickzetta.connector.v0.connection import connect
31
+ from clickzetta_studio_utils._utils import _Utils
32
+
33
+ conn = connect(
34
+ username="<USERNAME>",
35
+ password="<PASSWORD>",
36
+ service="<SERVICE>",
37
+ instance="<INSTANCE>",
38
+ workspace="<WORKSPACE>",
39
+ schema="<SCHEMA>",
40
+ vcluster="<VCLUSTER>",
41
+ )
42
+
43
+ try:
44
+ dbutils = _Utils(conn)
45
+
46
+ dbutils.fs.put("volume://<VOLUME>/data/1.txt", "hello clickzetta")
47
+ print(dbutils.fs.ls("volume://<VOLUME>/data/"))
48
+ print(dbutils.fs.head("volume://<VOLUME>/data/1.txt"))
49
+ dbutils.fs.rm("volume://<VOLUME>/data/1.txt")
50
+ finally:
51
+ conn.close()
52
+ ```
53
+
54
+ `dbutils.fs` returns a `clickzetta.connector.v0._fsutil.FsUtil` instance, so you can continue using the filesystem capabilities provided by `clickzetta-connector`.
55
+
56
+ ## Notebook `dbutils.widgets`
57
+
58
+ Notebook runtimes can initialize a Databricks-like `dbutils.widgets` object from scheduler-provided runtime config.
59
+
60
+ ```python
61
+ from clickzetta_studio_utils import init_dbutils
62
+
63
+ dbutils = init_dbutils(
64
+ {
65
+ "runtime": {
66
+ "tenantId": 1,
67
+ "projectId": 2,
68
+ "scheduleTaskId": 3,
69
+ "scheduleInstanceId": 4,
70
+ "executeLogId": 5,
71
+ },
72
+ "dbutilsApi": {
73
+ "baseUrl": "http://scheduler-worker:8080",
74
+ },
75
+ "widgetValues": {
76
+ "biz_date": "2026-07-27",
77
+ },
78
+ }
79
+ )
80
+
81
+ dbutils.widgets.text("biz_date", "2026-07-20", "Business Date")
82
+ print(dbutils.widgets.get("biz_date")) # keeps the runtime value: 2026-07-27
83
+ ```
84
+
85
+ Supported APIs:
86
+
87
+ ```python
88
+ dbutils.widgets.text(name, defaultValue="", label=None)
89
+ dbutils.widgets.dropdown(name, defaultValue, choices, label=None)
90
+ dbutils.widgets.combobox(name, defaultValue, choices, label=None)
91
+ dbutils.widgets.multiselect(name, defaultValue, choices, label=None)
92
+ dbutils.widgets.get(name)
93
+ dbutils.widgets.getAll()
94
+ dbutils.widgets.remove(name)
95
+ dbutils.widgets.removeAll()
96
+ ```
97
+
98
+ Widget declaration and removal events are posted to `POST /v1/dbutils/widgets/events` when `dbutilsApi.baseUrl` is configured. Event write failures are logged as warnings and do not change local widget behavior.
99
+
100
+ ## Notebook `dbutils.jobs.taskValues`
101
+
102
+ Notebook runtimes can use `dbutils.jobs.taskValues` to persist values for downstream task instances and to share values across cells in the current Python kernel.
103
+
104
+ ```python
105
+ dbutils.jobs.taskValues.set("row_count", 100)
106
+
107
+ print(dbutils.jobs.taskValues.get("row_count"))
108
+ print(dbutils.jobs.taskValues.get(taskKey="extract_orders", key="row_count", default=0))
109
+ ```
110
+
111
+ `set()` writes local in-memory state first, then posts serialized JSON to `POST /v1/dbutils/task-values/set` when `dbutilsApi.baseUrl` is configured. `get(taskKey=...)` reads upstream values through `POST /v1/dbutils/task-values/get` using scheduler-provided producer mappings.
112
+
113
+ ## Development
114
+
115
+ Common commands:
116
+
117
+ ```bash
118
+ # Check code style and lint issues
119
+ uv run ruff check .
120
+
121
+ # Format Python files
122
+ uv run ruff format .
123
+
124
+ # Run tests
125
+ uv run pytest
126
+
127
+ # Build source and wheel distributions
128
+ uv build
129
+ ```
130
+
131
+ ## Testing Notes
132
+
133
+ The test suite includes integration tests that connect to a real ClickZetta service and operate on temporary Volumes. Before running them, make sure that:
134
+
135
+ - Connection parameters are valid, and the account has permission to create and delete Volumes and read/write files.
136
+ - Personal, production, or long-lived credentials are not committed. Prefer environment variables or local untracked configuration when credentials are needed.
137
+ - You run targeted tests for the code you changed first, then run the full test suite when appropriate.
138
+
139
+ ## Project Information
140
+
141
+ - Package name: `clickzetta-studio-utils`
142
+ - Import module: `clickzetta_studio_utils`
143
+ - Runtime dependency: `clickzetta-connector>=1.0.25`
144
+ - Build backend: `pdm-backend`
@@ -0,0 +1,7 @@
1
+ clickzetta_studio_utils-0.1.0.dist-info/METADATA,sha256=MTZ2-X1YLNCIA-OpO0i7Nr9JU_f2Lo-UEOmo7tS2WvM,4473
2
+ clickzetta_studio_utils-0.1.0.dist-info/WHEEL,sha256=VP-D4TPS230sME9Z3vb3INXvo1yt0924YRm5AOsk_dE,90
3
+ clickzetta_studio_utils-0.1.0.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
+ clickzetta_studio_utils/__init__.py,sha256=qCqPbw5lrellgRQRKI6FLmUmmXJPPdpDxl167ZIdXSQ,237
5
+ clickzetta_studio_utils/_utils.py,sha256=UF_MyHPDuOX2kf0wV1s-67DkNvroIaLF541bA8WpYks,278
6
+ clickzetta_studio_utils/dbutils.py,sha256=JPX7m5wYbUS790F83lWQgiG8V-VvzHz7_S-MfrRb3Ao,12682
7
+ clickzetta_studio_utils-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: pdm-backend (2.4.9)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+
3
+ [gui_scripts]
4
+