sqlite_bro.py 71 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. from __future__ import print_function, unicode_literals, division # Python2.7
  4. import sqlite3 as sqlite
  5. import sys
  6. import os
  7. import locale
  8. import csv
  9. import datetime
  10. import io
  11. import codecs
  12. import shlex # Simple lexical analysis
  13. try: # We are Python 3.3+
  14. from tkinter import *
  15. from tkinter import font, ttk, filedialog, messagebox
  16. from tkinter.ttk import *
  17. except: # or we are still Python2.7
  18. from Tkinter import *
  19. import Tkinter as tkinter
  20. import tkFont as font
  21. import tkFileDialog as filedialog
  22. import tkMessageBox as messagebox
  23. from ttk import *
  24. import ttk as ttk
  25. class App:
  26. """the GUI graphic application"""
  27. def __init__(self):
  28. """create a tkk graphic interface with a main window tk_win"""
  29. self.__version__ = '0.9.1'
  30. self._title = "2019-06-16a : 'Support un-named Tabs!'"
  31. self.conn = None # Baresql database object
  32. self.database_file = ""
  33. self.tk_win = Tk()
  34. self.tk_win.title('A graphic SQLite Client in 1 Python file')
  35. self.tk_win.option_add('*tearOff', FALSE) # hint of tk documentation
  36. self.tk_win.minsize(600, 200) # minimal size
  37. self.font_size = 10
  38. self.font_wheight = 0
  39. self.initialdir = "."
  40. # With a Menubar and Toolbar
  41. self.create_menu()
  42. self.create_toolbar()
  43. # Create style "ButtonNotebook"
  44. self.create_style()
  45. # Initiate Drag State
  46. self.state_drag = False
  47. self.state_drag_index = 0
  48. # With a Panedwindow of two frames: 'Database' and 'Queries'
  49. p = ttk.Panedwindow(self.tk_win, orient=HORIZONTAL)
  50. p.pack(fill=BOTH, expand=1)
  51. f_database = ttk.Labelframe(p, text='Databases', width=200, height=100)
  52. p.add(f_database)
  53. f_queries = ttk.Labelframe(p, text='Queries', width=200, height=100)
  54. p.add(f_queries)
  55. # build tree view 't' inside the left 'Database' Frame
  56. self.db_tree = ttk.Treeview(f_database, displaycolumns=[],
  57. columns=("detail", "action"))
  58. self.db_tree.tag_configure("run")
  59. self.db_tree.pack(fill=BOTH, expand=1)
  60. # create a notebook 'n' inside the right 'Queries' Frame
  61. self.n = NotebookForQueries(self.tk_win, f_queries, [])
  62. # Bind keyboard shortcuts
  63. self.tk_win.bind('<F9>', self.run_tab)
  64. def create_menu(self):
  65. """create the menu of the application"""
  66. menubar = Menu(self.tk_win)
  67. self.tk_win['menu'] = menubar
  68. # feeding the top level menu
  69. self.menu = Menu(menubar)
  70. menubar.add_cascade(menu=self.menu, label='Database')
  71. self.menu_help = Menu(menubar)
  72. menubar.add_cascade(menu=self.menu_help, label='?')
  73. # feeding database sub-menu
  74. self.menu.add_command(label='New Database', command=self.new_db)
  75. self.menu.add_command(label='New In-Memory Database',
  76. command=lambda: self.new_db(":memory:"))
  77. self.menu.add_command(label='Open Database ...',
  78. command=self.open_db)
  79. self.menu.add_command(label='Open Database ...(legacy auto-commit)',
  80. command=lambda: self.open_db(""))
  81. self.menu.add_command(label='Close Database', command=self.close_db)
  82. self.menu.add_separator()
  83. self.menu.add_command(label='Attach Database', command=self.attach_db)
  84. self.menu.add_separator()
  85. self.menu.add_command(label='Quit', command=self.quit_db)
  86. self.menu_help.add_command(label='about',
  87. command=lambda: messagebox.showinfo(message="""
  88. \nSQLite_bro : a graphic SQLite Client in 1 Python file
  89. \n("""+self.__version__+") " + self._title+"""
  90. \n(https://github.com/stonebig/sqlite_bro)"""))
  91. def create_toolbar(self):
  92. """create the toolbar of the application"""
  93. self.toolbar = Frame(self.tk_win, relief=RAISED)
  94. self.toolbar.pack(side=TOP, fill=X)
  95. self.tk_icon = self.get_tk_icons()
  96. # list of (image, action, tooltip) :
  97. to_show = [
  98. ('refresh_img', self.actualize_db, "Actualize databases"),
  99. ('run_img', self.run_tab, "Run script selection"),
  100. ('newtab_img', lambda x=self: x.n.new_query_tab("___", ""),
  101. "Create a new script"),
  102. ('csvin_img', self.import_csvtb, "Import a CSV file into a table"),
  103. ('csvex_img', self.export_csvtb,
  104. "Export selected table to a CSV file"),
  105. ('dbdef_img', self.savdb_script,
  106. "Save main database as a SQL script"),
  107. ('qryex_img', self.export_csvqr,
  108. "Export script selection to a CSV file"),
  109. ('exe_img', self.exsav_script,
  110. "Run script+output to a file (First 200 rec. per Qry)"),
  111. ('sqlin_img', self.load_script, "Load a SQL script file"),
  112. ('sqlsav_img', self.sav_script, "Save a SQL script in a file"),
  113. ('chgsz_img', self.chg_fontsize, "Modify font size")]
  114. for img, action, tip in to_show:
  115. b = Button(self.toolbar, image=self.tk_icon[img], command=action)
  116. b.pack(side=LEFT, padx=2, pady=2)
  117. self.createToolTip(b, tip)
  118. def set_initialdir(self, proposal):
  119. """change initial dir, if possible"""
  120. if os.path.isfile(proposal):
  121. self.initialdir = os.path.dirname(proposal)
  122. def new_db(self, filename=''):
  123. """create a new database"""
  124. if filename == '':
  125. filename = filedialog.asksaveasfilename(
  126. initialdir=self.initialdir, defaultextension='.db',
  127. title="Define a new database name and location",
  128. filetypes=[("default", "*.db"), ("other", "*.db*"),
  129. ("all", "*.*")])
  130. if filename != '':
  131. self.database_file = filename
  132. if os.path.isfile(filename):
  133. self.set_initialdir(filename)
  134. if messagebox.askyesno(
  135. message='Confirm Destruction of previous Datas ?',
  136. icon='question', title='Destroying'):
  137. os.remove(filename)
  138. self.conn = Baresql(self.database_file)
  139. self.actualize_db()
  140. def open_db(self, isolation_level=None):
  141. """open an existing database"""
  142. filename = filedialog.askopenfilename(
  143. initialdir=self.initialdir, defaultextension='.db',
  144. filetypes=[("default", "*.db"), ("other", "*.db*"),
  145. ("all", "*.*")])
  146. if filename != '':
  147. self.set_initialdir(filename)
  148. self.database_file = filename
  149. self.conn = Baresql(self.database_file)
  150. self.actualize_db()
  151. def load_script(self):
  152. """load a script file, ask validation of detected Python code"""
  153. filename = filedialog.askopenfilename(
  154. initialdir=self.initialdir, defaultextension='.sql',
  155. filetypes=[("default", "*.sql"), ("other", "*.txt"),
  156. ("all", "*.*")])
  157. if filename != '':
  158. self.set_initialdir(filename)
  159. text = os.path.split(filename)[1].split(".")[0]
  160. with io.open(filename, encoding=guess_encoding(filename)[0]) as f:
  161. script = f.read()
  162. sqls = self.conn.get_sqlsplit(script, remove_comments=True)
  163. dg = [s for s in sqls if s.strip(' \t\n\r')[:5] == "pydef"]
  164. if dg:
  165. fields = ['', ['In Script File:', filename, 'r', 100], '',
  166. ["Python Script", "".join(dg), 'r', 80, 20]]
  167. create_dialog(("Ok for this Python Code ?"), fields,
  168. ("Confirm", self.load_script_ok),
  169. [text, script])
  170. else:
  171. new_tab_ref = self.n.new_query_tab(text, script)
  172. def load_script_ok(self, thetop, entries, actions):
  173. """continue loading of script after confirmation dialog"""
  174. new_tab_ref = self.n.new_query_tab(*actions)
  175. thetop.destroy()
  176. def savdb_script(self):
  177. """save database as a script file"""
  178. filename = filedialog.asksaveasfilename(
  179. initialdir=self.initialdir, defaultextension='.db',
  180. title="save database structure in a text file",
  181. filetypes=[("default", "*.sql"), ("other", "*.txt"),
  182. ("all", "*.*")])
  183. if filename != '':
  184. self.set_initialdir(filename)
  185. with io.open(filename, 'w', encoding='utf-8') as f:
  186. for line in self.conn.iterdump():
  187. f.write('%s\n' % line)
  188. def sav_script(self):
  189. """save a script in a file"""
  190. active_tab_id = self.n.notebook.select()
  191. if active_tab_id != '':
  192. # get current selection (or all)
  193. fw = self.n.fw_labels[active_tab_id]
  194. script = fw.get(1.0, END)[:-1]
  195. filename = filedialog.asksaveasfilename(
  196. initialdir=self.initialdir, defaultextension='.db',
  197. title="save script in a sql file",
  198. filetypes=[("default", "*.sql"), ("other", "*.txt"),
  199. ("all", "*.*")])
  200. if filename != "":
  201. self.set_initialdir(filename)
  202. with io.open(filename, 'w', encoding='utf-8') as f:
  203. if "你好 мир Artisou à croute" not in script:
  204. f.write("/*utf-8 tag : 你好 мир Artisou à croute*/\n")
  205. f.write(script)
  206. def attach_db(self):
  207. """attach an existing database"""
  208. filename = filedialog.askopenfilename(
  209. initialdir=self.initialdir, defaultextension='.db',
  210. title="Choose a database to attach ",
  211. filetypes=[("default", "*.db"), ("other", "*.db*"),
  212. ("all", "*.*")])
  213. attach = os.path.basename(filename).split(".")[0]
  214. avoid = {i[1]: 0 for i in get_leaves(self.conn, 'attached_databases')}
  215. att, indice = attach, 0
  216. while attach in avoid:
  217. attach, indice = att + "_" + str(indice), indice + 1
  218. if filename != '':
  219. self.set_initialdir(filename)
  220. attach_order = "ATTACH DATABASE '%s' as '%s' " % (filename, attach)
  221. self.conn.execute(attach_order)
  222. self.actualize_db()
  223. def close_db(self):
  224. """close the database"""
  225. self.conn.close
  226. self.new_db(":memory:")
  227. self.actualize_db()
  228. def actualize_db(self):
  229. """refresh the database view"""
  230. # bind double-click for easy user interaction
  231. self.db_tree.tag_bind('run', '<Double-1>', self.t_doubleClicked)
  232. self.db_tree.tag_bind('run_up', '<Double-1>', self.t_doubleClicked)
  233. # delete existing tree entries before re-creating them
  234. for node in self.db_tree.get_children():
  235. self.db_tree.delete(node)
  236. # create top node
  237. dbtext = os.path.basename(self.database_file)
  238. id0 = self.db_tree.insert(
  239. "", 0, "Database", text="main (%s)" % dbtext, values=(dbtext, ""))
  240. # add Database Objects, by Category
  241. for categ in ['master_table', 'table', 'view', 'trigger', 'index',
  242. 'pydef']:
  243. self.feed_dbtree(id0, categ, "main")
  244. # for attached databases
  245. for att_db in self.feed_dbtree(id0, 'attached_databases'):
  246. # create another top node
  247. dbtext2, insert_position = att_db + " (Attached)", 'end'
  248. if att_db == "temp":
  249. dbtext2, insert_position = "temp (%s)" % dbtext, 0
  250. id0 = self.db_tree.insert("", insert_position, dbtext2,
  251. text=dbtext2, values=(att_db, ""))
  252. # add attached Database Objects, by Category
  253. for categ in ['master_table', 'table', 'view', 'trigger', 'index']:
  254. self.feed_dbtree(id0, categ, att_db)
  255. # update time of last refresh
  256. self.db_tree.heading('#0', text=(
  257. datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
  258. def quit_db(self):
  259. """quit the application"""
  260. if messagebox.askyesno(message='Are you sure you want to quit ?',
  261. icon='question', title='Quiting'):
  262. self.tk_win.destroy()
  263. def run_tab(self, event=None):
  264. """clear previous results and run current script of a tab"""
  265. active_tab_id = self.n.notebook.select()
  266. if active_tab_id != '':
  267. # remove previous results
  268. self.n.remove_treeviews(active_tab_id)
  269. # get current selection (or all)
  270. fw = self.n.fw_labels[active_tab_id]
  271. try:
  272. script = (fw.get('sel.first', 'sel.last'))
  273. except:
  274. script = fw.get(1.0, END)[:-1]
  275. self.create_and_add_results(script, active_tab_id)
  276. fw.focus_set() # workaround bug http://bugs.python.org/issue17511
  277. def exsav_script(self):
  278. """write script commands + top results to a log file"""
  279. # idea from http://blog.mp933.fr/post/2014/05/15/Script-vs.-copy/paste
  280. active_tab_id = self.n.notebook.select()
  281. if active_tab_id != '':
  282. # get current selection (or all)
  283. fw = self.n.fw_labels[active_tab_id]
  284. script = fw.get(1.0, END)[:-1]
  285. filename = filedialog.asksaveasfilename(
  286. initialdir=self.initialdir, defaultextension='.db',
  287. title="execute Script + output in a log file",
  288. filetypes=[("default", "*.txt"), ("other", "*.log"),
  289. ("all", "*.*")])
  290. if filename == "":
  291. return
  292. self.set_initialdir(filename)
  293. with io.open(filename, 'w', encoding='utf-8') as f:
  294. if "你好 мир Artisou à croute" not in script:
  295. f.write("/*utf-8 tag : 你好 мир Artisou à croute*/\n")
  296. self.create_and_add_results(script, active_tab_id, limit=99, log=f)
  297. fw.focus_set() # workaround bug http://bugs.python.org/issue17511
  298. def chg_fontsize(self):
  299. """change the display font size"""
  300. sizes = [10, 13, 14]
  301. font_types = ["TkDefaultFont", "TkTextFont", "TkFixedFont",
  302. "TkMenuFont", "TkHeadingFont", "TkCaptionFont",
  303. "TkSmallCaptionFont", "TkIconFont", "TkTooltipFont"]
  304. ww = ['normal', 'bold']
  305. if self.font_size < max(sizes):
  306. self.font_size = min([i for i in sizes if i > self.font_size])
  307. else:
  308. self.font_size = sizes[0]
  309. self.font_wheight = 0
  310. ff = 'Helvetica' if self.font_size != min(sizes) else 'Courier'
  311. self.font_wheight = 0 if self.font_size == min(sizes) else 1
  312. for typ in font_types:
  313. default_font = font.nametofont(typ)
  314. default_font.configure(size=self.font_size,
  315. weight=ww[self.font_wheight], family=ff)
  316. def t_doubleClicked(self, event):
  317. """launch action when dbl_click on the Database structure"""
  318. # determine item to consider
  319. selitem = self.db_tree.focus() # the item having the focus
  320. seltag = self.db_tree.item(selitem, "tag")[0]
  321. if seltag == "run_up": # 'run-up' tag ==> dbl-click 1 level up
  322. selitem = self.db_tree.parent(selitem)
  323. # get final information : text, selection and action
  324. definition, action = self.db_tree.item(selitem, "values")
  325. tab_text = self.db_tree.item(selitem, "text")
  326. script = action + " limit 999 " if action != "" else definition
  327. # create a new tab and run it if action suggest it
  328. new_tab_ref = self.n.new_query_tab(tab_text, script)
  329. if action != '':
  330. self.run_tab() # run the new_tab created
  331. def get_tk_icons(self):
  332. """return a dictionary of icon_in_tk_format, from B64 images"""
  333. # to create this base 64 from a toto.gif image of 24x24 size do :
  334. # import base64
  335. # b64 = base64.encodestring(open(r"toto.gif","rb").read())
  336. # print("'gif_img': '''\\\n" + b64.decode("utf8") + "'''")
  337. icons = {
  338. 'run_img': '''\
  339. R0lGODdhGAAYAJkAADOqM////wCqMwAAACwAAAAAGAAYAAACM4SPqcvt7wJ8oU5W8025b9OFW0hO
  340. 5EmdKKauSosKL9zJC21FsK27kG+qfUC5IciITConBQA7
  341. ''',
  342. 'exe_img': '''\
  343. R0lGODdhGAAYALsAAP///zOqM/8AAGSJtqHA4Jyen3ul0+jo6Y6z2cLCwaSmpACqM4ODgmKGs4yM
  344. jYOPniwAAAAAGAAYAAAEhBDISacqOBdWOy1HKB6F41mKwihH4r4kdypD0wx4rg8nUDSEoHAY5J0K
  345. AyFiyWQaPY+kYUqtGp4dx26b60kE4LC3FwaPyeJOYM1ur8sCzxrgZovN6sDEHdYD4nkVb2BzPYUV
  346. hIdyfouMi14BC5COgoqBHQttk5VumxJ1bJuZoJacpKE9EQA7
  347. ''',
  348. 'refresh_img': '''\
  349. R0lGODdhGAAYAJkAAP///zOqMwCqMwAAACwAAAAAGAAYAAACSoSPqcvt4aIJEFU5g7AUC9px1/JR
  350. 3yYy4LqAils2IZdFMzCP6nhLd2/j6VqHD+1RAQKLHVfC+VwtcT3pNKOTYjTC4SOK+YbH5EYBADs=
  351. ''',
  352. 'newtab_img': '''\
  353. R0lGODdhGAAYAJkAAP///56fnQAAAAAAACwAAAAAGAAYAAACSoSPqcsm36KDsj1R1d00840E4ige
  354. 3xWSo9YppRGwGKPGCUrXtfQCut3ouYC5IHEhTCSHRt4x5fytIlKSs0l9HpZKLcy7BXOhRUYBADs=
  355. ''',
  356. 'csvin_img': '''\
  357. R0lGODdhGAAYAMwAAPj4+AAAADOqM2FkZtjY2r7Awujo6V9gYeDg4b/Cwzc3N0pKSl9fX5GRkVVV
  358. VXl6fKSmpLCxsouNkFdXV97d4N7e4N7g4IyMjZyen6SopwAAAAAAAAAAAAAAAAAAAAAAACwAAAAA
  359. GAAYAAAFlSAgjkBgmuUZlONKii4br/MLv/Gt47ia/rYcT2bb0VowVFFF8+2K0KjUJqhOo1XBlaQV
  360. Zbdc7Rc8ylrJ5THaa5YqFozBgOFQAMznl6FhsO37UwMEBgiFFRYIhANXBxgJBQUJkpAZi1MEBxAR
  361. kI8REAMUVxIEcgcDpqYEElcODwSvsK8PllMLAxeQkA0DDmhvEwwLdmAhADs=
  362. ''',
  363. 'csvex_img': '''\
  364. R0lGODdhGAAYAMwAAPj4+AAAADOqM2FkZtjY2r7AwuDg4b/Cw+jo6V9gYTc3N0pKSlVVVV9fX5GR
  365. kaSmpLCxsnl6fIuNkN7g4N7d4KSop4yMjZyen1dXVwAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAA
  366. GAAYAAAFkiAgjkBgmuUZlONKii4br/MLv/Gt47ia/rYcT2bb0VowVFFF8+2K0Kh0JKhap1BrFZu9
  367. cl9awZd03Y4BXvQ5DVUsGoNBg6FAm6MOhA3h4A4ICAaCBhOCCANYAxcHBQUHj44ViFMECQ8QjY0Q
  368. DwMUWBIEcQkDo6MEElgMEQSsrawRk1MLAxaZBQ4DDGduGA0LdV8hADs=
  369. ''',
  370. 'qryex_img': '''\
  371. R0lGODdhGAAYAJkAAP///56fnQAAAP8AACwAAAAAGAAYAAACXIQPoporeR4yEtZ3J511e845zah1
  372. oKV9WEQxqYOJX0rX9oDndp3jO6/7aXqDVOCIPB50Pk0yaQgCijSlITBt/p4B6ZbL3VkBYKxt7DTX
  373. 0BN2uowUw+NndVq+tk8KADs=
  374. ''',
  375. 'sqlin_img': '''\
  376. R0lGODdhGAAYALsAAP///46z2Xul02SJtp6fnenp6f8AAMLCwaHA4IODgoCo01RymIOPnmKGswAA
  377. AAAAACwAAAAAGAAYAAAEkRDIOYm9N9G9SfngR2jclnhHqh7FWAKZF84uFxdr3pb3TPOWEy6n2tkG
  378. jcZgyWw6OwOEdEqtIgYbRjTA7Xq/WIoW8S17wxOteR1AS9Ts8sI08Aru+Px9TknU9YB5fBN+AYGH
  379. gxJ+dwoCjY+OCpKNiQAGBk6ZTgsGE5edLy+XlqOhop+gpiWoqqGoqa0Ur7CxABEAOw==
  380. ''',
  381. 'sqlsav_img': '''\
  382. R0lGODdhGAAYALsAAP///56fnZyen/8AAGSJtqHA4Hul0+jo6Y6z2cLCwaSmpIODgmKGs4yMjYOP
  383. ngAAACwAAAAAGAAYAAAEgxDISacSOItVOxVHKB5C41mKsihH4r4kdyoEwxB4rhMnIDCFoHAY5J0E
  384. BCFiyWQaPY6kYUqtGp6dxm6b60kG4LC3FwaPyeJzpzzwaDQTsbkTqNsx3zmgfapPAnt6Y3Z1Amlq
  385. AoR3cF5+EoqFY4k9jpSAfQKSkJCDm4SZXpN9l5aUoB4RADs=
  386. ''',
  387. 'dbdef_img': '''\
  388. R0lGODdhGAAYAMwAAPj4+DOqM2SJtmFkZqHA4NjY2sLCwejo6b7Awpyen3ul046z2aSop+Dg4V9g
  389. YZGRkaSmpLCxsouNkDc3N2dxekpKSlVVVYyMjWKGs4ODgnl6fJ+goYOPnl9fX0xMTAAAACwAAAAA
  390. GAAYAAAFuCAgjuTIJGiaZGVLJkcsH9DjmgxzMYfh/4fVDcAQCDDGpNI4hGAI0KgUKhgmBNGFdrut
  391. 3jhYhXhMVnhdj6U6OWy7h4G4/O2Sx+n1OZ5kD5QmHgOCAx0TJXN3Iw8HLQc2InoAfiIUBQcNmJkN
  392. BxSSiS0DCT8/CAYMA55DBQMQEQilCBGndBKrAw4Ot7kFEm+rG66vsRCob7WCube3vG8WGgXQ0dAa
  393. nW8VAxfCCA8DFnsAExUWAxWGeCEAOw==
  394. ''',
  395. 'chgsz_img': '''\
  396. R0lGODdhGAAYAJkAAP///wAAADOqMwCqMywAAAAAGAAYAAACZISPGRvpb1iDRjy5KBBWYc0NXjQ9
  397. A8cdDFkiZyiIwDpnCYqzCF2lr2rTHVKbDgsTJG52yE8R0nRSJA7qNOhpVbFPHhdhPF20w46S+f2h
  398. xlzceksqu6ET7JwtLRrhwNt+1HdDUQAAOw==
  399. ''',
  400. 'img_close': '''\
  401. R0lGODlhCAAIAMIBAAAAADs7O4+Pj9nZ2Ts7Ozs7Ozs7Ozs7OyH+EUNyZWF0ZWQgd2l0aCBHSU1Q
  402. ACH5BAEKAAQALAAAAAAIAAgAAAMVGDBEA0qNJyGw7AmxmuaZhWEU5kEJADs=
  403. ''',
  404. 'img_closeactive': '''\
  405. R0lGODlhCAAIAMIEAAAAAP/SAP/bNNnZ2cbGxsbGxsbGxsbGxiH5BAEKAAQALAAAAAAIAAgAAAMV
  406. GDBEA0qNJyGw7AmxmuaZhWEU5kEJADs=
  407. ''',
  408. 'img_closepressed': '''\
  409. R0lGODdhCAAIAIgAAPAAAP///ywAAAAACAAIAAACDkyAeJYM7FR8Ex7aVpIFADs=
  410. '''
  411. }
  412. return {k: PhotoImage(k, data=v) for k, v in icons.items()}
  413. def btn_chg_tab_ok(self, thetop, entries, actions):
  414. """chg a tab title"""
  415. widget, index = actions
  416. # build dico of result
  417. d = {f[0]: f[1]() for f in entries
  418. if not isinstance(f, (type('e'), type(u'e')))}
  419. title = d['new label'].strip()
  420. thetop.destroy()
  421. widget.tab(index, text=title)
  422. def btn_presstwice(self, event):
  423. """double-click on a tab definition to change label"""
  424. x, y, widget = event.x, event.y, event.widget
  425. elem = widget.identify(x, y)
  426. index = widget.index("@%d,%d" % (x, y))
  427. titre = widget.tab(index, 'text')
  428. # determine selected table
  429. actions = [widget, index]
  430. title = 'Changing Tab label'
  431. fields = ['', ['current label', (titre), 'r', 30], '',
  432. ['new label', titre, 'w', 30]]
  433. create_dialog(title, fields, ("Ok", self.btn_chg_tab_ok), actions)
  434. def btn_press(self, event):
  435. """button press over a widget with a 'close' element"""
  436. x, y, widget = event.x, event.y, event.widget
  437. elem = widget.identify(x, y) # widget is the notebook
  438. if "close" in elem: # close button function
  439. index = widget.index("@%d,%d" % (x, y))
  440. widget.state(['pressed'])
  441. widget.pressed_index = index
  442. else: # move function
  443. index = widget.index("@%d,%d" % (x, y))
  444. self.state_drag = True
  445. self.state_drag_widgetid = widget.tabs()[index]
  446. self.state_drag_index = index
  447. def btn_Movex(self, event):
  448. """make the tab follows if button is pressed and mouse moves"""
  449. x, y, widget = event.x, event.y, event.widget
  450. elem = widget.identify(x, y)
  451. index = widget.index("@%d,%d" % (x, y))
  452. if self.state_drag:
  453. if self.state_drag_index != index:
  454. self.btn_Move(widget, self.state_drag_index, index)
  455. self.state_drag_index = index
  456. def btn_Move(self, notebook, old_index, new_index):
  457. """Move old_index tab to new_index position"""
  458. # stackoverflow.com/questions/11570786/tkinter-treeview-drag-and-drop
  459. if new_index != "":
  460. target_index = new_index
  461. if new_index >= len(notebook.tabs())-1:
  462. target_index = "end"
  463. titre = notebook.tab(old_index, 'text')
  464. notebook.forget(old_index)
  465. notebook.insert(target_index, self.state_drag_widgetid, text=titre)
  466. notebook.select(new_index)
  467. def btn_release(self, event):
  468. """button release over a widget with a 'close' element"""
  469. x, y, widget = event.x, event.y, event.widget
  470. elem = widget.identify(x, y)
  471. index = self.state_drag_index
  472. if "close" in elem or "label" in elem:
  473. index = widget.index("@%d,%d" % (x, y))
  474. if "close" in elem and widget.instate(['pressed']):
  475. if widget.pressed_index == index:
  476. widget.forget(index)
  477. widget.event_generate("<<NotebookClosedTab>>")
  478. if self.state_drag and elem.strip() != "":
  479. if self.state_drag_index != index:
  480. self.btn_Move(widget, self.state_drag_index, index)
  481. self.state_drag = False
  482. if not widget.instate(['pressed']):
  483. return
  484. widget.state(["!pressed"])
  485. widget.pressed_index = None
  486. def create_style(self):
  487. """create a Notebook style with close button"""
  488. # from https://github.com/python-git/python/blob/master/Demo/tkinter/
  489. # ttk/notebook_closebtn.py
  490. # himself from http://paste.tclers.tk/896
  491. style = ttk.Style()
  492. style.element_create("close", "image", "img_close",
  493. ("active", "pressed", "!disabled", "img_closepressed"),
  494. ("active", "!disabled", "img_closeactive"), border=6, sticky='')
  495. style.layout("ButtonNotebook", [
  496. ("ButtonNotebook.client", {"sticky": "nswe"})])
  497. style.layout("ButtonNotebook.Tab", [
  498. ("ButtonNotebook.tab", {"sticky": "nswe", "children":
  499. [("ButtonNotebook.padding", {"side": "top", "sticky": "nswe",
  500. "children":
  501. [("ButtonNotebook.focus", {"side": "top", "sticky": "nswe",
  502. "children":
  503. [("ButtonNotebook.label", {"side": "left", "sticky": ''}),
  504. ("ButtonNotebook.close", {"side": "left", "sticky": ''})]
  505. })]
  506. })]
  507. })]
  508. )
  509. self.tk_win.bind_class("TNotebook", "<ButtonPress-1>",
  510. self.btn_press, True)
  511. self.tk_win.bind_class("TNotebook", "<ButtonRelease-1>",
  512. self.btn_release)
  513. self.tk_win.bind_class("TNotebook", "<B1-Motion>", self.btn_Movex)
  514. self.tk_win.bind_class("TNotebook", "<Double-1>", self.btn_presstwice)
  515. def createToolTip(self, widget, text):
  516. """create a tooptip box for a widget."""
  517. # www.daniweb.com/software-development/python/code/234888/tooltip-box
  518. def enter(event):
  519. global tipwindow
  520. x = y = 0
  521. try:
  522. tipwindow = tipwindow
  523. except:
  524. tipwindow = None
  525. if tipwindow or not text:
  526. return
  527. x, y, cx, cy = widget.bbox("insert")
  528. x += widget.winfo_rootx() + 27
  529. y += widget.winfo_rooty() + 27
  530. # Creates a toplevel window
  531. tipwindow = tw = Toplevel(widget)
  532. # Leaves only the label and removes the app window
  533. tw.wm_overrideredirect(1)
  534. tw.wm_geometry("+%d+%d" % (x, y))
  535. label = Label(tw, text=text, justify=LEFT, background="#ffffe0",
  536. relief=SOLID, borderwidth=1)
  537. label.pack(ipadx=1)
  538. def close(event):
  539. global tipwindow
  540. tw = tipwindow
  541. tipwindow = None
  542. if tw:
  543. tw.destroy()
  544. widget.bind("<Enter>", enter)
  545. widget.bind("<Leave>", close)
  546. def feed_dbtree(self, root_id, category, attached_db=""):
  547. """feed database treeview for category, return list of leaves names"""
  548. # prepare re-formatting functions for fields and database names
  549. def f(t): return ('"%s"' % t.replace('"', '""')) if t != "" else t
  550. def db(t): return ('"%s".' % t.replace('"', '""')) if t != "" else t
  551. attached = db(attached_db)
  552. # get Category list of [unique_name, name, definition, sub_category]
  553. tables = get_leaves(self.conn, category, attached_db)
  554. if len(tables) > 0:
  555. # level 1 : create the "category" node (as Category is not empty)
  556. root_txt = "%s(%s)" % (attached, category)
  557. idt = self.db_tree.insert(
  558. root_id, "end", root_txt,
  559. text="%s (%s)" % (category, len(tables)), values=("", ""))
  560. for t_id, t_name, definition, sub_cat in tables:
  561. # level 2 : print object creation, and '(Definition)' if fields
  562. sql3 = ""
  563. if sub_cat != '':
  564. # it's a table : prepare a Query with names of each column
  565. sub_c = get_leaves(self.conn, sub_cat, attached_db, t_name)
  566. colnames = [col[1] for col in sub_c]
  567. columns = [col[1] + ' ' + col[2] for col in sub_c]
  568. sql3 = 'select "'+'" , "'.join(colnames)+'" from ' + (
  569. '%s%s' % (attached, f(t_name)))
  570. idc = self.db_tree.insert(
  571. idt, "end", "%s%s" % (root_txt, t_id),
  572. text=t_name, tags=('run',), values=(definition, sql3))
  573. if sql3 != "":
  574. self.db_tree.insert(
  575. idc, "end", ("%s%s;d" % (root_txt, t_id)),
  576. text=['(Definition)'], tags=('run',),
  577. values=(definition, ""))
  578. # level 3 : Insert a line per column of the Table/View
  579. for c in range(len(sub_c)):
  580. self.db_tree.insert(
  581. idc, "end", "%s%s%s" % (root_txt, t_id, sub_c[c][0]),
  582. text=columns[c], tags=('run_up',), values=('', ''))
  583. return [i[1] for i in tables]
  584. def create_and_add_results(self, instructions, tab_tk_id,
  585. limit=-1, log=None):
  586. """execute instructions and add them to given tab results"""
  587. a_jouer = self.conn.get_sqlsplit(instructions, remove_comments=False)
  588. # must read :https://www.youtube.com/watch?v=09tM18_st4I#t=1751
  589. # stackoverflow.com/questions/15856976/transactions-with-python-sqlite3
  590. isolation = self.conn.conn.isolation_level
  591. counter = 0
  592. shell_list = ['', '']
  593. if isolation == "": # Sqlite3 and dump.py default don't match
  594. self.conn.conn.isolation_level = None # right behavior
  595. cu = self.conn.conn.cursor()
  596. sql_error = False
  597. def beurk(r):
  598. """format data line log"""
  599. s = ['"' + s.replace('"', '""') + '"' if
  600. isinstance(s, (type('e'), type(u'e'))) else str(s) for s in r]
  601. return "("+",".join(s)+")"
  602. def bip(c):
  603. """format instruction log header"""
  604. timing = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  605. return ("\n---------N°%s----------[" % counter + timing + "]\n\n")
  606. for instruction in a_jouer:
  607. if log is not None: # write to logFile
  608. counter += 1
  609. log.write(bip(counter))
  610. log.write(instruction)
  611. log.write("\n")
  612. instru = next(self.conn.get_sqlsplit(instruction,
  613. remove_comments=True))
  614. instru = instru.replace(";", "").strip(' \t\n\r')
  615. first_line = (instru + "\n").splitlines()[0]
  616. if instru[:5] == "pydef":
  617. pydef = self.conn.createpydef(instru)
  618. titles = ("Creating embedded python function",)
  619. rows = self.conn.conn_def[pydef]['pydef'].splitlines()
  620. rows.append(self.conn.conn_def[pydef]['inst'])
  621. self.n.add_treeview(tab_tk_id, titles, rows, "Info", pydef)
  622. if log is not None: # write to logFile
  623. log.write("\n".join(['("%s")' % r for r in rows])+"\n")
  624. elif instru[:1] == ".": # a shell command !
  625. # handle a ".function" here !
  626. # import FILE TABLE
  627. shell_list = shlex.split(instru) # magic standard library
  628. try:
  629. if shell_list[0] == '.import' and len(shell_list) >= 2:
  630. csv_file = shell_list[1]
  631. guess = guess_csv(csv_file)
  632. if len(shell_list) >= 3:
  633. guess.table_name = shell_list[2]
  634. # Create csv reader and give it to import
  635. reading = read_this_csv(csv_file,
  636. guess.encodings[0],
  637. guess.default_sep,
  638. guess.default_quote,
  639. guess.has_header,
  640. guess.default_decims[0])
  641. guess_sql = guess_sql_creation(guess.table_name,
  642. guess.default_sep, ".",
  643. guess.has_header,
  644. guess.dlines,
  645. guess.default_quote)[0]
  646. self.conn.insert_reader(reading, guess.table_name,
  647. guess_sql, create_table=False,
  648. replace=False)
  649. self.n.add_treeview(tab_tk_id, ('table', 'file'),
  650. ((guess.table_name, csv_file),),
  651. "Info", first_line)
  652. if log is not None: # write to logFile
  653. log.write('-- File %s imported in "%s"\n' % (
  654. csv_file, guess.table_name))
  655. except IOError as err:
  656. msg = ("I/O error: {0}".format(err))
  657. self.n.add_treeview(tab_tk_id, ('Error !',), [(msg,)],
  658. "Error !", instru)
  659. if log is not None: # write to logFile
  660. log.write("Error ! %s : %s" % (msg, instru))
  661. sql_error = True
  662. break
  663. elif instruction != "":
  664. try:
  665. if shell_list[0] == '.once':
  666. shell_list[0] = ' '
  667. encode_in = 'utf-8-sig' if os.name == 'nt' else 'utf-8'
  668. self.conn.export_writer(instruction, shell_list[1],
  669. encoding=encode_in)
  670. self.n.add_treeview(tab_tk_id, ('qry', 'file'),
  671. ((instruction, shell_list[1]),),
  672. "Info", ".once %s" % shell_list[1])
  673. else:
  674. cur = cu.execute(instruction)
  675. rows = cur.fetchall()
  676. # a query may have no result( like for an "update")
  677. if cur.description is not None:
  678. titles = [row_info[0] for
  679. row_info in cur.description]
  680. self.n.add_treeview(
  681. tab_tk_id, titles, rows, "Qry", first_line)
  682. if log is not None: # write to logFile
  683. log.write(beurk(titles) + "\n")
  684. log.write("\n".join(
  685. [beurk(l) for l in rows[:limit]]) + "\n")
  686. if len(rows) > limit:
  687. log.write("%s more..." % len((rows)-limit))
  688. except sqlite.Error as msg: # OperationalError
  689. self.n.add_treeview(tab_tk_id, ('Error !',), [(msg,)],
  690. "Error !", first_line)
  691. if log is not None: # write to logFile
  692. log.write("Error ! %s" % msg)
  693. sql_error = True
  694. break
  695. if self.conn.conn.isolation_level != isolation:
  696. # if we're in 'backward' compatible mode (automatic commit)
  697. try:
  698. if self.conn.conn.in_transaction: # python 3.2
  699. if not sql_error:
  700. cu.execute("COMMIT;")
  701. if log is not None: # write to logFile
  702. log.write("\n-------COMMIT;--------\n" % counter)
  703. else:
  704. cu.execute("ROLLBACK;")
  705. except:
  706. if not sql_error:
  707. try:
  708. cu.execute("COMMIT;")
  709. if log is not None: # write to logFile
  710. log.write("\n-------COMMIT;--------\n" % counter)
  711. except:
  712. pass
  713. else:
  714. try:
  715. cu.execute("ROLLBACK;")
  716. except:
  717. pass
  718. self.conn.conn.isolation_level = isolation # restore standard
  719. def import_csvtb(self):
  720. """import csv dialog (with guessing of encoding and separator)"""
  721. csv_file = filedialog.askopenfilename(
  722. initialdir=self.initialdir, defaultextension='.db',
  723. title="Choose a csv fileto import ",
  724. filetypes=[("default", "*.csv"), ("other", "*.txt"),
  725. ("all", "*.*")])
  726. if csv_file != '':
  727. self.set_initialdir(csv_file)
  728. # guess all via an object
  729. guess = guess_csv(csv_file)
  730. guess_sql = guess_sql_creation(guess.table_name, guess.default_sep,
  731. ".", guess.has_header, guess.dlines,
  732. guess.default_quote)[2]
  733. # check it via dialog box
  734. fields_in = ['', ['csv Name', csv_file, 'r', 100], '',
  735. ['table Name', guess.table_name],
  736. ['column separator', guess.default_sep, 'w', 20],
  737. ['string delimiter', guess.default_quote, 'w', 20],
  738. '', ['Decimal separator', guess.default_decims],
  739. ['Encoding', guess.encodings],
  740. 'Fliflaps', ['Header line', guess.has_header],
  741. ['Create table', True],
  742. ['Replace existing data', True], '',
  743. ['first 3 lines', guess.dlines, 'r', 100, 10], '',
  744. ['use manual creation request', False], '',
  745. ['creation request', guess_sql, 'w', 100, 10]]
  746. actions = ([self.conn, self.actualize_db])
  747. create_dialog(("Importing %s" % csv_file), fields_in,
  748. ("Import", import_csvtb_ok), actions)
  749. def export_csvtb(self):
  750. """get selected table definition and launch cvs export dialog"""
  751. # determine selected table
  752. actions = [self.conn, self.db_tree]
  753. selitem = self.db_tree.focus() # get tree item having the focus
  754. if selitem != '':
  755. seltag = self.db_tree.item(selitem, "tag")[0]
  756. if seltag == "run_up": # if 'run-up', do as dbl-click 1 level up
  757. selitem = self.db_tree.parent(selitem)
  758. # get final information
  759. definition, query = self.db_tree.item(selitem, "values")
  760. if query != "": # run the export_csv dialog
  761. title = ('Export Table "%s" to ?' %
  762. self.db_tree.item(selitem, "text"))
  763. self.export_csv_dialog(query, title, actions)
  764. def export_csvqr(self):
  765. """get tab selected definition and launch cvs export dialog"""
  766. actions = [self.conn, self.n]
  767. active_tab_id = self.n.notebook.select()
  768. if active_tab_id != '': # get current selection (or all)
  769. fw = self.n.fw_labels[active_tab_id]
  770. try:
  771. query = fw.get('sel.first', 'sel.last')
  772. except:
  773. query = fw.get(1.0, END)[:-1]
  774. if query != "":
  775. self.export_csv_dialog(query, "Export Query", actions)
  776. def export_csv_dialog(self, query="--", text="undefined.csv", actions=[]):
  777. """export csv dialog"""
  778. # proposed encoding (we favorize utf-8 or utf-8-sig)
  779. encodings = ["utf-8", locale.getdefaultlocale()[1], "utf-16",
  780. "utf-8-sig"]
  781. if os.name == 'nt':
  782. encodings = ["utf-8-sig", locale.getdefaultlocale()[1], "utf-16",
  783. "utf-8"]
  784. # proposed csv separator
  785. default_sep = [",", "|", ";"]
  786. csv_file = filedialog.asksaveasfilename(
  787. initialdir=self.initialdir, defaultextension='.db', title=text,
  788. filetypes=[("default", "*.csv"), ("other", "*.txt"),
  789. ("all", "*.*")])
  790. if csv_file != "":
  791. # Idea from (http://www.python-course.eu/tkinter_entry_widgets.php)
  792. fields = ['', ['csv Name', csv_file, 'r', 100], '',
  793. ['column separator', default_sep],
  794. ['Header line', True],
  795. ['Encoding', encodings], '',
  796. ["Data to export (MUST be 1 Request)",
  797. (query), 'w', 100, 10]]
  798. create_dialog(("Export to %s" % csv_file), fields,
  799. ("Export", export_csv_ok), actions)
  800. class NotebookForQueries():
  801. """Create a Notebook with a list in the First frame
  802. and query results in following treeview frames """
  803. def __init__(self, tk_win, root, queries):
  804. self.tk_win = tk_win
  805. self.root = root
  806. self.notebook = Notebook(root, style="ButtonNotebook") # ttk.
  807. self.fw_labels = {} # tab_tk_id -> Scripting frame python object
  808. self.fw_result_nbs = {} # tab_tk_id -> Notebook of Results
  809. # resize rules
  810. root.columnconfigure(0, weight=1)
  811. root.rowconfigure(0, weight=1)
  812. # grid widgets
  813. self.notebook.grid(row=0, column=0, sticky=(N, W, S, E))
  814. def new_query_tab(self, title, query):
  815. """add a Tab 'title' to the notebook, containing the Script 'query'"""
  816. fw_welcome = ttk.Panedwindow(self.tk_win, orient=VERTICAL) # tk_win
  817. fw_welcome.pack(fill='both', expand=True)
  818. self.notebook.add(fw_welcome, text=(title))
  819. # new "editable" script
  820. f1 = ttk.Labelframe(fw_welcome, text='Script', width=200, height=100)
  821. fw_welcome.add(f1)
  822. fw_label = Text(f1, bd=1, undo=True)
  823. scroll = ttk.Scrollbar(f1, command=fw_label.yview)
  824. fw_label.configure(yscrollcommand=scroll.set)
  825. fw_label.insert(END, (query))
  826. fw_label.pack(side=LEFT, expand=YES, fill=BOTH, padx=2, pady=2)
  827. scroll.pack(side=RIGHT, expand=NO, fill=BOTH, padx=2, pady=2)
  828. # keep tab reference by tk id
  829. working_tab_id = "." + fw_welcome._name
  830. # keep tab reference to script (by tk id)
  831. self.fw_labels[working_tab_id] = fw_label
  832. # new "Results" Container
  833. fr = ttk.Labelframe(fw_welcome, text='Results', width=200, height=100)
  834. fw_welcome.add(fr)
  835. # containing a notebook
  836. fw_result_nb = Notebook(fr, style="ButtonNotebook")
  837. fw_result_nb.pack(fill='both', expand=True)
  838. # resize rules
  839. fw_welcome.columnconfigure(0, weight=1)
  840. # keep reference to result_nb objects (by tk id)
  841. self.fw_result_nbs[working_tab_id] = fw_result_nb
  842. # activate this tab print(self.notebook.tabs())
  843. self.notebook.select(working_tab_id)
  844. # workaround to have a visible result pane on initial launch
  845. self.add_treeview(
  846. working_tab_id, "_", "", "click on ('->') to run Script")
  847. return working_tab_id # gives back tk_id reference of the new tab
  848. def del_tab(self):
  849. """delete active notebook tab's results"""
  850. given_tk_id = self.notebook.select()
  851. if given_tk_id != '':
  852. self.notebook.forget(given_tk_id)
  853. def remove_treeviews(self, given_tk_id):
  854. """remove results from given tab tk_id"""
  855. if given_tk_id != '':
  856. myz = self.fw_result_nbs[given_tk_id]
  857. for xx in list(myz.children.values()):
  858. xx.grid_forget()
  859. xx.destroy()
  860. def add_treeview(self, given_tk_id, columns, data, title="__", subt=""):
  861. """add a dataset result to the given tab tk_id"""
  862. # ensure we work on lists
  863. if isinstance(columns, (type('e'), type(u'e'))):
  864. tree_columns = [columns]
  865. else:
  866. tree_columns = columns
  867. lines = [data] if isinstance(data, (type('e'), type(u'e'))) else data
  868. # get back reference to Notebooks of Results
  869. # (see http://www.astro.washington.edu/users/rowen/TkinterSummary.html)
  870. fw_result_nb = self.fw_result_nbs[given_tk_id]
  871. # create a Labelframe to contain new resultset and scrollbars
  872. f2 = ttk.Labelframe(
  873. fw_result_nb, text=('(%s lines) %s' % (len(lines), subt)),
  874. width=200, height=100)
  875. f2.pack(fill='both', expand=True)
  876. fw_result_nb.add(f2, text=title)
  877. # ttk.Style().configure('TLabelframe.label', font=("Arial",14, "bold"))
  878. # lines=queries
  879. fw_Box = Treeview(f2, columns=tree_columns, show="headings",
  880. padding=(2, 2, 2, 2))
  881. fw_vsb = Scrollbar(f2, orient="vertical", command=fw_Box.yview)
  882. fw_hsb = Scrollbar(f2, orient="horizontal", command=fw_Box.xview)
  883. fw_Box.configure(yscrollcommand=fw_vsb.set, xscrollcommand=fw_hsb.set)
  884. fw_Box.grid(column=0, row=0, sticky='nsew', in_=f2)
  885. fw_vsb.grid(column=1, row=0, sticky='ns', in_=f2)
  886. fw_hsb.grid(column=0, row=2, sticky='ew', in_=f2)
  887. # this new Treeview may occupy all variable space
  888. f2.grid_columnconfigure(0, weight=1)
  889. f2.grid_rowconfigure(0, weight=1)
  890. # feed Treeview Header
  891. for col in tuple(tree_columns):
  892. fw_Box.heading(
  893. col, text=col.title(),
  894. command=lambda c=col: self.sortby(fw_Box, c, 0))
  895. fw_Box.column(col, width=font.Font().measure(col.title()))
  896. def flat(x):
  897. """replace line_return by space, if given a string"""
  898. if isinstance(x, (type('e'), type(u'e'))):
  899. return x.replace("\n", " ")
  900. return x
  901. # feed Treeview Lines
  902. for items in lines:
  903. # if line is a string, redo a tuple
  904. item = (items,) if isinstance(items,
  905. (type('e'), type(u'e'))) else items
  906. # replace line_return by space (grid don't like line_returns)
  907. line_cells = tuple(flat(item[c]) for c in range(len(tree_columns)))
  908. # insert the line of data
  909. fw_Box.insert('', 'end', values=line_cells)
  910. # adjust columns length if necessary and possible
  911. for indx, val in enumerate(line_cells):
  912. try:
  913. ilen = font.Font().measure(val)
  914. if fw_Box.column(tree_columns[indx],
  915. width=None) < ilen and ilen < 400:
  916. fw_Box.column(tree_columns[indx], width=ilen)
  917. except:
  918. pass
  919. def sortby(self, tree, col, descending):
  920. """Sort a ttk treeview contents when a column is clicked on."""
  921. # grab values to sort
  922. data = [(tree.set(child, col), child) for child in tree.get_children()]
  923. # reorder data
  924. data.sort(reverse=descending)
  925. for indx, item in enumerate(data):
  926. tree.move(item[1], '', indx)
  927. # switch the heading so that it will sort in the opposite direction
  928. tree.heading(col, command=lambda col=col:
  929. self.sortby(tree, col, int(not descending)))
  930. class guess_csv():
  931. """guess everything about a csv file of data to import in SQL"""
  932. def __init__(self, csv_file):
  933. self.has_header = True
  934. self.default_sep = ","
  935. self.default_quote = '"'
  936. self.encodings = guess_encoding(csv_file)
  937. self.table_name = os.path.basename(csv_file).split(".")[0]
  938. with io.open(csv_file, encoding=self.encodings[0]) as f:
  939. self.preview = f.read(9999)
  940. try:
  941. dialect = csv.Sniffer().sniff(self.preview)
  942. self.has_header = csv.Sniffer().has_header(self.preview)
  943. self.default_sep = dialect.delimiter
  944. self.default_quote = Dialect.quotechar
  945. except:
  946. pass # sniffer can fail
  947. self.default_decims = [".", ","]
  948. if self.default_sep == ";":
  949. self.default_decims = [",", "."]
  950. self.dlines = "\n\n".join(self.preview.splitlines()[:3])
  951. def guess_sql_creation(table_name, separ, decim, header, data, quoter='"'):
  952. """guess the sql creation request for the table who will receive data"""
  953. try:
  954. dlines = list(csv.reader(data.replace('\n\n', '\n').splitlines(),
  955. delimiter=separ, quotechar=quoter))
  956. except: # minimal hack for python2.7
  957. dlines = list(csv.reader(data.replace('\n\n', '\n').splitlines(),
  958. delimiter=str(separ), quotechar=str(quoter)))
  959. r, val = list(dlines[0]), list(dlines[1])
  960. typ = ['TEXT']*len(r) # default value is TEXT
  961. for i in range(len(r)):
  962. try:
  963. float(val[i].replace(decim, '.')) # unless it can be a real
  964. typ[i] = 'REAL'
  965. except:
  966. checker = sqlite.connect(':memory:')
  967. # avoid the false positive 'now'
  968. val_not_now = val[i].replace('w', 'www').replace('W', 'WWW')
  969. test = "select datetime('{0}')".format(val_not_now)
  970. try:
  971. if checker.execute(test).fetchall()[0][0]:
  972. typ[i] = 'DATETIME' # and unless SQLite can see a DATETIME
  973. except:
  974. pass
  975. checker.close
  976. if header:
  977. # de-duplicate column names, if needed by pastixing with '_'+number
  978. for i in range(len(r)):
  979. if r[i] in r[:i] :
  980. j=1
  981. while r[i]+'_'+str(j) in r[:i] + r[i+1:]:
  982. j +=1
  983. r[i]+= '_'+str(j)
  984. head = ",\n".join([('"%s" %s' % (r[i], typ[i]))
  985. for i in range(len(r))])
  986. sql_crea = ('CREATE TABLE "%s" (%s);' % (table_name, head))
  987. else:
  988. head = ",".join(["c_" + ("000" + str(i))[-3:] for i in range(len(r))])
  989. sql_crea = ('CREATE TABLE "%s" (%s);' % (table_name, head))
  990. return sql_crea, typ, head
  991. def guess_encoding(csv_file):
  992. """guess the encoding of the given file"""
  993. with io.open(csv_file, "rb") as f:
  994. data = f.read(5)
  995. if data.startswith(b"\xEF\xBB\xBF"): # UTF-8 with a "BOM"
  996. return ["utf-8-sig"]
  997. elif data.startswith(b"\xFF\xFE") or data.startswith(b"\xFE\xFF"):
  998. return ["utf-16"]
  999. else: # in Windows, guessing utf-8 doesn't work, so we have to try
  1000. try:
  1001. with io.open(csv_file, encoding="utf-8") as f:
  1002. preview = f.read(222222)
  1003. return ["utf-8"]
  1004. except:
  1005. return [locale.getdefaultlocale()[1], "utf-8"]
  1006. def create_dialog(title, fields_in, buttons, actions):
  1007. """create a formular with title, fields, button, data"""
  1008. # drawing the request form
  1009. top = Toplevel()
  1010. top.title(title)
  1011. top.columnconfigure(0, weight=1)
  1012. top.rowconfigure(0, weight=1)
  1013. # drawing global frame
  1014. content = ttk.Frame(top)
  1015. content.grid(column=0, row=0, sticky=(N, S, E, W))
  1016. content.columnconfigure(0, weight=1)
  1017. # fields = Horizontal FrameLabel, or
  1018. # label, default_value, 'r' or 'w' default_width,default_height
  1019. fields = fields_in
  1020. mf_col = -1
  1021. for f in range(len(fields)): # same structure out
  1022. field = fields[f]
  1023. if isinstance(field, (type('e'), type(u'e'))) or mf_col == -1:
  1024. # a new horizontal frame
  1025. mf_col += 1
  1026. ta_col = -1
  1027. if isinstance(field, (type('e'), type(u'e'))) and field == '':
  1028. mf_frame = ttk.Frame(content, borderwidth=1)
  1029. else:
  1030. mf_frame = ttk.LabelFrame(content, borderwidth=1, text=field)
  1031. mf_frame.grid(column=0, row=mf_col, sticky='nsew')
  1032. Grid.rowconfigure(mf_frame, 0, weight=1)
  1033. content.rowconfigure(mf_col, weight=1)
  1034. if not isinstance(field, (type('e'), type(u'e'))):
  1035. # a new vertical frame
  1036. ta_col += 1
  1037. Grid.columnconfigure(mf_frame, ta_col, weight=1)
  1038. packing_frame = ttk.Frame(mf_frame, borderwidth=1)
  1039. packing_frame.grid(column=ta_col, row=0, sticky='nsew')
  1040. Grid.columnconfigure(packing_frame, 0, weight=1)
  1041. # prepare width and height and writable status
  1042. width = field[3] if len(field) > 3 else 30
  1043. height = field[4] if len(field) > 4 else 30
  1044. status = "normal"
  1045. if len(field) >= 3 and field[2] == "r":
  1046. status = "disabled"
  1047. # switch between object types
  1048. if len(field) > 4:
  1049. # datas
  1050. d_frame = ttk.LabelFrame(packing_frame, borderwidth=5,
  1051. width=width, height=height,
  1052. text=field[0])
  1053. d_frame.grid(column=0, row=0, sticky='nsew', pady=1, padx=1)
  1054. Grid.rowconfigure(packing_frame, 0, weight=1)
  1055. fw_label = Text(d_frame, bd=1, width=width, height=height,
  1056. undo=True)
  1057. fw_label.pack(side=LEFT, expand=YES, fill=BOTH)
  1058. scroll = ttk.Scrollbar(d_frame, command=fw_label.yview)
  1059. scroll.pack(side=RIGHT, expand=NO, fill=Y)
  1060. fw_label.configure(yscrollcommand=scroll.set)
  1061. fw_label.insert(END, ("%s" % field[1]))
  1062. fw_label.configure(state=status)
  1063. Grid.rowconfigure(d_frame, 0, weight=1)
  1064. Grid.columnconfigure(d_frame, 0, weight=1)
  1065. # Data Text Extractor in the fields list ()
  1066. # see stackoverflow.com/questions/17677649 (loop and lambda)
  1067. fields[f][1] = lambda x=fw_label: x.get('1.0', 'end')
  1068. elif isinstance(field[1], type(True)):
  1069. # boolean Field
  1070. name_var = BooleanVar()
  1071. name = ttk.Checkbutton(packing_frame, text=field[0],
  1072. variable=name_var, onvalue=True,
  1073. state=status)
  1074. name_var.set(field[1])
  1075. name.grid(column=0, row=0, sticky='nsew', pady=5, padx=5)
  1076. fields[f][1] = name_var.get
  1077. else: # Text or Combo
  1078. namelbl = ttk.Label(packing_frame, text=field[0])
  1079. namelbl.grid(column=0, row=0, sticky='nsw', pady=5, padx=5)
  1080. name_var = StringVar()
  1081. if not isinstance(field[1], (list, tuple)):
  1082. name = ttk.Entry(packing_frame, textvariable=name_var,
  1083. width=width, state=status)
  1084. name_var.set(field[1])
  1085. else:
  1086. name = ttk.Combobox(packing_frame, textvariable=name_var,
  1087. state=status)
  1088. name['values'] = list(field[1])
  1089. name.current(0)
  1090. name.grid(column=1, row=0, sticky='nsw', pady=0, padx=10)
  1091. fields[f][1] = name_var.get
  1092. # adding button below the same way
  1093. mf_col += 1
  1094. packing_frame = ttk.LabelFrame(content, borderwidth=5)
  1095. packing_frame.grid(column=0, row=mf_col, sticky='nsew')
  1096. okbutton = ttk.Button(
  1097. packing_frame, text=buttons[0],
  1098. command=lambda a=top, b=fields, c=actions: (buttons[1])(a, b, c))
  1099. cancelbutton = ttk.Button(packing_frame, text="Cancel",
  1100. command=top.destroy)
  1101. okbutton.grid(column=0, row=mf_col)
  1102. cancelbutton.grid(column=1, row=mf_col)
  1103. for x in range(3):
  1104. Grid.columnconfigure(packing_frame, x, weight=1)
  1105. top.grab_set()
  1106. def import_csvtb_ok(thetop, entries, actions):
  1107. """read input values from tk formular"""
  1108. conn, actualize_db = actions
  1109. # build dico of result
  1110. d = {f[0]: f[1]() for f in entries
  1111. if not isinstance(f, (type('e'), type(u'e')))}
  1112. # affect to variables
  1113. csv_file = d['csv Name'].strip()
  1114. table_name = d['table Name'].strip()
  1115. separ = d['column separator']
  1116. decim = d['Decimal separator']
  1117. quotechar = d['string delimiter']
  1118. # action
  1119. if csv_file != "(none)" and len(csv_file)*len(table_name)*len(separ) > 1:
  1120. thetop.destroy()
  1121. # do initialization job
  1122. sql, typ, head = guess_sql_creation(table_name, separ, decim,
  1123. d['Header line'],
  1124. d["first 3 lines"], quotechar)
  1125. if d['use manual creation request']:
  1126. sql = ('CREATE TABLE "%s" (%s);' %
  1127. (table_name, d["creation request"]))
  1128. # Create csv reader function and give it to insert
  1129. reading = read_this_csv(csv_file, d['Encoding'], separ,
  1130. quotechar, d['Header line'], decim)
  1131. conn.insert_reader(reading, table_name, sql,
  1132. create_table=d['Create table'],
  1133. replace=d['Replace existing data'])
  1134. # refresh
  1135. actualize_db()
  1136. def read_this_csv(csv_file, encoding, delimiter , quotechar, header, decim):
  1137. """yield csv data records from a file """
  1138. # handle Python 2/3
  1139. try:
  1140. reader = csv.reader(open(csv_file, 'r', encoding=encoding),
  1141. delimiter=delimiter, quotechar=quotechar)
  1142. except: # minimal hack for 2.7
  1143. reader = csv.reader(open(csv_file, 'r'),
  1144. delimiter=str(delimiter), quotechar=str(quotechar))
  1145. # handle header
  1146. if header:
  1147. next(reader)
  1148. # otherwise handle special decimal treatment
  1149. for row in reader:
  1150. if decim != "." and not isinstance(row, (type('e'), type(u'e'))):
  1151. for i in range(len(row)):
  1152. row[i] = row[i].replace(decim, ".")
  1153. yield(row)
  1154. def export_csv_ok(thetop, entries, actions):
  1155. "export a csv table (action)"
  1156. conn = actions[0]
  1157. # build dico of result
  1158. d = {f[0]: f[1]() for f in entries
  1159. if not isinstance(f, (type('e'), type(u'e')))}
  1160. csv_file = d['csv Name'].strip()
  1161. conn.export_writer(d["Data to export (MUST be 1 Request)"], csv_file,
  1162. header=d['Header line'],
  1163. delimiter=d['column separator'],
  1164. encoding=d['Encoding'],
  1165. quotechar='"')
  1166. def get_leaves(conn, category, attached_db="", tbl=""):
  1167. """returns a list of 'category' objects in attached_db
  1168. [objectCode, objectLabel, Definition, 'sub-level']
  1169. """
  1170. # create formatting shortcuts
  1171. def f(t): return ('"%s"' % t.replace('"', '""')) if t != "" else t
  1172. def d(t): return ('%s.' % t) if t != "" else t
  1173. # Initialize datas
  1174. Tables, db, tb = [], d(attached_db), f(tbl)
  1175. master = "sqlite_master" if db != "temp." else "sqlite_temp_master"
  1176. if category == "pydef": # pydef request is not sql, answer is direct
  1177. Tables = [[k, k, v['pydef'], ''] for k, v in conn.conn_def.items()]
  1178. elif category == 'attached_databases':
  1179. # get all attached database, but not the first one ('main')
  1180. resu = list((conn.execute("PRAGMA database_list").fetchall()))[1:]
  1181. for c in resu:
  1182. instruct = "ATTACH DATABASE %s as %s" % (f(c[2]), f(c[1]))
  1183. Tables.append([c[0], c[1], instruct, ''])
  1184. elif category == 'fields':
  1185. resu = conn.execute("PRAGMA %sTABLE_INFO(%s)" % (db, tb)).fetchall()
  1186. Tables = [[c[1], c[1], c[2], ''] for c in resu]
  1187. elif category in ('index', 'trigger', 'master_table', 'table', 'view'):
  1188. # others are 1 sql request that generates directly Tables
  1189. if category in ('index', 'trigger'):
  1190. sql = """SELECT '{0}' || name, name, coalesce(sql,'--auto') , ''
  1191. FROM {0}{3} WHERE type='{1}' ORDER BY name"""
  1192. elif category == 'master_table':
  1193. sql = """SELECT '{0}{3}', '{3}', '--auto', 'fields'
  1194. UNION SELECT '{0}'||name, name, sql, 'fields'
  1195. FROM {0}{3}
  1196. WHERE type='table' AND name LIKE 'sqlite_%' ORDER BY name"""
  1197. elif category in ('table', 'view'):
  1198. sql = """SELECT '{0}' || name, name, sql , 'fields'
  1199. FROM {0}{3} WHERE type = '{1}' AND NOT
  1200. (type='table' AND name LIKE 'sqlite_%') ORDER BY name"""
  1201. Tables = list(conn.execute(sql.format(db, category, tbl,
  1202. master)).fetchall())
  1203. return Tables
  1204. class Baresql():
  1205. """a small wrapper around sqlite3 module"""
  1206. def __init__(self, connection="", keep_log=False, cte_inline=True,
  1207. isolation_level=None):
  1208. self.dbname = connection.replace(":///", "://").replace(
  1209. "sqlite://", "")
  1210. self.conn = sqlite.connect(self.dbname,
  1211. detect_types=sqlite.PARSE_DECLTYPES)
  1212. # pydef and logging infrastructure
  1213. self.conn_def = {}
  1214. self.do_log = keep_log
  1215. self.log = []
  1216. self.conn.isolation_level = isolation_level # commit experience
  1217. def close(self):
  1218. """close database and clear dictionnary of registered 'pydef'"""
  1219. self.conn.close
  1220. self.conn_def = {}
  1221. def iterdump(self):
  1222. """dump the database (add tweaks over the default dump)"""
  1223. # force detection of utf-8 by placing an only utf-8 comment at top
  1224. yield("/*utf-8 tag : 你好 мир Artisou à croute*/\n")
  1225. # add the Python functions pydef
  1226. for k in self.conn_def.values():
  1227. yield(k['pydef'] + ";\n")
  1228. # disable Foreign Constraints at Load
  1229. yield("PRAGMA foreign_keys = OFF; /*if SQlite */;")
  1230. yield("\n/* SET foreign_key_checks = 0;/*if Mysql*/;")
  1231. # how to parametrize Mysql to SQL92 standard
  1232. yield("/* SET sql_mode = 'PIPES_AS_CONCAT';/*if Mysql*/;")
  1233. yield("/* SET SQL_MODE = ANSI_QUOTES; /*if Mysql*/;\n")
  1234. # now the standard dump (notice it uses BEGIN TRANSACTION)
  1235. for line in self.conn.iterdump():
  1236. yield(line)
  1237. # re-instantiate Foreign_keys = True
  1238. for row in self.conn.execute("PRAGMA foreign_keys"):
  1239. flag = 'ON' if row[0] == 1 else 'OFF'
  1240. yield("PRAGMA foreign_keys = %s;/*if SQlite*/;" % flag)
  1241. yield("PRAGMA foreign_keys = %s;/*if SQlite bug*/;" % flag)
  1242. yield("PRAGMA foreign_key_check;/*if SQLite, check*/;")
  1243. yield("\n/*SET foreign_key_checks = %s;/*if Mysql*/;\n" % row[0])
  1244. def execute(self, sql, env=None):
  1245. """execute sql but intercept log"""
  1246. if self.do_log:
  1247. self.log.append(sql)
  1248. return self.conn.execute(sql)
  1249. def createpydef(self, sql):
  1250. """generates and register a pydef instruction"""
  1251. instruction = sql.strip('; \t\n\r')
  1252. # create Python function in Python
  1253. exec(instruction[2:], globals(), locals())
  1254. # add Python function in SQLite
  1255. firstline = (instruction[5:].splitlines()[0]).lstrip()
  1256. firstline = firstline.replace(" ", "") + "("
  1257. instr_name = firstline.split("(", 1)[0].strip()
  1258. instr_parms = firstline.count(',')+1
  1259. instr_add = (("self.conn.create_function('%s', %s, %s)" % (
  1260. instr_name, instr_parms, instr_name)))
  1261. exec(instr_add, globals(), locals())
  1262. # housekeeping definition of pydef in a dictionnary
  1263. the_help = dict(globals(), **locals())[instr_name].__doc__
  1264. self.conn_def[instr_name] = {
  1265. 'parameters': instr_parms, 'inst': instr_add,
  1266. 'help': the_help, 'pydef': instruction}
  1267. return instr_name
  1268. def get_tokens(self, sql, start=0, shell_tokens=False):
  1269. """
  1270. from given sql start position, yield tokens (value + token type)
  1271. if shell_tokens is True, identify line shell_tokens as sqlite.exe does
  1272. """
  1273. length = len(sql)
  1274. i = start
  1275. can_be_shell_command = True
  1276. dico = {' ': 'TK_SP', '\t': 'TK_SP', '\n': 'TK_SP', '\f': 'TK_SP',
  1277. '\r': 'TK_SP', '(': 'TK_LP', ')': 'TK_RP', ';': 'TK_SEMI',
  1278. ',': 'TK_COMMA', '/': 'TK_OTHER', "'": 'TK_STRING',
  1279. "-": 'TK_OTHER', '"': 'TK_STRING', "`": 'TK_STRING'}
  1280. while length > start:
  1281. token = 'TK_OTHER'
  1282. if shell_tokens and can_be_shell_command and i < length and (
  1283. (sql[i] == "." and i == start) or
  1284. (i > start and sql[i-1:i] == "\n.")):
  1285. # a command line shell ! (supposed on one starting line)
  1286. token = 'TK_SHELL'
  1287. i = sql.find("\n", start)
  1288. if i <= 0:
  1289. i = length
  1290. elif sql[i] == "-" and i < length and sql[i:i+2] == "--":
  1291. # this Token is an end-of-line comment : --blabla
  1292. token = 'TK_COM'
  1293. i = sql.find("\n", start)
  1294. if i <= 0:
  1295. i = length
  1296. elif sql[i] == "/" and i < length and sql[i:i+2] == "/*":
  1297. # this Token is a comment block : /* and bla bla \n bla */
  1298. token = 'TK_COM'
  1299. i = sql.find("*/", start) + 2
  1300. if i <= 1:
  1301. i = length
  1302. elif sql[i] not in dico:
  1303. # this token is a distinct word (tagged as 'TK_OTHER')
  1304. while i < length and sql[i] not in dico:
  1305. i += 1
  1306. else:
  1307. # default token analyze case
  1308. token = dico[sql[i]]
  1309. if token == 'TK_SP':
  1310. # find the end of the 'Spaces' Token just detected
  1311. while (i < length and sql[i] in dico and
  1312. dico[sql[i]] == 'TK_SP'):
  1313. i += 1
  1314. elif token == 'TK_STRING':
  1315. # find the end of the 'String' Token just detected
  1316. delimiter = sql[i]
  1317. if delimiter != "'":
  1318. token = 'TK_ID' # usefull nuance ?
  1319. while(i < length):
  1320. i = sql.find(delimiter, i+1)
  1321. if i <= 0: # String is never closed
  1322. i = length
  1323. token = 'TK_ERROR'
  1324. elif i < length - 1 and sql[i+1] == delimiter:
  1325. i += 1 # double '' case, so ignore and continue
  1326. else:
  1327. i += 1
  1328. break # normal End of a String
  1329. else:
  1330. if i < length:
  1331. i += 1
  1332. yield sql[start:i], token
  1333. if token == 'TK_SEMI': # a new sql order can be a new shell token
  1334. can_be_shell_command = True
  1335. elif token not in ('TK_COM', 'TK_SP'): # can't be a shell token
  1336. can_be_shell_command = False
  1337. start = i
  1338. def get_sqlsplit(self, sql, remove_comments=False):
  1339. """yield a list of separated sql orders from a sql file"""
  1340. trigger_mode = False
  1341. mysql = [""]
  1342. for tokv, token in self.get_tokens(sql, shell_tokens=True):
  1343. # clear comments option
  1344. if token != 'TK_COM' or not remove_comments:
  1345. mysql.append(tokv)
  1346. # special case for Trigger : semicolumn don't count
  1347. if token == 'TK_OTHER':
  1348. tok = tokv.upper()
  1349. if tok == "TRIGGER":
  1350. trigger_mode = True
  1351. translvl = 0
  1352. elif trigger_mode and tok in('BEGIN', 'CASE'):
  1353. translvl += 1
  1354. elif trigger_mode and tok == 'END':
  1355. translvl -= 1
  1356. if translvl <= 0:
  1357. trigger_mode = False
  1358. elif (token == 'TK_SEMI' and not trigger_mode):
  1359. # end of a single sql
  1360. yield "".join(mysql)
  1361. mysql = []
  1362. elif (token == 'TK_SHELL'):
  1363. # end of a shell order
  1364. yield("" + tokv)
  1365. mysql = []
  1366. if mysql != []:
  1367. yield("".join(mysql))
  1368. def insert_reader(self, reader, table_name, create_sql=None,
  1369. create_table=True, replace=True, header=False):
  1370. """import a given csv reader into a given table"""
  1371. curs = self.conn.cursor()
  1372. # 1-do initialization job
  1373. # speed-up dead otherwise dead slow speed if not memory database
  1374. try:
  1375. curs.execute('begin transaction')
  1376. except:
  1377. pass
  1378. # check if table exists
  1379. here = curs.execute('PRAGMA table_info("%s")' % table_name).fetchall()
  1380. if create_sql and (create_table or len(here) == 0):
  1381. curs.execute('drop TABLE if exists "%s";' % table_name)
  1382. curs.execute(create_sql)
  1383. if replace:
  1384. curs.execute('delete from "%s";' % table_name)
  1385. # count rows of target table
  1386. nbcol = len(curs.execute('pragma table_info("%s")' % table_name
  1387. ).fetchall())
  1388. sql = 'INSERT INTO "%s" VALUES(%s);' % (table_name,
  1389. ", ".join(["?"]*nbcol))
  1390. # read first_line if hasked to skip headers
  1391. if header:
  1392. next(reader)
  1393. # 2-push records
  1394. curs.executemany(sql, reader)
  1395. self.conn.commit()
  1396. def export_writer(self, sql, csv_file, header=True,
  1397. delimiter=',', encoding='utf-8', quotechar='"'):
  1398. """export a csv table (action)"""
  1399. cursor = self.conn.cursor()
  1400. cursor.execute(sql)
  1401. if sys.version_info[0] != 2: # python3
  1402. fout = io.open(csv_file, 'w', newline='', encoding=encoding)
  1403. writer = csv.writer(fout, delimiter=delimiter,
  1404. quotechar='"', quoting=csv.QUOTE_MINIMAL)
  1405. else: # python2.7 (minimal)
  1406. fout = io.open(csv_file, 'wb')
  1407. writer = csv.writer(fout, delimiter=str(delimiter),
  1408. quotechar=str('"'), quoting=csv.QUOTE_MINIMAL)
  1409. if header:
  1410. writer.writerow([i[0] for i in cursor.description]) # heading row
  1411. writer.writerows(cursor.fetchall())
  1412. fout.close
  1413. def _main():
  1414. app = App()
  1415. # start with a memory Database and a welcome
  1416. app.new_db(":memory:")
  1417. welcome_text = """-- SQLite Memo (Demo = click on green "->" and "@" icons)
  1418. \n-- to CREATE a table 'items' and a table 'parts' :
  1419. DROP TABLE IF EXISTS item; DROP TABLE IF EXISTS part;
  1420. CREATE TABLE item (ItemNo, Description,Kg , PRIMARY KEY (ItemNo));
  1421. CREATE TABLE part(ParentNo, ChildNo , Description TEXT , Qty_per REAL);
  1422. \n-- to CREATE an index :
  1423. DROP INDEX IF EXISTS parts_id1;
  1424. CREATE INDEX parts_id1 ON part(ParentNo Asc, ChildNo Desc);
  1425. \n-- to CREATE a view 'v1':
  1426. DROP VIEW IF EXISTS v1;
  1427. CREATE VIEW v1 as select * from item inner join part as p ON ItemNo=p.ParentNo;
  1428. \n-- to INSERT datas
  1429. INSERT INTO item values("T","Ford",1000);
  1430. INSERT INTO item select "A","Merced",1250 union all select "W","Wheel",9 ;
  1431. INSERT INTO part select ItemNo,"W","needed",Kg/250 from item where Kg>250;
  1432. \n-- to CREATE a Python embedded function (enclose them by "py" and ";") :
  1433. pydef py_sin(s):
  1434. "sinus function : example loading module, handling input/output as strings"
  1435. import math as py_math
  1436. return ("%s" % py_math.sin(s*1));
  1437. pydef py_fib(n):
  1438. "fibonacci : example with function call (may only be internal) "
  1439. fib = lambda n: n if n < 2 else fib(n-1) + fib(n-2)
  1440. return("%s" % fib(n*1));
  1441. pydef py_power(x,y):
  1442. "power function : example loading module, handling input/output as strings"
  1443. import math as py_math
  1444. return ("%s" % ((x*1) ** (y*1)) );
  1445. \n-- to USE a python embedded function and nesting of embedded functions:
  1446. select py_sin(1) as sinus, py_power(2, 1*py_fib(6)) as power, sqlite_version();
  1447. \n-- to EXPORT :
  1448. -- a TABLE, select TABLE, then click on icon 'SQL->CSV'
  1449. -- a QUERY RESULT, select the SCRIPT text, then click on icon '???->CSV',
  1450. -- example : select the end of this line: SELECT SQLITE_VERSION()
  1451. \n\n-- to use COMMIT and ROLLBACK :
  1452. BEGIN TRANSACTION;
  1453. UPDATE item SET Kg = Kg + 1;
  1454. COMMIT;
  1455. BEGIN TRANSACTION;
  1456. UPDATE item SET Kg = 0;
  1457. select Kg, Description from Item;
  1458. ROLLBACK;
  1459. select Kg, Description from Item;
  1460. \n\n-- to use SAVEPOINT :
  1461. SAVEPOINT remember_Neo; -- create a savepoint
  1462. UPDATE item SET Description = 'Smith'; -- do things
  1463. SELECT ItemNo, Description FROM Item; -- see things done
  1464. ROLLBACK TO SAVEPOINT remember_Neo; -- go back to savepoint state
  1465. SELECT ItemNo, Description FROM Item; -- see all is back to normal
  1466. RELEASE SAVEPOINT remember_Neo; -- free memory
  1467. \n\n-- '.' commands understood:
  1468. -- .once FILENAME Output for the next SQL command only to FILENAME
  1469. -- .import FILE TABLE Import data from FILE into TABLE
  1470. -- (create table only if it doesn't exist, keep existing records)
  1471. .once 'this_file_of_result.txt'
  1472. select ItemNo, Description from item order by ItemNo desc;
  1473. .import 'this_file_of_result.txt' in_this_table
  1474. """
  1475. app.n.new_query_tab("Welcome", welcome_text)
  1476. app.tk_win.mainloop()
  1477. if __name__ == '__main__':
  1478. _main() # create a tkk graphic interface with a main window tk_win