jupyter-duckdb 1.2.102__py3-none-any.whl → 1.2.103__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.
- duckdb_kernel/db/Connection.py +3 -0
- duckdb_kernel/db/implementation/duckdb/Connection.py +3 -0
- duckdb_kernel/db/implementation/postgres/Connection.py +4 -0
- duckdb_kernel/db/implementation/sqlite/Connection.py +3 -0
- duckdb_kernel/kernel.py +187 -166
- duckdb_kernel/magics/MagicCommandCallback.py +7 -14
- duckdb_kernel/magics/MagicCommandHandler.py +9 -9
- duckdb_kernel/magics/MagicState.py +11 -0
- duckdb_kernel/magics/__init__.py +1 -0
- {jupyter_duckdb-1.2.102.dist-info → jupyter_duckdb-1.2.103.dist-info}/METADATA +13 -12
- {jupyter_duckdb-1.2.102.dist-info → jupyter_duckdb-1.2.103.dist-info}/RECORD +13 -13
- {jupyter_duckdb-1.2.102.dist-info → jupyter_duckdb-1.2.103.dist-info}/WHEEL +1 -1
- duckdb_kernel/magics/StringWrapper.py +0 -3
- {jupyter_duckdb-1.2.102.dist-info → jupyter_duckdb-1.2.103.dist-info}/top_level.txt +0 -0
duckdb_kernel/db/Connection.py
CHANGED
|
@@ -19,6 +19,7 @@ class Connection(Base):
|
|
|
19
19
|
self.host: str = host
|
|
20
20
|
self.port: int = port
|
|
21
21
|
self.username: Optional[str] = username
|
|
22
|
+
self.password: Optional[str] = password
|
|
22
23
|
|
|
23
24
|
options = {
|
|
24
25
|
'host': host,
|
|
@@ -43,6 +44,9 @@ class Connection(Base):
|
|
|
43
44
|
def close(self):
|
|
44
45
|
self.con.close()
|
|
45
46
|
|
|
47
|
+
def copy(self) -> 'Connection':
|
|
48
|
+
return Connection(self.host, self.port, self.username, self.password, self.database_name)
|
|
49
|
+
|
|
46
50
|
def __str__(self) -> str:
|
|
47
51
|
user = f'{self.username}@' if self.username is not None else ''
|
|
48
52
|
return f'PostgreSQL: {user}{self.host}:{self.port}/{self.database_name}'
|
duckdb_kernel/kernel.py
CHANGED
|
@@ -10,7 +10,6 @@ from typing import Optional, Dict, List, Tuple
|
|
|
10
10
|
from ipykernel.kernelbase import Kernel
|
|
11
11
|
|
|
12
12
|
from .db import Connection, DatabaseError, Table
|
|
13
|
-
from .visualization.lib import *
|
|
14
13
|
from .db.error import *
|
|
15
14
|
from .magics import *
|
|
16
15
|
from .parser import RAParser, DCParser, ParserError
|
|
@@ -40,8 +39,11 @@ class DuckDBKernel(Kernel):
|
|
|
40
39
|
self._magics: MagicCommandHandler = MagicCommandHandler()
|
|
41
40
|
|
|
42
41
|
self._magics.add(
|
|
43
|
-
MagicCommand('create').arg('database').opt('of').opt('
|
|
44
|
-
MagicCommand('load').arg('database').opt('
|
|
42
|
+
MagicCommand('create').arg('database').opt('of').opt('name').on(self._create_magic),
|
|
43
|
+
MagicCommand('load').arg('database').opt('name').on(self._load_magic),
|
|
44
|
+
MagicCommand('copy').arg('source').arg('target').on(self._copy_magic),
|
|
45
|
+
MagicCommand('use').arg('name').on(self._use_magic),
|
|
46
|
+
MagicCommand('load_tests').arg('tests').on(self._load_tests_magic),
|
|
45
47
|
MagicCommand('test').arg('name').result(True).on(self._test_magic),
|
|
46
48
|
MagicCommand('all', 'all_rows').on(self._all_magic),
|
|
47
49
|
MagicCommand('max_rows').arg('count').on(self._max_rows_magic),
|
|
@@ -60,8 +62,8 @@ class DuckDBKernel(Kernel):
|
|
|
60
62
|
)
|
|
61
63
|
|
|
62
64
|
# create placeholders for database and tests
|
|
63
|
-
self._db:
|
|
64
|
-
self._tests:
|
|
65
|
+
self._db: Dict[str, Connection] = {}
|
|
66
|
+
self._tests: Dict = {}
|
|
65
67
|
|
|
66
68
|
# output related functions
|
|
67
69
|
def print(self, text: str, name: str = 'stdout'):
|
|
@@ -98,68 +100,64 @@ class DuckDBKernel(Kernel):
|
|
|
98
100
|
})
|
|
99
101
|
|
|
100
102
|
# database related functions
|
|
101
|
-
def _load_database(self, path: str):
|
|
102
|
-
if self._db
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
#
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
from .db.implementation.duckdb import Connection as DuckDB
|
|
130
|
-
self._db = DuckDB(path)
|
|
103
|
+
def _load_database(self, path: str, name: str) -> Connection:
|
|
104
|
+
if name in self._db:
|
|
105
|
+
raise ValueError(f'duplicate database name {name}')
|
|
106
|
+
|
|
107
|
+
# If the provided path looks like a postgres url,
|
|
108
|
+
# we want to use the postgres driver.
|
|
109
|
+
if path.startswith(('postgresql://', 'postgres://', 'pgsql://', 'psql://', 'pg://')):
|
|
110
|
+
# pull data from connection string
|
|
111
|
+
re_expr = r'(postgresql|postgres|pgsql|psql|pg)://((.*?)(:(.*?))?@)?(.*?)(:(\d+))?(/(.*))?'
|
|
112
|
+
match = re.fullmatch(re_expr, path)
|
|
113
|
+
|
|
114
|
+
host = match.group(6)
|
|
115
|
+
port = int(match.group(8)) if match.group(8) is not None else 5432
|
|
116
|
+
username = match.group(3)
|
|
117
|
+
password = match.group(5)
|
|
118
|
+
database_name = match.group(10)
|
|
119
|
+
|
|
120
|
+
# load and create instance
|
|
121
|
+
from .db.implementation.postgres import Connection as Postgres
|
|
122
|
+
self._db[name] = Postgres(host, port, username, password, database_name)
|
|
123
|
+
|
|
124
|
+
# Otherwise the provided path is used to create an
|
|
125
|
+
# in-process instance.
|
|
126
|
+
else:
|
|
127
|
+
# By default, we try to load DuckDB.
|
|
128
|
+
try:
|
|
129
|
+
from .db.implementation.duckdb import Connection as DuckDB
|
|
130
|
+
self._db[name] = DuckDB(path)
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
# If DuckDB is not installed or fails to load,
|
|
133
|
+
# we use SQLite instead.
|
|
134
|
+
except ImportError:
|
|
135
|
+
self.print('DuckDB is not available\n')
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
from .db.implementation.sqlite import Connection as SQLite
|
|
138
|
+
self._db[name] = SQLite(path)
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
return self._db[name]
|
|
141
|
+
|
|
142
|
+
def _unload_database(self, name: str):
|
|
143
|
+
if name in self._db:
|
|
144
|
+
self._db[name].close()
|
|
145
|
+
del self._db[name]
|
|
143
146
|
|
|
144
|
-
def _unload_database(self):
|
|
145
|
-
if self._db is not None:
|
|
146
|
-
self._db.close()
|
|
147
|
-
self._db = None
|
|
148
147
|
return True
|
|
149
148
|
else:
|
|
150
149
|
return False
|
|
151
150
|
|
|
152
|
-
def _execute_stmt(self, name: str, query: str
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if self._db is None:
|
|
151
|
+
def _execute_stmt(self, silent: bool, state: MagicState, name: str, query: str) \
|
|
152
|
+
-> Tuple[Optional[List[str]], Optional[List[List]]]:
|
|
153
|
+
if state.db is None:
|
|
156
154
|
raise AssertionError('load a database first')
|
|
157
155
|
|
|
158
156
|
# execute query and store start and end timestamp
|
|
159
157
|
st = time.time()
|
|
160
158
|
|
|
161
159
|
try:
|
|
162
|
-
columns, rows =
|
|
160
|
+
columns, rows = state.db.execute(query)
|
|
163
161
|
except EmptyResultError:
|
|
164
162
|
columns, rows = None, None
|
|
165
163
|
|
|
@@ -168,32 +166,32 @@ class DuckDBKernel(Kernel):
|
|
|
168
166
|
# print result if not silent
|
|
169
167
|
if not silent:
|
|
170
168
|
# print EXPLAIN queries as raw text if using DuckDB
|
|
171
|
-
if query.strip().startswith('EXPLAIN') and
|
|
169
|
+
if query.strip().startswith('EXPLAIN') and state.db.plain_explain():
|
|
172
170
|
for ekey, evalue in rows:
|
|
173
171
|
html = f'<b>{ekey}</b><br><pre>{evalue}</pre>'
|
|
174
172
|
break
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
else:
|
|
174
|
+
html = ''
|
|
177
175
|
|
|
178
176
|
# print every other query as a table
|
|
179
177
|
else:
|
|
180
178
|
if columns is not None:
|
|
181
179
|
# table header
|
|
182
|
-
mapped_columns = (column_name_mapping.get(c, c) for c in columns)
|
|
180
|
+
mapped_columns = (state.column_name_mapping.get(c, c) for c in columns)
|
|
183
181
|
table_header = ''.join(f'<th>{c}</th>' for c in mapped_columns)
|
|
184
182
|
|
|
185
183
|
# table data
|
|
186
|
-
if max_rows is not None and len(rows) > max_rows:
|
|
184
|
+
if state.max_rows is not None and len(rows) > state.max_rows:
|
|
187
185
|
table_data = f'''
|
|
188
|
-
{rows_table(rows[:math.ceil(max_rows / 2)])}
|
|
186
|
+
{rows_table(rows[:math.ceil(state.max_rows / 2)])}
|
|
189
187
|
<tr>
|
|
190
188
|
<td colspan="{len(columns)}"
|
|
191
189
|
style="text-align: center"
|
|
192
|
-
title="{row_count(len(rows) - max_rows)} omitted">
|
|
190
|
+
title="{row_count(len(rows) - state.max_rows)} omitted">
|
|
193
191
|
...
|
|
194
192
|
</td>
|
|
195
193
|
</tr>
|
|
196
|
-
{rows_table(rows[-math.floor(max_rows // 2):])}
|
|
194
|
+
{rows_table(rows[-math.floor(state.max_rows // 2):])}
|
|
197
195
|
'''
|
|
198
196
|
else:
|
|
199
197
|
table_data = rows_table(rows)
|
|
@@ -220,15 +218,25 @@ class DuckDBKernel(Kernel):
|
|
|
220
218
|
return columns, rows
|
|
221
219
|
|
|
222
220
|
# magic command related functions
|
|
223
|
-
def _create_magic(self, silent: bool,
|
|
224
|
-
|
|
221
|
+
def _create_magic(self, silent: bool, state: MagicState,
|
|
222
|
+
path: str, of: Optional[str], name: Optional[str]):
|
|
223
|
+
self._load(silent, state, path, True, of, name)
|
|
224
|
+
|
|
225
|
+
def _load_magic(self, silent: bool, state: MagicState,
|
|
226
|
+
path: str, name: Optional[str]):
|
|
227
|
+
self._load(silent, state, path, False, None, name)
|
|
228
|
+
|
|
229
|
+
def _load(self, silent: bool, state: MagicState,
|
|
230
|
+
path: str, create: bool, of: Optional[str], name: Optional[str]):
|
|
231
|
+
# use default name if non provided
|
|
232
|
+
if name is None:
|
|
233
|
+
name = 'default'
|
|
225
234
|
|
|
226
|
-
|
|
227
|
-
|
|
235
|
+
if not silent:
|
|
236
|
+
self.print(f'--- connection {name} ---\n')
|
|
228
237
|
|
|
229
|
-
def _load(self, silent: bool, path: str, create: bool, of: Optional[str], with_tests: Optional[str]):
|
|
230
238
|
# unload current database if necessary
|
|
231
|
-
if self._unload_database():
|
|
239
|
+
if self._unload_database(name):
|
|
232
240
|
if not silent:
|
|
233
241
|
self.print('unloaded database\n')
|
|
234
242
|
|
|
@@ -245,10 +253,10 @@ class DuckDBKernel(Kernel):
|
|
|
245
253
|
if create and os.path.exists(path):
|
|
246
254
|
os.remove(path)
|
|
247
255
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
256
|
+
state.db = self._load_database(path, name)
|
|
257
|
+
if not silent:
|
|
258
|
+
# self.print(f'loaded database "{path}"\n')
|
|
259
|
+
self.print(str(state.db) + '\n')
|
|
252
260
|
|
|
253
261
|
# copy data from source database
|
|
254
262
|
if of is not None:
|
|
@@ -264,18 +272,18 @@ class DuckDBKernel(Kernel):
|
|
|
264
272
|
content = file.read()
|
|
265
273
|
|
|
266
274
|
# You can only execute one statement at a time using SQLite.
|
|
267
|
-
if not
|
|
275
|
+
if not state.db.multiple_statements_per_query():
|
|
268
276
|
statements = re.split(r';\r?\n', content)
|
|
269
277
|
for statement in statements:
|
|
270
278
|
try:
|
|
271
|
-
|
|
279
|
+
state.db.execute(statement)
|
|
272
280
|
except EmptyResultError:
|
|
273
281
|
pass
|
|
274
282
|
|
|
275
283
|
# Other DBMS can execute multiple statements at a time.
|
|
276
284
|
else:
|
|
277
285
|
try:
|
|
278
|
-
|
|
286
|
+
state.db.execute(content)
|
|
279
287
|
except EmptyResultError:
|
|
280
288
|
pass
|
|
281
289
|
|
|
@@ -289,29 +297,51 @@ class DuckDBKernel(Kernel):
|
|
|
289
297
|
of_db.execute('SHOW TABLES')
|
|
290
298
|
for table, in of_db.fetchall():
|
|
291
299
|
transfer_df = of_db.query(f'SELECT * FROM {table}').to_df()
|
|
292
|
-
|
|
300
|
+
state.db.execute(f'CREATE TABLE {table} AS SELECT * FROM transfer_df')
|
|
293
301
|
|
|
294
302
|
if not silent:
|
|
295
303
|
self.print(f'transferred table {table}\n')
|
|
296
304
|
|
|
297
|
-
|
|
298
|
-
if
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
305
|
+
def _copy_magic(self, silent: bool, state: MagicState, source: str, target: str):
|
|
306
|
+
if source not in self._db:
|
|
307
|
+
raise ValueError(f'unknown connection {source}')
|
|
308
|
+
|
|
309
|
+
if not silent:
|
|
310
|
+
self.print(f'--- connection {target} ---\n')
|
|
311
|
+
|
|
312
|
+
# unload current database if necessary
|
|
313
|
+
if self._unload_database(target):
|
|
314
|
+
if not silent:
|
|
315
|
+
self.print('unloaded database\n')
|
|
316
|
+
|
|
317
|
+
# copy connection
|
|
318
|
+
self._db[target] = self._db[source].copy()
|
|
319
|
+
state.db = self._db[target]
|
|
320
|
+
|
|
321
|
+
if not silent:
|
|
322
|
+
self.print(str(state.db) + '\n')
|
|
323
|
+
|
|
324
|
+
def _use_magic(self, silent: bool, state: MagicState, name: str):
|
|
325
|
+
if name not in self._db:
|
|
326
|
+
raise ValueError(f'unknown connection {name}')
|
|
327
|
+
|
|
328
|
+
state.db = self._db[name]
|
|
329
|
+
|
|
330
|
+
def _load_tests_magic(self, silent: bool, state: MagicState, tests: str):
|
|
331
|
+
with open(tests, 'r', encoding='utf-8') as tests_file:
|
|
332
|
+
self._tests = json.load(tests_file)
|
|
333
|
+
for test in self._tests.values():
|
|
334
|
+
if 'attributes' in test:
|
|
335
|
+
rows = {k: [] for k in test['attributes']}
|
|
336
|
+
for row in test['equals']:
|
|
337
|
+
for k, v in zip(test['attributes'], row):
|
|
338
|
+
rows[k].append(v)
|
|
309
339
|
|
|
310
|
-
|
|
340
|
+
test['equals'] = rows
|
|
311
341
|
|
|
312
|
-
|
|
342
|
+
self.print(f'loaded tests from {tests}\n')
|
|
313
343
|
|
|
314
|
-
def _test_magic(self, silent: bool, result_columns: List[str], result: List[List], name: str):
|
|
344
|
+
def _test_magic(self, silent: bool, state: MagicState, result_columns: List[str], result: List[List], name: str):
|
|
315
345
|
# If the query was empty, result_columns and result may be None.
|
|
316
346
|
if result_columns is None or result is None:
|
|
317
347
|
self.print_data(wrap_image(False, 'Statement did not return data.'))
|
|
@@ -400,31 +430,29 @@ class DuckDBKernel(Kernel):
|
|
|
400
430
|
elif above > 0:
|
|
401
431
|
raise TestError(f'{row_count(above)} unnecessary')
|
|
402
432
|
|
|
403
|
-
def _all_magic(self, silent: bool):
|
|
404
|
-
|
|
405
|
-
'max_rows': None
|
|
406
|
-
}
|
|
433
|
+
def _all_magic(self, silent: bool, state: MagicState):
|
|
434
|
+
state.max_rows = None
|
|
407
435
|
|
|
408
|
-
def _max_rows_magic(self, silent: bool, count: str):
|
|
436
|
+
def _max_rows_magic(self, silent: bool, state: MagicState, count: str):
|
|
409
437
|
if count.lower() != 'none':
|
|
410
438
|
DuckDBKernel.DEFAULT_MAX_ROWS = int(count)
|
|
411
439
|
else:
|
|
412
440
|
DuckDBKernel.DEFAULT_MAX_ROWS = None
|
|
413
441
|
|
|
414
|
-
|
|
415
|
-
return {
|
|
416
|
-
'max_rows': int(count) if count.lower() != 'none' else None
|
|
417
|
-
}
|
|
442
|
+
state.max_rows = DuckDBKernel.DEFAULT_MAX_ROWS
|
|
418
443
|
|
|
419
|
-
def
|
|
420
|
-
if
|
|
421
|
-
raise AssertionError('load a database first')
|
|
444
|
+
def _query_max_rows_magic(self, silent: bool, state: MagicState, count: str):
|
|
445
|
+
state.max_rows = int(count) if count.lower() != 'none' else None
|
|
422
446
|
|
|
447
|
+
def _schema_magic(self, silent: bool, state: MagicState, td: bool, only: Optional[str]):
|
|
423
448
|
if silent:
|
|
424
449
|
return
|
|
425
450
|
|
|
451
|
+
if state.db is None:
|
|
452
|
+
raise AssertionError('load a database first')
|
|
453
|
+
|
|
426
454
|
# analyze tables
|
|
427
|
-
tables =
|
|
455
|
+
tables = state.db.analyze()
|
|
428
456
|
|
|
429
457
|
# apply filter
|
|
430
458
|
if only is None:
|
|
@@ -466,7 +494,9 @@ class DuckDBKernel(Kernel):
|
|
|
466
494
|
|
|
467
495
|
self.print_data(svg)
|
|
468
496
|
|
|
469
|
-
def _store_magic(self, silent: bool,
|
|
497
|
+
def _store_magic(self, silent: bool, state: MagicState,
|
|
498
|
+
result_columns: List[str], result: List[List],
|
|
499
|
+
file: str, noheader: bool):
|
|
470
500
|
_, ext = file.rsplit('.', 1)
|
|
471
501
|
|
|
472
502
|
# csv
|
|
@@ -486,44 +516,38 @@ class DuckDBKernel(Kernel):
|
|
|
486
516
|
else:
|
|
487
517
|
raise ValueError(f'extension {ext} not supported')
|
|
488
518
|
|
|
489
|
-
def _ra_magic(self, silent: bool,
|
|
490
|
-
if self._db is None:
|
|
491
|
-
raise AssertionError('load a database first')
|
|
492
|
-
|
|
519
|
+
def _ra_magic(self, silent: bool, state: MagicState, analyze: bool):
|
|
493
520
|
if silent:
|
|
494
521
|
return
|
|
495
522
|
|
|
496
|
-
if not code.strip():
|
|
523
|
+
if not state.code.strip():
|
|
497
524
|
return
|
|
498
525
|
|
|
526
|
+
if state.db is None:
|
|
527
|
+
raise AssertionError('load a database first')
|
|
528
|
+
|
|
499
529
|
# analyze tables
|
|
500
|
-
tables =
|
|
530
|
+
tables = state.db.analyze()
|
|
501
531
|
|
|
502
532
|
# parse ra input
|
|
503
|
-
root_node = RAParser.parse_query(code)
|
|
533
|
+
root_node = RAParser.parse_query(state.code)
|
|
504
534
|
|
|
505
535
|
# create and show visualization
|
|
506
536
|
if analyze:
|
|
507
|
-
vd = RATreeDrawer(
|
|
537
|
+
vd = RATreeDrawer(state.db, root_node, tables)
|
|
538
|
+
|
|
508
539
|
svg = vd.to_interactive_svg()
|
|
509
|
-
|
|
510
|
-
'generated_code': {
|
|
511
|
-
node_id: node.to_sql_with_renamed_columns(tables)
|
|
512
|
-
for node_id, node in vd.nodes.items()
|
|
513
|
-
}
|
|
514
|
-
}
|
|
540
|
+
self.print_data(svg)
|
|
515
541
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
'generated_code': root_node.to_sql_with_renamed_columns(tables)
|
|
542
|
+
state.code = {
|
|
543
|
+
node_id: node.to_sql_with_renamed_columns(tables)
|
|
544
|
+
for node_id, node in vd.nodes.items()
|
|
520
545
|
}
|
|
521
546
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
return data
|
|
547
|
+
else:
|
|
548
|
+
state.code = root_node.to_sql_with_renamed_columns(tables)
|
|
525
549
|
|
|
526
|
-
def _all_ra_magic(self, silent: bool, value: str):
|
|
550
|
+
def _all_ra_magic(self, silent: bool, state: MagicState, value: str):
|
|
527
551
|
if value.lower() in ('1', 'on', 'true'):
|
|
528
552
|
self._magics['ra'].default(True)
|
|
529
553
|
self._magics['dc'].default(False)
|
|
@@ -532,31 +556,29 @@ class DuckDBKernel(Kernel):
|
|
|
532
556
|
else:
|
|
533
557
|
self._magics['ra'].default(False)
|
|
534
558
|
|
|
535
|
-
def _dc_magic(self, silent: bool,
|
|
536
|
-
if self._db is None:
|
|
537
|
-
raise AssertionError('load a database first')
|
|
538
|
-
|
|
559
|
+
def _dc_magic(self, silent: bool, state: MagicState):
|
|
539
560
|
if silent:
|
|
540
561
|
return
|
|
541
562
|
|
|
542
|
-
if not code.strip():
|
|
563
|
+
if not state.code.strip():
|
|
543
564
|
return
|
|
544
565
|
|
|
566
|
+
if state.db is None:
|
|
567
|
+
raise AssertionError('load a database first')
|
|
568
|
+
|
|
545
569
|
# analyze tables
|
|
546
|
-
tables =
|
|
570
|
+
tables = state.db.analyze()
|
|
547
571
|
|
|
548
572
|
# parse dc input
|
|
549
|
-
root_node = DCParser.parse_query(code)
|
|
573
|
+
root_node = DCParser.parse_query(state.code)
|
|
550
574
|
|
|
551
575
|
# generate sql
|
|
552
576
|
sql, cnm = root_node.to_sql_with_renamed_columns(tables)
|
|
553
577
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
'column_name_mapping': cnm
|
|
557
|
-
}
|
|
578
|
+
state.code = sql
|
|
579
|
+
state.column_name_mapping.update(cnm)
|
|
558
580
|
|
|
559
|
-
def _all_dc_magic(self, silent: bool, value: str):
|
|
581
|
+
def _all_dc_magic(self, silent: bool, state: MagicState, value: str):
|
|
560
582
|
if value.lower() in ('1', 'on', 'true'):
|
|
561
583
|
self._magics['dc'].default(True)
|
|
562
584
|
self._magics['ra'].default(False)
|
|
@@ -565,41 +587,43 @@ class DuckDBKernel(Kernel):
|
|
|
565
587
|
else:
|
|
566
588
|
self._magics['dc'].default(False)
|
|
567
589
|
|
|
568
|
-
def _guess_parser_magic(self, silent: bool, value: str):
|
|
590
|
+
def _guess_parser_magic(self, silent: bool, state: MagicState, value: str):
|
|
569
591
|
if value.lower() in ('1', 'on', 'true'):
|
|
570
592
|
self._magics['auto_parser'].default(True)
|
|
571
593
|
self.print('The correct parser is guessed for each subsequently executed cell.\n')
|
|
572
594
|
else:
|
|
573
595
|
self._magics['auto_parser'].default(False)
|
|
574
596
|
|
|
575
|
-
def _auto_parser_magic(self, silent: bool,
|
|
597
|
+
def _auto_parser_magic(self, silent: bool, state: MagicState):
|
|
576
598
|
# do not handle statements starting with SQL keywords
|
|
577
|
-
first_word = code.strip().split(maxsplit=1)
|
|
599
|
+
first_word = state.code.strip().split(maxsplit=1)
|
|
578
600
|
if len(first_word) > 0:
|
|
579
601
|
if first_word[0].upper() in SQL_KEYWORDS:
|
|
580
602
|
return
|
|
581
603
|
|
|
582
604
|
# try to parse DC
|
|
583
605
|
try:
|
|
584
|
-
|
|
606
|
+
self._dc_magic(silent, state)
|
|
585
607
|
except ParserError as e:
|
|
586
608
|
if e.depth > 0:
|
|
587
609
|
raise e
|
|
588
610
|
|
|
589
611
|
# try to parse RA
|
|
590
612
|
try:
|
|
591
|
-
|
|
613
|
+
self._ra_magic(silent, state, analyze=False)
|
|
592
614
|
except ParserError as e:
|
|
593
615
|
if e.depth > 0:
|
|
594
616
|
raise e
|
|
595
617
|
|
|
596
|
-
def _plotly_magic(self, silent: bool,
|
|
618
|
+
def _plotly_magic(self, silent: bool, state: MagicState,
|
|
619
|
+
cols: List, rows: List[Tuple],
|
|
620
|
+
type: str, mapping: str, title: str = None):
|
|
597
621
|
# split mapping and handle asterisks
|
|
598
622
|
mapping = [m.strip() for m in mapping.split(',')]
|
|
599
623
|
|
|
600
624
|
for i in range(len(mapping)):
|
|
601
625
|
if mapping[i] == '*':
|
|
602
|
-
mapping = mapping[:i] + cols + mapping[i+1:]
|
|
626
|
+
mapping = mapping[:i] + cols + mapping[i + 1:]
|
|
603
627
|
|
|
604
628
|
# convert all column names to lower case
|
|
605
629
|
lower_cols = [c.lower() for c in cols]
|
|
@@ -668,7 +692,9 @@ class DuckDBKernel(Kernel):
|
|
|
668
692
|
# finally print the code
|
|
669
693
|
self.print_data(html, mime='text/html')
|
|
670
694
|
|
|
671
|
-
def _plotly_raw_magic(self, silent: bool,
|
|
695
|
+
def _plotly_raw_magic(self, silent: bool, state: MagicState,
|
|
696
|
+
cols: List, rows: List[Tuple],
|
|
697
|
+
title: str = None):
|
|
672
698
|
if len(cols) != 1 and len(rows) != 1:
|
|
673
699
|
raise ValueError(f'expected exactly one column and one row')
|
|
674
700
|
|
|
@@ -683,34 +709,27 @@ class DuckDBKernel(Kernel):
|
|
|
683
709
|
**kwargs):
|
|
684
710
|
try:
|
|
685
711
|
# get magic command
|
|
686
|
-
|
|
712
|
+
if len(self._db) > 0:
|
|
713
|
+
init_db = self._db[list(self._db.keys())[0]]
|
|
714
|
+
else:
|
|
715
|
+
init_db = None
|
|
687
716
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
'max_rows': DuckDBKernel.DEFAULT_MAX_ROWS
|
|
691
|
-
}
|
|
717
|
+
magic_state = MagicState(init_db, code, DuckDBKernel.DEFAULT_MAX_ROWS)
|
|
718
|
+
pre_query_callbacks, post_query_callbacks = self._magics(silent, magic_state)
|
|
692
719
|
|
|
720
|
+
# execute magic commands here if it does not depend on query results
|
|
693
721
|
for callback in pre_query_callbacks:
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
# overwrite clean_code with generated code
|
|
697
|
-
if 'generated_code' in execution_args:
|
|
698
|
-
clean_code = execution_args['generated_code']
|
|
699
|
-
del execution_args['generated_code']
|
|
700
|
-
|
|
701
|
-
# set default column name mapping if none provided
|
|
702
|
-
if 'column_name_mapping' not in execution_args:
|
|
703
|
-
execution_args['column_name_mapping'] = {}
|
|
722
|
+
callback()
|
|
704
723
|
|
|
705
724
|
# execute statement if needed
|
|
706
725
|
cols, rows = None, None
|
|
707
726
|
|
|
708
|
-
if not isinstance(
|
|
709
|
-
|
|
727
|
+
if not isinstance(magic_state.code, dict):
|
|
728
|
+
magic_state.code = {'default': magic_state.code}
|
|
710
729
|
|
|
711
|
-
for name, code in
|
|
730
|
+
for name, code in magic_state.code.items():
|
|
712
731
|
if code.strip():
|
|
713
|
-
cols, rows = self._execute_stmt(
|
|
732
|
+
cols, rows = self._execute_stmt(silent, magic_state, name, code)
|
|
714
733
|
|
|
715
734
|
# execute magic command here if it does depend on query results
|
|
716
735
|
for callback in post_query_callbacks:
|
|
@@ -734,5 +753,7 @@ class DuckDBKernel(Kernel):
|
|
|
734
753
|
}
|
|
735
754
|
|
|
736
755
|
def do_shutdown(self, restart):
|
|
737
|
-
self.
|
|
756
|
+
for name in list(self._db.keys()):
|
|
757
|
+
self._unload_database(name)
|
|
758
|
+
|
|
738
759
|
return super().do_shutdown(restart)
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
from typing import Optional, List
|
|
2
2
|
|
|
3
|
-
from . import MagicCommand
|
|
4
|
-
from .StringWrapper import StringWrapper
|
|
3
|
+
from . import MagicCommand, MagicState
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
class MagicCommandCallback:
|
|
8
|
-
def __init__(self, mc: MagicCommand, silent: bool,
|
|
7
|
+
def __init__(self, mc: MagicCommand, silent: bool, state: MagicState, *args, **kwargs):
|
|
9
8
|
self._mc: MagicCommand = mc
|
|
10
9
|
self._silent: bool = silent
|
|
11
|
-
self.
|
|
10
|
+
self._state: MagicState = state
|
|
12
11
|
self._args = args
|
|
13
12
|
self._kwargs = kwargs
|
|
14
13
|
|
|
@@ -18,14 +17,8 @@ class MagicCommandCallback:
|
|
|
18
17
|
|
|
19
18
|
def __call__(self, columns: Optional[List[str]] = None, rows: Optional[List[List]] = None):
|
|
20
19
|
if self._mc.requires_code:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return result
|
|
26
|
-
|
|
27
|
-
if self._mc.requires_query_result:
|
|
28
|
-
return self._mc(self._silent, columns, rows, *self._args, **self._kwargs)
|
|
29
|
-
|
|
20
|
+
self._mc(self._silent, self._state, *self._args, **self._kwargs)
|
|
21
|
+
elif self._mc.requires_query_result:
|
|
22
|
+
self._mc(self._silent, self._state, columns, rows, *self._args, **self._kwargs)
|
|
30
23
|
else:
|
|
31
|
-
|
|
24
|
+
self._mc(self._silent, self._state, *self._args, **self._kwargs)
|
|
@@ -2,7 +2,8 @@ import re
|
|
|
2
2
|
from typing import Dict, Tuple, List
|
|
3
3
|
|
|
4
4
|
from . import MagicCommand, MagicCommandException, MagicCommandCallback
|
|
5
|
-
from .
|
|
5
|
+
from .MagicState import MagicState
|
|
6
|
+
from ..db import Connection
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class MagicCommandHandler:
|
|
@@ -18,8 +19,8 @@ class MagicCommandHandler:
|
|
|
18
19
|
def __getitem__(self, key: str) -> MagicCommand:
|
|
19
20
|
return self._magics[key.lower()]
|
|
20
21
|
|
|
21
|
-
def __call__(self, silent: bool,
|
|
22
|
-
|
|
22
|
+
def __call__(self, silent: bool, state: MagicState) \
|
|
23
|
+
-> Tuple[List[MagicCommandCallback], List[MagicCommandCallback]]:
|
|
23
24
|
enabled_callbacks: List[MagicCommandCallback] = []
|
|
24
25
|
|
|
25
26
|
# enable commands with default==True
|
|
@@ -27,21 +28,21 @@ class MagicCommandHandler:
|
|
|
27
28
|
if magic.is_default:
|
|
28
29
|
flags = {name: False for name, _ in magic.flags}
|
|
29
30
|
optionals = {name: default for name, default, _ in magic.optionals}
|
|
30
|
-
callback = MagicCommandCallback(magic, silent,
|
|
31
|
+
callback = MagicCommandCallback(magic, silent, state, **flags, **optionals)
|
|
31
32
|
|
|
32
33
|
enabled_callbacks.append(callback)
|
|
33
34
|
|
|
34
35
|
# search for magic commands in code
|
|
35
36
|
while True:
|
|
36
37
|
# ensure code starts with '%' or '%%' but not with '%%%'
|
|
37
|
-
match = re.match(r'^%{1,2}([^% ]+?)([ \t]*$| .+?$)', code, re.MULTILINE | re.IGNORECASE)
|
|
38
|
+
match = re.match(r'^%{1,2}([^% ]+?)([ \t]*$| .+?$)', state.code, re.MULTILINE | re.IGNORECASE)
|
|
38
39
|
|
|
39
40
|
if match is None:
|
|
40
41
|
break
|
|
41
42
|
|
|
42
43
|
# remove magic command from code
|
|
43
44
|
start, end = match.span()
|
|
44
|
-
code = code[:start] + code[end + 1:]
|
|
45
|
+
state.code = state.code[:start] + state.code[end + 1:]
|
|
45
46
|
|
|
46
47
|
# extract command
|
|
47
48
|
command = match.group(1).lower()
|
|
@@ -99,7 +100,7 @@ class MagicCommandHandler:
|
|
|
99
100
|
optionals[name.lower()] = value
|
|
100
101
|
|
|
101
102
|
# add to callbacks
|
|
102
|
-
callback = MagicCommandCallback(magic, silent,
|
|
103
|
+
callback = MagicCommandCallback(magic, silent, state, *args, **flags, **optionals)
|
|
103
104
|
enabled_callbacks.append(callback)
|
|
104
105
|
|
|
105
106
|
# disable overwritten callbacks
|
|
@@ -129,5 +130,4 @@ class MagicCommandHandler:
|
|
|
129
130
|
post_query_callbacks.append(callback)
|
|
130
131
|
|
|
131
132
|
# return callbacks
|
|
132
|
-
|
|
133
|
-
return code, pre_query_callbacks, post_query_callbacks
|
|
133
|
+
return pre_query_callbacks, post_query_callbacks
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from typing import Union, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from ..db import Connection
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MagicState:
|
|
7
|
+
def __init__(self, db: Connection, code: str, max_rows: Optional[int]):
|
|
8
|
+
self.db: Connection = db
|
|
9
|
+
self.code: Union[str, Dict] = code
|
|
10
|
+
self.max_rows: Optional[int] = max_rows
|
|
11
|
+
self.column_name_mapping: Dict[str, str] = {}
|
duckdb_kernel/magics/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: jupyter-duckdb
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.103
|
|
4
4
|
Summary: a basic wrapper kernel for DuckDB
|
|
5
5
|
Home-page: https://github.com/erictroebs/jupyter-duckdb
|
|
6
6
|
Author: Eric Tröbs
|
|
@@ -118,23 +118,24 @@ another DuckDB file or a file with SQL statements. In the first case the
|
|
|
118
118
|
included tables will be copied to the new database, while in the second case the
|
|
119
119
|
SQL statements are just executed. We find this feature very useful to work in a
|
|
120
120
|
temporary copy of the data and therefore be able to restart at any time. The
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
optional parameter `NAME` may be used to name a connection and reference it
|
|
122
|
+
later by using the magic command `USE`.
|
|
123
123
|
|
|
124
124
|
```
|
|
125
125
|
%CREATE data.duckdb OF my_statements.sql
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
`LOAD` on the other hand loads an existing database and returns an error if it
|
|
129
|
-
does not exist. (That is why `OF` cannot be used with `LOAD`! `
|
|
130
|
-
|
|
129
|
+
does not exist. (That is why `OF` cannot be used with `LOAD`! `NAME` on the
|
|
130
|
+
other hand is available also with this magic command.)
|
|
131
131
|
|
|
132
132
|
```
|
|
133
133
|
%LOAD data.duckdb
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
loaded, the current one is closed first and saved to disk
|
|
136
|
+
Multiple databases can be open at any time. If a new database with the same
|
|
137
|
+
name is created or loaded, the current one is closed first and saved to disk
|
|
138
|
+
if necessary.
|
|
138
139
|
|
|
139
140
|
Please note that `:memory:` is also a valid file path for DuckDB. The data is
|
|
140
141
|
then stored exclusively in the main memory. In combination with `CREATE`
|
|
@@ -213,10 +214,10 @@ FROM bar
|
|
|
213
214
|
|
|
214
215
|
### Ship Tests With Your Notebooks
|
|
215
216
|
|
|
216
|
-
Simple tests can be loaded
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
217
|
+
Simple tests can be loaded from json files with the help of magic command
|
|
218
|
+
`LOAD_TESTS`. These tests are stored as a JSON file. Each test is assigned a
|
|
219
|
+
unique name, a result set and whether the test should check the order of the
|
|
220
|
+
result. A very simple test file looks like the following JSON object:
|
|
220
221
|
|
|
221
222
|
```json
|
|
222
223
|
{
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
duckdb_kernel/__init__.py,sha256=6auU6zeJrsA4fxPSr2PYamS8fG-SMXTn5YQFXF2cseo,33
|
|
2
2
|
duckdb_kernel/__main__.py,sha256=Z3GwHEBWoQjNm2Y84ijnbA0Lk66L7nsFREuqhZ_ptk0,165
|
|
3
3
|
duckdb_kernel/kernel.json,sha256=_7E8Ci2FSdCvnzCjsOaue8QE8AvpS5JLQuxORO5IGtA,127
|
|
4
|
-
duckdb_kernel/kernel.py,sha256=
|
|
4
|
+
duckdb_kernel/kernel.py,sha256=qb2dEj2XhNE_9-xlpQBky7Cd4km_dqRLTXfNaNQNdpM,28189
|
|
5
5
|
duckdb_kernel/db/Column.py,sha256=GM5P6sFdlYK92hiKln5-6038gIDOTxh1AYbR4kiga_w,559
|
|
6
|
-
duckdb_kernel/db/Connection.py,sha256=
|
|
6
|
+
duckdb_kernel/db/Connection.py,sha256=tBXQBYt9c52RLbpl9sakNuAm0Z84--fhZ4efo8ACz-U,670
|
|
7
7
|
duckdb_kernel/db/Constraint.py,sha256=1YgUHk7s8mHCVedbcuJKyXDykj7_ybbwT3Dk9p2VMis,287
|
|
8
8
|
duckdb_kernel/db/DatabaseError.py,sha256=43zl8yym1f-fxH_UtGIbWnDnBE_TRwr9aCziY9t40QY,41
|
|
9
9
|
duckdb_kernel/db/ForeignKey.py,sha256=iurUAXwTwSIpLXsL0B7BA8jqDTfW4_wkeHxoqQbZwiU,470
|
|
@@ -11,19 +11,19 @@ duckdb_kernel/db/Table.py,sha256=HfvGX54kD_XvmLApYSmxtTQNvz2YYaaUNpm4e8dSOVY,934
|
|
|
11
11
|
duckdb_kernel/db/__init__.py,sha256=PKQYQDCW7VQYxmzhQK6A0Qloka9FdMfeFQMfY-CjBSA,198
|
|
12
12
|
duckdb_kernel/db/error/EmptyResultError.py,sha256=N9Oxi2HDZBKaRQsfRsWpJJGOYX4BjdQqWOU-XvzUzNY,92
|
|
13
13
|
duckdb_kernel/db/error/__init__.py,sha256=oHfhfbcfyTJ3pAPN835omdQcebvJTauuULFx5gm9rq4,47
|
|
14
|
-
duckdb_kernel/db/implementation/duckdb/Connection.py,sha256=
|
|
14
|
+
duckdb_kernel/db/implementation/duckdb/Connection.py,sha256=nFzj2wjHORFuRn27zfKhHtwu1Hulph7X4ImCaEbQtbQ,7051
|
|
15
15
|
duckdb_kernel/db/implementation/duckdb/__init__.py,sha256=HKogB1es4wOiQUoh7_eT32xnUFLmzoCyR_0LuY9r8YQ,35
|
|
16
|
-
duckdb_kernel/db/implementation/postgres/Connection.py,sha256=
|
|
16
|
+
duckdb_kernel/db/implementation/postgres/Connection.py,sha256=WCCdkM4V-Y2Pg-ofeTLHpyT7jgbQaw7qnaW5M1bYbX4,9490
|
|
17
17
|
duckdb_kernel/db/implementation/postgres/__init__.py,sha256=HKogB1es4wOiQUoh7_eT32xnUFLmzoCyR_0LuY9r8YQ,35
|
|
18
18
|
duckdb_kernel/db/implementation/postgres/util.py,sha256=4nr1mqXhlwkMVXbJSfJ7dRlUm6UskpvgKApe7GRwmBI,281
|
|
19
|
-
duckdb_kernel/db/implementation/sqlite/Connection.py,sha256=
|
|
19
|
+
duckdb_kernel/db/implementation/sqlite/Connection.py,sha256=W_7Eb2u2dHl54h5FXGQVc9oIFa6czYMhkNc2UWbTrkE,7118
|
|
20
20
|
duckdb_kernel/db/implementation/sqlite/__init__.py,sha256=HKogB1es4wOiQUoh7_eT32xnUFLmzoCyR_0LuY9r8YQ,35
|
|
21
21
|
duckdb_kernel/magics/MagicCommand.py,sha256=l0EmnqgGZ0HyqQhdTljAaftflVo_RYp-U5UiDftYxAA,3180
|
|
22
|
-
duckdb_kernel/magics/MagicCommandCallback.py,sha256=
|
|
22
|
+
duckdb_kernel/magics/MagicCommandCallback.py,sha256=Otl7Sa53eJtkMU5gBup3VTVCQfr7p00kt-xpnlETVe4,877
|
|
23
23
|
duckdb_kernel/magics/MagicCommandException.py,sha256=MwuWkpA6NoCqz437urdI0RVXhbSbVdziuRoi7slYFPc,49
|
|
24
|
-
duckdb_kernel/magics/MagicCommandHandler.py,sha256=
|
|
25
|
-
duckdb_kernel/magics/
|
|
26
|
-
duckdb_kernel/magics/__init__.py,sha256=
|
|
24
|
+
duckdb_kernel/magics/MagicCommandHandler.py,sha256=hKNHRfa0BSPei4QXdVlLlbpvZIhJJ91aaT_HTMiK28E,4700
|
|
25
|
+
duckdb_kernel/magics/MagicState.py,sha256=Vt_KwUwQP3446c1snSxS68Skl5AZQzrJd2Q1ETpKuKI,344
|
|
26
|
+
duckdb_kernel/magics/__init__.py,sha256=ggxzDzDEsKMZzYsWw9JqYVJhciJPvPVYGV7oNo9Yp-E,239
|
|
27
27
|
duckdb_kernel/parser/DCParser.py,sha256=16c1mxa494KP9OreUKQHsSQKoDGZ7NNp2u_gi_D-dkw,2293
|
|
28
28
|
duckdb_kernel/parser/LogicParser.py,sha256=_vZwE5OPRUEN8aEC_fSZAYKR_dpexqNthXog9OFHYRY,1233
|
|
29
29
|
duckdb_kernel/parser/ParserError.py,sha256=qJQVloFtID1HgVDQ1Io247bODT1ic3oO9Z1ZrWR-2Mk,321
|
|
@@ -93,7 +93,7 @@ duckdb_kernel/visualization/lib/__init__.py,sha256=LYi0YPtn5fXOejbLIqbt_3KzP-Xrw
|
|
|
93
93
|
duckdb_kernel/visualization/lib/plotly-3.0.1.min.js,sha256=oy6Be7Eh6eiQFs5M7oXuPxxm9qbJXEtTpfSI93dW16Q,4653932
|
|
94
94
|
duckdb_kernel/visualization/lib/ra.css,sha256=foz1v69EQ117BDduB9QyHH978PbRs2TG1kBS4VGqZbI,57
|
|
95
95
|
duckdb_kernel/visualization/lib/ra.js,sha256=VzMRn55ztcd5Kfu2B6gdRPARpi8n-fvs8oNFnfp55Ec,1845
|
|
96
|
-
jupyter_duckdb-1.2.
|
|
97
|
-
jupyter_duckdb-1.2.
|
|
98
|
-
jupyter_duckdb-1.2.
|
|
99
|
-
jupyter_duckdb-1.2.
|
|
96
|
+
jupyter_duckdb-1.2.103.dist-info/METADATA,sha256=KF4yFiFhppasCLN85BH8KVdKBOudeEbFIkplYy08rMg,9132
|
|
97
|
+
jupyter_duckdb-1.2.103.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
|
|
98
|
+
jupyter_duckdb-1.2.103.dist-info/top_level.txt,sha256=KvRRPMnmkQNuhyBsXoPmwyt26LRDp0O-0HN6u0Dm5jA,14
|
|
99
|
+
jupyter_duckdb-1.2.103.dist-info/RECORD,,
|
|
File without changes
|