You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1606 lines
71 KiB
1606 lines
71 KiB
6 years ago
|
#!/usr/bin/python
|
||
|
# -*- coding: utf-8 -*-
|
||
|
from __future__ import print_function, unicode_literals, division # Python2.7
|
||
|
|
||
|
|
||
|
import sqlite3 as sqlite
|
||
|
import sys
|
||
|
import os
|
||
|
import locale
|
||
|
import csv
|
||
|
import datetime
|
||
|
import io
|
||
|
import codecs
|
||
|
import shlex # Simple lexical analysis
|
||
|
|
||
|
try: # We are Python 3.3+
|
||
|
from tkinter import *
|
||
|
from tkinter import font, ttk, filedialog, messagebox
|
||
|
from tkinter.ttk import *
|
||
|
except: # or we are still Python2.7
|
||
|
from Tkinter import *
|
||
|
import Tkinter as tkinter
|
||
|
import tkFont as font
|
||
|
import tkFileDialog as filedialog
|
||
|
import tkMessageBox as messagebox
|
||
|
from ttk import *
|
||
|
import ttk as ttk
|
||
|
|
||
|
|
||
|
class App:
|
||
|
"""the GUI graphic application"""
|
||
|
def __init__(self):
|
||
|
"""create a tkk graphic interface with a main window tk_win"""
|
||
|
self.__version__ = '0.8.11'
|
||
|
self._title = "2016-03-06a : 'Combine Functions!'"
|
||
|
self.conn = None # Baresql database object
|
||
|
self.database_file = ""
|
||
|
self.tk_win = Tk()
|
||
|
self.tk_win.title('A graphic SQLite Client in 1 Python file')
|
||
|
self.tk_win.option_add('*tearOff', FALSE) # hint of tk documentation
|
||
|
self.tk_win.minsize(600, 200) # minimal size
|
||
|
|
||
|
self.font_size = 10
|
||
|
self.font_wheight = 0
|
||
|
self.initialdir = "."
|
||
|
# With a Menubar and Toolbar
|
||
|
self.create_menu()
|
||
|
self.create_toolbar()
|
||
|
|
||
|
# Create style "ButtonNotebook"
|
||
|
self.create_style()
|
||
|
# Initiate Drag State
|
||
|
self.state_drag = False
|
||
|
self.state_drag_index = 0
|
||
|
|
||
|
# With a Panedwindow of two frames: 'Database' and 'Queries'
|
||
|
p = ttk.Panedwindow(self.tk_win, orient=HORIZONTAL)
|
||
|
p.pack(fill=BOTH, expand=1)
|
||
|
|
||
|
f_database = ttk.Labelframe(p, text='Databases', width=200, height=100)
|
||
|
p.add(f_database)
|
||
|
f_queries = ttk.Labelframe(p, text='Queries', width=200, height=100)
|
||
|
p.add(f_queries)
|
||
|
|
||
|
# build tree view 't' inside the left 'Database' Frame
|
||
|
self.db_tree = ttk.Treeview(f_database, displaycolumns=[],
|
||
|
columns=("detail", "action"))
|
||
|
self.db_tree.tag_configure("run")
|
||
|
self.db_tree.pack(fill=BOTH, expand=1)
|
||
|
|
||
|
# create a notebook 'n' inside the right 'Queries' Frame
|
||
|
self.n = NotebookForQueries(self.tk_win, f_queries, [])
|
||
|
|
||
|
# Bind keyboard shortcuts
|
||
|
self.tk_win.bind('<F9>', self.run_tab)
|
||
|
|
||
|
def create_menu(self):
|
||
|
"""create the menu of the application"""
|
||
|
menubar = Menu(self.tk_win)
|
||
|
self.tk_win['menu'] = menubar
|
||
|
|
||
|
# feeding the top level menu
|
||
|
self.menu = Menu(menubar)
|
||
|
menubar.add_cascade(menu=self.menu, label='Database')
|
||
|
self.menu_help = Menu(menubar)
|
||
|
menubar.add_cascade(menu=self.menu_help, label='?')
|
||
|
|
||
|
# feeding database sub-menu
|
||
|
self.menu.add_command(label='New Database', command=self.new_db)
|
||
|
self.menu.add_command(label='New In-Memory Database',
|
||
|
command=lambda: self.new_db(":memory:"))
|
||
|
self.menu.add_command(label='Open Database ...',
|
||
|
command=self.open_db)
|
||
|
self.menu.add_command(label='Open Database ...(legacy auto-commit)',
|
||
|
command=lambda: self.open_db(""))
|
||
|
self.menu.add_command(label='Close Database', command=self.close_db)
|
||
|
self.menu.add_separator()
|
||
|
self.menu.add_command(label='Attach Database', command=self.attach_db)
|
||
|
self.menu.add_separator()
|
||
|
self.menu.add_command(label='Quit', command=self.quit_db)
|
||
|
|
||
|
self.menu_help.add_command(label='about',
|
||
|
command=lambda: messagebox.showinfo(message="""
|
||
|
\nSQLite_bro : a graphic SQLite Client in 1 Python file
|
||
|
\n("""+self.__version__+") " + self._title+"""
|
||
|
\n(https://github.com/stonebig/sqlite_bro)"""))
|
||
|
|
||
|
def create_toolbar(self):
|
||
|
"""create the toolbar of the application"""
|
||
|
self.toolbar = Frame(self.tk_win, relief=RAISED)
|
||
|
self.toolbar.pack(side=TOP, fill=X)
|
||
|
self.tk_icon = self.get_tk_icons()
|
||
|
|
||
|
# list of (image, action, tooltip) :
|
||
|
to_show = [
|
||
|
('refresh_img', self.actualize_db, "Actualize databases"),
|
||
|
('run_img', self.run_tab, "Run script selection"),
|
||
|
('newtab_img', lambda x=self: x.n.new_query_tab("___", ""),
|
||
|
"Create a new script"),
|
||
|
('csvin_img', self.import_csvtb, "Import a CSV file into a table"),
|
||
|
('csvex_img', self.export_csvtb,
|
||
|
"Export selected table to a CSV file"),
|
||
|
('dbdef_img', self.savdb_script,
|
||
|
"Save main database as a SQL script"),
|
||
|
('qryex_img', self.export_csvqr,
|
||
|
"Export script selection to a CSV file"),
|
||
|
('exe_img', self.exsav_script,
|
||
|
"Run script+output to a file (First 200 rec. per Qry)"),
|
||
|
('sqlin_img', self.load_script, "Load a SQL script file"),
|
||
|
('sqlsav_img', self.sav_script, "Save a SQL script in a file"),
|
||
|
('chgsz_img', self.chg_fontsize, "Modify font size")]
|
||
|
|
||
|
for img, action, tip in to_show:
|
||
|
b = Button(self.toolbar, image=self.tk_icon[img], command=action)
|
||
|
b.pack(side=LEFT, padx=2, pady=2)
|
||
|
self.createToolTip(b, tip)
|
||
|
|
||
|
def set_initialdir(self, proposal):
|
||
|
"""change initial dir, if possible"""
|
||
|
if os.path.isfile(proposal):
|
||
|
self.initialdir = os.path.dirname(proposal)
|
||
|
|
||
|
def new_db(self, filename=''):
|
||
|
"""create a new database"""
|
||
|
if filename == '':
|
||
|
filename = filedialog.asksaveasfilename(
|
||
|
initialdir=self.initialdir, defaultextension='.db',
|
||
|
title="Define a new database name and location",
|
||
|
filetypes=[("default", "*.db"), ("other", "*.db*"),
|
||
|
("all", "*.*")])
|
||
|
if filename != '':
|
||
|
self.database_file = filename
|
||
|
if os.path.isfile(filename):
|
||
|
self.set_initialdir(filename)
|
||
|
if messagebox.askyesno(
|
||
|
message='Confirm Destruction of previous Datas ?',
|
||
|
icon='question', title='Destroying'):
|
||
|
os.remove(filename)
|
||
|
self.conn = Baresql(self.database_file)
|
||
|
self.actualize_db()
|
||
|
|
||
|
def open_db(self, isolation_level=None):
|
||
|
"""open an existing database"""
|
||
|
filename = filedialog.askopenfilename(
|
||
|
initialdir=self.initialdir, defaultextension='.db',
|
||
|
filetypes=[("default", "*.db"), ("other", "*.db*"),
|
||
|
("all", "*.*")])
|
||
|
if filename != '':
|
||
|
self.set_initialdir(filename)
|
||
|
self.database_file = filename
|
||
|
self.conn = Baresql(self.database_file)
|
||
|
self.actualize_db()
|
||
|
|
||
|
def load_script(self):
|
||
|
"""load a script file, ask validation of detected Python code"""
|
||
|
filename = filedialog.askopenfilename(
|
||
|
initialdir=self.initialdir, defaultextension='.sql',
|
||
|
filetypes=[("default", "*.sql"), ("other", "*.txt"),
|
||
|
("all", "*.*")])
|
||
|
if filename != '':
|
||
|
self.set_initialdir(filename)
|
||
|
text = os.path.split(filename)[1].split(".")[0]
|
||
|
with io.open(filename, encoding=guess_encoding(filename)[0]) as f:
|
||
|
script = f.read()
|
||
|
sqls = self.conn.get_sqlsplit(script, remove_comments=True)
|
||
|
dg = [s for s in sqls if s.strip(' \t\n\r')[:5] == "pydef"]
|
||
|
if dg:
|
||
|
fields = ['', ['In Script File:', filename, 'r', 100], '',
|
||
|
["Python Script", "".join(dg), 'r', 80, 20]]
|
||
|
|
||
|
create_dialog(("Ok for this Python Code ?"), fields,
|
||
|
("Confirm", self.load_script_ok),
|
||
|
[text, script])
|
||
|
else:
|
||
|
new_tab_ref = self.n.new_query_tab(text, script)
|
||
|
|
||
|
def load_script_ok(self, thetop, entries, actions):
|
||
|
"""continue loading of script after confirmation dialog"""
|
||
|
new_tab_ref = self.n.new_query_tab(*actions)
|
||
|
thetop.destroy()
|
||
|
|
||
|
def savdb_script(self):
|
||
|
"""save database as a script file"""
|
||
|
filename = filedialog.asksaveasfilename(
|
||
|
initialdir=self.initialdir, defaultextension='.db',
|
||
|
title="save database structure in a text file",
|
||
|
filetypes=[("default", "*.sql"), ("other", "*.txt"),
|
||
|
("all", "*.*")])
|
||
|
if filename != '':
|
||
|
self.set_initialdir(filename)
|
||
|
with io.open(filename, 'w', encoding='utf-8') as f:
|
||
|
for line in self.conn.iterdump():
|
||
|
f.write('%s\n' % line)
|
||
|
|
||
|
def sav_script(self):
|
||
|
"""save a script in a file"""
|
||
|
active_tab_id = self.n.notebook.select()
|
||
|
if active_tab_id != '':
|
||
|
# get current selection (or all)
|
||
|
fw = self.n.fw_labels[active_tab_id]
|
||
|
script = fw.get(1.0, END)[:-1]
|
||
|
filename = filedialog.asksaveasfilename(
|
||
|
initialdir=self.initialdir, defaultextension='.db',
|
||
|
title="save script in a sql file",
|
||
|
filetypes=[("default", "*.sql"), ("other", "*.txt"),
|
||
|
("all", "*.*")])
|
||
|
if filename != "":
|
||
|
self.set_initialdir(filename)
|
||
|
with io.open(filename, 'w', encoding='utf-8') as f:
|
||
|
if "你好 мир Artisou à croute" not in script:
|
||
|
f.write("/*utf-8 tag : 你好 мир Artisou à croute*/\n")
|
||
|
f.write(script)
|
||
|
|
||
|
def attach_db(self):
|
||
|
"""attach an existing database"""
|
||
|
filename = filedialog.askopenfilename(
|
||
|
initialdir=self.initialdir, defaultextension='.db',
|
||
|
title="Choose a database to attach ",
|
||
|
filetypes=[("default", "*.db"), ("other", "*.db*"),
|
||
|
("all", "*.*")])
|
||
|
attach = os.path.basename(filename).split(".")[0]
|
||
|
avoid = {i[1]: 0 for i in get_leaves(self.conn, 'attached_databases')}
|
||
|
att, indice = attach, 0
|
||
|
while attach in avoid:
|
||
|
attach, indice = att + "_" + str(indice), indice + 1
|
||
|
if filename != '':
|
||
|
self.set_initialdir(filename)
|
||
|
attach_order = "ATTACH DATABASE '%s' as '%s' " % (filename, attach)
|
||
|
self.conn.execute(attach_order)
|
||
|
self.actualize_db()
|
||
|
|
||
|
def close_db(self):
|
||
|
"""close the database"""
|
||
|
self.conn.close
|
||
|
self.new_db(":memory:")
|
||
|
self.actualize_db()
|
||
|
|
||
|
def actualize_db(self):
|
||
|
"""refresh the database view"""
|
||
|
# bind double-click for easy user interaction
|
||
|
self.db_tree.tag_bind('run', '<Double-1>', self.t_doubleClicked)
|
||
|
self.db_tree.tag_bind('run_up', '<Double-1>', self.t_doubleClicked)
|
||
|
|
||
|
# delete existing tree entries before re-creating them
|
||
|
for node in self.db_tree.get_children():
|
||
|
self.db_tree.delete(node)
|
||
|
# create top node
|
||
|
dbtext = os.path.basename(self.database_file)
|
||
|
id0 = self.db_tree.insert(
|
||
|
"", 0, "Database", text="main (%s)" % dbtext, values=(dbtext, ""))
|
||
|
# add Database Objects, by Category
|
||
|
for categ in ['master_table', 'table', 'view', 'trigger', 'index',
|
||
|
'pydef']:
|
||
|
self.feed_dbtree(id0, categ, "main")
|
||
|
# for attached databases
|
||
|
for att_db in self.feed_dbtree(id0, 'attached_databases'):
|
||
|
# create another top node
|
||
|
dbtext2, insert_position = att_db + " (Attached)", 'end'
|
||
|
if att_db == "temp":
|
||
|
dbtext2, insert_position = "temp (%s)" % dbtext, 0
|
||
|
id0 = self.db_tree.insert("", insert_position, dbtext2,
|
||
|
text=dbtext2, values=(att_db, ""))
|
||
|
# add attached Database Objects, by Category
|
||
|
for categ in ['master_table', 'table', 'view', 'trigger', 'index']:
|
||
|
self.feed_dbtree(id0, categ, att_db)
|
||
|
# update time of last refresh
|
||
|
self.db_tree.heading('#0', text=(
|
||
|
datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
|
||
|
|
||
|
def quit_db(self):
|
||
|
"""quit the application"""
|
||
|
if messagebox.askyesno(message='Are you sure you want to quit ?',
|
||
|
icon='question', title='Quiting'):
|
||
|
self.tk_win.destroy()
|
||
|
|
||
|
def run_tab(self, event=None):
|
||
|
"""clear previous results and run current script of a tab"""
|
||
|
active_tab_id = self.n.notebook.select()
|
||
|
if active_tab_id != '':
|
||
|
# remove previous results
|
||
|
self.n.remove_treeviews(active_tab_id)
|
||
|
# get current selection (or all)
|
||
|
fw = self.n.fw_labels[active_tab_id]
|
||
|
try:
|
||
|
script = (fw.get('sel.first', 'sel.last'))
|
||
|
except:
|
||
|
script = fw.get(1.0, END)[:-1]
|
||
|
self.create_and_add_results(script, active_tab_id)
|
||
|
fw.focus_set() # workaround bug http://bugs.python.org/issue17511
|
||
|
|
||
|
def exsav_script(self):
|
||
|
"""write script commands + top results to a log file"""
|
||
|
# idea from http://blog.mp933.fr/post/2014/05/15/Script-vs.-copy/paste
|
||
|
active_tab_id = self.n.notebook.select()
|
||
|
if active_tab_id != '':
|
||
|
# get current selection (or all)
|
||
|
fw = self.n.fw_labels[active_tab_id]
|
||
|
script = fw.get(1.0, END)[:-1]
|
||
|
filename = filedialog.asksaveasfilename(
|
||
|
initialdir=self.initialdir, defaultextension='.db',
|
||
|
title="execute Script + output in a log file",
|
||
|
filetypes=[("default", "*.txt"), ("other", "*.log"),
|
||
|
("all", "*.*")])
|
||
|
if filename == "":
|
||
|
return
|
||
|
self.set_initialdir(filename)
|
||
|
with io.open(filename, 'w', encoding='utf-8') as f:
|
||
|
if "你好 мир Artisou à croute" not in script:
|
||
|
f.write("/*utf-8 tag : 你好 мир Artisou à croute*/\n")
|
||
|
self.create_and_add_results(script, active_tab_id, limit=99, log=f)
|
||
|
fw.focus_set() # workaround bug http://bugs.python.org/issue17511
|
||
|
|
||
|
def chg_fontsize(self):
|
||
|
"""change the display font size"""
|
||
|
sizes = [10, 13, 14]
|
||
|
font_types = ["TkDefaultFont", "TkTextFont", "TkFixedFont",
|
||
|
"TkMenuFont", "TkHeadingFont", "TkCaptionFont",
|
||
|
"TkSmallCaptionFont", "TkIconFont", "TkTooltipFont"]
|
||
|
ww = ['normal', 'bold']
|
||
|
if self.font_size < max(sizes):
|
||
|
self.font_size = min([i for i in sizes if i > self.font_size])
|
||
|
else:
|
||
|
self.font_size = sizes[0]
|
||
|
self.font_wheight = 0
|
||
|
|
||
|
ff = 'Helvetica' if self.font_size != min(sizes) else 'Courier'
|
||
|
self.font_wheight = 0 if self.font_size == min(sizes) else 1
|
||
|
for typ in font_types:
|
||
|
default_font = font.nametofont(typ)
|
||
|
default_font.configure(size=self.font_size,
|
||
|
weight=ww[self.font_wheight], family=ff)
|
||
|
|
||
|
def t_doubleClicked(self, event):
|
||
|
"""launch action when dbl_click on the Database structure"""
|
||
|
# determine item to consider
|
||
|
selitem = self.db_tree.focus() # the item having the focus
|
||
|
seltag = self.db_tree.item(selitem, "tag")[0]
|
||
|
if seltag == "run_up": # 'run-up' tag ==> dbl-click 1 level up
|
||
|
selitem = self.db_tree.parent(selitem)
|
||
|
# get final information : text, selection and action
|
||
|
definition, action = self.db_tree.item(selitem, "values")
|
||
|
tab_text = self.db_tree.item(selitem, "text")
|
||
|
script = action + " limit 999 " if action != "" else definition
|
||
|
|
||
|
# create a new tab and run it if action suggest it
|
||
|
new_tab_ref = self.n.new_query_tab(tab_text, script)
|
||
|
if action != '':
|
||
|
self.run_tab() # run the new_tab created
|
||
|
|
||
|
def get_tk_icons(self):
|
||
|
"""return a dictionary of icon_in_tk_format, from B64 images"""
|
||
|
# to create this base 64 from a toto.gif image of 24x24 size do :
|
||
|
# import base64
|
||
|
# b64 = base64.encodestring(open(r"toto.gif","rb").read())
|
||
|
# print("'gif_img': '''\\\n" + b64.decode("utf8") + "'''")
|
||
|
icons = {
|
||
|
'run_img': '''\
|
||
|
R0lGODdhGAAYAJkAADOqM////wCqMwAAACwAAAAAGAAYAAACM4SPqcvt7wJ8oU5W8025b9OFW0hO
|
||
|
5EmdKKauSosKL9zJC21FsK27kG+qfUC5IciITConBQA7
|
||
|
''',
|
||
|
'exe_img': '''\
|
||
|
R0lGODdhGAAYALsAAP///zOqM/8AAGSJtqHA4Jyen3ul0+jo6Y6z2cLCwaSmpACqM4ODgmKGs4yM
|
||
|
jYOPniwAAAAAGAAYAAAEhBDISacqOBdWOy1HKB6F41mKwihH4r4kdypD0wx4rg8nUDSEoHAY5J0K
|
||
|
AyFiyWQaPY+kYUqtGp4dx26b60kE4LC3FwaPyeJOYM1ur8sCzxrgZovN6sDEHdYD4nkVb2BzPYUV
|
||
|
hIdyfouMi14BC5COgoqBHQttk5VumxJ1bJuZoJacpKE9EQA7
|
||
|
''',
|
||
|
'refresh_img': '''\
|
||
|
R0lGODdhGAAYAJkAAP///zOqMwCqMwAAACwAAAAAGAAYAAACSoSPqcvt4aIJEFU5g7AUC9px1/JR
|
||
|
3yYy4LqAils2IZdFMzCP6nhLd2/j6VqHD+1RAQKLHVfC+VwtcT3pNKOTYjTC4SOK+YbH5EYBADs=
|
||
|
''',
|
||
|
'newtab_img': '''\
|
||
|
R0lGODdhGAAYAJkAAP///56fnQAAAAAAACwAAAAAGAAYAAACSoSPqcsm36KDsj1R1d00840E4ige
|
||
|
3xWSo9YppRGwGKPGCUrXtfQCut3ouYC5IHEhTCSHRt4x5fytIlKSs0l9HpZKLcy7BXOhRUYBADs=
|
||
|
''',
|
||
|
'csvin_img': '''\
|
||
|
R0lGODdhGAAYAMwAAPj4+AAAADOqM2FkZtjY2r7Awujo6V9gYeDg4b/Cwzc3N0pKSl9fX5GRkVVV
|
||
|
VXl6fKSmpLCxsouNkFdXV97d4N7e4N7g4IyMjZyen6SopwAAAAAAAAAAAAAAAAAAAAAAACwAAAAA
|
||
|
GAAYAAAFlSAgjkBgmuUZlONKii4br/MLv/Gt47ia/rYcT2bb0VowVFFF8+2K0KjUJqhOo1XBlaQV
|
||
|
Zbdc7Rc8ylrJ5THaa5YqFozBgOFQAMznl6FhsO37UwMEBgiFFRYIhANXBxgJBQUJkpAZi1MEBxAR
|
||
|
kI8REAMUVxIEcgcDpqYEElcODwSvsK8PllMLAxeQkA0DDmhvEwwLdmAhADs=
|
||
|
''',
|
||
|
'csvex_img': '''\
|
||
|
R0lGODdhGAAYAMwAAPj4+AAAADOqM2FkZtjY2r7AwuDg4b/Cw+jo6V9gYTc3N0pKSlVVVV9fX5GR
|
||
|
kaSmpLCxsnl6fIuNkN7g4N7d4KSop4yMjZyen1dXVwAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAA
|
||
|
GAAYAAAFkiAgjkBgmuUZlONKii4br/MLv/Gt47ia/rYcT2bb0VowVFFF8+2K0Kh0JKhap1BrFZu9
|
||
|
cl9awZd03Y4BXvQ5DVUsGoNBg6FAm6MOhA3h4A4ICAaCBhOCCANYAxcHBQUHj44ViFMECQ8QjY0Q
|
||
|
DwMUWBIEcQkDo6MEElgMEQSsrawRk1MLAxaZBQ4DDGduGA0LdV8hADs=
|
||
|
''',
|
||
|
'qryex_img': '''\
|
||
|
R0lGODdhGAAYAJkAAP///56fnQAAAP8AACwAAAAAGAAYAAACXIQPoporeR4yEtZ3J511e845zah1
|
||
|
oKV9WEQxqYOJX0rX9oDndp3jO6/7aXqDVOCIPB50Pk0yaQgCijSlITBt/p4B6ZbL3VkBYKxt7DTX
|
||
|
0BN2uowUw+NndVq+tk8KADs=
|
||
|
''',
|
||
|
'sqlin_img': '''\
|
||
|
R0lGODdhGAAYALsAAP///46z2Xul02SJtp6fnenp6f8AAMLCwaHA4IODgoCo01RymIOPnmKGswAA
|
||
|
AAAAACwAAAAAGAAYAAAEkRDIOYm9N9G9SfngR2jclnhHqh7FWAKZF84uFxdr3pb3TPOWEy6n2tkG
|
||
|
jcZgyWw6OwOEdEqtIgYbRjTA7Xq/WIoW8S17wxOteR1AS9Ts8sI08Aru+Px9TknU9YB5fBN+AYGH
|
||
|
gxJ+dwoCjY+OCpKNiQAGBk6ZTgsGE5edLy+XlqOhop+gpiWoqqGoqa0Ur7CxABEAOw==
|
||
|
''',
|
||
|
'sqlsav_img': '''\
|
||
|
R0lGODdhGAAYALsAAP///56fnZyen/8AAGSJtqHA4Hul0+jo6Y6z2cLCwaSmpIODgmKGs4yMjYOP
|
||
|
ngAAACwAAAAAGAAYAAAEgxDISacSOItVOxVHKB5C41mKsihH4r4kdyoEwxB4rhMnIDCFoHAY5J0E
|
||
|
BCFiyWQaPY6kYUqtGp6dxm6b60kG4LC3FwaPyeJzpzzwaDQTsbkTqNsx3zmgfapPAnt6Y3Z1Amlq
|
||
|
AoR3cF5+EoqFY4k9jpSAfQKSkJCDm4SZXpN9l5aUoB4RADs=
|
||
|
''',
|
||
|
'dbdef_img': '''\
|
||
|
R0lGODdhGAAYAMwAAPj4+DOqM2SJtmFkZqHA4NjY2sLCwejo6b7Awpyen3ul046z2aSop+Dg4V9g
|
||
|
YZGRkaSmpLCxsouNkDc3N2dxekpKSlVVVYyMjWKGs4ODgnl6fJ+goYOPnl9fX0xMTAAAACwAAAAA
|
||
|
GAAYAAAFuCAgjuTIJGiaZGVLJkcsH9DjmgxzMYfh/4fVDcAQCDDGpNI4hGAI0KgUKhgmBNGFdrut
|
||
|
3jhYhXhMVnhdj6U6OWy7h4G4/O2Sx+n1OZ5kD5QmHgOCAx0TJXN3Iw8HLQc2InoAfiIUBQcNmJkN
|
||
|
BxSSiS0DCT8/CAYMA55DBQMQEQilCBGndBKrAw4Ot7kFEm+rG66vsRCob7WCube3vG8WGgXQ0dAa
|
||
|
nW8VAxfCCA8DFnsAExUWAxWGeCEAOw==
|
||
|
''',
|
||
|
'chgsz_img': '''\
|
||
|
R0lGODdhGAAYAJkAAP///wAAADOqMwCqMywAAAAAGAAYAAACZISPGRvpb1iDRjy5KBBWYc0NXjQ9
|
||
|
A8cdDFkiZyiIwDpnCYqzCF2lr2rTHVKbDgsTJG52yE8R0nRSJA7qNOhpVbFPHhdhPF20w46S+f2h
|
||
|
xlzceksqu6ET7JwtLRrhwNt+1HdDUQAAOw==
|
||
|
''',
|
||
|
'img_close': '''\
|
||
|
R0lGODlhCAAIAMIBAAAAADs7O4+Pj9nZ2Ts7Ozs7Ozs7Ozs7OyH+EUNyZWF0ZWQgd2l0aCBHSU1Q
|
||
|
ACH5BAEKAAQALAAAAAAIAAgAAAMVGDBEA0qNJyGw7AmxmuaZhWEU5kEJADs=
|
||
|
''',
|
||
|
'img_closeactive': '''\
|
||
|
R0lGODlhCAAIAMIEAAAAAP/SAP/bNNnZ2cbGxsbGxsbGxsbGxiH5BAEKAAQALAAAAAAIAAgAAAMV
|
||
|
GDBEA0qNJyGw7AmxmuaZhWEU5kEJADs=
|
||
|
''',
|
||
|
'img_closepressed': '''\
|
||
|
R0lGODdhCAAIAIgAAPAAAP///ywAAAAACAAIAAACDkyAeJYM7FR8Ex7aVpIFADs=
|
||
|
'''
|
||
|
}
|
||
|
return {k: PhotoImage(k, data=v) for k, v in icons.items()}
|
||
|
|
||
|
def btn_chg_tab_ok(self, thetop, entries, actions):
|
||
|
"""chg a tab title"""
|
||
|
widget, index = actions
|
||
|
# build dico of result
|
||
|
d = {f[0]: f[1]() for f in entries
|
||
|
if not isinstance(f, (type('e'), type(u'e')))}
|
||
|
|
||
|
title = d['new label'].strip()
|
||
|
thetop.destroy()
|
||
|
widget.tab(index, text=title)
|
||
|
|
||
|
def btn_presstwice(self, event):
|
||
|
"""double-click on a tab definition to change label"""
|
||
|
x, y, widget = event.x, event.y, event.widget
|
||
|
elem = widget.identify(x, y)
|
||
|
if "label" in elem: # and widget.instate(['pressed']):
|
||
|
index = widget.index("@%d,%d" % (x, y))
|
||
|
titre = widget.tab(index, 'text')
|
||
|
# determine selected table
|
||
|
actions = [widget, index]
|
||
|
title = 'Changing Tab label'
|
||
|
fields = ['', ['current label', (titre), 'r', 30], '',
|
||
|
['new label', titre, 'w', 30]]
|
||
|
create_dialog(title, fields, ("Ok", self.btn_chg_tab_ok), actions)
|
||
|
|
||
|
def btn_press(self, event):
|
||
|
"""button press over a widget with a 'close' element"""
|
||
|
x, y, widget = event.x, event.y, event.widget
|
||
|
elem = widget.identify(x, y) # widget is the notebook
|
||
|
if "close" in elem: # close button function
|
||
|
index = widget.index("@%d,%d" % (x, y))
|
||
|
widget.state(['pressed'])
|
||
|
widget.pressed_index = index
|
||
|
elif "label" in elem: # move function
|
||
|
index = widget.index("@%d,%d" % (x, y))
|
||
|
self.state_drag = True
|
||
|
self.state_drag_widgetid = widget.tabs()[index]
|
||
|
self.state_drag_index = index
|
||
|
|
||
|
def btn_Movex(self, event):
|
||
|
"""make the tab follows if button is pressed and mouse moves"""
|
||
|
x, y, widget = event.x, event.y, event.widget
|
||
|
elem = widget.identify(x, y)
|
||
|
if "label" in elem: # and widget.instate(['pressed']):
|
||
|
index = widget.index("@%d,%d" % (x, y))
|
||
|
if self.state_drag:
|
||
|
if self.state_drag_index != index:
|
||
|
self.btn_Move(widget, self.state_drag_index, index)
|
||
|
self.state_drag_index = index
|
||
|
|
||
|
def btn_Move(self, notebook, old_index, new_index):
|
||
|
"""Move old_index tab to new_index position"""
|
||
|
# stackoverflow.com/questions/11570786/tkinter-treeview-drag-and-drop
|
||
|
if new_index != "":
|
||
|
target_index = new_index
|
||
|
if new_index >= len(notebook.tabs())-1:
|
||
|
target_index = "end"
|
||
|
titre = notebook.tab(old_index, 'text')
|
||
|
notebook.forget(old_index)
|
||
|
notebook.insert(target_index, self.state_drag_widgetid, text=titre)
|
||
|
notebook.select(new_index)
|
||
|
|
||
|
def btn_release(self, event):
|
||
|
"""button release over a widget with a 'close' element"""
|
||
|
x, y, widget = event.x, event.y, event.widget
|
||
|
elem = widget.identify(x, y)
|
||
|
index = self.state_drag_index
|
||
|
if "close" in elem or "label" in elem:
|
||
|
index = widget.index("@%d,%d" % (x, y))
|
||
|
if "close" in elem and widget.instate(['pressed']):
|
||
|
if widget.pressed_index == index:
|
||
|
widget.forget(index)
|
||
|
widget.event_generate("<<NotebookClosedTab>>")
|
||
|
if self.state_drag and elem.strip() != "":
|
||
|
if self.state_drag_index != index:
|
||
|
self.btn_Move(widget, self.state_drag_index, index)
|
||
|
self.state_drag = False
|
||
|
|
||
|
if not widget.instate(['pressed']):
|
||
|
return
|
||
|
widget.state(["!pressed"])
|
||
|
widget.pressed_index = None
|
||
|
|
||
|
def create_style(self):
|
||
|
"""create a Notebook style with close button"""
|
||
|
# from https://github.com/python-git/python/blob/master/Demo/tkinter/
|
||
|
# ttk/notebook_closebtn.py
|
||
|
# himself from http://paste.tclers.tk/896
|
||
|
style = ttk.Style()
|
||
|
|
||
|
style.element_create("close", "image", "img_close",
|
||
|
("active", "pressed", "!disabled", "img_closepressed"),
|
||
|
("active", "!disabled", "img_closeactive"), border=6, sticky='')
|
||
|
|
||
|
style.layout("ButtonNotebook", [
|
||
|
("ButtonNotebook.client", {"sticky": "nswe"})])
|
||
|
style.layout("ButtonNotebook.Tab", [
|
||
|
("ButtonNotebook.tab", {"sticky": "nswe", "children":
|
||
|
[("ButtonNotebook.padding", {"side": "top", "sticky": "nswe",
|
||
|
"children":
|
||
|
[("ButtonNotebook.focus", {"side": "top", "sticky": "nswe",
|
||
|
"children":
|
||
|
[("ButtonNotebook.label", {"side": "left", "sticky": ''}),
|
||
|
("ButtonNotebook.close", {"side": "left", "sticky": ''})]
|
||
|
})]
|
||
|
})]
|
||
|
})]
|
||
|
)
|
||
|
|
||
|
self.tk_win.bind_class("TNotebook", "<ButtonPress-1>",
|
||
|
self.btn_press, True)
|
||
|
self.tk_win.bind_class("TNotebook", "<ButtonRelease-1>",
|
||
|
self.btn_release)
|
||
|
self.tk_win.bind_class("TNotebook", "<B1-Motion>", self.btn_Movex)
|
||
|
self.tk_win.bind_class("TNotebook", "<Double-1>", self.btn_presstwice)
|
||
|
|
||
|
def createToolTip(self, widget, text):
|
||
|
"""create a tooptip box for a widget."""
|
||
|
# www.daniweb.com/software-development/python/code/234888/tooltip-box
|
||
|
def enter(event):
|
||
|
global tipwindow
|
||
|
x = y = 0
|
||
|
try:
|
||
|
tipwindow = tipwindow
|
||
|
except:
|
||
|
tipwindow = None
|
||
|
if tipwindow or not text:
|
||
|
return
|
||
|
x, y, cx, cy = widget.bbox("insert")
|
||
|
x += widget.winfo_rootx() + 27
|
||
|
y += widget.winfo_rooty() + 27
|
||
|
# Creates a toplevel window
|
||
|
tipwindow = tw = Toplevel(widget)
|
||
|
# Leaves only the label and removes the app window
|
||
|
tw.wm_overrideredirect(1)
|
||
|
tw.wm_geometry("+%d+%d" % (x, y))
|
||
|
label = Label(tw, text=text, justify=LEFT, background="#ffffe0",
|
||
|
relief=SOLID, borderwidth=1)
|
||
|
label.pack(ipadx=1)
|
||
|
|
||
|
def close(event):
|
||
|
global tipwindow
|
||
|
tw = tipwindow
|
||
|
tipwindow = None
|
||
|
if tw:
|
||
|
tw.destroy()
|
||
|
|
||
|
widget.bind("<Enter>", enter)
|
||
|
widget.bind("<Leave>", close)
|
||
|
|
||
|
def feed_dbtree(self, root_id, category, attached_db=""):
|
||
|
"""feed database treeview for category, return list of leaves names"""
|
||
|
|
||
|
# prepare re-formatting functions for fields and database names
|
||
|
def f(t): return ('"%s"' % t.replace('"', '""')) if t != "" else t
|
||
|
|
||
|
def db(t): return ('"%s".' % t.replace('"', '""')) if t != "" else t
|
||
|
attached = db(attached_db)
|
||
|
|
||
|
# get Category list of [unique_name, name, definition, sub_category]
|
||
|
tables = get_leaves(self.conn, category, attached_db)
|
||
|
if len(tables) > 0:
|
||
|
# level 1 : create the "category" node (as Category is not empty)
|
||
|
root_txt = "%s(%s)" % (attached, category)
|
||
|
idt = self.db_tree.insert(
|
||
|
root_id, "end", root_txt,
|
||
|
text="%s (%s)" % (category, len(tables)), values=("", ""))
|
||
|
for t_id, t_name, definition, sub_cat in tables:
|
||
|
# level 2 : print object creation, and '(Definition)' if fields
|
||
|
sql3 = ""
|
||
|
if sub_cat != '':
|
||
|
# it's a table : prepare a Query with names of each column
|
||
|
sub_c = get_leaves(self.conn, sub_cat, attached_db, t_name)
|
||
|
colnames = [col[1] for col in sub_c]
|
||
|
columns = [col[1] + ' ' + col[2] for col in sub_c]
|
||
|
sql3 = 'select "'+'" , "'.join(colnames)+'" from ' + (
|
||
|
'%s%s' % (attached, f(t_name)))
|
||
|
idc = self.db_tree.insert(
|
||
|
idt, "end", "%s%s" % (root_txt, t_id),
|
||
|
text=t_name, tags=('run',), values=(definition, sql3))
|
||
|
if sql3 != "":
|
||
|
self.db_tree.insert(
|
||
|
idc, "end", ("%s%s;d" % (root_txt, t_id)),
|
||
|
text=['(Definition)'], tags=('run',),
|
||
|
values=(definition, ""))
|
||
|
# level 3 : Insert a line per column of the Table/View
|
||
|
for c in range(len(sub_c)):
|
||
|
self.db_tree.insert(
|
||
|
idc, "end", "%s%s%s" % (root_txt, t_id, sub_c[c][0]),
|
||
|
text=columns[c], tags=('run_up',), values=('', ''))
|
||
|
return [i[1] for i in tables]
|
||
|
|
||
|
def create_and_add_results(self, instructions, tab_tk_id,
|
||
|
limit=-1, log=None):
|
||
|
"""execute instructions and add them to given tab results"""
|
||
|
a_jouer = self.conn.get_sqlsplit(instructions, remove_comments=False)
|
||
|
# must read :https://www.youtube.com/watch?v=09tM18_st4I#t=1751
|
||
|
# stackoverflow.com/questions/15856976/transactions-with-python-sqlite3
|
||
|
isolation = self.conn.conn.isolation_level
|
||
|
counter = 0
|
||
|
shell_list = ['', '']
|
||
|
if isolation == "": # Sqlite3 and dump.py default don't match
|
||
|
self.conn.conn.isolation_level = None # right behavior
|
||
|
cu = self.conn.conn.cursor()
|
||
|
sql_error = False
|
||
|
|
||
|
def beurk(r):
|
||
|
"""format data line log"""
|
||
|
s = ['"' + s.replace('"', '""') + '"' if
|
||
|
isinstance(s, (type('e'), type(u'e'))) else str(s) for s in r]
|
||
|
return "("+",".join(s)+")"
|
||
|
|
||
|
def bip(c):
|
||
|
"""format instruction log header"""
|
||
|
timing = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
|
return ("\n---------N°%s----------[" % counter + timing + "]\n\n")
|
||
|
|
||
|
for instruction in a_jouer:
|
||
|
if log is not None: # write to logFile
|
||
|
counter += 1
|
||
|
log.write(bip(counter))
|
||
|
log.write(instruction)
|
||
|
log.write("\n")
|
||
|
instru = next(self.conn.get_sqlsplit(instruction,
|
||
|
remove_comments=True))
|
||
|
instru = instru.replace(";", "").strip(' \t\n\r')
|
||
|
first_line = (instru + "\n").splitlines()[0]
|
||
|
if instru[:5] == "pydef":
|
||
|
pydef = self.conn.createpydef(instru)
|
||
|
titles = ("Creating embedded python function",)
|
||
|
rows = self.conn.conn_def[pydef]['pydef'].splitlines()
|
||
|
rows.append(self.conn.conn_def[pydef]['inst'])
|
||
|
self.n.add_treeview(tab_tk_id, titles, rows, "Info", pydef)
|
||
|
if log is not None: # write to logFile
|
||
|
log.write("\n".join(['("%s")' % r for r in rows])+"\n")
|
||
|
elif instru[:1] == ".": # a shell command !
|
||
|
# handle a ".function" here !
|
||
|
# import FILE TABLE
|
||
|
shell_list = shlex.split(instru) # magic standard library
|
||
|
try:
|
||
|
if shell_list[0] == '.import' and len(shell_list) >= 2:
|
||
|
csv_file = shell_list[1]
|
||
|
guess = guess_csv(csv_file)
|
||
|
if len(shell_list) >= 3:
|
||
|
guess.table_name = shell_list[2]
|
||
|
# Create csv reader and give it to import
|
||
|
reading = read_this_csv(csv_file,
|
||
|
guess.encodings[0],
|
||
|
guess.default_sep,
|
||
|
guess.default_quote,
|
||
|
guess.has_header,
|
||
|
guess.default_decims[0])
|
||
|
guess_sql = guess_sql_creation(guess.table_name,
|
||
|
guess.default_sep, ".",
|
||
|
guess.has_header,
|
||
|
guess.dlines,
|
||
|
guess.default_quote)[0]
|
||
|
self.conn.insert_reader(reading, guess.table_name,
|
||
|
guess_sql, create_table=False,
|
||
|
replace=False)
|
||
|
self.n.add_treeview(tab_tk_id, ('table', 'file'),
|
||
|
((guess.table_name, csv_file),),
|
||
|
"Info", first_line)
|
||
|
if log is not None: # write to logFile
|
||
|
log.write('-- File %s imported in "%s"\n' % (
|
||
|
csv_file, guess.table_name))
|
||
|
except IOError as err:
|
||
|
msg = ("I/O error: {0}".format(err))
|
||
|
self.n.add_treeview(tab_tk_id, ('Error !',), [(msg,)],
|
||
|
"Error !", instru)
|
||
|
if log is not None: # write to logFile
|
||
|
log.write("Error ! %s : %s" % (msg, instru))
|
||
|
sql_error = True
|
||
|
break
|
||
|
elif instruction != "":
|
||
|
try:
|
||
|
if shell_list[0] == '.once':
|
||
|
shell_list[0] = ' '
|
||
|
encode_in = 'utf-8-sig' if os.name == 'nt' else 'utf-8'
|
||
|
self.conn.export_writer(instruction, shell_list[1],
|
||
|
encoding=encode_in)
|
||
|
self.n.add_treeview(tab_tk_id, ('qry', 'file'),
|
||
|
((instruction, shell_list[1]),),
|
||
|
"Info", ".once %s" % shell_list[1])
|
||
|
else:
|
||
|
cur = cu.execute(instruction)
|
||
|
rows = cur.fetchall()
|
||
|
# a query may have no result( like for an "update")
|
||
|
if cur.description is not None:
|
||
|
titles = [row_info[0] for
|
||
|
row_info in cur.description]
|
||
|
self.n.add_treeview(
|
||
|
tab_tk_id, titles, rows, "Qry", first_line)
|
||
|
if log is not None: # write to logFile
|
||
|
log.write(beurk(titles) + "\n")
|
||
|
log.write("\n".join(
|
||
|
[beurk(l) for l in rows[:limit]]) + "\n")
|
||
|
if len(rows) > limit:
|
||
|
log.write("%s more..." % len((rows)-limit))
|
||
|
except sqlite.Error as msg: # OperationalError
|
||
|
self.n.add_treeview(tab_tk_id, ('Error !',), [(msg,)],
|
||
|
"Error !", first_line)
|
||
|
if log is not None: # write to logFile
|
||
|
log.write("Error ! %s" % msg)
|
||
|
sql_error = True
|
||
|
break
|
||
|
|
||
|
if self.conn.conn.isolation_level != isolation:
|
||
|
# if we're in 'backward' compatible mode (automatic commit)
|
||
|
try:
|
||
|
if self.conn.conn.in_transaction: # python 3.2
|
||
|
if not sql_error:
|
||
|
cu.execute("COMMIT;")
|
||
|
if log is not None: # write to logFile
|
||
|
log.write("\n-------COMMIT;--------\n" % counter)
|
||
|
else:
|
||
|
cu.execute("ROLLBACK;")
|
||
|
except:
|
||
|
if not sql_error:
|
||
|
try:
|
||
|
cu.execute("COMMIT;")
|
||
|
if log is not None: # write to logFile
|
||
|
log.write("\n-------COMMIT;--------\n" % counter)
|
||
|
except:
|
||
|
pass
|
||
|
else:
|
||
|
try:
|
||
|
cu.execute("ROLLBACK;")
|
||
|
except:
|
||
|
pass
|
||
|
self.conn.conn.isolation_level = isolation # restore standard
|
||
|
|
||
|
def import_csvtb(self):
|
||
|
"""import csv dialog (with guessing of encoding and separator)"""
|
||
|
csv_file = filedialog.askopenfilename(
|
||
|
initialdir=self.initialdir, defaultextension='.db',
|
||
|
title="Choose a csv fileto import ",
|
||
|
filetypes=[("default", "*.csv"), ("other", "*.txt"),
|
||
|
("all", "*.*")])
|
||
|
if csv_file != '':
|
||
|
self.set_initialdir(csv_file)
|
||
|
# guess all via an object
|
||
|
guess = guess_csv(csv_file)
|
||
|
guess_sql = guess_sql_creation(guess.table_name, guess.default_sep,
|
||
|
".", guess.has_header, guess.dlines,
|
||
|
guess.default_quote)[2]
|
||
|
# check it via dialog box
|
||
|
fields_in = ['', ['csv Name', csv_file, 'r', 100], '',
|
||
|
['table Name', guess.table_name],
|
||
|
['column separator', guess.default_sep, 'w', 20],
|
||
|
['string delimiter', guess.default_quote, 'w', 20],
|
||
|
'', ['Decimal separator', guess.default_decims],
|
||
|
['Encoding', guess.encodings],
|
||
|
'Fliflaps', ['Header line', guess.has_header],
|
||
|
['Create table', True],
|
||
|
['Replace existing data', True], '',
|
||
|
['first 3 lines', guess.dlines, 'r', 100, 10], '',
|
||
|
['use manual creation request', False], '',
|
||
|
['creation request', guess_sql, 'w', 100, 10]]
|
||
|
actions = ([self.conn, self.actualize_db])
|
||
|
create_dialog(("Importing %s" % csv_file), fields_in,
|
||
|
("Import", import_csvtb_ok), actions)
|
||
|
|
||
|
def export_csvtb(self):
|
||
|
"""get selected table definition and launch cvs export dialog"""
|
||
|
# determine selected table
|
||
|
actions = [self.conn, self.db_tree]
|
||
|
selitem = self.db_tree.focus() # get tree item having the focus
|
||
|
if selitem != '':
|
||
|
seltag = self.db_tree.item(selitem, "tag")[0]
|
||
|
if seltag == "run_up": # if 'run-up', do as dbl-click 1 level up
|
||
|
selitem = self.db_tree.parent(selitem)
|
||
|
# get final information
|
||
|
definition, query = self.db_tree.item(selitem, "values")
|
||
|
if query != "": # run the export_csv dialog
|
||
|
title = ('Export Table "%s" to ?' %
|
||
|
self.db_tree.item(selitem, "text"))
|
||
|
self.export_csv_dialog(query, title, actions)
|
||
|
|
||
|
def export_csvqr(self):
|
||
|
"""get tab selected definition and launch cvs export dialog"""
|
||
|
actions = [self.conn, self.n]
|
||
|
active_tab_id = self.n.notebook.select()
|
||
|
if active_tab_id != '': # get current selection (or all)
|
||
|
fw = self.n.fw_labels[active_tab_id]
|
||
|
try:
|
||
|
query = fw.get('sel.first', 'sel.last')
|
||
|
except:
|
||
|
query = fw.get(1.0, END)[:-1]
|
||
|
if query != "":
|
||
|
self.export_csv_dialog(query, "Export Query", actions)
|
||
|
|
||
|
def export_csv_dialog(self, query="--", text="undefined.csv", actions=[]):
|
||
|
"""export csv dialog"""
|
||
|
# proposed encoding (we favorize utf-8 or utf-8-sig)
|
||
|
encodings = ["utf-8", locale.getdefaultlocale()[1], "utf-16",
|
||
|
"utf-8-sig"]
|
||
|
if os.name == 'nt':
|
||
|
encodings = ["utf-8-sig", locale.getdefaultlocale()[1], "utf-16",
|
||
|
"utf-8"]
|
||
|
# proposed csv separator
|
||
|
default_sep = [",", "|", ";"]
|
||
|
csv_file = filedialog.asksaveasfilename(
|
||
|
initialdir=self.initialdir, defaultextension='.db', title=text,
|
||
|
filetypes=[("default", "*.csv"), ("other", "*.txt"),
|
||
|
("all", "*.*")])
|
||
|
if csv_file != "":
|
||
|
# Idea from (http://www.python-course.eu/tkinter_entry_widgets.php)
|
||
|
fields = ['', ['csv Name', csv_file, 'r', 100], '',
|
||
|
['column separator', default_sep],
|
||
|
['Header line', True],
|
||
|
['Encoding', encodings], '',
|
||
|
["Data to export (MUST be 1 Request)",
|
||
|
(query), 'w', 100, 10]]
|
||
|
|
||
|
create_dialog(("Export to %s" % csv_file), fields,
|
||
|
("Export", export_csv_ok), actions)
|
||
|
|
||
|
|
||
|
class NotebookForQueries():
|
||
|
"""Create a Notebook with a list in the First frame
|
||
|
and query results in following treeview frames """
|
||
|
|
||
|
def __init__(self, tk_win, root, queries):
|
||
|
self.tk_win = tk_win
|
||
|
self.root = root
|
||
|
self.notebook = Notebook(root, style="ButtonNotebook") # ttk.
|
||
|
|
||
|
self.fw_labels = {} # tab_tk_id -> Scripting frame python object
|
||
|
self.fw_result_nbs = {} # tab_tk_id -> Notebook of Results
|
||
|
|
||
|
# resize rules
|
||
|
root.columnconfigure(0, weight=1)
|
||
|
root.rowconfigure(0, weight=1)
|
||
|
# grid widgets
|
||
|
self.notebook.grid(row=0, column=0, sticky=(N, W, S, E))
|
||
|
|
||
|
def new_query_tab(self, title, query):
|
||
|
"""add a Tab 'title' to the notebook, containing the Script 'query'"""
|
||
|
|
||
|
fw_welcome = ttk.Panedwindow(self.tk_win, orient=VERTICAL) # tk_win
|
||
|
fw_welcome.pack(fill='both', expand=True)
|
||
|
self.notebook.add(fw_welcome, text=(title))
|
||
|
|
||
|
# new "editable" script
|
||
|
f1 = ttk.Labelframe(fw_welcome, text='Script', width=200, height=100)
|
||
|
fw_welcome.add(f1)
|
||
|
fw_label = Text(f1, bd=1, undo=True)
|
||
|
|
||
|
scroll = ttk.Scrollbar(f1, command=fw_label.yview)
|
||
|
fw_label.configure(yscrollcommand=scroll.set)
|
||
|
fw_label.insert(END, (query))
|
||
|
fw_label.pack(side=LEFT, expand=YES, fill=BOTH, padx=2, pady=2)
|
||
|
scroll.pack(side=RIGHT, expand=NO, fill=BOTH, padx=2, pady=2)
|
||
|
|
||
|
# keep tab reference by tk id
|
||
|
working_tab_id = "." + fw_welcome._name
|
||
|
|
||
|
# keep tab reference to script (by tk id)
|
||
|
self.fw_labels[working_tab_id] = fw_label
|
||
|
|
||
|
# new "Results" Container
|
||
|
fr = ttk.Labelframe(fw_welcome, text='Results', width=200, height=100)
|
||
|
fw_welcome.add(fr)
|
||
|
|
||
|
# containing a notebook
|
||
|
fw_result_nb = Notebook(fr, style="ButtonNotebook")
|
||
|
fw_result_nb.pack(fill='both', expand=True)
|
||
|
# resize rules
|
||
|
fw_welcome.columnconfigure(0, weight=1)
|
||
|
# keep reference to result_nb objects (by tk id)
|
||
|
self.fw_result_nbs[working_tab_id] = fw_result_nb
|
||
|
|
||
|
# activate this tab print(self.notebook.tabs())
|
||
|
self.notebook.select(working_tab_id)
|
||
|
# workaround to have a visible result pane on initial launch
|
||
|
self.add_treeview(
|
||
|
working_tab_id, "_", "", "click on ('->') to run Script")
|
||
|
return working_tab_id # gives back tk_id reference of the new tab
|
||
|
|
||
|
def del_tab(self):
|
||
|
"""delete active notebook tab's results"""
|
||
|
given_tk_id = self.notebook.select()
|
||
|
if given_tk_id != '':
|
||
|
self.notebook.forget(given_tk_id)
|
||
|
|
||
|
def remove_treeviews(self, given_tk_id):
|
||
|
"""remove results from given tab tk_id"""
|
||
|
if given_tk_id != '':
|
||
|
myz = self.fw_result_nbs[given_tk_id]
|
||
|
for xx in list(myz.children.values()):
|
||
|
xx.grid_forget()
|
||
|
xx.destroy()
|
||
|
|
||
|
def add_treeview(self, given_tk_id, columns, data, title="__", subt=""):
|
||
|
"""add a dataset result to the given tab tk_id"""
|
||
|
# ensure we work on lists
|
||
|
if isinstance(columns, (type('e'), type(u'e'))):
|
||
|
tree_columns = [columns]
|
||
|
else:
|
||
|
tree_columns = columns
|
||
|
lines = [data] if isinstance(data, (type('e'), type(u'e'))) else data
|
||
|
|
||
|
# get back reference to Notebooks of Results
|
||
|
# (see http://www.astro.washington.edu/users/rowen/TkinterSummary.html)
|
||
|
fw_result_nb = self.fw_result_nbs[given_tk_id]
|
||
|
|
||
|
# create a Labelframe to contain new resultset and scrollbars
|
||
|
f2 = ttk.Labelframe(
|
||
|
fw_result_nb, text=('(%s lines) %s' % (len(lines), subt)),
|
||
|
width=200, height=100)
|
||
|
f2.pack(fill='both', expand=True)
|
||
|
fw_result_nb.add(f2, text=title)
|
||
|
|
||
|
# ttk.Style().configure('TLabelframe.label', font=("Arial",14, "bold"))
|
||
|
# lines=queries
|
||
|
fw_Box = Treeview(f2, columns=tree_columns, show="headings",
|
||
|
padding=(2, 2, 2, 2))
|
||
|
fw_vsb = Scrollbar(f2, orient="vertical", command=fw_Box.yview)
|
||
|
fw_hsb = Scrollbar(f2, orient="horizontal", command=fw_Box.xview)
|
||
|
fw_Box.configure(yscrollcommand=fw_vsb.set, xscrollcommand=fw_hsb.set)
|
||
|
fw_Box.grid(column=0, row=0, sticky='nsew', in_=f2)
|
||
|
fw_vsb.grid(column=1, row=0, sticky='ns', in_=f2)
|
||
|
fw_hsb.grid(column=0, row=2, sticky='ew', in_=f2)
|
||
|
|
||
|
# this new Treeview may occupy all variable space
|
||
|
f2.grid_columnconfigure(0, weight=1)
|
||
|
f2.grid_rowconfigure(0, weight=1)
|
||
|
|
||
|
# feed Treeview Header
|
||
|
for col in tuple(tree_columns):
|
||
|
fw_Box.heading(
|
||
|
col, text=col.title(),
|
||
|
command=lambda c=col: self.sortby(fw_Box, c, 0))
|
||
|
fw_Box.column(col, width=font.Font().measure(col.title()))
|
||
|
|
||
|
def flat(x):
|
||
|
"""replace line_return by space, if given a string"""
|
||
|
if isinstance(x, (type('e'), type(u'e'))):
|
||
|
return x.replace("\n", " ")
|
||
|
return x
|
||
|
|
||
|
# feed Treeview Lines
|
||
|
for items in lines:
|
||
|
# if line is a string, redo a tuple
|
||
|
item = (items,) if isinstance(items,
|
||
|
(type('e'), type(u'e'))) else items
|
||
|
|
||
|
# replace line_return by space (grid don't like line_returns)
|
||
|
line_cells = tuple(flat(item[c]) for c in range(len(tree_columns)))
|
||
|
# insert the line of data
|
||
|
fw_Box.insert('', 'end', values=line_cells)
|
||
|
# adjust columns length if necessary and possible
|
||
|
for indx, val in enumerate(line_cells):
|
||
|
try:
|
||
|
ilen = font.Font().measure(val)
|
||
|
if fw_Box.column(tree_columns[indx],
|
||
|
width=None) < ilen and ilen < 400:
|
||
|
fw_Box.column(tree_columns[indx], width=ilen)
|
||
|
except:
|
||
|
pass
|
||
|
|
||
|
def sortby(self, tree, col, descending):
|
||
|
"""Sort a ttk treeview contents when a column is clicked on."""
|
||
|
# grab values to sort
|
||
|
data = [(tree.set(child, col), child) for child in tree.get_children()]
|
||
|
|
||
|
# reorder data
|
||
|
data.sort(reverse=descending)
|
||
|
for indx, item in enumerate(data):
|
||
|
tree.move(item[1], '', indx)
|
||
|
|
||
|
# switch the heading so that it will sort in the opposite direction
|
||
|
tree.heading(col, command=lambda col=col:
|
||
|
self.sortby(tree, col, int(not descending)))
|
||
|
|
||
|
|
||
|
class guess_csv():
|
||
|
"""guess everything about a csv file of data to import in SQL"""
|
||
|
def __init__(self, csv_file):
|
||
|
self.has_header = True
|
||
|
self.default_sep = ","
|
||
|
self.default_quote = '"'
|
||
|
self.encodings = guess_encoding(csv_file)
|
||
|
self.table_name = os.path.basename(csv_file).split(".")[0]
|
||
|
with io.open(csv_file, encoding=self.encodings[0]) as f:
|
||
|
self.preview = f.read(9999)
|
||
|
try:
|
||
|
dialect = csv.Sniffer().sniff(self.preview)
|
||
|
self.has_header = csv.Sniffer().has_header(self.preview)
|
||
|
self.default_sep = dialect.delimiter
|
||
|
self.default_quote = Dialect.quotechar
|
||
|
except:
|
||
|
pass # sniffer can fail
|
||
|
self.default_decims = [".", ","]
|
||
|
if self.default_sep == ";":
|
||
|
self.default_decims = [",", "."]
|
||
|
self.dlines = "\n\n".join(self.preview.splitlines()[:3])
|
||
|
|
||
|
|
||
|
def guess_sql_creation(table_name, separ, decim, header, data, quoter='"'):
|
||
|
"""guess the sql creation request for the table who will receive data"""
|
||
|
try:
|
||
|
dlines = list(csv.reader(data.replace('\n\n', '\n').splitlines(),
|
||
|
delimiter=separ, quotechar=quoter))
|
||
|
except: # minimal hack for python2.7
|
||
|
dlines = list(csv.reader(data.replace('\n\n', '\n').splitlines(),
|
||
|
delimiter=str(separ), quotechar=str(quoter)))
|
||
|
r, val = list(dlines[0]), list(dlines[1])
|
||
|
typ = ['TEXT']*len(r) # default value is TEXT
|
||
|
for i in range(len(r)):
|
||
|
try:
|
||
|
float(val[i].replace(decim, '.')) # unless it can be a real
|
||
|
typ[i] = 'REAL'
|
||
|
except:
|
||
|
checker = sqlite.connect(':memory:')
|
||
|
# avoid the false positive 'now'
|
||
|
val_not_now = val[i].replace('w', 'www').replace('W', 'WWW')
|
||
|
test = "select datetime('{0}')".format(val_not_now)
|
||
|
try:
|
||
|
if checker.execute(test).fetchall()[0][0]:
|
||
|
typ[i] = 'DATETIME' # and unless SQLite can see a DATETIME
|
||
|
except:
|
||
|
pass
|
||
|
checker.close
|
||
|
if header:
|
||
|
head = ",\n".join([('"%s" %s' % (r[i], typ[i]))
|
||
|
for i in range(len(r))])
|
||
|
sql_crea = ('CREATE TABLE "%s" (%s);' % (table_name, head))
|
||
|
else:
|
||
|
head = ",".join(["c_" + ("000" + str(i))[-3:] for i in range(len(r))])
|
||
|
sql_crea = ('CREATE TABLE "%s" (%s);' % (table_name, head))
|
||
|
return sql_crea, typ, head
|
||
|
|
||
|
|
||
|
def guess_encoding(csv_file):
|
||
|
"""guess the encoding of the given file"""
|
||
|
with io.open(csv_file, "rb") as f:
|
||
|
data = f.read(5)
|
||
|
if data.startswith(b"\xEF\xBB\xBF"): # UTF-8 with a "BOM"
|
||
|
return ["utf-8-sig"]
|
||
|
elif data.startswith(b"\xFF\xFE") or data.startswith(b"\xFE\xFF"):
|
||
|
return ["utf-16"]
|
||
|
else: # in Windows, guessing utf-8 doesn't work, so we have to try
|
||
|
try:
|
||
|
with io.open(csv_file, encoding="utf-8") as f:
|
||
|
preview = f.read(222222)
|
||
|
return ["utf-8"]
|
||
|
except:
|
||
|
return [locale.getdefaultlocale()[1], "utf-8"]
|
||
|
|
||
|
|
||
|
def create_dialog(title, fields_in, buttons, actions):
|
||
|
"""create a formular with title, fields, button, data"""
|
||
|
# drawing the request form
|
||
|
top = Toplevel()
|
||
|
top.title(title)
|
||
|
top.columnconfigure(0, weight=1)
|
||
|
top.rowconfigure(0, weight=1)
|
||
|
# drawing global frame
|
||
|
content = ttk.Frame(top)
|
||
|
content.grid(column=0, row=0, sticky=(N, S, E, W))
|
||
|
content.columnconfigure(0, weight=1)
|
||
|
# fields = Horizontal FrameLabel, or
|
||
|
# label, default_value, 'r' or 'w' default_width,default_height
|
||
|
fields = fields_in
|
||
|
mf_col = -1
|
||
|
for f in range(len(fields)): # same structure out
|
||
|
field = fields[f]
|
||
|
if isinstance(field, (type('e'), type(u'e'))) or mf_col == -1:
|
||
|
# a new horizontal frame
|
||
|
mf_col += 1
|
||
|
ta_col = -1
|
||
|
if isinstance(field, (type('e'), type(u'e'))) and field == '':
|
||
|
mf_frame = ttk.Frame(content, borderwidth=1)
|
||
|
else:
|
||
|
mf_frame = ttk.LabelFrame(content, borderwidth=1, text=field)
|
||
|
mf_frame.grid(column=0, row=mf_col, sticky='nsew')
|
||
|
Grid.rowconfigure(mf_frame, 0, weight=1)
|
||
|
content.rowconfigure(mf_col, weight=1)
|
||
|
if not isinstance(field, (type('e'), type(u'e'))):
|
||
|
# a new vertical frame
|
||
|
ta_col += 1
|
||
|
Grid.columnconfigure(mf_frame, ta_col, weight=1)
|
||
|
packing_frame = ttk.Frame(mf_frame, borderwidth=1)
|
||
|
packing_frame.grid(column=ta_col, row=0, sticky='nsew')
|
||
|
Grid.columnconfigure(packing_frame, 0, weight=1)
|
||
|
# prepare width and height and writable status
|
||
|
width = field[3] if len(field) > 3 else 30
|
||
|
height = field[4] if len(field) > 4 else 30
|
||
|
status = "normal"
|
||
|
if len(field) >= 3 and field[2] == "r":
|
||
|
status = "disabled"
|
||
|
# switch between object types
|
||
|
if len(field) > 4:
|
||
|
# datas
|
||
|
d_frame = ttk.LabelFrame(packing_frame, borderwidth=5,
|
||
|
width=width, height=height,
|
||
|
text=field[0])
|
||
|
d_frame.grid(column=0, row=0, sticky='nsew', pady=1, padx=1)
|
||
|
Grid.rowconfigure(packing_frame, 0, weight=1)
|
||
|
fw_label = Text(d_frame, bd=1, width=width, height=height,
|
||
|
undo=True)
|
||
|
fw_label.pack(side=LEFT, expand=YES, fill=BOTH)
|
||
|
scroll = ttk.Scrollbar(d_frame, command=fw_label.yview)
|
||
|
scroll.pack(side=RIGHT, expand=NO, fill=Y)
|
||
|
fw_label.configure(yscrollcommand=scroll.set)
|
||
|
fw_label.insert(END, ("%s" % field[1]))
|
||
|
fw_label.configure(state=status)
|
||
|
Grid.rowconfigure(d_frame, 0, weight=1)
|
||
|
Grid.columnconfigure(d_frame, 0, weight=1)
|
||
|
# Data Text Extractor in the fields list ()
|
||
|
# see stackoverflow.com/questions/17677649 (loop and lambda)
|
||
|
fields[f][1] = lambda x=fw_label: x.get('1.0', 'end')
|
||
|
elif isinstance(field[1], type(True)):
|
||
|
# boolean Field
|
||
|
name_var = BooleanVar()
|
||
|
name = ttk.Checkbutton(packing_frame, text=field[0],
|
||
|
variable=name_var, onvalue=True,
|
||
|
state=status)
|
||
|
name_var.set(field[1])
|
||
|
name.grid(column=0, row=0, sticky='nsew', pady=5, padx=5)
|
||
|
fields[f][1] = name_var.get
|
||
|
else: # Text or Combo
|
||
|
namelbl = ttk.Label(packing_frame, text=field[0])
|
||
|
namelbl.grid(column=0, row=0, sticky='nsw', pady=5, padx=5)
|
||
|
name_var = StringVar()
|
||
|
if not isinstance(field[1], (list, tuple)):
|
||
|
name = ttk.Entry(packing_frame, textvariable=name_var,
|
||
|
width=width, state=status)
|
||
|
name_var.set(field[1])
|
||
|
else:
|
||
|
name = ttk.Combobox(packing_frame, textvariable=name_var,
|
||
|
state=status)
|
||
|
name['values'] = list(field[1])
|
||
|
name.current(0)
|
||
|
name.grid(column=1, row=0, sticky='nsw', pady=0, padx=10)
|
||
|
fields[f][1] = name_var.get
|
||
|
# adding button below the same way
|
||
|
mf_col += 1
|
||
|
packing_frame = ttk.LabelFrame(content, borderwidth=5)
|
||
|
packing_frame.grid(column=0, row=mf_col, sticky='nsew')
|
||
|
okbutton = ttk.Button(
|
||
|
packing_frame, text=buttons[0],
|
||
|
command=lambda a=top, b=fields, c=actions: (buttons[1])(a, b, c))
|
||
|
cancelbutton = ttk.Button(packing_frame, text="Cancel",
|
||
|
command=top.destroy)
|
||
|
okbutton.grid(column=0, row=mf_col)
|
||
|
cancelbutton.grid(column=1, row=mf_col)
|
||
|
for x in range(3):
|
||
|
Grid.columnconfigure(packing_frame, x, weight=1)
|
||
|
top.grab_set()
|
||
|
|
||
|
|
||
|
def import_csvtb_ok(thetop, entries, actions):
|
||
|
"""read input values from tk formular"""
|
||
|
conn, actualize_db = actions
|
||
|
# build dico of result
|
||
|
d = {f[0]: f[1]() for f in entries
|
||
|
if not isinstance(f, (type('e'), type(u'e')))}
|
||
|
# affect to variables
|
||
|
csv_file = d['csv Name'].strip()
|
||
|
table_name = d['table Name'].strip()
|
||
|
separ = d['column separator']
|
||
|
decim = d['Decimal separator']
|
||
|
quotechar = d['string delimiter']
|
||
|
# action
|
||
|
if csv_file != "(none)" and len(csv_file)*len(table_name)*len(separ) > 1:
|
||
|
thetop.destroy()
|
||
|
# do initialization job
|
||
|
sql, typ, head = guess_sql_creation(table_name, separ, decim,
|
||
|
d['Header line'],
|
||
|
d["first 3 lines"], quotechar)
|
||
|
if d['use manual creation request']:
|
||
|
sql = ('CREATE TABLE "%s" (%s);' %
|
||
|
(table_name, d["creation request"]))
|
||
|
|
||
|
# Create csv reader function and give it to insert
|
||
|
reading = read_this_csv(csv_file, d['Encoding'], separ,
|
||
|
quotechar, d['Header line'], decim)
|
||
|
|
||
|
conn.insert_reader(reading, table_name, sql,
|
||
|
create_table=d['Create table'],
|
||
|
replace=d['Replace existing data'])
|
||
|
# refresh
|
||
|
actualize_db()
|
||
|
|
||
|
|
||
|
def read_this_csv(csv_file, encoding, delimiter , quotechar, header, decim):
|
||
|
"""yield csv data records from a file """
|
||
|
# handle Python 2/3
|
||
|
try:
|
||
|
reader = csv.reader(open(csv_file, 'r', encoding=encoding),
|
||
|
delimiter=delimiter, quotechar=quotechar)
|
||
|
except: # minimal hack for 2.7
|
||
|
reader = csv.reader(open(csv_file, 'r'),
|
||
|
delimiter=str(delimiter), quotechar=str(quotechar))
|
||
|
# handle header
|
||
|
if header:
|
||
|
next(reader)
|
||
|
# otherwise handle special decimal treatment
|
||
|
for row in reader:
|
||
|
if decim != "." and not isinstance(row, (type('e'), type(u'e'))):
|
||
|
for i in range(len(row)):
|
||
|
row[i] = row[i].replace(decim, ".")
|
||
|
yield(row)
|
||
|
|
||
|
|
||
|
def export_csv_ok(thetop, entries, actions):
|
||
|
"export a csv table (action)"
|
||
|
conn = actions[0]
|
||
|
# build dico of result
|
||
|
d = {f[0]: f[1]() for f in entries
|
||
|
if not isinstance(f, (type('e'), type(u'e')))}
|
||
|
|
||
|
csv_file = d['csv Name'].strip()
|
||
|
conn.export_writer(d["Data to export (MUST be 1 Request)"], csv_file,
|
||
|
header=d['Header line'],
|
||
|
delimiter=d['column separator'],
|
||
|
encoding=d['Encoding'],
|
||
|
quotechar='"')
|
||
|
|
||
|
|
||
|
def get_leaves(conn, category, attached_db="", tbl=""):
|
||
|
"""returns a list of 'category' objects in attached_db
|
||
|
[objectCode, objectLabel, Definition, 'sub-level']
|
||
|
"""
|
||
|
# create formatting shortcuts
|
||
|
def f(t): return ('"%s"' % t.replace('"', '""')) if t != "" else t
|
||
|
|
||
|
def d(t): return ('%s.' % t) if t != "" else t
|
||
|
|
||
|
# Initialize datas
|
||
|
Tables, db, tb = [], d(attached_db), f(tbl)
|
||
|
master = "sqlite_master" if db != "temp." else "sqlite_temp_master"
|
||
|
|
||
|
if category == "pydef": # pydef request is not sql, answer is direct
|
||
|
Tables = [[k, k, v['pydef'], ''] for k, v in conn.conn_def.items()]
|
||
|
elif category == 'attached_databases':
|
||
|
# get all attached database, but not the first one ('main')
|
||
|
resu = list((conn.execute("PRAGMA database_list").fetchall()))[1:]
|
||
|
for c in resu:
|
||
|
instruct = "ATTACH DATABASE %s as %s" % (f(c[2]), f(c[1]))
|
||
|
Tables.append([c[0], c[1], instruct, ''])
|
||
|
elif category == 'fields':
|
||
|
resu = conn.execute("PRAGMA %sTABLE_INFO(%s)" % (db, tb)).fetchall()
|
||
|
Tables = [[c[1], c[1], c[2], ''] for c in resu]
|
||
|
elif category in ('index', 'trigger', 'master_table', 'table', 'view'):
|
||
|
# others are 1 sql request that generates directly Tables
|
||
|
if category in ('index', 'trigger'):
|
||
|
sql = """SELECT '{0}' || name, name, coalesce(sql,'--auto') , ''
|
||
|
FROM {0}{3} WHERE type='{1}' ORDER BY name"""
|
||
|
elif category == 'master_table':
|
||
|
sql = """SELECT '{0}{3}', '{3}', '--auto', 'fields'
|
||
|
UNION SELECT '{0}'||name, name, sql, 'fields'
|
||
|
FROM {0}{3}
|
||
|
WHERE type='table' AND name LIKE 'sqlite_%' ORDER BY name"""
|
||
|
elif category in ('table', 'view'):
|
||
|
sql = """SELECT '{0}' || name, name, sql , 'fields'
|
||
|
FROM {0}{3} WHERE type = '{1}' AND NOT
|
||
|
(type='table' AND name LIKE 'sqlite_%') ORDER BY name"""
|
||
|
Tables = list(conn.execute(sql.format(db, category, tbl,
|
||
|
master)).fetchall())
|
||
|
return Tables
|
||
|
|
||
|
|
||
|
class Baresql():
|
||
|
"""a small wrapper around sqlite3 module"""
|
||
|
def __init__(self, connection="", keep_log=False, cte_inline=True,
|
||
|
isolation_level=None):
|
||
|
self.dbname = connection.replace(":///", "://").replace(
|
||
|
"sqlite://", "")
|
||
|
self.conn = sqlite.connect(self.dbname,
|
||
|
detect_types=sqlite.PARSE_DECLTYPES)
|
||
|
# pydef and logging infrastructure
|
||
|
self.conn_def = {}
|
||
|
self.do_log = keep_log
|
||
|
self.log = []
|
||
|
self.conn.isolation_level = isolation_level # commit experience
|
||
|
|
||
|
def close(self):
|
||
|
"""close database and clear dictionnary of registered 'pydef'"""
|
||
|
self.conn.close
|
||
|
self.conn_def = {}
|
||
|
|
||
|
def iterdump(self):
|
||
|
"""dump the database (add tweaks over the default dump)"""
|
||
|
# force detection of utf-8 by placing an only utf-8 comment at top
|
||
|
yield("/*utf-8 tag : 你好 мир Artisou à croute*/\n")
|
||
|
# add the Python functions pydef
|
||
|
for k in self.conn_def.values():
|
||
|
yield(k['pydef'] + ";\n")
|
||
|
# disable Foreign Constraints at Load
|
||
|
yield("PRAGMA foreign_keys = OFF; /*if SQlite */;")
|
||
|
yield("\n/* SET foreign_key_checks = 0;/*if Mysql*/;")
|
||
|
# how to parametrize Mysql to SQL92 standard
|
||
|
yield("/* SET sql_mode = 'PIPES_AS_CONCAT';/*if Mysql*/;")
|
||
|
yield("/* SET SQL_MODE = ANSI_QUOTES; /*if Mysql*/;\n")
|
||
|
# now the standard dump (notice it uses BEGIN TRANSACTION)
|
||
|
for line in self.conn.iterdump():
|
||
|
yield(line)
|
||
|
# re-instantiate Foreign_keys = True
|
||
|
for row in self.conn.execute("PRAGMA foreign_keys"):
|
||
|
flag = 'ON' if row[0] == 1 else 'OFF'
|
||
|
yield("PRAGMA foreign_keys = %s;/*if SQlite*/;" % flag)
|
||
|
yield("PRAGMA foreign_keys = %s;/*if SQlite bug*/;" % flag)
|
||
|
yield("PRAGMA foreign_key_check;/*if SQLite, check*/;")
|
||
|
yield("\n/*SET foreign_key_checks = %s;/*if Mysql*/;\n" % row[0])
|
||
|
|
||
|
def execute(self, sql, env=None):
|
||
|
"""execute sql but intercept log"""
|
||
|
if self.do_log:
|
||
|
self.log.append(sql)
|
||
|
return self.conn.execute(sql)
|
||
|
|
||
|
def createpydef(self, sql):
|
||
|
"""generates and register a pydef instruction"""
|
||
|
instruction = sql.strip('; \t\n\r')
|
||
|
# create Python function in Python
|
||
|
exec(instruction[2:], globals(), locals())
|
||
|
# add Python function in SQLite
|
||
|
firstline = (instruction[5:].splitlines()[0]).lstrip()
|
||
|
firstline = firstline.replace(" ", "") + "("
|
||
|
instr_name = firstline.split("(", 1)[0].strip()
|
||
|
instr_parms = firstline.count(',')+1
|
||
|
instr_add = (("self.conn.create_function('%s', %s, %s)" % (
|
||
|
instr_name, instr_parms, instr_name)))
|
||
|
exec(instr_add, globals(), locals())
|
||
|
# housekeeping definition of pydef in a dictionnary
|
||
|
the_help = dict(globals(), **locals())[instr_name].__doc__
|
||
|
self.conn_def[instr_name] = {
|
||
|
'parameters': instr_parms, 'inst': instr_add,
|
||
|
'help': the_help, 'pydef': instruction}
|
||
|
return instr_name
|
||
|
|
||
|
def get_tokens(self, sql, start=0, shell_tokens=False):
|
||
|
"""
|
||
|
from given sql start position, yield tokens (value + token type)
|
||
|
if shell_tokens is True, identify line shell_tokens as sqlite.exe does
|
||
|
"""
|
||
|
length = len(sql)
|
||
|
i = start
|
||
|
can_be_shell_command = True
|
||
|
dico = {' ': 'TK_SP', '\t': 'TK_SP', '\n': 'TK_SP', '\f': 'TK_SP',
|
||
|
'\r': 'TK_SP', '(': 'TK_LP', ')': 'TK_RP', ';': 'TK_SEMI',
|
||
|
',': 'TK_COMMA', '/': 'TK_OTHER', "'": 'TK_STRING',
|
||
|
"-": 'TK_OTHER', '"': 'TK_STRING', "`": 'TK_STRING'}
|
||
|
while length > start:
|
||
|
token = 'TK_OTHER'
|
||
|
if shell_tokens and can_be_shell_command and i < length and (
|
||
|
(sql[i] == "." and i == start) or
|
||
|
(i > start and sql[i-1:i] == "\n.")):
|
||
|
# a command line shell ! (supposed on one starting line)
|
||
|
token = 'TK_SHELL'
|
||
|
i = sql.find("\n", start)
|
||
|
if i <= 0:
|
||
|
i = length
|
||
|
elif sql[i] == "-" and i < length and sql[i:i+2] == "--":
|
||
|
# this Token is an end-of-line comment : --blabla
|
||
|
token = 'TK_COM'
|
||
|
i = sql.find("\n", start)
|
||
|
if i <= 0:
|
||
|
i = length
|
||
|
elif sql[i] == "/" and i < length and sql[i:i+2] == "/*":
|
||
|
# this Token is a comment block : /* and bla bla \n bla */
|
||
|
token = 'TK_COM'
|
||
|
i = sql.find("*/", start) + 2
|
||
|
if i <= 1:
|
||
|
i = length
|
||
|
elif sql[i] not in dico:
|
||
|
# this token is a distinct word (tagged as 'TK_OTHER')
|
||
|
while i < length and sql[i] not in dico:
|
||
|
i += 1
|
||
|
else:
|
||
|
# default token analyze case
|
||
|
token = dico[sql[i]]
|
||
|
if token == 'TK_SP':
|
||
|
# find the end of the 'Spaces' Token just detected
|
||
|
while (i < length and sql[i] in dico and
|
||
|
dico[sql[i]] == 'TK_SP'):
|
||
|
i += 1
|
||
|
elif token == 'TK_STRING':
|
||
|
# find the end of the 'String' Token just detected
|
||
|
delimiter = sql[i]
|
||
|
if delimiter != "'":
|
||
|
token = 'TK_ID' # usefull nuance ?
|
||
|
while(i < length):
|
||
|
i = sql.find(delimiter, i+1)
|
||
|
if i <= 0: # String is never closed
|
||
|
i = length
|
||
|
token = 'TK_ERROR'
|
||
|
elif i < length - 1 and sql[i+1] == delimiter:
|
||
|
i += 1 # double '' case, so ignore and continue
|
||
|
else:
|
||
|
i += 1
|
||
|
break # normal End of a String
|
||
|
else:
|
||
|
if i < length:
|
||
|
i += 1
|
||
|
yield sql[start:i], token
|
||
|
if token == 'TK_SEMI': # a new sql order can be a new shell token
|
||
|
can_be_shell_command = True
|
||
|
elif token not in ('TK_COM', 'TK_SP'): # can't be a shell token
|
||
|
can_be_shell_command = False
|
||
|
start = i
|
||
|
|
||
|
def get_sqlsplit(self, sql, remove_comments=False):
|
||
|
"""yield a list of separated sql orders from a sql file"""
|
||
|
trigger_mode = False
|
||
|
mysql = [""]
|
||
|
for tokv, token in self.get_tokens(sql, shell_tokens=True):
|
||
|
# clear comments option
|
||
|
if token != 'TK_COM' or not remove_comments:
|
||
|
mysql.append(tokv)
|
||
|
# special case for Trigger : semicolumn don't count
|
||
|
if token == 'TK_OTHER':
|
||
|
tok = tokv.upper()
|
||
|
if tok == "TRIGGER":
|
||
|
trigger_mode = True
|
||
|
translvl = 0
|
||
|
elif trigger_mode and tok in('BEGIN', 'CASE'):
|
||
|
translvl += 1
|
||
|
elif trigger_mode and tok == 'END':
|
||
|
translvl -= 1
|
||
|
if translvl <= 0:
|
||
|
trigger_mode = False
|
||
|
elif (token == 'TK_SEMI' and not trigger_mode):
|
||
|
# end of a single sql
|
||
|
yield "".join(mysql)
|
||
|
mysql = []
|
||
|
elif (token == 'TK_SHELL'):
|
||
|
# end of a shell order
|
||
|
yield("" + tokv)
|
||
|
mysql = []
|
||
|
if mysql != []:
|
||
|
yield("".join(mysql))
|
||
|
|
||
|
def insert_reader(self, reader, table_name, create_sql=None,
|
||
|
create_table=True, replace=True, header=False):
|
||
|
"""import a given csv reader into a given table"""
|
||
|
curs = self.conn.cursor()
|
||
|
# 1-do initialization job
|
||
|
# speed-up dead otherwise dead slow speed if not memory database
|
||
|
try:
|
||
|
curs.execute('begin transaction')
|
||
|
except:
|
||
|
pass
|
||
|
# check if table exists
|
||
|
here = curs.execute('PRAGMA table_info("%s")' % table_name).fetchall()
|
||
|
if create_sql and (create_table or len(here) == 0):
|
||
|
curs.execute('drop TABLE if exists "%s";' % table_name)
|
||
|
curs.execute(create_sql)
|
||
|
if replace:
|
||
|
curs.execute('delete from "%s";' % table_name)
|
||
|
# count rows of target table
|
||
|
nbcol = len(curs.execute('pragma table_info("%s")' % table_name
|
||
|
).fetchall())
|
||
|
sql = 'INSERT INTO "%s" VALUES(%s);' % (table_name,
|
||
|
", ".join(["?"]*nbcol))
|
||
|
# read first_line if hasked to skip headers
|
||
|
if header:
|
||
|
next(reader)
|
||
|
# 2-push records
|
||
|
curs.executemany(sql, reader)
|
||
|
self.conn.commit()
|
||
|
|
||
|
def export_writer(self, sql, csv_file, header=True,
|
||
|
delimiter=',', encoding='utf-8', quotechar='"'):
|
||
|
"""export a csv table (action)"""
|
||
|
cursor = self.conn.cursor()
|
||
|
cursor.execute(sql)
|
||
|
if sys.version_info[0] != 2: # python3
|
||
|
fout = io.open(csv_file, 'w', newline='', encoding=encoding)
|
||
|
writer = csv.writer(fout, delimiter=delimiter,
|
||
|
quotechar='"', quoting=csv.QUOTE_MINIMAL)
|
||
|
else: # python2.7 (minimal)
|
||
|
fout = io.open(csv_file, 'wb')
|
||
|
writer = csv.writer(fout, delimiter=str(delimiter),
|
||
|
quotechar=str('"'), quoting=csv.QUOTE_MINIMAL)
|
||
|
if header:
|
||
|
writer.writerow([i[0] for i in cursor.description]) # heading row
|
||
|
writer.writerows(cursor.fetchall())
|
||
|
fout.close
|
||
|
|
||
|
|
||
|
def _main():
|
||
|
app = App()
|
||
|
# start with a memory Database and a welcome
|
||
|
app.new_db(":memory:")
|
||
|
welcome_text = """-- SQLite Memo (Demo = click on green "->" and "@" icons)
|
||
|
\n-- to CREATE a table 'items' and a table 'parts' :
|
||
|
DROP TABLE IF EXISTS item; DROP TABLE IF EXISTS part;
|
||
|
CREATE TABLE item (ItemNo, Description,Kg , PRIMARY KEY (ItemNo));
|
||
|
CREATE TABLE part(ParentNo, ChildNo , Description TEXT , Qty_per REAL);
|
||
|
\n-- to CREATE an index :
|
||
|
DROP INDEX IF EXISTS parts_id1;
|
||
|
CREATE INDEX parts_id1 ON part(ParentNo Asc, ChildNo Desc);
|
||
|
\n-- to CREATE a view 'v1':
|
||
|
DROP VIEW IF EXISTS v1;
|
||
|
CREATE VIEW v1 as select * from item inner join part as p ON ItemNo=p.ParentNo;
|
||
|
\n-- to INSERT datas
|
||
|
INSERT INTO item values("T","Ford",1000);
|
||
|
INSERT INTO item select "A","Merced",1250 union all select "W","Wheel",9 ;
|
||
|
INSERT INTO part select ItemNo,"W","needed",Kg/250 from item where Kg>250;
|
||
|
\n-- to CREATE a Python embedded function (enclose them by "py" and ";") :
|
||
|
pydef py_sin(s):
|
||
|
"sinus function : example loading module, handling input/output as strings"
|
||
|
import math as py_math
|
||
|
return ("%s" % py_math.sin(s*1));
|
||
|
pydef py_fib(n):
|
||
|
"fibonacci : example with function call (may only be internal) "
|
||
|
fib = lambda n: n if n < 2 else fib(n-1) + fib(n-2)
|
||
|
return("%s" % fib(n*1));
|
||
|
pydef py_power(x,y):
|
||
|
"power function : example loading module, handling input/output as strings"
|
||
|
import math as py_math
|
||
|
return ("%s" % ((x*1) ** (y*1)) );
|
||
|
\n-- to USE a python embedded function and nesting of embedded functions:
|
||
|
select py_sin(1) as sinus, py_power(2, 1*py_fib(6)) as power, sqlite_version();
|
||
|
\n-- to EXPORT :
|
||
|
-- a TABLE, select TABLE, then click on icon 'SQL->CSV'
|
||
|
-- a QUERY RESULT, select the SCRIPT text, then click on icon '???->CSV',
|
||
|
-- example : select the end of this line: SELECT SQLITE_VERSION()
|
||
|
\n\n-- to use COMMIT and ROLLBACK :
|
||
|
BEGIN TRANSACTION;
|
||
|
UPDATE item SET Kg = Kg + 1;
|
||
|
COMMIT;
|
||
|
BEGIN TRANSACTION;
|
||
|
UPDATE item SET Kg = 0;
|
||
|
select Kg, Description from Item;
|
||
|
ROLLBACK;
|
||
|
select Kg, Description from Item;
|
||
|
\n\n-- to use SAVEPOINT :
|
||
|
SAVEPOINT remember_Neo; -- create a savepoint
|
||
|
UPDATE item SET Description = 'Smith'; -- do things
|
||
|
SELECT ItemNo, Description FROM Item; -- see things done
|
||
|
ROLLBACK TO SAVEPOINT remember_Neo; -- go back to savepoint state
|
||
|
SELECT ItemNo, Description FROM Item; -- see all is back to normal
|
||
|
RELEASE SAVEPOINT remember_Neo; -- free memory
|
||
|
\n\n-- '.' commands understood:
|
||
|
-- .once FILENAME Output for the next SQL command only to FILENAME
|
||
|
-- .import FILE TABLE Import data from FILE into TABLE
|
||
|
-- (create table only if it doesn't exist, keep existing records)
|
||
|
.once 'this_file_of_result.txt'
|
||
|
select ItemNo, Description from item order by ItemNo desc;
|
||
|
.import 'this_file_of_result.txt' in_this_table
|
||
|
|
||
|
"""
|
||
|
app.n.new_query_tab("Welcome", welcome_text)
|
||
|
app.tk_win.mainloop()
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
_main() # create a tkk graphic interface with a main window tk_win
|