hotdata-jupyter 0.2.1__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,62 @@
1
+ """Jupyter integration package for Hotdata runtime."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("hotdata-jupyter")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0+unknown"
9
+
10
+ from hotdata_runtime import (
11
+ DEFAULT_SCHEMA,
12
+ HotdataClient,
13
+ LoadManagedTableResult,
14
+ ManagedDatabase,
15
+ QueryResult,
16
+ WorkspaceSelection,
17
+ from_env,
18
+ resolve_workspace_selection,
19
+ workspace_health_lines,
20
+ )
21
+
22
+ from hotdata_jupyter.databases import (
23
+ ManagedDatabaseWriter,
24
+ create_managed_database,
25
+ display_managed_databases_panel,
26
+ load_managed_table_from_bytes,
27
+ managed_database_writer,
28
+ managed_databases_markdown,
29
+ )
30
+ from hotdata_jupyter.display import display_query_result, result_markdown
31
+ from hotdata_jupyter.env import load_default_env_files, load_env_file
32
+ from hotdata_jupyter.magics import HotdataMagics, load_ipython_extension
33
+ from hotdata_jupyter.metadata import notebook_metadata
34
+ from hotdata_jupyter.workspace import WorkspaceSelector, workspace_selector_from_env
35
+
36
+ __all__ = [
37
+ "DEFAULT_SCHEMA",
38
+ "HotdataClient",
39
+ "HotdataMagics",
40
+ "LoadManagedTableResult",
41
+ "ManagedDatabase",
42
+ "ManagedDatabaseWriter",
43
+ "QueryResult",
44
+ "WorkspaceSelection",
45
+ "WorkspaceSelector",
46
+ "__version__",
47
+ "create_managed_database",
48
+ "display_managed_databases_panel",
49
+ "display_query_result",
50
+ "from_env",
51
+ "load_default_env_files",
52
+ "load_env_file",
53
+ "load_ipython_extension",
54
+ "load_managed_table_from_bytes",
55
+ "managed_database_writer",
56
+ "managed_databases_markdown",
57
+ "notebook_metadata",
58
+ "resolve_workspace_selection",
59
+ "result_markdown",
60
+ "workspace_health_lines",
61
+ "workspace_selector_from_env",
62
+ ]
@@ -0,0 +1,237 @@
1
+ """Jupyter UI for managed Hotdata databases (create + parquet table loads)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import tempfile
7
+
8
+ import ipywidgets as widgets
9
+ from hotdata_runtime import (
10
+ DEFAULT_SCHEMA,
11
+ HotdataClient,
12
+ HotdataError,
13
+ HotdataTransientError,
14
+ LoadManagedTableResult,
15
+ ManagedDatabase,
16
+ classify_sdk_error,
17
+ )
18
+ from IPython.display import Markdown, display
19
+
20
+
21
+ def _parse_table_names(text: str) -> list[str]:
22
+ return [line.strip() for line in text.splitlines() if line.strip()]
23
+
24
+
25
+ def _format_runtime_error(error: HotdataError) -> str:
26
+ """Render a typed runtime/SDK error as a readable widget status message.
27
+
28
+ Transient (retryable) failures get a hint to try again so notebook users
29
+ are not left guessing whether the operation can simply be re-run.
30
+ """
31
+ if isinstance(error, HotdataTransientError):
32
+ return f"{error} (temporary - try again)"
33
+ return str(error)
34
+
35
+
36
+ def _upload_parquet_bytes(client: HotdataClient, contents: bytes) -> str:
37
+ with tempfile.NamedTemporaryFile(suffix=".parquet", delete=False) as tmp:
38
+ tmp.write(contents)
39
+ path = tmp.name
40
+ try:
41
+ return client.upload_parquet(path)
42
+ finally:
43
+ os.unlink(path)
44
+
45
+
46
+ def managed_databases_markdown(client: HotdataClient) -> str:
47
+ dbs = client.list_managed_databases()
48
+ if not dbs:
49
+ return (
50
+ "### Managed databases\n\n"
51
+ "_No managed databases yet._ Create one below, or with the CLI: "
52
+ "`hotdata databases create --name <name> --table <table>`."
53
+ )
54
+ lines = [
55
+ "### Managed databases",
56
+ "",
57
+ "| description | id | sql_prefix |",
58
+ "| --- | --- | --- |",
59
+ ]
60
+ for db in dbs:
61
+ prefix = f"{db.id}.{{schema}}.{{table}}"
62
+ lines.append(f"| {db.description or db.id} | `{db.id}` | `{prefix}` |")
63
+ lines.append("")
64
+ lines.append("_Query as `database.schema.table` in SQL._")
65
+ return "\n".join(lines)
66
+
67
+
68
+ def display_managed_databases_panel(client: HotdataClient) -> None:
69
+ display(Markdown(managed_databases_markdown(client)))
70
+
71
+
72
+ def create_managed_database(
73
+ client: HotdataClient,
74
+ *,
75
+ name: str,
76
+ schema: str = DEFAULT_SCHEMA,
77
+ tables: list[str] | None = None,
78
+ ) -> ManagedDatabase:
79
+ return client.create_managed_database(description=name, schema=schema, tables=tables)
80
+
81
+
82
+ def load_managed_table_from_bytes(
83
+ client: HotdataClient,
84
+ database: str,
85
+ table: str,
86
+ contents: bytes,
87
+ *,
88
+ schema: str = DEFAULT_SCHEMA,
89
+ ) -> LoadManagedTableResult:
90
+ upload_id = _upload_parquet_bytes(client, contents)
91
+ return client.load_managed_table(
92
+ database,
93
+ table,
94
+ schema=schema,
95
+ upload_id=upload_id,
96
+ )
97
+
98
+
99
+ class ManagedDatabaseWriter:
100
+ """Create managed databases and load parquet files from a Jupyter notebook."""
101
+
102
+ def __init__(
103
+ self,
104
+ client: HotdataClient,
105
+ *,
106
+ default_schema: str = DEFAULT_SCHEMA,
107
+ ) -> None:
108
+ self._client = client
109
+ self._default_schema = default_schema
110
+ self._status = widgets.Output()
111
+
112
+ self.name = widgets.Text(description="Database name", layout=widgets.Layout(width="100%"))
113
+ self.schema = widgets.Text(
114
+ value=default_schema,
115
+ description="Schema",
116
+ layout=widgets.Layout(width="100%"),
117
+ )
118
+ self.tables = widgets.Textarea(
119
+ description="Tables",
120
+ placeholder="One table name per line",
121
+ layout=widgets.Layout(width="100%", height="80px"),
122
+ )
123
+ self.create = widgets.Button(description="Create database", button_style="success")
124
+
125
+ self._rebuild_database_pick()
126
+ self.table = widgets.Text(description="Table name", layout=widgets.Layout(width="100%"))
127
+ self.file = widgets.FileUpload(accept=".parquet", description="Parquet file")
128
+ self.load = widgets.Button(description="Load table", button_style="success")
129
+
130
+ self.create.on_click(self._on_create)
131
+ self.load.on_click(self._on_load)
132
+
133
+ def _rebuild_database_pick(self) -> None:
134
+ current = getattr(getattr(self, "database", None), "value", None)
135
+ dbs = self._client.list_managed_databases()
136
+ if not dbs:
137
+ self.database = widgets.Dropdown(
138
+ options=[("(create one first)", "")],
139
+ value="",
140
+ description="Database",
141
+ layout=widgets.Layout(width="100%"),
142
+ )
143
+ return
144
+ options = [(db.description or db.id, db.id) for db in dbs]
145
+ value = current if current in {val for _, val in options} else options[0][1]
146
+ self.database = widgets.Dropdown(
147
+ options=options,
148
+ value=value,
149
+ description="Database",
150
+ layout=widgets.Layout(width="100%"),
151
+ )
152
+
153
+ def _on_create(self, _button: widgets.Button) -> None:
154
+ db_name = self.name.value.strip()
155
+ if not db_name:
156
+ self._write_status("Enter a database name.", error=True)
157
+ return
158
+ schema = self.schema.value.strip() or self._default_schema
159
+ tables = _parse_table_names(self.tables.value)
160
+ try:
161
+ db = self._client.create_managed_database(
162
+ description=db_name,
163
+ schema=schema,
164
+ tables=tables or None,
165
+ )
166
+ self._rebuild_database_pick()
167
+ self._write_status(
168
+ f"Created {db.description or db.id} ({db.id}). "
169
+ "Load parquet into a declared table below."
170
+ )
171
+ except HotdataError as e:
172
+ self._write_status(_format_runtime_error(e), error=True)
173
+ except (ValueError, KeyError) as e:
174
+ self._write_status(_format_runtime_error(classify_sdk_error(e)), error=True)
175
+
176
+ def _on_load(self, _button: widgets.Button) -> None:
177
+ database = self.database.value
178
+ table = self.table.value.strip()
179
+ if not database:
180
+ self._write_status("Choose or create a database first.", error=True)
181
+ return
182
+ if not table:
183
+ self._write_status("Enter a table name.", error=True)
184
+ return
185
+ if not self.file.value:
186
+ self._write_status("Choose a parquet file to upload.", error=True)
187
+ return
188
+ schema = self.schema.value.strip() or self._default_schema
189
+ try:
190
+ contents = self.file.value[0]["content"]
191
+ upload_id = _upload_parquet_bytes(self._client, contents)
192
+ loaded = self._client.load_managed_table(
193
+ database,
194
+ table,
195
+ schema=schema,
196
+ upload_id=upload_id,
197
+ )
198
+ self._write_status(f"Loaded {loaded.full_name} · {loaded.row_count} rows.")
199
+ except HotdataError as e:
200
+ self._write_status(_format_runtime_error(e), error=True)
201
+ except (ValueError, KeyError, OSError) as e:
202
+ self._write_status(_format_runtime_error(classify_sdk_error(e)), error=True)
203
+
204
+ def _write_status(self, message: str, *, error: bool = False) -> None:
205
+ with self._status:
206
+ self._status.clear_output(wait=True)
207
+ display(Markdown(f"**Error:** {message}" if error else message))
208
+
209
+ @property
210
+ def ui(self):
211
+ return widgets.VBox(
212
+ [
213
+ widgets.HTML("<h4>Create database</h4>"),
214
+ self.name,
215
+ self.schema,
216
+ self.tables,
217
+ self.create,
218
+ widgets.HTML("<h4>Load parquet table</h4>"),
219
+ self.database,
220
+ self.table,
221
+ self.file,
222
+ self.load,
223
+ self._status,
224
+ ]
225
+ )
226
+
227
+ def display(self) -> None:
228
+ display_managed_databases_panel(self._client)
229
+ display(self.ui)
230
+
231
+
232
+ def managed_database_writer(
233
+ client: HotdataClient,
234
+ *,
235
+ default_schema: str = DEFAULT_SCHEMA,
236
+ ) -> ManagedDatabaseWriter:
237
+ return ManagedDatabaseWriter(client, default_schema=default_schema)
@@ -0,0 +1,35 @@
1
+ """Display adapters for Hotdata query results in notebooks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from hotdata_runtime import QueryResult
6
+ from IPython.display import Markdown, display
7
+
8
+
9
+ def result_markdown(result: QueryResult) -> str:
10
+ meta = result.metadata_dict()
11
+ parts: list[str] = [
12
+ f"**rows** `{meta['row_count']}`",
13
+ f"**columns** `{meta['column_count']}`",
14
+ ]
15
+ if meta["result_id"]:
16
+ parts.append(f"**result_id** `{meta['result_id']}`")
17
+ if meta["query_run_id"]:
18
+ parts.append(f"**query_run_id** `{meta['query_run_id']}`")
19
+ if meta["execution_time_ms"] is not None:
20
+ parts.append(f"**execution_time_ms** `{meta['execution_time_ms']}`")
21
+ if meta["warning"]:
22
+ parts.append(f"**warning** `{meta['warning']}`")
23
+ if meta["error_message"]:
24
+ parts.append(f"**error** `{meta['error_message']}`")
25
+ return " · ".join(parts)
26
+
27
+
28
+ def display_query_result(
29
+ result: QueryResult,
30
+ *,
31
+ title: str = "Hotdata result",
32
+ ):
33
+ display(Markdown(f"### {title}"))
34
+ display(Markdown(result_markdown(result)))
35
+ return result.to_pandas()
hotdata_jupyter/env.py ADDED
@@ -0,0 +1,40 @@
1
+ """Load Hotdata credentials into the current process (e.g. Jupyter kernel)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+
9
+ def load_env_file(path: str | Path, *, override: bool = False) -> bool:
10
+ """Parse a shell-style ``KEY=VALUE`` file into ``os.environ``.
11
+
12
+ Returns True if the file existed and was read.
13
+ """
14
+ p = Path(path).expanduser()
15
+ if not p.is_file():
16
+ return False
17
+ for raw in p.read_text(encoding="utf-8").splitlines():
18
+ line = raw.strip()
19
+ if not line or line.startswith("#") or "=" not in line:
20
+ continue
21
+ key, _, value = line.partition("=")
22
+ key = key.strip()
23
+ value = value.strip().strip("'\"")
24
+ if not key:
25
+ continue
26
+ if override or key not in os.environ:
27
+ os.environ[key] = value
28
+ return True
29
+
30
+
31
+ def load_default_env_files() -> list[str]:
32
+ """Load ``~/.env`` then ``.env`` in the current working directory."""
33
+ loaded: list[str] = []
34
+ home = Path.home() / ".env"
35
+ if load_env_file(home):
36
+ loaded.append(str(home))
37
+ cwd = Path.cwd() / ".env"
38
+ if cwd != home and load_env_file(cwd):
39
+ loaded.append(str(cwd))
40
+ return loaded
@@ -0,0 +1,38 @@
1
+ """IPython magics for executing Hotdata SQL."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from hotdata_runtime import HotdataClient
6
+ from IPython.core.magic import Magics, line_cell_magic, magics_class
7
+
8
+ from hotdata_jupyter.display import display_query_result
9
+
10
+
11
+ @magics_class
12
+ class HotdataMagics(Magics):
13
+ def __init__(self, shell, client: HotdataClient | None = None):
14
+ super().__init__(shell)
15
+ self._client = client
16
+
17
+ def _runtime_client(self) -> HotdataClient:
18
+ if self._client is None:
19
+ self._client = HotdataClient.from_env()
20
+ return self._client
21
+
22
+ @line_cell_magic
23
+ def hotdata(self, line: str, cell: str | None = None):
24
+ if cell is not None:
25
+ # Cell magic: %%hotdata [database]
26
+ # Optional database name on the first line scopes the query.
27
+ sql = cell
28
+ database = line.strip() or None
29
+ else:
30
+ # Line magic: %hotdata SELECT ...
31
+ sql = line
32
+ database = None
33
+ result = self._runtime_client().execute_sql(sql, database=database)
34
+ return display_query_result(result)
35
+
36
+
37
+ def load_ipython_extension(ipython) -> None:
38
+ ipython.register_magics(HotdataMagics)
@@ -0,0 +1,13 @@
1
+ """Notebook metadata helpers for Hotdata sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from hotdata_runtime import HotdataClient
6
+
7
+
8
+ def notebook_metadata(client: HotdataClient) -> dict[str, str | None]:
9
+ return {
10
+ "workspace_id": client.workspace_id,
11
+ "host": client.host,
12
+ "session_id": client.session_id,
13
+ }
@@ -0,0 +1,99 @@
1
+ """Jupyter workspace selection (aligned with hotdata-marimo)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ipywidgets as widgets
6
+ from hotdata_runtime import (
7
+ HotdataClient,
8
+ default_api_key,
9
+ default_host,
10
+ default_session_id,
11
+ resolve_workspace_selection,
12
+ )
13
+ from IPython.display import Markdown
14
+
15
+
16
+ class WorkspaceSelector:
17
+ """Workspace picker that rebuilds ``HotdataClient`` when the selection changes."""
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ api_key: str,
23
+ host: str | None = None,
24
+ session_id: str | None = None,
25
+ label: str = "Workspace",
26
+ ) -> None:
27
+ self._api_key = api_key
28
+ self._host = host or default_host()
29
+ self._session_id = session_id
30
+ self._client_cache: HotdataClient | None = None
31
+ self._client_cache_wid: str | None = None
32
+ selection = resolve_workspace_selection(api_key, self._host, session_id)
33
+ self._explicit = selection.source == "explicit_env"
34
+ if self._explicit:
35
+ self._pick = None
36
+ self._workspace_id = selection.workspace_id
37
+ return
38
+
39
+ workspaces = selection.workspaces
40
+ if len(workspaces) == 1:
41
+ self._pick = None
42
+ self._workspace_id = workspaces[0].public_id
43
+ return
44
+
45
+ labels: list[tuple[str, str]] = []
46
+ seen: set[str] = set()
47
+ for w in workspaces:
48
+ base = w.name
49
+ label_text = base if base not in seen else f"{base} ({w.public_id})"
50
+ seen.add(base)
51
+ labels.append((label_text, w.public_id))
52
+
53
+ labels.sort(key=lambda t: 0 if t[1] == selection.workspace_id else 1)
54
+ self._pick = widgets.Dropdown(
55
+ options=labels,
56
+ value=selection.workspace_id,
57
+ description=label,
58
+ layout=widgets.Layout(width="100%"),
59
+ )
60
+ self._workspace_id = selection.workspace_id
61
+
62
+ @property
63
+ def workspace_id(self) -> str:
64
+ if self._pick is None:
65
+ return self._workspace_id
66
+ value = self._pick.value
67
+ return value if value else self._workspace_id
68
+
69
+ @property
70
+ def client(self) -> HotdataClient:
71
+ wid = self.workspace_id
72
+ if self._client_cache is None or self._client_cache_wid != wid:
73
+ self._client_cache = HotdataClient(
74
+ self._api_key,
75
+ wid,
76
+ host=self._host,
77
+ session_id=self._session_id,
78
+ )
79
+ self._client_cache_wid = wid
80
+ return self._client_cache
81
+
82
+ @property
83
+ def ui(self):
84
+ if self._pick is None:
85
+ return Markdown(f"**Workspace** `{self.workspace_id}`")
86
+ _ = self._pick.value
87
+ return self._pick
88
+
89
+
90
+ def workspace_selector_from_env(*, label: str = "Workspace") -> WorkspaceSelector:
91
+ api_key = default_api_key()
92
+ if not api_key:
93
+ raise RuntimeError("HOTDATA_API_KEY must be set.")
94
+ return WorkspaceSelector(
95
+ api_key=api_key,
96
+ host=default_host(),
97
+ session_id=default_session_id(),
98
+ label=label,
99
+ )
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: hotdata-jupyter
3
+ Version: 0.2.1
4
+ Summary: Jupyter integration for Hotdata runtime
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: hotdata-runtime>=0.3.0
8
+ Requires-Dist: hotdata>=0.4.1
9
+ Requires-Dist: ipython>=8.18
10
+ Requires-Dist: ipywidgets>=8.1
11
+ Description-Content-Type: text/markdown
12
+
13
+ # hotdata-jupyter
14
+
15
+ [Jupyter](https://jupyter.org/) helpers for [Hotdata](https://hotdata.dev) — rich query display, workspace selection, managed databases, and a `%%hotdata` cell magic for running SQL directly in cells.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install hotdata-jupyter
21
+ ```
22
+
23
+ ## Authentication
24
+
25
+ Set `HOTDATA_API_KEY` in your environment. Optionally set `HOTDATA_WORKSPACE` to pin a specific workspace (the first available workspace is used if unset).
26
+
27
+ ## Quickstart
28
+
29
+ ```python
30
+ import hotdata_jupyter as hj
31
+
32
+ client = hj.from_env()
33
+ result = client.execute_sql("SELECT 1 AS ok")
34
+ hj.display_query_result(result)
35
+ ```
36
+
37
+ ## Workspace selection
38
+
39
+ When `HOTDATA_WORKSPACE` is set, the client connects to that workspace directly. If you have multiple workspaces, use the interactive picker — it renders a dropdown and updates `ws.client` when the selection changes:
40
+
41
+ ```python
42
+ ws = hj.workspace_selector_from_env()
43
+ display(ws.ui)
44
+ client = ws.client
45
+ ```
46
+
47
+ ## Running SQL
48
+
49
+ ```python
50
+ result = client.execute_sql("SELECT * FROM orders LIMIT 10")
51
+ hj.display_query_result(result)
52
+ ```
53
+
54
+ `display_query_result` renders the row count, column names, and a pandas DataFrame inline in the notebook.
55
+
56
+ ## Cell magic
57
+
58
+ Load the extension once per session, then use `%%hotdata` cells to write SQL without wrapping it in Python strings. The last active client is picked up automatically:
59
+
60
+ ```python
61
+ %load_ext hotdata_jupyter
62
+ ```
63
+
64
+ ```
65
+ %%hotdata
66
+ SELECT
67
+ product,
68
+ SUM(amount) AS total
69
+ FROM orders
70
+ GROUP BY product
71
+ ORDER BY total DESC
72
+ ```
73
+
74
+ ## Managed databases
75
+
76
+ List the managed databases in your workspace:
77
+
78
+ ```python
79
+ hj.display_managed_databases_panel(client)
80
+ ```
81
+
82
+ Create a database and load parquet files programmatically:
83
+
84
+ ```python
85
+ db = hj.create_managed_database(client, name="sales", tables=["orders"])
86
+
87
+ with open("orders.parquet", "rb") as f:
88
+ loaded = hj.load_managed_table_from_bytes(client, "sales", "orders", f.read())
89
+
90
+ print(f"Loaded {loaded.row_count} rows into {loaded.full_name}")
91
+ ```
92
+
93
+ Or use the interactive ipywidgets form:
94
+
95
+ ```python
96
+ writer = hj.managed_database_writer(client)
97
+ writer.display()
98
+ ```
99
+
100
+ ## Open the demo notebook
101
+
102
+ ```bash
103
+ jupyter lab examples/demo.ipynb
104
+ ```
105
+
106
+ The demo covers workspace selection, connection listing, schema browsing, query history, and cell magics.
107
+
108
+ ## Development
109
+
110
+ ```bash
111
+ uv sync --locked
112
+ uv run pytest
113
+ ```
@@ -0,0 +1,10 @@
1
+ hotdata_jupyter/__init__.py,sha256=a0Xgbq3D3Go-Qs7VjIbRlK5qWE2eMR5gYLVLoUo9QgY,1752
2
+ hotdata_jupyter/databases.py,sha256=QVS0LLh7kdsJDpBje3cEEh3ti0w8IcixoYVE6DCO57w,8064
3
+ hotdata_jupyter/display.py,sha256=6X5k6dIWHwTuLkXTgFbC50g-PUBbaxPLSZyjFnB2zVs,1115
4
+ hotdata_jupyter/env.py,sha256=mb7QxNwQnd_QbTonZb2qSCNVncXKK2rI8rurnDEJAOU,1214
5
+ hotdata_jupyter/magics.py,sha256=cZCo5TsxrmHgNtUFlTjirKH7A-IupQpjggXuue2A7no,1209
6
+ hotdata_jupyter/metadata.py,sha256=BJdYle83OkRb8-q1tYlw27OEYHh9JapuBJ_4sP-1cK0,340
7
+ hotdata_jupyter/workspace.py,sha256=hLVcITsL8ySX6etcVzUpyQnC9kgcU3HiTXTlNqFldrU,3078
8
+ hotdata_jupyter-0.2.1.dist-info/METADATA,sha256=5rduKZgN3d1EF5g0maBIdKxqAFve39g_b5pEZem0ekc,2643
9
+ hotdata_jupyter-0.2.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
10
+ hotdata_jupyter-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any