#!/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('', 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', '', self.t_doubleClicked) self.db_tree.tag_bind('run_up', '', 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("<>") 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", "", self.btn_press, True) self.tk_win.bind_class("TNotebook", "", self.btn_release) self.tk_win.bind_class("TNotebook", "", self.btn_Movex) self.tk_win.bind_class("TNotebook", "", 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) widget.bind("", 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