# -*- coding: utf-8 -*-
# KrySA - Statistical analysis for rats
# Version: 0.3.7
# Copyright (C) 2016, KeyWeeUsr(Peter Badida) <keyweeusr@gmail.com>
# License: GNU GPL v3.0, More info in LICENSE.txt
from kivy.config import Config
import os
# local & RTD docs fix
if not any(['BUILDDIR' in os.environ, 'READTHEDOCS' in os.environ]):
Config.set('graphics', 'window_state', 'maximized')
import numpy
import scipy
import re
import json
import math
import string
import sqlite3
import os.path as op
from kivy.app import App
from kivy.clock import Clock
from functools import partial
from dropdown import DropDown
from kivy.logger import Logger
from kivy.uix.popup import Popup
from kivy.uix.image import Image
from kivy.uix.label import Label
from time import gmtime, strftime
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.textinput import TextInput
from kivy.uix.scrollview import ScrollView
from kivy.uix.gridlayout import GridLayout
from kivy.uix.recycleview import RecycleView
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.stencilview import StencilView
from kivy.uix.tabbedpanel import TabbedPanelItem
from kivy.uix.treeview import TreeView, TreeViewLabel
from kivy.uix.recyclegridlayout import RecycleGridLayout
from kivy.properties import StringProperty, ObjectProperty, \
BooleanProperty, ListProperty
from tasks import Task
from tasks.basic import Basic
from tasks.avgs import Avgs
from tasks.manipulate import Manipulate
[docs]class ResultGrid(GridLayout):
'''A black gridlayout, together with :mod:`main.Wrap` makes a table
container for results that need a table.
.. versionadded:: 0.3.2
'''
[docs]class Wrap(Label):
'''A white label with automatically wrapped text.
.. versionadded:: 0.3.2
'''
background_color = ListProperty([0, 0, 0, 0])
[docs]class PageBox(BoxLayout):
'''A layout that includes Page widget together with transparent separator.
It's used for adding new results from Tasks.
.. versionadded:: 0.2.0
'''
def __init__(self, **kwargs):
super(PageBox, self).__init__(**kwargs)
self.add_widget = self.ids.page.add_widget
[docs]class PaperLabel(Label):
'''A label with visual properties as a paper sheet.
.. versionadded:: 0.2.0
'''
[docs]class ErrorPop(Popup):
'''An error popup to let user know something is missing or typed wrong
when console is disabled.
.. versionadded:: 0.1.2
'''
message = StringProperty('')
def __init__(self, **kw):
self.message = kw.get('msg', '')
super(ErrorPop, self).__init__(**kw)
[docs]class NewDataValue(BoxLayout):
'''A layout handling the behavior of inputs and button for each new
value in :ref:`data`.
.. versionadded:: 0.1.4
'''
def __init__(self, **kw):
self.app = App.get_running_app()
super(NewDataValue, self).__init__(**kw)
self.filter = kw['filter']
[docs]class NewDataColumn(BoxLayout):
'''A layout handling the behavior of type, values(``NewDataValue``) and
some buttons for each new column in :ref:`data`.
.. versionadded:: 0.1.4
'''
def __init__(self, **kw):
self.app = App.get_running_app()
super(NewDataColumn, self).__init__(**kw)
@staticmethod
[docs] def free(items):
'''Frees all locked cells in the column except a column type. If
a wrong type is used, removing the whole column is necessary.
(protection against corrupting :ref:`sqlite`)
.. versionadded:: 0.1.4
'''
for item in items:
if hasattr(item, 'disabled'):
try:
if item.parent.ids['coltype'] == item:
continue
except:
item.disabled = False
else:
# enable children
for i in item:
try:
if i.parent.ids['coltype'] == item:
continue
except:
i.disabled = False
[docs] def checklock(self, disable, coltype, check, *args):
'''Disables all cells in the column, then check them against a list
of strings that could be used to corrupt :ref:`sqlite` . If the check
is done without an error, another check is made to protect against
using an empty string ``''`` as a value, which if used inappropriately
results in a crash.
.. versionadded:: 0.1.4
'''
msg = 'Please remove any SQL keyword present in the column!'
for item in disable:
if hasattr(item, 'disabled'):
item.disabled = True
else:
# disable children
for i in item:
i.disabled = True
# check for any sql keywords and correct values
for item in check:
try:
for keyword in self.app.sql_blacklist:
if keyword in item.text.upper():
error = ErrorPop(msg=msg)
error.open()
return
except AttributeError:
for i in item.children:
for keyword in self.app.sql_blacklist:
if keyword in i.ids.value.text.upper():
error = ErrorPop(msg=msg)
error.open()
return
try:
if not coltype.text == 'TEXT':
float(i.ids.value.text)
except ValueError:
if coltype.text == 'REAL':
i.ids.value.text = '0.0'
elif coltype.text == 'INTEGER':
i.ids.value.text = '0'
[docs] def paste(self, values, sep):
'''Paste a value(s) from a user's clipboard as a column values. A user
can choose what kind of separator was used on the values, for example::
1 2 3 4 5 # (space)
1\\t2\\t3\\t4\\t # (tab)
1\\n2\\n3\\n4\\n # Unix-like new line character (<enter>/<return>)
If in doubt and your values were copied from a column (e.g.
spreadsheet), use `OS default`, which will choose between ``\\n``
(Unix-like) or ``\\r\\n`` (Windows) new line separators.
.. versionadded:: 0.3.4
'''
if sep == 'space':
values = values.split(' ')
elif sep == 'OS default':
values = values.split(os.linesep)
else:
values = values.split(sep[1:])
for val in values:
v = NewDataValue(filter=self.filter)
v.ids.value.text = val
self.ids.vals.add_widget(v)
self.ids.vals.parent.scroll_to(self.ids.vals.children[0])
[docs]class NewDataLayout(BoxLayout):
'''A layout handling the behavior of ``NewDataColumn`` and some inputs for
each new value in :ref:`data`.
.. versionadded:: 0.1.3
'''
def __init__(self, **kw):
self.app = App.get_running_app()
super(NewDataLayout, self).__init__(**kw)
[docs]class CreateWizard(Popup):
'''A popup handling the behavior for creating a new :ref:`data`,
i.e a wizard.
.. versionadded:: 0.1.3
'''
run = ObjectProperty(None)
def __init__(self, **kw):
super(CreateWizard, self).__init__(**kw)
self.run = kw.get('run')
wdg = kw.get('wdg')
if wdg:
self.ids.container.add_widget(wdg)
[docs]class Dialog(Popup):
'''A dialog handling the behavior for creating or opening files e.g.
:ref:`project` or :ref:`data`.
.. versionadded:: 0.1.0
'''
confirm = StringProperty('')
run = ObjectProperty(None)
dirs = BooleanProperty(False)
project = BooleanProperty(False)
filter = ListProperty([])
def __init__(self, **kw):
super(Dialog, self).__init__(**kw)
self.confirm = kw.get('confirm', '')
self.run = kw.get('run')
self.dirs = kw.get('dirs', False)
self.project = kw.get('project', False)
if self.project:
self.ids.name.hint_text = 'Project.krysa'
self.filter = [lambda folder, fname: fname.endswith('.krysa')]
else:
self.ids.name.hint_text = 'example.sqlite'
self.filter = [lambda folder, fname: fname.endswith('.sqlite')]
[docs]class SideItem(BoxLayout):
'''Supposed to be a part of settings, most likely will be removed/replaced.
.. versionadded:: 0.1.0
'''
[docs]class TableItem(TextInput):
'''An item handling the behavior or each separate value in the
:mod:`main.Table` such as updating/editing values in :ref:`data`.
.. versionadded:: 0.1.0
'''
def __init__(self, **kwargs):
super(TableItem, self).__init__(**kwargs)
self.bind(focus=self.on_focus)
[docs] def on_focus(self, widget, focused):
'''Makes sure the unconfirmed value is discarded e.g. when clicked
outside of the widget.
'''
if not focused:
self.text = self.old_text
[docs] def update_value(self, txt, *args):
'''On ``<enter>`` (``return``) key updates the values
:mod:`main.TableItem.text` and :mod:`main.TableItem.old_text` in
:mod:`main.Table`.
.. versionadded:: 0.1.0
'''
data = []
cols = self.cols - 1
for i in self.origin.data:
if 'cell' in i:
data.append(i)
chunks = [data[x:x + cols] for x in xrange(0, len(data), cols)]
orig_type = type(chunks[self.r][self.c - 1])
place = self.cols * (self.r + 1) - (self.cols - self.c)
self.origin.data[place]['text'] = txt
self.origin.data[place]['old_text'] = txt
self.origin.refresh_from_data()
[docs]class Table(ScrollView):
'''A view handling the behavior of the inputs from :ref:`sqlite`. Separates
the values from :ref:`sqlite` according to its :ref:`data`'s column types
into three Python categories - `int`, `float` or `unicode` and assigns
an alphabetic order for each column together with row number to each value.
.. versionadded:: 0.1.0
'''
# use with ....add_widget(Table(max_cols=3, max_rows=3))
# Grid -> Scroll, grid as container - better for sizing and placing
# set "address" for table pos in grid to item
number_size = (30, 30)
default_size = (100, 30)
def __init__(self, **kw):
app = App.get_running_app()
self.rv = RecycleView(bar_width='10dp',
scroll_type=['bars', 'content'])
container = RecycleGridLayout(size_hint=(None, None))
container.viewclass = TableItem
container.bind(minimum_size=container.setter('size'))
self.max_rows = kw.get('max_rows', 1)
self.rows = self.max_rows + 1
self.max_cols = kw.get('max_cols', 1)
self.cols = self.max_cols + 1
self.values = kw.get('values', [])
self.labels = kw.get('labels')
self.types = kw.get('types')
container.rows = self.rows
container.cols = self.cols
ltr = [' (' + letter + ')' for letter in self.get_letters()]
self.rv.add_widget(container)
if 'item_size' not in kw:
item_size = self.default_size
super(Table, self).__init__(**kw)
for r in range(self.rows):
for c in range(self.cols):
if r == 0:
if c == 0:
self.rv.data.append({'text': u'', 'disabled': True,
'size': self.number_size,
'origin': self.rv})
else:
self.rv.data.append(
{
'text': self.labels[c - 1] + ltr[c - 1],
'_text': self.labels[c - 1],
'disabled': True,
'cell': 'label' + str(c - 1),
'type': type(u''),
'size': self.default_size,
'origin': self.rv
}
)
else:
if c == 0:
self.rv.data.append({'text': unicode(r),
'disabled': True,
'size': self.number_size,
'origin': self.rv})
else:
filter = app.root.simple_chars
try:
val = self.values.pop(0)
text_type = type(val)
filter_val = repr(text_type)[7:-2]
if filter_val in ['int', 'float']:
filter = filter_val
except IndexError:
print 'values < space'
val = '.'
if 'e+' in str(val) or 'e-' in str(val):
val = '{0:.10f}'.format(val)
if self.types:
if self.types[c - 1] == 'INTEGER':
filter = 'int'
text_type = type(1)
elif self.types[c - 1] == 'REAL':
filter = 'float'
text_type = type(1.1)
self.rv.data.append(
{
'text': unicode(val),
'old_text': unicode(val),
'disabled': False,
'cell': self.labels[c - 1] + str(r),
'r': r,
'rows': self.rows,
'c': c,
'cols': self.cols,
'type': text_type,
'size': self.default_size,
'input_filter': filter,
'origin': self.rv
}
)
if self.values:
raise Exception('Not enough space for all values! \
Increase rows/cols!')
self.add_widget(self.rv)
[docs] def get_letters(self):
'''Gets a list of letters the same length as :ref:`data`'s columns.
.. versionadded:: 0.1.0
'''
letters = [chr(letter + 65) for letter in range(26)]
result = []
label = []
cols = range(self.cols + 1)
for i in cols:
if i != 0:
while i:
i, rem = divmod(i - 1, 26)
label[:0] = letters[rem]
result.append(''.join(label))
label = []
return result
[docs] def lock(self, disabled=True):
'''docs
.. versionadded:: 0.1.0
'''
for i in self.rv.data:
if 'cell' in i and 'label' not in i['cell']:
i['disabled'] = disabled
self.rv.refresh_from_data()
[docs] def clean(self, *args):
'''Removes all data from :mod:`main.Table`
.. versionadded:: 0.1.0
'''
self.rv.data = []
[docs]class ProcessFlow(BoxLayout, StencilView):
'''A canvas on which will be displayed actions for each :ref:`data` related
to them, such as used tasks connected with result of the tasks.
.. versionadded:: 0.1.0
(Not implemented yet)
'''
def __init__(self, **kw):
super(ProcessFlow, self).__init__(**kw)
app = App.get_running_app()
self.texture = Image(source=app.path + '/data/grid.png').texture
self.texture.wrap = 'repeat'
[docs]class Body(FloatLayout):
'''The main layout for the application. It handles menu values, their
appropriate functions, filtering of user's input and functions for
accessing :ref:`sqlite` in :mod:`main.Table`.
.. versionadded:: 0.1.0
'''
def __init__(self, **kw):
self.app = App.get_running_app()
self.tables = []
self.app.menu = {'file': (['New...', self.new],
['Open Project', self.open_project],
['Close Project', self.close_project],
['Save Project', self.save_project],
['Import Data', self.import_data],
['Export Data', self.export_data],
['_Recent Projects', self.test],
['Exit', self.app.stop],),
'edit': (['_Undo', self.test],
['_Redo', self.test],
['_Cut', self.test],
['_Copy', self.test],
['_Paste', self.test],
['_Delete', self.test],
['_Find...', self.test],
['_Replace...', self.test],
['_Protect Data', self.test],
['_Go To...', self.test],),
'tasks': (['Basic', self.basic],
['_Averages', self.avgs],
['Manipulate', self.manipulate],),
'help': (['_KrySA Help', self.test],
['_Getting Started Tutorial', self.test],
['About KrySA', self.about],),
}
super(Body, self).__init__(**kw)
[docs] def new(self, button, *args):
'''Opens a submenu for ``New`` menu.
.. versionadded:: 0.1.0
'''
d = DropDown(allow_sides=True, auto_width=False)
buttons = []
# Project saving works, but implementing ProcessFlow is still ToDo
buttons.append(SizedButton(text='Project'))
buttons[0].bind(on_release=self._new_project)
buttons.append(SizedButton(text='Data'))
buttons[1].bind(on_release=self._new_data)
for b in buttons:
d.add_widget(b)
d.open(button)
[docs] def _new_project(self, *args):
'''Closes already opened :ref:`project` if available and opens a dialog
for creating a new one.
.. versionadded:: 0.1.2
'''
self.close_project()
self.savedlg = Dialog(title='New Project',
confirm='Save',
run=self._save_project,
dirs=True,
project=True)
self.savedlg.open()
[docs] def _new_data(self, *args):
'''Opens a wizard for creating a new :ref:`data` if a :ref:`project` is
available or shows a warning if it doesn't exist.
.. versionadded:: 0.1.3
'''
if not self.app.project_exists:
error = ErrorPop(msg='No project exists!')
error.open()
return
widget = NewDataLayout()
self.wiz_newdata = CreateWizard(title='New Data', wdg=widget,
run=partial(self._save_data, widget))
self.wiz_newdata.open()
[docs] def _save_data(self, wizard, *args):
'''Gets data from the wizard, puts them into :mod:`main.Table` and exports them
into :ref:`sqlite`.
.. versionadded:: 0.1.4
'''
labels = []
types = []
values = []
table_name = wizard.ids.table_name.text
if not len(table_name):
error = ErrorPop(msg='Please name your data!')
error.open()
return
if not len(wizard.ids.columns.children):
error = ErrorPop(msg='There must be at least one column!')
error.open()
return
for child in reversed(wizard.ids.columns.children):
child.ids.checklock.dispatch('on_release')
for child in reversed(wizard.ids.columns.children):
if child.ids.colname.text not in labels:
lbl = re.findall(r'([a-zA-Z0-9])', child.ids.colname.text)
if not len(lbl):
error = ErrorPop(msg='Use only a-z A-Z 0-9 characters!')
error.open()
return
labels.append(''.join(lbl))
else:
error = ErrorPop(msg='Each column must have a unique name!')
error.open()
return
types.append(child.ids.coltype.text)
column_values = []
for value_wdg in reversed(child.ids.vals.children):
column_values.append(value_wdg.ids.value.text)
values.append(column_values)
max_cols = len(values)
max_rows = len(max(values, key=len))
# create table in KrySA, then export
tabletab = TabbedPanelItem(text=table_name)
self.ids.tabpanel.add_widget(tabletab)
_values = values[:]
values = []
for i in range(max_rows):
for j in range(max_cols):
try:
values.append(_values[j][i])
except IndexError:
if types[j] == 'INTEGER':
values.append(0)
elif types[j] == 'REAL':
values.append(0.0)
else:
values.append(u'')
self.tables.append((table_name, Table(max_cols=max_cols,
max_rows=max_rows, pos=self.pos,
size=self.size, values=values,
labels=labels, types=types)))
tabletab.content = self.tables[-1][1]
# export to data.sqlite in <project>/data directory
data = op.join(self.app.project_dir, 'data')
self._export_data([data], 'data.sqlite')
self.wiz_newdata.dismiss()
def open_project(self, *args):
self.close_project()
self.opendlg = Dialog(title='Open Project',
confirm='Open',
run=self._open_project,
project=True)
self.opendlg.open()
[docs] def _open_project(self, selection, *args):
'''Opens a :ref:`project` from path selected in ``Dialog`` and imports
:ref:`sqlite`.
.. versionadded:: 0.1.7
'''
if not selection:
return
else:
selection, fname = op.split(selection[0])
data = op.join(selection, 'data')
results = op.join(selection, 'results')
self.app.project_exists = True
self.app.project_dir = selection
self.app.project_name = fname.split('.')[0]
# (dummy for now)
# dump widgets' properties from process flow to dict, then to json
with open(op.join(selection, fname), 'rb') as f:
project = json.loads(f.read())
print project
# import from project automatically
self._import_data([op.join(data, 'data.sqlite')])
# import results
for file in os.listdir(results):
self.set_page('', op.join(results, file), result_type='import')
[docs] def close_project(self, *args):
'''Clears all important variables, removes all :ref:`data` available in
:mod:`main.Table` and switches to :mod:`main.ProcessFlow`.
.. versionadded:: 0.1.0
'''
# call this before a new project
self.app.project_exists = False
self.app.project_dir = ''
self.app.project_name = ''
tp = self.ids.tabpanel
while len(tp.tab_list) > 1:
self.tables.pop()
tp.remove_widget(tp.tab_list[0])
self.ids.flow.clear_widgets()
tp.switch_to(tp.tab_list[0])
def save_project(self, *args):
# same as _new_project + export data & results
# make some output if unsaved changes available(e.g. * before name)
if self.app.project_exists:
self._save_project()
return
self.savedlg = Dialog(title='New Project',
confirm='Save',
run=self._save_project,
dirs=True,
project=True)
self.savedlg.open()
[docs] def _save_project(self, selection=None, fname=None, *args):
'''Saves a :ref:`project` to path selected in ``Dialog`` and exports
:ref:`sqlite`.
.. versionadded:: 0.1.2
'''
if not selection:
if not self.app.project_exists:
return
selection = op.dirname(self.app.project_dir)
fname = self.app.project_name
else:
selection = selection[0]
if '.' not in fname:
fname = fname + '.krysa'
if selection != op.dirname(self.app.project_dir):
try:
os.mkdir(op.join(selection, fname.split('.')[0]))
except OSError:
error = ErrorPop(msg='Project folder already exists!')
error.open()
return
selection = op.join(selection, fname.split('.')[0])
data = op.join(selection, 'data')
results = op.join(selection, 'results')
if selection != self.app.project_dir:
os.mkdir(data)
os.mkdir(results)
if op.exists(op.join(selection, fname)):
os.remove(op.join(selection, fname))
self.app.project_exists = True
self.app.project_dir = selection
self.app.project_name = fname.split('.')[0]
# (dummy for now)
# dump widgets' properties from process flow to dict, then to json
project = {u'test': u'blah'}
with open(op.join(selection, fname), 'wb') as f:
f.write(json.dumps(project, indent=4))
# let user set table columns, add to tab, then:
self._export_data([data], 'data.sqlite')
# exporting results to pdf -> 0.3.x
self._export_results(results)
def import_data(self, *args):
self.opendlg = Dialog(title='Import Data',
confirm='Import',
run=self._import_data)
self.opendlg.open()
[docs] def _import_data(self, selection, *args):
'''Imports :ref:`sqlite` from path selected in ``Dialog`` and puts it
to :mod:`main.Table`.
.. versionadded:: 0.1.0
'''
# limit table name and column name to [a-zA-Z]
# CREATE TABLE test(
# Column INTEGER NOT NULL CHECK(
# typeof(Column) = 'integer'))
if not selection:
return
else:
selection = selection[0]
if '.sqlite' not in selection:
return
conn = sqlite3.connect(op.join(selection))
c = conn.cursor()
# get tables first!
c.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = [tab[0] for tab in c.fetchall()]
for table in tables:
c.execute("pragma table_info({0})".format(table))
table_info = c.fetchall()
labels = [lbl[1] for lbl in table_info]
# allow only: INTEGER, REAL, TEXT
try:
types = [type[2][0] for type in table_info]
except IndexError:
error = ErrorPop(msg='Bad file: No defined types in columns!')
error.open()
return
tabletab = TabbedPanelItem(text=table)
self.ids.tabpanel.add_widget(tabletab)
c.execute('select * from {0}'.format(table))
values = [item for item in c.fetchone()]
max_cols = len(values)
values += [item for sublist in c.fetchall() for item in sublist]
max_rows = int(math.ceil(len(values) / float(max_cols)))
self.tables.append((table, Table(max_cols=max_cols,
max_rows=max_rows, pos=self.pos,
size=self.size, values=values,
labels=labels)))
tabletab.content = self.tables[-1][1]
self.opendlg.dismiss()
conn.close()
# ToDo: implement automatic saving to Project's data.sqlite
# ToDo: if the same table name exist in tables, add ' (2)' or something
def export_data(self, *args):
self.savedlg = Dialog(title='Export Data',
confirm='Export',
run=self._export_data,
dirs=True)
self.savedlg.open()
@staticmethod
[docs] def _extract_rows(data):
'''Extract values from :mod:`main.Table`'s dictionary into a flat list.
Example:
===== ===== =====
Data1 Data2 Data3
1 2.0 3
===== ===== =====
[u'Data1', u'Data2', u'Data3', u'1', 2.0, 3, ...]
.. versionadded:: 0.1.0
'''
rows = []
for item in data:
try:
item['cell']
if issubclass(item['type'], float):
rows.append(float(item['text']))
elif issubclass(item['type'], int):
rows.append(int(item['text']))
else:
rows.append(item['text'])
except KeyError:
pass
# protect against '' values -> int(''), float('')
except ValueError:
# duplicated zeros to prevent sqlite3.IntegrityError
# - raised when there is type checking for inserting
# values to the table e.g. type(0) != type(0.0)
if issubclass(item['type'], float):
rows.append(0.0)
elif issubclass(item['type'], int):
rows.append(0)
data = []
return rows
[docs] def _export_data(self, selection, fname, *args):
'''Exports all available :ref:`data` (visible as tabs) as :ref:`sqlite`
into path selected in ``Dialog``.
.. versionadded:: 0.1.1
'''
col_types = {"<type 'int'>": 'INTEGER', "<type 'float'>": 'REAL'}
if not selection:
return
else:
selection = selection[0]
if op.exists(op.join(selection, fname)):
os.remove(op.join(selection, fname))
conn = sqlite3.connect(op.join(selection, fname))
c = conn.cursor()
for table in self.tables:
col_string = ''
max_cols = table[1].max_cols
columns = table[1].rv.data[1:max_cols + 1] # types need whole dict
types = table[1].rv.data[max_cols + 2:2 * (max_cols + 1)]
for i, col in enumerate(columns):
try:
type = col_types[repr(types[i]['type'])]
except KeyError:
type = 'TEXT'
# force column to check inserted values
col_string += (
col['_text'] + " " + type +
" NOT NULL CHECK(typeof(" + col['_text'] +
") = '" + type.lower() + "'),"
)
if col_string.endswith(','):
col_string = col_string[:-1]
c.execute(
"CREATE TABLE " + table[0] + "(" + col_string + ")"
)
rows = self._extract_rows(table[1].rv.data)[max_cols:]
cnks = []
for x in xrange(0, len(rows), max_cols):
cnks.append(rows[x:x + max_cols])
for chunk in cnks:
_chunk = []
for cnk in chunk:
if not isinstance(cnk, (str, unicode)):
_chunk.append(unicode(cnk))
else:
_chunk.append('\'' + cnk + '\'')
values = ','.join(_chunk)
c.execute(
"INSERT INTO " + table[0] +
" VALUES(" + values + ")"
)
conn.commit()
conn.close()
try:
self.savedlg.dismiss()
except AttributeError:
# either closed, or wasn't even opened
pass
def _export_results(self, selection, *args):
# 0.3.0
if not selection:
return
for file in os.listdir(selection):
os.remove(op.join(selection, file))
result_wdg = self.ids.results
for i, result in enumerate(reversed(self.ids.results.children)):
try:
where = op.join(selection, str(i).zfill(3) + '.png')
result.children[2].children[0].export_to_png(where)
except IndexError:
print 'No results available.'
# menu showing functions
@staticmethod
def basic(button, *args):
drop = DropDown(allow_sides=True, auto_width=False)
for t in Basic.names:
but = SizedButton(text=t[0])
but.bind(on_release=t[1])
drop.add_widget(but)
drop.open(button)
@staticmethod
def avgs(button, *args):
drop = DropDown(allow_sides=True, auto_width=False)
for t in Avgs.names:
but = SizedButton(text=t[0])
but.bind(on_release=t[1])
drop.add_widget(but)
drop.open(button)
@staticmethod
def manipulate(button, *args):
drop = DropDown(allow_sides=True, auto_width=False)
for t in Manipulate.names:
but = SizedButton(text=t[0])
but.bind(on_release=t[1])
drop.add_widget(but)
drop.open(button)
@staticmethod
[docs] def about(*args):
'''Displays `about` page of the app and includes other credits.
.. versionadded:: 0.1.0
'''
aboutdlg = Popup(title='About')
text = (
'Copyright (C) 2016, KeyWeeUsr(Peter Badida)\n'
'License: GNU GPL v3.0\n'
'Find me @ https://github.com/KeyWeeUsr\n\n'
'Used Software:\n\n'
'Python\n'
'Copyright (C) 2001-2016 Python Software Foundation\n'
'All rights reserved.\n\n'
'Kivy\n'
'Copyright (C) 2010-2016 Kivy Team and other contributors\n\n'
'NumPy\n'
'Copyright (C) 2005-2016, NumPy Developers.\n'
'All rights reserved.\n\n'
'SciPy\n'
'Copyright (C) 2003-2013 SciPy Developers.\n'
'All rights reserved.\n\n'
'MatPlotLib\n'
'Copyright (C) 2002-2016 John D. Hunter\n'
'All Rights Reserved\n'
)
aboutdlg.content = Label(text=text)
aboutdlg.open()
@staticmethod
def test(*args):
print 'ping: ', args
# non-menu related functions
@staticmethod
def get_column(address):
col = 0
for c in address:
if c in string.ascii_letters:
col = col * 26 + (ord(c.upper()) - ord('A')) + 1
return col
[docs] def from_address(self, table, address, extended=False, *args):
'''Gets value(s) from :mod:`main.Table` according to the address such as
``A1`` or ``A1:B2``. Values are fetched in the way that the final list
contains even empty (``u''``) values. It is not expected of user to use
:ref:`task` for strings and most of them won't even run. To get
non-empty values for a :ref:`task` use for example Python's
``filter()``::
values = filter(lambda x: len(str(x)), values)
This `filter`, however, will remain values such as ``None`` untouched.
.. versionadded:: 0.1.0
.. versionchanged:: 0.3.5
Added extended options and a possibility to get ``:all`` values from
data.
'''
values = []
col_row = [] # [column, row] such as [x, y] |_
if ':' not in address:
match = re.findall(r'([a-zA-Z]+)([0-9]+)', address)
col_row.append([self.get_column(match[0][0]), match[0][1]])
else:
if 'all' in address:
start_col = 1
start_row = 1
end_col = self.tables[table][1].cols
end_row = self.tables[table][1].rows
else:
addresses = address.split(':')
match = re.findall(r'([a-zA-Z]+)([0-9]+)', ' '.join(addresses))
start_col = self.get_column(match[0][0])
start_row = int(match[0][1])
end_col = self.get_column(match[1][0])
end_row = int(match[1][1])
for col in range(start_col - 1, end_col):
for row in range(start_row - 1, end_row):
col_row.append([col + 1, row + 1])
col_range = 0
row_range = 0
val_place = [None, None] # [column, row]
table_cols = self.tables[table][1].cols
table_rows = self.tables[table][1].rows
labels = self.tables[table][1].labels
while col_row:
c, r = col_row.pop(0)
c = int(c)
r = int(r)
if 0 < r < table_rows and 0 < c < table_cols:
# increment ranges
if extended:
if val_place[0] != c:
val_place[0] = c
col_range += 1
if val_place[1] != r:
val_place[1] = r
row_range += 1
# get values
key = table_cols * (r + 1) - (table_cols - c)
item = self.tables[table][1].rv.data[key]
if item['r'] == r and item['c'] == c:
if issubclass(item['type'], float):
values.append(float(item['text']))
elif issubclass(item['type'], int):
values.append(int(item['text']))
else:
values.append(item['text'])
if extended:
if row_range < col_range:
# return 1 manually if only one row
# otherwise row_range == 0
row_range = 1
else:
row_range = row_range / int(col_range)
return values, col_range, row_range, labels
else:
return values
[docs] def set_page(self, task, result, result_type='text', footer='time'):
'''Creates a :mod:`main.PageBox` for a result. The header consists of the
:ref:`task`'s name, the footer is by default the time when the result
was created and the content depends on `result_type` which can be -
text, image(path to image) or widget. If `result_type == 'widget'`,
result has to be an instance of a widget (obviously containing the
output), e.g.::
b = Button(text='my output')
set_page('MyTask', b, result_type='widget')
.. note:: When exporting pages, everything is converted into images
(pngs), therefore making fancy behaving widgets is irrelevant.
.. versionadded:: 0.2.0
.. versionchanged:: 0.3.2
Added tables as a result type.
'''
page = PageBox()
head = PaperLabel(text=task, size_hint_y=None, height='30dp')
if result_type == 'text':
content = PaperLabel(text=result)
elif result_type in ['image', 'import']:
content = Image(source=result)
elif 'table' in result_type:
# result == list of values
box = BoxLayout(orientation='vertical')
grid = ResultGrid(cols=int(result_type.strip('table')),
padding=1, spacing=1, size_hint=[0.95, None],
pos_hint={'center_x': 0.5})
grid.bind(minimum_height=grid.setter('height'))
for value in result:
grid.add_widget(Wrap(text=' ' + str(value), color=[0, 0, 0, 1],
background_color=[1, 1, 1, 1]))
box.add_widget(Widget())
box.add_widget(grid)
box.add_widget(Widget())
content = box
elif result_type == 'widget':
content = result
# turn off with footer=None
if result_type == 'import':
pass
elif not footer:
foot = PaperLabel(size_hint_y=None, height='30dp')
else:
if footer == 'time':
t = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())
foot = PaperLabel(text=t,
size_hint_y=None,
height='30dp')
else:
foot = PaperLabel(text=footer, size_hint_y=None,
height='30dp')
if not result_type == 'import':
page.add_widget(head)
page.add_widget(content)
page.add_widget(foot)
else:
page.add_widget(content)
self.ids.results.add_widget(page, 1)
panel = self.ids.resultspanel
if panel.width == panel.min_size:
panel.width = panel.max_size
@staticmethod
def simple_chars(substring, from_undo):
chars = re.findall(r'([a-zA-Z0-9.])', substring)
return u''.join(chars)
@staticmethod
def address_chars(substring, from_undo):
chars = re.findall(r'([a-zA-Z0-9:])', substring)
return u''.join(chars)
def update_tree(self, dt):
tree = self.ids.tree
lab = TreeViewLabel
for node in tree.iterate_all_nodes():
tree.remove_node(node)
root = tree.add_node(lab(text=self.app.project_name, is_open=True))
for path, folders, files in os.walk(self.app.project_dir):
if op.basename(self.app.project_dir) != op.basename(path):
dirname = op.basename(path).title()
parent = tree.add_node(lab(text=dirname, is_open=True), root)
for file in files:
filename = op.basename(file)
tree.add_node(lab(text=filename, is_open=True), parent)
[docs]class KrySA(App):
'''The main class of the application through which is handled the
communication of other classes with getting an instance of the app via
``App.get_running_app()``.
Other than that, it holds important variables of :ref:`project`, sql
blacklist for :ref:`sqlite` creating and updating or the application
properties themselves.
'''
path = op.dirname(op.abspath(__file__))
icon = path + '/data/icon.png'
project_exists = BooleanProperty(False)
project_name = ''
project_dir = ''
title = 'KrySA'
errorcls = ErrorPop
tablecls = Table
sql_blacklist = ['DROP', 'EXEC', 'DECLARE', 'UPDATE', 'CREATE', 'DELETE',
'INSERT', 'JOIN', '=', '"', "'", ';']
[docs] def on_project_exists(self, instance, exists):
'''Checks change of :mod:`main.KrySA.project_exists` and if
:ref:`project` exists, schedules updating of its tree to 5 second
interval.
.. versionadded:: 0.3.0
'''
if exists:
self.treeclock = Clock.schedule_interval(self.root.update_tree, 5)
else:
Clock.unschedule(self.treeclock)
[docs] def build(self):
'''Default Kivy function for getting the root widget of application.
'''
# read MenuDrop if an idea of removing it comes up
MenuDrop()
return Body()
if __name__ == '__main__':
KrySA().run()