Skip to content

Commit 70171b6

Browse files
committed
refs #281, getting a start on converting over to Pandas for ResultSets
Got it pretty close to working (at least for the sqlite driver). More debugging and troubleshooting to go!
1 parent d494d67 commit 70171b6

File tree

1 file changed

+265
-19
lines changed

1 file changed

+265
-19
lines changed

pysimplesql/pysimplesql.py

+265-19
Original file line numberDiff line numberDiff line change
@@ -997,17 +997,18 @@ def requery(
997997
self.rows.load_sort_settings(sort_settings)
998998
self.rows.sort(self.table)
999999

1000-
for row in self.rows:
1001-
# perform transform one row at a time
1002-
if self.transform is not None:
1003-
self.transform(self, row, TFORM_DECODE)
1000+
# Perform transform one row at a time
1001+
if self.transform is not None:
1002+
self.rows = self.rows.apply(
1003+
lambda row: self.transform(self, row, TFORM_DECODE) or row, axis=1
1004+
)
10041005

1005-
# Strip trailing white space, as this is what sg[element].get() does, so we
1006-
# can have an equal comparison. Not the prettiest solution. Will look into
1007-
# this more on the PySimpleGUI end and make a follow-up ticket.
1008-
for k, v in row.items():
1009-
if type(v) is str:
1010-
row[k] = v.rstrip()
1006+
# Strip trailing white space, as this is what sg[element].get() does, so we
1007+
# can have an equal comparison. Not the prettiest solution. Will look into
1008+
# this more on the PySimpleGUI end and make a follow-up ticket.
1009+
self.rows = self.rows.applymap(
1010+
lambda x: x.rstrip() if isinstance(x, str) else x
1011+
)
10111012

10121013
if select_first:
10131014
self.first(
@@ -1423,18 +1424,18 @@ def get_current_pk(self) -> int:
14231424
"""
14241425
return self.get_current(self.pk_column)
14251426

1426-
def get_current_row(self) -> Union[ResultRow, None]:
1427+
def get_current_row(self) -> Union[pd.Series, None]:
14271428
"""
14281429
Get the row for the currently selected record of this table.
14291430
1430-
:returns: A `ResultRow` object
1431+
:returns: A pandas Series object
14311432
"""
1432-
if self.rows:
1433+
if not self.rows.empty:
14331434
# force the current_index to be in bounds!
14341435
# For child reparenting
14351436
self.current_index = self.current_index
14361437

1437-
return self.rows[self.current_index]
1438+
return self.rows.iloc[self.current_index]
14381439
return None
14391440

14401441
def add_selector(
@@ -3054,7 +3055,10 @@ def update_elements(
30543055
disable = (
30553056
len(self[data_key].rows) == 0
30563057
or self._edit_protect
3057-
or self[data_key].get_current_row().virtual
3058+
or self[data_key]
3059+
.get_current_row()
3060+
.attrs.get("virtual", False)
3061+
.iloc[0]
30583062
)
30593063
win[m["event"]].update(disabled=disable)
30603064

@@ -3183,6 +3187,13 @@ def update_elements(
31833187
else:
31843188
lst = []
31853189
for row in target_table.rows:
3190+
print(
3191+
row,
3192+
pk_column,
3193+
description,
3194+
row[pk_column],
3195+
row[description],
3196+
)
31863197
lst.append(ElementRow(row[pk_column], row[description]))
31873198

31883199
# Map the value to the combobox, by getting the description_column
@@ -5856,7 +5867,242 @@ def copy(self):
58565867
return ResultRow(self.row.copy(), virtual=self.virtual)
58575868

58585869

5859-
class ResultSet:
5870+
import pandas as pd
5871+
5872+
5873+
class ResultSet(pd.DataFrame):
5874+
"""
5875+
The ResultSet class is a generic result class so that working with the resultset of
5876+
the different supported databases behave in a consistent manner. A `ResultSet` is a
5877+
Pandas dataframe with some extra functionality to make working with abstracted
5878+
database drivers easier.
5879+
5880+
ResultSets can be thought up as rows of information. Iterating through a ResultSet
5881+
is very simple:
5882+
ResultSet = driver.execute('SELECT * FROM Journal;')
5883+
for row in rows:
5884+
print(row['title'])
5885+
5886+
Note: The lastrowid is set by the caller, but by pysimplesql convention, the
5887+
lastrowid should only be set after and INSERT statement is executed.
5888+
"""
5889+
5890+
SORT_NONE = 0
5891+
SORT_ASC = 1
5892+
SORT_DESC = 2
5893+
5894+
def __init__(
5895+
self,
5896+
rows: List[Dict[str, Any]] = [],
5897+
lastrowid: int = None,
5898+
exception: str = None,
5899+
column_info: ColumnInfo = None,
5900+
) -> None:
5901+
"""
5902+
Create a new ResultSet instance.
5903+
5904+
:param rows: a list of dicts representing a row of data, with each key being a
5905+
column name
5906+
:param lastrowid: The primary key of an inserted item.
5907+
:param exception: If an exception was encountered during the query, it will be
5908+
passed along here
5909+
:column_info: a `ColumnInfo` object can be supplied so that column information
5910+
can be accessed
5911+
"""
5912+
super().__init__(rows)
5913+
self.lastrowid = lastrowid
5914+
self.exception = exception
5915+
self.column_info = column_info
5916+
self.sort_column = None
5917+
self.sort_reverse = False
5918+
self.attrs["original_index"] = self.index.copy() # Store the original index
5919+
self.attrs["virtual"] = pd.Series(
5920+
[False] * len(self), index=self.index
5921+
) # Store virtual flags for each row
5922+
5923+
def __str__(self):
5924+
return str(self.to_dict(orient="records"))
5925+
5926+
def fetchone(self) -> pd.Series:
5927+
"""
5928+
Fetch the first record in the ResultSet.
5929+
5930+
:returns: A `pd.Series` object representing the row
5931+
"""
5932+
return self.iloc[0] if len(self) else pd.Series(dtype=object)
5933+
5934+
def fetchall(self) -> ResultSet:
5935+
"""
5936+
ResultSets don't actually support a fetchall(), since the rows are already
5937+
returned. This is more of a comfort method that does nothing, for those that are
5938+
used to calling fetchall().
5939+
5940+
:returns: The same ResultSet that called fetchall()
5941+
"""
5942+
return self
5943+
5944+
def insert(self, row: dict, idx: int = None, virtual: bool = False) -> None:
5945+
"""
5946+
Insert a new virtual row into the `ResultSet`. Virtual rows are ones that exist
5947+
in memory, but not in the database. When a save action is performed, virtual
5948+
rows will be added into the database.
5949+
5950+
:param row: A dict representation of a row of data
5951+
:param idx: The index where the row should be inserted (default to last index)
5952+
:returns: None
5953+
"""
5954+
row_series = pd.Series(row)
5955+
if idx is None:
5956+
idx = len(self)
5957+
idx_label = self.index.max() + 1 if len(self) > 0 else 0
5958+
self.loc[idx_label] = row_series
5959+
self.attrs["original_index"] = self.attrs["original_index"].insert(
5960+
idx, idx_label
5961+
)
5962+
self.attrs["virtual"].loc[idx_label] = virtual
5963+
self.sort_index()
5964+
5965+
def purge_virtual(self) -> None:
5966+
"""
5967+
Purge virtual rows from the `ResultSet`.
5968+
5969+
:returns: None
5970+
"""
5971+
virtual_rows = self.attrs["virtual"][self.attrs["virtual"]].index
5972+
self.drop(virtual_rows, inplace=True)
5973+
self.attrs["original_index"] = self.attrs["original_index"].drop(virtual_rows)
5974+
self.attrs["virtual"] = self.attrs["virtual"].drop(virtual_rows)
5975+
5976+
def sort_by_column(self, column: str, table: str, reverse=False) -> None:
5977+
"""
5978+
Sort the `ResultSet` by column. Using the mapped relationships of the database,
5979+
foreign keys will automatically sort based on the parent table's description
5980+
column, rather than the foreign key number.
5981+
5982+
:param column: The name of the column to sort the `ResultSet` by
5983+
:param table: The name of the table the column belongs to
5984+
:param reverse: Reverse the sort; False = ASC, True = DESC
5985+
:returns: None
5986+
"""
5987+
# Target sorting by this ResultSet
5988+
rows = self # search criteria is based on rows
5989+
target_col = column # Looking in rows for this column
5990+
target_val = column # to be equal to the same column in self.rows
5991+
5992+
# We don't want to sort by foreign keys directly - we want to sort by the
5993+
# description column of the foreign table that the foreign key references
5994+
rels = Relationship.get_relationships(table)
5995+
for rel in rels:
5996+
if column == rel.fk_column:
5997+
rows = rel.frm[
5998+
rel.parent_table
5999+
] # change the rows used for sort criteria
6000+
target_col = rel.pk_column # change our target column to look in
6001+
target_val = rel.frm[
6002+
rel.parent_table
6003+
].description_column # and return the value in this column
6004+
break
6005+
6006+
def get_sort_key(row):
6007+
try:
6008+
return next(
6009+
r[target_val]
6010+
for _, r in rows.iterrows()
6011+
if r[target_col] == row[column]
6012+
)
6013+
except StopIteration:
6014+
return None
6015+
6016+
try:
6017+
self.sort_values(
6018+
by=self.index, key=get_sort_key, ascending=not reverse, inplace=True
6019+
)
6020+
except KeyError:
6021+
logger.debug(f"ResultSet could not sort by column {column}. KeyError.")
6022+
6023+
def sort_by_index(self, index: int, table: str, reverse=False):
6024+
"""
6025+
Sort the `ResultSet` by column index Using the mapped relationships of the
6026+
database, foreign keys will automatically sort based on the parent table's
6027+
description column, rather than the foreign key number.
6028+
6029+
:param index: The index of the column to sort the `ResultSet` by
6030+
:param table: The name of the table the column belongs to
6031+
:param reverse: Reverse the sort; False = ASC, True = DESC
6032+
:returns: None
6033+
"""
6034+
column = self.columns[index]
6035+
self.sort_by_column(column, table, reverse)
6036+
6037+
def store_sort_settings(self) -> list:
6038+
"""
6039+
Store the current sort settingg. Sort settings are just the sort column and
6040+
reverse setting. Sort order can be restored with
6041+
`ResultSet.load_sort_settings()`.
6042+
6043+
:returns: A list containing the sort_column and the sort_reverse
6044+
"""
6045+
return [self.sort_column, self.sort_reverse]
6046+
6047+
def load_sort_settings(self, sort_settings: list) -> None:
6048+
"""
6049+
Load a previously stored sort setting. Sort settings are just the sort columm
6050+
and reverse setting.
6051+
6052+
:param sort_settings: A list as returned by `ResultSet.store_sort_settings()`
6053+
"""
6054+
self.sort_column = sort_settings[0]
6055+
self.sort_reverse = sort_settings[1]
6056+
6057+
def sort_reset(self) -> None:
6058+
"""
6059+
Reset the sort order to the original when this ResultSet was created. Each
6060+
ResultRow has the original order stored.
6061+
6062+
:returns: None
6063+
"""
6064+
self.sort_index(inplace=True)
6065+
6066+
def sort(self, table: str) -> None:
6067+
"""
6068+
Sort according to the internal sort_column and sort_reverse variables. This is a
6069+
good way to re-sort without changing the sort_cycle.
6070+
6071+
:param table: The table associated with this ResultSet. Passed along to
6072+
`ResultSet.sort_by_column()`
6073+
:returns: None
6074+
"""
6075+
if self.sort_column is None:
6076+
self.sort_reset()
6077+
else:
6078+
self.sort_by_column(self.sort_column, table, self.sort_reverse)
6079+
6080+
def sort_cycle(self, column: str, table: str) -> int:
6081+
"""
6082+
Cycle between original sort order of the ResultSet, ASC by column, and DESC by
6083+
column with each call.
6084+
6085+
:param column: The column name to cycle the sort on
6086+
:param table: The table that the column belongs to
6087+
:returns: A ResultSet sort constant; ResultSet.SORT_NONE, ResultSet.SORT_ASC, or
6088+
ResultSet.SORT_DESC
6089+
"""
6090+
if column != self.sort_column:
6091+
self.sort_column = column
6092+
self.sort_reverse = False
6093+
self.sort(table)
6094+
return ResultSet.SORT_ASC
6095+
if not self.sort_reverse:
6096+
self.sort_reverse = True
6097+
self.sort(table)
6098+
return ResultSet.SORT_DESC
6099+
self.sort_reverse = False
6100+
self.sort_column = None
6101+
self.sort(table)
6102+
return ResultSet.SORT_NONE
6103+
6104+
6105+
class ResultSet2:
58606106

58616107
"""
58626108
The ResultSet class is a generic result class so that working with the resultset of
@@ -6751,7 +6997,7 @@ def get_tables(self):
67516997
'WHERE type="table" AND name NOT like "sqlite%";'
67526998
)
67536999
cur = self.execute(q, silent=True)
6754-
return [row["name"] for row in cur]
7000+
return list(cur["name"])
67557001

67567002
def column_info(self, table):
67577003
# Return a list of column names
@@ -6760,7 +7006,7 @@ def column_info(self, table):
67607006
names = []
67617007
col_info = ColumnInfo(self, table)
67627008

6763-
for row in rows:
7009+
for index, row in rows.iterrows():
67647010
name = row["name"]
67657011
names.append(name)
67667012
domain = row["type"]
@@ -6790,7 +7036,7 @@ def relationships(self):
67907036
f"PRAGMA foreign_key_list({self.quote_table(from_table)})", silent=True
67917037
)
67927038

6793-
for row in rows:
7039+
for index, row in rows.iterrows():
67947040
dic = {}
67957041
# Add the relationship if it's in the requery list
67967042
if row["on_update"] == "CASCADE":

0 commit comments

Comments
 (0)