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.
- sqlite_bro/__init__.py +1 -0
- sqlite_bro/sqlite_bro.py +2307 -0
- sqlite_bro/tests/__init__.py +0 -0
- sqlite_bro/tests/test_general.py +68 -0
- sqlite_bro-0.13.1.dist-info/LICENSE +21 -0
- sqlite_bro-0.13.1.dist-info/METADATA +89 -0
- sqlite_bro-0.13.1.dist-info/RECORD +9 -0
- sqlite_bro-0.13.1.dist-info/WHEEL +4 -0
- sqlite_bro-0.13.1.dist-info/entry_points.txt +3 -0
sqlite_bro/sqlite_bro.py
ADDED
|
@@ -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
|