jupyter-duckdb 1.2.102__py3-none-any.whl → 1.2.104__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.
@@ -10,6 +10,9 @@ class Connection:
10
10
  def close(self):
11
11
  pass
12
12
 
13
+ def copy(self) -> 'Connection':
14
+ raise NotImplementedError
15
+
13
16
  @staticmethod
14
17
  def plain_explain() -> bool:
15
18
  return False
@@ -15,6 +15,9 @@ class Connection(Base):
15
15
  def close(self):
16
16
  self.con.close()
17
17
 
18
+ def copy(self) -> 'Connection':
19
+ return Connection(self.path)
20
+
18
21
  @staticmethod
19
22
  def plain_explain() -> bool:
20
23
  return True
@@ -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}'
@@ -16,6 +16,9 @@ class Connection(Base):
16
16
  def close(self):
17
17
  self.con.close()
18
18
 
19
+ def copy(self) -> 'Connection':
20
+ return Connection(self.path)
21
+
19
22
  @staticmethod
20
23
  def multiple_statements_per_query() -> bool:
21
24
  return False
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('with_tests').on(self._create_magic),
44
- MagicCommand('load').arg('database').opt('with_tests').on(self._load_magic),
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: Optional[Connection] = None
64
- self._tests: Optional[Dict] = None
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 is None:
103
- # If the provided path looks like a postgres url,
104
- # we want to use the postgres driver.
105
- if path.startswith(('postgresql://', 'postgres://', 'pgsql://', 'psql://', 'pg://')):
106
- # pull data from connection string
107
- re_expr = r'(postgresql|postgres|pgsql|psql|pg)://((.*?)(:(.*?))?@)?(.*?)(:(\d+))?(/(.*))?'
108
- match = re.fullmatch(re_expr, path)
109
-
110
- host = match.group(6)
111
- port = int(match.group(8)) if match.group(8) is not None else 5432
112
- username = match.group(3)
113
- password = match.group(5)
114
- database_name = match.group(10)
115
-
116
- # load and create instance
117
- try:
118
- from .db.implementation.postgres import Connection as Postgres
119
- self._db = Postgres(host, port, username, password, database_name)
120
- except ImportError:
121
- self.print('psycopg could not be found', name='stderr')
122
- return False
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 = 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
- # 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')
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
- from .db.implementation.sqlite import Connection as SQLite
138
- self._db = SQLite(path)
137
+ from .db.implementation.sqlite import Connection as SQLite
138
+ self._db[name] = SQLite(path)
139
139
 
140
- return True
141
- else:
142
- return False
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, silent: bool,
153
- column_name_mapping: Dict[str, str],
154
- max_rows: Optional[int]) -> Tuple[Optional[List[str]], Optional[List[List]]]:
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 = self._db.execute(query)
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 self._db.plain_explain():
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
- return None, None
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, path: str, of: Optional[str], with_tests: Optional[str]):
224
- self._load(silent, path, True, of, with_tests)
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)
225
228
 
226
- def _load_magic(self, silent: bool, path: str, with_tests: Optional[str]):
227
- self._load(silent, path, False, None, with_tests)
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'
234
+
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
- if self._load_database(path):
249
- if not silent:
250
- # self.print(f'loaded database "{path}"\n')
251
- self.print(str(self._db) + '\n')
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 self._db.multiple_statements_per_query():
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
- self._db.execute(statement)
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
- self._db.execute(content)
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
- self._db.execute(f'CREATE TABLE {table} AS SELECT * FROM transfer_df')
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
- # load tests
298
- if with_tests is None:
299
- self._tests = {}
300
- else:
301
- with open(with_tests, 'r', encoding='utf-8') as tests_file:
302
- self._tests = json.load(tests_file)
303
- for test in self._tests.values():
304
- if 'attributes' in test:
305
- rows = {k: [] for k in test['attributes']}
306
- for row in test['equals']:
307
- for k, v in zip(test['attributes'], row):
308
- rows[k].append(v)
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}')
309
308
 
310
- test['equals'] = rows
309
+ if not silent:
310
+ self.print(f'--- connection {target} ---\n')
311
311
 
312
- self.print(f'loaded tests from {with_tests}\n')
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')
313
323
 
314
- def _test_magic(self, silent: bool, result_columns: List[str], result: List[List], name: str):
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)
339
+
340
+ test['equals'] = rows
341
+
342
+ self.print(f'loaded tests from {tests}\n')
343
+
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
- return {
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
- def _query_max_rows_magic(self, silent: bool, count: str):
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 _schema_magic(self, silent: bool, td: bool, only: Optional[str]):
420
- if self._db is None:
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 = self._db.analyze()
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, result_columns: List[str], result: List[List], file: str, noheader: 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, code: str, analyze: 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 = self._db.analyze()
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(self._db, root_node, tables)
537
+ vd = RATreeDrawer(state.db, root_node, tables)
538
+
508
539
  svg = vd.to_interactive_svg()
509
- data = {
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
- else:
517
- svg = ''
518
- data = {
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
- # return data
523
- self.print_data(svg)
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, code: str):
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 = self._db.analyze()
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
- return {
555
- 'generated_code': sql,
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,45 @@ 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, code: str):
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
- return self._dc_magic(silent, code)
606
+ self._dc_magic(silent, state)
607
+ return
585
608
  except ParserError as e:
586
609
  if e.depth > 0:
587
610
  raise e
588
611
 
589
612
  # try to parse RA
590
613
  try:
591
- return self._ra_magic(silent, code, analyze=False)
614
+ self._ra_magic(silent, state, analyze=False)
615
+ return
592
616
  except ParserError as e:
593
617
  if e.depth > 0:
594
618
  raise e
595
619
 
596
- def _plotly_magic(self, silent: bool, cols: List, rows: List[Tuple], type: str, mapping: str, title: str = None):
620
+ def _plotly_magic(self, silent: bool, state: MagicState,
621
+ cols: List, rows: List[Tuple],
622
+ type: str, mapping: str, title: str = None):
597
623
  # split mapping and handle asterisks
598
624
  mapping = [m.strip() for m in mapping.split(',')]
599
625
 
600
626
  for i in range(len(mapping)):
601
627
  if mapping[i] == '*':
602
- mapping = mapping[:i] + cols + mapping[i+1:]
628
+ mapping = mapping[:i] + cols + mapping[i + 1:]
603
629
 
604
630
  # convert all column names to lower case
605
631
  lower_cols = [c.lower() for c in cols]
@@ -668,7 +694,9 @@ class DuckDBKernel(Kernel):
668
694
  # finally print the code
669
695
  self.print_data(html, mime='text/html')
670
696
 
671
- def _plotly_raw_magic(self, silent: bool, cols: List, rows: List[Tuple], title: str = None):
697
+ def _plotly_raw_magic(self, silent: bool, state: MagicState,
698
+ cols: List, rows: List[Tuple],
699
+ title: str = None):
672
700
  if len(cols) != 1 and len(rows) != 1:
673
701
  raise ValueError(f'expected exactly one column and one row')
674
702
 
@@ -683,34 +711,27 @@ class DuckDBKernel(Kernel):
683
711
  **kwargs):
684
712
  try:
685
713
  # get magic command
686
- clean_code, pre_query_callbacks, post_query_callbacks = self._magics(silent, code)
714
+ if len(self._db) > 0:
715
+ init_db = self._db[list(self._db.keys())[0]]
716
+ else:
717
+ init_db = None
687
718
 
688
- # execute magic commands here if it does not depend on query results
689
- execution_args = {
690
- 'max_rows': DuckDBKernel.DEFAULT_MAX_ROWS
691
- }
719
+ magic_state = MagicState(init_db, code, DuckDBKernel.DEFAULT_MAX_ROWS)
720
+ pre_query_callbacks, post_query_callbacks = self._magics(silent, magic_state)
692
721
 
722
+ # execute magic commands here if it does not depend on query results
693
723
  for callback in pre_query_callbacks:
694
- execution_args.update(callback())
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'] = {}
724
+ callback()
704
725
 
705
726
  # execute statement if needed
706
727
  cols, rows = None, None
707
728
 
708
- if not isinstance(clean_code, dict):
709
- clean_code = {'default': clean_code}
729
+ if not isinstance(magic_state.code, dict):
730
+ magic_state.code = {'default': magic_state.code}
710
731
 
711
- for name, code in clean_code.items():
732
+ for name, code in magic_state.code.items():
712
733
  if code.strip():
713
- cols, rows = self._execute_stmt(name, code, silent, **execution_args)
734
+ cols, rows = self._execute_stmt(silent, magic_state, name, code)
714
735
 
715
736
  # execute magic command here if it does depend on query results
716
737
  for callback in post_query_callbacks:
@@ -734,5 +755,7 @@ class DuckDBKernel(Kernel):
734
755
  }
735
756
 
736
757
  def do_shutdown(self, restart):
737
- self._unload_database()
758
+ for name in list(self._db.keys()):
759
+ self._unload_database(name)
760
+
738
761
  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, code: StringWrapper, *args, **kwargs):
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._code: StringWrapper = code
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
- result = self._mc(self._silent, self._code.value, *self._args, **self._kwargs)
22
- if 'generated_code' in result:
23
- self._code.value = result['generated_code']
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
- return self._mc(self._silent, *self._args, **self._kwargs)
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 .StringWrapper import StringWrapper
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, code: str) -> Tuple[str, List[MagicCommandCallback], List[MagicCommandCallback]]:
22
- code_wrapper = StringWrapper()
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, code_wrapper, **flags, **optionals)
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, code_wrapper, *args, **flags, **optionals)
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
- code_wrapper.value = code
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] = {}
@@ -2,3 +2,4 @@ from .MagicCommand import MagicCommand
2
2
  from .MagicCommandCallback import MagicCommandCallback
3
3
  from .MagicCommandException import MagicCommandException
4
4
  from .MagicCommandHandler import MagicCommandHandler
5
+ from .MagicState import MagicState
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: jupyter-duckdb
3
- Version: 1.2.102
3
+ Version: 1.2.104
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
- last optional parameter `WITH_TESTS` is described in
122
- detail [below](#ship-tests-with-your-notebooks).
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`! `WITH_TESTS` on
130
- the other hand is available also with this magic command.)
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
- Only one database can be open at any time. If a new database is created or
137
- loaded, the current one is closed first and saved to disk if necessary.
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 together with the database with the help of
217
- the `WITH_TESTS` parameter. These tests are stored as a JSON file. Each test is
218
- assigned a unique name, a result set and whether the test should check the order
219
- of the result. A very simple test file looks like the following JSON object:
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=DIixbtvXldvVlmf_6jgUWWC_Mu5UY82oebHaJCrH90Y,27236
4
+ duckdb_kernel/kernel.py,sha256=bI9qO4svrRqGJUXGqlOrdAwYmaC8PjMMQjsl1lGi9d4,28227
5
5
  duckdb_kernel/db/Column.py,sha256=GM5P6sFdlYK92hiKln5-6038gIDOTxh1AYbR4kiga_w,559
6
- duckdb_kernel/db/Connection.py,sha256=5pH-CwGh-r9Q2QwJKGSxvoINBU-sqmvZyG4Q1digfeE,599
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=uBL-UVrSo8EJNXnnyAc2AwAYJ1QQQmH0noV6pnngZHs,6977
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=39wv-mvKHdu4u_ADFiSbAvGMVEs3FtuzRYIH4uzJ-pw,9307
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=SSLpdigWjO_3lUXdsx3eH5OXOTBlZiXOgIyPCf6L8D0,7044
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=9XVidNtzs8Lwq_T59VrH89t5LUgJcc5C7grusElXVW4,1041
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=noRm22EsfTZI_FL_N15f6NYbZSPvejvS24rlA5hkDkQ,4730
25
- duckdb_kernel/magics/StringWrapper.py,sha256=W6qIfeHU51do1edd6yXgw6K7orzjwSHU4oWAI5DuKEE,96
26
- duckdb_kernel/magics/__init__.py,sha256=DA8gnQeRCUt1Scy3_NQ9w5CPmMEY9i8YwB-g392pN1U,204
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.102.dist-info/METADATA,sha256=020EZdonW3kvcFer9t5LsfTlgDI5s1WhgJyvzgAYJjk,9115
97
- jupyter_duckdb-1.2.102.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
98
- jupyter_duckdb-1.2.102.dist-info/top_level.txt,sha256=KvRRPMnmkQNuhyBsXoPmwyt26LRDp0O-0HN6u0Dm5jA,14
99
- jupyter_duckdb-1.2.102.dist-info/RECORD,,
96
+ jupyter_duckdb-1.2.104.dist-info/METADATA,sha256=F-V4ffa6OXgxrTAKjmSH8H4hP4NB1VjjItZqhfraCyA,9132
97
+ jupyter_duckdb-1.2.104.dist-info/WHEEL,sha256=DK49LOLCYiurdXXOXwGJm6U4DkHkg4lcxjhqwRa0CP4,91
98
+ jupyter_duckdb-1.2.104.dist-info/top_level.txt,sha256=KvRRPMnmkQNuhyBsXoPmwyt26LRDp0O-0HN6u0Dm5jA,14
99
+ jupyter_duckdb-1.2.104.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.1.0)
2
+ Generator: setuptools (78.0.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,3 +0,0 @@
1
- class StringWrapper:
2
- def __init__(self, value: str = None):
3
- self.value: str = value