A three-part series on unmixing using NMF.
Part 1: Introduction and Implementation
A three-part series on unmixing using NMF.
Part 1: Introduction and Implementation
The write() and writestr() methods of the ZipFile class in Python's zipfile library allow the addition of an entire file or a single string as a member file within a zip file. However, if a large amount of data will be generated dynamically, these methods do not allow separate data items to be written sequentially to a member file within a zip file.
The StreamableZipfile class in the following code snippet provides this missing capability.
import time
import zipfile
class StreamableZipfile(object):
def __init__(self, zipfile_name, mode='a'):
# Compression type and level are only available in Python
# since versions 3.3 and 3.7, respectively.
self.zf = zipfile.ZipFile(zipfile_name, mode,
compression=zipfile.ZIP_BZIP2, compresslevel=9)
def close(self):
self.zf.close()
def member_file(self, member_filename):
# Creates a ZipInfo object (file) within the zipfile
# and opens it for writing.
self.current_zinfo = zipfile.ZipInfo(filename=member_filename,
date_time=time.localtime(time.time())[:6])
self.current_zinfo.compress_type = self.zf.compression
self.current_zinfo._compresslevel = self.zf.compresslevel
self.current_zinfo.file_size = 0
self.current_handle = self.zf.open(self.current_zinfo, mode='w')
def close_member(self):
self.current_handle.close()
def write(self, str_data):
# Writes the given text to the currently open member.
data = str_data.encode("utf-8")
with self.zf._lock:
self.current_zinfo.file_size = self.current_zinfo.file_size + len(data)
self.current_handle.write(data)
This simple implementation does not include any error checking and always compresses the data with the bzip2 algorithm, using the highest compression level. A more robust and flexible implementation would eliminate these limitations. Modifications are also needed for versions of Python prior to 3.7.
Use of the StreamableZipfile class is illustrated by the following code, which creates a zip file containing two separate files, where lines are written sequentially to each of the files.
import os
zfname = "Test.zip"
if os.path.isfile(zfname):
os.remove(zfname)
# Open the streamable zip file and write several lines to a file within it.
zfile = StreamableZipfile(zfname)
zfile.member_file('file1.txt')
zfile.write("This is file 1, line 1\n")
zfile.write("This is file 1, line 2\n")
zfile.write("This is file 1, line 3\n")
zfile.close_member()
zfile.close()
# Open the same zip file and write lines to another file within it.
zfile = StreamableZipfile(zfname)
zfile.member_file('file2.txt')
zfile.write("This is file 2, line 1\n")
zfile.write("This is file 2, line 2\n")
zfile.write("This is file 2, line 3\n")
zfile.close_member()
zfile.close()
class CsvFile(object):
def __init__(self, filename):
self.fn = filename
self.f = None
self.open()
self.rdr = csv.reader(self.f)
self.headers = next(self.rdr)
def open(self):
if self.f is None:
mode = "rb" if sys.version_info < (3,) else "r"
self.f = open(self.fn, mode)
def reader(self):
return self.rdr
def close(self):
self.rdr = None
self.f.close()
self.f = None
class Database(object):
def __init__(self, conn_info):
self.paramstr = '%s'
self.conn = None
def insert_sql(self, tablename, csvfile):
return "insert into %s (%s) values (%s);" % (
tablename,
",".join(csvfile.headers),
",".join([self.paramstr] * len(csvfile.headers))
)
class PgDb(Database):
def __init__(self, conn_info):
self.db_type = 'p'
import psycopg2
self.paramstr = "%s"
connstr = "host=%(server)s dbname=%(db)s user=%(user)s password=%(pw)s" % conn_info
self.conn = psycopg2.connect(connstr)
class MariaDb(Database):
def __init__(self, conn_info):
self.db_type = 'm'
import pymysql
self.paramstr = "%s"
self.conn = pymysql.connect(host=conn_info["server"], database=conn_info["db"], port=3306, user=conn_info["user"], password=conn_info["pw"])
def postgres_copy(csvfile, db):
curs = db.conn.cursor()
rf = open(csvfile.fn, "rt")
# Read and discard headers
hdrs = rf.readline()
copy_cmd = "copy copy_test from stdin with (format csv)"
curs.copy_expert(copy_cmd, rf)
def simple_copy(csvfile, db):
ins_sql = db.insert_sql('copy_test', csvfile)
curs = db.conn.cursor()
rdr = csvfile.reader()
for line in rdr:
curs.execute(ins_sql, clean_line(line))
db.conn.commit()
def buffer1_copy(csvfile, db, buflines):
ins_sql = db.insert_sql('copy_test', csvfile)
curs = db.conn.cursor()
rdr = csvfile.reader()
eof = False
while True:
b = []
for j in range(buflines):
try:
line = next(rdr)
except StopIteration:
eof = True
else:
b.append(clean_line(line))
if len(b) > 0:
curs.executemany(ins_sql, b)
if eof:
break
db.conn.commit()
def queue_copy(csvfile, db, q_size):
ins_sql = db.insert_sql('copy_test', csvfile)
curs = db.conn.cursor()
rdr = csvfile.reader()
buffer = queue.Queue(maxsize=q_size)
# Have all CSV lines been read?
read_all = threading.Event()
def write_to_db():
while not read_all.is_set() or not buffer.empty():
line = buffer.get()
curs.execute(ins_sql, line)
buffer.task_done()
def get_from_csv():
for line in rdr:
buffer.put(clean_line(line))
read_all.set()
writer = threading.Thread(target=write_to_db)
reader = threading.Thread(target=get_from_csv)
writer.start()
reader.start()
read_all.wait()
reader.join()
writer.join()
db.conn.commit()
def double_buffer_copy(csvfile, db, buf_size):
ins_sql = db.insert_sql('copy_test', csvfile)
curs = db.conn.cursor()
rdr = csvfile.reader()
buf_lock = threading.Condition()
read_all = threading.Event()
# Define an object with a mutable list as a buffer.
class BufObj(object):
def __init__(self, buffer):
self.buffer = buffer
b = BufObj([])
def write_to_db():
while not read_all.is_set() or len(b.buffer) > 0:
buf_lock.acquire()
b2 = b.buffer
b.buffer = []
buf_lock.notify()
buf_lock.release()
curs.executemany(ins_sql, b2)
def get_from_csv():
for line in rdr:
line = clean_line(line)
buf_lock.acquire()
while len(b.buffer) == buf_size:
buf_lock.wait()
b.buffer.append(line)
buf_lock.release()
read_all.set()
writer = threading.Thread(target=write_to_db)
reader = threading.Thread(target=get_from_csv)
writer.start()
reader.start()
read_all.wait()
reader.join()
writer.join()
db.conn.commit()
def double_buffer_bulk_copy(csvfile, db, buf_size):
ins_sql = db.insert_sql('copy_test', csvfile)
curs = db.conn.cursor()
rdr = csvfile.reader()
buf_lock = threading.Condition()
read_all = threading.Event()
class BufObj(object):
def __init__(self, buffer):
self.buffer = buffer
b = BufObj([])
def write_to_db():
while not read_all.is_set() or len(b.buffer) > 0:
buf_lock.acquire()
b2 = b.buffer
b.buffer = []
buf_lock.notify()
buf_lock.release()
curs.executemany(ins_sql, b2)
def get_from_csv():
while not read_all.is_set():
buf_lock.acquire()
while len(b.buffer) > 0:
buf_lock.wait()
for j in range(buf_size):
try:
line = next(rdr)
except StopIteration:
read_all.set()
else:
b.buffer.append(clean_line(line))
buf_lock.release()
writer = threading.Thread(target=write_to_db)
reader = threading.Thread(target=get_from_csv)
writer.start()
reader.start()
read_all.wait()
reader.join()
writer.join()
db.conn.commit()
| Average time / 5 runs (seconds) | ||||
| Algorithm | Buffer size (rows) | Postgres | MariaDB | |
| 1. Postgres copy | 0.41 | |||
| 2. Simple row-by-row copy | 11.95 | 18.86 | ||
| 3. Buffered read/write in one thread | 500 | 10.34 | 8.41 | |
| 4. Producer-consumer, one buffer | 500 | 13.38 | 19.87 | |
| 5. Producer-consumer, two buffers, single-row reading | 500 | 10.57 | 8.78 | |
| 6. Producer-consumer, two buffers, multi-row reading | 500 | 10.57 | 9.83 | |
| 3. Buffered read/write in one thread | 1,500 | 10.17 | 7.66 | |
| 4. Producer-consumer, one buffer | 1,500 | 13.26 | 21.25 | |
| 5. Producer-consumer, two buffers, single-row reading | 1,500 | 10.27 | 7.89 | |
| 6. Producer-consumer, two buffers, multi-row reading | 1,500 | 10.33 | 7.29 | |
| Data Type | Postgres | MariaDB | SQL Server | Firebird | MS-Access | SQLite |
|---|---|---|---|---|---|---|
| Timestamp with time zone | 1184 | |||||
| Timestamp | 1184 | 7 | type 'datetime.datetime' | None | ||
| Datetime | 12 | class 'datetime.datetime' | class 'datetime.datetime' | None | ||
| Date | 1082 | 10 | class 'datetime.date' | type 'datetime.date' | class 'datetime.datetime' | None |
| Time | 1083 | 11 | class 'datetime.time' | type 'datetime.time' | class 'datetime.datetime' | None |
| Boolean | 16 | 16 | class 'bool' | class 'bool' | ||
| Small integer | 21 | 1 | class 'int' | class 'int' | ||
| Integer | 23 | 2 | class 'int' | type 'int' | class 'int' | None |
| Long integer | 20 | 3 | class 'int' | type 'long' | None | |
| Single | 701 | 4 | class 'float' | type 'float' | class 'float' | None |
| Double precision | 701 | 5 | class 'float' | type 'float' | class 'float' | None |
| Decimal | 1700 | 0 | class 'decimal.Decimal' | class 'decimal.Decimal' | None | |
| Currency | 790 | class 'decimal.Decimal' | class 'decimal.Decimal' | |||
| Character | 1042 | class 'str' | type 'str' | class 'str' | None | |
| Character varying | 1043 | 15 | class 'str' | type 'str' | class 'str' | None |
| Text | 25 | class 'str' | type 'str' | class 'str' | None | |
| Binary / BLOB | 17 | 249,250,251,252 | class 'bytearray' | type 'str' | type 'bytearray' | None |
class SomeError(Exception):
def __init__(self, msg):
self.message = msg
def generate_exception():
return 'five' / 'two'
def test_local():
er = None
try:
generate_exception()
except TypeError as er:
pass
except:
er = SomeError("Unexpected exception.")
if er:
print("An exception occurred.")
test_local()
TkLayout package simplifies creation of the visual layout of a Tkinter GUI, the TkPane package simplifies the creation, use, and re-use of Tkinter widgets and groups of Tkinter widgets, particularly those that are used to collect or manage data, and that need to interact with other components of the GUI.TkPane package provides a TkPane class that can be subclassed to create custom 'pane' objects. Several general-purpose panes are provided in the tkpane.lib module. Pane objects are completely stand-alone, with no inherent dependencies on any other components of the GUI. Pane objects have a standard set of methods, however, by which other panes (or other application code) can enable or disable those panes, or clear their displays. Pane objects also have internal lists of callback functions that are automatically executed when their data are changed to become valid, when their data are changed to become invalid, or when the user leaves the pane (via a keyboard focus change or a mouse movement). Dependencies between panes are easily established with the requires() method. For example, the statementrun_button_pane.requires(input_file_pane)
run_button_pane pane will be disabled when the input_file_pane pane contains invalid data, and will be enabled when the input_file_pane pane contains valid data. When one pane enables another, the first pane passes its own data to the second pane.requires() method may be all that is needed in many cases. If more complex interactions are required, however, callback lists can be modified directly to enable other types of inter-pane activation and data sharing.__init()__) must be passed the Tkinter widget within which the frame will be embedded. Typically that parent widget is a frame. This required argument of pane constructors is identical to the argument that must be passed to the 'build' functions of TkLayout objects. Consequently, TkLayout and TkPane objects work together very well, and the combination of the two practically eliminates all of the fiddling with visual and functional aspects of a GUI when using Tkinter directly.tkpane.lib.try:
import Tkinter as tk
except:
import tkinter as tk
import tkpane.lib
import tklayout
import time
# Add a method to the AppLayout class to get a pane: the first child of
# a frame's widgets.
def layout_pane(self, pane_name):
return self.frame_widgets(pane_name)[0]
tklayout.AppLayout.pane = layout_pane
# Create simple sets of data to display in the TableDisplayPane.
ds1_headers = ["Title", "Author", "Published"]
ds1_data = [["Kon-Tiki: Across the Pacific in a Raft", "Thor Heyerdahl", 1950],
["Mawson's Will", "Lennard Bickel", 2000],
["Southern Cross to Pole Star - Tschiffely's Ride", "A. F. Tschiffely", 1933],
["The Hitchhiker's Guide to the Galaxy", "Douglas Adams", 1979]]
ds2_headers = ["row_id","row_number","long_text","some_date","some_number"]
ds2_data = [
["Row 4",4,"Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.","1951-03-19",61.9917173461],
["Row 8",8,"Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus.","1977-07-21",34.5729855806],
["Row 12",12,"DJs flock by when MTV ax quiz prog.","1983-10-12",2.3773967111]
]
def build_message_pane(parent):
tkpane.lib.MessagePane(parent, "Enter user credentials and an output directory.")
def build_table_pane(parent):
tkpane.lib.TableDisplayPane(parent, "This is an example message to accompany the data table that is long enough that it should wrap when the window is resized to a relatively small size.", ds1_headers, ds1_data)
def build_button_pane(parent):
def no_action():
pass
tkpane.lib.OkCancelPane(parent, no_action, no_action)
# Lay out the panes
lo = tklayout.AppLayout()
inp_panes = lo.row_elements(["user_pane", "output_pane"], row_weight=0)
app = lo.column_elements(["message_pane", inp_panes, "table_pane",
"button_pane", "status_pane"],
row_weights=[0,0,1,0,0])
root = tk.Tk()
root.title("Demo of the TkPane Package")
# Use an extra frame within the root element with padding to add extra space
# around the outermost app widgets.
appframe = tk.Frame(root, padx=11, pady=11)
appframe.pack(expand=True, fill=tk.BOTH)
lo.create_layout(appframe, app)
lo.build_elements({"message_pane": build_message_pane,
"user_pane": tkpane.lib.UserPane,
"output_pane": tkpane.lib.OutputDirPane,
"table_pane": build_table_pane,
"button_pane": build_button_pane,
"status_pane": tkpane.lib.StatusProgressPane
})
# Get the pane objects for customization.
user_pane = lo.pane("user_pane")
output_pane = lo.pane("output_pane")
button_pane = lo.pane("button_pane")
status_pane = lo.pane("status_pane")
table_pane = lo.pane("table_pane")
# Require a user name and output directory to be entered for the 'OK' button
# to be enabled.
button_pane.requires(user_pane)
button_pane.requires(output_pane)
# Start the demo application with the 'OK' button disabled.
tkpane.en_or_dis_able_all([button_pane])
# Make the user and output directory panes report their status.
user_pane.status_reporter = status_pane
output_pane.status_reporter = status_pane
# Make the buttons change the data and the status bar.
def ok_click(*args):
status_pane.set_status("OK button clicked.")
time.sleep(0.5)
status_pane.set_status("Working...")
for p in range(10, 110, 10):
status_pane.set_value(p)
root.update_idletasks()
time.sleep(0.5)
status_pane.set_status("Done.")
table_pane.display_data(ds2_headers, ds2_data)
button_pane.set_ok_action(ok_click)
def cancel_click(*args):
status_pane.clear([])
button_pane.set_cancel_action(cancel_click)
# Bind and to the buttons.
root.bind("", ok_click)
root.bind("", cancel_click)
# Run the application
root.mainloop()
TkPane package can be downloaded from the Python Package Index (PyPI), or installed with pip.pip install tkpane
TkPane is available on ReadTheDocs.group_1 ← column_of (topic_selection, date_selection) group_2 ← row_of (group_1, format_options) group_3 ← column_of (header, group_2, output_selection, buttons)
group_3 element then describes the entire GUI, including the nested hierarchy of other elements.TkLayout Python package allows GUIs to be built by describing them in just this way. The key to this process is separation of the steps of describing the interface and creating the interface.AppLayout
object as follows:group_1 = column_elements("topic_selection", "date_selection")
group_2 = row_elements(group_1, "format_options")
group_3 = column_elements("header", group_2, "output_selection", "buttons")
row_elements and column_elements
methods synthesize and return new element names that identify the element groupings that are created—i.e., group_1, group_2, and group_3.
After the structure of the GUI has been described, the frames that implement that structure are created with another method of the AppLayout object, as follows:create_layout(root, group_3)
root argument is a Tkinter top level object or other container that will contain the GUI.create_layout method creates a series of nested frames, as shown in Figure 2 for the GUI illustrated in Figure 1.AppLayout object using that element's name. The GUI implementer can then populate each of these frames with other GUI widgets as appropriate. The AppLayout class will also take a dictionary of element names and element-construction functions, and run those functions to populate each element's enclosing frame. The functions that create the elements for each frame require only the enclosing frame object, and do not need to know anything about how the elements are laid out in relation to one another.pip install tklayout
create table dependencies as
select
tc.table_name as child,
tu.table_name as parent
from
information_schema.table_constraints as tc
inner join information_schema.constraint_table_usage as tu
on tu.constraint_name = tc.constraint_name
where
tc.constraint_type = 'FOREIGN KEY'
and tc.table_name <> tu.table_name;
def dependency_order(dep_list):
rem_tables = list(set([t[0] for t in dep_list] + [t[1] for t in dep_list]))
rem_dep = copy.copy(dep_list)
sortkey = 1
ret_list = []
while len(rem_dep) > 0:
tbls = [tbl for tbl in rem_tables if tbl not in [dep[0] for dep in rem_dep] ]
ret_list.extend([ (tb, sortkey) for tb in tbls ])
rem_tables = [ tbl for tbl in rem_tables if tbl not in tbls ]
rem_dep = [ dep for dep in rem_dep if dep[1] not in tbls ]
sortkey += 1
if len(rem_tables) > 0:
ret_list.extend([(tb, sortkey) for tb in rem_tables])
ret_list.sort(cmp=lambda x,y: cmp(x[1], y[1]))
return [ item[0] for item in ret_list ]
with recursive dep_depth as ( select dep.child, dep.parent, 1 as lvl from dependencies as dep union all select dep.child, dep.parent, dd.lvl + 1 as lvl from dep_depth as dd inner join dependencies as dep on dep.parent = dd.child ) select table_name, table_order from ( select dd.parent as table_name, max(lvl) as table_order from dep_depth as dd group by table_name union select dd.child as table_name, max(lvl) + 1 as level from dep_depth as dd left join dependencies as dp on dp.parent = dd.child where dp.parent is null group by dd.child ) as all_levels;
-- ====================================================================
-- Initialize the tables used to summarize dependency order.
-- Table created:
-- dep_level: A copy of "dependencies" with an additional column
-- to store the level in the hierarchy. The dependency
-- level is set by an execsql counter variable for
-- generality.
-- ====================================================================
-- !x! sub current_level !!$counter_530!!
select
child,
parent,
!!current_level!! as lvl
into
temporary table dep_level
from
dependencies;
-- ====================================================================
-- Create a view to evaluate whether there are any remaining
-- dependencies to evaluate.
-- View created:
-- unprocessed: The number of parent tables whose children are
-- not already listed as parents in the 'dep_level' table.
-- Tables used:
-- dep_level
-- dependencies
-- ====================================================================
create temporary view unprocessed as
select count(distinct dep.child) as unproc
from
dep_level as dl
inner join dependencies as dep on dl.child = dep.parent
where
dl.lvl = (select max(lvl) from dep_level);
-- ====================================================================
-- Define an execsql sub-script to increment the dependency level.
-- ====================================================================
-- !x! begin script add_new_level
-- !x! sub last_level !!current_level!!
-- !x! sub current_level !!$counter_530!!
insert into dep_level
(child, parent, lvl)
select distinct
dep.child,
dl.child as parent,
!!current_level!!
from
dep_level as dl
inner join dependencies as dep on dl.child = dep.parent
where
dl.lvl = !!last_level!!;
-- !x! end script
-- ====================================================================
-- Define and execute an execsql sub-script to increment the dependency
-- level as many times as necessary.
-- ====================================================================
-- !x! begin script add_levels
-- !x! subdata remaining unprocessed
-- !x! execute script add_new_level
-- !x! subdata remaining unprocessed
-- !x! if(is_gt(!!remaining!!, 0)) { execute script add_levels }
-- !x! end script
-- !x! execute script add_levels
-- ====================================================================
-- Convert the dependency levels into a table order.
-- ====================================================================
create temporary table dependency_order as
select
table_name,
table_order
from (
select
dd.parent as table_name,
max(lvl) as table_order
from
dep_level as dd
group by
table_name
union
select
dd.child as table_name,
max(lvl) + 1 as level
from
dep_level as dd
left join dependencies as dp on dp.parent = dd.child
where
dp.parent is null
group by
dd.child
) as all_levels;
db_updated.py [-t table] [-c column] [-u user] server database
import argparse
import psycopg2
import getpass
import os
import os.path
import time
FLAG_FILE = 'db_updated.flag'
def get_user(server_name, database_name):
user_name = raw_input("User name for %s database %s: " % (server_name, database_name))
return user_name
def get_password(server_name, database_name, user_name):
passwd = getpass.getpass("Password for user %s on %s database %s: " % (user_name, server_name, database_name))
return passwd
def clparser():
desc_msg = "Create or update the database flag file if the given database has been recently modified."
parser = argparse.ArgumentParser(description=desc_msg)
parser.add_argument('-u', '--db_user', dest='db_user', action='store')
parser.add_argument('-t', '--table', dest='table', action='append')
parser.add_argument('-c', '--column', dest='column', action='store')
parser.add_argument('server', action='store')
parser.add_argument('database', action='store')
return parser
def main():
parser = clparser()
arg = parser.parse_args()
if not os.path.exists(FLAG_FILE):
open(FLAG_FILE, 'a').close()
if arg.db_user:
db_user = arg.db_user
pw = get_password(arg.server, arg.database, db_user)
conn = psycopg2.connect(host=arg.server, database=arg.database, port=5432, user=unicode(db_user), password=unicode(pw))
else:
conn = psycopg2.connect(host=arg.server, database=arg.database, port=5432)
curs = conn.cursor()
column = arg.column or "rev_time"
if arg.table:
tables = 0
sql = ''
for t in arg.table:
if tables > 0:
sql = sql + " union "
tables += 1
sql = sql + "select max(%s) as latest from %s" % (column, t)
if tables > 1:
sql = "select max(latest) from (%s) as all_t" % sql
sql = sql + ";"
else:
sql = "select max(%s) from auditlog;" % column
curs.execute(sql)
revtime = time.mktime(curs.fetchone()[0].timetuple())
conn.close()
os.utime(FLAG_FILE, (revtime, revtime))
main()
.PHONY: all all: db_updated.flag data_summary.csv db_updated.py -u user_name server_name db_name data_summary.csv: data.csv summary_stats.R Rscript --vanilla summary_stats.R data.csv data_summary.csv data.csv: db_updated.flag get_data.sql execsql.py -tp -u user_name get_data.sql server_name db_name db_updated.flag: db_updated.py -u user_name server_name db_name
class ConsoleUIError(Exception):
def __init__(self, msg):
self.value = msg
def __repr__(self):
return ("ConsoleUIError(%r)" % self.value)
class ConsoleUI(object):
class TkUI(object):
def __init__(self, kill_event, stop_update_event, msg_queue, status_queue, title=None):
self.kill_event = kill_event
self.stop_update_event = stop_update_event
self.msg_queue = msg_queue
self.status_queue = status_queue
import Tkinter as tk1
import ttk as ttk1
self.win = tk1.Toplevel()
self.status_msg = tk1.StringVar()
self.status_msg.set('')
self.win.title(title if title else "execsql console")
console_frame = ttk1.Frame(master=self.win, padding="2 2 2 2")
console_frame.grid(column=0, row=0, sticky=tk1.NSEW)
self.textarea = tk1.Text(console_frame, width=100, height=25, wrap='none')
# Status bar
statusframe = ttk1.Frame(master=self.win)
statusbar = ttk1.Label(statusframe, text='', textvariable=self.status_msg,
relief=tk1.RIDGE, anchor=tk1.W)
statusbar.pack(side=tk1.BOTTOM, fill=tk1.X)
statusframe.grid(column=0, row=1, sticky=tk1.EW)
# Scrollbars
vscroll = tk1.Scrollbar(console_frame, orient="vertical", command=self.textarea.yview)
hscroll = tk1.Scrollbar(console_frame, orient="horizontal", command=self.textarea.xview)
self.textarea.configure(yscrollcommand=vscroll.set)
self.textarea.configure(xscrollcommand=hscroll.set)
self.textarea.grid(column=0, row=0, sticky=tk1.NSEW)
vscroll.grid(column=1, row=0, sticky=tk1.NS)
hscroll.grid(column=0, row=2, sticky=tk1.EW)
# Allow resizing
self.win.columnconfigure(0, weight=1)
self.win.rowconfigure(0, weight=1)
console_frame.columnconfigure(0, weight=1)
console_frame.rowconfigure(0, weight=1)
# Kill on window close
self.win.protocol("WM_DELETE_WINDOW", self.kill)
# Display and center the window
self.win.update_idletasks()
m = re.match("(\d+)x(\d+)([-+]\d+)([-+]\d+)", self.win.geometry())
wwd = int(m.group(1))
wht = int(m.group(2))
swd = self.win.winfo_screenwidth()
sht = self.win.winfo_screenheight()
xpos = (swd/2) - (wwd/2)
ypos = (sht/2) - (wht/2)
self.win.geometry("%dx%d+%d+%d" % (wwd, wht, xpos, ypos))
self.win.grab_set()
self.win._root().withdraw()
self.update_id = self.win.after(200, self.update)
self.win.wait_window(self.win)
def kill(self):
if self.update_id:
self.win.after_cancel(self.update_id)
self.update_id = None
self.win.destroy()
self.win.update_idletasks()
def update(self):
self.update_id = None
while not self.msg_queue.empty():
msg = self.msg_queue.get(False)
self.textarea.insert('end', msg)
self.textarea.see('end')
self.msg_queue.task_done()
while not self.status_queue.empty():
msg = self.status_queue.get(False)
self.status_msg.set(msg)
if self.kill_event.is_set():
self.kill()
else:
if not self.stop_update_event.is_set():
self.update_id = self.win.after(200, self.update)
def __init__(self, title=None):
self.title = title
self.msg_queue = Queue.Queue()
self.status_queue = Queue.Queue()
self.kill_event = threading.Event()
self.stop_update_event = threading.Event()
self.consolethread = None
self.update_id = None
# Start the local event loop in a thread.
def openconsole():
self.active = True
self.ui = self.TkUI(self.kill_event, self.stop_update_event, self.msg_queue,
self.status_queue, self.title)
# Deallocate the Tk object here to avoid the "main thread is not in main loop" error.
self.ui = None
self.consolethread = threading.Thread(target=openconsole)
self.consolethread.start()
def write(self, msg):
self.active = self.consolethread and self.consolethread.is_alive()
if not self.active:
raise ConsoleUIError(msg)
self.msg_queue.put(msg)
def write_status(self, msg):
self.active = self.consolethread and self.consolethread.is_alive()
if not self.active:
raise ConsoleUIError(msg)
self.status_queue.put(msg)
def deactivate(self):
self.kill_event.set()
if self.consolethread and self.consolethread.is_alive():
self.consolethread.join()
self.active = False
def wait_for_user_quit(self):
self.stop_update_event.set()
if self.consolethread and self.consolethread.is_alive():
self.consolethread.join()
self.active = False
execsql is a Python application that reads a text file of SQL commands and executes them against a database. The supported databases are:execsql also implements a number of metacommands that allow:execsql allows variables to be defined and used to dynamically modify SQL code and metacommands. An INCLUDE metacommand can be used to modularize SQL scripts and facilitate code re-use. Variables can be used to parameterize included scripts. Conditional inclusion of scripts can be used to implement loops. Automatically incrementing counter variables can be used to control loops or generate unique values to be used in input or output.execsql is fundamentally a command-line application that can be used to automate database operations, either by itself or as part of a toolchain that may include other steps to either pre-process data before loading, or post-process or analyze data that are extracted from a database. However, execsql also has the ability to present data to the user in dialog boxes, request several types of input from the user either on the terminal or in GUI dialogs, and display messages to the user either on the terminal or in a GUI console. These features allow flexible interactive applications to be created.execsql is documentation of all data operations. The use of SQL scripts, rather than ad-hoc interactive operations in a GUI, is key to meeting that goal. In addition, execsql logs all usage information, including databases used, script files executed (including script file version information), variable assignments, user input, and errors. Chain-of-custody procedures are used in some disciplines to ensure traceability from collection through (typically) laboratory analysis. However, no formal chain of custody procedures ordinarily are applied to data during or after entry into a database. SQL script files and execsql log files can be used to produce a complete record of data operations so that traceability of data can be extended into the database environment.execsql and its documentation are available from the Python Package Index (PyPI). Note that the search functionality in PyPI and in pip search is broken, and will return links to obsolete versions of execsql that are no longer available. The direct link will take you to the latest available version.
import re
class CsvDiagError(Exception):
def __init__(self, msg):
self.value = msg
def __str__(self):
return self.value
class CsvLine():
escchar = u"\\"
def __init__(self, line_text):
self.text = line_text
self.delim_counts = {}
self.item_errors = [] # A list of error messages.
def __str__(self):
return u"; ".join([u"Text: <<%s>>" % self.text, \
u"Delimiter counts: <<%s>>" % ", ".join([u"%s: %d" % (k, self.delim_counts[k]) for k in self.delim_counts.keys()]) ])
def count_delim(self, delim):
# If the delimiter is a space, consider multiple spaces to be equivalent
# to a single delimiter, split on the space(s), and consider the delimiter
# count to be one fewer than the items returned.
if delim == u" ":
self.delim_counts[delim] = max(0, len(re.split(r' +', self.text)) - 1)
else:
self.delim_counts[delim] = self.text.count(delim)
def delim_count(self, delim):
return self.delim_counts[delim]
def _well_quoted(self, element, qchar):
# A well-quoted element has either no quotes, a quote on each end and none
# in the middle, or quotes on both ends and every internal quote is either
# doubled or escaped.
# Returns a tuple of three booleans; the first indicates whether the element is
# well-quoted, the second indicates whether the quote character is used
# at all, and the third indicates whether the escape character is used.
if qchar not in element:
return (True, False, False)
if len(element) == 0:
return (True, False, False)
if element[0] == qchar and element[-1] == qchar and qchar not in element[1:-1]:
return (True, True, False)
# The element has quotes; if it doesn't have one on each end, it is not well-quoted.
if not (element[0] == qchar and element[-1] == qchar):
return (False, True, False)
e = element[1:-1]
# If there are no quotes left after removing doubled quotes, this is well-quoted.
if qchar not in e.replace(qchar+qchar, u''):
return (True, True, False)
# if there are no quotes left after removing escaped quotes, this is well-quoted.
if qchar not in e.replace(self.escchar+qchar, u''):
return (True, True, True)
return (False, True, False)
def record_format_error(self, pos_no, errmsg):
self.item_errors.append(u"%s in position %d." % (errmsg, pos_no))
def items(self, delim, qchar):
# Parses the line into a list of items, breaking it at delimiters that are not
# within quoted stretches. (This is a almost CSV parser, for valid delim and qchar,
# except that it does not eliminate quote characters or reduce escaped quotes.)
self.item_errors = []
if qchar is None:
if delim is None:
return self.text
else:
if delim == u" ":
return re.split(r' +', self.text)
else:
return self.text.split(delim)
elements = [] # The list of items on the line that will be returned.
eat_multiple_delims = delim == u" "
# States of the FSM:
# _IN_QUOTED: An opening quote has been seen, but no closing quote encountered.
# Actions / transition:
# quote: save char in escape buffer / _ESCAPED
# esc_char : save char in escape buffer / _ESCAPED
# delimiter: save char in element buffer / _IN_QUOTED
# other: save char in element buffer / _IN_QUOTED
# _ESCAPED: An escape character has been seen while _IN_QUOTED (and is in the escape buffer).
# Actions / transitions
# quote: save escape buffer in element buffer, empty escape buffer,
# save char in element buffer / _IN_QUOTED
# delimiter: save escape buffer in element buffer, empty escape buffer,
# save element buffer, empty element buffer / _BETWEEN
# other: save escape buffer in element buffer, empty escape buffer,
# save char in element buffer / _IN_QUOTED
# _QUOTE_IN_QUOTED: A quote has been seen while _IN_QUOTED (and is in the escape buffer).
# Actions / transitions
# quote: save escape buffer in element buffer, empty escape buffer,
# save char in element buffer / _IN_QUOTED
# delimiter: save escape buffer in element buffer, empty escape buffer,
# save element buffer, empty element buffer / _DELIMITED
# other: save escape buffer in element buffer, empty escape buffer,
# save char in element buffer / _IN_QUOTED
# (An 'other' character in this position represents a bad format:
# a quote not followed by another quote or a delimiter.)
# _IN_UNQUOTED: A non-delimiter, non-quote has been seen.
# Actions / transitions
# quote: save char in element buffer / _IN_UNQUOTED
# (This represents a bad format.)
# delimiter: save element buffer, empty element buffer / _DELIMITED
# other: save char in element buffer / _IN_UNQUOTED
# _BETWEEN: Not in an element, and a delimiter not seen. This is the starting state,
# and the state following a closing quote but before a delimiter is seen.
# Actions / transition:
# quote: save char in element buffer / _IN_QUOTED
# delimiter: save element buffer, empty element buffer / _DELIMITED
# (The element buffer should be empty, representing a null data item.)
# other: save char in element buffer / _IN_UNQUOTED
# _DELIMITED: A delimiter has been seen while not in a quoted item.
# Actions / transition:
# quote: save char in element buffer / _IN_QUOTED
# delimiter: if eat_multiple: no action / _DELIMITED
# if not eat_multiple: save element buffer, empty element buffer / _DELIMITED
# other: save char in element buffer / _IN_UNQUOTED
# At end of line: save escape buffer in element buffer, save element buffer. For a well-formed
# line, these should be empty, but they may not be.
#
# Define the state constants, which will also be used as indexes into an execution vector.
_IN_QUOTED, _ESCAPED, _QUOTE_IN_QUOTED, _IN_UNQUOTED, _BETWEEN, _DELIMITED = range(6)
#
# Because of Python 2.7's scoping rules:
# * The escape buffer and current element are defined as mutable objects that will have their
# first elements modified, rather than as string variables. (Python 2.x does not allow
# modification of a variable in an enclosing scope that is not the global scope, but
# mutable objects like lists can be altered. Another approach would be to implement this
# as a class and use instance variables.)
# * The action functions return the next state rather than assigning it directly to the 'state' variable.
esc_buf = [u'']
current_element = [u'']
def in_quoted():
if c == self.escchar:
esc_buf[0] = c
return _ESCAPED
elif c == qchar:
esc_buf[0] = c
return _QUOTE_IN_QUOTED
else:
current_element[0] += c
return _IN_QUOTED
def escaped():
if c == delim:
current_element[0] += esc_buf[0]
esc_buf[0] = u''
elements.append(current_element[0])
current_element[0] = u''
return _BETWEEN
else:
current_element[0] += esc_buf[0]
esc_buf[0] = u''
current_element[0] += c
return _IN_QUOTED
def quote_in_quoted():
if c == qchar:
current_element[0] += esc_buf[0]
esc_buf[0] = u''
current_element[0] += c
return _IN_QUOTED
elif c == delim:
current_element[0] += esc_buf[0]
esc_buf[0] = u''
elements.append(current_element[0])
current_element[0] = u''
return _DELIMITED
else:
current_element[0] += esc_buf[0]
esc_buf[0] = u''
current_element[0] += c
self.record_format_error(i+1, "Unexpected character following a closing quote")
return _IN_QUOTED
def in_unquoted():
if c == delim:
elements.append(current_element[0])
current_element[0] = u''
return _DELIMITED
else:
current_element[0] += c
return _IN_UNQUOTED
def between():
if c == qchar:
current_element[0] += c
return _IN_QUOTED
elif c == delim:
elements.append(current_element[0])
current_element[0] = u''
return _DELIMITED
else:
current_element[0] += c
return _IN_UNQUOTED
def delimited():
if c == qchar:
current_element[0] += c
return _IN_QUOTED
elif c == delim:
if not eat_multiple_delims:
elements.append(current_element[0])
current_element[0] = u''
return _DELIMITED
else:
current_element[0] += c
return _IN_UNQUOTED
# Functions in the execution vector must be ordered identically to the
# indexes represented by the state constants.
exec_vector = [ in_quoted, escaped, quote_in_quoted, in_unquoted, between, delimited ]
# Set the starting state.
state = _BETWEEN
# Process the line of text.
for i, c in enumerate(self.text):
state = exec_vector[state]()
# Process the end-of-line condition.
if len(esc_buf[0]) > 0:
current_element[0] += esc_buf[0]
if len(current_element[0]) > 0:
elements.append(current_element[0])
return elements
def well_quoted_line(self, delim, qchar):
# Returns a tuple of boolean, int, and boolean, indicating: 1) whether the line is
# well-quoted, 2) the number of elements for which the quote character is used,
# and 3) whether the escape character is used.
wq = [ self._well_quoted(el, qchar) for el in self.items(delim, qchar) ]
return ( all([b[0] for b in wq]), sum([b[1] for b in wq]), any([b[2] for b in wq]) )
def diagnose_delim(linestream, possible_delimiters=None, possible_quotechars=None):
# Returns a tuple consisting of the delimiter, quote character, and escape
# character for quote characters within elements of a line. All may be None.
# If the escape character is not None, it will be u"\".
# Arguments:
# * linestream: An iterable file-like object with a 'next()' method that returns lines of text
# as bytes or unicode.
# * possible_delimiters: A list of single characters that might be used to separate items on
# a line. If not specified, the default consists of tab, comma, semicolon, and vertical rule.
# If a space character is included, multiple space characters will be treated as a single
# delimiter--so it's best if there are no missing values on space-delimited lines, though
# that is not necessarily a fatal flaw unless there is a very high fraction of missing values.
# * possible_quotechars: A list of single characters that might be used to quote items on
# a line. If not specified, the default consists of single and double quotes.
if not possible_delimiters:
possible_delimiters = [ u"\t", u",", u";", u"|"]
if not possible_quotechars:
possible_quotechars = [ u'"', u"'"]
lines = []
for i in range(100):
try:
ln = linestream.next()
except StopIteration:
break
except:
raise
while len(ln) > 0 and ln[-1] in (u"\n", u"\r"):
ln = ln[:-1]
if len(ln) > 0:
lines.append(CsvLine(ln))
if len(lines) == 0:
raise CsvDiagError(u"CSV diagnosis error: no lines read")
for ln in lines:
for d in possible_delimiters:
ln.count_delim(d)
# For each delimiter, find the minimum number of delimiters found on any line, and the number of lines
# with that minimum number
delim_stats = {}
for d in possible_delimiters:
dcounts = [ ln.delim_count(d) for ln in lines ]
min_count = min(dcounts)
delim_stats[d] = (min_count, dcounts.count(min_count))
# Remove delimiters that were never found.
for k in delim_stats.keys():
if delim_stats[k][0] == 0:
del(delim_stats[k])
def all_well_quoted(delim, qchar):
# Returns a tuple of boolean, int, and boolean indicating: 1) whether the line is
# well-quoted, 2) the total number of lines and elements for which the quote character
# is used, and 3) the escape character used.
wq = [ l.well_quoted_line(delim, qchar) for l in lines ]
return ( all([b[0] for b in wq]), sum([b[1] for b in wq]), CsvLine.escchar if any([b[2] for b in wq]) else None )
def eval_quotes(delim):
# Returns a tuple of the form to be returned by 'diagnose_delim()'.
ok_quotes = {}
for q in possible_quotechars:
allwq = all_well_quoted(delim, q)
if allwq[0]:
ok_quotes[q] = (allwq[1], allwq[2])
if len(ok_quotes) == 0:
return (delim, None, None) # No quotes, no escapechar
else:
max_use = max([ v[0] for v in ok_quotes.values() ])
if max_use == 0:
return (delim, None, None)
# If multiple quote characters have the same usage, return (arbitrarily) the first one.
for q in ok_quotes.keys():
if ok_quotes[q][0] == max_use:
return (delim, q, ok_quotes[q][1])
if len(delim_stats) == 0:
# None of the delimiters were found. Some other delimiter may apply,
# or the input may contain a single value on each line.
# Identify possible quote characters.
return eval_quotes(None)
else:
if len(delim_stats) > 1:
# If one of them is a space, prefer the non-space
if u" " in delim_stats.keys():
del(delim_stats[u" "])
if len(delim_stats) == 1:
return eval_quotes(delim_stats.keys()[0])
# Assign weights to the delimiters. The weight is the square of the minimum number of delimiters
# on a line times the number of lines with that delimiter.
delim_wts = {}
for d in delim_stats.keys():
delim_wts[d] = delim_stats[d][0]**2 * delim_stats[d][1]
# Evaluate quote usage for each delimiter, from most heavily weighted to least.
# Return the first good pair where the quote character is used.
delim_order = sorted(delim_wts, key=delim_wts.get, reverse=True)
for d in delim_order:
quote_check = eval_quotes(d)
if quote_check[0] and quote_check[1]:
return quote_check
# There are no delimiters for which quotes are OK.
return (delim_order[0], None, None)
# Should never get here
raise CsvDiagError(u"CSV diagnosis error: an untested set of conditions are present")