From 189f7668778d4b6355b8e825465e714b69d9f26e Mon Sep 17 00:00:00 2001 From: stonebig Date: Sun, 15 Jun 2014 08:54:19 +0200 Subject: [PATCH] pypi version 0.7.0 --- CHANGES.txt | 87 +++ README.md | 4 - README.rst | 57 ++ docs/sqlite_bro.GIF | Bin 0 -> 83319 bytes setup.cfg | 2 + setup.py | 54 ++ sqlite_bro.py | 1305 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1505 insertions(+), 4 deletions(-) create mode 100644 CHANGES.txt delete mode 100644 README.md create mode 100644 README.rst create mode 100644 docs/sqlite_bro.GIF create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 sqlite_bro.py diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100644 index 0000000..e921466 --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,87 @@ +Changelog +========= + + +2014-06-15 : +------------ + +* create a github project 'sqlite_bro', from 'sqlite_py_manager' baresql example + +* discover how to publish on Pypi + + + +2014-06-14c : "It's a long way to temporary !" +-------------------------------------------- + +* works with temporary tables + + + +2014-06-10a : 'Sanitizer of Python (xkcd.com/327)' +------------------------------------------------ + +* imported python functions must be validated + + + +2014-06-09a : 'The magic 8th PEP' +--------------------------------- + +* PEP8 alignement + + +2014-06-07a : 'Yield me a token' +-------------------------------- + +* the pythonic way to generate tokens is 'Yield' + + +2014-06-04a : 'Log me out !' +-------------------------- + +* export SQL + SQL top result in a file in 1 click + + +2014-06-01a 'Commit and Rollback' +--------------------------------- + +* support COMMIT and ROLLBACK + + +2014-06-03a : 'See me now ?' +-------------------------- + +* character INCREASE icon, so the back of the class can see + + +2014-05-25a : 'sql everywhere' +-------------------------- + +* make it work as low as Python 2.7 + SQlite 3.6.21 + + +2014-05-25a : 'Assassination of Class Room +------------------------------------------ + +* the GUI is a Class now + + +2014-05-11 +---------- + +* addition of Tooltips over icons + + + +2014-05-06 +---------- + +* addition of the Welcome Demo + + + +2014-05-01 +---------- + +* birth : need of a ZERO-requirements SQLite Browser for a Python Class diff --git a/README.md b/README.md deleted file mode 100644 index fda37e4..0000000 --- a/README.md +++ /dev/null @@ -1,4 +0,0 @@ -sqlite_bro -========== - -a graphic SQLite browser in 1 Python file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..780b038 --- /dev/null +++ b/README.rst @@ -0,0 +1,57 @@ +sqlite_bro : a graphic SQLite browser in 1 Python file +====================================================== + +sqlite_bro is a tool to browse SQLite databases with +any basic python installation. + + +Features +-------- + +* Tabular browsing of a SQLite database + +* Import/Export of .csv files with auto-detection + +* Import/Export of .sql script + +* Export of database creation .sql script + +* Support of sql-embedded Python functions + +* Easy to distribute : 1 Python source file, 2.7/3.3+ compatible + +* Easy to start : just launch sqlite_bro + +* Easy to learn : Welcome example, minimal interface + +* Easy to teach : Character size, SQL + SQL result export on a click + +Installation +------------ + +You can install, upgrade, uninstall sqlite_bro.py with these commands:: + + $ pip install sqlite_bro + $ pip install --upgrade sqlite_bro + $ pip uninstall sqlite_bro + +or just launch it from IPython with %load https://raw.githubusercontent.com/stonebig/sqlite_bro/master/sqlite_bro.py + +or just copy the file 'sqlite_bro.py' to any pc (with python installed) + +Example usage +------------- + +:: + + $ sqlite_bro + +Screenshot +---------- + +.. image:: https://raw.githubusercontent.com/stonebig/sqlite_bro/master/docs/sqlite_bro.GIF + +Links +----- + +* `Fork me on GitHub `_ \ No newline at end of file diff --git a/docs/sqlite_bro.GIF b/docs/sqlite_bro.GIF new file mode 100644 index 0000000000000000000000000000000000000000..97eca6d3970e964243ab91e3225bfbdc9bd8cb81 GIT binary patch literal 83319 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 literal 0 HcmV?d00001 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b754807 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] + description-file = README.rst \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ae8c7e0 --- /dev/null +++ b/setup.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +from __future__ import with_statement +from setuptools import setup + + +def get_version(): + with open('sqlite_bro.py') as f: + for line in f: + if line.strip().startswith('self.__version__'): + return eval(line.split('=')[-1]) + + +def get_long_description(): + descr = [] + for fname in 'README.rst', 'CHANGES.txt': + with open(fname) as f: + descr.append(f.read()) + return '\n\n'.join(descr) + + +setup( + name='sqlite_bro', + version=get_version(), + description="a graphic SQLite Client in 1 Python file", + long_description=get_long_description(), + keywords='sqlite', + author='stonebig', + author_email='', + url='https://github.com/stonebig/baresql/blob/master/examples', + license='MIT license', + py_modules=['sqlite_bro'], + namespace_packages=[], + include_package_data=True, + zip_safe=False, + install_requires=[ + # Nothing + ], + entry_points={ + 'console_scripts': [ + 'sqlite_bro = sqlite_bro:_main', + ], + }, + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Console', + 'Intended Audience :: Education', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.3', + 'Topic :: Scientific/Engineering :: Information Analysis', + ] +) \ No newline at end of file diff --git a/sqlite_bro.py b/sqlite_bro.py new file mode 100644 index 0000000..c30aaa7 --- /dev/null +++ b/sqlite_bro.py @@ -0,0 +1,1305 @@ +#!/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 + +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.7.0' + self._title= "2014-06-15a : my first Pypi !" + 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 + # With a Menubar and Toolbar + self.create_menu() + self.create_toolbar() + + # 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, []) + + 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='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"), + ('deltab_img', lambda x=self: x.n.del_tab(), "Delete current tab"), + ('deltabresult_img', lambda x=self: x.n.remove_treeviews( + x.n.notebook.select()), "Clear tab Result"), + ('newtab_img', lambda x=self: x.n.new_query_tab("___", ""), + "Create a New Tab"), + ('csvin_img', lambda x=self: import_csvtb([x.conn, x.actualize_db]), + "Import a CSV file into a Table"), + ('csvex_img', lambda x=self: export_csvtb([x.conn, x.db_tree]), + "Export Selected Table to a CSV file"), + ('dbdef_img', self.savdb_script, + "Save main Database as a SQL Script"), + ('qryex_img', lambda x=self: export_csvqr([x.conn, x.n]), + "Export Selected Query 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 new_db(self, filename=''): + """create a new database""" + if filename == '': + filename = filedialog.asksaveasfilename( + 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): + 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): + """open an existing database""" + filename = filedialog.askopenfilename( + defaultextension='.db', + filetypes=[("default", "*.db"), ("other", "*.db*"), + ("all", "*.*")]) + if 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( + defaultextension='.sql', + filetypes=[("default", "*.sql"), ("other", "*.txt"), + ("all", "*.*")]) + if filename != '': + text = ((filename.replace("\\", "/")).split("/")[-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( + defaultextension='.db', + title="save database structure in a text file", + filetypes=[("default", "*.sql"), ("other", "*.txt"), + ("all", "*.*")]) + if 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( + defaultextension='.db', + title="save script in a sql file", + filetypes=[("default", "*.sql"), ("other", "*.txt"), + ("all", "*.*")]) + if 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( + defaultextension='.db', + title="Choose a database to attach ", + filetypes=[("default", "*.db"), ("other", "*.db*"), + ("all", "*.*")]) + attach = ((filename.replace("\\", "/")).split("/")[-1]).split(".")[0] + + if 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""" + try: + self.db_tree.delete("Database") + except: + pass + self.conn.close + + 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 = (self.database_file.replace("\\", "/")).split("/")[-1] + 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): + """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( + defaultextension='.db', + title="execute Script + output in a log file", + filetypes=[("default", "*.txt"), ("other", "*.log"), + ("all", "*.*")]) + if filename == "": + return + 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= +''', + 'deltab_img': '''\ +R0lGODdhGAAYAKoAAP///56fnf9VAMzVzP9VMwAAAAAAAAAAACwAAAAAGAAYAAADZAi63P4wykmB +uO6KFnpX2raEnBeMGkgyZqsRoci2Znx9zKDjQGd7Dd2AFoiZgjuXEZhLomy9U3MojVk0PIUQt7re +kFSVTCxdbMsPLDgLYZ+JxDU8PuXN3c4JPqxHA84Ue2wPbAkAOw== +''', + 'deltabresult_img': '''\ +R0lGODdhGAAYAJkAAP///56fnf8AAAAAACwAAAAAGAAYAAACXIyPBsu9CYObQDFbqcM16cZFondJ +z0ie33aA5nqGL4y4cCnf8cytdanr5HS/k0CAMhxzRyQu0Gw9m8gDtdhpNBdbIW/G7e5sDqprGBak +x0CAmXGVqsTlpciOOhYAADs= +''', + '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== +''' + } + return {k: PhotoImage(data=v) for k, v in icons.items()} + + 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 + 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 = self.conn.get_sqlsplit(instruction, + remove_comments=True)[0] + 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 instruction != "": + try: + 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 + + 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 + + +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) # 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) + + 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) + 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))) + + +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, typ = list(dlines[0]), list(dlines[1]) + for i in range(len(r)): + try: + float(typ[i].replace(decim, '.')) + typ[i] = 'REAL' + except: + typ[i] = 'TEXT' + 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 import_csvtb(actions): + """import csv dialog (with guessing of encoding and separator)""" + csv_file = filedialog.askopenfilename( + defaultextension='.db', + title="Choose a csv fileto import ", + filetypes=[("default", "*.csv"), ("other", "*.txt"), ("all", "*.*")]) + # guess encoding + encodings = guess_encoding(csv_file) + # guess Header and delimiter + with io.open(csv_file, encoding=encodings[0]) as f: + preview = f.read(9999) + has_header = True + default_sep = "," + default_quote = '"' + try: + dialect = csv.Sniffer().sniff(preview) + has_header = csv.Sniffer().has_header(preview) + default_sep = dialect.delimiter + default_quote = Dialect.quotechar + except: + pass # sniffer can fail + default_decim = [".", ","] if default_sep != ";" else [",", "."] + + # Request form : List of Horizontal Frame names 'FramLabel' + # or fields : 'Label', 'InitialValue',['r' or 'w', Width, Height] + table_name = (csv_file.replace("\\", "/")).split("/")[-1].split(".")[0] + dlines = "\n\n".join(preview.splitlines()[:3]) + guess_sql = guess_sql_creation(table_name, default_sep, default_decim, + has_header, dlines, default_quote)[2] + fields_in = ['', ['csv Name', csv_file, 'r', 100], '', + ['table Name', table_name], + ['column separator', default_sep, 'w', 20], + ['string delimiter', default_quote, 'w', 20], + '', ['Decimal separator', default_decim], + ['Encoding', encodings], + 'Fliflaps', ['Header line', has_header], + ['Create table', True], + ['Replace existing data', True], '', + ['first 3 lines', dlines, 'r', 100, 10], '', + ['use manual creation request', False], '', + ['creation request', guess_sql, 'w', 100, 10]] + + create_dialog(("Importing %s" % csv_file), fields_in, + ("Import", import_csvtb_ok), actions) + + +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 export_csv_dialog(query="select 42", 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( + defaultextension='.db', title=text, + filetypes=[("default", "*.csv"), ("other", "*.txt"), ("all", "*.*")]) + if csv_file != "": + # Request form (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) + + +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) + 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() + curs = conn.conn.cursor() + # do initialization job + sql, typ, head = guess_sql_creation(table_name, separ, decim, + d['Header line'], + d["first 3 lines"], quotechar) + if d['Create table']: + curs.execute('drop TABLE if exists "%s";' % table_name) + if d['use manual creation request']: + sql = ('CREATE TABLE "%s" (%s);' % + (table_name, d["creation request"])) + curs.execute(sql) + if d['Replace existing data']: + curs.execute('delete from "%s";' % table_name) + sql = 'INSERT INTO "%s" VALUES(%s);' % (table_name, + ", ".join(["?"]*len(typ))) + + try: + reader = csv.reader(open(csv_file, 'r', encoding=d['Encoding']), + delimiter=separ, quotechar=quotechar) + except: # minimal hack for 2.7 + reader = csv.reader(open(csv_file, 'r'), + delimiter=str(separ), quotechar=str(quotechar)) + # read first_line if needed to skip headers + if d['Header line']: + row = next(reader) + if decim != ".": # one by one needed + for row in reader: + if not isinstance(row, (type('e'), type(u'e'))): + for i in range(len(row)): + row[i] = row[i].replace(decim, ".") + curs.execute(sql, row) + else: + curs.executemany(sql, reader) + conn.conn.commit() + actualize_db() + + +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() + cursor = conn.conn.cursor() + cursor.execute(d["Data to export (MUST be 1 Request)"]) + thetop.destroy() + if sys.version_info[0] != 2: # python3 + fout = io.open(csv_file, 'w', newline='', encoding=d['Encoding']) + writer = csv.writer(fout, delimiter=d['column separator'], + quotechar='"', quoting=csv.QUOTE_MINIMAL) + else: # python2.7 (minimal) + fout = io.open(csv_file, 'wb') + writer = csv.writer(fout, delimiter=str(d['column separator']), + quotechar=str('"'), quoting=csv.QUOTE_MINIMAL) + if d['Header line']: + writer.writerow([i[0] for i in cursor.description]) # heading row + writer.writerows(cursor.fetchall()) + fout.close + + +def export_csvtb(actions): + """get selected table definition and launch cvs export dialog""" + # determine selected table + db_tree = actions[1] + selitem = db_tree.focus() # get tree item having the focus + if selitem != '': + seltag = db_tree.item(selitem, "tag")[0] + if seltag == "run_up": # if 'run-up', do as if dbl-click 1 level up + selitem = db_tree.parent(selitem) + # get final information + definition, query = db_tree.item(selitem, "values") + if query != "": # run the export_csv dialog + title = ('Export Table "%s" to ?' % db_tree.item(selitem, "text")) + export_csv_dialog(query, title, actions) + + +def export_csvqr(actions): + """get tab selected definition and launch cvs export dialog""" + n = actions[1] + active_tab_id = n.notebook.select() + if active_tab_id != '': # get current selection (or all) + fw = n.fw_labels[active_tab_id] + try: + query = fw.get('sel.first', 'sel.last') + except: + query = fw.get(1.0, END)[:-1] + if query != "": + export_csv_dialog(query, "Export Query", actions) + + +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): + 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 = [] + + 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): + """from given sql start position, yield tokens (value + token type)""" + length = len(sql) + i = start + token = 'TK_OTHER' + 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: + if 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 + start = i + + def get_sqlsplit(self, sql, remove_comments=False): + """split an sql file in list of separated sql orders""" + trigger_mode = False + sqls = [] + mysql = [""] + for tokv, token in self.get_tokens(sql): + # 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 + sqls.append("".join(mysql)) + mysql = [] + if mysql != []: + sqls.append("".join(mysql)) + return sqls + +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' : +create table item (ItemNo, Description,Kg , PRIMARY KEY (ItemNo)); +create table part(ParentNo, ChildNo , Description TEXT , Qty_per REAL); +\n-- to CREATE an index : +CREATE INDEX parts_id1 ON part(ParentNo Asc, ChildNo Desc); +-- to CREATE a view '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)); +\n-- to USE a python embedded function : +select py_sin(1) as sinus_1, py_fib(8) as fib_8, 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() """ + 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