sqlite-bro 0.13.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,2307 @@
1
+ #!/usr/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ from __future__ import print_function, unicode_literals, division # Python2.7
4
+
5
+ try:
6
+ import argparse # Python>=3.2
7
+ except ImportError:
8
+ pass # Python<3.2
9
+
10
+ import sqlite3 as sqlite
11
+ import sys
12
+ import os
13
+ import locale
14
+ import csv
15
+ import datetime
16
+ import io
17
+ import shlex # Simple lexical analysis
18
+ from os.path import expanduser
19
+ import tempfile as tmpf
20
+ import subprocess
21
+
22
+ try: # We are Python 3.3+
23
+ from tkinter import *
24
+ from tkinter import font, ttk, filedialog, messagebox
25
+ from tkinter.ttk import *
26
+ except ImportError: # or we are still Python2.7
27
+ from Tkinter import *
28
+ import Tkinter as tkinter
29
+ import tkFont as font
30
+ import tkFileDialog as filedialog
31
+ import tkMessageBox as messagebox
32
+ from ttk import *
33
+ import ttk as ttk
34
+
35
+ tipwindow = None
36
+
37
+ # starting Python-3.13, PEP 667 forces us to us a specific dictionnary pydef_locals instead of locals()
38
+ pydef_locals ={}
39
+
40
+ class App:
41
+ """the GUI graphic application"""
42
+
43
+ def __init__(self, use_gui=True):
44
+ """create a tkk graphic interface with a main window tk_win"""
45
+ self.__version__ = "0.13.1"
46
+ self._title = "of 2024-05-11b : 'Setup me down !'"
47
+ self.conn = None # Baresql database object
48
+ self.database_file = ""
49
+ self.initialdir = "."
50
+
51
+ # Do we use a GUI ?
52
+ self.use_gui = use_gui # gui ok by default
53
+ try:
54
+ self.tk_win = Tk()
55
+ except:
56
+ self.use_gui = False
57
+
58
+ if self.use_gui:
59
+ self.tk_win.title(
60
+ "A graphic SQLite Client in 1 Python file (" + self.__version__ + ")"
61
+ )
62
+ self.tk_win.option_add("*tearOff", FALSE) # hint of tk documentation
63
+ self.tk_win.minsize(600, 200) # minimal size
64
+
65
+ self.font_size = 10
66
+ self.font_wheight = 0
67
+ # With a Menubar and Toolbar
68
+ self.create_menu()
69
+ self.create_toolbar()
70
+
71
+ # Create style "ButtonNotebook"
72
+ self.create_style()
73
+ # Initiate Drag State
74
+ self.state_drag = False
75
+ self.state_drag_index = 0
76
+
77
+ # With a Panedwindow of two frames: 'Database' and 'Queries'
78
+ p = ttk.Panedwindow(self.tk_win, orient=HORIZONTAL)
79
+ p.pack(fill=BOTH, expand=1)
80
+
81
+ f_database = ttk.Labelframe(p, text="Databases", width=200, height=100)
82
+ p.add(f_database)
83
+ f_queries = ttk.Labelframe(p, text="Queries", width=200, height=100)
84
+ p.add(f_queries)
85
+
86
+ # build tree view 't' inside the left 'Database' Frame
87
+ self.db_tree = ttk.Treeview(
88
+ f_database, displaycolumns=[], columns=("detail", "action")
89
+ )
90
+ self.db_tree.tag_configure("run")
91
+ self.db_tree.pack(fill=BOTH, expand=1)
92
+
93
+ # create a notebook 'n' inside the right 'Queries' Frame
94
+ self.n = NotebookForQueries(self.tk_win, f_queries, [], self.use_gui)
95
+
96
+ # Bind keyboard shortcuts
97
+ self.tk_win.bind("<F9>", self.run_tab)
98
+ else:
99
+ # create a GUI-Less notebook 'n'
100
+ self.n = NotebookForQueries(None, None, [], self.use_gui)
101
+
102
+ # define default home directory
103
+ self.home = expanduser("~")
104
+
105
+ # defaults for export
106
+ self.default_header = True
107
+ self.default_separator = ","
108
+ self.current_directory = os.getcwd()
109
+
110
+ # Initiate Output State
111
+ self.once_mode = False
112
+ self.encode_in = "utf-8"
113
+ self.output_file = None
114
+ self.init_output = True
115
+ self.output_mode = False
116
+ self.x_mode = False
117
+
118
+ def create_menu(self):
119
+ """create the menu of the application"""
120
+ menubar = Menu(self.tk_win)
121
+ self.tk_win["menu"] = menubar
122
+
123
+ # feeding the top level menu
124
+ self.menu = Menu(menubar)
125
+ menubar.add_cascade(menu=self.menu, label="Database")
126
+ self.menu_help = Menu(menubar)
127
+ menubar.add_cascade(menu=self.menu_help, label="?")
128
+
129
+ # feeding database sub-menu
130
+ self.menu.add_command(label="New Database", command=self.new_db)
131
+ self.menu.add_command(
132
+ label="New In-Memory Database", command=lambda: self.new_db(":memory:")
133
+ )
134
+ self.menu.add_command(label="Open Database ...", command=self.open_db)
135
+ self.menu.add_command(
136
+ label="Open Database ...(legacy auto-commit)",
137
+ command=lambda: self.open_db(""),
138
+ )
139
+ self.menu.add_command(label="Close Database", command=self.close_db)
140
+ self.menu.add_separator()
141
+ self.menu.add_command(label="Attach Database", command=self.attach_db)
142
+ if sys.version_info[:2] >= (3, 7):
143
+ self.menu.add_separator()
144
+ self.menu.add_command(label="Backup main Database", command=self.backup_db)
145
+ self.menu.add_command(
146
+ label="Restore into main Database", command=self.restore_db
147
+ )
148
+ self.menu.add_separator()
149
+ self.menu.add_command(label="Quit", command=self.quit_db)
150
+
151
+ self.menu_help.add_command(
152
+ label="about",
153
+ command=lambda: messagebox.showinfo(
154
+ message="""
155
+ \nSQLite_bro : a graphic SQLite Client in 1 Python file
156
+ \nVersion """
157
+ + self.__version__
158
+ + " "
159
+ + self._title
160
+ + "\n(https://github.com/stonebig/sqlite_bro)"
161
+ + "\n\nrun by: "
162
+ + sys.executable
163
+ + "\n\nhome: "
164
+ + self.home
165
+ + "\n\ncurrent directory: "
166
+ + os.getcwd()
167
+ ),
168
+ )
169
+
170
+ def create_toolbar(self):
171
+ """create the toolbar of the application"""
172
+ self.toolbar = Frame(self.tk_win, relief=RAISED)
173
+ self.toolbar.pack(side=TOP, fill=X)
174
+ self.tk_icon = self.get_tk_icons()
175
+
176
+ # list of (image, action, tooltip) :
177
+ to_show = [
178
+ ("refresh_img", self.actualize_db, "Actualize databases"),
179
+ ("run_img", self.run_tab, "Run script selection"),
180
+ (
181
+ "newtab_img",
182
+ lambda x=self: x.n.new_query_tab("___", ""),
183
+ "Create a new script",
184
+ ),
185
+ ("csvin_img", self.import_csvtb, "Import a CSV file into a table"),
186
+ ("csvex_img", self.export_csvtb, "Export selected table to a CSV file"),
187
+ ("dbdef_img", self.savdb_script, "Save main database as a SQL script"),
188
+ ("qryex_img", self.export_csvqr, "Export script selection to a CSV file"),
189
+ (
190
+ "exe_img",
191
+ self.exsav_script,
192
+ "Run script+output to a file (First 200 rec. per Qry)",
193
+ ),
194
+ ("sqlin_img", self.load_script, "Load a SQL script file"),
195
+ ("sqlsav_img", self.sav_script, "Save a SQL script in a file"),
196
+ ("chgsz_img", self.chg_fontsize, "Modify font size"),
197
+ (
198
+ "img_run_temp",
199
+ self.run_temp,
200
+ "Run script selection and Display output in temporary files",
201
+ ),
202
+ ("img_clean_temp", self.clean_temp, "Remove old temporary files"),
203
+ ]
204
+
205
+ for img, action, tip in to_show:
206
+ b = Button(self.toolbar, image=self.tk_icon[img], command=action)
207
+ b.pack(side=LEFT, padx=2, pady=2)
208
+ self.createToolTip(b, tip)
209
+
210
+ def set_initialdir(self, proposal):
211
+ """change initial dir, if possible"""
212
+ if os.path.isfile(proposal):
213
+ self.initialdir = os.path.dirname(proposal)
214
+
215
+ def new_db(self, filename=""):
216
+ """create a new database"""
217
+ if filename == "":
218
+ filename = filedialog.asksaveasfilename(
219
+ initialdir=self.initialdir,
220
+ defaultextension=".db",
221
+ title="Define a new database name and location",
222
+ filetypes=[("default", "*.db"), ("other", "*.db*"), ("all", "*.*")],
223
+ )
224
+ if filename != "":
225
+ self.database_file = filename
226
+ if os.path.isfile(filename):
227
+ self.set_initialdir(filename)
228
+ if messagebox.askyesno(
229
+ message="Confirm Destruction of previous Datas ?",
230
+ icon="question",
231
+ title="Destroying",
232
+ ):
233
+ os.remove(filename)
234
+ self.conn = Baresql(self.database_file)
235
+ self.actualize_db()
236
+
237
+ def open_db(self, filename="", isolation_level=None):
238
+ """open an existing database"""
239
+ if filename == "":
240
+ filename = filedialog.askopenfilename(
241
+ initialdir=self.initialdir,
242
+ defaultextension=".db",
243
+ filetypes=[("default", "*.db"), ("other", "*.db*"), ("all", "*.*")],
244
+ )
245
+ if filename != "":
246
+ self.set_initialdir(filename)
247
+ self.database_file = filename
248
+ self.conn = Baresql(self.database_file)
249
+ self.actualize_db()
250
+
251
+ def backup_db(self, filename="", isolation_level=None):
252
+ """Backup the current database"""
253
+ if filename == "":
254
+ filename = filedialog.asksaveasfilename(
255
+ initialdir=self.initialdir,
256
+ defaultextension=".db",
257
+ title="Define a new database name and location",
258
+ filetypes=[("default", "*.db"), ("other", "*.db*"), ("all", "*.*")],
259
+ )
260
+ if filename != "":
261
+ if os.path.isfile(filename):
262
+ self.set_initialdir(filename)
263
+ if messagebox.askyesno(
264
+ message="Confirm Destruction of previous Datas ?",
265
+ icon="question",
266
+ title="Destroying",
267
+ ):
268
+ os.remove(filename)
269
+ db_to = sqlite.connect(filename)
270
+ self.conn.conn.backup(db_to)
271
+ db_to.close()
272
+ self.actualize_db()
273
+
274
+ def restore_db(self, filename="", isolation_level=None):
275
+ """Restore an existing database into current one"""
276
+ if filename == "":
277
+ filename = filedialog.askopenfilename(
278
+ initialdir=self.initialdir,
279
+ defaultextension=".db",
280
+ filetypes=[("default", "*.db"), ("other", "*.db*"), ("all", "*.*")],
281
+ )
282
+ if filename != "":
283
+ db_from = sqlite.connect(filename)
284
+ db_from.backup(self.conn.conn)
285
+ db_from.close
286
+ self.actualize_db()
287
+
288
+ def load_script(self):
289
+ """load a script file, ask validation of detected Python code"""
290
+ filename = filedialog.askopenfilename(
291
+ initialdir=self.initialdir,
292
+ defaultextension=".sql",
293
+ filetypes=[("default", "*.sql"), ("other", "*.txt"), ("all", "*.*")],
294
+ )
295
+ if filename != "":
296
+ self.set_initialdir(filename)
297
+ text = os.path.split(filename)[1].split(".")[0]
298
+ with io.open(filename, encoding=guess_encoding(filename)[0]) as f:
299
+ script = f.read()
300
+ sqls = self.conn.get_sqlsplit(script, remove_comments=True)
301
+ dg = [
302
+ s
303
+ for s in sqls
304
+ if s.strip(" \t\n\r")[:5].lower() in ("pydef", ".read", ".shel")
305
+ or s.strip(" \t\n\r")[:1].lower() == "."
306
+ ]
307
+ if dg:
308
+ fields = [
309
+ "",
310
+ ["In Script File:", filename, "r", 100],
311
+ "",
312
+ ["non pure SQL code", "\n".join(dg), "r", 80, 20],
313
+ ]
314
+
315
+ create_dialog(
316
+ ("Ok for this non pure SQL code ?"),
317
+ fields,
318
+ ("Confirm", self.load_script_ok),
319
+ [text, script],
320
+ )
321
+ else:
322
+ new_tab_ref = self.n.new_query_tab(text, script)
323
+
324
+ def load_script_ok(self, thetop, entries, actions):
325
+ """continue loading of script after confirmation dialog"""
326
+ new_tab_ref = self.n.new_query_tab(*actions)
327
+ thetop.destroy()
328
+
329
+ def savdb_script(self):
330
+ """save database as a script file"""
331
+ filename = filedialog.asksaveasfilename(
332
+ initialdir=self.initialdir,
333
+ defaultextension=".db",
334
+ title="save database structure in a text file",
335
+ filetypes=[("default", "*.sql"), ("other", "*.txt"), ("all", "*.*")],
336
+ )
337
+ if filename != "":
338
+ self.set_initialdir(filename)
339
+ with io.open(filename, "w", encoding="utf-8") as f:
340
+ for line in self.conn.iterdump():
341
+ f.write("%s\n" % line)
342
+
343
+ def sav_script(self):
344
+ """save a script in a file"""
345
+ active_tab_id = self.n.notebook.select()
346
+ if active_tab_id != "":
347
+ # get current selection (or all)
348
+ fw = self.n.fw_labels[active_tab_id]
349
+ script = fw.get(1.0, END)[:-1]
350
+ filename = filedialog.asksaveasfilename(
351
+ initialdir=self.initialdir,
352
+ defaultextension=".db",
353
+ title="save script in a sql file",
354
+ filetypes=[("default", "*.sql"), ("other", "*.txt"), ("all", "*.*")],
355
+ )
356
+ if filename != "":
357
+ self.set_initialdir(filename)
358
+ with io.open(filename, "w", encoding="utf-8") as f:
359
+ if "你好 мир Artisou à croute" not in script:
360
+ f.write("/*utf-8 tag : 你好 мир Artisou à croute*/\n")
361
+ f.write(script)
362
+
363
+ def attach_db(self, filename="", attach_as=""):
364
+ """attach an existing database"""
365
+ if filename == "":
366
+ filename = filedialog.askopenfilename(
367
+ initialdir=self.initialdir,
368
+ defaultextension=".db",
369
+ title="Choose a database to attach ",
370
+ filetypes=[("default", "*.db"), ("other", "*.db*"), ("all", "*.*")],
371
+ )
372
+ if attach_as == "":
373
+ attach = os.path.basename(filename).split(".")[0]
374
+ else:
375
+ attach = attach_as
376
+ avoid = {i[1]: 0 for i in get_leaves(self.conn, "attached_databases")}
377
+ att, indice = attach, 0
378
+ while attach in avoid:
379
+ attach, indice = att + "_" + str(indice), indice + 1
380
+ if filename != "":
381
+ self.set_initialdir(filename)
382
+ attach_order = "ATTACH DATABASE '%s' as '%s' " % (filename, attach)
383
+ self.conn.execute(attach_order)
384
+ self.actualize_db()
385
+
386
+ def close_db(self):
387
+ """close the database"""
388
+ self.conn.close
389
+ self.new_db(":memory:")
390
+ self.actualize_db()
391
+
392
+ def actualize_db(self):
393
+ """refresh the database view"""
394
+ if not self.use_gui:
395
+ return
396
+
397
+ # bind double-click for easy user interaction
398
+ self.db_tree.tag_bind("run", "<Double-1>", self.t_doubleClicked)
399
+ self.db_tree.tag_bind("run_up", "<Double-1>", self.t_doubleClicked)
400
+
401
+ # delete existing tree entries before re-creating them
402
+ for node in self.db_tree.get_children():
403
+ self.db_tree.delete(node)
404
+ # create top node
405
+ dbtext = os.path.basename(self.database_file)
406
+ id0 = self.db_tree.insert(
407
+ "", 0, "Database", text="main (%s)" % dbtext, values=(dbtext, "")
408
+ )
409
+ # add Database Objects, by Category
410
+ for categ in ["master_table", "table", "view", "trigger", "index", "pydef"]:
411
+ self.feed_dbtree(id0, categ, "main")
412
+ # for attached databases
413
+ for att_db in self.feed_dbtree(id0, "attached_databases"):
414
+ # create another top node
415
+ dbtext2, insert_position = att_db + " (Attached)", "end"
416
+ if att_db == "temp":
417
+ dbtext2, insert_position = "temp (%s)" % dbtext, 0
418
+ id0 = self.db_tree.insert(
419
+ "", insert_position, dbtext2, text=dbtext2, values=(att_db, "")
420
+ )
421
+ # add attached Database Objects, by Category
422
+ for categ in ["master_table", "table", "view", "trigger", "index"]:
423
+ self.feed_dbtree(id0, categ, att_db)
424
+ # update time of last refresh
425
+ self.db_tree.heading(
426
+ "#0", text=(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
427
+ )
428
+
429
+ def quit_db(self):
430
+ """quit the application"""
431
+ if messagebox.askyesno(
432
+ message="Are you sure you want to quit ?", icon="question", title="Quiting"
433
+ ):
434
+ self.tk_win.destroy()
435
+
436
+ def run_tab(self, event=None):
437
+ """clear previous results and run current script of a tab"""
438
+ if not self.use_gui:
439
+ active_tab_id = len(self.n.nongui_tabs) - 1
440
+ script = self.n.nongui_tabs[active_tab_id]
441
+ # print ("silently running,active_tab_id , script) # testtmp
442
+ self.create_and_add_results(script, active_tab_id)
443
+ return
444
+ active_tab_id = self.n.notebook.select()
445
+ if active_tab_id != "":
446
+ # remove previous results
447
+ self.n.remove_treeviews(active_tab_id)
448
+ # get current selection (or all)
449
+ fw = self.n.fw_labels[active_tab_id]
450
+ try:
451
+ script = fw.get("sel.first", "sel.last")
452
+ except:
453
+ script = fw.get(1.0, END)[:-1]
454
+ self.create_and_add_results(script, active_tab_id)
455
+ fw.focus_set() # workaround bug http://bugs.python.org/issue17511
456
+
457
+ def exsav_script(self):
458
+ """write script commands + top results to a log file"""
459
+ # idea from http://blog.mp933.fr/post/2014/05/15/Script-vs.-copy/paste
460
+ active_tab_id = self.n.notebook.select()
461
+ if active_tab_id != "":
462
+ # get current selection (or all)
463
+ fw = self.n.fw_labels[active_tab_id]
464
+ script = fw.get(1.0, END)[:-1]
465
+ filename = filedialog.asksaveasfilename(
466
+ initialdir=self.initialdir,
467
+ defaultextension=".db",
468
+ title="execute Script + output in a log file",
469
+ filetypes=[("default", "*.txt"), ("other", "*.log"), ("all", "*.*")],
470
+ )
471
+ if filename == "":
472
+ return
473
+ self.set_initialdir(filename)
474
+ with io.open(filename, "w", encoding="utf-8") as f:
475
+ if "你好 мир Artisou à croute" not in script:
476
+ f.write("/*utf-8 tag : 你好 мир Artisou à croute*/\n")
477
+ self.create_and_add_results(script, active_tab_id, limit=99, log=f)
478
+ fw.focus_set() # workaround bug http://bugs.python.org/issue17511
479
+
480
+ def run_temp(self):
481
+ """run selected script commands and display results via tmp files"""
482
+ # backup existing defaults
483
+ once_mode_bkp = self.once_mode
484
+ encode_in_bkp = self.encode_in
485
+ output_file_bkp = self.output_file
486
+ init_output_bkp = self.init_output
487
+ output_mode_bkp = self.output_mode
488
+ x_mode_bkp = self.x_mode
489
+
490
+ active_tab_id = self.n.notebook.select()
491
+ if active_tab_id != "":
492
+ self.n.remove_treeviews(active_tab_id)
493
+
494
+ if active_tab_id != "":
495
+ # get current selection (or all)
496
+ fw = self.n.fw_labels[active_tab_id]
497
+ fw = self.n.fw_labels[active_tab_id]
498
+ try:
499
+ query = fw.get("sel.first", "sel.last")
500
+ except:
501
+ query = fw.get(1.0, END)[:-1]
502
+ query = "\n.output --bom -x \n" + query
503
+ # print("run temp", query)
504
+ self.create_and_add_results(query, active_tab_id)
505
+ fw.focus_set() # workaround bug http://bugs.python.org/issue17511
506
+
507
+ # restore previous defaults
508
+ self.once_mode = once_mode_bkp
509
+ self.encode_in = encode_in_bkp
510
+ self.output_file = output_file_bkp
511
+ self.init_output = init_output_bkp
512
+ self.output_mode = output_mode_bkp
513
+ self.x_mode = x_mode_bkp
514
+
515
+ def chg_fontsize(self):
516
+ """change the display font size"""
517
+ sizes = [10, 13, 14]
518
+ font_types = [
519
+ "TkDefaultFont",
520
+ "TkTextFont",
521
+ "TkFixedFont",
522
+ "TkMenuFont",
523
+ "TkHeadingFont",
524
+ "TkCaptionFont",
525
+ "TkSmallCaptionFont",
526
+ "TkIconFont",
527
+ "TkTooltipFont",
528
+ ]
529
+ ww = ["normal", "bold"]
530
+ if self.font_size < max(sizes):
531
+ self.font_size = min([i for i in sizes if i > self.font_size])
532
+ else:
533
+ self.font_size = sizes[0]
534
+ self.font_wheight = 0
535
+
536
+ ff = "Helvetica" if self.font_size != min(sizes) else "Courier"
537
+ self.font_wheight = 0 if self.font_size == min(sizes) else 1
538
+ for typ in font_types:
539
+ default_font = font.nametofont(typ)
540
+ default_font.configure(
541
+ size=self.font_size, weight=ww[self.font_wheight], family=ff
542
+ )
543
+
544
+ def clean_temp(self):
545
+ """clear temp directory"""
546
+ ff = tmpf.NamedTemporaryFile(delete=True, suffix="_sqlite_bro.csv").name
547
+ temp_directory = os.path.dirname(ff)
548
+ report = [
549
+ ("", ""),
550
+ ]
551
+ for file in os.listdir(temp_directory):
552
+ if file.endswith("_sqlite_bro.csv"):
553
+ print("removing ", os.path.join(temp_directory, file))
554
+ try:
555
+ os.remove(os.path.join(temp_directory, file))
556
+ report += [
557
+ ("removing ", os.path.join(temp_directory, file)),
558
+ ]
559
+ except PermissionError:
560
+ report += [
561
+ ("PermissionError ", os.path.join(temp_directory, file)),
562
+ ]
563
+
564
+ active_tab_id = self.n.notebook.select()
565
+ if active_tab_id != "":
566
+ self.n.remove_treeviews(active_tab_id)
567
+ self.n.add_treeview(
568
+ active_tab_id,
569
+ ("Cleanup", "file"),
570
+ (report),
571
+ "Cleanup",
572
+ ".Cleaning tmp files",
573
+ )
574
+
575
+ def t_doubleClicked(self, event):
576
+ """launch action when dbl_click on the Database structure"""
577
+ # determine item to consider
578
+ selitem = self.db_tree.focus() # the item having the focus
579
+ seltag = self.db_tree.item(selitem, "tag")[0]
580
+ if seltag == "run_up": # 'run-up' tag ==> dbl-click 1 level up
581
+ selitem = self.db_tree.parent(selitem)
582
+ # get final information : text, selection and action
583
+ definition, action = self.db_tree.item(selitem, "values")
584
+ tab_text = self.db_tree.item(selitem, "text")
585
+ script = action + " limit 999 " if action != "" else definition
586
+
587
+ # create a new tab and run it if action suggest it
588
+ new_tab_ref = self.n.new_query_tab(tab_text, script)
589
+ if action != "":
590
+ self.run_tab() # run the new_tab created
591
+
592
+ def get_tk_icons(self):
593
+ """return a dictionary of icon_in_tk_format, from B64 images"""
594
+ # to create this base 64 from a toto.gif image of 24x24 size do :
595
+ # import base64
596
+ # b64 = base64.encodestring(open(r"toto.gif","rb").read())
597
+ # print("'gif_img': '''\\\n" + b64.decode("utf8") + "'''")
598
+ icons = {
599
+ "run_img": """\
600
+ R0lGODdhGAAYAJkAADOqM////wCqMwAAACwAAAAAGAAYAAACM4SPqcvt7wJ8oU5W8025b9OFW0hO
601
+ 5EmdKKauSosKL9zJC21FsK27kG+qfUC5IciITConBQA7
602
+ """,
603
+ "exe_img": """\
604
+ R0lGODdhGAAYALsAAP///zOqM/8AAGSJtqHA4Jyen3ul0+jo6Y6z2cLCwaSmpACqM4ODgmKGs4yM
605
+ jYOPniwAAAAAGAAYAAAEhBDISacqOBdWOy1HKB6F41mKwihH4r4kdypD0wx4rg8nUDSEoHAY5J0K
606
+ AyFiyWQaPY+kYUqtGp4dx26b60kE4LC3FwaPyeJOYM1ur8sCzxrgZovN6sDEHdYD4nkVb2BzPYUV
607
+ hIdyfouMi14BC5COgoqBHQttk5VumxJ1bJuZoJacpKE9EQA7
608
+ """,
609
+ "refresh_img": """\
610
+ R0lGODdhGAAYAJkAAP///zOqMwCqMwAAACwAAAAAGAAYAAACSoSPqcvt4aIJEFU5g7AUC9px1/JR
611
+ 3yYy4LqAils2IZdFMzCP6nhLd2/j6VqHD+1RAQKLHVfC+VwtcT3pNKOTYjTC4SOK+YbH5EYBADs=
612
+ """,
613
+ "newtab_img": """\
614
+ R0lGODdhGAAYAJkAAP///56fnQAAAAAAACwAAAAAGAAYAAACSoSPqcsm36KDsj1R1d00840E4ige
615
+ 3xWSo9YppRGwGKPGCUrXtfQCut3ouYC5IHEhTCSHRt4x5fytIlKSs0l9HpZKLcy7BXOhRUYBADs=
616
+ """,
617
+ "csvin_img": """\
618
+ R0lGODdhGAAYAMwAAPj4+AAAADOqM2FkZtjY2r7Awujo6V9gYeDg4b/Cwzc3N0pKSl9fX5GRkVVV
619
+ VXl6fKSmpLCxsouNkFdXV97d4N7e4N7g4IyMjZyen6SopwAAAAAAAAAAAAAAAAAAAAAAACwAAAAA
620
+ GAAYAAAFlSAgjkBgmuUZlONKii4br/MLv/Gt47ia/rYcT2bb0VowVFFF8+2K0KjUJqhOo1XBlaQV
621
+ Zbdc7Rc8ylrJ5THaa5YqFozBgOFQAMznl6FhsO37UwMEBgiFFRYIhANXBxgJBQUJkpAZi1MEBxAR
622
+ kI8REAMUVxIEcgcDpqYEElcODwSvsK8PllMLAxeQkA0DDmhvEwwLdmAhADs=
623
+ """,
624
+ "csvex_img": """\
625
+ R0lGODdhGAAYAMwAAPj4+AAAADOqM2FkZtjY2r7AwuDg4b/Cw+jo6V9gYTc3N0pKSlVVVV9fX5GR
626
+ kaSmpLCxsnl6fIuNkN7g4N7d4KSop4yMjZyen1dXVwAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAA
627
+ GAAYAAAFkiAgjkBgmuUZlONKii4br/MLv/Gt47ia/rYcT2bb0VowVFFF8+2K0Kh0JKhap1BrFZu9
628
+ cl9awZd03Y4BXvQ5DVUsGoNBg6FAm6MOhA3h4A4ICAaCBhOCCANYAxcHBQUHj44ViFMECQ8QjY0Q
629
+ DwMUWBIEcQkDo6MEElgMEQSsrawRk1MLAxaZBQ4DDGduGA0LdV8hADs=
630
+ """,
631
+ "qryex_img": """\
632
+ R0lGODdhGAAYAJkAAP///56fnQAAAP8AACwAAAAAGAAYAAACXIQPoporeR4yEtZ3J511e845zah1
633
+ oKV9WEQxqYOJX0rX9oDndp3jO6/7aXqDVOCIPB50Pk0yaQgCijSlITBt/p4B6ZbL3VkBYKxt7DTX
634
+ 0BN2uowUw+NndVq+tk8KADs=
635
+ """,
636
+ "sqlin_img": """\
637
+ R0lGODdhGAAYALsAAP///46z2Xul02SJtp6fnenp6f8AAMLCwaHA4IODgoCo01RymIOPnmKGswAA
638
+ AAAAACwAAAAAGAAYAAAEkRDIOYm9N9G9SfngR2jclnhHqh7FWAKZF84uFxdr3pb3TPOWEy6n2tkG
639
+ jcZgyWw6OwOEdEqtIgYbRjTA7Xq/WIoW8S17wxOteR1AS9Ts8sI08Aru+Px9TknU9YB5fBN+AYGH
640
+ gxJ+dwoCjY+OCpKNiQAGBk6ZTgsGE5edLy+XlqOhop+gpiWoqqGoqa0Ur7CxABEAOw==
641
+ """,
642
+ "sqlsav_img": """\
643
+ R0lGODdhGAAYALsAAP///56fnZyen/8AAGSJtqHA4Hul0+jo6Y6z2cLCwaSmpIODgmKGs4yMjYOP
644
+ ngAAACwAAAAAGAAYAAAEgxDISacSOItVOxVHKB5C41mKsihH4r4kdyoEwxB4rhMnIDCFoHAY5J0E
645
+ BCFiyWQaPY6kYUqtGp6dxm6b60kG4LC3FwaPyeJzpzzwaDQTsbkTqNsx3zmgfapPAnt6Y3Z1Amlq
646
+ AoR3cF5+EoqFY4k9jpSAfQKSkJCDm4SZXpN9l5aUoB4RADs=
647
+ """,
648
+ "dbdef_img": """\
649
+ R0lGODdhGAAYAMwAAPj4+DOqM2SJtmFkZqHA4NjY2sLCwejo6b7Awpyen3ul046z2aSop+Dg4V9g
650
+ YZGRkaSmpLCxsouNkDc3N2dxekpKSlVVVYyMjWKGs4ODgnl6fJ+goYOPnl9fX0xMTAAAACwAAAAA
651
+ GAAYAAAFuCAgjuTIJGiaZGVLJkcsH9DjmgxzMYfh/4fVDcAQCDDGpNI4hGAI0KgUKhgmBNGFdrut
652
+ 3jhYhXhMVnhdj6U6OWy7h4G4/O2Sx+n1OZ5kD5QmHgOCAx0TJXN3Iw8HLQc2InoAfiIUBQcNmJkN
653
+ BxSSiS0DCT8/CAYMA55DBQMQEQilCBGndBKrAw4Ot7kFEm+rG66vsRCob7WCube3vG8WGgXQ0dAa
654
+ nW8VAxfCCA8DFnsAExUWAxWGeCEAOw==
655
+ """,
656
+ "chgsz_img": """\
657
+ R0lGODdhGAAYAJkAAP///wAAADOqMwCqMywAAAAAGAAYAAACZISPGRvpb1iDRjy5KBBWYc0NXjQ9
658
+ A8cdDFkiZyiIwDpnCYqzCF2lr2rTHVKbDgsTJG52yE8R0nRSJA7qNOhpVbFPHhdhPF20w46S+f2h
659
+ xlzceksqu6ET7JwtLRrhwNt+1HdDUQAAOw==
660
+ """,
661
+ "img_close": """\
662
+ R0lGODlhCAAIAMIBAAAAADs7O4+Pj9nZ2Ts7Ozs7Ozs7Ozs7OyH+EUNyZWF0ZWQgd2l0aCBHSU1Q
663
+ ACH5BAEKAAQALAAAAAAIAAgAAAMVGDBEA0qNJyGw7AmxmuaZhWEU5kEJADs=
664
+ """,
665
+ "img_closeactive": """\
666
+ R0lGODlhCAAIAMIEAAAAAP/SAP/bNNnZ2cbGxsbGxsbGxsbGxiH5BAEKAAQALAAAAAAIAAgAAAMV
667
+ GDBEA0qNJyGw7AmxmuaZhWEU5kEJADs=
668
+ """,
669
+ "img_closepressed": """\
670
+ R0lGODdhCAAIAIgAAPAAAP///ywAAAAACAAIAAACDkyAeJYM7FR8Ex7aVpIFADs=
671
+ """,
672
+ "img_run_temp": """\
673
+ R0lGODlhGAAYAHAAACH5BAEAAPwALAAAAAAYABgAhwAAAAAAMwAAZgAAmQAAzAAA/wArAAArMwAr
674
+ ZgArmQArzAAr/wBVAABVMwBVZgBVmQBVzABV/wCAAACAMwCAZgCAmQCAzACA/wCqAACqMwCqZgCq
675
+ mQCqzACq/wDVAADVMwDVZgDVmQDVzADV/wD/AAD/MwD/ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMA
676
+ zDMA/zMrADMrMzMrZjMrmTMrzDMr/zNVADNVMzNVZjNVmTNVzDNV/zOAADOAMzOAZjOAmTOAzDOA
677
+ /zOqADOqMzOqZjOqmTOqzDOq/zPVADPVMzPVZjPVmTPVzDPV/zP/ADP/MzP/ZjP/mTP/zDP//2YA
678
+ AGYAM2YAZmYAmWYAzGYA/2YrAGYrM2YrZmYrmWYrzGYr/2ZVAGZVM2ZVZmZVmWZVzGZV/2aAAGaA
679
+ M2aAZmaAmWaAzGaA/2aqAGaqM2aqZmaqmWaqzGaq/2bVAGbVM2bVZmbVmWbVzGbV/2b/AGb/M2b/
680
+ Zmb/mWb/zGb//5kAAJkAM5kAZpkAmZkAzJkA/5krAJkrM5krZpkrmZkrzJkr/5lVAJlVM5lVZplV
681
+ mZlVzJlV/5mAAJmAM5mAZpmAmZmAzJmA/5mqAJmqM5mqZpmqmZmqzJmq/5nVAJnVM5nVZpnVmZnV
682
+ zJnV/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswAmcwAzMwA/8wrAMwrM8wrZswrmcwrzMwr
683
+ /8xVAMxVM8xVZsxVmcxVzMxV/8yAAMyAM8yAZsyAmcyAzMyA/8yqAMyqM8yqZsyqmcyqzMyq/8zV
684
+ AMzVM8zVZszVmczVzMzV/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A//8rAP8r
685
+ M/8rZv8rmf8rzP8r//9VAP9VM/9VZv9Vmf9VzP9V//+AAP+AM/+AZv+Amf+AzP+A//+qAP+qM/+q
686
+ Zv+qmf+qzP+q///VAP/VM//VZv/Vmf/VzP/V////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAA
687
+ AAiFAPcJHEiwoMGDBgEo3Lew4UKEAwEIlEiRoUWIEzNmVCgR48WOFkF6HEmypMmTAqWpXIky5Upp
688
+ LV2qPDikps2bNV/CNFhzH06bLGfyHDIQ50qi+3YW7Ekwp0qmCKESfCp1KESbGKsuzZAVKU2tTb0S
689
+ zHCz69KfZgX+xHqVKNm1YIvCxYkxIAA7
690
+ """,
691
+ "img_clean_temp": """\
692
+ R0lGODlhGAAYAHAAACwAAAAAGAAYAIcAAAAAADMAAGYAAJkAAMwAAP8AKwAAKzMAK2YAK5kAK8wA
693
+ K/8AVQAAVTMAVWYAVZkAVcwAVf8AgAAAgDMAgGYAgJkAgMwAgP8AqgAAqjMAqmYAqpkAqswAqv8A
694
+ 1QAA1TMA1WYA1ZkA1cwA1f8A/wAA/zMA/2YA/5kA/8wA//8zAAAzADMzAGYzAJkzAMwzAP8zKwAz
695
+ KzMzK2YzK5kzK8wzK/8zVQAzVTMzVWYzVZkzVcwzVf8zgAAzgDMzgGYzgJkzgMwzgP8zqgAzqjMz
696
+ qmYzqpkzqswzqv8z1QAz1TMz1WYz1Zkz1cwz1f8z/wAz/zMz/2Yz/5kz/8wz//9mAABmADNmAGZm
697
+ AJlmAMxmAP9mKwBmKzNmK2ZmK5lmK8xmK/9mVQBmVTNmVWZmVZlmVcxmVf9mgABmgDNmgGZmgJlm
698
+ gMxmgP9mqgBmqjNmqmZmqplmqsxmqv9m1QBm1TNm1WZm1Zlm1cxm1f9m/wBm/zNm/2Zm/5lm/8xm
699
+ //+ZAACZADOZAGaZAJmZAMyZAP+ZKwCZKzOZK2aZK5mZK8yZK/+ZVQCZVTOZVWaZVZmZVcyZVf+Z
700
+ gACZgDOZgGaZgJmZgMyZgP+ZqgCZqjOZqmaZqpmZqsyZqv+Z1QCZ1TOZ1WaZ1ZmZ1cyZ1f+Z/wCZ
701
+ /zOZ/2aZ/5mZ/8yZ///MAADMADPMAGbMAJnMAMzMAP/MKwDMKzPMK2bMK5nMK8zMK//MVQDMVTPM
702
+ VWbMVZnMVczMVf/MgADMgDPMgGbMgJnMgMzMgP/MqgDMqjPMqmbMqpnMqszMqv/M1QDM1TPM1WbM
703
+ 1ZnM1czM1f/M/wDM/zPM/2bM/5nM/8zM////AAD/ADP/AGb/AJn/AMz/AP//KwD/KzP/K2b/K5n/
704
+ K8z/K///VQD/VTP/VWb/VZn/Vcz/Vf//gAD/gDP/gGb/gJn/gMz/gP//qgD/qjP/qmb/qpn/qsz/
705
+ qv//1QD/1TP/1Wb/1Zn/1cz/1f///wD//zP//2b//5n//8z///8AAAAAAAAAAAAAAAAIcwD3CRxI
706
+ sKDBgwgTKlzIUKEfPw0T/qES8SAViBUL+vmTsSAVjh0H/sEYct/FkgJHojRJMqM9P1VaVrRHE2ZJ
707
+ e/BqhsRJM2fHnD1puuQJ9GdQewIBKN23tOnSfTR5FgSQlKlVqlQXZs169anCrQOxrhyLMCAAOw==
708
+ """,
709
+ }
710
+ return {k: PhotoImage(k, data=v) for k, v in icons.items()}
711
+
712
+ def btn_chg_tab_ok(self, thetop, entries, actions):
713
+ """chg a tab title"""
714
+ widget, index = actions
715
+ # build dico of result
716
+ d = {f[0]: f[1]() for f in entries if not isinstance(f, (type("e"), type("e")))}
717
+
718
+ title = d["new label"].strip()
719
+ thetop.destroy()
720
+ widget.tab(index, text=title)
721
+
722
+ def btn_presstwice(self, event):
723
+ """double-click on a tab definition to change label"""
724
+ x, y, widget = event.x, event.y, event.widget
725
+ elem = widget.identify(x, y)
726
+ index = widget.index("@%d,%d" % (x, y))
727
+ titre = widget.tab(index, "text")
728
+ # determine selected table
729
+ actions = [widget, index]
730
+ title = "Changing Tab label"
731
+ fields = [
732
+ "",
733
+ ["current label", (titre), "r", 30],
734
+ "",
735
+ ["new label", titre, "w", 30],
736
+ ]
737
+ create_dialog(title, fields, ("Ok", self.btn_chg_tab_ok), actions)
738
+
739
+ def btn_press(self, event):
740
+ """button press over a widget with a 'close' element"""
741
+ x, y, widget = event.x, event.y, event.widget
742
+ elem = widget.identify(x, y) # widget is the notebook
743
+ if "close" in elem: # close button function
744
+ index = widget.index("@%d,%d" % (x, y))
745
+ widget.state(["pressed"])
746
+ widget.pressed_index = index
747
+ else: # move function
748
+ index = widget.index("@%d,%d" % (x, y))
749
+ self.state_drag = True
750
+ self.state_drag_widgetid = widget.tabs()[index]
751
+ self.state_drag_index = index
752
+
753
+ def btn_Movex(self, event):
754
+ """make the tab follows if button is pressed and mouse moves"""
755
+ x, y, widget = event.x, event.y, event.widget
756
+ elem = widget.identify(x, y)
757
+ index = widget.index("@%d,%d" % (x, y))
758
+ if self.state_drag:
759
+ if self.state_drag_index != index:
760
+ self.btn_Move(widget, self.state_drag_index, index)
761
+ self.state_drag_index = index
762
+
763
+ def btn_Move(self, notebook, old_index, new_index):
764
+ """Move old_index tab to new_index position"""
765
+ # stackoverflow.com/questions/11570786/tkinter-treeview-drag-and-drop
766
+ if new_index != "":
767
+ target_index = new_index
768
+ if new_index >= len(notebook.tabs()) - 1:
769
+ target_index = "end"
770
+ titre = notebook.tab(old_index, "text")
771
+ notebook.forget(old_index)
772
+ notebook.insert(target_index, self.state_drag_widgetid, text=titre)
773
+ notebook.select(new_index)
774
+
775
+ def btn_release(self, event):
776
+ """button release over a widget with a 'close' element"""
777
+ x, y, widget = event.x, event.y, event.widget
778
+ elem = widget.identify(x, y)
779
+ index = self.state_drag_index
780
+ if "close" in elem or "label" in elem:
781
+ index = widget.index("@%d,%d" % (x, y))
782
+ if "close" in elem and widget.instate(["pressed"]):
783
+ if widget.pressed_index == index:
784
+ widget.forget(index)
785
+ widget.event_generate("<<NotebookClosedTab>>")
786
+ if self.state_drag and elem.strip() != "":
787
+ if self.state_drag_index != index:
788
+ self.btn_Move(widget, self.state_drag_index, index)
789
+ self.state_drag = False
790
+
791
+ if not widget.instate(["pressed"]):
792
+ return
793
+ widget.state(["!pressed"])
794
+ widget.pressed_index = None
795
+
796
+ def create_style(self):
797
+ """create a Notebook style with close button"""
798
+ # from https://github.com/python-git/python/blob/master/Demo/tkinter/
799
+ # ttk/notebook_closebtn.py
800
+ # himself from http://paste.tclers.tk/896
801
+ style = ttk.Style()
802
+
803
+ style.element_create(
804
+ "close",
805
+ "image",
806
+ "img_close",
807
+ ("active", "pressed", "!disabled", "img_closepressed"),
808
+ ("active", "!disabled", "img_closeactive"),
809
+ border=6,
810
+ sticky="",
811
+ )
812
+
813
+ style.layout("ButtonNotebook", [("ButtonNotebook.client", {"sticky": "nswe"})])
814
+ style.layout(
815
+ "ButtonNotebook.Tab",
816
+ [
817
+ (
818
+ "ButtonNotebook.tab",
819
+ {
820
+ "sticky": "nswe",
821
+ "children": [
822
+ (
823
+ "ButtonNotebook.padding",
824
+ {
825
+ "side": "top",
826
+ "sticky": "nswe",
827
+ "children": [
828
+ (
829
+ "ButtonNotebook.focus",
830
+ {
831
+ "side": "top",
832
+ "sticky": "nswe",
833
+ "children": [
834
+ (
835
+ "ButtonNotebook.label",
836
+ {"side": "left", "sticky": ""},
837
+ ),
838
+ (
839
+ "ButtonNotebook.close",
840
+ {"side": "left", "sticky": ""},
841
+ ),
842
+ ],
843
+ },
844
+ )
845
+ ],
846
+ },
847
+ )
848
+ ],
849
+ },
850
+ )
851
+ ],
852
+ )
853
+
854
+ self.tk_win.bind_class("TNotebook", "<ButtonPress-1>", self.btn_press, True)
855
+ self.tk_win.bind_class("TNotebook", "<ButtonRelease-1>", self.btn_release)
856
+ self.tk_win.bind_class("TNotebook", "<B1-Motion>", self.btn_Movex)
857
+ self.tk_win.bind_class("TNotebook", "<Double-1>", self.btn_presstwice)
858
+
859
+ def createToolTip(self, widget, text):
860
+ """create a tooptip box for a widget."""
861
+ # www.daniweb.com/software-development/python/code/234888/tooltip-box
862
+ def enter(event):
863
+ global tipwindow
864
+ x = y = 0
865
+ if tipwindow or not text:
866
+ return
867
+ x, y, cx, cy = widget.bbox("insert")
868
+ x += widget.winfo_rootx() + 27
869
+ y += widget.winfo_rooty() + 27
870
+ # Creates a toplevel window
871
+ tipwindow = tw = Toplevel(widget)
872
+ # Leaves only the label and removes the app window
873
+ tw.wm_overrideredirect(1)
874
+ tw.wm_geometry("+%d+%d" % (x, y))
875
+ label = Label(
876
+ tw,
877
+ text=text,
878
+ justify=LEFT,
879
+ background="#ffffe0",
880
+ relief=SOLID,
881
+ borderwidth=1,
882
+ )
883
+ label.pack(ipadx=1)
884
+
885
+ def close(event):
886
+ global tipwindow
887
+ tw = tipwindow
888
+ tipwindow = None
889
+ if tw:
890
+ tw.destroy()
891
+
892
+ widget.bind("<Enter>", enter)
893
+ widget.bind("<Leave>", close)
894
+
895
+ def feed_dbtree(self, root_id, category, attached_db=""):
896
+ """feed database treeview for category, return list of leaves names"""
897
+
898
+ # prepare re-formatting functions for fields and database names
899
+ def f(t):
900
+ return ('"%s"' % t.replace('"', '""')) if t != "" else t
901
+
902
+ def db(t):
903
+ return ('"%s".' % t.replace('"', '""')) if t != "" else t
904
+
905
+ attached = db(attached_db)
906
+
907
+ # get Category list of [unique_name, name, definition, sub_category]
908
+ tables = get_leaves(self.conn, category, attached_db)
909
+ if len(tables) > 0:
910
+ # level 1 : create the "category" node (as Category is not empty)
911
+ root_txt = "%s(%s)" % (attached, category)
912
+ idt = self.db_tree.insert(
913
+ root_id,
914
+ "end",
915
+ root_txt,
916
+ text="%s (%s)" % (category, len(tables)),
917
+ values=("", ""),
918
+ )
919
+ for t_id, t_name, definition, sub_cat in tables:
920
+ # level 2 : print object creation, and '(Definition)' if fields
921
+ sql3 = ""
922
+ if sub_cat != "":
923
+ # it's a table : prepare a Query with names of each column
924
+ sub_c = get_leaves(self.conn, sub_cat, attached_db, t_name)
925
+ colnames = [col[1] for col in sub_c]
926
+ columns = [col[1] + " " + col[2] for col in sub_c]
927
+ sql3 = (
928
+ 'select "'
929
+ + '" , "'.join(colnames)
930
+ + '" from '
931
+ + ("%s%s" % (attached, f(t_name)))
932
+ )
933
+ idc = self.db_tree.insert(
934
+ idt,
935
+ "end",
936
+ "%s%s" % (root_txt, t_id),
937
+ text=t_name,
938
+ tags=("run",),
939
+ values=(definition, sql3),
940
+ )
941
+ if sql3 != "":
942
+ self.db_tree.insert(
943
+ idc,
944
+ "end",
945
+ ("%s%s;d" % (root_txt, t_id)),
946
+ text=["(Definition)"],
947
+ tags=("run",),
948
+ values=(definition, ""),
949
+ )
950
+ # level 3 : Insert a line per column of the Table/View
951
+ for c in range(len(sub_c)):
952
+ self.db_tree.insert(
953
+ idc,
954
+ "end",
955
+ "%s%s%s" % (root_txt, t_id, sub_c[c][0]),
956
+ text=columns[c],
957
+ tags=("run_up",),
958
+ values=("", ""),
959
+ )
960
+ return [i[1] for i in tables]
961
+
962
+ def create_and_add_results(self, instructions, tab_tk_id, limit=-1, log=None):
963
+ """execute instructions and add them to given tab results"""
964
+ a_jouer = self.conn.get_sqlsplit(instructions, remove_comments=False)
965
+ # must read :https://www.youtube.com/watch?v=09tM18_st4I#t=1751
966
+ # stackoverflow.com/questions/15856976/transactions-with-python-sqlite3
967
+ isolation = self.conn.conn.isolation_level
968
+ counter = 0
969
+ shell_list = ["", ""]
970
+ if isolation == "": # Sqlite3 and dump.py default don't match
971
+ self.conn.conn.isolation_level = None # right behavior
972
+ cu = self.conn.conn.cursor()
973
+ sql_error = False
974
+
975
+ def beurk(r):
976
+ """format data line log"""
977
+ s = [
978
+ '"' + s.replace('"', '""') + '"'
979
+ if isinstance(s, (type("e"), type("e")))
980
+ else str(s)
981
+ for s in r
982
+ ]
983
+ return "(" + ",".join(s) + ")"
984
+
985
+ def bip(c):
986
+ """format instruction log header"""
987
+ timing = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
988
+ return "\n---------N°%s----------[" % counter + timing + "]\n\n"
989
+
990
+ for instruction in a_jouer:
991
+ if log is not None: # write to logFile
992
+ counter += 1
993
+ log.write(bip(counter))
994
+ log.write(instruction)
995
+ log.write("\n")
996
+ instru = next(self.conn.get_sqlsplit(instruction, remove_comments=True))
997
+ instru = instru.replace(";", ";").strip(" \t\n\r")
998
+ first_line = (instru + "\n").splitlines()[0]
999
+ if instru[:5] == "pydef":
1000
+ pydef = self.conn.createpydef(instru)
1001
+ titles = ("Creating embedded python function",)
1002
+ rows = self.conn.conn_def[pydef]["pydef"].splitlines()
1003
+ rows.append(self.conn.conn_def[pydef]["inst"])
1004
+ self.n.add_treeview(tab_tk_id, titles, rows, "Info", pydef)
1005
+ if log is not None: # write to logFile
1006
+ log.write("\n".join(['("%s")' % r for r in rows]) + "\n")
1007
+ elif instru[:1] == ".": # a shell command !
1008
+ # handle a ".function" here !
1009
+ # import FILE TABLE
1010
+ shell_list = shlex.split(instru, posix=False) # magic standard library
1011
+ try:
1012
+ if shell_list[0] == ".cd" and len(shell_list) >= 2:
1013
+ db_file = shell_list[1]
1014
+ db_file = db_file.strip("'")
1015
+ db_file = db_file.strip('"')
1016
+ if (db_file + "z")[0] == "~":
1017
+ self.current_directory = os.path.join(
1018
+ self.home, db_file[1:]
1019
+ )
1020
+ elif (db_file + "z")[:2] == "..":
1021
+ self.current_directory = os.path.join(
1022
+ self.current_directory, db_file
1023
+ )
1024
+ elif (db_file + "z")[:1] == ".":
1025
+ self.current_directory = os.path.join(
1026
+ self.current_directory, db_file[1:]
1027
+ )
1028
+ else:
1029
+ self.current_directory = db_file
1030
+ os.chdir(self.current_directory)
1031
+ self.current_directory = os.getcwd()
1032
+ if shell_list[0] == ".headers" and len(shell_list) >= 2:
1033
+ if shell_list[1].lower() == "off":
1034
+ self.default_header = False
1035
+ elif shell_list[1].lower() == "on":
1036
+ self.default_header = True
1037
+ if shell_list[0] == ".separator" and len(shell_list) >= 2:
1038
+ self.default_separator = shell_list[1]
1039
+ if shell_list[0] in (".once", ".output", ".excel"):
1040
+ if shell_list[0] == ".excel":
1041
+ shell_list = [".once", "--bom", "-x"]
1042
+ if shell_list[0] == ".once":
1043
+ self.once_mode, self.init_output = True, True
1044
+ else:
1045
+ self.output_mode, self.init_output = True, True
1046
+ self.x_mode = False
1047
+ self.encode_in = "utf-8"
1048
+ if "--bom" in shell_list: # keep access to the option
1049
+ self.encode_in = "utf-8-sig"
1050
+ if shell_list[1] == "--bom":
1051
+ self.output_file = shell_list[2]
1052
+ else:
1053
+ self.output_file = shell_list[1]
1054
+ self.output_file = self.output_file.strip("'")
1055
+ self.output_file = self.output_file.strip('"')
1056
+ if (self.output_file + "") == "-x":
1057
+ self.x_mode = True
1058
+ with tmpf.TemporaryFile() as ff:
1059
+ ff = tmpf.NamedTemporaryFile(
1060
+ delete=False, suffix="_sqlite_bro.csv"
1061
+ )
1062
+ self.output_file = ff.name
1063
+ if (self.output_file + "z")[0] == "~":
1064
+ self.output_file = os.path.join(
1065
+ self.home, self.output_file[1:]
1066
+ )
1067
+ if self.output_file is None or self.output_file == "":
1068
+ self.output_mode, self.init_output = False, False
1069
+ if shell_list[0] == ".print":
1070
+ if self.output_mode or self.once_mode:
1071
+ write_mode = (
1072
+ "w" if self.init_output else "a"
1073
+ ) # Write or Append
1074
+ with io.open(
1075
+ self.output_file, write_mode, encoding=self.encode_in
1076
+ ) as fout:
1077
+ fout.writelines(instru[len(".print") + 1 :] + "\n")
1078
+ self.init_output, self.once_mode = False, False
1079
+ if shell_list[0] == ".import" and len(shell_list) >= 2:
1080
+ csv_file = shell_list[1]
1081
+ csv_file = csv_file.strip("'")
1082
+ csv_file = csv_file.strip('"')
1083
+ if (csv_file + "z")[0] == "~":
1084
+ csv_file = os.path.join(self.home, csv_file[1:])
1085
+ guess = guess_csv(csv_file)
1086
+ if len(shell_list) >= 3:
1087
+ guess.table_name = shell_list[2]
1088
+ # Create csv reader and give it to import
1089
+ reading = read_this_csv(
1090
+ csv_file,
1091
+ guess.encodings[0],
1092
+ guess.default_sep,
1093
+ guess.default_quote,
1094
+ guess.has_header,
1095
+ guess.default_decims[0],
1096
+ )
1097
+ guess_sql = guess_sql_creation(
1098
+ guess.table_name,
1099
+ guess.default_sep,
1100
+ ".",
1101
+ guess.has_header,
1102
+ guess.dlines,
1103
+ guess.default_quote,
1104
+ )[0]
1105
+ self.conn.insert_reader(
1106
+ reading,
1107
+ guess.table_name,
1108
+ guess_sql,
1109
+ create_table=False,
1110
+ replace=False,
1111
+ )
1112
+ self.n.add_treeview(
1113
+ tab_tk_id,
1114
+ ("table", "file"),
1115
+ ((guess.table_name, csv_file),),
1116
+ "Info",
1117
+ first_line,
1118
+ )
1119
+ if log is not None: # write to logFile
1120
+ log.write(
1121
+ '-- File %s imported in "%s"\n'
1122
+ % (csv_file, guess.table_name)
1123
+ )
1124
+ if shell_list[0] == ".dump":
1125
+ if len(shell_list) >= 2:
1126
+ csv_file = shell_list[1]
1127
+ csv_file = csv_file.strip("'")
1128
+ csv_file = csv_file.strip('"')
1129
+ if (csv_file + "z")[0] == "~":
1130
+ csv_file = os.path.join(self.home, csv_file[1:])
1131
+ with io.open(csv_file, "w", encoding="utf-8") as f:
1132
+ for line in self.conn.iterdump():
1133
+ f.write("%s\n" % line)
1134
+ else:
1135
+ self.n.add_treeview(
1136
+ tab_tk_id,
1137
+ ("output"),
1138
+ ([("%s" % line) for line in self.conn.iterdump()]),
1139
+ "Dump",
1140
+ ".dump",
1141
+ )
1142
+ if shell_list[0] == ".read" and len(shell_list) >= 2:
1143
+ filename = shell_list[1]
1144
+ filename = filename.strip("'")
1145
+ filename = filename.strip('"')
1146
+ if (filename + "z")[0] == "~":
1147
+ filename = os.path.join(self.home, filename[1:])
1148
+ with io.open(
1149
+ filename, encoding=guess_encoding(filename)[0]
1150
+ ) as f:
1151
+ read_this = f.read()
1152
+ self.n.new_query_tab(".Read", read_this)
1153
+ self.run_tab()
1154
+ self.actualize_db()
1155
+ if shell_list[0] == ".open":
1156
+ self.close_db
1157
+ if len(shell_list) >= 2:
1158
+ filename = shell_list[1]
1159
+ if (filename + "z")[0] == "~":
1160
+ filename = os.path.join(self.home, filename[1:])
1161
+ self.open_db(filename)
1162
+ else:
1163
+ self.new_db(":memory:")
1164
+ self.actualize_db()
1165
+ if shell_list[0] == ".restore" and len(shell_list) >= 2:
1166
+ filename = shell_list[1]
1167
+ if (filename + "z")[0] == "~":
1168
+ filename = os.path.join(self.home, filename[1:])
1169
+ db_from = sqlite.connect(filename)
1170
+ db_from.backup(self.conn.conn)
1171
+ db_from.close
1172
+ self.actualize_db()
1173
+ if shell_list[0] == ".backup" and len(shell_list) >= 2:
1174
+ filename = shell_list[1]
1175
+ if (filename + "z")[0] == "~":
1176
+ filename = os.path.join(self.home, filename[1:])
1177
+ db_to = sqlite.connect(filename)
1178
+ self.conn.conn.backup(db_to)
1179
+ db_to.close()
1180
+ if shell_list[0] == ".shell" and len(shell_list) >= 2:
1181
+ os.system(instru[len(".print") + 1 :] + "\n")
1182
+
1183
+ except IOError as err:
1184
+ msg = "I/O error: {0}".format(err)
1185
+ self.n.add_treeview(
1186
+ tab_tk_id, ("Error !",), [(msg,)], "Error !", instru
1187
+ )
1188
+ if not self.use_gui:
1189
+ print("Error !", [msg])
1190
+ if log is not None: # write to logFile
1191
+ log.write("Error ! %s : %s" % (msg, instru))
1192
+ sql_error = True
1193
+ break
1194
+ elif len("".join(instruction.split())) > 1: # PyPy answer 42 to blanks sql
1195
+ nb_columns = -1
1196
+ try:
1197
+ if self.output_mode or self.once_mode:
1198
+ nb_columns = self.conn.export_writer(
1199
+ instruction,
1200
+ self.output_file,
1201
+ header=self.default_header,
1202
+ delimiter=self.default_separator,
1203
+ encoding=self.encode_in,
1204
+ initialize=self.init_output,
1205
+ )
1206
+ if nb_columns > 0:
1207
+ self.once_mode, self.init_output = False, False
1208
+ if nb_columns > 0:
1209
+ self.n.add_treeview(
1210
+ tab_tk_id,
1211
+ ("qry_to_csv", "file"),
1212
+ ((instruction, self.output_file),),
1213
+ "Qry",
1214
+ # ".once %s" % self.output_file,
1215
+ first_line,
1216
+ )
1217
+ if self.x_mode and nb_columns > 0:
1218
+ os.system(
1219
+ "start excel.exe " + self.output_file.replace("\\", "/")
1220
+ )
1221
+ with tmpf.TemporaryFile() as ff:
1222
+ ff = tmpf.NamedTemporaryFile(
1223
+ delete=False, suffix="_sqlite_bro.csv"
1224
+ )
1225
+ self.output_file = ff.name
1226
+ else:
1227
+ cur = cu.execute(instruction)
1228
+ rows = cur.fetchall()
1229
+ # a query may have no result( like for an "update")
1230
+ if (
1231
+ cur.description is not None and len(cur.description) > 0
1232
+ ): # pypy needs this test
1233
+ titles = [row_info[0] for row_info in cur.description]
1234
+ self.n.add_treeview(
1235
+ tab_tk_id, titles, rows, "Qry", first_line
1236
+ )
1237
+ if log is not None: # write to logFile
1238
+ log.write(beurk(titles) + "\n")
1239
+ log.write(
1240
+ "\n".join([beurk(l) for l in rows[:limit]]) + "\n"
1241
+ )
1242
+ if len(rows) > limit:
1243
+ log.write("%s more..." % len((rows) - limit))
1244
+ except sqlite.Error as msg: # OperationalError
1245
+ self.n.add_treeview(
1246
+ tab_tk_id, ("Error !",), [(msg,)], "Error !", first_line
1247
+ )
1248
+ if log is not None: # write to logFile
1249
+ log.write("Error ! %s" % msg)
1250
+ sql_error = True
1251
+ break
1252
+
1253
+ if self.conn.conn.isolation_level != isolation:
1254
+ # if we're in 'backward' compatible mode (automatic commit)
1255
+ try:
1256
+ if self.conn.conn.in_transaction: # python 3.2
1257
+ if not sql_error:
1258
+ cu.execute("COMMIT;")
1259
+ if log is not None: # write to logFile
1260
+ log.write("\n-------COMMIT;--------\n" % counter)
1261
+ else:
1262
+ cu.execute("ROLLBACK;")
1263
+ except:
1264
+ if not sql_error:
1265
+ try:
1266
+ cu.execute("COMMIT;")
1267
+ if log is not None: # write to logFile
1268
+ log.write("\n-------COMMIT;--------\n" % counter)
1269
+ except:
1270
+ pass
1271
+ else:
1272
+ try:
1273
+ cu.execute("ROLLBACK;")
1274
+ except:
1275
+ pass
1276
+ self.conn.conn.isolation_level = isolation # restore standard
1277
+
1278
+ def import_csvtb(self):
1279
+ """import csv dialog (with guessing of encoding and separator)"""
1280
+ csv_file = filedialog.askopenfilename(
1281
+ initialdir=self.initialdir,
1282
+ defaultextension=".db",
1283
+ title="Choose a csv fileto import ",
1284
+ filetypes=[("default", "*.csv"), ("other", "*.txt"), ("all", "*.*")],
1285
+ )
1286
+ if csv_file != "":
1287
+ self.set_initialdir(csv_file)
1288
+ # guess all via an object
1289
+ guess = guess_csv(csv_file)
1290
+ guess_sql = guess_sql_creation(
1291
+ guess.table_name,
1292
+ guess.default_sep,
1293
+ ".",
1294
+ guess.has_header,
1295
+ guess.dlines,
1296
+ guess.default_quote,
1297
+ )[2]
1298
+ # check it via dialog box
1299
+ fields_in = [
1300
+ "",
1301
+ ["csv Name", csv_file, "r", 100],
1302
+ "",
1303
+ ["table Name", guess.table_name],
1304
+ ["column separator", guess.default_sep, "w", 20],
1305
+ ["string delimiter", guess.default_quote, "w", 20],
1306
+ "",
1307
+ ["Decimal separator", guess.default_decims],
1308
+ ["Encoding", guess.encodings],
1309
+ "Fliflaps",
1310
+ ["Header line", guess.has_header],
1311
+ ["Create table", True],
1312
+ ["Replace existing data", True],
1313
+ "",
1314
+ ["first 3 lines", guess.dlines, "r", 100, 10],
1315
+ "",
1316
+ ["use manual creation request", False],
1317
+ "",
1318
+ ["creation request", guess_sql, "w", 100, 10],
1319
+ ]
1320
+ actions = [self.conn, self.actualize_db]
1321
+ create_dialog(
1322
+ ("Importing %s" % csv_file),
1323
+ fields_in,
1324
+ ("Import", import_csvtb_ok),
1325
+ actions,
1326
+ )
1327
+
1328
+ def export_csvtb(self):
1329
+ """get selected table definition and launch cvs export dialog"""
1330
+ # determine selected table
1331
+ actions = [self.conn, self.db_tree]
1332
+ selitem = self.db_tree.focus() # get tree item having the focus
1333
+ if selitem != "":
1334
+ seltag = self.db_tree.item(selitem, "tag")[0]
1335
+ if seltag == "run_up": # if 'run-up', do as dbl-click 1 level up
1336
+ selitem = self.db_tree.parent(selitem)
1337
+ # get final information
1338
+ definition, query = self.db_tree.item(selitem, "values")
1339
+ if query != "": # run the export_csv dialog
1340
+ title = 'Export Table "%s" to ?' % self.db_tree.item(selitem, "text")
1341
+ self.export_csv_dialog(query, title, actions)
1342
+
1343
+ def export_csvqr(self):
1344
+ """get tab selected definition and launch cvs export dialog"""
1345
+ actions = [self.conn, self.n]
1346
+ active_tab_id = self.n.notebook.select()
1347
+ if active_tab_id != "": # get current selection (or all)
1348
+ fw = self.n.fw_labels[active_tab_id]
1349
+ try:
1350
+ query = fw.get("sel.first", "sel.last")
1351
+ except:
1352
+ query = fw.get(1.0, END)[:-1]
1353
+ if query != "":
1354
+ self.export_csv_dialog(query, "Export Query", actions)
1355
+
1356
+ def export_csv_dialog(self, query="--", text="undefined.csv", actions=[]):
1357
+ """export csv dialog"""
1358
+ # proposed encoding (we favorize utf-8 or utf-8-sig)
1359
+ encodings = ["utf-8", locale.getdefaultlocale()[1], "utf-16", "utf-8-sig"]
1360
+ if os.name == "nt":
1361
+ encodings = ["utf-8-sig", locale.getdefaultlocale()[1], "utf-16", "utf-8"]
1362
+ # proposed csv separator
1363
+ default_sep = [",", "|", ";"]
1364
+ csv_file = filedialog.asksaveasfilename(
1365
+ initialdir=self.initialdir,
1366
+ defaultextension=".db",
1367
+ title=text,
1368
+ filetypes=[("default", "*.csv"), ("other", "*.txt"), ("all", "*.*")],
1369
+ )
1370
+ if csv_file != "":
1371
+ # Idea from (http://www.python-course.eu/tkinter_entry_widgets.php)
1372
+ fields = [
1373
+ "",
1374
+ ["csv Name", csv_file, "r", 100],
1375
+ "",
1376
+ ["column separator", default_sep],
1377
+ ["Header line", True],
1378
+ ["Encoding", encodings],
1379
+ "",
1380
+ ["Data to export (MUST be 1 Request)", (query), "w", 100, 10],
1381
+ ]
1382
+
1383
+ create_dialog(
1384
+ ("Export to %s" % csv_file), fields, ("Export", export_csv_ok), actions
1385
+ )
1386
+
1387
+
1388
+ class NotebookForQueries:
1389
+ """Create a Notebook with a list in the First frame
1390
+ and query results in following treeview frames"""
1391
+
1392
+ def __init__(self, tk_win, root, queries, use_gui):
1393
+ self.use_gui = use_gui
1394
+ self.nongui_tabs = [
1395
+ "",
1396
+ ]
1397
+ if self.use_gui:
1398
+ self.tk_win = tk_win
1399
+ self.root = root
1400
+ self.notebook = Notebook(root, style="ButtonNotebook") # ttk.
1401
+
1402
+ self.fw_labels = {} # tab_tk_id -> Scripting frame python object
1403
+ self.fw_result_nbs = {} # tab_tk_id -> Notebook of Results
1404
+
1405
+ # resize rules
1406
+ root.columnconfigure(0, weight=1)
1407
+ root.rowconfigure(0, weight=1)
1408
+ # grid widgets
1409
+ self.notebook.grid(row=0, column=0, sticky=(N, W, S, E))
1410
+
1411
+ def new_query_tab(self, title, query):
1412
+ """add a Tab 'title' to the notebook, containing the Script 'query'"""
1413
+
1414
+ if not self.use_gui:
1415
+ self.nongui_tabs += [query]
1416
+ return len(self.nongui_tabs) - 1
1417
+
1418
+ fw_welcome = ttk.Panedwindow(self.tk_win, orient=VERTICAL) # tk_win
1419
+ fw_welcome.pack(fill="both", expand=True)
1420
+ self.notebook.add(fw_welcome, text=(title))
1421
+
1422
+ # new "editable" script
1423
+ f1 = ttk.Labelframe(fw_welcome, text="Script", width=200, height=100)
1424
+ fw_welcome.add(f1)
1425
+ fw_label = Text(f1, bd=1, undo=True)
1426
+
1427
+ scroll = ttk.Scrollbar(f1, command=fw_label.yview)
1428
+ fw_label.configure(yscrollcommand=scroll.set)
1429
+ fw_label.insert(END, (query))
1430
+ fw_label.pack(side=LEFT, expand=YES, fill=BOTH, padx=2, pady=2)
1431
+ scroll.pack(side=RIGHT, expand=NO, fill=BOTH, padx=2, pady=2)
1432
+
1433
+ # keep tab reference by tk id
1434
+ working_tab_id = "." + fw_welcome._name
1435
+
1436
+ # keep tab reference to script (by tk id)
1437
+ self.fw_labels[working_tab_id] = fw_label
1438
+
1439
+ # new "Results" Container
1440
+ fr = ttk.Labelframe(fw_welcome, text="Results", width=200, height=100)
1441
+ fw_welcome.add(fr)
1442
+
1443
+ # containing a notebook
1444
+ fw_result_nb = Notebook(fr, style="ButtonNotebook")
1445
+ fw_result_nb.pack(fill="both", expand=True)
1446
+ # resize rules
1447
+ fw_welcome.columnconfigure(0, weight=1)
1448
+ # keep reference to result_nb objects (by tk id)
1449
+ self.fw_result_nbs[working_tab_id] = fw_result_nb
1450
+
1451
+ # activate this tab print(self.notebook.tabs())
1452
+ self.notebook.select(working_tab_id)
1453
+ # workaround to have a visible result pane on initial launch
1454
+ self.add_treeview(working_tab_id, "_", "", "click on ('->') to run Script")
1455
+ return working_tab_id # gives back tk_id reference of the new tab
1456
+
1457
+ def del_tab(self):
1458
+ """delete active notebook tab's results"""
1459
+ given_tk_id = self.notebook.select()
1460
+ if given_tk_id != "":
1461
+ self.notebook.forget(given_tk_id)
1462
+
1463
+ def remove_treeviews(self, given_tk_id):
1464
+ """remove results from given tab tk_id"""
1465
+ if given_tk_id != "":
1466
+ myz = self.fw_result_nbs[given_tk_id]
1467
+ for xx in list(myz.children.values()):
1468
+ xx.grid_forget()
1469
+ xx.destroy()
1470
+
1471
+ def add_treeview(self, given_tk_id, columns, data, title="__", subt=""):
1472
+ """add a dataset result to the given tab tk_id"""
1473
+ if not self.use_gui:
1474
+ return
1475
+ # ensure we work on lists
1476
+ if isinstance(columns, (type("e"), type("e"))):
1477
+ tree_columns = [columns]
1478
+ else:
1479
+ tree_columns = columns
1480
+ lines = [data] if isinstance(data, (type("e"), type("e"))) else data
1481
+
1482
+ # get back reference to Notebooks of Results
1483
+ # (see http://www.astro.washington.edu/users/rowen/TkinterSummary.html)
1484
+ fw_result_nb = self.fw_result_nbs[given_tk_id]
1485
+
1486
+ # create a Labelframe to contain new resultset and scrollbars
1487
+ f2 = ttk.Labelframe(
1488
+ fw_result_nb,
1489
+ text=("(%s lines) %s" % (len(lines), subt)),
1490
+ width=200,
1491
+ height=100,
1492
+ )
1493
+ f2.pack(fill="both", expand=True)
1494
+ fw_result_nb.add(f2, text=title)
1495
+
1496
+ # ttk.Style().configure('TLabelframe.label', font=("Arial",14, "bold"))
1497
+ # lines=queries
1498
+ fw_Box = Treeview(
1499
+ f2, columns=tree_columns, show="headings", padding=(2, 2, 2, 2)
1500
+ )
1501
+ fw_vsb = Scrollbar(f2, orient="vertical", command=fw_Box.yview)
1502
+ fw_hsb = Scrollbar(f2, orient="horizontal", command=fw_Box.xview)
1503
+ fw_Box.configure(yscrollcommand=fw_vsb.set, xscrollcommand=fw_hsb.set)
1504
+ fw_Box.grid(column=0, row=0, sticky="nsew", in_=f2)
1505
+ fw_vsb.grid(column=1, row=0, sticky="ns", in_=f2)
1506
+ fw_hsb.grid(column=0, row=2, sticky="ew", in_=f2)
1507
+
1508
+ # this new Treeview may occupy all variable space
1509
+ f2.grid_columnconfigure(0, weight=1)
1510
+ f2.grid_rowconfigure(0, weight=1)
1511
+
1512
+ # feed Treeview Header
1513
+ for col in tuple(tree_columns):
1514
+ fw_Box.heading(
1515
+ col, text=col.title(), command=lambda c=col: self.sortby(fw_Box, c, 0)
1516
+ )
1517
+ fw_Box.column(col, width=font.Font().measure(col.title()))
1518
+
1519
+ def flat(x):
1520
+ """replace line_return by space, if given a string"""
1521
+ if isinstance(x, (type("e"), type("e"))):
1522
+ return x.replace("\n", " ")
1523
+ return x
1524
+
1525
+ # feed Treeview Lines
1526
+ for items in lines:
1527
+ # if line is a string, redo a tuple
1528
+ item = (items,) if isinstance(items, (type("e"), type("e"))) else items
1529
+
1530
+ # replace line_return by space (grid don't like line_returns)
1531
+ line_cells = tuple(flat(item[c]) for c in range(len(tree_columns)))
1532
+ # insert the line of data
1533
+ fw_Box.insert("", "end", values=line_cells)
1534
+ # adjust columns length if necessary and possible
1535
+ for indx, val in enumerate(line_cells):
1536
+ try:
1537
+ ilen = font.Font().measure(val)
1538
+ if (
1539
+ fw_Box.column(tree_columns[indx], width=None) < ilen
1540
+ and ilen < 400
1541
+ ):
1542
+ fw_Box.column(tree_columns[indx], width=ilen)
1543
+ except:
1544
+ pass
1545
+
1546
+ def sortby(self, tree, col, descending):
1547
+ """Sort a ttk treeview contents when a column is clicked on."""
1548
+ # grab values to sort
1549
+ data = [(tree.set(child, col), child) for child in tree.get_children()]
1550
+
1551
+ # reorder data
1552
+ data.sort(reverse=descending)
1553
+ for indx, item in enumerate(data):
1554
+ tree.move(item[1], "", indx)
1555
+
1556
+ # switch the heading so that it will sort in the opposite direction
1557
+ tree.heading(
1558
+ col, command=lambda col=col: self.sortby(tree, col, int(not descending))
1559
+ )
1560
+
1561
+
1562
+ class guess_csv:
1563
+ """guess everything about a csv file of data to import in SQL"""
1564
+
1565
+ def __init__(self, csv_file):
1566
+ self.has_header = True
1567
+ self.default_sep = ","
1568
+ self.default_quote = '"'
1569
+ self.encodings = guess_encoding(csv_file)
1570
+ self.table_name = os.path.basename(csv_file).split(".")[0]
1571
+ with io.open(csv_file, encoding=self.encodings[0]) as f:
1572
+ self.preview = f.read(9999)
1573
+ try:
1574
+ dialect = csv.Sniffer().sniff(self.preview)
1575
+ self.has_header = csv.Sniffer().has_header(self.preview)
1576
+ self.default_sep = dialect.delimiter
1577
+ self.default_quote = Dialect.quotechar
1578
+ except:
1579
+ pass # sniffer can fail
1580
+ self.default_decims = [".", ","]
1581
+ if self.default_sep == ";":
1582
+ self.default_decims = [",", "."]
1583
+ self.dlines = "\n\n".join(self.preview.splitlines()[:3])
1584
+
1585
+
1586
+ def guess_sql_creation(table_name, separ, decim, header, data, quoter='"'):
1587
+ """guess the sql creation request for the table who will receive data"""
1588
+ try:
1589
+ dlines = list(
1590
+ csv.reader(
1591
+ data.replace("\n\n", "\n").splitlines(),
1592
+ delimiter=separ,
1593
+ quotechar=quoter,
1594
+ )
1595
+ )
1596
+ except: # minimal hack for python2.7
1597
+ dlines = list(
1598
+ csv.reader(
1599
+ data.replace("\n\n", "\n").splitlines(),
1600
+ delimiter=str(separ),
1601
+ quotechar=str(quoter),
1602
+ )
1603
+ )
1604
+ r, val = list(dlines[0]), list(dlines[1])
1605
+ typ = ["TEXT"] * len(r) # default value is TEXT
1606
+ for i in range(len(r)):
1607
+ try:
1608
+ float(val[i].replace(decim, ".")) # unless it can be a real
1609
+ typ[i] = "REAL"
1610
+ except:
1611
+ checker = sqlite.connect(":memory:")
1612
+ # avoid the false positive 'now'
1613
+ val_not_now = val[i].replace("w", "www").replace("W", "WWW")
1614
+ test = "select datetime('{0}')".format(val_not_now)
1615
+ try:
1616
+ if checker.execute(test).fetchall()[0][0]:
1617
+ typ[i] = "DATETIME" # and unless SQLite can see a DATETIME
1618
+ except:
1619
+ pass
1620
+ checker.close
1621
+ if header:
1622
+ # de-duplicate column names, if needed by pastixing with '_'+number
1623
+ for i in range(len(r)):
1624
+
1625
+ if r[i] == "": # 2022-02-04a replace empty column title per usual default
1626
+ r[i] = "c_" + ("000" + str(i))[-3:]
1627
+
1628
+ if r[i] in r[:i]:
1629
+ j = 1
1630
+ while r[i] + "_" + str(j) in r[:i] + r[i + 1 :]:
1631
+ j += 1
1632
+ r[i] += "_" + str(j)
1633
+ head = ",\n".join([('"%s" %s' % (r[i], typ[i])) for i in range(len(r))])
1634
+ sql_crea = 'CREATE TABLE "%s" (%s);' % (table_name, head)
1635
+ else:
1636
+ head = ",".join(["c_" + ("000" + str(i))[-3:] for i in range(len(r))])
1637
+ sql_crea = 'CREATE TABLE "%s" (%s);' % (table_name, head)
1638
+ return sql_crea, typ, head
1639
+
1640
+
1641
+ def guess_encoding(csv_file):
1642
+ """guess the encoding of the given file"""
1643
+ with io.open(csv_file, "rb") as f:
1644
+ data = f.read(5)
1645
+ if data.startswith(b"\xEF\xBB\xBF"): # UTF-8 with a "BOM"
1646
+ return ["utf-8-sig"]
1647
+ elif data.startswith(b"\xFF\xFE") or data.startswith(b"\xFE\xFF"):
1648
+ return ["utf-16"]
1649
+ else: # in Windows, guessing utf-8 doesn't work, so we have to try
1650
+ try:
1651
+ with io.open(csv_file, encoding="utf-8") as f:
1652
+ preview = f.read(222222)
1653
+ return ["utf-8"]
1654
+ except:
1655
+ return [locale.getdefaultlocale()[1], "utf-8"]
1656
+
1657
+
1658
+ def create_dialog(title, fields_in, buttons, actions):
1659
+ """create a formular with title, fields, button, data"""
1660
+ # drawing the request form
1661
+ top = Toplevel()
1662
+ top.title(title)
1663
+ top.columnconfigure(0, weight=1)
1664
+ top.rowconfigure(0, weight=1)
1665
+ # drawing global frame
1666
+ content = ttk.Frame(top)
1667
+ content.grid(column=0, row=0, sticky=(N, S, E, W))
1668
+ content.columnconfigure(0, weight=1)
1669
+ # fields = Horizontal FrameLabel, or
1670
+ # label, default_value, 'r' or 'w' default_width,default_height
1671
+ fields = fields_in
1672
+ mf_col = -1
1673
+ for f in range(len(fields)): # same structure out
1674
+ field = fields[f]
1675
+ if isinstance(field, (type("e"), type("e"))) or mf_col == -1:
1676
+ # a new horizontal frame
1677
+ mf_col += 1
1678
+ ta_col = -1
1679
+ if isinstance(field, (type("e"), type("e"))) and field == "":
1680
+ mf_frame = ttk.Frame(content, borderwidth=1)
1681
+ else:
1682
+ mf_frame = ttk.LabelFrame(content, borderwidth=1, text=field)
1683
+ mf_frame.grid(column=0, row=mf_col, sticky="nsew")
1684
+ Grid.rowconfigure(mf_frame, 0, weight=1)
1685
+ content.rowconfigure(mf_col, weight=1)
1686
+ if not isinstance(field, (type("e"), type("e"))):
1687
+ # a new vertical frame
1688
+ ta_col += 1
1689
+ Grid.columnconfigure(mf_frame, ta_col, weight=1)
1690
+ packing_frame = ttk.Frame(mf_frame, borderwidth=1)
1691
+ packing_frame.grid(column=ta_col, row=0, sticky="nsew")
1692
+ Grid.columnconfigure(packing_frame, 0, weight=1)
1693
+ # prepare width and height and writable status
1694
+ width = field[3] if len(field) > 3 else 30
1695
+ height = field[4] if len(field) > 4 else 30
1696
+ status = "normal"
1697
+ if len(field) >= 3 and field[2] == "r":
1698
+ status = "disabled"
1699
+ # switch between object types
1700
+ if len(field) > 4:
1701
+ # datas
1702
+ d_frame = ttk.LabelFrame(
1703
+ packing_frame,
1704
+ borderwidth=5,
1705
+ width=width,
1706
+ height=height,
1707
+ text=field[0],
1708
+ )
1709
+ d_frame.grid(column=0, row=0, sticky="nsew", pady=1, padx=1)
1710
+ Grid.rowconfigure(packing_frame, 0, weight=1)
1711
+ fw_label = Text(d_frame, bd=1, width=width, height=height, undo=True)
1712
+ fw_label.pack(side=LEFT, expand=YES, fill=BOTH)
1713
+ scroll = ttk.Scrollbar(d_frame, command=fw_label.yview)
1714
+ scroll.pack(side=RIGHT, expand=NO, fill=Y)
1715
+ fw_label.configure(yscrollcommand=scroll.set)
1716
+ fw_label.insert(END, ("%s" % field[1]))
1717
+ fw_label.configure(state=status)
1718
+ Grid.rowconfigure(d_frame, 0, weight=1)
1719
+ Grid.columnconfigure(d_frame, 0, weight=1)
1720
+ # Data Text Extractor in the fields list ()
1721
+ # see stackoverflow.com/questions/17677649 (loop and lambda)
1722
+ fields[f][1] = lambda x=fw_label: x.get("1.0", "end")
1723
+ elif isinstance(field[1], type(True)):
1724
+ # boolean Field
1725
+ name_var = BooleanVar()
1726
+ name = ttk.Checkbutton(
1727
+ packing_frame,
1728
+ text=field[0],
1729
+ variable=name_var,
1730
+ onvalue=True,
1731
+ state=status,
1732
+ )
1733
+ name_var.set(field[1])
1734
+ name.grid(column=0, row=0, sticky="nsew", pady=5, padx=5)
1735
+ fields[f][1] = name_var.get
1736
+ else: # Text or Combo
1737
+ namelbl = ttk.Label(packing_frame, text=field[0])
1738
+ namelbl.grid(column=0, row=0, sticky="nsw", pady=5, padx=5)
1739
+ name_var = StringVar()
1740
+ if not isinstance(field[1], (list, tuple)):
1741
+ name = ttk.Entry(
1742
+ packing_frame, textvariable=name_var, width=width, state=status
1743
+ )
1744
+ name_var.set(field[1])
1745
+ else:
1746
+ name = ttk.Combobox(
1747
+ packing_frame, textvariable=name_var, state=status
1748
+ )
1749
+ name["values"] = list(field[1])
1750
+ name.current(0)
1751
+ name.grid(column=1, row=0, sticky="nsw", pady=0, padx=10)
1752
+ fields[f][1] = name_var.get
1753
+ # adding button below the same way
1754
+ mf_col += 1
1755
+ packing_frame = ttk.LabelFrame(content, borderwidth=5)
1756
+ packing_frame.grid(column=0, row=mf_col, sticky="nsew")
1757
+ okbutton = ttk.Button(
1758
+ packing_frame,
1759
+ text=buttons[0],
1760
+ command=lambda a=top, b=fields, c=actions: (buttons[1])(a, b, c),
1761
+ )
1762
+ cancelbutton = ttk.Button(packing_frame, text="Cancel", command=top.destroy)
1763
+ okbutton.grid(column=0, row=mf_col)
1764
+ cancelbutton.grid(column=1, row=mf_col)
1765
+ for x in range(3):
1766
+ Grid.columnconfigure(packing_frame, x, weight=1)
1767
+ top.grab_set()
1768
+
1769
+
1770
+ def import_csvtb_ok(thetop, entries, actions):
1771
+ """read input values from tk formular"""
1772
+ conn, actualize_db = actions
1773
+ # build dico of result
1774
+ d = {f[0]: f[1]() for f in entries if not isinstance(f, (type("e"), type("e")))}
1775
+ # affect to variables
1776
+ csv_file = d["csv Name"].strip()
1777
+ table_name = d["table Name"].strip()
1778
+ separ = d["column separator"]
1779
+ decim = d["Decimal separator"]
1780
+ quotechar = d["string delimiter"]
1781
+ # action
1782
+ if csv_file != "(none)" and len(csv_file) * len(table_name) * len(separ) > 1:
1783
+ thetop.destroy()
1784
+ # do initialization job
1785
+ sql, typ, head = guess_sql_creation(
1786
+ table_name, separ, decim, d["Header line"], d["first 3 lines"], quotechar
1787
+ )
1788
+ if d["use manual creation request"]:
1789
+ sql = 'CREATE TABLE "%s" (%s);' % (table_name, d["creation request"])
1790
+
1791
+ # Create csv reader function and give it to insert
1792
+ reading = read_this_csv(
1793
+ csv_file, d["Encoding"], separ, quotechar, d["Header line"], decim
1794
+ )
1795
+
1796
+ conn.insert_reader(
1797
+ reading,
1798
+ table_name,
1799
+ sql,
1800
+ create_table=d["Create table"],
1801
+ replace=d["Replace existing data"],
1802
+ )
1803
+ # refresh
1804
+ actualize_db()
1805
+
1806
+
1807
+ def read_this_csv(csv_file, encoding, delimiter, quotechar, header, decim):
1808
+ """yield csv data records from a file"""
1809
+ # handle Python 2/3
1810
+ try:
1811
+ reader = csv.reader(
1812
+ open(csv_file, "r", encoding=encoding),
1813
+ delimiter=delimiter,
1814
+ quotechar=quotechar,
1815
+ )
1816
+ except: # minimal hack for 2.7
1817
+ reader = csv.reader(
1818
+ open(csv_file, "r"), delimiter=str(delimiter), quotechar=str(quotechar)
1819
+ )
1820
+ # handle header
1821
+ if header:
1822
+ next(reader)
1823
+ # otherwise handle special decimal treatment
1824
+ for row in reader:
1825
+ if decim != "." and not isinstance(row, (type("e"), type("e"))):
1826
+ for i in range(len(row)):
1827
+ row[i] = row[i].replace(decim, ".")
1828
+ yield (row)
1829
+
1830
+
1831
+ def export_csv_ok(thetop, entries, actions):
1832
+ "export a csv table (action)"
1833
+ conn = actions[0]
1834
+ # build dico of result
1835
+ d = {f[0]: f[1]() for f in entries if not isinstance(f, (type("e"), type("e")))}
1836
+
1837
+ csv_file = d["csv Name"].strip()
1838
+ conn.export_writer(
1839
+ d["Data to export (MUST be 1 Request)"],
1840
+ csv_file,
1841
+ header=d["Header line"],
1842
+ delimiter=d["column separator"],
1843
+ encoding=d["Encoding"],
1844
+ quotechar='"',
1845
+ )
1846
+
1847
+
1848
+ def get_leaves(conn, category, attached_db="", tbl=""):
1849
+ """returns a list of 'category' objects in attached_db
1850
+ [objectCode, objectLabel, Definition, 'sub-level']
1851
+ """
1852
+ # create formatting shortcuts
1853
+ def f(t):
1854
+ return ('"%s"' % t.replace('"', '""')) if t != "" else t
1855
+
1856
+ def d(t):
1857
+ return ("%s." % t) if t != "" else t
1858
+
1859
+ # Initialize datas
1860
+ Tables, db, tb = [], d(attached_db), f(tbl)
1861
+ master = "sqlite_master" if db != "temp." else "sqlite_temp_master"
1862
+
1863
+ if category == "pydef": # pydef request is not sql, answer is direct
1864
+ Tables = [[k, k, v["pydef"], ""] for k, v in conn.conn_def.items()]
1865
+ elif category == "attached_databases":
1866
+ # get all attached database, but not the first one ('main')
1867
+ resu = list((conn.execute("PRAGMA database_list").fetchall()))[1:]
1868
+ for c in resu:
1869
+ instruct = "ATTACH DATABASE %s as %s" % (f(c[2]), f(c[1]))
1870
+ Tables.append([c[0], c[1], instruct, ""])
1871
+ elif category == "fields":
1872
+ resu = conn.execute("PRAGMA %sTABLE_INFO(%s)" % (db, tb)).fetchall()
1873
+ Tables = [[c[1], c[1], c[2], ""] for c in resu]
1874
+ elif category in ("index", "trigger", "master_table", "table", "view"):
1875
+ # others are 1 sql request that generates directly Tables
1876
+ if category in ("index", "trigger"):
1877
+ sql = """SELECT '{0}' || name, name, coalesce(sql,'--auto') , ''
1878
+ FROM {0}{3} WHERE type='{1}' ORDER BY name"""
1879
+ elif category == "master_table":
1880
+ sql = """SELECT '{0}{3}', '{3}', '--auto', 'fields'
1881
+ UNION SELECT '{0}'||name, name, sql, 'fields'
1882
+ FROM {0}{3}
1883
+ WHERE type='table' AND name LIKE 'sqlite_%' ORDER BY name"""
1884
+ elif category in ("table", "view"):
1885
+ sql = """SELECT '{0}' || name, name, sql , 'fields'
1886
+ FROM {0}{3} WHERE type = '{1}' AND NOT
1887
+ (type='table' AND name LIKE 'sqlite_%') ORDER BY name"""
1888
+ Tables = list(conn.execute(sql.format(db, category, tbl, master)).fetchall())
1889
+ return Tables
1890
+
1891
+
1892
+ class Baresql:
1893
+ """a small wrapper around sqlite3 module"""
1894
+
1895
+ def __init__(
1896
+ self, connection="", keep_log=False, cte_inline=True, isolation_level=None
1897
+ ):
1898
+ self.dbname = connection.replace(":///", "://").replace("sqlite://", "")
1899
+ self.conn = sqlite.connect(self.dbname, detect_types=sqlite.PARSE_DECLTYPES)
1900
+ # pydef and logging infrastructure
1901
+ self.conn_def = {}
1902
+ self.do_log = keep_log
1903
+ self.log = []
1904
+ self.conn.isolation_level = isolation_level # commit experience
1905
+
1906
+ def close(self):
1907
+ """close database and clear dictionnary of registered 'pydef'"""
1908
+ self.conn.close
1909
+ self.conn_def = {}
1910
+
1911
+ def iterdump(self):
1912
+ """dump the database (add tweaks over the default dump)"""
1913
+ # force detection of utf-8 by placing an only utf-8 comment at top
1914
+ yield ("/*utf-8 tag : 你好 мир Artisou à croute*/\n")
1915
+ # add the Python functions pydef
1916
+ for k in self.conn_def.values():
1917
+ yield (k["pydef"] + ";\n")
1918
+ # disable Foreign Constraints at Load
1919
+ yield ("PRAGMA foreign_keys = OFF; /*if SQlite */;")
1920
+ yield ("\n/* SET foreign_key_checks = 0;/*if Mysql*/;")
1921
+ # how to parametrize Mysql to SQL92 standard
1922
+ yield ("/* SET sql_mode = 'PIPES_AS_CONCAT';/*if Mysql*/;")
1923
+ yield ("/* SET SQL_MODE = ANSI_QUOTES; /*if Mysql*/;\n")
1924
+ # now the standard dump (notice it uses BEGIN TRANSACTION)
1925
+ for line in self.conn.iterdump():
1926
+ yield (line)
1927
+ # re-instantiate Foreign_keys = True
1928
+ for row in self.conn.execute("PRAGMA foreign_keys"):
1929
+ flag = "ON" if row[0] == 1 else "OFF"
1930
+ yield ("PRAGMA foreign_keys = %s;/*if SQlite*/;" % flag)
1931
+ yield ("PRAGMA foreign_keys = %s;/*if SQlite bug*/;" % flag)
1932
+ yield ("PRAGMA foreign_key_check;/*if SQLite, check*/;")
1933
+ yield ("\n/*SET foreign_key_checks = %s;/*if Mysql*/;\n" % row[0])
1934
+
1935
+ def execute(self, sql, env=None):
1936
+ """execute sql but intercept log"""
1937
+ if self.do_log:
1938
+ self.log.append(sql)
1939
+ return self.conn.execute(sql)
1940
+
1941
+ def createpydef(self, sql):
1942
+ """generates and register a pydef instruction"""
1943
+ import re
1944
+
1945
+ instruction = sql.strip("; \t\n\r")
1946
+ # create Python function in Python
1947
+ exec(instruction[2:], globals(), pydef_locals)
1948
+ # add Python function in SQLite
1949
+ instr_header = re.findall(r"\w+", instruction[: (instruction + ")").find(")")])
1950
+ instr_name = instr_header[1]
1951
+ instr_parms = len(instr_header) - 2
1952
+ instr_pointer=eval(instr_name, globals(), pydef_locals)
1953
+ self.conn.create_function(instr_name, instr_parms, instr_pointer)
1954
+ instr_add = "self.conn.create_function('%s', %s, %s)" % (
1955
+ instr_name,
1956
+ instr_parms,
1957
+ instr_name,
1958
+ )
1959
+ # housekeeping definition of pydef in a dictionnary
1960
+ the_help = pydef_locals[instr_name].__doc__
1961
+ self.conn_def[instr_name] = {
1962
+ "parameters": instr_parms,
1963
+ "inst": instr_add,
1964
+ "help": the_help,
1965
+ "pydef": instruction,
1966
+ }
1967
+ return instr_name
1968
+
1969
+ def get_tokens(self, sql, start=0, shell_tokens=False):
1970
+ """
1971
+ from given sql start position, yield tokens (value + token type)
1972
+ if shell_tokens is True, identify line shell_tokens as sqlite.exe does
1973
+ """
1974
+ length = len(sql)
1975
+ i = start
1976
+ can_be_shell_command = True
1977
+ dico = {
1978
+ " ": "TK_SP",
1979
+ "\t": "TK_SP",
1980
+ "\n": "TK_SP",
1981
+ "\f": "TK_SP",
1982
+ "\r": "TK_SP",
1983
+ "(": "TK_LP",
1984
+ ")": "TK_RP",
1985
+ ";": "TK_SEMI",
1986
+ ",": "TK_COMMA",
1987
+ "/": "TK_OTHER",
1988
+ "'": "TK_STRING",
1989
+ "-": "TK_OTHER",
1990
+ '"': "TK_STRING",
1991
+ "`": "TK_STRING",
1992
+ }
1993
+ while length > start:
1994
+ token = "TK_OTHER"
1995
+ if (
1996
+ shell_tokens
1997
+ and can_be_shell_command
1998
+ and i < length
1999
+ and (
2000
+ (sql[i] == "." and i == start)
2001
+ or (i > start and sql[i - 1 : i] == "\n.")
2002
+ )
2003
+ ):
2004
+ # a command line shell ! (supposed on one starting line)
2005
+ token = "TK_SHELL"
2006
+ i = sql.find("\n", start)
2007
+ if i <= 0:
2008
+ i = length
2009
+ elif sql[i] == "-" and i < length and sql[i : i + 2] == "--":
2010
+ # this Token is an end-of-line comment : --blabla
2011
+ token = "TK_COM"
2012
+ i = sql.find("\n", start)
2013
+ if i <= 0:
2014
+ i = length
2015
+ elif sql[i] == "/" and i < length and sql[i : i + 2] == "/*":
2016
+ # this Token is a comment block : /* and bla bla \n bla */
2017
+ token = "TK_COM"
2018
+ i = sql.find("*/", start) + 2
2019
+ if i <= 1:
2020
+ i = length
2021
+ elif sql[i] not in dico:
2022
+ # this token is a distinct word (tagged as 'TK_OTHER')
2023
+ while i < length and sql[i] not in dico:
2024
+ i += 1
2025
+ else:
2026
+ # default token analyze case
2027
+ token = dico[sql[i]]
2028
+ if token == "TK_SP":
2029
+ # find the end of the 'Spaces' Token just detected
2030
+ while i < length and sql[i] in dico and dico[sql[i]] == "TK_SP":
2031
+ i += 1
2032
+ elif token == "TK_STRING":
2033
+ # find the end of the 'String' Token just detected
2034
+ delimiter = sql[i]
2035
+ if delimiter != "'":
2036
+ token = "TK_ID" # usefull nuance ?
2037
+ while i < length:
2038
+ i = sql.find(delimiter, i + 1)
2039
+ if i <= 0: # String is never closed
2040
+ i = length
2041
+ token = "TK_ERROR"
2042
+ elif i < length - 1 and sql[i + 1] == delimiter:
2043
+ i += 1 # double '' case, so ignore and continue
2044
+ else:
2045
+ i += 1
2046
+ break # normal End of a String
2047
+ else:
2048
+ if i < length:
2049
+ i += 1
2050
+ yield sql[start:i], token
2051
+ if token == "TK_SEMI": # a new sql order can be a new shell token
2052
+ can_be_shell_command = True
2053
+ elif token not in ("TK_COM", "TK_SP", "TK_SHELL"): # can't be a shell token
2054
+ can_be_shell_command = False
2055
+ start = i
2056
+
2057
+ def get_sqlsplit(self, sql, remove_comments=False):
2058
+ """yield a list of separated sql orders from a sql file"""
2059
+ trigger_mode = False
2060
+ mysql = [""]
2061
+ for tokv, token in self.get_tokens(sql, shell_tokens=True):
2062
+ # clear comments option
2063
+ if token != "TK_COM" or not remove_comments:
2064
+ mysql.append(tokv)
2065
+ # special case for Trigger : semicolumn don't count
2066
+ if token == "TK_OTHER":
2067
+ tok = tokv.upper()
2068
+ if tok == "TRIGGER":
2069
+ trigger_mode = True
2070
+ translvl = 0
2071
+ elif trigger_mode and tok in ("BEGIN", "CASE"):
2072
+ translvl += 1
2073
+ elif trigger_mode and tok == "END":
2074
+ translvl -= 1
2075
+ if translvl <= 0:
2076
+ trigger_mode = False
2077
+ elif token == "TK_SEMI" and not trigger_mode:
2078
+ # end of a single sql
2079
+ yield "".join(mysql)
2080
+ mysql = []
2081
+ elif token == "TK_SHELL":
2082
+ # end of a shell order
2083
+ yield ("" + tokv)
2084
+ mysql = []
2085
+ if mysql != []:
2086
+ yield ("".join(mysql))
2087
+
2088
+ def insert_reader(
2089
+ self,
2090
+ reader,
2091
+ table_name,
2092
+ create_sql=None,
2093
+ create_table=True,
2094
+ replace=True,
2095
+ header=False,
2096
+ ):
2097
+ """import a given csv reader into a given table"""
2098
+ curs = self.conn.cursor()
2099
+ # 1-do initialization job
2100
+ # speed-up dead otherwise dead slow speed if not memory database
2101
+ try:
2102
+ curs.execute("begin transaction")
2103
+ except:
2104
+ pass
2105
+ # check if table exists
2106
+ here = curs.execute('PRAGMA table_info("%s")' % table_name).fetchall()
2107
+ if create_sql and (create_table or len(here) == 0):
2108
+ curs.execute('drop TABLE if exists "%s";' % table_name)
2109
+ curs.execute(create_sql)
2110
+ if replace:
2111
+ curs.execute('delete from "%s";' % table_name)
2112
+ # count rows of target table
2113
+ nbcol = len(curs.execute('pragma table_info("%s")' % table_name).fetchall())
2114
+ sql = 'INSERT INTO "%s" VALUES(%s);' % (table_name, ", ".join(["?"] * nbcol))
2115
+ # read first_line if hasked to skip headers
2116
+ if header:
2117
+ next(reader)
2118
+ # 2-push records
2119
+ curs.executemany(sql, reader)
2120
+ self.conn.commit()
2121
+
2122
+ def export_writer(
2123
+ self,
2124
+ sql,
2125
+ csv_file,
2126
+ header=True,
2127
+ delimiter=",",
2128
+ encoding="utf-8",
2129
+ quotechar='"',
2130
+ initialize=True,
2131
+ ):
2132
+ """export a csv table and return number of columns"""
2133
+ cursor = self.conn.cursor()
2134
+ cursor.execute(sql)
2135
+ # do nothing if nothing
2136
+ if cursor.description is None or len(cursor.description) == 0:
2137
+ return -1
2138
+ else:
2139
+ nb_columns = len(cursor.description)
2140
+ # with PyPy, the "with io.open" for is more than necessary
2141
+ if sys.version_info[0] != 2: # python3
2142
+ write_mode = "w" if initialize else "a" # Write or Append
2143
+ with io.open(csv_file, write_mode, newline="", encoding=encoding) as fout:
2144
+ writer = csv.writer(
2145
+ fout, delimiter=delimiter, quotechar='"', quoting=csv.QUOTE_MINIMAL
2146
+ )
2147
+ if header:
2148
+ writer.writerow(
2149
+ [i if isinstance(i, str) else i[0] for i in cursor.description]
2150
+ ) # PyPy as a strange list of list
2151
+ writer.writerows(cursor.fetchall())
2152
+ fout.close # PyPy3-7.3.5 needs that close
2153
+ else: # python2.7 (minimal)
2154
+ write_mode = "wb" if initialize else "ab" # Write or Append
2155
+ with io.open(csv_file, write_mode) as fout:
2156
+ writer = csv.writer(
2157
+ fout,
2158
+ delimiter=str(delimiter),
2159
+ quotechar=str('"'),
2160
+ quoting=csv.QUOTE_MINIMAL,
2161
+ )
2162
+ if header:
2163
+ writer.writerow(
2164
+ [i if isinstance(i, str) else i[0] for i in cursor.description]
2165
+ ) # heading row with anti-PyPy bug
2166
+ writer.writerows(cursor.fetchall())
2167
+ fout.close # PyPy3-7.3.5 needs that close
2168
+ return nb_columns
2169
+
2170
+
2171
+ def _main():
2172
+ welcome_text = """-- SQLite Memo (Demo = click on green "->" and "@" icons)
2173
+ \n-- to CREATE a table 'items' and a table 'parts' :
2174
+ DROP TABLE IF EXISTS item; DROP TABLE IF EXISTS part;
2175
+ CREATE TABLE item (ItemNo, Description,Kg , PRIMARY KEY (ItemNo));
2176
+ CREATE TABLE part(ParentNo, ChildNo , Description TEXT , Qty_per REAL);
2177
+ \n-- to CREATE an index :
2178
+ DROP INDEX IF EXISTS parts_id1;
2179
+ CREATE INDEX parts_id1 ON part(ParentNo Asc, ChildNo Desc);
2180
+ \n-- to CREATE a view 'v1':
2181
+ DROP VIEW IF EXISTS v1;
2182
+ CREATE VIEW v1 as select * from item inner join part as p ON ItemNo=p.ParentNo;
2183
+ \n-- to INSERT datas
2184
+ INSERT INTO item values("T","Ford",1000);
2185
+ INSERT INTO item select "A","Merced",1250 union all select "W","Wheel",9 ;
2186
+ INSERT INTO part select ItemNo,"W","needed",Kg/250 from item where Kg>250;
2187
+ \n-- to CREATE a Python embedded function, enclose them by "py" and ";" :
2188
+ pydef py_hello():
2189
+ "hello world"
2190
+ return ("Hello, World !");
2191
+ pydef py_fib(n):
2192
+ "fibonacci : example with function call (may only be internal) "
2193
+ fib = lambda n: n if n < 2 else fib(n-1) + fib(n-2)
2194
+ return("%s" % fib(n*1));
2195
+
2196
+ -- to USE a python embedded function and nesting of embedded functions:
2197
+ select py_hello(), py_fib(6) as fibonacci, sqlite_version();
2198
+ \n-- to EXPORT :
2199
+ -- a TABLE, select TABLE, then click on icon 'SQL->CSV'
2200
+ -- a QUERY RESULT, select the SCRIPT text, then click on icon '???->CSV',
2201
+ -- example : select the end of this line: SELECT SQLITE_VERSION()
2202
+ \n\n-- to use COMMIT and ROLLBACK :
2203
+ BEGIN TRANSACTION;
2204
+ UPDATE item SET Kg = Kg + 1;
2205
+ COMMIT;
2206
+ BEGIN TRANSACTION;
2207
+ UPDATE item SET Kg = 0;
2208
+ select Kg, Description from Item;
2209
+ ROLLBACK;
2210
+ select Kg, Description from Item;
2211
+ \n\n-- to use SAVEPOINT :
2212
+ SAVEPOINT remember_Neo; -- create a savepoint
2213
+ UPDATE item SET Description = 'Smith'; -- do things
2214
+ SELECT ItemNo, Description FROM Item; -- see things done
2215
+ ROLLBACK TO SAVEPOINT remember_Neo; -- go back to savepoint state
2216
+ SELECT ItemNo, Description FROM Item; -- see all is back to normal
2217
+ RELEASE SAVEPOINT remember_Neo; -- free memory
2218
+
2219
+ \n\n-- '.' commands understood:
2220
+ -- .backup FILE Backup DB (default "main") to FILE (if Python>=3.7)
2221
+ -- .cd DIRECTORY Change the working directory to DIRECTORY
2222
+ -- .dump ?FILE? Render database content as SQL (to FILE if specified)
2223
+ -- .excel Display the output of next command in spreadsheet
2224
+ -- .headers on|off Turn display of headers on or off
2225
+ -- .import FILE TABLE Import data from FILE into TABLE
2226
+ -- (create TABLE only if it doesn't exist, keep existing records)
2227
+ -- .once [--bom] FILE Output of next SQL command to FILE [with utf-8 bom]
2228
+ -- .open ?FILE? Close existing database and reopen FILE
2229
+ -- .output ?FILE? Send output to FILE or stdout if FILE is omitted
2230
+ -- .print STRING... Print literal STRING
2231
+ -- .read FILE Read input from FILE
2232
+ -- .restore FILE Restore DB (default "main") from FILE (if Python>=3.7)
2233
+ -- .separator COL Set column separator in next .once exports (default ,)
2234
+ -- .shell CMD ARGS... Run CMD ARGS... in a system shell
2235
+
2236
+ .headers on
2237
+ .separator ;
2238
+ .once --bom '~this_file_of result.txt'
2239
+ select ItemNo, Description from item order by ItemNo desc;
2240
+ .import '~this_file_of result.txt' in_this_table
2241
+ .cd ~
2242
+ ATTACH 'test.db' as toto;
2243
+ DROP TABLE IF EXISTS toto.new_item;
2244
+ CREATE TABLE toto.new_item as select * from "main"."item";
2245
+ .dump
2246
+ """
2247
+
2248
+ if "argparse" in globals(): # not before Python-3.2
2249
+ parser = argparse.ArgumentParser(
2250
+ description="sqlite_bro : a graphic SQLite browser in 1 Python file"
2251
+ )
2252
+ parser.add_argument(
2253
+ "-q", "--quiet", action="store_true", help="do not launch the gui"
2254
+ )
2255
+ parser.add_argument(
2256
+ "-w",
2257
+ "--wait",
2258
+ action="store_true",
2259
+ help="wait the user to launch the scripts",
2260
+ )
2261
+ parser.add_argument(
2262
+ "-db",
2263
+ "--database",
2264
+ default=":memory:",
2265
+ type=str,
2266
+ help="specify initial Database if not ':memory:'",
2267
+ )
2268
+ parser.add_argument(
2269
+ "-sc", "--scripts", type=str, help="qive a list of initial scripts"
2270
+ )
2271
+ args = parser.parse_args()
2272
+
2273
+ if args.quiet:
2274
+ app = App(use_gui=False)
2275
+ else:
2276
+ app = App(use_gui=True)
2277
+ # start with a memory Database and a welcome
2278
+ app.new_db(":memory:")
2279
+ if args.database:
2280
+ app.open_db(args.database)
2281
+ if args.scripts:
2282
+ if isinstance(args.scripts, str):
2283
+ scripts = [args.scripts, "", ""]
2284
+ else:
2285
+ scripts = args.scripts
2286
+ for script in scripts:
2287
+ if os.path.isfile(script):
2288
+ with io.open(script, encoding=guess_encoding(script)[0]) as f:
2289
+ welcome_text = f.read()
2290
+ app.n.new_query_tab("Welcome", welcome_text)
2291
+ if not args.wait:
2292
+ app.run_tab()
2293
+ else:
2294
+ app.n.new_query_tab("Welcome", welcome_text)
2295
+ if args.quiet:
2296
+ app.close_db
2297
+ else:
2298
+ app = App(use_gui=True)
2299
+ # start with a memory Database and a welcome
2300
+ app.new_db(":memory:")
2301
+ app.n.new_query_tab("Welcome", welcome_text)
2302
+ if app.use_gui:
2303
+ app.tk_win.mainloop()
2304
+
2305
+
2306
+ if __name__ == "__main__":
2307
+ _main() # create a tkk graphic interface with a main window tk_win