Skip to content

API Reference (more pages to come)

DISCLAIMER: While pysimplesql works with and was inspired by the excellent

PySimpleGUI™ project, it has no affiliation.

Rapidly build and deploy database applications in Python pysimplesql binds

PySimpleGUI to various databases for rapid, effortless database application development. Makes a great replacement for MS Access or LibreOffice Base! Have the full power and language features of Python while having the power and control of managing your own codebase. pysimplesql not only allows for super simple automatic control (not one single line of SQL needs written to use pysimplesql), but also allows for very low level control for situations that warrant it.


NAMING CONVENTIONS USED THROUGHOUT THE SOURCE CODE

There is a lot of ambiguity with database terminology, as many terms are used interchangeably in some circumstances, but not in others. The Internet has post after post debating this topic. See one example here: https://dba.stackexchange.com/questions/65609/column-vs-field-have-i-been-using-these-terms-incorrectly # fmt: skip To avoid confusion in the source code, specific naming conventions will be used whenever possible.

Naming conventions can fall under 4 categories: - referencing the database (variables, functions, etc. that relate to the database) - referencing the DataSet (variables, functions, etc. that relate to the DataSet) - referencing pysimplesql - referencing PySimpleGUI

  • Database related: driver - a SQLDriver derived class t, table, tables - the database table name(s) r, row, rows - A group of related data in a table c, col, cols, column, columns - A set of values of a particular type q, query - An SQL query string domain - the data type of the data (INTEGER, TEXT, etc.)

  • DataSet related: r, row, rows, df - A row, or collection of rows from querying the database c, col, cols, column, columns - A set of values of a particular type Record - A collection of fields that make up a row field - the value found where a row intersects a column

  • pysimplesql related frm - a Form object dataset, datasets - a DataSet object, or collection of DataSet objects data_key - the key (name) of a dataset object

  • PySimpleGUI related win, window - A PySimpleGUI Window object element - a Window element element_key - a window element key


TFORM_ENCODE: int = 1 module-attribute

TODO

TFORM_DECODE: int = 0 module-attribute

TODO

PROMPT_MODE: int = 1 module-attribute

TODO

AUTOSAVE_MODE: int = 2 module-attribute

TODO

SAVE_FAIL: int = 1 module-attribute

Save failed due to callback or database error

SAVE_SUCCESS: int = 2 module-attribute

Save was successful

SAVE_NONE: int = 4 module-attribute

There was nothing to save

keygen = KeyGen(separator=':') module-attribute

This is a global keygen instance for general purpose use.

See KeyGen for more info

ElementType

Bases: Enum

Types for automatic mapping.

EventType

Bases: Enum

Event Types.

FUNCTION = auto() class-attribute instance-attribute

Custom events (requires 'function')

PromptSaveReturn

Bases: Enum

prompt_save return enums.

PROCEED = auto() class-attribute instance-attribute

After prompt_save, proceeded to save

NONE = auto() class-attribute instance-attribute

Found no records changed

DISCARDED = auto() class-attribute instance-attribute

User declined to save

Boolean

Bases: Flag

Enumeration class providing a convenient way to differentiate when a function may return a 'truthy' or 'falsy' value, such as 1, "", or 0.

Used in value_changed

TRUE = True class-attribute instance-attribute

Represents the boolean value True.

FALSE = False class-attribute instance-attribute

Represents the boolean value False.

ValidateMode

Bases: Enum

Enumeration class representing different validation modes.

STRICT = 'strict' class-attribute instance-attribute

Strict prevents invalid values from being entered.

RELAXED = 'relaxed' class-attribute instance-attribute

Relaxed allows invalid input, but ensures validation occurs before saving to the database.

DISABLED = 'disabled' class-attribute instance-attribute

Validation is turned off, and no checks or restrictions are applied.

ValidateRule

Bases: Enum

Collection of enums used ValidateResponse.

REQUIRED = 'required' class-attribute instance-attribute

Required field. Either set as 'NOTNULL' in database, or later in ColumnClass

PYTHON_TYPE = 'python_type' class-attribute instance-attribute

After casting, value is still not correct python type.

PRECISION = 'precision' class-attribute instance-attribute

Value has too many numerical places

MIN_VALUE = 'min_value' class-attribute instance-attribute

Value less than set mininum value

MAX_VALUE = 'max_value' class-attribute instance-attribute

Value greater than set maximum value

MIN_LENGTH = 'min_length' class-attribute instance-attribute

Value's length is less than minimum length

MAX_LENGTH = 'max_length' class-attribute instance-attribute

Value's length is greater than than maximum length

CUSTOM = 'custom' class-attribute instance-attribute

Special enum to be used when returning a ValidateResponse in your own `custom_validate_fn'.

Example
import re
def is_valid_email(email):
    valid_email = re.match(r".+\@.+\..+", email) is not None
    if not valid_email:
        return ss.ValidateResponse(
            ss.ValidateRule.CUSTOM, email, " is not a valid email"
        )
    return ss.ValidateResponse()

ValidateResponse dataclass

Represents the response returned by validate method.

Attributes:

Name Type Description
exception Union[ValidateRule, None]

Indicates validation failure, if any. None for valid responses.

value str

The value that was being validated.

rule str

The specific ValidateRule that caused the exception, if applicable.

Example

How how to create a ok popup from an exception:

```python
response = frm[data_key].column_info[col].validate(value)
if response.exception:
    msg = f"{ss.lang.dataset_save_validate_error_header}"
    field = ss.lang.dataset_save_validate_error_field.format_map(
        ss.LangFormat(field=col)
    )
    exception = ss.lang[response.exception].format_map(
        ss.LangFormat(value=response.value, rule=response.rule)
    )
    msg += f"{field}{exception}"
    frm.popup.ok(lang.dataset_save_validate_error_title, msg)
```

CellFormatFn

Collection of functions to pre-format values before populating sg.Table values.

Each function must accept and return 1 value. Additional arguments can be filled in via a lambda.

Example
fn = lambda x: ss.CellFormatFn.decimal_places(x, 2)
frm[data_key].column_info[col].cell_format_fn = fn

bool_to_checkbox(val) staticmethod

Converts a boolean value to a themepack.checkbox_true/false.

Source code in pysimplesql\pysimplesql.py
398
399
400
401
402
403
404
405
406
407
@staticmethod
def bool_to_checkbox(
    val: Union[str, int, bool]
) -> Union[themepack.checkbox_true, themepack.checkbox_false]:
    """Converts a boolean value to a themepack.checkbox_true/false."""
    return (
        themepack.checkbox_true
        if checkbox_to_bool(val)
        else themepack.checkbox_false
    )

decimal_places(val, decimal_places) staticmethod

Format the value to specified decimal places using the system locale.

Source code in pysimplesql\pysimplesql.py
409
410
411
412
413
414
415
@staticmethod
def decimal_places(val: Union[int, float, Decimal], decimal_places: int):
    """Format the value to specified decimal places using the system locale."""
    format_string = f"%.{decimal_places}f"
    if val not in EMPTY:
        return locale.format_string(format_string, val)
    return val

TableRow(pk, *args, **kwargs)

Bases: list

Convenience class used by Tables to associate a primary key with a row of data.

Note: This is typically not used by the end user.

Source code in pysimplesql\pysimplesql.py
428
429
430
def __init__(self, pk: int, *args, **kwargs) -> None:
    self.pk = pk
    super().__init__(*args, **kwargs)

ElementRow(pk, val)

Convenience class used by listboxes and comboboxes to associate a primary key with a row of data.

Note: This is typically not used by the end user.

Source code in pysimplesql\pysimplesql.py
452
453
454
def __init__(self, pk: int, val: Union[str, int]) -> None:
    self.pk = pk
    self.val = val

Relationship dataclass

Information from Foreign-Keys.

Parameters:

Name Type Description Default
join_type str

The join type. I.e. "LEFT JOIN", "INNER JOIN", etc.

required
child_table str

The table name of the fk table

required
fk_column Union[str, int]

The child table's foreign key column

required
parent_table str

The table name of the parent table

required
pk_column Union[str, int]

The parent table's primary key column

required
update_cascade bool

True if the child's fk_column ON UPDATE rule is 'CASCADE'

required
delete_cascade bool

True if the child's fk_column ON DELETE rule is 'CASCADE'

required
driver Driver

A SQLDriver instance.

required

__str__()

Return a join clause when cast to a string.

Source code in pysimplesql\pysimplesql.py
517
518
519
def __str__(self) -> str:
    """Return a join clause when cast to a string."""
    return self.driver.relationship_to_join_clause(self)

RelationshipStore dataclass

Bases: list

Used to track primary/foreign key relationships in the database.

See the following for more information: add_relationship and auto_add_relationships.

Note: This class is not typically used the end user

get_rels_for(table)

Return the relationships for the passed-in table.

Parameters:

Name Type Description Default
table str

The table to get relationships for

required

Returns:

Type Description
List[Relationship]

A list of @Relationship objects

Source code in pysimplesql\pysimplesql.py
534
535
536
537
538
539
540
541
542
543
def get_rels_for(self, table: str) -> List[Relationship]:
    """Return the relationships for the passed-in table.

    Args:
        table: The table to get relationships for

    Returns:
        A list of @Relationship objects
    """
    return [r for r in self if r.child_table == table]

get_update_cascade_tables(table)

Return a unique list of the relationships for this table that should requery with this table.

Parameters:

Name Type Description Default
table str

The table to get cascaded children for

required

Returns:

Type Description
List[str]

A unique list of table names

Source code in pysimplesql\pysimplesql.py
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
def get_update_cascade_tables(self, table: str) -> List[str]:
    """Return a unique list of the relationships for this table that should requery
    with this table.

    Args:
        table: The table to get cascaded children for

    Returns:
        A unique list of table names
    """
    rel = [
        r.child_table
        for r in self
        if r.parent_table == table and r.on_update_cascade
    ]
    # make unique
    return list(set(rel))

get_delete_cascade_tables(table)

Return a unique list of the relationships for this table that should be deleted with this table.

Parameters:

Name Type Description Default
table str

The table to get cascaded children for

required

Returns:

Type Description
List[str]

A unique list of table names

Source code in pysimplesql\pysimplesql.py
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
def get_delete_cascade_tables(self, table: str) -> List[str]:
    """Return a unique list of the relationships for this table that should be
    deleted with this table.

    Args:
        table: The table to get cascaded children for

    Returns:
        A unique list of table names
    """
    rel = [
        r.child_table
        for r in self
        if r.parent_table == table and r.on_delete_cascade
    ]
    # make unique
    return list(set(rel))

get_parent(table)

Return the parent table for the passed-in table.

Parameters:

Name Type Description Default
table str

The table (str) to get relationships for

required

Returns:

Type Description
Union[str, None]

The name of the Parent table, or None if there is none

Source code in pysimplesql\pysimplesql.py
581
582
583
584
585
586
587
588
589
590
591
592
593
def get_parent(self, table: str) -> Union[str, None]:
    """Return the parent table for the passed-in table.

    Args:
        table: The table (str) to get relationships for

    Returns:
        The name of the Parent table, or None if there is none
    """
    for r in self:
        if r.child_table == table and r.on_update_cascade:
            return r.parent_table
    return None

is_parent_virtual(table, frm)

Return True if current row of parent table is virtual.

Parameters:

Name Type Description Default
table str

The table (str) to get relationships for

required
frm Form

Form reference

required

Returns:

Type Description
Union[bool, None]

True if current row of parent table is virtual

Source code in pysimplesql\pysimplesql.py
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
def is_parent_virtual(self, table: str, frm: Form) -> Union[bool, None]:
    """Return True if current row of parent table is virtual.

    Args:
        table: The table (str) to get relationships for
        frm: Form reference

    Returns:
        True if current row of parent table is virtual
    """
    for r in self:
        if r.child_table == table and r.on_update_cascade:
            try:
                return frm[r.parent_table].pk_is_virtual()
            except AttributeError:
                return False
    return None

get_update_cascade_fk_column(table)

Return the cascade fk that filters for the passed-in table.

Parameters:

Name Type Description Default
table str

The table name of the child

required

Returns:

Type Description
Union[str, None]

The name of the cascade-fk, or None

Source code in pysimplesql\pysimplesql.py
613
614
615
616
617
618
619
620
621
622
623
624
625
def get_update_cascade_fk_column(self, table: str) -> Union[str, None]:
    """Return the cascade fk that filters for the passed-in table.

    Args:
        table: The table name of the child

    Returns:
        The name of the cascade-fk, or None
    """
    for r in self:
        if r.child_table == table and r.on_update_cascade:
            return r.fk_column
    return None

get_delete_cascade_fk_column(table)

Return the cascade fk that filters for the passed-in table.

Parameters:

Name Type Description Default
table str

The table name of the child

required

Returns:

Type Description
Union[str, None]

The name of the cascade-fk, or None

Source code in pysimplesql\pysimplesql.py
627
628
629
630
631
632
633
634
635
636
637
638
639
def get_delete_cascade_fk_column(self, table: str) -> Union[str, None]:
    """Return the cascade fk that filters for the passed-in table.

    Args:
        table: The table name of the child

    Returns:
        The name of the cascade-fk, or None
    """
    for r in self:
        if r.child_table == table and r.on_delete_cascade:
            return r.fk_column
    return None

get_dependent_columns(frm_reference, table)

Returns a dictionary of the key and column names that use the description_column text of the given parent table in their ElementRow objects.

This method is used to determine which GUI field and selector elements to update when a new description_column value is saved. The returned dictionary contains the key as the key and the corresponding column name as the value.

Parameters:

Name Type Description Default
frm_reference Form

A Form object representing the parent form.

required
table str

The name of the parent table.

required

Returns:

Type Description
Dict[str, str]

A dictionary of {datakey: column} pairs.

Source code in pysimplesql\pysimplesql.py
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
def get_dependent_columns(self, frm_reference: Form, table: str) -> Dict[str, str]:
    """Returns a dictionary of the `DataSet.key` and column names that use the
    description_column text of the given parent table in their `ElementRow`
    objects.

    This method is used to determine which GUI field and selector elements to update
    when a new `DataSet.description_column` value is saved. The returned dictionary
    contains the `DataSet.key` as the key and the corresponding column name as the
    value.

    Args:
        frm_reference: A `Form` object representing the parent form.
        table: The name of the parent table.

    Returns:
        A dictionary of `{datakey: column}` pairs.
    """
    return {
        frm_reference[dataset].key: r.fk_column
        for r in self
        for dataset in frm_reference.datasets
        if r.parent_table == table
        and frm_reference[dataset].table == r.child_table
        and not r.on_update_cascade
    }

ElementMap dataclass

Map a PySimpleGUI element to a specific DataSet column.

This is what makes the GUI automatically update to the contents of the database. This happens automatically when a PySimpleGUI Window is bound to a Form by using the bind parameter of Form creation, or by executing auto_map_elements() as long as the Table.column naming convention is used, This method can be used to manually map any element to any DataSet column regardless of naming convention.

Parameters:

Name Type Description Default
element Element

A PySimpleGUI Element

required
dataset DataSet

A DataSet object

required
column str

The name of the column to bind to the element

required
where_column str

Used for key, value shorthand

None
where_value str

Used for key, value shorthand

None

Returns:

Type Description

None

CurrentRow dataclass

has_backup: bool property

Returns True if the current_row has a backup row, and False otherwise.

A pandas Series object is stored rows.attrs["row_backup"] before a 'CellEdit' or 'LiveUpdate' operation is initiated, so that it can be compared in records_changed and save_record or used to restore if changes are discarded during a prompt_save operations.

Returns:

Type Description
bool

True if a backup row is present that matches, and False otherwise.

pk: int property

Get the primary key of the currently selected record.

Returns:

Type Description
int

the primary key

backup()

Creates a backup copy of the current row in rows.

Source code in pysimplesql\pysimplesql.py
762
763
764
765
766
def backup(self) -> None:
    """Creates a backup copy of the current row in `DataSet.rows`."""
    rows = self.dataset.rows
    if not self.has_backup:
        rows.attrs["row_backup"] = self.get().copy()

restore_backup()

Restores the backup row to the current row in rows.

This method replaces the current row in the dataset with the backup row, if a backup row is present.

Source code in pysimplesql\pysimplesql.py
768
769
770
771
772
773
774
775
776
def restore_backup(self) -> None:
    """Restores the backup row to the current row in `DataSet.rows`.

    This method replaces the current row in the dataset with the backup row, if a
    backup row is present.
    """
    rows = self.dataset.rows
    if self.has_backup:
        rows.iloc[self.index] = rows.attrs["row_backup"].copy()

get()

Get the row for the currently selected record of this table.

Returns:

Type Description
Union[Series, None]

A pandas Series object

Source code in pysimplesql\pysimplesql.py
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
def get(self) -> Union[pd.Series, None]:
    """Get the row for the currently selected record of this table.

    Returns:
        A pandas Series object
    """
    rows = self.dataset.rows
    if not rows.empty:
        # force the current.index to be in bounds!
        # For child reparenting
        self.index = self.index

        # make sure to return as python type
        return rows.astype("O").iloc[self.index]
    return None

get_original()

Returns a copy of current row as it was fetched in a query from SQLDriver.

If a backup of the current row is present, this method returns a copy of that row. Otherwise, it returns a copy of the current row. Returns None if rows is empty.

Source code in pysimplesql\pysimplesql.py
794
795
796
797
798
799
800
801
802
803
804
805
806
def get_original(self) -> pd.Series:
    """Returns a copy of current row as it was fetched in a query from `SQLDriver`.

    If a backup of the current row is present, this method returns a copy of that
    row. Otherwise, it returns a copy of the current row. Returns None if
    `DataSet.rows` is empty.
    """
    rows = self.dataset.rows
    if self.has_backup:
        return rows.attrs["row_backup"].copy()
    if not rows.empty:
        return self.get().copy()
    return None

get_value(column, default='')

Get the value for the supplied column in the current row.

You can also use indexing of the Form object to get the current value of a column I.e. frm[{DataSet}].[{column}].

Parameters:

Name Type Description Default
column str

The column you want to get the value from

required
default Union[str, int]

A value to return if the record is null

''

Returns:

Type Description
Union[str, int]

The value of the column requested

Source code in pysimplesql\pysimplesql.py
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
def get_value(self, column: str, default: Union[str, int] = "") -> Union[str, int]:
    """Get the value for the supplied column in the current row.

    You can also use indexing of the `Form` object to get the current value of a
    column I.e. frm[{DataSet}].[{column}].

    Args:
        column: The column you want to get the value from
        default: A value to return if the record is null

    Returns:
        The value of the column requested
    """
    logger.debug(f"Getting current record for {self.dataset.table}.{column}")
    if self.dataset.row_count:
        if self.get()[column] is not None:
            return self.get()[column]
        return default
    return default

set_value(column, value, write_event=False)

Set the value for the supplied column in the current row, making a backup if needed.

You can also use indexing of the Form object to set the current value of a column. I.e. frm[{DataSet}].[{column}] = 'New value'.

Parameters:

Name Type Description Default
column str

The column you want to set the value for

required
value Union[str, int]

A value to set the current record's column to

required
write_event bool

(optional) If True, writes an event to PySimpleGui as [after_record_edit][pysimplesql.pysimplesql.after_record_edit].

False

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
def set_value(
    self, column: str, value: Union[str, int], write_event: bool = False
) -> None:
    """Set the value for the supplied column in the current row, making a backup if
    needed.

    You can also use indexing of the `Form` object to set the current value of a
    column. I.e. frm[{DataSet}].[{column}] = 'New value'.

    Args:
        column: The column you want to set the value for
        value: A value to set the current record's column to
        write_event: (optional) If True, writes an event to PySimpleGui as
            `after_record_edit`.

    Returns:
        None
    """
    rows = self.dataset.rows
    dataset = self.dataset
    logger.debug(f"Setting current record for {dataset.key}.{column} = {value}")
    self.backup()
    rows.loc[rows.index[self.index], column] = value
    if write_event:
        self.dataset.frm.window.write_event_value(
            "after_record_edit",
            {
                "frm_reference": dataset.frm,
                "data_key": dataset.key,
                "column": column,
                "value": value,
            },
        )
    # call callback
    if "after_record_edit" in dataset.callbacks:
        dataset.callbacks["after_record_edit"](
            dataset.frm, dataset.frm.window, dataset.key
        )

DataSet dataclass

DataSet objects are used for an internal representation of database tables.

DataSet instances are added by the following Form methods: add_dataset, auto_add_datasets. A DataSet is synonymous for a SQL Table (though you can technically have multiple DataSet objects referencing the same table, with each DataSet object having its own sorting, where clause, etc.). Note: While users will interact with DataSet objects often in pysimplesql, they typically aren't created manually by the user.

Parameters:

Name Type Description Default
data_key InitVar[str]

The name you are assigning to this DataSet object (I.e. 'people') Accessible via key.

required
frm_reference InitVar[Form]

This is a reference to the @ Form object, for convenience. Accessible via frm

required
table str

Name of the table

required
pk_column str

The name of the column containing the primary key for this table.

required
description_column str

The name of the column used for display to users (normally in a combobox or listbox).

required
query Optional[str]

You can optionally set an initial query here. If none is provided, it will default to "SELECT * FROM {table}"

''
order_clause Optional[str]

The sort order of the returned query. If none is provided it will default to "ORDER BY {description_column} ASC"

''
filtered bool

(optional) If True, the relationships will be considered and an appropriate WHERE clause will be generated. False will display all records in the table.

True
prompt_save

(optional) Default: Mode set in Form. Prompt to save changes when dirty records are present. There are two modes available, PROMPT_MODE to prompt to save when unsaved changes are present. AUTOSAVE_MODE to automatically save when unsaved changes are present.

required
save_quiet bool

(optional) Default: Set in Form. True to skip info popup on save. Error popups will still be shown.

None
duplicate_children bool

(optional) Default: Set in Form. If record has children, prompt user to choose to duplicate current record, or both.

None
validate_mode ValidateMode

STRICT to prevent invalid values from being entered. RELAXED allows invalid input, but ensures validation occurs before saving to the database.

None

Attributes:

Name Type Description
key str

TODO

description_column: str instance-attribute

TODO

key: str = field_(init=False) class-attribute instance-attribute

Short for 'data_key'

frm: Form = field_(init=False) class-attribute instance-attribute

TODO

driver: Driver = field_(init=False) class-attribute instance-attribute

TODO

relationships: RelationshipStore = field_(init=False) class-attribute instance-attribute

TODO

rows: pd.DataFrame = field_(init=False) class-attribute instance-attribute

TODO

join_clause: str = field_(init=False) class-attribute instance-attribute

TODO

where_clause: str = field_(init=False) class-attribute instance-attribute

TODO

search_order: List[str] = field_(init=False) class-attribute instance-attribute

TODO

row_count: int property

Returns the number of rows in the dataset. If the dataset is not a pandas DataFrame, returns 0.

Returns:

Type Description
int

The number of rows in the dataset.

__getitem__(column)

Retrieve the value of the specified column in the current row.

Parameters:

Name Type Description Default
column str

The key of the column to retrieve.

required

Returns:

Type Description
Union[str, int]

The current value of the specified column.

Source code in pysimplesql\pysimplesql.py
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
def __getitem__(self, column: str) -> Union[str, int]:
    """Retrieve the value of the specified column in the current row.

    Args:
        column: The key of the column to retrieve.

    Returns:
        The current value of the specified column.
    """
    return self.current.get_value(column)

__setitem__(column, value)

Set the value of the specified column in the current row.

Parameters:

Name Type Description Default
column

The key of the column to set.

required
value Union[str, int]

The value to set the column to.

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
def __setitem__(self, column, value: Union[str, int]) -> None:
    """Set the value of the specified column in the current row.

    Args:
        column: The key of the column to set.
        value: The value to set the column to.

    Returns:
        None
    """
    self.current.set_value(column, value)

purge_form(frm, reset_keygen) classmethod

Purge the tracked instances related to frm.

Parameters:

Name Type Description Default
frm Form

the Form to purge DataSet` instances from

required
reset_keygen bool

Reset the keygen after purging?

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
@classmethod
def purge_form(cls, frm: Form, reset_keygen: bool) -> None:
    """Purge the tracked instances related to frm.

    Args:
        frm: the `Form` to purge `DataSet`` instances from
        reset_keygen: Reset the keygen after purging?

    Returns:
        None
    """
    new_instances = []
    selector_keys = []

    for dataset in DataSet.instances:
        if dataset.frm != frm:
            new_instances.append(dataset)
        else:
            logger.debug(
                f"Removing DataSet {dataset.key} related to "
                f"{frm.driver.__class__.__name__}"
            )
            # we need to get a list of elements to purge from the keygen
            for s in dataset.selector:
                selector_keys.append(s["element"].key)

    # Reset the keygen for selectors and elements from this Form
    # This is probably a little hack-ish, perhaps I should relocate the keygen?
    if reset_keygen:
        for k in selector_keys:
            keygen.reset_key(k)
        keygen.reset_from_form(frm)
    # Update the internally tracked instances
    DataSet.instances = new_instances

set_prompt_save(mode)

Set the prompt to save action when navigating records.

Parameters:

Name Type Description Default
mode int

Use PROMPT_MODE to prompt to save when unsaved changes are present. AUTOSAVE_MODE to automatically save when unsaved changes are present.

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
def set_prompt_save(self, mode: int) -> None:
    """Set the prompt to save action when navigating records.

    Args:
        mode: Use `PROMPT_MODE` to prompt to save when unsaved changes are present.
            `AUTOSAVE_MODE` to automatically save when unsaved changes are present.

    Returns:
        None
    """
    self._prompt_save = mode

set_search_order(order)

Set the search order when using the search box.

This is a list of column names to be searched, in order

Parameters:

Name Type Description Default
order List[str]

A list of column names to search

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
def set_search_order(self, order: List[str]) -> None:
    """Set the search order when using the search box.

    This is a list of column names to be searched, in order

    Args:
        order: A list of column names to search

    Returns:
        None
    """
    self.search_order = order

set_callback(callback, fctn)

Set DataSet callbacks. A runtime error will be thrown if the callback is not supported.

The following callbacks are supported
  • before_save: called before a record is saved. The save will continue if the callback returns true, or the record will rollback if the callback returns false.
  • after_save: called after a record is saved. The save will commit to the database if the callback returns true, else it will rollback the transaction
  • before_update: Alias for before_save
  • after_update: Alias for after_save
  • before_delete: called before a record is deleted. The delete will move forward if the callback returns true, else the transaction will rollback
  • after_delete: called after a record is deleted. The delete will commit to the database if the callback returns true, else it will rollback the transaction
  • before_duplicate: called before a record is duplicate. The duplicate will move forward if the callback returns true, else the transaction will rollback
  • after_duplicate: called after a record is duplicate. The duplicate will commit to the database if the callback returns true, else it will rollback the transaction
  • before_search: called before searching. The search will continue if the callback returns True
  • after_search: called after a search has been performed. The record change will undo if the callback returns False
  • record_changed: called after a record has changed (previous,next, etc.)
  • after_record_edit: called after the internal DataSet row is edited via a sg.Table cell-edit, or field live-update.

Parameters:

Name Type Description Default
callback str

The name of the callback, from the list above

required
fctn Callable[[Form, Window, key], bool]

The function to call. Note, the function must take at least two parameters, a Form instance, and a sg.Window instance, with an optional key, and return True or False

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
def set_callback(
    self, callback: str, fctn: Callable[[Form, sg.Window, DataSet.key], bool]
) -> None:
    """Set DataSet callbacks. A runtime error will be thrown if the callback is not
    supported.

    The following callbacks are supported:
        - before_save: called before a record is saved. The save will continue if
            the callback returns true, or the record will rollback if the callback
            returns false.
        - after_save: called after a record is saved. The save will commit to the
            database if the callback returns true, else it will rollback the
            transaction
        - before_update: Alias for before_save
        - after_update:  Alias for after_save
        - before_delete: called before a record is deleted.  The delete will move
            forward if the callback returns true, else the transaction will rollback
        - after_delete: called after a record is deleted. The delete will commit to
            the database if the callback returns true, else it will rollback the
            transaction
        - before_duplicate: called before a record is duplicate.  The duplicate will
            move forward if the callback returns true, else the transaction will
            rollback
        - after_duplicate: called after a record is duplicate. The duplicate will
            commit to the database if the callback returns true, else it will
            rollback the transaction
        - before_search: called before searching.  The search will continue if the
            callback returns True
        - after_search: called after a search has been performed.  The record change
            will undo if the callback returns False
        - record_changed: called after a record has changed (previous,next, etc.)
        - after_record_edit: called after the internal `DataSet` row is edited via a
            `sg.Table` cell-edit, or `field` live-update.

    Args:
        callback: The name of the callback, from the list above
        fctn: The function to call. Note, the function must take at least two
            parameters, a `Form` instance, and a `sg.Window` instance, with
            an optional `DataSet.key`, and return True or False

    Returns:
        None
    """
    logger.info(f"Callback {callback} being set on table {self.table}")
    supported = [
        "before_save",
        "after_save",
        "before_delete",
        "after_delete",
        "before_duplicate",
        "after_duplicate",
        "before_update",
        "after_update",  # Aliases for before/after_save
        "before_search",
        "after_search",
        "record_changed",
        "after_record_edit",
    ]
    if callback in supported:
        # handle our convenience aliases
        callback = "before_save" if callback == "before_update" else callback
        callback = "after_save" if callback == "after_update" else callback
        self.callbacks[callback] = lambda *args: self._invoke_callback(fctn, *args)
    else:
        raise RuntimeError(f'Callback "{callback}" not supported.')

set_transform(fn)

Set a transform on the data for this DataSet.

Here you can set custom a custom transform to both decode data from the database and encode data written to the database. This allows you to have dates stored as timestamps in the database yet work with a human-readable format in the GUI and within PySimpleSQL. This transform happens only while PySimpleSQL actually reads from or writes to the database.

Parameters:

Name Type Description Default
fn Callable

A callable function to preform encode/decode. This function should take three arguments: query, row (which will be populated by a dictionary of the row data), and an encode parameter (1 to encode, 0 to decode - see constants TFORM_ENCODE and TFORM_DECODE). Note that this transform works on one row at a time. See the example 'journal_with_data_manipulation.py' for a usage example.

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
def set_transform(self, fn: Callable) -> None:
    """Set a transform on the data for this `DataSet`.

    Here you can set custom a custom transform to both decode data from the
    database and encode data written to the database. This allows you to have dates
    stored as timestamps in the database yet work with a human-readable format in
    the GUI and within PySimpleSQL. This transform happens only while PySimpleSQL
    actually reads from or writes to the database.

    Args:
        fn: A callable function to preform encode/decode. This function should
            take three arguments: query, row (which will be populated by a
            dictionary of the row data), and an encode parameter (1 to encode, 0 to
            decode - see constants `TFORM_ENCODE` and `TFORM_DECODE`). Note that
            this transform works on one row at a time. See the example
            'journal_with_data_manipulation.py' for a usage example.

    Returns:
        None
    """
    self.transform = fn

set_query(query)

Set the query string for the DataSet.

This is more for advanced users. It defaults to "SELECT * FROM {table};" This can override the default

Parameters:

Name Type Description Default
query str

The query string you would like to associate with the table

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
def set_query(self, query: str) -> None:
    """Set the query string for the `DataSet`.

    This is more for advanced users.  It defaults to "SELECT * FROM {table};"
    This can override the default

    Args:
        query: The query string you would like to associate with the table

    Returns:
        None
    """
    logger.debug(f"Setting {self.table} query to {query}")
    self.query = query

set_join_clause(clause)

Set the DataSet object's join string.

This is more for advanced users, as it will automatically generate from the database Relationships otherwise.

Parameters:

Name Type Description Default
clause str

The join clause, such as "LEFT JOIN That on This.pk=That.fk"

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
def set_join_clause(self, clause: str) -> None:
    """Set the `DataSet` object's join string.

    This is more for advanced users, as it will automatically generate from the
    database Relationships otherwise.

    Args:
        clause: The join clause, such as "LEFT JOIN That on This.pk=That.fk"

    Returns:
        None
    """
    logger.debug(f"Setting {self.table} join clause to {clause}")
    self.join_clause = clause

set_where_clause(clause)

Set the DataSet object's where clause.

This is ADDED TO the auto-generated where clause from Relationship data

Parameters:

Name Type Description Default
clause str

The where clause, such as "WHERE pkThis=100"

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
def set_where_clause(self, clause: str) -> None:
    """Set the `DataSet` object's where clause.

    This is ADDED TO the auto-generated where clause from Relationship data

    Args:
        clause: The where clause, such as "WHERE pkThis=100"

    Returns:
        None
    """
    logger.debug(
        f"Setting {self.table} where clause to {clause} for DataSet {self.key}"
    )
    self.where_clause = clause

set_order_clause(clause)

Set the DataSet object's order clause.

This is more for advanced users, as it will automatically generate from the database Relationships otherwise.

Parameters:

Name Type Description Default
clause str

The order clause, such as "Order by name ASC"

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
def set_order_clause(self, clause: str) -> None:
    """Set the `DataSet` object's order clause.

    This is more for advanced users, as it will automatically generate from the
    database Relationships otherwise.

    Args:
        clause: The order clause, such as "Order by name ASC"

    Returns:
        None
    """
    logger.debug(f"Setting {self.table} order clause to {clause}")
    self.order_clause = clause

update_column_info(column_info=None)

Generate column information for the DataSet object.

This may need done, for example, when a manual query using joins is used. This is more for advanced users.

Parameters:

Name Type Description Default
column_info ColumnInfo

(optional) A ColumnInfo instance. Defaults to being generated by the SQLDriver.

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
def update_column_info(self, column_info: ColumnInfo = None) -> None:
    """Generate column information for the `DataSet` object.

    This may need done, for example, when a manual query using joins is used. This
    is more for advanced users.

    Args:
        column_info: (optional) A `ColumnInfo` instance. Defaults to being generated
            by the `SQLDriver`.

    Returns:
        None
    """
    # Now we need to set  new column names, as the query could have changed
    if column_info is not None:
        self.column_info = column_info
    else:
        self.column_info = self.driver.column_info(self.table)

set_description_column(column)

Set the DataSet object's description column.

This is the column that will display in Listboxes, Comboboxes, Tables, etc. By default, this is initialized to either the 'description','name' or 'title' column, or the 2nd column of the table if none of those columns exist. This method allows you to specify a different column to use as the description for the record.

Parameters:

Name Type Description Default
column str

The name of the column to use

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
def set_description_column(self, column: str) -> None:
    """Set the `DataSet` object's description column.

    This is the column that will display in Listboxes, Comboboxes, Tables, etc. By
    default, this is initialized to either the 'description','name' or 'title'
    column, or the 2nd column of the table if none of those columns exist. This
    method allows you to specify a different column to use as the description for
    the record.

    Args:
        column: The name of the column to use

    Returns:
        None
    """
    self.description_column = column

records_changed(column=None, recursive=True)

Checks if records have been changed.

This is done by comparing PySimpleGUI control values with the stored DataSet values.

Parameters:

Name Type Description Default
column str

Limit the changed records search to just the supplied column name

None
recursive bool

True to check related DataSet instances

True

Returns:

Type Description
bool

True or False on whether changed records were found

Source code in pysimplesql\pysimplesql.py
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
def records_changed(self, column: str = None, recursive: bool = True) -> bool:
    """Checks if records have been changed.

    This is done by comparing PySimpleGUI control values with the stored `DataSet`
    values.

    Args:
        column: Limit the changed records search to just the supplied column name
        recursive: True to check related `DataSet` instances

    Returns:
        True or False on whether changed records were found
    """
    logger.debug(f'Checking if records have changed in table "{self.table}"...')

    # Virtual rows wills always be considered dirty
    if self.pk_is_virtual():
        return True

    if self.current.has_backup and not self.current.get().equals(
        self.current.get_original()
    ):
        return True

    dirty = False
    # First check the current record to see if it's dirty
    for mapped in self.frm.element_map:
        # Compare the DB version to the GUI version
        if mapped.table == self.table:
            # if passed custom column name
            if column is not None and mapped.column != column:
                continue

            # if sg.Text
            if isinstance(mapped.element, sg.Text):
                continue

            # don't check if there aren't any rows. Fixes checkbox = '' when no
            # rows.
            if not len(self.frm[mapped.table].rows.index):
                continue

            # Get the element value and cast it, so we can compare it to the
            # database version.
            element_val = self.column_info[mapped.column].cast(mapped.element.get())

            # Get the table value.  If this is a keyed element, we need figure out
            # the appropriate table column.
            table_val = None
            if mapped.where_column is not None:
                for _, row in self.rows.iterrows():
                    if row[mapped.where_column] == mapped.where_value:
                        table_val = row[mapped.column]
            else:
                table_val = self[mapped.column]

            new_value = self.value_changed(
                mapped.column,
                table_val,
                element_val,
                bool(isinstance(mapped.element, sg.Checkbox)),
            )
            if new_value is not Boolean.FALSE:
                dirty = True
                logger.debug("CHANGED RECORD FOUND!")
                logger.debug(
                    f"\telement type: {type(element_val)} "
                    f"column_type: {type(table_val)}"
                )
                logger.debug(
                    f"\t{mapped.element.Key}:{element_val} != "
                    f"{mapped.column}:{table_val}"
                )
                return dirty

    # handle recursive checking next
    if recursive:
        for rel in self.frm.relationships:
            if rel.parent_table == self.table and rel.on_update_cascade:
                dirty = self.frm[rel.child_table].records_changed()
                if dirty:
                    break
    return dirty

value_changed(column_name, old_value, new_value, is_checkbox)

Verifies if a new value is different from an old value and returns the cast value ready to be inserted into a database.

Parameters:

Name Type Description Default
column_name str

The name of the column used in casting.

required
old_value

The value to check against.

required
new_value

The value being checked.

required
is_checkbox bool

Whether or not additional logic should be applied to handle checkboxes.

required

Returns:

Type Description
Union[Any, Boolean]

The cast value ready to be inserted into a database if the new value is

Union[Any, Boolean]

different from the old value. Returns FALSE otherwise.

Source code in pysimplesql\pysimplesql.py
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
def value_changed(
    self, column_name: str, old_value, new_value, is_checkbox: bool
) -> Union[Any, Boolean]:
    """Verifies if a new value is different from an old value and returns the cast
    value ready to be inserted into a database.

    Args:
        column_name: The name of the column used in casting.
        old_value: The value to check against.
        new_value: The value being checked.
        is_checkbox: Whether or not additional logic should be applied to handle
            checkboxes.

    Returns:
        The cast value ready to be inserted into a database if the new value is
        different from the old value. Returns `Boolean.FALSE` otherwise.
    """
    table_val = old_value
    # convert numpy to normal type
    with contextlib.suppress(AttributeError):
        table_val = table_val.tolist()

    # get cast new value to correct type
    for col in self.column_info:
        if col.name == column_name:
            new_value = col.cast(new_value)
            element_val = new_value
            table_val = col.cast(table_val)
            break

    if is_checkbox:
        table_val = checkbox_to_bool(table_val)
        element_val = checkbox_to_bool(element_val)

    # Sanitize things a bit due to empty values being slightly different in
    # the two cases.
    if table_val is None:
        table_val = ""

    # Strip trailing whitespace from strings
    if isinstance(table_val, str):
        table_val = table_val.rstrip()
    if isinstance(element_val, str):
        element_val = element_val.rstrip()

    # Make the comparison
    # Temporary debug output
    debug = False
    if debug:
        print(
            f"element: {element_val}({type(element_val)})"
            f"db: {table_val}({type(table_val)})"
        )
    if element_val != table_val:
        return new_value if new_value is not None else ""
    return Boolean.FALSE

prompt_save(update_elements=True)

Prompts the user, asking if they want to save when changes are detected.

This is called when the current record is about to change.

Parameters:

Name Type Description Default
update_elements bool

(optional) Passed to save_records() -> save_record_recursive() to update_elements. Additionally used to discard changes if user reply's 'No' to prompt.

True

Returns:

Type Description
Union[Type[PromptSaveReturn], SAVE_FAIL]

A prompt return value of one of the following: PROCEED,

Union[Type[PromptSaveReturn], SAVE_FAIL]
Source code in pysimplesql\pysimplesql.py
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
def prompt_save(
    self, update_elements: bool = True
) -> Union[Type[PromptSaveReturn], SAVE_FAIL]:
    """Prompts the user, asking if they want to save when changes are detected.

    This is called when the current record is about to change.

    Args:
        update_elements: (optional) Passed to `Form.save_records()` ->
            `DataSet.save_record_recursive()` to update_elements. Additionally used
            to discard changes if user reply's 'No' to prompt.

    Returns:
        A prompt return value of one of the following: `PromptSaveReturn.PROCEED`,
        `PromptSaveReturn.DISCARDED`, or `PromptSaveReturn.NONE`.
    """
    # Return False if there is nothing to check or _prompt_save is False
    if self.current.index is None or not self.row_count or not self._prompt_save:
        return PromptSaveReturn.NONE

    # See if any rows are virtual
    vrows = len(self.virtual_pks)

    # Check if any records have changed
    changed = self.records_changed() or vrows
    if changed:
        if self._prompt_save == AUTOSAVE_MODE:
            save_changes = "yes"
        else:
            save_changes = self.frm.popup.yes_no(
                lang.dataset_prompt_save_title, lang.dataset_prompt_save
            )
        if save_changes == "yes":
            # save this record's cascaded relationships, last to first
            if (
                self.frm.save_records(
                    table=self.table, update_elements=update_elements
                )
                & SAVE_FAIL
            ):
                logger.debug("Save failed during prompt-save. Resetting selectors")
                # set all selectors back to previous position
                self.frm.update_selectors()
                return SAVE_FAIL
            return PromptSaveReturn.PROCEED
        # if no
        self.purge_virtual()
        self.current.restore_backup()

        # set_by_index already takes care of this, but just in-case this method is
        # called another way.
        if vrows and update_elements:
            self.frm.update_elements(self.key)

        return PromptSaveReturn.DISCARDED
    # if no changes
    return PromptSaveReturn.NONE

requery(select_first=True, filtered=True, update_elements=True, requery_dependents=True)

Requeries the table.

The DataSet object maintains an internal representation of the actual database table. The requery method will query the actual database and sync the DataSet object to it.

Parameters:

Name Type Description Default
select_first bool

(optional) If True, the first record will be selected after the requery.

True
filtered bool

(optional) If True, the relationships will be considered and an appropriate WHERE clause will be generated. If False all records in the table will be fetched.

True
update_elements bool

(optional) Passed to first() to update_elements. Note that the select_first parameter must equal True to use this parameter.

True
requery_dependents bool

(optional) passed to first() to requery_dependents. Note that the select_first parameter must = True to use this parameter.

True

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
def requery(
    self,
    select_first: bool = True,
    filtered: bool = True,
    update_elements: bool = True,
    requery_dependents: bool = True,
) -> None:
    """Requeries the table.

    The `DataSet` object maintains an internal representation of
    the actual database table. The requery method will query the actual database and
    sync the `DataSet` object to it.

    Args:
        select_first: (optional) If True, the first record will be selected after
            the requery.
        filtered: (optional) If True, the relationships will be considered and an
            appropriate WHERE clause will be generated. If False all records in the
            table will be fetched.
        update_elements: (optional) Passed to `DataSet.first()` to update_elements.
            Note that the select_first parameter must equal True to use this
            parameter.
        requery_dependents: (optional) passed to `DataSet.first()` to
            requery_dependents. Note that the select_first parameter must = True to
            use this parameter.

    Returns:
        None
    """
    join = ""
    where = ""

    if not self.filtered:
        filtered = False

    if filtered:
        # Stop requery short if parent has no records or current row is virtual
        parent_table = self.relationships.get_parent(self.table)
        if parent_table and (
            not len(self.frm[parent_table].rows.index)
            or self.relationships.is_parent_virtual(self.table, self.frm)
        ):
            # purge rows
            self.rows = Result.set(pd.DataFrame(columns=self.column_info.names))

            if update_elements:
                self.frm.update_elements(self.key)
            if requery_dependents:
                self.requery_dependents(update_elements=update_elements)
            return

        # else, get join/where clause like normal
        join = self.driver.generate_join_clause(self)
        where = self.driver.generate_where_clause(self)

    query = self.query + " " + join + " " + where + " " + self.order_clause
    # We want to store our sort settings before we wipe out the current DataFrame
    try:
        sort_settings = self.store_sort_settings()
    except (AttributeError, KeyError):
        sort_settings = [None, SORT_NONE]  # default for first query

    rows = self.driver.execute(query)
    self.rows = rows

    if self.row_count and self.pk_column is not None:
        if "sort_order" not in self.rows.attrs:
            # Store the sort order as a dictionary in the attrs of the DataFrame
            sort_order = self.rows[self.pk_column].to_list()
            self.rows.attrs["sort_order"] = {self.pk_column: sort_order}
        # now we can restore the sort order
        self.load_sort_settings(sort_settings)
        self.sort(self.table)

    # Perform transform one row at a time
    if self.transform is not None:
        self.rows = self.rows.apply(
            lambda row: self.transform(self, row, TFORM_DECODE) or row, axis=1
        )

    # Strip trailing white space, as this is what sg[element].get() does, so we
    # can have an equal comparison. Not the prettiest solution.  Will look into
    # this more on the PySimpleGUI end and make a follow-up ticket.
    # TODO: Is the [:,:] still needed now that we are working with DateFrames?
    self.rows.loc[:, :] = self.rows.applymap(
        lambda x: x.rstrip() if isinstance(x, str) else x
    )

    # fill in columns if empty
    if self.rows.columns.empty:
        self.rows = Result.set(pd.DataFrame(columns=self.column_info.names))

    # reset search string
    self.search_string = ""

    if select_first:
        self.first(
            update_elements=update_elements,
            requery_dependents=requery_dependents,
            skip_prompt_save=True,  # already saved
        )

requery_dependents(child=False, update_elements=True)

Requery parent DataSet instances as defined by the relationships of the table.

Parameters:

Name Type Description Default
child bool

(optional) If True, will requery self. Default False; used to skip requery when called by parent.

False
update_elements bool

(optional) passed to requery() -> first() to update_elements.

True

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
def requery_dependents(
    self, child: bool = False, update_elements: bool = True
) -> None:
    """Requery parent `DataSet` instances as defined by the relationships of the
    table.

    Args:
        child: (optional) If True, will requery self. Default False; used to skip
            requery when called by parent.
        update_elements: (optional) passed to `DataSet.requery()` ->
            `DataSet.first()` to update_elements.

    Returns:
        None
    """
    if child:
        # dependents=False: no recursive dependent requery
        self.requery(update_elements=update_elements, requery_dependents=False)

    for rel in self.relationships:
        if rel.parent_table == self.table and rel.on_update_cascade:
            logger.debug(
                f"Requerying dependent table {self.frm[rel.child_table].table}"
            )
            self.frm[rel.child_table].requery_dependents(
                child=True, update_elements=update_elements
            )

first(update_elements=True, requery_dependents=True, skip_prompt_save=False)

Move to the first record of the table.

Only one entry in the table is ever considered "Selected" This is one of several functions that influences which record is currently selected. See first(), previous(), next(), last(), search(), set_by_pk(), set_by_index().

Parameters:

Name Type Description Default
update_elements bool

(optional) Update the GUI elements after switching records.

True
requery_dependents bool

(optional) Requery dependents after switching records

True
skip_prompt_save bool

(optional) True to skip prompting to save dirty records

False

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
def first(
    self,
    update_elements: bool = True,
    requery_dependents: bool = True,
    skip_prompt_save: bool = False,
) -> None:
    """Move to the first record of the table.

    Only one entry in the table is ever considered "Selected"  This is one of
    several functions that influences which record is currently selected. See
    `DataSet.first()`, `DataSet.previous()`, `DataSet.next()`, `DataSet.last()`,
    `DataSet.search()`, `DataSet.set_by_pk()`, `DataSet.set_by_index()`.

    Args:
        update_elements: (optional) Update the GUI elements after switching records.
        requery_dependents: (optional) Requery dependents after switching records
        skip_prompt_save: (optional) True to skip prompting to save dirty records

    Returns:
        None
    """
    logger.debug(f"Moving to the first record of table {self.table}")
    # prompt_save
    if (
        not skip_prompt_save
        # don't update self/dependents if we are going to below anyway
        and self.prompt_save(update_elements=False) == SAVE_FAIL
    ):
        return

    self.current.index = 0
    if update_elements:
        self.frm.update_elements(self.key)
    if requery_dependents:
        self.requery_dependents(update_elements=update_elements)
    # callback
    if "record_changed" in self.callbacks:
        self.callbacks["record_changed"](self.frm, self.frm.window, self.key)

last(update_elements=True, requery_dependents=True, skip_prompt_save=False)

Move to the last record of the table.

Only one entry in the table is ever considered "Selected". This is one of several functions that influences which record is currently selected. See first(), previous(), next(), last(), search(), set_by_pk(), set_by_index().

Parameters:

Name Type Description Default
update_elements bool

(optional) Update the GUI elements after switching records.

True
requery_dependents bool

(optional) Requery dependents after switching records

True
skip_prompt_save bool

(optional) True to skip prompting to save dirty records

False

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
def last(
    self,
    update_elements: bool = True,
    requery_dependents: bool = True,
    skip_prompt_save: bool = False,
) -> None:
    """Move to the last record of the table.

    Only one entry in the table is ever considered "Selected". This is one of
    several functions that influences which record is currently selected. See
    `DataSet.first()`, `DataSet.previous()`, `DataSet.next()`, `DataSet.last()`,
    `DataSet.search()`, `DataSet.set_by_pk()`, `DataSet.set_by_index()`.

    Args:
        update_elements: (optional) Update the GUI elements after switching records.
        requery_dependents: (optional) Requery dependents after switching records
        skip_prompt_save: (optional) True to skip prompting to save dirty records

    Returns:
        None
    """
    logger.debug(f"Moving to the last record of table {self.table}")
    # prompt_save
    if (
        not skip_prompt_save
        # don't update self/dependents if we are going to below anyway
        and self.prompt_save(update_elements=False) == SAVE_FAIL
    ):
        return

    self.current.index = self.row_count - 1

    if update_elements:
        self.frm.update_elements(self.key)
    if requery_dependents:
        self.requery_dependents()
    # callback
    if "record_changed" in self.callbacks:
        self.callbacks["record_changed"](self.frm, self.frm.window, self.key)

next(update_elements=True, requery_dependents=True, skip_prompt_save=False)

Move to the next record of the table.

Only one entry in the table is ever considered "Selected". This is one of several functions that influences which record is currently selected. See first(), previous(), next(), last(), search(), set_by_pk(), set_by_index().

Parameters:

Name Type Description Default
update_elements bool

(optional) Update the GUI elements after switching records.

True
requery_dependents bool

(optional) Requery dependents after switching records

True
skip_prompt_save bool

(optional) True to skip prompting to save dirty records

False

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
def next(
    self,
    update_elements: bool = True,
    requery_dependents: bool = True,
    skip_prompt_save: bool = False,
) -> None:
    """Move to the next record of the table.

    Only one entry in the table is ever considered "Selected". This is one of
    several functions that influences which record is currently selected. See
    `DataSet.first()`, `DataSet.previous()`, `DataSet.next()`, `DataSet.last()`,
    `DataSet.search()`, `DataSet.set_by_pk()`, `DataSet.set_by_index()`.

    Args:
        update_elements: (optional) Update the GUI elements after switching records.
        requery_dependents: (optional) Requery dependents after switching records
        skip_prompt_save: (optional) True to skip prompting to save dirty records

    Returns:
        None
    """
    if self.current.index < self.row_count - 1:
        logger.debug(f"Moving to the next record of table {self.table}")
        # prompt_save
        if (
            not skip_prompt_save
            # don't update self/dependents if we are going to below anyway
            and self.prompt_save(update_elements=False) == SAVE_FAIL
        ):
            return

        self.current.index += 1
        if update_elements:
            self.frm.update_elements(self.key)
        if requery_dependents:
            self.requery_dependents()
        # callback
        if "record_changed" in self.callbacks:
            self.callbacks["record_changed"](self.frm, self.frm.window, self.key)

previous(update_elements=True, requery_dependents=True, skip_prompt_save=False)

Move to the previous record of the table.

Only one entry in the table is ever considered "Selected". This is one of several functions that influences which record is currently selected. See first(), previous(), next(), last(), search(), set_by_pk(), set_by_index().

Parameters:

Name Type Description Default
update_elements bool

(optional) Update the GUI elements after switching records.

True
requery_dependents bool

(optional) Requery dependents after switching records

True
skip_prompt_save bool

(optional) True to skip prompting to save dirty records

False

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
def previous(
    self,
    update_elements: bool = True,
    requery_dependents: bool = True,
    skip_prompt_save: bool = False,
) -> None:
    """Move to the previous record of the table.

    Only one entry in the table is ever considered "Selected". This is one of
    several functions that influences which record is currently selected. See
    `DataSet.first()`, `DataSet.previous()`, `DataSet.next()`, `DataSet.last()`,
    `DataSet.search()`, `DataSet.set_by_pk()`, `DataSet.set_by_index()`.

    Args:
        update_elements: (optional) Update the GUI elements after switching records.
        requery_dependents: (optional) Requery dependents after switching records
        skip_prompt_save: (optional) True to skip prompting to save dirty records

    Returns:
        None
    """
    if self.current.index > 0:
        logger.debug(f"Moving to the previous record of table {self.table}")
        # prompt_save
        if (
            not skip_prompt_save
            # don't update self/dependents if we are going to below anyway
            and self.prompt_save(update_elements=False) == SAVE_FAIL
        ):
            return

        self.current.index -= 1
        if update_elements:
            self.frm.update_elements(self.key)
        if requery_dependents:
            self.requery_dependents()
        # callback
        if "record_changed" in self.callbacks:
            self.callbacks["record_changed"](self.frm, self.frm.window, self.key)

search(search_string, update_elements=True, requery_dependents=True, skip_prompt_save=False, display_message=None)

Move to the next record in the DataSet that contains [search_string][pysimplesql.pysimplesql.search_string].

Successive calls will search from the current position, and wrap around back to the beginning. The search order from set_search_order() will be used. If the search order is not set by the user, it will default to the description column (see set_description_column()). Only one entry in the table is ever considered "Selected" This is one of several functions that influences which record is currently selected. See first(), previous(), next(), last(), search(), set_by_pk(), set_by_index().

Parameters:

Name Type Description Default
search_string str

The search string to look for

required
update_elements bool

(optional) Update the GUI elements after switching records.

True
requery_dependents bool

(optional) Requery dependents after switching records

True
skip_prompt_save bool

(optional) True to skip prompting to save dirty records

False
display_message bool

Displays a message "Search Failed: ...", otherwise is silent on fail.

None

Returns:

Type Description
Union[SEARCH_FAILED, SEARCH_RETURNED, SEARCH_ABORTED]

One of the following search values: [SEARCH_FAILED][pysimplesql.pysimplesql.SEARCH_FAILED], [SEARCH_RETURNED][pysimplesql.pysimplesql.SEARCH_RETURNED],

Union[SEARCH_FAILED, SEARCH_RETURNED, SEARCH_ABORTED]

[SEARCH_ABORTED][pysimplesql.pysimplesql.SEARCH_ABORTED].

Source code in pysimplesql\pysimplesql.py
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
def search(
    self,
    search_string: str,
    update_elements: bool = True,
    requery_dependents: bool = True,
    skip_prompt_save: bool = False,
    display_message: bool = None,
) -> Union[SEARCH_FAILED, SEARCH_RETURNED, SEARCH_ABORTED]:
    """Move to the next record in the `DataSet` that contains `search_string`.

    Successive calls will search from the current position, and wrap around back to
    the beginning. The search order from `DataSet.set_search_order()` will be used.
    If the search order is not set by the user, it will default to the description
    column (see `DataSet.set_description_column()`). Only one entry in the table is
    ever considered "Selected"  This is one of several functions that influences
    which record is currently selected. See `DataSet.first()`, `DataSet.previous()`,
    `DataSet.next()`, `DataSet.last()`, `DataSet.search()`, `DataSet.set_by_pk()`,
    `DataSet.set_by_index()`.

    Args:
        search_string: The search string to look for
        update_elements: (optional) Update the GUI elements after switching records.
        requery_dependents: (optional) Requery dependents after switching records
        skip_prompt_save: (optional) True to skip prompting to save dirty records
        display_message: Displays a message "Search Failed: ...", otherwise is
            silent on fail.

    Returns:
        One of the following search values: `SEARCH_FAILED`, `SEARCH_RETURNED`,
        `SEARCH_ABORTED`.
    """
    # See if the string is an element name
    # TODO this is a bit of an ugly hack, but it works
    if search_string in self.frm.window.key_dict:
        search_string = self.frm.window[search_string].get()
    if not search_string or not self.row_count:
        return SEARCH_ABORTED

    logger.debug(
        f'Searching for a record of table {self.table} "'
        f'with search string "{search_string}"'
    )
    logger.debug(f"DEBUG: {self.search_order} {self.rows.columns[0]}")

    # prompt_save
    if (
        not skip_prompt_save
        # don't update self/dependents if we are going to below anyway
        and self.prompt_save(update_elements=False) == SAVE_FAIL
    ):
        return None

    # callback
    if "before_search" in self.callbacks and not self.callbacks["before_search"](
        self.frm, self.frm.window, self.key
    ):
        return SEARCH_ABORTED

    # Reset _prev_search if search_string is different
    if search_string != self._prev_search.search_string:
        self._prev_search = _PrevSearch(search_string)

    # Reorder search_columns to start with the column in _prev_search
    search_columns = self.search_order.copy()
    if self._prev_search.column in search_columns:
        idx = search_columns.index(self._prev_search.column)
        search_columns = search_columns[idx:] + search_columns[:idx]

    # reorder rows to be idx + 1, and wrap around back to the beginning
    rows = self.rows.copy().reset_index()
    idx = self.current.index + 1 % len(rows)
    rows = pd.concat([rows.loc[idx:], rows.loc[:idx]])

    # fill in descriptions for cols in search_order
    rows = self.map_fk_descriptions(rows, self.search_order)

    pk = None
    for column in search_columns:
        # update _prev_search column
        self._prev_search.column = column

        # search through processed rows, looking for search_string
        result = rows[
            rows[column].astype(str).str.contains(str(search_string), case=False)
        ]
        if not result.empty:
            # save index for later, if callback returns False
            old_index = self.current.index

            # grab the first result
            pk = result.iloc[0][self.pk_column]

            # search next column if the same pk is found again
            if pk in self._prev_search.pks:
                continue

            # if pk is same as one we are on, we can just updated_elements
            if pk == self[self.pk_column]:
                if update_elements:
                    self.frm.update_elements(self.key)
                if requery_dependents:
                    self.requery_dependents()
                return SEARCH_RETURNED

            # otherwise, this is a new pk
            break

    if pk:
        # Update _prev_search with the pk
        self._prev_search.pks.append(pk)

        # jump to the pk
        self.set_by_pk(
            pk=pk,
            update_elements=update_elements,
            requery_dependents=requery_dependents,
            skip_prompt_save=True,
        )

        # callback
        if "after_search" in self.callbacks and not self.callbacks["after_search"](
            self.frm, self.frm.window, self.key
        ):
            self.current.index = old_index
            self.frm.update_elements(self.key)
            self.requery_dependents()
            return SEARCH_ABORTED

        # record changed callback
        if "record_changed" in self.callbacks:
            self.callbacks["record_changed"](self.frm, self.frm.window, self.key)
        return SEARCH_RETURNED

    # didn't find anything
    self.frm.popup.ok(
        lang.dataset_search_failed_title,
        lang.dataset_search_failed.format_map(
            LangFormat(search_string=search_string)
        ),
    )
    return SEARCH_FAILED

set_by_index(index, update_elements=True, requery_dependents=True, skip_prompt_save=False, omit_elements=None)

Move to the record of the table located at the specified index in DataSet.

Only one entry in the table is ever considered "Selected". This is one of several functions that influences which record is currently selected. See first(), previous(), next(), last(), search(), set_by_pk(), set_by_index().

Parameters:

Name Type Description Default
index int

The index of the record to move to.

required
update_elements bool

(optional) Update the GUI elements after switching records.

True
requery_dependents bool

(optional) Requery dependents after switching records

True
skip_prompt_save bool

(optional) True to skip prompting to save dirty records

False
omit_elements List[str]

(optional) A list of elements to omit from updating

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
def set_by_index(
    self,
    index: int,
    update_elements: bool = True,
    requery_dependents: bool = True,
    skip_prompt_save: bool = False,
    omit_elements: List[str] = None,
) -> None:
    """Move to the record of the table located at the specified index in DataSet.

    Only one entry in the table is ever considered "Selected". This is one of
    several functions that influences which record is currently selected. See
    `DataSet.first()`, `DataSet.previous()`, `DataSet.next()`, `DataSet.last()`,
    `DataSet.search()`, `DataSet.set_by_pk()`, `DataSet.set_by_index()`.

    Args:
        index: The index of the record to move to.
        update_elements: (optional) Update the GUI elements after switching records.
        requery_dependents: (optional) Requery dependents after switching records
        skip_prompt_save: (optional) True to skip prompting to save dirty records
        omit_elements: (optional) A list of elements to omit from updating

    Returns:
        None
    """
    # if already there
    if self.current.index == index:
        return

    logger.debug(f"Moving to the record at index {index} on {self.table}")
    if omit_elements is None:
        omit_elements = []

    if skip_prompt_save is False:
        # see if sg.Table has potential changes
        if len(omit_elements) and self.records_changed(recursive=False):
            # most likely will need to update, either to
            # discard virtual or update after save
            omit_elements = []
        # don't update self/dependents if we are going to below anyway
        if self.prompt_save(update_elements=False) == SAVE_FAIL:
            return

    self.current.index = index
    if update_elements:
        self.frm.update_elements(self.key, omit_elements=omit_elements)
    if requery_dependents:
        self.requery_dependents()

set_by_pk(pk, update_elements=True, requery_dependents=True, skip_prompt_save=False, omit_elements=None)

Move to the record with this primary key.

This is useful when modifying a record (such as renaming). The primary key can be stored, the record re-named, and then the current record selection updated regardless of the new sort order.

Only one entry in the table is ever considered "Selected". This is one of several functions that influences which record is currently selected. See first(), previous(), next(), last(), search(), set_by_pk(), set_by_index().

Parameters:

Name Type Description Default
pk int

The record to move to containing the primary key

required
update_elements bool

(optional) Update the GUI elements after switching records.

True
requery_dependents bool

(optional) Requery dependents after switching records

True
skip_prompt_save bool

(optional) True to skip prompting to save dirty records

False
omit_elements list[str]

(optional) A list of elements to omit from updating

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
def set_by_pk(
    self,
    pk: int,
    update_elements: bool = True,
    requery_dependents: bool = True,
    skip_prompt_save: bool = False,
    omit_elements: list[str] = None,
) -> None:
    """Move to the record with this primary key.

    This is useful when modifying a record (such as renaming).  The primary key can
    be stored, the record re-named, and then the current record selection updated
    regardless of the new sort order.

    Only one entry in the table is ever considered "Selected". This is one of
    several functions that influences which record is currently selected. See
    `DataSet.first()`, `DataSet.previous()`, `DataSet.next()`, `DataSet.last()`,
    `DataSet.search()`, `DataSet.set_by_pk()`, `DataSet.set_by_index()`.

    Args:
        pk: The record to move to containing the primary key
        update_elements: (optional) Update the GUI elements after switching records.
        requery_dependents: (optional) Requery dependents after switching records
        skip_prompt_save: (optional) True to skip prompting to save dirty records
        omit_elements: (optional) A list of elements to omit from updating

    Returns:
        None
    """
    logger.debug(f"Setting table {self.table} record by primary key {pk}")

    # Get the numerical index of where the primary key is located.
    # If the pk value can't be found, set to the last index
    try:
        idx = [
            i for i, value in enumerate(self.rows[self.pk_column]) if value == pk
        ]
    except (IndexError, KeyError):
        idx = None
        logger.debug("Error finding pk!")

    idx = idx[0] if idx else self.row_count

    self.set_by_index(
        index=idx,
        update_elements=update_elements,
        requery_dependents=requery_dependents,
        skip_prompt_save=skip_prompt_save,
        omit_elements=omit_elements,
    )

get_keyed_value(value_column, key_column, key_value)

Return [value_column][pysimplesql.pysimplesql.value_column] wherekey_column=[key_value][pysimplesql.pysimplesql.key_value].

Useful for datastores with key/value pairs.

Parameters:

Name Type Description Default
value_column str

The column to fetch the value from

required
key_column str

The column in which to search for the value

required
key_value Union[str, int]

The value to search for

required

Returns:

Type Description
Union[str, int, None]

Returns the value found in [value_column][pysimplesql.pysimplesql.value_column]

Source code in pysimplesql\pysimplesql.py
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
def get_keyed_value(
    self, value_column: str, key_column: str, key_value: Union[str, int]
) -> Union[str, int, None]:
    """Return `value_column` where` key_column`=`key_value`.

    Useful for datastores with key/value pairs.

    Args:
        value_column: The column to fetch the value from
        key_column: The column in which to search for the value
        key_value: The value to search for

    Returns:
        Returns the value found in `value_column`
    """
    for _, row in self.rows.iterrows():
        if row[key_column] == key_value:
            return row[value_column]
    return None

add_selector(element, data_key, where_column=None, where_value=None)

Use an element such as a listbox, combobox or a table as a selector item for this table.

Note: This is not typically used by the end user, as this is called from the selector() convenience function.

Parameters:

Name Type Description Default
element Element

the PySimpleGUI element used as a selector element

required
data_key str

the DataSet item this selector will operate on

required
where_column str

(optional)

None
where_value str

(optional)

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
def add_selector(
    self,
    element: sg.Element,
    data_key: str,
    where_column: str = None,
    where_value: str = None,
) -> None:
    """Use an element such as a listbox, combobox or a table as a selector item for
    this table.

    Note: This is not typically used by the end user, as this is called from the
    `selector()` convenience function.

    Args:
        element: the PySimpleGUI element used as a selector element
        data_key: the `DataSet` item this selector will operate on
        where_column: (optional)
        where_value: (optional)

    Returns:
        None
    """
    if not isinstance(element, (sg.Listbox, sg.Slider, sg.Combo, sg.Table)):
        raise RuntimeError(
            f"add_selector() error: {element} is not a supported element."
        )

    logger.debug(f"Adding {element.Key} as a selector for the {self.table} table.")
    d = {
        "element": element,
        "data_key": data_key,
        "where_column": where_column,
        "where_value": where_value,
    }
    self.selector.append(d)

insert_record(values=None, skip_prompt_save=False)

Insert a new record virtually in the DataSet object.

If values are passed, it will initially set those columns to the values (I.e. {'name': 'New Record', 'note': ''}), otherwise they will be fetched from the database if present.

Parameters:

Name Type Description Default
values Dict[str, Union[str, int]]

column:value pairs

None
skip_prompt_save bool

Skip prompting the user to save dirty records before the insert.

False

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
def insert_record(
    self, values: Dict[str, Union[str, int]] = None, skip_prompt_save: bool = False
) -> None:
    """Insert a new record virtually in the `DataSet` object.

    If values are passed, it will initially set those columns to the values (I.e.
    {'name': 'New Record', 'note': ''}), otherwise they will be fetched from the
    database if present.

    Args:
        values: column:value pairs
        skip_prompt_save: Skip prompting the user to save dirty records before the
            insert.

    Returns:
        None
    """
    # prompt_save
    if (
        not skip_prompt_save
        # don't update self/dependents if we are going to below anyway
        and self.prompt_save(update_elements=False) == SAVE_FAIL
    ):
        return

    # Don't insert if parent has no records or is virtual
    parent_table = self.relationships.get_parent(self.table)
    if (
        parent_table
        and not len(self.frm[parent_table].rows)
        or self.relationships.is_parent_virtual(self.table, self.frm)
    ):
        logger.debug(f"{parent_table=} is empty or current row is virtual")
        return

    # Get a new dict for a new row with default values already filled in
    new_values = self.column_info.default_row_dict(self)

    # If the values parameter was passed in, overwrite any values in the dict
    if values is not None:
        for k, v in values.items():
            if k in new_values:
                new_values[k] = v

    # Make sure we take into account the foreign key relationships...
    for r in self.relationships:
        if self.table == r.child_table and r.on_update_cascade:
            new_values[r.fk_column] = self.frm[r.parent_table].current.pk

    # Update the pk to match the expected pk the driver would generate on insert.
    new_values[self.pk_column] = self.driver.next_pk(self.table, self.pk_column)

    # Insert the new values using DataSet.insert_row(),
    # marking the new row as virtual
    self.insert_row(new_values)

    # and move to the new record
    # do this in insert_record, because possibly current.index is already 0
    # and set_by_index will return early before update/requery if so.
    self.current.index = self.row_count
    self.frm.update_elements(self.key)
    self.requery_dependents()

save_record(display_message=None, update_elements=True, validate_fields=None)

Save the currently selected record.

Saves any changes made via the GUI back to the database. The before_save and after_save [callbacks][pysimplesql.pysimplesql.DataSet.callbacks] will call your own functions for error checking if needed!.

Parameters:

Name Type Description Default
display_message bool

Displays a message "Updates saved successfully", otherwise is silent on success.

None
update_elements bool

Update the GUI elements after saving

True
validate_fields bool

Validate fields before saving to database.

None

Returns:

Type Description
int

SAVE_NONE, SAVE_FAIL or SAVE_SUCCESS masked with SHOW_MESSAGE

Source code in pysimplesql\pysimplesql.py
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
def save_record(
    self,
    display_message: bool = None,
    update_elements: bool = True,
    validate_fields: bool = None,
) -> int:
    """Save the currently selected record.

    Saves any changes made via the GUI back to the database.  The
    before_save and after_save `DataSet.callbacks` will call your
    own functions for error checking if needed!.

    Args:
        display_message: Displays a message "Updates saved successfully", otherwise
            is silent on success.
        update_elements: Update the GUI elements after saving
        validate_fields: Validate fields before saving to database.

    Returns:
        SAVE_NONE, SAVE_FAIL or SAVE_SUCCESS masked with SHOW_MESSAGE
    """
    logger.debug(f"Saving records for table {self.table}...")
    if display_message is None:
        display_message = not self.save_quiet

    if validate_fields is None:
        validate_fields = self.validate_mode

    # Ensure that there is actually something to save
    if not self.row_count:
        self.frm.popup.info(
            lang.dataset_save_empty, display_message=display_message
        )
        return SAVE_NONE + SHOW_MESSAGE

    # callback
    if "before_save" in self.callbacks and not self.callbacks["before_save"](
        self.frm, self.frm.window, self.key
    ):
        logger.debug("We are not saving!")
        if update_elements:
            self.frm.update_elements(self.key)
        if display_message:
            self.frm.popup.ok(
                lang.dataset_save_callback_false_title,
                lang.dataset_save_callback_false,
            )
        return SAVE_FAIL + SHOW_MESSAGE

    # Check right away to see if any records have changed, no need to proceed any
    # further than we have to.
    if not self.records_changed(recursive=False) and self.frm.force_save is False:
        self.frm.popup.info(lang.dataset_save_none, display_message=display_message)
        return SAVE_NONE + SHOW_MESSAGE

    # Work with a copy of the original row and transform it if needed
    # While saving, we are working with just the current row of data,
    # unless it's 'keyed' via ?/=
    current_row = self.current.get().copy()

    # Track the keyed queries we have to run.
    # Set to None, so we can tell later if there were keyed elements
    # {'column':column, 'changed_row': row, 'where_clause': where_clause}
    keyed_queries: Optional[List] = None

    # Propagate GUI data back to the stored current_row
    for mapped in [m for m in self.frm.element_map if m.dataset == self]:
        # skip if sg.Text
        if isinstance(mapped.element, sg.Text):
            continue

        # convert the data into the correct type using the domain in ColumnInfo
        if isinstance(mapped.element, sg.Combo):
            # try to get ElementRow pk
            try:
                element_val = self.column_info[mapped.column].cast(
                    mapped.element.get().get_pk_ignore_placeholder()
                )
            # of if plain-ole combobox:
            except AttributeError:
                element_val = self.column_info[mapped.column].cast(
                    mapped.element.get()
                )
        else:
            element_val = self.column_info[mapped.column].cast(mapped.element.get())

        # Looked for keyed elements first
        if mapped.where_column is not None:
            if keyed_queries is None:
                # Make the list here so != None if keyed elements
                keyed_queries = []
            for index, row in self.rows.iterrows():
                if (
                    row[mapped.where_column] == mapped.where_value
                    and row[mapped.column] != element_val
                ):
                    # This record has changed.  We will save it

                    # propagate the value back to self.rows
                    self.rows.loc[
                        self.rows.index[index], mapped.column
                    ] = element_val

                    changed = {mapped.column: element_val}
                    where_col = self.driver.quote_column(mapped.where_column)
                    where_val = self.driver.quote_value(mapped.where_value)
                    where_clause = f"WHERE {where_col} = {where_val}"
                    keyed_queries.append(
                        {
                            "column": mapped.column,
                            "changed_row": changed,
                            "where_clause": where_clause,
                        }
                    )
        else:
            # field elements override _CellEdit's
            current_row[mapped.column] = element_val

    # create diff of columns if not virtual
    new_dict = current_row.fillna("").to_dict()

    if self.pk_is_virtual():
        changed_row_dict = new_dict
    else:
        old_dict = self.current.get_original().fillna("").to_dict()
        changed_row_dict = {
            key: new_dict[key]
            for key in new_dict
            if old_dict.get(key) != new_dict[key]
        }

    # Remove the pk column, any virtual or generated columns
    changed_row_dict = {
        col: value
        for col, value in changed_row_dict.items()
        if col != self.pk_column
        and col not in self.column_info.get_virtual_names()
        and not self.column_info[col].generated
    }

    if not bool(changed_row_dict) and not keyed_queries:
        # if user is not using liveupdate, they can change something using celledit
        # but then change it back in field element (which overrides the celledit)
        # this refreshes the selector/comboboxes so that gui is up-to-date.
        if self.current.has_backup:
            self.current.restore_backup()
            self.frm.update_selectors(self.key)
            self.frm.update_fields(self.key)
        return SAVE_NONE + SHOW_MESSAGE

    # apply any transformations
    if self.transform is not None:
        self.transform(self, changed_row_dict, TFORM_ENCODE)

    # check to make sure we have valid inputs
    if validate_fields:
        invalid_response = {}
        for col, value in changed_row_dict.items():
            response = self.column_info[col].validate(value)
            if response.exception:
                invalid_response[col] = response
        if invalid_response:
            msg = f"{lang.dataset_save_validate_error_header}"
            for col, response in invalid_response.items():
                field = lang.dataset_save_validate_error_field.format_map(
                    LangFormat(field=col)
                )
                exception = lang[response.exception].format_map(
                    LangFormat(value=response.value, rule=response.rule)
                )
                msg += f"{field}{exception}\n"
            self.frm.popup.ok(lang.dataset_save_validate_error_title, msg)
            return SAVE_FAIL

    # check to see if cascading-fk has changed before we update database
    cascade_fk_changed = False
    cascade_fk_column = self.relationships.get_update_cascade_fk_column(self.table)
    if cascade_fk_column:
        # check if fk
        for mapped in self.frm.element_map:
            if mapped.dataset == self and mapped.column == cascade_fk_column:
                cascade_fk_changed = self.records_changed(
                    column=cascade_fk_column, recursive=False
                )

    # Update the database from the stored rows
    # ----------------------------------------

    # reset search string
    self.search_string = ""

    # Save or Insert the record as needed
    if keyed_queries is not None:
        # Now execute all the saved queries from earlier
        for q in keyed_queries:
            # Update the database from the stored rows
            if self.transform is not None:
                self.transform(self, q["changed_row"], TFORM_ENCODE)
            result = self.driver.save_record(
                self, q["changed_row"], q["where_clause"]
            )
            if result.attrs["exception"] is not None:
                self.frm.popup.ok(
                    lang.dataset_save_keyed_fail_title,
                    lang.dataset_save_keyed_fail.format_map(
                        LangFormat(exception=result.exception)
                    ),
                )
                self.driver.rollback()
                return SAVE_FAIL  # Do not show the message in this case

    else:
        if self.pk_is_virtual():
            result = self.driver.insert_record(
                self.table, self.current.pk, self.pk_column, changed_row_dict
            )
        else:
            result = self.driver.save_record(self, changed_row_dict)

        if result.attrs["exception"] is not None:
            self.frm.popup.ok(
                lang.dataset_save_fail_title,
                lang.dataset_save_fail.format_map(
                    LangFormat(exception=result.attrs["exception"])
                ),
            )
            self.driver.rollback()
            return SAVE_FAIL  # Do not show the message in this case

        # Store the pk, so we can move to it later - use the value returned in the
        # attrs if possible. The expected pk may have changed from autoincrement
        # and/or concurrent access.
        pk = (
            result.attrs["lastrowid"]
            if result.attrs["lastrowid"] is not None
            else self.current.pk
        )
        self.current.set_value(self.pk_column, pk, write_event=False)

        # then update the current row data
        self.rows.iloc[self.current.index] = current_row

        # If child changes parent, move index back and requery/requery_dependents
        if (
            cascade_fk_changed and not self.pk_is_virtual()
        ):  # Virtual rows already requery, and have no dependents.
            self.frm[self.table].requery(select_first=False)  # keep spot in table
            self.frm[self.table].requery_dependents()

        # Lets refresh our data
        if self.pk_is_virtual():
            # Requery so that the new row honors the order clause
            self.requery(select_first=False, update_elements=False)
            if update_elements:
                # Then move to the record
                self.set_by_pk(
                    pk,
                    skip_prompt_save=True,
                    requery_dependents=False,
                )
                # only need to reset the Insert button
                self.frm.update_actions()

    # callback
    if "after_save" in self.callbacks and not self.callbacks["after_save"](
        self.frm, self.frm.window, self.key
    ):
        self.driver.rollback()
        return SAVE_FAIL + SHOW_MESSAGE

    # If we made it here, we can commit the changes, since the save and insert above
    # do not commit or rollback
    self.driver.commit()

    # Sort so the saved row honors the current order.
    if "sort_column" in self.rows.attrs and self.rows.attrs["sort_column"]:
        self.sort(self.table)

    # Discard backup
    self.purge_row_backup()

    if update_elements:
        self.frm.update_elements(self.key)

    # if the description_column has changed, make sure to update other elements
    # that may depend on it, that otherwise wouldn't be requeried because they are
    # not setup as on_update_cascade.
    if self.description_column in changed_row_dict:
        dependent_columns = self.relationships.get_dependent_columns(
            self.frm, self.table
        )
        for key, col in dependent_columns.items():
            self.frm.update_fields(key, columns=[col], combo_values_only=True)
            if self.frm[key].column_likely_in_selector(col):
                self.frm.update_selectors(key)

    logger.debug("Record Saved!")
    self.frm.popup.info(lang.dataset_save_success, display_message=display_message)

    return SAVE_SUCCESS + SHOW_MESSAGE

save_record_recursive(results, display_message=False, check_prompt_save=False, update_elements=True)

Recursively save changes, taking into account the relationships of the tables.

Parameters:

Name Type Description Default
results SaveResultsDict

Used in save_records to collect save_record returns. Pass an empty dict to get list of {table : result}

required
display_message bool

Passed to save_record. Displays a message that updates were saved successfully, otherwise is silent on success.

False
check_prompt_save bool

Used when called from prompt_save. Updates elements without saving if individual [_prompt_save()][pysimplesql.pysimplesql.DataSet._prompt_save] is False.

False
update_elements bool

Update GUI elements, additionally passed to dependents.

True

Returns:

Type Description
SaveResultsDict

dict of {table : results}

Source code in pysimplesql\pysimplesql.py
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
def save_record_recursive(
    self,
    results: SaveResultsDict,
    display_message: bool = False,
    check_prompt_save: bool = False,
    update_elements: bool = True,
) -> SaveResultsDict:
    """Recursively save changes, taking into account the relationships of the
    tables.

    Args:
        results: Used in `Form.save_records` to collect `DataSet.save_record`
            returns. Pass an empty dict to get list of {table : result}
        display_message: Passed to `DataSet.save_record`. Displays a message that
            updates were saved successfully, otherwise is silent on success.
        check_prompt_save: Used when called from `Form.prompt_save`. Updates
            elements without saving if individual `DataSet._prompt_save()` is False.
        update_elements: Update GUI elements, additionally passed to dependents.

    Returns:
        dict of {table : results}
    """
    for rel in self.relationships:
        if rel.parent_table == self.table and rel.on_update_cascade:
            self.frm[rel.child_table].save_record_recursive(
                results=results,
                display_message=display_message,
                check_prompt_save=check_prompt_save,
                update_elements=update_elements,
            )
    # if dataset-level doesn't allow prompt_save
    if check_prompt_save and self._prompt_save is False:
        if update_elements:
            self.frm.update_elements(self.key)
        results[self.table] = PromptSaveReturn.NONE
        return results
    # otherwise, proceed
    result = self.save_record(
        display_message=display_message, update_elements=update_elements
    )
    results[self.table] = result
    return results

delete_record(cascade=True)

Delete the currently selected record.

The before_delete and after_delete callbacks are run during this process to give some control over the process.

Parameters:

Name Type Description Default
cascade bool

Delete child records (as defined by Relationships that were set up) before deleting this record.

True

Returns:

Type Description

None

Source code in pysimplesql\pysimplesql.py
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
def delete_record(
    self, cascade: bool = True
):  # TODO: check return type, we return True below
    """Delete the currently selected record.

    The before_delete and after_delete callbacks are run during this process
    to give some control over the process.

    Args:
        cascade: Delete child records (as defined by `Relationship`s that were set
            up) before deleting this record.

    Returns:
        None
    """
    # Ensure that there is actually something to delete
    if not self.row_count:
        return None

    # callback
    if "before_delete" in self.callbacks and not self.callbacks["before_delete"](
        self.frm, self.frm.window, self.key
    ):
        return None

    children = []
    if cascade:
        children = self.relationships.get_delete_cascade_tables(self.table)

    msg_children = ", ".join(children)
    if len(children):
        msg = lang.delete_cascade.format_map(LangFormat(children=msg_children))
    else:
        msg = lang.delete_single
    answer = self.frm.popup.yes_no(lang.delete_title, msg)
    if answer == "no":
        return True

    if self.pk_is_virtual():
        self.purge_virtual()
        self.frm.update_elements(self.key)
        # only need to reset the Insert button
        self.frm.update_actions()
        return None

    # Delete child records first!
    result = self.driver.delete_record(self, True)

    if (
        not isinstance(result, pd.DataFrame)
        and result == DELETE_RECURSION_LIMIT_ERROR
    ):
        self.frm.popup.ok(
            lang.delete_failed_title,
            lang.delete_failed.format_map(
                LangFormat(exception=lang.delete_recursion_limit_error)
            ),
        )
    elif result.attrs["exception"] is not None:
        self.frm.popup.ok(
            lang.delete_failed_title,
            lang.delete_failed.format_map(LangFormat(exception=result.exception)),
        )

    # callback
    if "after_delete" in self.callbacks:
        if not self.callbacks["after_delete"](self.frm, self.frm.window, self.key):
            self.driver.rollback()
        else:
            self.driver.commit()
    else:
        self.driver.commit()

    self.requery(select_first=False)
    self.frm.update_elements(self.key)
    self.requery_dependents()
    return None

duplicate_record(children=None, skip_prompt_save=False)

Duplicate the currently selected record.

The before_duplicate and after_duplicate callbacks are run during this process to give some control over the process.

Parameters:

Name Type Description Default
children bool

Duplicate child records (as defined by Relationships that were set up) before duplicating this record.

None
skip_prompt_save bool

(optional) True to skip prompting to save dirty records

False

Returns:

Type Description
Union[bool, None]

None

Source code in pysimplesql\pysimplesql.py
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
def duplicate_record(
    self,
    children: bool = None,
    skip_prompt_save: bool = False,
) -> Union[bool, None]:  # TODO check return type, returns True within
    """Duplicate the currently selected record.

    The before_duplicate and after_duplicate callbacks are run during this
    process to give some control over the process.

    Args:
        children: Duplicate child records (as defined by `Relationship`s that were
            set up) before duplicating this record.
        skip_prompt_save: (optional) True to skip prompting to save dirty records

    Returns:
        None
    """
    # Ensure that there is actually something to duplicate
    if not self.row_count or self.pk_is_virtual():
        return None

    # prompt_save
    if (
        not skip_prompt_save
        # don't update self/dependents if we are going to below anyway
        and self.prompt_save(update_elements=False) == SAVE_FAIL
    ):
        return None

    # callback
    if "before_duplicate" in self.callbacks and not self.callbacks[
        "before_duplicate"
    ](self.frm, self.frm.window, self.key):
        return None

    if children is None:
        children = self.duplicate_children

    child_list = []
    if children:
        child_list = self.relationships.get_update_cascade_tables(self.table)

    msg_children = ", ".join(child_list)
    msg = lang.duplicate_child.format_map(
        LangFormat(children=msg_children)
    ).splitlines()
    layout = [[sg.T(line, font="bold")] for line in msg]
    if len(child_list):
        answer = sg.Window(
            lang.duplicate_child_title,
            [
                layout,
                [
                    sg.Button(
                        button_text=lang.duplicate_child_button_dupparent,
                        key="parent",
                        use_ttk_buttons=themepack.use_ttk_buttons,
                        pad=themepack.popup_button_pad,
                    )
                ],
                [
                    sg.Button(
                        button_text=lang.duplicate_child_button_dupboth,
                        key="cascade",
                        use_ttk_buttons=themepack.use_ttk_buttons,
                        pad=themepack.popup_button_pad,
                    )
                ],
                [
                    sg.Button(
                        button_text=lang.button_cancel,
                        key="cancel",
                        use_ttk_buttons=themepack.use_ttk_buttons,
                        pad=themepack.popup_button_pad,
                    )
                ],
            ],
            keep_on_top=True,
            modal=True,
            ttk_theme=themepack.ttk_theme,
            icon=themepack.icon,
        ).read(close=True)
        if answer[0] == "parent":
            children = False
        elif answer[0] in ["cancel", None]:
            return True
    else:
        msg = lang.duplicate_single
        answer = self.frm.popup.yes_no(lang.duplicate_single_title, msg)
        if answer == "no":
            return True
    # Store our current pk, so we can move to it if the duplication fails
    pk = self.current.pk

    # Have the driver duplicate the record
    result = self.driver.duplicate_record(self, children)
    if result.attrs["exception"]:
        self.driver.rollback()
        self.frm.popup.ok(
            lang.duplicate_failed_title,
            lang.duplicate_failed.format_map(
                LangFormat(exception=result.attrs["exception"])
            ),
        )
    else:
        pk = result.attrs["lastrowid"]

    # callback
    if "after_duplicate" in self.callbacks:
        if not self.callbacks["after_duplicate"](
            self.frm, self.frm.window, self.key
        ):
            self.driver.rollback()
        else:
            self.driver.commit()
    else:
        self.driver.commit()
    self.driver.commit()

    # requery and move to new pk
    self.requery(select_first=False)
    self.set_by_pk(pk, skip_prompt_save=True)
    return None

get_description_for_pk(pk)

Get the description from the DataSet on the matching pk.

Return the description from description_column for the row where the [pk_column][pysimplesql.pysimplesql.DataSet.pk_column] = [pk][pysimplesql.pysimplesql.pk].

Parameters:

Name Type Description Default
pk int

The primary key from which to find the description for

required

Returns:

Type Description
Union[str, int, None]

The value found in the description column, or None if nothing is found

Source code in pysimplesql\pysimplesql.py
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
def get_description_for_pk(self, pk: int) -> Union[str, int, None]:
    """Get the description from the `DataSet` on the matching pk.

    Return the description from `DataSet.description_column` for the row where the
    `DataSet.pk_column` = `pk`.

    Args:
        pk: The primary key from which to find the description for

    Returns:
        The value found in the description column, or None if nothing is found
    """
    # We don't want to update other views comboboxes/tableviews until row is
    # actually saved. So first check their current
    current_row = self.current.get_original()
    if current_row[self.pk_column] == pk:
        return current_row[self.description_column]
    try:
        index = self.rows.loc[self.rows[self.pk_column] == pk].index[0]
        return self.rows[self.description_column].iloc[index]
    except IndexError:
        return None

pk_is_virtual(pk=None)

Check whether pk is virtual.

Parameters:

Name Type Description Default
pk int

The pk to check. If None, the pk of the current row will be checked.

None

Returns:

Type Description
bool

True or False based on whether the row is virtual

Source code in pysimplesql\pysimplesql.py
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
def pk_is_virtual(self, pk: int = None) -> bool:
    """Check whether pk is virtual.

    Args:
        pk: The pk to check. If None, the pk of the current row will be checked.

    Returns:
        True or False based on whether the row is virtual
    """
    if not self.row_count:
        return False

    if pk is None:
        pk = self.current.get()[self.pk_column]

    return bool(pk in self.virtual_pks)

purge_row_backup()

Deletes the backup row from the dataset.

This method sets the "row_backup" attribute of the dataset to None.

Source code in pysimplesql\pysimplesql.py
2738
2739
2740
2741
2742
2743
def purge_row_backup(self) -> None:
    """Deletes the backup row from the dataset.

    This method sets the "row_backup" attribute of the dataset to None.
    """
    self.rows.attrs["row_backup"] = None

table_values(columns=None, mark_unsaved=False, apply_search_filter=False, apply_cell_format_fn=True)

Create a values list of [TableRows][pysimplesql.pysimplesql.TableRows]s for use in a PySimpleGUI Table element.

Parameters:

Name Type Description Default
columns List[str]

A list of column names to create table values for. Defaults to getting them from the rows DataFrame.

None
mark_unsaved bool

Place a marker next to virtual records, or records with unsaved changes.

False
apply_search_filter bool

Filter rows to only those columns in search_order that contain [search_string][pysimplesql.pysimplesql.DataSet.search_string].

False
apply_cell_format_fn bool

If set, apply() DataSet.column_info[col].cell_format_fn to rows column

True

Returns:

Type Description
List[TableRow]

A list of TableRows suitable for using with PySimpleGUI Table element

List[TableRow]

values.

Source code in pysimplesql\pysimplesql.py
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
def table_values(
    self,
    columns: List[str] = None,
    mark_unsaved: bool = False,
    apply_search_filter: bool = False,
    apply_cell_format_fn: bool = True,
) -> List[TableRow]:
    """Create a values list of `TableRows`s for use in a PySimpleGUI Table element.

    Args:
        columns: A list of column names to create table values for. Defaults to
            getting them from the `DataSet.rows` DataFrame.
        mark_unsaved: Place a marker next to virtual records, or records with
            unsaved changes.
        apply_search_filter: Filter rows to only those columns in
            `DataSet.search_order` that contain `DataSet.search_string`.
        apply_cell_format_fn: If set, apply()
            `DataSet.column_info[col].cell_format_fn` to rows column

    Returns:
        A list of `TableRow`s suitable for using with PySimpleGUI Table element
        values.
    """
    if not self.row_count:
        return []

    try:
        all_columns = list(self.rows.columns)
    except IndexError:
        all_columns = []

    columns = all_columns if columns is None else columns

    rows = self.rows.copy()
    pk_column = self.pk_column

    if mark_unsaved:
        virtual_row_pks = self.virtual_pks.copy()
        # add pk of current row if it has changes
        if self.current.has_backup and not self.current.get().equals(
            self.current.get_original()
        ):
            virtual_row_pks.append(
                self.rows.loc[
                    self.rows[pk_column] == self.current.get()[pk_column],
                    pk_column,
                ].to_numpy()[0]
            )

        # Create a new column 'marker' with the desired values
        rows["marker"] = " "
        mask = rows[pk_column].isin(virtual_row_pks)
        rows.loc[mask, "marker"] = themepack.marker_unsaved
    else:
        rows["marker"] = " "

    # get fk descriptions
    rows = self.map_fk_descriptions(rows, columns)

    # return early if empty
    if rows.empty:
        return []

    # filter rows to only contain search, or virtual/unsaved row
    if apply_search_filter and self.search_string not in EMPTY:
        masks = [
            rows[col].astype(str).str.contains(self.search_string, case=False)
            | rows[pk_column].isin(virtual_row_pks)
            for col in self.search_order
        ]
        mask_pd = pd.concat(masks, axis=1).any(axis=1)
        # Apply the mask to filter the DataFrame
        rows = rows[mask_pd]

    # apply cell format functions
    if apply_cell_format_fn:
        for column in columns:
            if self.column_info[column] and self.column_info[column].cell_format_fn:
                fn = self.column_info[column].cell_format_fn
                rows[column] = rows[column].apply(fn)

    # set the pk to the index to use below
    rows["pk_idx"] = rows[pk_column].copy()
    rows = rows.set_index("pk_idx")

    # insert the marker
    columns.insert(0, "marker")

    # resort rows with requested columns
    rows = rows[columns]

    # fastest way yet to generate list of TableRows
    return [
        TableRow(pk, values.tolist())
        for pk, values in zip(
            rows.index,
            np.vstack((rows.fillna("").astype("O").to_numpy().T, rows.index)).T,
        )
    ]

column_likely_in_selector(column)

Determines whether the given column is likely to be displayed in a selector.

Parameters:

Name Type Description Default
column str

The name of the column to check.

required

Returns:

Type Description
bool

True if the column is likely to be displayed, False otherwise.

Source code in pysimplesql\pysimplesql.py
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
def column_likely_in_selector(self, column: str) -> bool:
    """Determines whether the given column is likely to be displayed in a selector.

    Args:
        column: The name of the column to check.

    Returns:
        True if the column is likely to be displayed, False otherwise.
    """
    # If there are no sg.Table selectors, return False
    if not any(
        isinstance(e["element"], sg.PySimpleGUI.Table) for e in self.selector
    ):
        return False

    # If table headings are not used, assume the column is displayed, return True
    if not any("TableBuilder" in e["element"].metadata for e in self.selector):
        return True

    # Otherwise, Return True/False if the column is in the list of table headings
    return any(
        "TableBuilder" in e["element"].metadata
        and column in e["element"].metadata["TableBuilder"].columns
        for e in self.selector
    )

combobox_values(column_name, insert_placeholder=True)

Returns the values to use in a sg.Combobox as a list of ElementRow objects.

Parameters:

Name Type Description Default
column_name str

The name of the table column for which to get the values.

required
insert_placeholder bool

If True, inserts [combo_placeholder][pysimplesql.pysimplesql.Languagepack.combo_placeholder] as first value.

True

Returns:

Type Description
Union[List[ElementRow], None]

A list of ElementRow objects representing the possible values for the

Union[List[ElementRow], None]

combobox column, or None if no matching relationship is found.

Source code in pysimplesql\pysimplesql.py
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
def combobox_values(
    self, column_name: str, insert_placeholder: bool = True
) -> Union[List[ElementRow], None]:
    """Returns the values to use in a sg.Combobox as a list of ElementRow objects.

    Args:
        column_name: The name of the table column for which to get the values.
        insert_placeholder: If True, inserts `Languagepack.combo_placeholder` as
            first value.

    Returns:
        A list of ElementRow objects representing the possible values for the
        combobox column, or None if no matching relationship is found.
    """
    if not self.row_count:
        return None

    rels = self.relationships.get_rels_for(self.table)
    rel = next((r for r in rels if r.fk_column == column_name), None)
    if rel is None:
        return None

    if not self.frm[rel.parent_table].row_count:
        return None

    rows = self.frm[rel.parent_table].rows.copy()
    pk_column = self.frm[rel.parent_table].pk_column
    description = self.frm[rel.parent_table].description_column

    # revert to original row (so unsaved changes don't show up in dropdowns)
    parent_current_row = self.frm[rel.parent_table].current.get_original()
    rows.iloc[self.frm[rel.parent_table].current.index] = parent_current_row

    # fastest way yet to generate this list of ElementRow
    combobox_values = [
        ElementRow(*values)
        for values in np.column_stack((rows[pk_column], rows[description]))
    ]

    if insert_placeholder:
        combobox_values.insert(0, ElementRow("Null", lang.combo_placeholder))
    return combobox_values

Get parent table name as it relates to this column.

Parameters:

Name Type Description Default
column str

The column name to get related table information for

required

Returns:

Type Description
str

The name of the related table, or the current table if none are found

Source code in pysimplesql\pysimplesql.py
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
def get_related_table_for_column(self, column: str) -> str:
    """Get parent table name as it relates to this column.

    Args:
        column: The column name to get related table information for

    Returns:
        The name of the related table, or the current table if none are found
    """
    rels = self.relationships.get_rels_for(self.table)
    for rel in rels:
        if column == rel.fk_column:
            return rel.parent_table
    return self.table  # None could be found, return our own table instead

map_fk_descriptions(rows, columns=None)

Maps foreign key descriptions to the specified columns in the given DataFrame.

Note

If passing in rows, please pass in a copy, eg: frm[data_key].rows.copy()

Parameters:

Name Type Description Default
rows DataFrame

The DataFrame containing the data to be processed.

required
columns list[str]

(Optional) The list of column names to map foreign key descriptions to. If none are provided, all columns of the DataFrame will be searched for foreign-key relationships.

None

Returns:

Type Description
DataFrame

The processed DataFrame with foreign key descriptions mapped to the specified columns.

Source code in pysimplesql\pysimplesql.py
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
def map_fk_descriptions(
    self, rows: pd.DataFrame, columns: list[str] = None
) -> pd.DataFrame:
    """Maps foreign key descriptions to the specified columns in the given
    DataFrame.


    Note:
        If passing in `DataSet.rows`, please pass in a copy, eg:
        ```frm[data_key].rows.copy()```

    Args:
        rows: The DataFrame containing the data to be processed.
        columns: (Optional) The list of column names to map foreign key descriptions
            to. If none are provided, all columns of the DataFrame will be searched
            for foreign-key relationships.

    Returns:
        The processed DataFrame with foreign key descriptions mapped to the
            specified columns.
    """
    if columns is None:
        columns = rows.columns

    # get fk descriptions
    rels = self.relationships.get_rels_for(self.table)
    for col in columns:
        for rel in rels:
            if col == rel.fk_column:
                # return early if parent is empty
                if not self.frm[rel.parent_table].row_count:
                    return rows

                parent_df = self.frm[rel.parent_table].rows
                parent_pk_column = self.frm[rel.parent_table].pk_column

                # get this before map(), to revert below
                parent_current_row = self.frm[
                    rel.parent_table
                ].current.get_original()
                condition = rows[col] == parent_current_row[parent_pk_column]

                # map descriptions to fk column
                description_column = self.frm[rel.parent_table].description_column
                mapping_dict = parent_df.set_index(parent_pk_column)[
                    description_column
                ].to_dict()
                rows[col] = rows[col].map(mapping_dict)

                # revert any unsaved changes for the single row
                rows.loc[condition, col] = parent_current_row[description_column]

                # we only want transform col once
                break
    return rows

quick_editor(pk_update_funct=None, funct_param=None, skip_prompt_save=False, column_attributes=None)

The quick editor is a dynamic PySimpleGUI Window for quick editing of tables. This is very useful for putting a button next to a combobox or listbox so that the available values can be added/edited/deleted easily. Note: This is not typically used by the end user, as it can be configured from the field() convenience function.

Parameters:

Name Type Description Default
pk_update_funct Callable

(optional) A function to call to determine the pk to select by default when the quick editor loads.

None
funct_param any

(optional) A parameter to pass to the 'pk_update_funct'

None
skip_prompt_save bool

(Optional) True to skip prompting to save dirty records

False
column_attributes dict

(Optional) Dictionary specifying column attributes for [column_info][pysimplesql.pysimplesql.DataSet.column_info]. The dictionary should be in the form {column_name: {attribute: value}}.

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
def quick_editor(
    self,
    pk_update_funct: Callable = None,
    funct_param: any = None,
    skip_prompt_save: bool = False,
    column_attributes: dict = None,
) -> None:
    """The quick editor is a dynamic PySimpleGUI Window for quick editing of tables.
    This is very useful for putting a button next to a combobox or listbox so that
    the available values can be added/edited/deleted easily.
    Note: This is not typically used by the end user, as it can be configured from
    the `field()` convenience function.

    Args:
        pk_update_funct: (optional) A function to call to determine the pk to select
            by default when the quick editor loads.
        funct_param: (optional) A parameter to pass to the 'pk_update_funct'
        skip_prompt_save: (Optional) True to skip prompting to save dirty records
        column_attributes: (Optional) Dictionary specifying column attributes for
            `DataSet.column_info`. The dictionary should be in the form
            {column_name: {attribute: value}}.

    Returns:
        None
    """
    # prompt_save
    if (
        not skip_prompt_save
        # don't update self/dependents if we are going to below anyway
        and self.prompt_save(update_elements=False) == SAVE_FAIL
    ):
        return

    # Reset the keygen to keep consistent naming
    logger.info("Creating Quick Editor window")
    keygen.reset()
    data_key = self.key
    layout = []
    table_builder = TableBuilder(
        num_rows=10,
        sort_enable=True,
        allow_cell_edits=True,
        add_save_heading_button=True,
        style=TableStyler(row_height=25),
    )

    for col in self.column_info.names:
        # set widths
        width = int(55 / (len(self.column_info.names) - 1))
        if col == self.pk_column:
            # make pk column either max length of contained pks, or len of name
            width = int(
                np.nanmax([self.rows[col].astype(str).map(len).max(), len(col) + 1])
            )
            justify = "left"
        elif self.column_info[col] and self.column_info[col].python_type in [
            int,
            float,
            Decimal,
        ]:
            justify = "right"
        else:
            justify = "left"
        table_builder.add_column(
            col, col.capitalize(), width=width, col_justify=justify
        )

    layout.append(
        [
            selector(
                data_key,
                table_builder,
                key=f"{data_key}:quick_editor",
            )
        ]
    )
    y_pad = 10
    layout.append([actions(data_key, edit_protect=False)])
    layout.append([sg.Sizer(h_pixels=0, v_pixels=y_pad)])

    fields_layout = [[sg.Sizer(h_pixels=0, v_pixels=y_pad)]]

    rels = self.relationships.get_rels_for(self.table)
    for col in self.column_info.names:
        found = False
        column = f"{data_key}.{col}"
        # make sure isn't pk
        if col != self.pk_column:
            # display checkboxes
            if (
                self.column_info[column]
                and self.column_info[column].python_type == bool
            ):
                fields_layout.append([field(column, sg.Checkbox)])
                found = True
                break
            # or display sg.combos
            for rel in rels:
                if col == rel.fk_column:
                    fields_layout.append(
                        [field(column, sg.Combo, quick_editor=False)]
                    )
                    found = True
                    break
            # otherwise, just display a regular input
            if not found:
                fields_layout.append([field(column)])

    fields_layout.append([sg.Sizer(h_pixels=0, v_pixels=y_pad)])
    layout.append([sg.Frame("Fields", fields_layout, expand_x=True)])
    layout.append([sg.Sizer(h_pixels=0, v_pixels=10)])
    layout.append(
        [
            sg.StatusBar(
                " " * 100,
                key="info:quick_editor",
                metadata={"type": ElementType.INFO},
            )
        ],
    )

    quick_win = sg.Window(
        lang.quick_edit_title.format_map(LangFormat(data_key=data_key)),
        layout,
        keep_on_top=True,
        modal=True,
        finalize=True,
        ttk_theme=themepack.ttk_theme,  # Must, otherwise will redraw window
        icon=themepack.icon,
        enable_close_attempted_event=True,
    )
    quick_frm = Form(
        self.frm.driver,
        bind_window=quick_win,
        live_update=True,
    )

    # Select the current entry to start with
    if pk_update_funct is not None:
        if funct_param is None:
            quick_frm[data_key].set_by_pk(pk_update_funct())
        else:
            quick_frm[data_key].set_by_pk(pk_update_funct(funct_param))

    if column_attributes:
        for col, kwargs in column_attributes.items():
            if quick_frm[data_key].column_info[col]:
                for attr, value in kwargs.items():
                    quick_frm[data_key].column_info[col][attr] = value

    while True:
        event, values = quick_win.read()

        if quick_frm.process_events(event, values):
            logger.debug(
                f"PySimpleSQL Quick Editor event handler handled the event {event}!"
            )
        if event == "-WINDOW CLOSE ATTEMPTED-":
            if quick_frm.popup.popup_info:
                quick_frm.popup.popup_info.close()
            self.requery()
            self.frm.update_elements()
            quick_win.close()
            quick_frm.close(close_driver=False)
            break
        logger.debug(f"This event ({event}) is not yet handled.")

add_simple_transform(transforms)

Merge a dictionary of transforms into the [_simple_transform][pysimplesql.pysimplesql.DataSet._simple_transform] dictionary.

Example
{'entry_date' : {
    'decode' : lambda row,col: datetime.utcfromtimestamp(int(row[col])).strftime('%m/%d/%y'),
    'encode' : lambda row,col: datetime.strptime(row[col], '%m/%d/%y').replace(tzinfo=timezone.utc).timestamp(),
}}

Parameters:

Name Type Description Default
transforms SimpleTransformsDict

A dict of dicts containing either 'encode' or 'decode' along with a callable to do the transform. See example above

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
def add_simple_transform(self, transforms: SimpleTransformsDict) -> None:
    """Merge a dictionary of transforms into the `DataSet._simple_transform`
    dictionary.

    Example:
        ```python
        {'entry_date' : {
            'decode' : lambda row,col: datetime.utcfromtimestamp(int(row[col])).strftime('%m/%d/%y'),
            'encode' : lambda row,col: datetime.strptime(row[col], '%m/%d/%y').replace(tzinfo=timezone.utc).timestamp(),
        }}
        ```

    Args:
        transforms: A dict of dicts containing either 'encode' or 'decode' along
            with a callable to do the transform. See example above

    Returns:
        None
    """  # noqa: E501
    for k, v in transforms.items():
        if not callable(v):
            RuntimeError(f"Transform for {k} must be callable!")
        self._simple_transform[k] = v

purge_virtual()

Purge virtual rows from the DataFrame.

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
def purge_virtual(self) -> None:
    """Purge virtual rows from the DataFrame.

    Returns:
        None
    """
    # remove the rows where virtual is True in place, along with the corresponding
    # virtual attribute
    virtual_rows = self.rows[self.rows[self.pk_column].isin(self.virtual_pks)]
    self.rows = self.rows.drop(index=virtual_rows.index)
    self.rows.attrs["virtual"] = []

sort_by_column(column, table, reverse=False)

Sort the DataFrame by column. Using the mapped relationships of the database, foreign keys will automatically sort based on the parent table's description column, rather than the foreign key number.

Parameters:

Name Type Description Default
column str

The name of the column to sort the DataFrame by

required
table str

The name of the table the column belongs to

required
reverse bool

Reverse the sort; False = ASC, True = DESC

False

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
def sort_by_column(self, column: str, table: str, reverse: bool = False) -> None:
    """Sort the DataFrame by column. Using the mapped relationships of the database,
    foreign keys will automatically sort based on the parent table's description
    column, rather than the foreign key number.

    Args:
        column: The name of the column to sort the DataFrame by
        table: The name of the table the column belongs to
        reverse: Reverse the sort; False = ASC, True = DESC

    Returns:
        None
    """
    # Target sorting by this DataFrame

    # We don't want to sort by foreign keys directly - we want to sort by the
    # description column of the foreign table that the foreign key references
    tmp_column = None
    rels = self.relationships.get_rels_for(table)

    transformed = False
    for rel in rels:
        if column == rel.fk_column:
            # Copy the specified column and apply mapping to obtain fk descriptions
            column_copy = pd.DataFrame(self.rows[column].copy())
            column_copy = self.map_fk_descriptions(column_copy, [column])[column]

            # Assign the transformed column to the temporary column
            tmp_column = f"temp_{rel.parent_table}.{rel.pk_column}"
            self.rows[tmp_column] = column_copy

            # Use the temporary column as the new sorting column
            column = tmp_column

            transformed = True
            break

    # handling datetime
    # TODO: user-defined format
    if (
        not transformed
        and self.column_info[column]
        and self.column_info[column].python_type in (dt.date, dt.time, dt.datetime)
    ):
        tmp_column = f"temp_{column}"
        self.rows[tmp_column] = pd.to_datetime(self.rows[column])
        column = tmp_column

    # sort
    try:
        self.rows = self.rows.sort_values(
            column,
            ascending=not reverse,
        )
    except (KeyError, TypeError) as e:
        logger.debug(f"DataFrame could not sort by column {column}. {e}")
    finally:
        # Drop the temporary description column (if it exists)
        if tmp_column is not None:
            self.rows = self.rows.drop(columns=tmp_column, errors="ignore")

sort_by_index(index, table, reverse=False)

Sort the self.rows DataFrame by column index Using the mapped relationships of the database, foreign keys will automatically sort based on the parent table's description column, rather than the foreign key number.

Parameters:

Name Type Description Default
index int

The index of the column to sort the DateFrame by

required
table str

The name of the table the column belongs to

required
reverse bool

Reverse the sort; False = ASC, True = DESC

False

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
def sort_by_index(self, index: int, table: str, reverse: bool = False) -> None:
    """Sort the self.rows DataFrame by column index Using the mapped relationships
    of the database, foreign keys will automatically sort based on the parent
    table's description column, rather than the foreign key number.

    Args:
        index: The index of the column to sort the DateFrame by
        table: The name of the table the column belongs to
        reverse: Reverse the sort; False = ASC, True = DESC

    Returns:
        None
    """
    column = self.rows.columns[index]
    self.sort_by_column(column, table, reverse)

store_sort_settings()

Store the current sort settingg. Sort settings are just the sort column and reverse setting. Sort order can be restored with load_sort_settings().

Returns:

Type Description
list

A list containing the sort_column and the sort_reverse

Source code in pysimplesql\pysimplesql.py
3265
3266
3267
3268
3269
3270
3271
3272
def store_sort_settings(self) -> list:
    """Store the current sort settingg. Sort settings are just the sort column and
    reverse setting. Sort order can be restored with `DataSet.load_sort_settings()`.

    Returns:
        A list containing the sort_column and the sort_reverse
    """
    return [self.rows.attrs["sort_column"], self.rows.attrs["sort_reverse"]]

load_sort_settings(sort_settings)

Load a previously stored sort setting. Sort settings are just the sort columm and reverse setting.

Parameters:

Name Type Description Default
sort_settings list

A list as returned by store_sort_settings()

required
Source code in pysimplesql\pysimplesql.py
3274
3275
3276
3277
3278
3279
3280
3281
3282
def load_sort_settings(self, sort_settings: list) -> None:
    """Load a previously stored sort setting. Sort settings are just the sort columm
    and reverse setting.

    Args:
        sort_settings: A list as returned by `DataSet.store_sort_settings()`
    """
    self.rows.attrs["sort_column"] = sort_settings[0]
    self.rows.attrs["sort_reverse"] = sort_settings[1]

sort_reset()

Reset the sort order to the original order as defined by the DataFram index.

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3284
3285
3286
3287
3288
3289
3290
3291
def sort_reset(self) -> None:
    """Reset the sort order to the original order as defined by the DataFram index.

    Returns:
        None
    """
    # Restore the original sort order
    self.rows = self.rows.sort_index()

sort(table, update_elements=True, sort_order=None)

Sort according to the internal sort_column and sort_reverse variables. This is a good way to re-sort without changing the sort_cycle.

Parameters:

Name Type Description Default
table str

The table associated with this DataSet. Passed along to sort_by_column()

required
update_elements bool

Update associated selectors and navigation buttons, and table header sort marker.

True
sort_order

A SORT_* constant (SORT_NONE, SORT_ASC, SORT_DESC). Note that the update_elements parameter must = True to use

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
def sort(self, table: str, update_elements: bool = True, sort_order=None) -> None:
    """Sort according to the internal sort_column and sort_reverse variables. This
    is a good way to re-sort without changing the sort_cycle.

    Args:
        table: The table associated with this DataSet.  Passed along to
            `DataSet.sort_by_column()`
        update_elements: Update associated selectors and navigation buttons, and
            table header sort marker.
        sort_order: A SORT_* constant (SORT_NONE, SORT_ASC, SORT_DESC).
            Note that the update_elements parameter must = True to use

    Returns:
        None
    """
    pk = self.current.pk
    if self.rows.attrs["sort_column"] is None:
        logger.debug("Sort column is None.  Resetting sort.")
        self.sort_reset()
    else:
        logger.debug(f"Sorting by column {self.rows.attrs['sort_column']}")
        self.sort_by_column(
            self.rows.attrs["sort_column"], table, self.rows.attrs["sort_reverse"]
        )
    self.set_by_pk(
        pk,
        update_elements=False,
        requery_dependents=False,
        skip_prompt_save=True,
    )
    if update_elements and self.row_count:
        self.frm.update_selectors(self.key)
        self.frm.update_actions(self.key)
        self._update_headings(self.rows.attrs["sort_column"], sort_order)

sort_cycle(column, table, update_elements=True)

Cycle between original sort order of the DataFrame, ASC by column, and DESC by column with each call.

Parameters:

Name Type Description Default
column str

The column name to cycle the sort on

required
table str

The table that the column belongs to

required
update_elements bool

Passed to sort to update update associated selectors and navigation buttons, and table header sort marker.

True

Returns:

Type Description
int

A sort constant; SORT_NONE, SORT_ASC, or SORT_DESC

Source code in pysimplesql\pysimplesql.py
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
def sort_cycle(self, column: str, table: str, update_elements: bool = True) -> int:
    """Cycle between original sort order of the DataFrame, ASC by column, and DESC
    by column with each call.

    Args:
        column: The column name to cycle the sort on
        table: The table that the column belongs to
        update_elements: Passed to `DataSet.sort` to update update associated
            selectors and navigation buttons, and table header sort marker.

    Returns:
        A sort constant; SORT_NONE, SORT_ASC, or SORT_DESC
    """
    if column != self.rows.attrs["sort_column"]:
        self.rows.attrs["sort_column"] = column
        self.rows.attrs["sort_reverse"] = False
        self.sort(table, update_elements=update_elements, sort_order=SORT_ASC)
        return SORT_ASC
    if not self.rows.attrs["sort_reverse"]:
        self.rows.attrs["sort_reverse"] = True
        self.sort(table, update_elements=update_elements, sort_order=SORT_DESC)
        return SORT_DESC
    self.rows.attrs["sort_reverse"] = False
    self.rows.attrs["sort_column"] = None
    self.sort(table, update_elements=update_elements, sort_order=SORT_NONE)
    return SORT_NONE

insert_row(row, idx=None)

Insert a new virtual row into the DataFrame. Virtual rows are ones that exist in memory, but not in the database. When a save action is performed, virtual rows will be added into the database.

Parameters:

Name Type Description Default
row dict

A dict representation of a row of data

required
idx int

The index where the row should be inserted (default to last index)

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
def insert_row(self, row: dict, idx: int = None) -> None:
    """Insert a new virtual row into the DataFrame. Virtual rows are ones that exist
    in memory, but not in the database. When a save action is performed, virtual
    rows will be added into the database.

    Args:
        row: A dict representation of a row of data
        idx: The index where the row should be inserted (default to last index)

    Returns:
        None
    """
    row_series = pd.Series(row, dtype=object)
    # Infer better data types for the Series
    # row_series = row_series.infer_objects()
    if self.rows.empty:
        self.rows = Result.set(
            pd.concat([self.rows, row_series.to_frame().T], ignore_index=True)
        )
    else:
        attrs = self.rows.attrs.copy()

        # TODO: idx currently does nothing
        if idx is None:
            idx = self.row_count

        self.rows = pd.concat(
            [self.rows, row_series.to_frame().T], ignore_index=True
        )
        self.rows.attrs = attrs

    self.rows.attrs["virtual"].append(row[self.pk_column])

validate_field(column_name, new_value, widget=None, display_message=False)

Validate the given field value for the specified column.

Parameters:

Name Type Description Default
column_name str

The name of the column to validate the field against.

required
new_value Any

The new value to validate.

required
widget

The widget associated with the field. (Optional)

None
display_message bool

Flag to display validation messages. (Default: False)

False

Returns:

Type Description
bool

True if the field value is valid, False otherwise.

Source code in pysimplesql\pysimplesql.py
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
def validate_field(
    self,
    column_name: str,
    new_value: Any,
    widget=None,
    display_message: bool = False,
) -> bool:
    """Validate the given field value for the specified column.

    Args:
        column_name: The name of the column to validate the field against.
        new_value: The new value to validate.
        widget: The widget associated with the field. (Optional)
        display_message: Flag to display validation messages. (Default: False)

    Returns:
        True if the field value is valid, False otherwise.
    """
    if column_name in self.column_info:
        # Validate the new value against the column's validation rules
        response = self.column_info[column_name].validate(new_value)
        # If validation fails, display an error message and return False
        if response.exception:
            self.frm.popup.info(
                lang[response.exception].format_map(
                    LangFormat(value=response.value, rule=response.rule)
                ),
                display_message=display_message,
            )
            if widget and themepack.validate_exception_animation is not None:
                themepack.validate_exception_animation(widget)
            return False
        # If validation passes, update the info element and return True
        self.frm.popup.update_info_element(erase=True)
        return True
    logger.debug(f"{column_name} not in dataset.column_info!")
    return None

Form dataclass

Form class.

Maintains an internal version of the actual database DataSet objects can be accessed by key, I.e. frm['data_key'].

Parameters:

Name Type Description Default
driver SQLDriver required
bind_window InitVar[Window]

Bind this window to the Form

None
parent Form

(optional)Parent Form to base dataset off of

None
filter str

(optional) Only import elements with the same filter set. Typically set with field(), but can also be set manually as a dict with the key 'filter' set in the element's metadata

None
select_first InitVar[bool]

(optional) Default:True. For each top-level parent, selects first row, populating children as well.

True
prompt_save

(optional) Default:PROMPT_MODE. Prompt to save changes when dirty records are present. There are two modes available, PROMPT_MODE to prompt to save when unsaved changes are present. AUTOSAVE_MODE to automatically save when unsaved changes are present.

required
save_quiet bool

(optional) Default:False. True to skip info popup on save. Error popups will still be shown.

False
duplicate_children bool

(optional) Default:True. If record has children, prompt user to choose to duplicate current record, or both.

True
description_column_names List[str]

(optional) A list of names to use for the DataSet object's description column, displayed in Listboxes, Comboboxes, and Tables instead of the primary key. The first matching column of the table is given priority. If no match is found, the second column is used. Default list: ['description', 'name', 'title'].

field(default_factory=lambda : ['description', 'name', 'title'])
live_update bool

(optional) Default value is False. If True, changes made in a field will be immediately pushed to associated selectors. If False, changes will be pushed only after a save action.

False
validate_mode ValidateMode

Passed to DataSet init to set validate mode. STRICT to prevent invalid values from being entered. RELAXED allows invalid input, but ensures validation occurs before saving to the database.

RELAXED

Returns:

Type Description

None

close(reset_keygen=True, close_driver=True)

Safely close out the Form.

Parameters:

Name Type Description Default
reset_keygen bool

True to reset the keygen for this Form

True
close_driver bool

True to also close associated [driver][pysimplesql.pysimplesql.Form.driver]

True
Source code in pysimplesql\pysimplesql.py
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
def close(self, reset_keygen: bool = True, close_driver: bool = True) -> None:
    """Safely close out the `Form`.

    Args:
        reset_keygen: True to reset the keygen for this `Form`
        close_driver: True to also close associated `Form.driver`
    """
    # First delete the dataset associated
    DataSet.purge_form(self, reset_keygen)
    if self.popup.popup_info:
        self.popup.popup_info.close()
    Form.purge_instance(self)
    if close_driver:
        self.driver.close()

bind(win)

Bind the PySimpleGUI Window to the Form for the purpose of GUI element, event and relationship mapping. This can happen automatically on Form creation with the bind parameter and is not typically called by the end user. This function literally just groups all the auto_* methods. auto_map_elements(), auto_map_events().

Parameters:

Name Type Description Default
win Window

The PySimpleGUI window

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
def bind(self, win: sg.Window) -> None:
    """Bind the PySimpleGUI Window to the Form for the purpose of GUI element, event
    and relationship mapping. This can happen automatically on `Form` creation with
    the bind parameter and is not typically called by the end user. This function
    literally just groups all the auto_* methods. `Form.auto_map_elements()`,
    `Form.auto_map_events()`.

    Args:
        win: The PySimpleGUI window

    Returns:
        None
    """
    logger.info("Binding Window to Form")
    self.window = win
    self.popup = Popup(self.window)
    self.auto_map_elements(win)
    self.auto_map_events(win)
    self.update_elements()
    # Creating cell edit instance, even if we arn't going to use it.
    self._celledit = _CellEdit(self)
    self.window.TKroot.bind("<Double-Button-1>", self._celledit)
    self._liveupdate = _LiveUpdate(self)
    if self.live_update:
        self.set_live_update(enable=True)
    logger.debug("Binding finished!")

execute(query)

Execute a query.

Convenience function to pass along to execute.

Parameters:

Name Type Description Default
query str

The query to execute

required

Returns:

Type Description
DataFrame

A pandas DataFrame object with attrs set for lastrowid and exception

Source code in pysimplesql\pysimplesql.py
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
def execute(self, query: str) -> pd.DataFrame:
    """Execute a query.

    Convenience function to pass along to `SQLDriver.execute`.

    Args:
        query: The query to execute

    Returns:
        A pandas DataFrame object with attrs set for lastrowid and exception
    """
    return self.driver.execute(query)

commit()

Commit a transaction.

Convenience function to pass along to commit().

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3603
3604
3605
3606
3607
3608
3609
3610
3611
def commit(self) -> None:
    """Commit a transaction.

    Convenience function to pass along to `SQLDriver.commit()`.

    Returns:
        None
    """
    self.driver.commit()

set_callback(callback_name, fctn)

Set Form callbacks.

A runtime error will be raised if the callback is not supported. The following callbacks are supported: update_elements Called after elements are updated via update_elements(). This allows for other GUI manipulation on each update of the GUI edit_enable Called before editing mode is enabled. This can be useful for asking for a password for example edit_disable Called after the editing mode is disabled.

{element_name} Called while updating MAPPED element. This overrides the default element update implementation. Note that the {element_name} callback function needs to return a value to pass to Win[element].update()

Parameters:

Name Type Description Default
callback_name str

The name of the callback, from the list above

required
fctn Callable[[Form, Window], Union[None, bool]]

The function to call. Note, the function must take in two parameters, a Form instance, and a PySimpleGUI.Window instance

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
def set_callback(
    self, callback_name: str, fctn: Callable[[Form, sg.Window], Union[None, bool]]
) -> None:
    """Set `Form` callbacks.

    A runtime error will be raised if the callback is not
    supported. The following callbacks are supported: update_elements Called after
    elements are updated via `Form.update_elements()`. This allows for other GUI
    manipulation on each update of the GUI edit_enable Called before editing mode is
    enabled. This can be useful for asking for a password for example edit_disable
    Called after the editing mode is disabled.

    {element_name} Called while updating MAPPED element. This overrides the default
    element update implementation. Note that the {element_name} callback function
    needs to return a value to pass to Win[element].update()

    Args:
        callback_name: The name of the callback, from the list above
        fctn: The function to call. Note, the function must take in two parameters,
            a Form instance, and a PySimpleGUI.Window instance

    Returns:
        None
    """
    logger.info(f"Callback {callback_name} being set on Form")
    supported = ["update_elements", "edit_enable", "edit_disable"]

    # Add in mapped elements
    for mapped in self.element_map:
        supported.append(mapped.element.key)

    # Add in other window elements
    for element in self.window.key_dict:
        supported.append(element)

    if callback_name in supported:
        self.callbacks[callback_name] = fctn
    else:
        raise RuntimeError(
            f'Callback "{callback_name}" not supported. callback: {callback_name} '
            f"supported: {supported}"
        )

add_dataset(data_key, table, pk_column, description_column, query='', order_clause='')

Manually add a DataSet object to the Form When you attach to a database, PySimpleSQL isn't aware of what it contains until this command is run Note that auto_add_datasets() does this automatically, which is called when a Form is created.

Parameters:

Name Type Description Default
data_key str

The key to give this DataSet. Use frm['data_key'] to access it.

required
table str

The name of the table in the database

required
pk_column str

The primary key column of the table in the database

required
description_column str

The column to be used to display to users in listboxes, comboboxes, etc.

required
query str

The initial query for the table. Auto generates "SELECT * FROM {table}" if none is passed

''
order_clause str

The initial sort order for the query

''

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
def add_dataset(
    self,
    data_key: str,
    table: str,
    pk_column: str,
    description_column: str,
    query: str = "",
    order_clause: str = "",
) -> None:
    """Manually add a `DataSet` object to the `Form` When you attach to a database,
    PySimpleSQL isn't aware of what it contains until this command is run Note that
    `Form.auto_add_datasets()` does this automatically, which is called when a
    `Form` is created.

    Args:
        data_key: The key to give this `DataSet`.  Use frm['data_key'] to access it.
        table: The name of the table in the database
        pk_column: The primary key column of the table in the database
        description_column: The column to be used to display to users in listboxes,
            comboboxes, etc.
        query: The initial query for the table.  Auto generates "SELECT * FROM
            {table}" if none is passed
        order_clause: The initial sort order for the query

    Returns:
        None
    """
    self.datasets.update(
        {
            data_key: DataSet(
                data_key,
                self,
                table,
                pk_column,
                description_column,
                query,
                order_clause,
            )
        }
    )
    # set a default sort order
    self[data_key].set_search_order([description_column])

set_fk_column_cascade(child_table, fk_column, update_cascade=None, delete_cascade=None)

Set a foreign key's update_cascade and delete_cascade behavior.

auto_add_relationships() does this automatically from the database schema.

Parameters:

Name Type Description Default
child_table str

Child table with the foreign key.

required
fk_column str

Foreign key column of the child table.

required
update_cascade bool

True to requery and filter child table on selected parent primary key.

None
delete_cascade bool

True to delete dependent child records if parent record is deleted.

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
def set_fk_column_cascade(
    self,
    child_table: str,
    fk_column: str,
    update_cascade: bool = None,
    delete_cascade: bool = None,
) -> None:
    """Set a foreign key's update_cascade and delete_cascade behavior.

    `SQLDriver.auto_add_relationships()` does this automatically from the database
    schema.

    Args:
        child_table: Child table with the foreign key.
        fk_column: Foreign key column of the child table.
        update_cascade: True to requery and filter child table on selected parent
            primary key.
        delete_cascade: True to delete dependent child records if parent record is
            deleted.

    Returns:
        None
    """
    for rel in self.relationships:
        if rel.child_table == child_table and rel.fk_column == fk_column:
            logger.info(f"Updating {fk_column=} self.relationships.")
            if update_cascade is not None:
                rel.update_cascade = update_cascade
            if delete_cascade is not None:
                rel.delete_cascade = delete_cascade

auto_add_datasets()

Automatically add DataSet objects from the database.

Works by looping through the tables available and creating a DataSet object for each. Each dataset key by default name of the table.

This is called automatically when a Form is created. Note that add_dataset() can do this manually on a per-table basis.

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
def auto_add_datasets(self) -> None:
    """Automatically add `DataSet` objects from the database.

    Works by looping through the tables available and creating a `DataSet` object
    for each. Each dataset key by default name of the table.

    This is called automatically when a `Form ` is created. Note that
    `Form.add_dataset()` can do this manually on a per-table basis.

    Returns:
        None
    """
    logger.info(
        "Automatically generating dataset for each table in the sqlite database"
    )
    # Clear any current dataset so successive calls won't double the entries
    self.datasets = {}
    tables = self.driver.get_tables()
    for table in tables:
        column_info = self.driver.column_info(table)

        # auto generate description column.  Default it to the 2nd column,
        # but can be overwritten below
        description_column = column_info.col_name(1)
        for col in column_info.names:
            if col in self.description_column_names:
                description_column = col
                break

        # Get our pk column
        pk_column = self.driver.pk_column(table)

        data_key = table
        logger.debug(
            f'Adding DataSet "{data_key}" on table {table} to Form with primary '
            f"key {pk_column} and description of {description_column}"
        )
        self.add_dataset(data_key, table, pk_column, description_column)
        self.datasets[data_key].column_info = column_info

map_element(element, dataset, column, where_column=None, where_value=None)

Map a PySimpleGUI element to a specific DataSet column. This is what makes the GUI automatically update to the contents of the database. This happens automatically when a PySimpleGUI Window is bound to a Form by using the bind parameter of Form creation, or by executing auto_map_elements() as long as the element metadata is configured properly. This method can be used to manually map any element to any DataSet column regardless of metadata configuration.

Parameters:

Name Type Description Default
element Element

A PySimpleGUI Element

required
dataset DataSet

A DataSet object

required
column str

The name of the column to bind to the element

required
where_column str

Used for ke, value shorthand TODO: expand on this

None
where_value str

Used for ey, value shorthand TODO: expand on this

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
def map_element(
    self,
    element: sg.Element,
    dataset: DataSet,
    column: str,
    where_column: str = None,
    where_value: str = None,
) -> None:
    """Map a PySimpleGUI element to a specific `DataSet` column.  This is what makes
    the GUI automatically update to the contents of the database.  This happens
    automatically when a PySimpleGUI Window is bound to a `Form` by using the bind
    parameter of `Form` creation, or by executing `Form.auto_map_elements()` as long
    as the element metadata is configured properly. This method can be used to
    manually map any element to any `DataSet` column regardless of metadata
    configuration.

    Args:
        element: A PySimpleGUI Element
        dataset: A `DataSet` object
        column: The name of the column to bind to the element
        where_column: Used for ke, value shorthand TODO: expand on this
        where_value: Used for ey, value shorthand TODO: expand on this

    Returns:
        None
    """
    logger.debug(f"Mapping element {element.key}")
    self.element_map.append(
        ElementMap(element, dataset, column, where_column, where_value)
    )

add_info_element(element)

Add an element to be updated with info messages.

Must be either

Parameters:

Name Type Description Default
element Union[StatusBar, Text]

A PySimpleGUI Element

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
def add_info_element(self, element: Union[sg.StatusBar, sg.Text]) -> None:
    """Add an element to be updated with info messages.

    Must be either

    Args:
        element: A PySimpleGUI Element

    Returns:
        None
    """
    if not isinstance(element, (sg.StatusBar, sg.Text)):
        logger.debug(f"Can only add info {element!s}")
        return
    logger.debug(f"Mapping element {element.key}")
    self.popup.info_elements.append(element)

auto_map_elements(win, keys=None)

Automatically map PySimpleGUI Elements to DataSet columns. A special naming convention has to be used for automatic mapping to happen. Note that map_element() can be used to manually map an Element to a column. Automatic mapping relies on a special naming convention as well as certain data in the Element's metadata. The convenience functions field(), selector(), and actions() do this automatically and should be used in almost all cases to make elements that conform to this standard, but this information will allow you to do this manually if needed. For individual fields, Element keys must be named 'Table.column'. Additionally, the metadata must contain a dict with the key of 'type' set to [FIELD][pysimplesql.pysimplesql.ElementType.FIELD]. For selectors, the key can be named whatever you want, but the metadata must contain a dict with the key of 'type' set to [SELECTOR][pysimplesql.pysimplesql.ElementType.SELECTOR].

Parameters:

Name Type Description Default
win Window

A PySimpleGUI Window

required
keys List[str]

(optional) Limit the auto mapping to this list of Element keys

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
def auto_map_elements(self, win: sg.Window, keys: List[str] = None) -> None:
    """Automatically map PySimpleGUI Elements to `DataSet` columns. A special naming
    convention has to be used for automatic mapping to happen.  Note that
    `Form.map_element()` can be used to manually map an Element to a column.
    Automatic mapping relies on a special naming convention as well as certain data
    in the Element's metadata. The convenience functions `field()`, `selector()`,
    and `actions()` do this automatically and should be used in almost all cases to
    make elements that conform to this standard, but this information will allow you
    to do this manually if needed. For individual fields, Element keys must be named
    'Table.column'. Additionally, the metadata must contain a dict with the key of
    'type' set to `ElementType.FIELD`. For selectors, the key can be named whatever
    you want, but the metadata must contain a dict with the key of 'type' set to
    `ElementType.SELECTOR`.

    Args:
        win: A PySimpleGUI Window
        keys: (optional) Limit the auto mapping to this list of Element keys

    Returns:
        None
    """
    logger.info("Automapping elements")
    # Clear previously mapped elements so successive calls won't produce duplicates
    self.element_map = []
    for key in win.key_dict:
        element = win[key]

        # Skip this element if there is no metadata present
        if not isinstance(element.metadata, dict):
            continue

        # Process the filter to ensure this element should be mapped to this Form
        if (
            "filter" in element.metadata
            and element.metadata["filter"] == self.filter
        ):
            element.metadata["Form"] = self
        if self.filter is None and "filter" not in element.metadata:
            element.metadata["Form"] = self

        # Skip this element if it's an event
        if element.metadata["type"] == ElementType.EVENT:
            continue

        if element.metadata["Form"] != self:
            continue
        # If we passed in a custom list of elements
        if keys is not None and key not in keys:
            continue

        # Map Record Element
        if element.metadata["type"] == ElementType.FIELD:
            # Does this record imply a where clause (indicated by ?)
            # If so, we can strip out the information we need
            data_key = element.metadata["data_key"]
            field = element.metadata["field"]
            if "?" in field:
                table_info, where_info = field.split("?")
            else:
                table_info = field
                where_info = None
            try:
                table, col = table_info.split(".")
            except ValueError:
                table, col = table_info, None

            if where_info is None:
                where_column = where_value = None
            else:
                where_column, where_value = where_info.split("=")

            # make sure we don't use reserved keywords that could end up in a query
            for keyword in [table, col, where_column, where_value]:
                if keyword is not None and keyword:
                    self.driver.check_keyword(keyword)

            # DataSet objects are named after the tables they represent
            # (with an optional prefix)
            # TODO: How to handle the prefix?
            # TODO: check in DataSet.table
            if table in self.datasets and col in self[table].column_info:
                # Map this element to DataSet.column
                self.map_element(
                    element, self[table], col, where_column, where_value
                )
                if isinstance(element, (_EnhancedInput, _EnhancedMultiline)) and (
                    col in self[table].column_info.names
                    and self[table].column_info[col].notnull
                ):
                    element.add_placeholder(
                        placeholder=lang.notnull_placeholder,
                        color=themepack.placeholder_color,
                    )
                if (
                    isinstance(element, _EnhancedInput)
                    and col in self[table].column_info.names
                ):
                    element.add_validate(self[table], col)

        # Map Selector Element
        elif element.metadata["type"] == ElementType.SELECTOR:
            k = element.metadata["table"]
            if k is None:
                continue
            if element.metadata["Form"] != self:
                continue
            if "?" in k:
                table_info, where_info = k.split("?")
                where_column, where_value = where_info.split("=")
            else:
                table_info = k
                where_column = where_value = None
            data_key = table_info

            if data_key in self.datasets:
                self[data_key].add_selector(
                    element, data_key, where_column, where_value
                )

                # Enable sorting if TableBuilder is present
                if (
                    isinstance(element, sg.Table)
                    and "TableBuilder" in element.metadata
                ):
                    table_builder: TableBuilder = element.metadata["TableBuilder"]
                    # We need a whole chain of things to happen
                    # when a heading is clicked on:
                    # 1 Run the ResultRow.sort_cycle() with the correct column name
                    # 2 Run TableBuilder._update_headings() with the:
                    #   Table element, sort_column, sort_reverse
                    # 3 Run update_elements() to see the changes
                    table_builder._enable_heading_function(
                        element,
                        _HeadingCallback(self, data_key),
                    )

            else:
                logger.debug(f"Can not add selector {element!s}")

        elif element.metadata["type"] == ElementType.INFO:
            self.add_info_element(element)

set_element_clauses(element, where_clause=None, order_clause=None)

Set the where and/or order clauses for the specified element in the element map.

Parameters:

Name Type Description Default
element Element

A PySimpleGUI Element

required
where_clause str

(optional) The where clause to set

None
order_clause str

(optional) The order clause to set

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
def set_element_clauses(
    self, element: sg.Element, where_clause: str = None, order_clause: str = None
) -> None:
    """Set the where and/or order clauses for the specified element in the element
    map.

    Args:
        element: A PySimpleGUI Element
        where_clause: (optional) The where clause to set
        order_clause: (optional) The order clause to set

    Returns:
        None
    """
    for mapped in self.element_map:
        if mapped.element == element:
            mapped.where_clause = where_clause
            mapped.order_clause = order_clause

map_event(event, fctn, table=None)

Manually map a PySimpleGUI event (returned by Window.read()) to a callable. The callable will execute when the event is detected by process_events(). Most users will not have to manually map any events, as auto_map_events() will create most needed events when a PySimpleGUI Window is bound to a Form by using the bind parameter of Form creation, or by executing auto_map_elements().

Parameters:

Name Type Description Default
event str

The event to watch for, as returned by PySimpleGUI Window.read() (an element name for example)

required
fctn Callable[[None], None]

The callable to run when the event is detected. It should take no parameters and have no return value

required
table str

(optional) currently not used

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
def map_event(
    self, event: str, fctn: Callable[[None], None], table: str = None
) -> None:
    """Manually map a PySimpleGUI event (returned by Window.read()) to a callable.
    The callable will execute when the event is detected by `Form.process_events()`.
    Most users will not have to manually map any events, as `Form.auto_map_events()`
    will create most needed events when a PySimpleGUI Window is bound to a `Form` by
    using the bind parameter of `Form` creation, or by executing
    `Form.auto_map_elements()`.

    Args:
        event: The event to watch for, as returned by PySimpleGUI Window.read() (an
            element name for example)
        fctn: The callable to run when the event is detected. It should take no
            parameters and have no return value
        table: (optional) currently not used

    Returns:
        None
    """
    dic = {"event": event, "function": fctn, "table": table}
    logger.debug(f"Mapping event {event} to function {fctn}")
    self.event_map.append(dic)

replace_event(event, fctn, table=None)

Replace an event that was manually mapped with auto_map_events() or map_event(). The callable will execute.

Parameters:

Name Type Description Default
event str

The event to watch for, as returned by PySimpleGUI Window.read() (an element name for example)

required
fctn Callable[[None], None]

The callable to run when the event is detected. It should take no parameters and have no return value

required
table str

(optional) currently not used

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
def replace_event(
    self, event: str, fctn: Callable[[None], None], table: str = None
) -> None:
    """Replace an event that was manually mapped with `Form.auto_map_events()` or
    `Form.map_event()`. The callable will execute.

    Args:
        event: The event to watch for, as returned by PySimpleGUI Window.read() (an
            element name for example)
        fctn: The callable to run when the event is detected. It should take no
            parameters and have no return value
        table: (optional) currently not used

    Returns:
        None
    """
    for e in self.event_map:
        if e["event"] == event:
            e["function"] = fctn
            e["table"] = table if table is not None else e["table"]

auto_map_events(win)

Automatically map events. pysimplesql relies on certain events to function properly. This method maps all the record navigation (previous, next, etc.) and database actions (insert, delete, save, etc.). Note that the event mapper is very general-purpose, and you can add your own event triggers to the mapper using map_event(), or even replace one of the auto-generated ones if you have specific needs by using replace_event().

Parameters:

Name Type Description Default
win Window

A PySimpleGUI Window

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
4053
4054
4055
4056
4057
4058
4059
4060
4061
4062
4063
4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
def auto_map_events(self, win: sg.Window) -> None:
    """Automatically map events. pysimplesql relies on certain events to function
    properly. This method maps all the record navigation (previous, next, etc.) and
    database actions (insert, delete, save, etc.).  Note that the event mapper is
    very general-purpose, and you can add your own event triggers to the mapper
    using `Form.map_event()`, or even replace one of the auto-generated ones if you
    have specific needs by using `Form.replace_event()`.

    Args:
        win: A PySimpleGUI Window

    Returns:
        None
    """
    logger.info("Automapping events")
    # Clear mapped events to ensure successive calls won't produce duplicates
    self.event_map = []

    for key in win.key_dict:
        # key = str(key)  # sometimes end up with an integer element 0?TODO:Research
        element = win[key]
        # Skip this element if there is no metadata present
        if not isinstance(element.metadata, dict):
            logger.debug(f"Skipping mapping of {key}")
            continue
        if element.metadata["Form"] != self:
            continue
        if element.metadata["type"] == ElementType.EVENT:
            event_type = element.metadata["event_type"]
            table = element.metadata["table"]
            column = element.metadata["column"]
            function = element.metadata["function"]
            funct = None

            data_key = table
            data_key = data_key if data_key in self.datasets else None
            if event_type == EventType.FIRST:
                if data_key:
                    funct = self[data_key].first
            elif event_type == EventType.PREVIOUS:
                if data_key:
                    funct = self[data_key].previous
            elif event_type == EventType.NEXT:
                if data_key:
                    funct = self[data_key].next
            elif event_type == EventType.LAST:
                if data_key:
                    funct = self[data_key].last
            elif event_type == EventType.SAVE:
                if data_key:
                    funct = self[data_key].save_record
            elif event_type == EventType.INSERT:
                if data_key:
                    funct = self[data_key].insert_record
            elif event_type == EventType.DELETE:
                if data_key:
                    funct = self[data_key].delete_record
            elif event_type == EventType.DUPLICATE:
                if data_key:
                    funct = self[data_key].duplicate_record
            elif event_type == EventType.EDIT_PROTECT_DB:
                self.edit_protect()  # Enable it!
                funct = self.edit_protect
            elif event_type == EventType.SAVE_DB:
                funct = self.save_records
            elif event_type == EventType.SEARCH:
                # Build the search box name
                search_element, command = key.split(":")
                search_box = f"{search_element}:search_input"
                if data_key:
                    funct = functools.partial(self[data_key].search, search_box)
                    # add placeholder
                    self.window[search_box].add_placeholder(
                        placeholder=lang.search_placeholder,
                        color=themepack.placeholder_color,
                    )
                    # bind dataset
                    self.window[search_box].bind_dataset(self[data_key])
            elif event_type == EventType.QUICK_EDIT:
                quick_editor_kwargs = {}
                if "quick_editor_kwargs" in element.metadata:
                    quick_editor_kwargs = element.metadata["quick_editor_kwargs"]
                referring_table = table
                table = self[table].get_related_table_for_column(column)
                funct = functools.partial(
                    self[table].quick_editor,
                    self[referring_table].current.get_value,
                    column,
                    **quick_editor_kwargs if quick_editor_kwargs else {},
                )
            elif event_type == EventType.FUNCTION:
                funct = function
            else:
                logger.debug(f"Unsupported event_type: {event_type}")

            if funct is not None:
                self.map_event(key, funct, data_key)

edit_protect()

The edit protect system allows records to be protected from accidental editing by disabling the insert, delete, duplicate and save buttons on the GUI. A button to toggle the edit protect mode can easily be added by using the actions() convenience function.

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
4124
4125
4126
4127
4128
4129
4130
4131
4132
4133
4134
4135
4136
4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
4147
4148
4149
def edit_protect(self) -> None:
    """The edit protect system allows records to be protected from accidental
    editing by disabling the insert, delete, duplicate and save buttons on the GUI.
    A button to toggle the edit protect mode can easily be added by using the
    `actions()` convenience function.

    Returns:
        None
    """
    logger.debug("Toggling edit protect mode.")
    # Callbacks
    if (
        self._edit_protect
        and "edit_enable" in self.callbacks
        and not self.callbacks["edit_enable"](self, self.window)
    ):
        return
    if (
        not self._edit_protect
        and "edit_disable" in self.callbacks
        and not self.callbacks["edit_disable"](self, self.window)
    ):
        return

    self._edit_protect = not self._edit_protect
    self.update_elements(edit_protect_only=True)

get_edit_protect()

Get the current edit protect state.

Returns:

Type Description
bool

True if edit protect is enabled, False if not enabled

Source code in pysimplesql\pysimplesql.py
4151
4152
4153
4154
4155
4156
4157
def get_edit_protect(self) -> bool:
    """Get the current edit protect state.

    Returns:
        True if edit protect is enabled, False if not enabled
    """
    return self._edit_protect

prompt_save()

Prompt to save if any GUI changes are found the affect any table on this form. The helps prevent data entry loss when performing an action that changes the current record of a DataSet.

Returns:

Type Description
Type[PromptSaveReturn]

One of the prompt constant values: PromptSaveReturn.PROCEED,

Type[PromptSaveReturn]

PromptSaveReturn.DISCARDED, PromptSaveReturn.NONE

Source code in pysimplesql\pysimplesql.py
4159
4160
4161
4162
4163
4164
4165
4166
4167
4168
4169
4170
4171
4172
4173
4174
4175
4176
4177
4178
4179
4180
4181
4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
def prompt_save(self) -> Type[PromptSaveReturn]:
    """Prompt to save if any GUI changes are found the affect any table on this
    form. The helps prevent data entry loss when performing an action that changes
    the current record of a `DataSet`.

    Returns:
        One of the prompt constant values: PromptSaveReturn.PROCEED,
        PromptSaveReturn.DISCARDED, PromptSaveReturn.NONE
    """
    user_prompted = False  # Has the user been prompted yet?
    for data_key in self.datasets:
        if not self[data_key]._prompt_save:
            continue

        if self[data_key].records_changed(recursive=False) and not user_prompted:
            # only show popup once, regardless of how many dataset have changed
            user_prompted = True
            if self._prompt_save == AUTOSAVE_MODE:
                save_changes = "yes"
            else:
                save_changes = self.popup.yes_no(
                    lang.form_prompt_save_title, lang.form_prompt_save
                )
            if save_changes != "yes":
                # update the elements to erase any GUI changes,
                # since we are choosing not to save
                for data_key_ in self.datasets:
                    self[data_key_].purge_virtual()
                    self[data_key_].current.restore_backup()
                self.update_elements()
                # We did have a change, regardless if the user chose not to save
                return PromptSaveReturn.DISCARDED
            break
    if user_prompted:
        self.save_records(check_prompt_save=True)
    return PromptSaveReturn.PROCEED if user_prompted else PromptSaveReturn.NONE

set_prompt_save(mode)

Set the prompt to save action when navigating records for all DataSet objects associated with this Form.

Parameters:

Name Type Description Default
mode int

Use PROMPT_MODE to prompt to save when unsaved changes are present. AUTOSAVE_MODE to autosave when unsaved changes are present.

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
def set_prompt_save(self, mode: int) -> None:
    """Set the prompt to save action when navigating records for all `DataSet`
    objects associated with this `Form`.

    Args:
        mode: Use `PROMPT_MODE` to prompt to save when unsaved changes are present.
            `AUTOSAVE_MODE` to autosave when unsaved changes are present.

    Returns:
        None
    """
    self._prompt_save = mode
    for data_key in self.datasets:
        self[data_key].set_prompt_save(mode)

set_force_save(force=False)

Force save without checking for changes first, so even an unchanged record will be written back to the database.

Parameters:

Name Type Description Default
force bool

True to force unchanged records to save.

False

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
def set_force_save(self, force: bool = False) -> None:
    """Force save without checking for changes first, so even an unchanged record
    will be written back to the database.

    Args:
        force: True to force unchanged records to save.

    Returns:
        None
    """
    self.force_save = force

set_live_update(enable)

Toggle the immediate sync of field elements with other elements in Form.

When live-update is enabled, changes in a field element are immediately reflected in other elements in the same Form. This is achieved by binding the Window to watch for events that may trigger updates, such as mouse clicks, key presses, or selection changes in a combo box.

Parameters:

Name Type Description Default
enable bool

If True, changes in a field element are immediately reflected in other elements in the same Form. If False, live-update is disabled.

required
Source code in pysimplesql\pysimplesql.py
4223
4224
4225
4226
4227
4228
4229
4230
4231
4232
4233
4234
4235
4236
4237
4238
4239
4240
4241
4242
4243
4244
4245
4246
def set_live_update(self, enable: bool) -> None:
    """Toggle the immediate sync of field elements with other elements in Form.

    When live-update is enabled, changes in a field element are immediately
    reflected in other elements in the same Form. This is achieved by binding the
    Window to watch for events that may trigger updates, such as mouse clicks, key
    presses, or selection changes in a combo box.

    Args:
        enable: If True, changes in a field element are immediately reflected in
            other elements in the same Form. If False, live-update is disabled.
    """
    bind_events = ["<ButtonRelease-1>", "<KeyPress>", "<<ComboboxSelected>>"]
    if enable and not self._liveupdate_binds:
        self.live_update = True
        for event in bind_events:
            self._liveupdate_binds[event] = self.window.TKroot.bind(
                event, self._liveupdate, "+"
            )
    elif not enable and self._liveupdate_binds:
        for event, bind in self._liveupdate_binds.items():
            self.window.TKroot.unbind(event, bind)
        self._liveupdate_binds = {}
        self.live_update = False

save_records(table=None, cascade_only=False, check_prompt_save=False, update_elements=True)

Save records of all DataSet objects` associated with this Form.

Parameters:

Name Type Description Default
table str

Name of table to save, as well as any cascaded relationships. Used in prompt_save()

None
cascade_only bool

Save only tables with cascaded relationships. Default False.

False
check_prompt_save bool

Passed to save_record_recursive to check if individual DataSet has prompt_save enabled. Used when save_records() is called from prompt_save().

False
update_elements bool

(optional) Passed to [save_record_recursive()][pysimplesql.pysimplesql.Form.save_record_recursive]

True

Returns:

Type Description
Union[SAVE_SUCCESS, SAVE_FAIL, SAVE_NONE]

result - can be used with RETURN BITMASKS

Source code in pysimplesql\pysimplesql.py
4248
4249
4250
4251
4252
4253
4254
4255
4256
4257
4258
4259
4260
4261
4262
4263
4264
4265
4266
4267
4268
4269
4270
4271
4272
4273
4274
4275
4276
4277
4278
4279
4280
4281
4282
4283
4284
4285
4286
4287
4288
4289
4290
4291
4292
4293
4294
4295
4296
4297
4298
4299
4300
4301
4302
4303
4304
4305
4306
4307
4308
4309
4310
4311
4312
4313
4314
4315
4316
4317
4318
4319
4320
4321
4322
4323
4324
4325
4326
4327
4328
4329
4330
4331
4332
4333
4334
4335
4336
def save_records(
    self,
    table: str = None,
    cascade_only: bool = False,
    check_prompt_save: bool = False,
    update_elements: bool = True,
) -> Union[SAVE_SUCCESS, SAVE_FAIL, SAVE_NONE]:
    """Save records of all `DataSet` objects` associated with this `Form`.

    Args:
        table: Name of table to save, as well as any cascaded relationships. Used in
            `DataSet.prompt_save()`
        cascade_only: Save only tables with cascaded relationships. Default False.
        check_prompt_save: Passed to `DataSet.save_record_recursive` to check if
            individual `DataSet` has prompt_save enabled. Used when
            `Form.save_records()` is called from `Form.prompt_save()`.
        update_elements: (optional) Passed to `Form.save_record_recursive()`

    Returns:
        result - can be used with RETURN BITMASKS
    """
    if check_prompt_save:
        logger.debug("Saving records in all datasets that allow prompt_save...")
    else:
        logger.debug("Saving records in all datasets...")

    display_message = not self.save_quiet

    result = 0
    show_message = True
    failed_tables = []

    if table:
        tables = [table]  # if passed single table
    # for cascade_only, build list of top-level dataset that have children
    elif cascade_only:
        tables = [
            dataset.table
            for dataset in self.datasets.values()
            if len(self.relationships.get_update_cascade_tables(dataset.table))
            and self.relationships.get_parent(dataset.table) is None
        ]
    # default behavior, build list of top-level dataset (ones without a parent)
    else:
        tables = [
            dataset.table
            for dataset in self.datasets.values()
            if self.relationships.get_parent(dataset.table) is None
        ]

    # call save_record_recursive on tables, which saves from last to first.
    result_list = []
    for q in tables:
        res = self[q].save_record_recursive(
            results={},
            display_message=False,
            check_prompt_save=check_prompt_save,
            update_elements=update_elements,
        )
        result_list.append(res)

    # flatten list of result dicts
    results = {k: v for d in result_list for k, v in d.items()}
    logger.debug(f"Form.save_records - results of tables - {results}")

    # get tables that failed
    for t, res in results.items():
        if not res & SHOW_MESSAGE:
            show_message = (
                False  # Only one instance of not showing the message hides all
            )
        if res & SAVE_FAIL:
            failed_tables.append(t)
        result |= res

    # Build a descriptive message, since the save spans many tables potentially
    msg = ""
    msg_tables = ", ".join(failed_tables)
    if result & SAVE_FAIL:
        if result & SAVE_SUCCESS:
            msg = lang.form_save_partial
        msg += lang.form_save_problem.format_map(LangFormat(tables=msg_tables))
        if show_message:
            self.popup.ok(lang.form_save_problem_title, msg)
        return result
    msg = lang.form_save_success if result & SAVE_SUCCESS else lang.form_save_none
    if show_message:
        self.popup.info(msg, display_message=display_message)
    return result

update_elements(target_data_key=None, edit_protect_only=False, omit_elements=None)

Updated the GUI elements to reflect values from the database for this Form instance only. Not to be confused with the main update_elements(), which updates GUI elements for all Form instances. This method also executes [update_selectors()][pysimplesql.pysimplesql.update_selectors], which updates selector elements.

Parameters:

Name Type Description Default
target_data_key str

(optional) dataset key to update elements for, otherwise updates elements for all datasets

None
edit_protect_only bool

(optional) If true, only update items affected by edit_protect

False
omit_elements List[str]

A list of elements to omit updating

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
4338
4339
4340
4341
4342
4343
4344
4345
4346
4347
4348
4349
4350
4351
4352
4353
4354
4355
4356
4357
4358
4359
4360
4361
4362
4363
4364
4365
4366
4367
4368
4369
4370
4371
4372
4373
4374
4375
4376
4377
4378
4379
4380
4381
4382
4383
4384
4385
4386
4387
4388
def update_elements(
    self,
    target_data_key: str = None,
    edit_protect_only: bool = False,
    omit_elements: List[str] = None,
) -> None:
    """Updated the GUI elements to reflect values from the database for this `Form`
    instance only. Not to be confused with the main `update_elements()`, which
    updates GUI elements for all `Form` instances. This method also executes
    `update_selectors()`, which updates selector elements.

    Args:
        target_data_key: (optional) dataset key to update elements for, otherwise
            updates elements for all datasets
        edit_protect_only: (optional) If true, only update items affected by
            edit_protect
        omit_elements: A list of elements to omit updating

    Returns:
        None
    """
    if omit_elements is None:
        omit_elements = []

    msg = "edit protect" if edit_protect_only else "PySimpleGUI"
    logger.debug(f"update_elements(): Updating {msg} elements")
    # Disable/Enable action elements based on edit_protect or other situations

    for data_key in self.datasets:
        if target_data_key is not None and data_key != target_data_key:
            continue

        # disable mapped elements for this table if
        # there are no records in this table or edit protect mode
        disable = not self[data_key].row_count or self._edit_protect
        self.update_element_states(data_key, disable)

    self.update_actions(target_data_key)

    if edit_protect_only:
        return

    self.update_fields(target_data_key, omit_elements)

    self.update_selectors(target_data_key, omit_elements)

    # Run callbacks
    if "update_elements" in self.callbacks:
        # Running user update function
        logger.info("Running the update_elements callback...")
        self.callbacks["update_elements"](self, self.window)

update_actions(target_data_key=None)

Update state for action-buttons.

Parameters:

Name Type Description Default
target_data_key str

(optional) dataset key to update elements for, otherwise updates elements for all datasets

None
Source code in pysimplesql\pysimplesql.py
4390
4391
4392
4393
4394
4395
4396
4397
4398
4399
4400
4401
4402
4403
4404
4405
4406
4407
4408
4409
4410
4411
4412
4413
4414
4415
4416
4417
4418
4419
4420
4421
4422
4423
4424
4425
4426
4427
4428
4429
4430
4431
4432
4433
4434
4435
4436
4437
4438
4439
4440
4441
4442
4443
4444
4445
4446
4447
4448
4449
4450
4451
4452
4453
4454
4455
4456
def update_actions(self, target_data_key: str = None) -> None:
    """Update state for action-buttons.

    Args:
        target_data_key: (optional) dataset key to update elements for, otherwise
            updates elements for all datasets
    """
    win = self.window
    for data_key in self.datasets:
        if target_data_key is not None and data_key != target_data_key:
            continue

        # call row_count @property once
        row_count = self[data_key].row_count

        for m in (m for m in self.event_map if m["table"] == self[data_key].table):
            # Disable delete and mapped elements for this table if there are no
            # records in this table or edit protect mode
            if ":table_delete" in m["event"]:
                disable = not row_count or self._edit_protect
                win[m["event"]].update(disabled=disable)

            # Disable duplicate if no rows, edit protect, or current row virtual
            elif ":table_duplicate" in m["event"]:
                disable = bool(
                    not row_count
                    or self._edit_protect
                    or self[data_key].pk_is_virtual()
                )
                win[m["event"]].update(disabled=disable)

            # Disable first/prev if only 1 row, or first row
            elif ":table_first" in m["event"] or ":table_previous" in m["event"]:
                disable = row_count < 2 or self[data_key].current.index == 0
                win[m["event"]].update(disabled=disable)

            # Disable next/last if only 1 row, or last row
            elif ":table_next" in m["event"] or ":table_last" in m["event"]:
                disable = row_count < 2 or (
                    self[data_key].current.index == row_count - 1
                )
                win[m["event"]].update(disabled=disable)

            # Disable insert on children with no parent/virtual parent records or
            # edit protect mode
            elif ":table_insert" in m["event"]:
                parent = self.relationships.get_parent(data_key)
                if parent is not None:
                    disable = bool(
                        not self[parent].row_count
                        or self._edit_protect
                        or self.relationships.is_parent_virtual(
                            self[data_key].table, self
                        )
                    )
                else:
                    disable = self._edit_protect
                win[m["event"]].update(disabled=disable)

            # Disable db_save when needed
            elif ":db_save" in m["event"] or ":save_table" in m["event"]:
                disable = not row_count or self._edit_protect
                win[m["event"]].update(disabled=disable)

            # Enable/Disable quick edit buttons
            elif ":quick_edit" in m["event"]:
                win[m["event"]].update(disabled=disable)

update_fields(target_data_key=None, omit_elements=None, columns=None, combo_values_only=False)

Updated the field elements to reflect their [rows][pysimplesql.pysimplesql.rows] DataFrame for this Form instance only.

Parameters:

Name Type Description Default
target_data_key str

(optional) dataset key to update elements for, otherwise updates elements for all datasets

None
omit_elements List[str]

A list of elements to omit updating

None
columns List[str]

A list of column names to update

None
combo_values_only bool

Updates the value list only for comboboxes.

False
Source code in pysimplesql\pysimplesql.py
4458
4459
4460
4461
4462
4463
4464
4465
4466
4467
4468
4469
4470
4471
4472
4473
4474
4475
4476
4477
4478
4479
4480
4481
4482
4483
4484
4485
4486
4487
4488
4489
4490
4491
4492
4493
4494
4495
4496
4497
4498
4499
4500
4501
4502
4503
4504
4505
4506
4507
4508
4509
4510
4511
4512
4513
4514
4515
4516
4517
4518
4519
4520
4521
4522
4523
4524
4525
4526
4527
4528
4529
4530
4531
4532
4533
4534
4535
4536
4537
4538
4539
4540
4541
4542
4543
4544
4545
4546
4547
4548
4549
4550
4551
4552
4553
4554
4555
4556
4557
4558
4559
4560
4561
4562
4563
4564
4565
4566
4567
4568
4569
4570
4571
4572
4573
4574
4575
4576
4577
4578
4579
4580
4581
4582
4583
4584
4585
4586
4587
4588
4589
4590
4591
4592
4593
4594
4595
4596
4597
4598
4599
4600
4601
4602
4603
4604
4605
4606
4607
4608
4609
4610
4611
4612
4613
4614
4615
4616
4617
4618
4619
4620
4621
4622
4623
4624
4625
4626
4627
4628
4629
4630
4631
4632
4633
4634
4635
4636
4637
def update_fields(
    self,
    target_data_key: str = None,
    omit_elements: List[str] = None,
    columns: List[str] = None,
    combo_values_only: bool = False,
) -> None:
    """Updated the field elements to reflect their `rows` DataFrame for this `Form`
    instance only.

    Args:
        target_data_key: (optional) dataset key to update elements for, otherwise
            updates elements for all datasets
        omit_elements: A list of elements to omit updating
        columns: A list of column names to update
        combo_values_only: Updates the value list only for comboboxes.
    """
    if omit_elements is None:
        omit_elements = []

    if columns is None:
        columns = []

    # Render GUI Elements
    # d= dictionary (the element map dictionary)
    for mapped in self.element_map:
        # If the optional target_data_key parameter was passed, we will only update
        # elements bound to that table
        if (
            target_data_key is not None
            and mapped.table != self[target_data_key].table
        ):
            continue

        # skip updating this element if requested
        if mapped.element in omit_elements:
            continue

        if combo_values_only and not isinstance(mapped.element, sg.Combo):
            continue

        if len(columns) and mapped.column not in columns:
            continue

        # Update Markers
        # --------------------------------------------------------------------------
        # Show the Required Record marker if the column has notnull set and
        # this is a virtual row
        marker_key = mapped.element.key + ":marker"
        try:
            if mapped.dataset.pk_is_virtual():
                # get the column name from the key
                col = mapped.column
                # get notnull from the column info
                if (
                    col in mapped.dataset.column_info.names
                    and mapped.dataset.column_info[col].notnull
                ):
                    self.window[marker_key].update(
                        visible=True,
                        text_color=themepack.marker_required_color,
                    )
            else:
                self.window[marker_key].update(visible=False)
                if self.window is not None:
                    self.window[marker_key].update(visible=False)
        except AttributeError:
            self.window[marker_key].update(visible=False)

        updated_val = None
        # If there is a callback for this element, use it
        if mapped.element.key in self.callbacks:
            self.callbacks[mapped.element.key]()

        if mapped.where_column is not None:
            # We are looking for a key,value pair or similar.
            # Sift through and see what to put
            updated_val = mapped.dataset.get_keyed_value(
                mapped.column, mapped.where_column, mapped.where_value
            )
            # TODO, may need to add more??
            if isinstance(mapped.element, sg.Checkbox):
                updated_val = checkbox_to_bool(updated_val)

        elif isinstance(mapped.element, sg.Combo):
            # Update elements with foreign dataset first
            # This will basically only be things like comboboxes
            # Find the relationship to determine which table to get data from
            combo_vals = mapped.dataset.combobox_values(mapped.column)
            if not combo_vals:
                logger.info(
                    f"Error! Could not find related data for element "
                    f"{mapped.element.key} bound to DataSet "
                    f"key {mapped.table}, column: {mapped.column}"
                )
                # we don't want to update the list in this case, as it was most
                # likely supplied and not tied to data
                updated_val = mapped.dataset[mapped.column]
                mapped.element.update(updated_val)
                continue

            # else, first...
            # set to currently selected pk in gui
            if combo_values_only:
                match_val = mapped.element.get().get_pk()
            # or set to what is saved in current row
            else:
                match_val = mapped.dataset[mapped.column]

            # grab first matching entry (value)
            updated_val = next(
                (entry for entry in combo_vals if entry.get_pk() == match_val),
                None,
            )
            # and update element
            mapped.element.update(values=combo_vals)

        elif isinstance(mapped.element, sg.Text):
            rels = self.relationships.get_rels_for(mapped.dataset.table)
            found = False
            # try to get description of linked if foreign-key
            for rel in rels:
                if mapped.column == rel.fk_column:
                    updated_val = mapped.dataset.frm[
                        rel.parent_table
                    ].get_description_for_pk(mapped.dataset[mapped.column])
                    found = True
                    break
            if not found:
                updated_val = mapped.dataset[mapped.column]
            mapped.element.update("")

        elif isinstance(mapped.element, sg.Table):
            # Tables use an array of arrays for values.  Note that the headings
            # can't be changed.
            values = mapped.dataset.table_values()
            # Select the current one
            pk = mapped.dataset.current.pk

            if len(values):  # noqa SIM108
                # set index to pk
                index = [[v[0] for v in values].index(pk)]
            else:  # if empty
                index = []

            # Update table, and set vertical scroll bar to follow selected element
            update_table_element(self.window, mapped.element, values, index)
            continue

        elif isinstance(mapped.element, (sg.Input, sg.Multiline)):
            # Update the element in the GUI
            # For text objects, lets clear it first...

            # HACK for sqlite query not making needed keys! This will clear
            mapped.element.update("")

            updated_val = mapped.dataset[mapped.column]

        elif isinstance(mapped.element, sg.Checkbox):
            updated_val = checkbox_to_bool(mapped.dataset[mapped.column])

        elif isinstance(mapped.element, sg.Image):
            val = mapped.dataset[mapped.column]

            try:
                val = eval(val)
            except:  # noqa: E722
                # treat it as a filename
                mapped.element.update(val)
            else:
                # update the bytes data
                mapped.element.update(data=val)
            # Prevent the update from triggering below, since we are doing it here
            updated_val = None
        else:
            sg.popup(f"Unknown element type {type(mapped.element)}")

        # Finally, we will update the actual GUI element!
        if updated_val is not None:
            mapped.element.update(updated_val)

update_selectors(target_data_key=None, omit_elements=None, search_filter_only=False)

Updated the selector elements to reflect their [rows][pysimplesql.pysimplesql.rows] DataFrame.

Parameters:

Name Type Description Default
target_data_key str

(optional) dataset key to update elements for, otherwise updates elements for all datasets.

None
omit_elements List[str]

A list of elements to omit updating

None
search_filter_only bool

Only update Table elements that have enabled apply_search_filter.

False

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
4639
4640
4641
4642
4643
4644
4645
4646
4647
4648
4649
4650
4651
4652
4653
4654
4655
4656
4657
4658
4659
4660
4661
4662
4663
4664
4665
4666
4667
4668
4669
4670
4671
4672
4673
4674
4675
4676
4677
4678
4679
4680
4681
4682
4683
4684
4685
4686
4687
4688
4689
4690
4691
4692
4693
4694
4695
4696
4697
4698
4699
4700
4701
4702
4703
4704
4705
4706
4707
4708
4709
4710
4711
4712
4713
4714
4715
4716
4717
4718
4719
4720
4721
4722
4723
4724
4725
4726
4727
4728
4729
4730
4731
4732
4733
4734
4735
4736
4737
4738
4739
4740
4741
4742
4743
4744
4745
4746
4747
4748
4749
4750
4751
4752
4753
4754
4755
4756
4757
4758
4759
4760
4761
4762
def update_selectors(
    self,
    target_data_key: str = None,
    omit_elements: List[str] = None,
    search_filter_only: bool = False,
) -> None:
    """Updated the selector elements to reflect their `rows` DataFrame.

    Args:
        target_data_key: (optional) dataset key to update elements for, otherwise
            updates elements for all datasets.
        omit_elements: A list of elements to omit updating
        search_filter_only: Only update Table elements that have enabled
            `TableBuilder.apply_search_filter`.

    Returns:
        None
    """
    if omit_elements is None:
        omit_elements = []

    # ---------
    # SELECTORS
    # ---------
    # We can update the selector elements
    # We do it down here because it's not a mapped element...
    # Check for selector events
    for data_key, dataset in self.datasets.items():
        if target_data_key is not None and target_data_key != data_key:
            continue

        if len(dataset.selector):
            for e in dataset.selector:
                logger.debug("update_elements: SELECTOR FOUND")
                # skip updating this element if requested
                if e["element"] in omit_elements:
                    continue

                element: sg.Element = e["element"]
                logger.debug(f"{type(element)}")
                pk_column = dataset.pk_column
                description_column = dataset.description_column
                if element.key in self.callbacks:
                    self.callbacks[element.key]()

                if isinstance(element, (sg.Listbox, sg.Combo)):
                    logger.debug("update_elements: List/Combo selector found...")
                    lst = []
                    for _, r in dataset.rows.iterrows():
                        if e["where_column"] is not None:
                            # TODO: Kind of a hackish way to check for equality.
                            if str(r[e["where_column"]]) == str(e["where_value"]):
                                lst.append(
                                    ElementRow(r[pk_column], r[description_column])
                                )
                            else:
                                pass
                        else:
                            lst.append(
                                ElementRow(r[pk_column], r[description_column])
                            )

                    element.update(
                        values=lst,
                        set_to_index=dataset.current.index,
                    )

                    # set vertical scroll bar to follow selected element
                    # (for listboxes only)
                    if isinstance(element, sg.Listbox):
                        try:
                            element.set_vscroll_position(
                                dataset.current.index / len(lst)
                            )
                        except ZeroDivisionError:
                            element.set_vscroll_position(0)

                elif isinstance(element, sg.Slider):
                    # Re-range the element depending on the number of records
                    l = dataset.row_count  # noqa: E741
                    element.update(value=dataset.current.index + 1, range=(1, l))

                elif isinstance(element, sg.Table):
                    logger.debug("update_elements: Table selector found...")
                    # Populate entries
                    apply_search_filter = False
                    columns = None  # default to all columns

                    if "TableBuilder" in element.metadata:
                        columns = element.metadata["TableBuilder"].columns
                        apply_search_filter = element.metadata[
                            "TableBuilder"
                        ].apply_search_filter

                    # skip Tables that don't request search_filter
                    if search_filter_only and not apply_search_filter:
                        continue

                    values = dataset.table_values(
                        columns,
                        mark_unsaved=True,
                        apply_search_filter=apply_search_filter,
                    )

                    # Get the primary key to select.
                    # Use the list above instead of getting it directly
                    # from the table, as the data has yet to be updated
                    pk = dataset.current.pk

                    found = False
                    if len(values):
                        # set to index by pk
                        try:
                            index = [[v.pk for v in values].index(pk)]
                            found = True
                        except ValueError:
                            index = []
                    else:  # if empty
                        index = []

                    logger.debug(f"Selector:: index:{index} found:{found}")

                    # Update table, and set vertical scroll bar to follow
                    update_table_element(self.window, element, values, index)

requery_all(select_first=True, filtered=True, update_elements=True, requery_dependents=True)

Requeries all DataSet objects associated with this Form. This effectively re-loads the data from the database into DataSet objects.

Parameters:

Name Type Description Default
select_first bool

passed to requery() -> first(). If True, the first record will be selected after the requery

True
filtered bool

passed to requery(). If True, the relationships will be considered and an appropriate WHERE clause will be generated. False will display all records from the table.

True
update_elements bool

passed to requery() -> first() to update_elements(). Note that the select_first parameter must = True to use this parameter.

True
requery_dependents bool

passed to requery() -> first() to [requery_dependents()][pysimplesql.pysimplesql.Form.requery_dependents]. Note that the select_first parameter must = True to use this parameter.

True

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
4764
4765
4766
4767
4768
4769
4770
4771
4772
4773
4774
4775
4776
4777
4778
4779
4780
4781
4782
4783
4784
4785
4786
4787
4788
4789
4790
4791
4792
4793
4794
4795
4796
4797
4798
4799
4800
4801
4802
4803
4804
4805
4806
4807
def requery_all(
    self,
    select_first: bool = True,
    filtered: bool = True,
    update_elements: bool = True,
    requery_dependents: bool = True,
) -> None:
    """Requeries all `DataSet` objects associated with this `Form`. This effectively
    re-loads the data from the database into `DataSet` objects.

    Args:
        select_first: passed to `DataSet.requery()` -> `DataSet.first()`. If True,
            the first record will be selected after the requery
        filtered: passed to `DataSet.requery()`. If True, the relationships will be
            considered and an appropriate WHERE clause will be generated. False will
            display all records from the table.
        update_elements: passed to `DataSet.requery()` -> `DataSet.first()` to
            `Form.update_elements()`. Note that the select_first parameter must =
            True to use this parameter.
        requery_dependents: passed to `DataSet.requery()` -> `DataSet.first()` to
            `Form.requery_dependents()`. Note that the select_first parameter must =
            True to use this parameter.

    Returns:
        None
    """
    logger.info("Requerying all datasets")

    # first let datasets requery through cascade
    for data_key in self.datasets:
        if self.relationships.get_parent(data_key) is None:
            self[data_key].requery(
                select_first=select_first,
                filtered=filtered,
                update_elements=update_elements,
                requery_dependents=requery_dependents,
            )

    # fill in any datasets that are empty
    for data_key in self.datasets:
        if self[data_key].rows.columns.empty:
            self[data_key].rows = Result.set(
                pd.DataFrame(columns=self[data_key].column_info.names)
            )

process_events(event, values)

Process mapped events for this specific Form instance.

Not to be confused with the main process_events(), which processes events for ALL Form instances. This should be called once per iteration in your event loop. Note: Events handled are responsible for requerying and updating elements as needed.

Parameters:

Name Type Description Default
event str

The event returned by PySimpleGUI.read()

required
values list

the values returned by PySimpleGUI.read()

required

Returns:

Type Description
bool

True if an event was handled, False otherwise

Source code in pysimplesql\pysimplesql.py
4809
4810
4811
4812
4813
4814
4815
4816
4817
4818
4819
4820
4821
4822
4823
4824
4825
4826
4827
4828
4829
4830
4831
4832
4833
4834
4835
4836
4837
4838
4839
4840
4841
4842
4843
4844
4845
4846
4847
4848
4849
4850
4851
4852
4853
4854
4855
4856
4857
4858
4859
4860
4861
4862
4863
4864
4865
4866
4867
4868
4869
def process_events(self, event: str, values: list) -> bool:
    """Process mapped events for this specific `Form` instance.

    Not to be confused with the main `process_events()`, which processes events for
    ALL `Form` instances. This should be called once per iteration in your event
    loop. Note: Events handled are responsible for requerying and updating elements
    as needed.

    Args:
        event: The event returned by PySimpleGUI.read()
        values: the values returned by PySimpleGUI.read()

    Returns:
        True if an event was handled, False otherwise
    """
    if self.window is None:
        logger.info(
            "***** Form appears to be unbound. "
            "Do you have frm.bind(win) in your code? *****"
        )
        return False
    if event:
        for e in self.event_map:
            if e["event"] == event:
                logger.debug(f"Executing event {event} via event mapping.")
                e["function"]()
                logger.debug("Done processing event!")
                return True

        # Check for  selector events
        for _data_key, dataset in self.datasets.items():
            if len(dataset.selector):
                for e in dataset.selector:
                    element: sg.Element = e["element"]
                    if element.key == event and len(dataset.rows) > 0:
                        changed = False  # assume that a change will not take place
                        if isinstance(element, sg.Listbox):
                            row = values[element.Key][0]
                            dataset.set_by_pk(row.get_pk())
                            changed = True
                        elif isinstance(element, sg.Slider):
                            dataset.set_by_index(int(values[event]) - 1)
                            changed = True
                        elif isinstance(element, sg.Combo):
                            row = values[event]
                            dataset.set_by_pk(row.get_pk())
                            changed = True
                        elif isinstance(element, sg.Table) and len(values[event]):
                            if isinstance(element, LazyTable):
                                pk = int(values[event])
                            else:
                                index = values[event][0]
                                pk = self.window[event].Values[index].pk
                            # no need to update the selector!
                            dataset.set_by_pk(pk, True, omit_elements=[element])

                            changed = True
                        if changed and "record_changed" in dataset.callbacks:
                            dataset.callbacks["record_changed"](self, self.window)
                        return changed
    return False

update_element_states(table, disable=None, visible=None)

Disable/enable and/or show/hide all elements associated with a table.

Parameters:

Name Type Description Default
table str

table name associated with elements to disable/enable

required
disable bool

True/False to disable/enable element(s), None for no change

None
visible bool

True/False to make elements visible or not, None for no change

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
4871
4872
4873
4874
4875
4876
4877
4878
4879
4880
4881
4882
4883
4884
4885
4886
4887
4888
4889
4890
4891
4892
4893
4894
4895
4896
4897
def update_element_states(
    self, table: str, disable: bool = None, visible: bool = None
) -> None:
    """Disable/enable and/or show/hide all elements associated with a table.

    Args:
        table: table name associated with elements to disable/enable
        disable: True/False to disable/enable element(s), None for no change
        visible: True/False to make elements visible or not, None for no change

    Returns:
        None
    """
    for mapped in self.element_map:
        if mapped.table != table:
            continue
        element = mapped.element
        if isinstance(element, (sg.Input, sg.Multiline, sg.Combo, sg.Checkbox)):
            # if element.Key in self.window.key_dict.keys():
            logger.debug(
                f"Updating element {element.Key} to disabled: "
                f"{disable}, visible: {visible}"
            )
            if disable is not None:
                element.update(disabled=disable)
            if visible is not None:
                element.update(visible=visible)

purge_instance(frm) classmethod

Remove self from Form.instances.

Parameters:

Name Type Description Default
frm Form

the Form to purge

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
4899
4900
4901
4902
4903
4904
4905
4906
4907
4908
4909
@classmethod
def purge_instance(cls, frm: Form) -> None:
    """Remove self from Form.instances.

    Args:
        frm: the `Form` to purge

    Returns:
        None
    """
    cls.instances = [i for i in cls.instances if i != frm]

Utility

Utility functions are a collection of functions and classes that directly improve on aspects of the pysimplesql module.

See the documentation for the following utility functions: process_events(), update_elements(), bind(), simple_transform(), KeyGen(),

Note: This is a dummy class that exists purely to enhance documentation and has no use to the end user.

Popup(window=None)

Popup helper class.

Has popup functions for internal use. Stores last info popup as last_info

Source code in pysimplesql\pysimplesql.py
5101
5102
5103
5104
5105
5106
5107
5108
5109
5110
5111
5112
5113
5114
5115
5116
def __init__(self, window: sg.Window = None) -> None:
    """Create a new Popup instance :returns: None."""
    self.window = window
    self.popup_info = None
    self.last_info_msg: str = ""
    self.last_info_time = None
    self.info_elements = []
    self._timeout_id = None
    self._window_kwargs = {
        "keep_on_top": True,
        "element_justification": "center",
        "enable_close_attempted_event": True,
        "icon": themepack.icon,
        "ttk_theme": themepack.ttk_theme,
        "finalize": True,
    }

ok(title, msg)

Internal use only.

Creates sg.Window with LanguagePack OK button

Source code in pysimplesql\pysimplesql.py
5118
5119
5120
5121
5122
5123
5124
5125
5126
5127
5128
5129
5130
5131
5132
5133
5134
5135
5136
5137
5138
5139
def ok(self, title, msg) -> None:
    """Internal use only.

    Creates sg.Window with LanguagePack OK button
    """
    msg_lines = msg.splitlines()
    layout = [[sg.Text(line, font="bold")] for line in msg_lines]
    layout.append(
        sg.Button(
            button_text=lang.button_ok,
            key="ok",
            use_ttk_buttons=themepack.use_ttk_buttons,
            pad=themepack.popup_button_pad,
        )
    )
    popup_win = sg.Window(title, layout=[layout], modal=True, **self._window_kwargs)

    while True:
        event, values = popup_win.read()
        if event in ["ok", "-WINDOW CLOSE ATTEMPTED-"]:
            break
    popup_win.close()

yes_no(title, msg)

Internal use only.

Creates sg.Window with LanguagePack Yes/No button

Source code in pysimplesql\pysimplesql.py
5141
5142
5143
5144
5145
5146
5147
5148
5149
5150
5151
5152
5153
5154
5155
5156
5157
5158
5159
5160
5161
5162
5163
5164
5165
5166
5167
5168
5169
5170
5171
5172
def yes_no(self, title, msg):
    """Internal use only.

    Creates sg.Window with LanguagePack Yes/No button
    """
    msg_lines = msg.splitlines()
    layout = [[sg.Text(line, font="bold")] for line in msg_lines]
    layout.append(
        sg.Button(
            button_text=lang.button_yes,
            key="yes",
            use_ttk_buttons=themepack.use_ttk_buttons,
            pad=themepack.popup_button_pad,
        )
    )
    layout.append(
        sg.Button(
            button_text=lang.button_no,
            key="no",
            use_ttk_buttons=themepack.use_ttk_buttons,
            pad=themepack.popup_button_pad,
        )
    )
    popup_win = sg.Window(title, layout=[layout], modal=True, **self._window_kwargs)

    while True:
        event, values = popup_win.read()
        if event in ["no", "yes", "-WINDOW CLOSE ATTEMPTED-"]:
            result = event
            break
    popup_win.close()
    return result

info(msg, display_message=True, auto_close_seconds=None)

Displays a popup message and saves the message to self.last_info, auto- closing after x seconds. The title of the popup window is defined in lang.info_popup_title.

Parameters:

Name Type Description Default
msg str

The message to display.

required
display_message bool

(optional) If True (default), displays the message in the popup window. If False, only saves [msg][pysimplesql.pysimplesql.msg] to [last_info_msg][pysimplesql.pysimplesql.self.last_info_msg].

True
auto_close_seconds int

(optional) The number of seconds before the popup window auto-closes. If not provided, it is obtained from themepack.popup_info_auto_close_seconds.

None
Source code in pysimplesql\pysimplesql.py
5174
5175
5176
5177
5178
5179
5180
5181
5182
5183
5184
5185
5186
5187
5188
5189
5190
5191
5192
5193
5194
5195
5196
5197
5198
5199
5200
5201
5202
5203
5204
5205
5206
5207
def info(
    self, msg: str, display_message: bool = True, auto_close_seconds: int = None
) -> None:
    """Displays a popup message and saves the message to self.last_info, auto-
    closing after x seconds. The title of the popup window is defined in
    lang.info_popup_title.

    Args:
        msg: The message to display.
        display_message: (optional) If True (default), displays the message in the
            popup window. If False, only saves `msg` to `self.last_info_msg`.
        auto_close_seconds: (optional) The number of seconds before the popup window
            auto-closes. If not provided, it is obtained from
            themepack.popup_info_auto_close_seconds.
    """
    title = lang.info_popup_title
    if auto_close_seconds is None:
        auto_close_seconds = themepack.popup_info_auto_close_seconds
    self.last_info_msg = msg
    self.update_info_element()
    if display_message:
        msg_lines = msg.splitlines()
        layout = [[sg.Text(line, font="bold")] for line in msg_lines]
        if self.popup_info:
            return
        self.popup_info = sg.Window(
            title=title,
            layout=layout,
            alpha_channel=themepack.popup_info_alpha_channel,
            **self._window_kwargs,
        )
        self.popup_info.TKroot.after(
            int(auto_close_seconds * 1000), self._auto_close
        )

update_info_element(message=None, auto_erase_seconds=None, timeout=False, erase=False)

Update any mapped info elements.

Parameters:

Name Type Description Default
message str

Text message to update info elements with

None
auto_erase_seconds int

The number of seconds before automatically erasing the information element. If None, the default value from themepack will be used.

None
timeout bool

A boolean flag indicating whether to erase the information element. If True, and the elapsed time since the information element was last updated exceeds the auto_erase_seconds, the element will be cleared.

False
erase bool

Default False. Erase info elements

False
Source code in pysimplesql\pysimplesql.py
5215
5216
5217
5218
5219
5220
5221
5222
5223
5224
5225
5226
5227
5228
5229
5230
5231
5232
5233
5234
5235
5236
5237
5238
5239
5240
5241
5242
5243
5244
5245
5246
5247
5248
5249
5250
5251
5252
5253
5254
5255
5256
5257
5258
5259
5260
5261
def update_info_element(
    self,
    message: str = None,
    auto_erase_seconds: int = None,
    timeout: bool = False,
    erase: bool = False,
) -> None:
    """Update any mapped info elements.

    Args:
        message: Text message to update info elements with
        auto_erase_seconds: The number of seconds before automatically erasing the
            information element. If None, the default value from themepack will be
            used.
        timeout: A boolean flag indicating whether to erase the information element.
            If True, and the elapsed time since the information element was last
            updated exceeds the auto_erase_seconds, the element will be cleared.
        erase: Default False. Erase info elements
    """
    if auto_erase_seconds is None:
        auto_erase_seconds = themepack.info_element_auto_erase_seconds

    # set the text-string to update
    message = message or self.last_info_msg
    if erase:
        message = ""
        if self._timeout_id:
            self.window.TKroot.after_cancel(self._timeout_id)

    elif timeout and self.last_info_time:
        elapsed_sec = time() - self.last_info_time
        if elapsed_sec >= auto_erase_seconds:
            message = ""

    # update elements
    for element in self.info_elements:
        element.update(message)

    # record time of update, and tk.after
    if not erase and self.window:
        self.last_info_time = time()
        if self._timeout_id:
            self.window.TKroot.after_cancel(self._timeout_id)
        self._timeout_id = self.window.TKroot.after(
            int(auto_erase_seconds * 1000),
            lambda: self.update_info_element(timeout=True),
        )

ProgressBar(title, max_value=100, hide_delay=100)

The progress bar is updated by calling the update method to update the progress in incremental steps until the close method is called

Parameters:

Name Type Description Default
title str

Title of the window

required
max_value int

Maximum value of the progress bar

100
hide_delay int

Delay in milliseconds before displaying the Window

100

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
5265
5266
5267
5268
5269
5270
5271
5272
5273
5274
5275
5276
5277
5278
5279
5280
5281
5282
5283
5284
5285
5286
5287
5288
5289
5290
5291
5292
5293
5294
5295
5296
5297
5298
5299
5300
5301
def __init__(self, title: str, max_value: int = 100, hide_delay: int = 100) -> None:
    """Creates a progress bar window with a message label and a progress bar.

    The progress bar is updated by calling the `ProgressBar.update` method to update
    the progress in incremental steps until the `ProgressBar.close` method is called

    Args:
        title: Title of the window
        max_value: Maximum value of the progress bar
        hide_delay: Delay in milliseconds before displaying the Window

    Returns:
        None
    """
    self.win = None
    self.title = title
    self.layout = [
        [sg.Text("", key="message", size=(50, 2))],
        [
            sg.ProgressBar(
                max_value,
                orientation="h",
                size=(30, 20),
                key="bar",
                style=themepack.ttk_theme,
            )
        ],
    ]

    self.max = max
    self.hide_delay = hide_delay
    self.start_time = time() * 1000
    self.update_queue = queue.Queue()  # Thread safe
    self.animate_thread = None
    self._stop_event = threading.Event()  # Added stop event
    self.last_phrase_time = None
    self.phrase_index = 0

update(message, current_count)

Updates the progress bar with the current progress message and value.

Parameters:

Name Type Description Default
message str

Message to display

required
current_count int

Current value of the progress bar

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
5303
5304
5305
5306
5307
5308
5309
5310
5311
5312
5313
5314
5315
5316
5317
5318
5319
5320
def update(self, message: str, current_count: int) -> None:
    """Updates the progress bar with the current progress message and value.

    Args:
        message: Message to display
        current_count: Current value of the progress bar

    Returns:
        None
    """
    if time() * 1000 - self.start_time < self.hide_delay:
        return

    if self.win is None:
        self._create_window()

    self.win["message"].update(message)
    self.win["bar"].update(current_count=current_count)

close()

Closes the progress bar window.

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
5322
5323
5324
5325
5326
5327
5328
5329
def close(self) -> None:
    """Closes the progress bar window.

    Returns:
        None
    """
    if self.win is not None:
        self.win.close()

ProgressAnimate(title, config=None)

The progress bar will animate indefinitely, until the process passed in to the run method finishes.

The config for the animated progress bar contains oscillators for the bar divider and colors, a list of phrases to be displayed, and the number of seconds to elapse between phrases. This is all specified in the config dict as follows: my_oscillators = { # oscillators for the bar divider and colors "bar": {"value_start": 0, "value_range": 100, "period": 3, "offset": 0}, "red": {"value_start": 0, "value_range": 255, "period": 2, "offset": 0}, "green": {"value_start": 0, "value_range": 255, "period": 3, "offset": 120}, "blue": {"value_start": 0, "value_range": 255, "period": 4, "offset": 240},

# phrases to display and the number of seconds to elapse between phrases
"phrases": [
    "Loading...", "Please be patient...", "This may take a while...",
    "Almost done...", "Almost there...", "Just a little longer...",
    "Please wait...", "Still working...",
],
"phrase_delay": 2

} Defaults are used for any keys that are not specified in the dictionary.

Parameters:

Name Type Description Default
title str

Title of the window

required
config dict

Dictionary of configuration options as listed above

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
5343
5344
5345
5346
5347
5348
5349
5350
5351
5352
5353
5354
5355
5356
5357
5358
5359
5360
5361
5362
5363
5364
5365
5366
5367
5368
5369
5370
5371
5372
5373
5374
5375
5376
5377
5378
5379
5380
5381
5382
5383
5384
5385
5386
5387
5388
5389
5390
5391
5392
5393
5394
5395
5396
5397
5398
5399
5400
5401
5402
5403
5404
5405
5406
5407
5408
5409
5410
5411
5412
5413
5414
5415
5416
5417
5418
5419
5420
5421
5422
5423
5424
5425
5426
5427
5428
5429
5430
5431
5432
5433
5434
def __init__(self, title: str, config: dict = None) -> None:
    """Creates an animated progress bar with a message label.

    The progress bar will animate indefinitely, until the process passed in to the
    `ProgressAnimate.run` method finishes.

    The config for the animated progress bar contains oscillators for the bar
    divider and colors, a list of phrases to be displayed, and the number of seconds
    to elapse between phrases.  This is all specified in the config dict
    as follows:
    my_oscillators = {
        # oscillators for the bar divider and colors
        "bar": {"value_start": 0, "value_range": 100, "period": 3, "offset": 0},
        "red": {"value_start": 0, "value_range": 255, "period": 2, "offset": 0},
        "green": {"value_start": 0, "value_range": 255, "period": 3, "offset": 120},
        "blue": {"value_start": 0, "value_range": 255, "period": 4, "offset": 240},

        # phrases to display and the number of seconds to elapse between phrases
        "phrases": [
            "Loading...", "Please be patient...", "This may take a while...",
            "Almost done...", "Almost there...", "Just a little longer...",
            "Please wait...", "Still working...",
        ],
        "phrase_delay": 2
    }
    Defaults are used for any keys that are not specified in the dictionary.

    Args:
        title: Title of the window
        config: Dictionary of configuration options as listed above

    Returns:
        None
    """
    default_config = {
        # oscillators for the bar divider and colors
        "bar": {"value_start": 0, "value_range": 100, "period": 3, "offset": 0},
        "red": {"value_start": 0, "value_range": 255, "period": 2, "offset": 0},
        "green": {"value_start": 0, "value_range": 255, "period": 3, "offset": 120},
        "blue": {"value_start": 0, "value_range": 255, "period": 4, "offset": 240},
        # phrases to display and the number of seconds to elapse between phrases
        "phrases": lang.animate_phrases,
        "phrase_delay": 5,
    }
    if config is None:
        config = {}

    if type(config) is not dict:
        raise ValueError("config must be a dictionary")

    if set(config.keys()) - set(default_config.keys()):
        raise NotImplementedError(
            f"config may only contain keys: {default_config.keys()}"
        )

    for k in ["bar", "red", "green", "blue"]:
        if k in config and not all(isinstance(v, (int, float)) for v in config[k]):
            raise ValueError(f"values for {k} component must all be numeric")
        required_keys = {"value_start", "value_range", "period", "offset"}
        if k in config and not required_keys.issubset(set(config.keys())):
            raise ValueError(f"{k} must contain all of {required_keys}")

    if "phrases" in config:
        if type(config["phrases"]) is not list:
            raise ValueError("phrases must be a list")
        if not all(isinstance(v, str) for v in config["phrases"]):
            raise ValueError("phrases must be a list of strings")

    if "phrase_delay" in config and not all(
        isinstance(v, (int, float)) for v in config["phrase_delay"]
    ):
        raise ValueError("phrase_delay must be numeric")

    self.config = {**default_config, **config}

    self.title = title
    self.win: sg.Window = None
    self.layout = [
        [sg.Text("", key="message", size=(50, 2))],
        [
            sg.ProgressBar(
                100,
                orientation="h",
                size=(30, 20),
                key="bar",
                style=themepack.ttk_theme,
            )
        ],
    ]
    self.last_phrase_time = None
    self.phrase_index = 0
    self.completed = asyncio.Event()

run(fn, *args, **kwargs)

Runs the function in a separate co-routine, while animating the progress bar in another.

Source code in pysimplesql\pysimplesql.py
5436
5437
5438
5439
5440
5441
5442
5443
def run(self, fn: Callable, *args, **kwargs):
    """Runs the function in a separate co-routine, while animating the progress bar
    in another.
    """
    if not callable(fn):
        raise ValueError("fn must be a callable")

    return asyncio.run(self._dispatch(fn, *args, **kwargs))

KeyGen(separator='!')

The keygen system provides a mechanism to generate unique keys for use as PySimpleGUI element keys.

This is needed because many auto-generated items will have the same name. If for example you had two save buttons on the screen at the same time, they must have unique names. The keygen will append a separator and an incremental number to keys that would otherwise be duplicates. A global KeyGen instance is created automatically, see keygen for info.

Parameters:

Name Type Description Default
separator str

The default separator that goes between the key and the incremental number

'!'

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
5547
5548
5549
5550
5551
5552
5553
5554
5555
5556
5557
5558
def __init__(self, separator: str = "!") -> None:
    """Create a new KeyGen instance.

    Args:
        separator: The default separator that goes between the key and the
            incremental number

    Returns:
        None
    """
    self._keygen = {}
    self._separator = separator

get(key, separator=None)

Get a generated key from the KeyGen.

Parameters:

Name Type Description Default
key str

The key from which to generate the new key. If the key has not been used before, then it will be returned unmodified. For each successive call with the same key, it will be appended with the separator character and an incremental number. For example, if the key 'button' was passed to get() 3 times in a row, then the keys 'button', 'button:1', and 'button:2' would be returned respectively.

required
separator str

(optional) override the default separator wth this separator

None

Returns:

Type Description
str

None

Source code in pysimplesql\pysimplesql.py
5560
5561
5562
5563
5564
5565
5566
5567
5568
5569
5570
5571
5572
5573
5574
5575
5576
5577
5578
5579
5580
5581
5582
5583
5584
5585
5586
5587
def get(self, key: str, separator: str = None) -> str:
    """Get a generated key from the `KeyGen`.

    Args:
        key: The key from which to generate the new key. If the key has not been
            used before, then it will be returned unmodified. For each successive
            call with the same key, it will be appended with the separator character
            and an incremental number. For example, if the key 'button' was passed
            to `KeyGen.get()` 3 times in a row, then the keys 'button', 'button:1',
            and 'button:2' would be returned respectively.
        separator: (optional) override the default separator wth this separator

    Returns:
        None
    """
    if separator is None:
        separator = self._separator

    # Generate a unique key by attaching a sequential integer to the end
    if key not in self._keygen:
        self._keygen[key] = 0
    return_key = key
    if self._keygen[key] > 0:
        # only modify the key if it is a duplicate!
        return_key += f"{separator}{self._keygen[key]!s}"
    logger.debug(f"Key generated: {return_key}")
    self._keygen[key] += 1
    return return_key

reset_key(key)

Reset the generation sequence for the supplied key.

Parameters:

Name Type Description Default
key str

The base key to reset te sequence for

required
Source code in pysimplesql\pysimplesql.py
5589
5590
5591
5592
5593
5594
5595
5596
def reset_key(self, key: str) -> None:
    """Reset the generation sequence for the supplied key.

    Args:
        key: The base key to reset te sequence for
    """
    with contextlib.suppress(KeyError):
        del self._keygen[key]

reset()

Reset the entire KeyGen and remove all keys.

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
5598
5599
5600
5601
5602
5603
5604
def reset(self) -> None:
    """Reset the entire `KeyGen` and remove all keys.

    Returns:
        None
    """
    self._keygen = {}

reset_from_form(frm)

Reset keys from the keygen that were from mapped PySimpleGUI elements of that Form.

Parameters:

Name Type Description Default
frm Form

The Form from which to get the list of mapped elements

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
5606
5607
5608
5609
5610
5611
5612
5613
5614
5615
5616
5617
5618
def reset_from_form(self, frm: Form) -> None:
    """Reset keys from the keygen that were from mapped PySimpleGUI elements of that
    `Form`.

    Args:
        frm: The `Form` from which to get the list of mapped elements

    Returns:
        None
    """
    # reset keys related to form
    for mapped in frm.element_map:
        self.reset_key(mapped.element.key)

LazyTable(*args, lazy_loading=False, **kwargs)

Bases: Table

The LazyTable is a subclass of sg.Table for improved performance by loading rows lazily during scroll events. Updating a sg.Table is generally fast, but with large DataSets that contain thousands of rows, there may be some noticeable lag. LazyTable overcomes this by only inserting a slice of rows during an [update()][pysimplesql.pysimplesql.update].

To use, simply replace sg.Table with LazyTable as the 'element' argument in a selector() function call in your layout.

Expects values in the form of [TableRow(pk, values)], and only becomes active after a update(values=, selected_rows=[int]) call.

Note

LazyTable does not support the sg.Table.row_colors argument.

Parameters:

Name Type Description Default
*args

sg.Table specific args

()
lazy_loading bool

True to enable lazy loading

False
**kwargs

Additional sg.Table specific kwargs.

{}

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
5646
5647
5648
5649
5650
5651
5652
5653
5654
5655
5656
5657
5658
5659
5660
5661
5662
5663
5664
5665
5666
5667
5668
5669
5670
5671
5672
5673
5674
5675
5676
5677
5678
5679
def __init__(self, *args, lazy_loading: bool = False, **kwargs) -> None:
    """Initilize LazyTable.

    Args:
        *args: `sg.Table` specific args
        lazy_loading: True to enable lazy loading
        **kwargs: Additional `sg.Table` specific kwargs.


    Returns:
        None
    """
    # remove LazyTable only
    self.headings_justification = kwargs.pop("headings_justification", None)
    cols_justification = kwargs.pop("cols_justification", None)
    self.frame_pack_kwargs = kwargs.pop("frame_pack_kwargs", None)

    super().__init__(*args, **kwargs)

    # set cols_justification after, since PySimpleGUI sets it in its init
    self.cols_justification = cols_justification

    self.data = []  # lazy slice of rows
    self.lazy_loading: bool = True
    self.lazy_insert_qty: int = 100

    self._start_index = 0
    self._end_index = 0
    self._start_alt_color = False
    self._end_alt_color = False
    self._finalized = False
    self._lock = threading.Lock()
    self._bg = None
    self._fg = None

insert_qty property

Number of rows to insert during an update(values=) and scroll events.

SelectedRows property

Returns the selected row(s) in the LazyTable.

Returns:

Type Description
  • If the LazyTable has data:
  • Retrieves the index of the selected row by matching the primary key (pk) value with the first selected item in the widget.
  • Returns the corresponding row from the data list based on the index.
  • If the LazyTable has no data:
  • Returns None.

:note: This property assumes that the LazyTable is using a primary key (pk) value to uniquely identify rows in the data list.

Convenience

Convenience functions are a collection of functions and classes that aide in building PySimpleGUI layouts that conform to pysimplesql standards so that your database application is up and running quickly, and with all the great automatic functionality pysimplesql has to offer. See the documentation for the following convenience functions: field(), selector(), actions(), TableBuilder.

Note: This is a dummy class that exists purely to enhance documentation and has no use to the end user.

TableStyler dataclass

TODO.

TableBuilder dataclass

Bases: list

This is a convenience class used to build table headings for PySimpleGUI.

In addition, TableBuilder objects can sort columns in ascending or descending order by clicking on the column in the heading in the PySimpleGUI Table element if the sort_enable parameter is set to True.

Parameters:

Name Type Description Default
num_rows int

Number of rows to display in the table.

required
sort_enable bool

True to enable sorting by heading column.

True
allow_cell_edits bool

Double-click to edit a cell value if True. Accepted edits update both sg.Table and associated field element. Note: primary key, generated, or [readonly][pysimplesql.pysimplesql.readonly] columns don't allow cell edits.

False
lazy_loading bool

For larger DataSets (see LazyTable).

False
add_save_heading_button bool

Adds a save button to the left-most heading column if True.

False
apply_search_filter bool

Filter rows to only those columns in search_order that contain [search_string][pysimplesql.pysimplesql.Dataself.search_string].

False
style TableStyler field(default_factory=TableStyler)

Returns:

Type Description

None

num_rows: int instance-attribute

Number of rows to display in the table.

sort_enable: bool = True class-attribute instance-attribute

True to enable sorting by heading column.

allow_cell_edits: bool = False class-attribute instance-attribute

Double-click to edit a cell value if True. Accepted edits update both sg.Table and associated field element. Note: primary key, generated, or [readonly][pysimplesql.pysimplesql.readonly] columns don't allow cell edits.

lazy_loading: bool = False class-attribute instance-attribute

For larger DataSets (see LazyTable).

add_save_heading_button: bool = False class-attribute instance-attribute

Adds a save button to the left-most heading column if True.

apply_search_filter: bool = False class-attribute instance-attribute

Filter rows to only those columns in search_order that contain [search_string][pysimplesql.pysimplesql.Dataself.search_string].

heading_names: List[str] property

Return a list of heading_names for use with the headings parameter of PySimpleGUI.Table.

Returns:

Type Description
List[str]

a list of heading names

columns property

Return a list of column names.

Returns:

Type Description

a list of column names

col_justify_map: List[str] property

Convenience method for creating PySimpleGUI tables.

Returns:

Type Description
List[str]

a list column justifications for use with PySimpleGUI Table

List[str]

cols_justification parameter

heading_justify_map: List[str] property

Convenience method for creating PySimpleGUI tables.

Returns:

Type Description
List[str]

a list heading justifications for use with LazyTable [headings_justification][pysimplesql.pysimplesql.headings_justification]

heading_anchor_map: List[str] property

Internal method for passing directly to treeview heading() function.

Returns:

Type Description
List[str]

a list heading anchors for use with treeview heading() function.

visible_map: List[Union[bool, int]] property

Convenience method for creating PySimpleGUI tables.

Returns:

Type Description
List[Union[bool, int]]

a list of visible columns for use with th PySimpleGUI Table

List[Union[bool, int]]

visible_column_map parameter

width_map: List[int] property

Convenience method for creating PySimpleGUI tables.

Returns:

Type Description
List[int]

a list column widths for use with th PySimpleGUI Table col_widths parameter

add_column(column, heading, width, col_justify='default', heading_justify='column', readonly=False, visible=True)

Add a new heading column to this TableBuilder object. Columns are added in the order that this method is called. Note that the primary key column does not need to be included, as primary keys are stored internally in the TableRow class.

Parameters:

Name Type Description Default
column str

The name of the column in the database

required
heading str

The name of this columns heading (title)

required
width int

The width for this column to display within the Table element

required
col_justify ColumnJustify

Default 'left'. Available options: 'left', 'right', 'center', 'default'.

'default'
heading_justify HeadingJustify

Defaults to 'column' inherity [col_justify][pysimplesql.pysimplesql.col_justify]. Available options: 'left', 'right', 'center', 'column', 'default'.

'column'
readonly bool

Indicates if the column is read-only when allow_cell_edits is True.

False
visible bool

True if the column is visible. Typically, the only hidden column would be the primary key column if any. This is also useful if the rows DataFrame has information that you don't want to display.

True

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
7359
7360
7361
7362
7363
7364
7365
7366
7367
7368
7369
7370
7371
7372
7373
7374
7375
7376
7377
7378
7379
7380
7381
7382
7383
7384
7385
7386
7387
7388
7389
7390
7391
7392
7393
7394
7395
7396
7397
7398
7399
7400
7401
7402
7403
7404
7405
7406
7407
7408
def add_column(
    self,
    column: str,
    heading: str,
    width: int,
    col_justify: ColumnJustify = "default",
    heading_justify: HeadingJustify = "column",
    readonly: bool = False,
    visible: bool = True,
) -> None:
    """Add a new heading column to this TableBuilder object.  Columns are added in
    the order that this method is called. Note that the primary key column does not
    need to be included, as primary keys are stored internally in the `TableRow`
    class.

    Args:
        column: The name of the column in the database
        heading: The name of this columns heading (title)
        width: The width for this column to display within the Table element
        col_justify: Default 'left'. Available options: 'left', 'right', 'center',
            'default'.
        heading_justify: Defaults to 'column' inherity `col_justify`. Available
            options: 'left', 'right', 'center', 'column', 'default'.
        readonly: Indicates if the column is read-only when
            `TableBuilder.allow_cell_edits` is True.
        visible: True if the column is visible.  Typically, the only hidden column
            would be the primary key column if any. This is also useful if the
            `DataSet.rows` DataFrame has information that you don't want to display.

    Returns:
        None
    """
    self.append({"heading": heading, "column": column})
    self._width_map.append(width)

    # column justify
    if col_justify == "default":
        col_justify = self.style.justification
    self._col_justify_map.append(col_justify)

    # heading justify
    if heading_justify == "column":
        heading_justify = col_justify
    if heading_justify == "default":
        heading_justify = self.style.justification
    self._heading_justify_map.append(heading_justify)

    self._visible_map.append(visible)
    if readonly:
        self.readonly_columns.append(column)

ThemePack(tp_dict=None)

ThemePacks are user-definable objects that allow for the look and feel of database applications built with PySimpleGUI + pysimplesql. This includes everything from icons, the ttk themes, to sounds. Pysimplesql comes with 3 pre-made ThemePacks: default (aka ss_small), ss_large and ss_text. Creating your own is easy as well! In fact, a ThemePack can be as simple as one line if you just want to change one aspect of the default ThemePack. Example: my_tp = {'search': 'Click here to search'} # I want a different search button.

Once a ThemePack is created, it's very easy to use. Here is a very simple example of using a ThemePack: ss.themepack(my_tp_dict_variable) # make a search button, using the 'search' key from the ThemePack sg.Button(ss.themepack.search, key='search_button')

Source code in pysimplesql\pysimplesql.py
8160
8161
8162
def __init__(self, tp_dict: Dict[str, str] = None) -> None:
    """Initialize the `ThemePack` class."""
    self.tp_dict = tp_dict or ThemePack.default

default: Dict[Any] = {'ttk_theme': 'default', 'use_ttk_buttons': True, 'default_element_pad': (5, 0), 'action_button_pad': (3, 0), 'popup_button_pad': (5, 5), 'edit_protect': b'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', 'quick_edit': b'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', 'save': b'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', 'first': b'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', 'previous': b'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', 'next': b'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', 'last': b'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', 'insert': b'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', 'delete': b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAHUHpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjarVhbkiQpDvznFHsEQDzEcUCA2d5gjr8OCLKqumd2xmwyOjMIgofkLlyqNuOP/07zH3x8sMGEmDmVlCw+oYTiKxpsz6fsX2fD/tUHexvf+s174dFFuNN5zFXHV/THz4S7h2vf+w3rG8+6kHsL7w+tnVe7fzUS/f70u6ALlXEaqXD+amrThUQHblP0G55Z57aezbeODJR6xEbk/SBHdv/ysYDOt+LL+EU/xlkqaBM5g5un6xIA+ebeA9B+BegbyLdlfqL/Wj/A91X76QeWSTFC47cvXPzRT28b/3Vjehb57y/8eAz/AvKcneccx7saEhBNGlEbbHeXwcAGyGlPS7gyvhHtvK+Ci221Asq7FdtwiSvOg5VpXHDdVTfd2HdxAhODHz7j7r142n1M2RcvYMlRWJebPoOxDgY9iR8G1AXyzxa39y17P3GMnbvDUO+wmMOUP73MX738J5eZUxZEzvLDCnb5FdcwYzG3fjEKhLipvMUN8L2UfvslflaoBgxbMDMcrLadJVp0n9iizTNhXMT9HCFnctcFABH2jjDGERiwyVF0ydnsfXYOODIIqrDcU/ANDLgYfYeRPhAlb7LHkcHemJPdHuujT351Q5tARKREGdzgTIGsECLiJwdGDNVIMcQYU8yRTSyxJkohxZRSTkvkaqYccswp58y55MrEgSMnzsxcuBZfCBoYSyq5cCmlVm8qNqpYq2J8RU/zjVposaWWG7fSqiB8JEiUJFlYitTuO3XIRE89d+6l1+HMgFKMMOJIIw8eZdSJWJs0w4wzzTx5llkfa8rqL9c/YM0pa34ztcblxxp6Tc53CbfkJC7OwJgPDoznxQAC2i/OLLsQ/GJucWYLZIyih5FxcWO6W4yBwjCcj9M97j7M/S3eTOS/xZv/f8yZRd2/wZwBdb/y9hvW+spzshk7p3BhagmnD5Aw4ogxzU4gJa2ujho6nHIB/xiBvboYa4ictyxSTl8BdnzmtF7JTKSQ/QQp/XGnRmecRBiIRHeeArAZclZbmQiQomVw/qhJ2GNK8alua2KC/JW47IrBAaW8m0ivfZ7lEsmg7s56kHLjBYicd0VmkmHTfteo2KFeSJhBJlX1I9Ok9syGQK+GAURhdsuDzqTRaSQAPXRxnimMUe/GFCaV8wprEPmhgBnAp74TrXDZ2CJ+aPsCIovPNfbtbysjFqHjPJcBm49dUHQzT7dF2hd/xofkU+tvtIvj0eTVbKGRl7/PBCwU6At6Ms+kkamzH3u1IBJGPs4FBCQd4HGEKg6jWi4mFwxKZ//uEf/Z6TvUWimpUz6Hjxv1rAQv137KrMFkV/aDtTHfSGG+AIsM0KyBOZgkraLmshxF+olUE/oNVRtSP4Ah4YZMN4oQ6eROuzQHPXyB1so1TRIWumCzqO3aQLrth+kqI5K9kCffLykBMCmhxo2Mf8dr7DwGANEZyO8nngFLO3s7Wbht+1zKrl2jUR73105qXE9ZZhms5ISMCaTrQInKnZBOtAQr65Cb1eIe9WyPdIO/5RUOHL/iyr9G7oPVOOFrrIWP7QV0yuFAjHpmDETrmTFamcB78BmZi4WIcSajg4MbBHfKx5162rRK1oMzaBc1JUQI9gV/WQgZOQPy8RfJn1VRbDqBHWuRFK/OrNLtszWAOmMEkd1CLnLNdtBVq47eu+t68DBx1oAM/dwPOSlZ0GzUaR/i6Ewppa9ss+PdaxBAqS9LV9ygtaznhVbpx/z6EXXpaRmkR1WpJ2jZ+HNJli3+0GRoXkjkVb7sIGr8RqW3TZjenwfmWbNGONQBEBvF4Zrt2nEaOc5CHVWpA9KVin2RPjTdrCM8D4szmjB/Y6vq8JNhVaNvOi4Q5a7HaUBqkWo4PRFGqmnvwfugK2ujsCOlEtJ5JWPsLrPCJFx9Wk7QGdEBtQwdLjzW03UDXiCH6Y4bYES2Jo+DcHi+2ZewiIdTJu2MPFTB8RDkpjt8TL4GjBcwL8nAENFO74q/Adr0QAr4kJM8ghiAppK1SGCq/BsdhV5TOmYlHI16T0nB7pp7zM44q0w5ZwYEyY1pnKp+90ZGc3rcCr800D4SbAp9DrxualdOPCxx/0Q9j/CMgq2nYGnX0rUQwkGdq/iDCX/zfkoB+7DFkUFJ+rOUwPpwJmyFRPeIV1uipibcSy8qzj6JZrck8eX3ZsuxBX9dxHPWQLdGaEfNgaJ0XB3VNF9cry+nrmpA8QIJQuUYZ3Z5NMqn3JArjbA0fbK+Gp2Cva9RUj61S9nc0Kmkm3Sp7kv+mJ8zLKy5EdnclVeEnd0M5NfVeYFRVZSg9RGOWVVd4GsfYs32pJkTAX7qJZR+HRUiqtPPyR968nm2cSFA+Lg+tEjFMSgvCUjXQxuA6ac3PK3q/Va5q7o9cYe/EQ5U1VsNxvWfTumUx5if/Av/m72RWEYWHWx/3l/Oh5EzjxSjuRV1rS8N2Rc1KX9Kj/6yykT5Xsz/AFfFmNHyuZtSAAABhGlDQ1BJQ0MgcHJvZmlsZQAAeJx9kT1Iw0AcxV9TpUVaBO0gopChOlkQFXHUKhShQqgVWnUwufQLmjQkLS6OgmvBwY/FqoOLs64OroIg+AHi5Oik6CIl/i8ptIjx4Lgf7+497t4BQqPMNKtrHND0qplKxMVMdlUMvCKIPoQxDFFmljEnSUl4jq97+Ph6F+NZ3uf+HGE1ZzHAJxLPMsOsEm8QT29WDc77xBFWlFXic+Ixky5I/Mh1xeU3zgWHBZ4ZMdOpeeIIsVjoYKWDWdHUiKeIo6qmU76QcVnlvMVZK9dY6578haGcvrLMdZpDSGARS5AgQkENJZRRRYxWnRQLKdqPe/gHHb9ELoVcJTByLKACDbLjB/+D391a+ckJNykUB7pfbPtjBAjsAs26bX8f23bzBPA/A1d6219pADOfpNfbWvQI6N0GLq7bmrIHXO4AA0+GbMqO5Kcp5PPA+xl9UxbovwV61tzeWvs4fQDS1FXyBjg4BEYLlL3u8e5gZ2//nmn19wNkDXKhWfC+CAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+QIEg0fGF2PInoAAAN+SURBVDjLVZPvTxN3AMafu++3d+0VmgrSnxa1lGtjDdEdSqJg3cY0zhVjpIklITF74b+x1/4Bezm3ZBkJ4BSiQxZ4IZRkQyzJkBpqZvlRSO9oWopcud61pXuxSOLz/vO8eD55mEmnE6qigAK83W7vypVKqWbg8B4+zygABRDCkhQuJJMrNUA3u91gVUWBw+eD4+bNmfCjR6/bL1+emgPohMt1DD91u/EjQKVodKrzwYPXJ65fn7GLIvRcDiwBeHru3Hw4Hu/bnZ+HPRSKRHt6Rv6WZfrEasUYgIlcjv7Q3z/SfuNGRHn2DK0nT/bBbJ4nAE89vb1dHYODfdnpaei5HMCyaOnoiH1VrTqSy8v92wCGL1yYFQcGIvKLF9CLRbAfP8IZCvWx9XoXXVtYSNXr9Tmb3x8BgIauQ/vwAa2BQOQLk+lxj82Gzmg0Io+OonpwAEIIOLcb+1tbc5upVIr5HcAUQIeuXBmxnzoVO8xkwDIMGJYF7/XC0dsLZWoKejYLptGAxe9HoVAY/3lpaWigqanGAMCEy4U/ZJnGr16dtTmdkcrGBo4qFdSLRTCyjLrJBGqxwCKK2Ne0uZ9Sqf6Y11u7t7MD5tPS4xyHN4ZBv7548TFfLg/rGxsglIIQApZhIIRC2NO0Xyffvv2+t62tdj+fBwCwx644Dk0AwPPw3r0LxjD+L6AUnNkMwvMwDAMnADQIOcbYT57/UVUqeb2znbduDecTCVBBAAFAGAaEZcFms+hobx/uEcXZhCzTMZ8PAMA8sVqRLpdp96VLI+Lt2zHl5UuoS0vgbDYIwSBMhKCRzcJECCil4IJBpDc3x39ZXR2Kulw18l21KgQ8nj/FePzbnelplBcXQQiBNRxGQVWTZcPItfl8HnZ/H7zFAq5SgScQCDuOjiK5zc0x2tLWFhYfPozknj+HmkzC1NQEIRhESdPeb71796UGgJekN2eDQZEqCnhCYJJlSJIUqVWrYdbI51fWX71KVDUNDABLIICiqqbXV1clu8t14HC5DhaTSenf3d00d+YMOEJgFUWkM5mEnMmsUEMQdGN7+5rOMPM2Seo70LT3u+l0d4vXWx7c2QEAjPl85YXl5W4zzydDfr/419pagq3VrhUBME/dbuh7ezA1N1tMFsudw1JphgpCISbLn935N6cTRUVp7Tx//pv8+vrkdrmsnT19Gv8BFBBmvuY6IW0AAAAASUVORK5CYII=', 'duplicate': b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw1AUhU9TRZGKQztIcchQnSyIFnHUKhShQqgVWnUweekfNGlIUlwcBdeCgz+LVQcXZ10dXAVB8AfE1cVJ0UVKvC8ptIjxwuN9nHfP4b37AKFZZZrVMwFoum1mUkkxl18V+14hIIAwokjIzDLmJCkN3/q6p16quzjP8u/7swbVgsWAgEg8ywzTJt4gnt60Dc77xBFWllXic+Jxky5I/Mh1xeM3ziWXBZ4ZMbOZeeIIsVjqYqWLWdnUiBPEMVXTKV/Ieaxy3uKsVeusfU/+wlBBX1nmOq0RpLCIJUgQoaCOCqqwEaddJ8VChs6TPv6o65fIpZCrAkaOBdSgQXb94H/we7ZWcWrSSwolgd4Xx/kYBfp2gVbDcb6PHad1AgSfgSu94681gZlP0hsdLXYEDG0DF9cdTdkDLneA4SdDNmVXCtISikXg/Yy+KQ+Eb4GBNW9u7XOcPgBZmlX6Bjg4BMZKlL3u8+7+7rn929Oe3w9rHnKk7x4JKQAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+cCARMnD1HzB0IAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAABJUlEQVQ4y6WTT2qDQBTGvxnLwFTETZfZZCu9hPdwJei2B3GThZcovUJAkx6hdXqBisxOycI/YF43VWxiTEo+eAy8gW9+35sZMMYeAWxM0zwAoEvFOSfbtvcA1piIAdhEUfTieR4451iSUgqu634BcMamaZqHoihoqqZpLtYv0WpqTFprIiLK85x836elKJP6GOKMBr7vU5ZldIuSJCEhxHY0GPBuldaaDMOg5akBqOsaYRjO7vV9j6sEZVnO9rXWBIAelk7uug5VVQHAuEopIYTA2S2cEgRBMDv9OI7/EIBzflcEblnWu1IK92gNQA2Ip2rbdsSeI5garf77DqSUx+ktfAP4TNP02XGcq9i73Q51Xb+dxRFCbA3DWPwHUsojgFfG2NMPCKbWh17KiKEAAAAASUVORK5CYII=', 'icon': b'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', 'search': 'Search', 'unsaved_column_header': '💾', 'unsaved_column_width': 3, 'marker_unsaved': '✱', 'marker_required': '✱', 'marker_required_color': 'red2', 'placeholder_color': 'grey', 'marker_sort_asc': '▼', 'marker_sort_desc': '▲', 'popup_info_auto_close_seconds': 1.5, 'popup_info_alpha_channel': 0.85, 'info_element_auto_erase_seconds': 5, 'live_update_typing_delay_seconds': 0.3, 'default_label_size': (15, 1), 'default_element_size': (30, 1), 'default_mline_size': (30, 7), 'use_cell_buttons': True, 'text_min_width': 80, 'combobox_min_width': 80, 'checkbox_min_width': 75, 'datepicker_min_width': 80, 'display_bool_as_checkbox': True, 'checkbox_true': '☑', 'checkbox_false': '☐', 'validate_exception_animation': lambda : shake_widget(widget)} class-attribute

Default Themepack.

__call__(tp_dict=None)

Update the ThemePack object from tp_dict.

NOTE: You can add additional keys if desired

tp_example = { 'ttk_theme : the name of a ttk theme 'edit_protect' : either base64 image (eg b''), or string eg '', f'' 'quick_edit' : either base64 image (eg b''), or string eg '', f'' 'save' : either base64 image (eg b''), or string eg '', f'' 'first' : either base64 image (eg b''), or string eg '', f'' 'previous' : either base64 image (eg b''), or string eg '', f'' 'next' : either base64 image (eg b''), or string eg '', f'' 'last' : either base64 image (eg b'') or a string eg '', f'' 'insert' : either base64 image (eg b''), or string eg '', f'' 'delete' : either base64 image (eg b''), or string eg '', f'' 'duplicate' : either base64 image (eg b''), or string eg '', f'' 'search' : either base64 image (eg b''), or string eg '', f'' 'marker_unsaved' : string eg '', f'', unicode 'marker_required' : string eg '', f'', unicode 'marker_required_color': string eg 'red', Tuple eg (255,0,0) 'marker_sort_asc': string eg '', f'', unicode 'marker_sort_desc': string eg '', f'', unicode }

For Base64, you can convert a whole folder using https://github.com/PySimpleGUI/PySimpleGUI-Base64-Encoder # fmt: skip Remember to us b'' around the string.

Parameters:

Name Type Description Default
tp_dict Dict[str, str]

(optional) A dict formatted as above to create the ThemePack from. If one is not supplied, a default ThemePack will be generated. Any keys not present in the supplied tp_dict will be generated from the default values. Additionally, tp_dict may contain additional keys not specified in the minimal default ThemePack.

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
8177
8178
8179
8180
8181
8182
8183
8184
8185
8186
8187
8188
8189
8190
8191
8192
8193
8194
8195
8196
8197
8198
8199
8200
8201
8202
8203
8204
8205
8206
8207
8208
8209
8210
8211
8212
8213
8214
8215
def __call__(self, tp_dict: Dict[str, str] = None) -> None:
    """Update the ThemePack object from tp_dict.

    Example minimal ThemePack: NOTE: You can add additional keys if desired
        tp_example = {
            'ttk_theme : the name of a ttk theme
            'edit_protect' : either base64 image (eg b''), or string eg '', f''
            'quick_edit' : either base64 image (eg b''), or string eg '', f''
            'save' : either base64 image (eg b''), or string eg '', f''
            'first' : either base64 image (eg b''), or string eg '', f''
            'previous' : either base64 image (eg b''), or string eg '', f''
            'next' : either base64 image (eg b''), or string eg '', f''
            'last' : either base64 image (eg b'') or a string eg '', f''
            'insert' : either base64 image (eg b''), or string eg '', f''
            'delete' : either base64 image (eg b''), or string eg '', f''
            'duplicate' : either base64 image (eg b''), or string eg '', f''
            'search' : either base64 image (eg b''), or string eg '', f''
            'marker_unsaved' : string eg '', f'',  unicode
            'marker_required' : string eg '', f'',  unicode
            'marker_required_color': string eg 'red', Tuple eg (255,0,0)
            'marker_sort_asc': string eg '', f'',  unicode
            'marker_sort_desc': string eg '', f'',  unicode
        }
    For Base64, you can convert a whole folder using https://github.com/PySimpleGUI/PySimpleGUI-Base64-Encoder # fmt: skip
    Remember to us b'' around the string.

    Args:
        tp_dict: (optional) A dict formatted as above to create the ThemePack from.
            If one is not supplied, a default ThemePack will be generated.  Any keys
            not present in the supplied tp_dict will be generated from the default
            values.  Additionally, tp_dict may contain additional keys not specified
            in the minimal default ThemePack.

    Returns:
        None
    """  # noqa: E501
    # For default use cases, load the default directly to avoid the overhead
    # of __getattr__() going through 2 key reads
    self.tp_dict = tp_dict or ThemePack.default

LanguagePack(lp_dict=None)

LanguagePacks are user-definable collections of strings that allow for localization of strings and messages presented to the end user.

Creating your own is easy as well! In fact, a LanguagePack can be as simple as one line if you just want to change one aspect of the default LanguagePack. Example: # I want the save popup to display this text in English in all caps lp_en = {'save_success': 'SAVED!'}

Source code in pysimplesql\pysimplesql.py
8378
8379
8380
def __init__(self, lp_dict=None) -> None:
    """Initialize the `LanguagePack` class."""
    self.lp_dict = lp_dict or type(self).default

default: Dict[Any] = {'button_cancel': ' Cancel ', 'button_ok': ' OK ', 'button_yes': ' Yes ', 'button_no': ' No ', 'description_column_str_null_default': 'New Record', 'notnull_placeholder': '*Required', 'search_placeholder': '🔍 Search...', 'combo_placeholder': 'Please select one:', 'duplicate_prepend': 'Copy of ', 'startup_form': 'Creating Form', 'startup_init': 'Initializing', 'startup_datasets': 'Adding datasets', 'startup_relationships': 'Adding relationships', 'startup_binding': 'Binding window to Form', 'SQLDriver_init': '{name} connection', 'SQLDriver_connecting': 'Connecting to database', 'SQLDriver_execute': 'Executing SQL commands', 'SQLDriver_file_not_found_title': 'Trouble finding db file', 'SQLDriver_file_not_found': 'Could not find file\n{file}', 'animate_phrases': ['Please wait...', 'Still working...'], 'info_popup_title': 'Info', 'form_save_success': 'Updates saved successfully.', 'form_save_none': 'There were no updates to save.', 'dataset_save_empty': 'There were no updates to save.', 'dataset_save_none': 'There were no changes to save!', 'dataset_save_success': 'Updates saved successfully.', 'form_prompt_save_title': 'Unsaved Changes', 'form_prompt_save': 'You have unsaved changes!\nWould you like to save them first?', 'dataset_prompt_save_title': 'Unsaved Changes', 'dataset_prompt_save': 'You have unsaved changes!\nWould you like to save them first?', 'form_save_problem_title': 'Problem Saving', 'form_save_partial': 'Some updates were saved successfully;', 'form_save_problem': 'There was a problem saving updates to the following tables:\n{tables}.', 'dataset_save_callback_false_title': 'Callback Prevented Save', 'dataset_save_callback_false': 'Updates not saved.', 'dataset_save_keyed_fail_title': 'Problem Saving', 'dataset_save_keyed_fail': 'Query failed: {exception}.', 'dataset_save_fail_title': 'Problem Saving', 'dataset_save_fail': 'Query failed: {exception}.', 'dataset_search_failed_title': 'Search Failed', 'dataset_search_failed': 'Failed to find:\n{search_string}', 'delete_title': 'Confirm Deletion', 'delete_cascade': 'Are you sure you want to delete this record?\nKeep in mind that child records:\n({children})\nwill also be deleted!', 'delete_single': 'Are you sure you want to delete this record?', 'delete_failed_title': 'Problem Deleting', 'delete_failed': 'Query failed: {exception}.', 'delete_recursion_limit_error': 'Delete Cascade reached max recursion limit.\nDELETE_CASCADE_RECURSION_LIMIT', 'duplicate_child_title': 'Confirm Duplication', 'duplicate_child': 'This record has child records:\n(in {children})\nWhich records would you like to duplicate?', 'duplicate_child_button_dupparent': 'Only duplicate this record.', 'duplicate_child_button_dupboth': '++ Duplicate this record and its children.', 'duplicate_single_title': 'Confirm Duplication', 'duplicate_single': 'Are you sure you want to duplicate this record?', 'duplicate_failed_title': 'Problem Duplicating', 'duplicate_failed': 'Query failed: {exception}.', 'error_title': 'Error', 'quick_edit_title': 'Quick Edit - {data_key}', 'import_module_failed_title': 'Problem importing module', 'import_module_failed': 'Unable to import module neccessary for {name}\nException: {exception}\n\nTry `pip install {requires}`', 'overwrite_title': 'Overwrite file?', 'overwrite': 'File exists, type YES to overwrite', 'overwrite_prompt': 'YES', 'dataset_save_validate_error_title': 'Error: Invalid Input(s)', 'dataset_save_validate_error_header': 'The following fields(s) have issues:\n', 'dataset_save_validate_error_field': '{field}: ', ValidateRule.REQUIRED: 'Field is required', ValidateRule.PYTHON_TYPE: '{value} could not be cast to correct type, {rule}', ValidateRule.PRECISION: '{value} exceeds max precision length, {rule}', ValidateRule.MIN_VALUE: '{value} less than minimum value, {rule}', ValidateRule.MAX_VALUE: '{value} more than max value, {rule}', ValidateRule.MIN_LENGTH: '{value} less than minimum length, {rule}', ValidateRule.MAX_LENGTH: '{value} more than max length, {rule}', ValidateRule.CUSTOM: '{value}{rule}'} class-attribute

Default LanguagePack.

__call__(lp_dict=None)

Update the LanguagePack instance.

Source code in pysimplesql\pysimplesql.py
8406
8407
8408
8409
8410
def __call__(self, lp_dict=None) -> None:
    """Update the LanguagePack instance."""
    # For default use cases, load the default directly to avoid the overhead
    # of __getattr__() going through 2 key reads
    self.lp_dict = lp_dict or type(self).default

LangFormat

Bases: dict

This is a convenience class used by LanguagePack format_map calls, allowing users to not include expected variables.

Note: This is typically not used by the end user.

Abstractions

Supporting multiple databases in your application can quickly become very complicated and unmanageable. pysimplesql abstracts all of this complexity and presents a unified API via abstracting the main concepts of database programming. See the following documentation for a better understanding of how this is accomplished. Column, ColumnInfo, SQLDriver, Sqlite, Mysql, Postgres.

Note: This is a dummy class that exists purely to enhance documentation and has no use to the end user.

Column dataclass

Base [ColumnClass][pysimplesql.pysimplesql.ColumnClass] represents a SQL column and helps casting/validating values.

The Column class is a generic column class. It holds a dict containing the column name, type whether the column is notnull, whether the column is a primary key and the default value, if any. Columns are typically stored in a ColumnInfo collection. There are multiple ways to get information from a Column, including subscript notation, and via properties. The available column info via these methods are name, domain, notnull, default and pk See example:

.. literalinclude:: ../doc_examples/Column.1.py :language: python :caption: Example code

cast(value)

Cast a value to the appropriate data type as defined by the column info for the column. This can be useful for comparing values between the database and the GUI.

:param value: The value you would like to cast :returns: The value, cast to a type as defined by the domain

Source code in pysimplesql\pysimplesql.py
8495
8496
8497
8498
8499
8500
8501
8502
8503
8504
8505
8506
8507
8508
def cast(self, value: Any) -> Any:
    """Cast a value to the appropriate data type as defined by the column info for
    the column. This can be useful for comparing values between the database and the
    GUI.

    :param value: The value you would like to cast
    :returns: The value, cast to a type as defined by the domain
    """
    if self.custom_cast_fn:
        try:
            return self.custom_cast_fn(value)
        except Exception as e:  # noqa: BLE001
            logger.debug(f"Error running custom_cast_fn, {e}")
    return str(value)

validate(value)

TODO.

Source code in pysimplesql\pysimplesql.py
8510
8511
8512
8513
8514
8515
8516
8517
8518
8519
8520
8521
8522
8523
8524
8525
8526
8527
8528
8529
8530
8531
8532
8533
def validate(self, value: Any) -> bool:
    """TODO."""
    value = self.cast(value)

    if self.notnull and value in EMPTY:
        return ValidateResponse(ValidateRule.REQUIRED, value, self.notnull)

    if value in EMPTY:
        return ValidateResponse()

    if self.custom_validate_fn:
        try:
            response = self.custom_validate_fn(value)
            if response.exception:
                return response
        except Exception as e:  # noqa: BLE001
            logger.debug(f"Error running custom_validate_fn, {e}")

    if not isinstance(value, self.python_type):
        return ValidateResponse(
            ValidateRule.PYTHON_TYPE, value, self.python_type.__name__
        )

    return ValidateResponse()

MinMaxCol dataclass

Bases: Column

Base ColumnClass for columns with minimum and maximum constraints.

This class extends the functionality of the base Column class to include optional validation based on minimum and maximum values.

Parameters:

Name Type Description Default
min_value Any valid value type compatible with the column's data type.

The minimum allowed value for the column (inclusive). Defaults to None, indicating no minimum constraint.

None
max_value Any valid value type compatible with the column's data type.

The maximum allowed value for the column (inclusive). Defaults to None, indicating no maximum constraint.

None

LengthCol dataclass

Bases: Column

Base ColumnClass for length-constrained columns.

This class represents a column with length constraints. It inherits from the base Column class and adds attributes to store the maximum and minimum length values. The [validate][pysimplesql.pysimplesql.validate] method is overridden to include length validations.

Parameters:

Name Type Description Default
max_length int

Maximum length allowed for the column value.

None
min_length int

Minimum length allowed for the column value.

None

LocaleCol dataclass

Bases: Column

Base ColumnClass that provides Locale functions.

Parameters:

Name Type Description Default
negative_symbol str

The symbol representing negative values in the locale.

localeconv()['negative_sign']
currency_symbol str

The symbol representing the currency in the locale.

localeconv()['currency_symbol']
Example

col = LocaleCol() normalized_value = col.strip_locale("$1,000.50")

ColumnInfo(driver, table)

Bases: List

Custom container that behaves like a List containing a collection of [Columns][pysimplesql.pysimplesql.Columns].

This class is responsible for maintaining information about all the columns (Column) in a table. While the individual Column elements of this collection contain information such as default values, primary key status, SQL data type, column name, and the notnull status - this class ties them all together into a collection and adds functionality to set default values for null columns and retrieve a dict representing a table row with all defaults already assigned. See example below: .. literalinclude:: ../doc_examples/ColumnInfo.1.py :language: python :caption: Example code

Source code in pysimplesql\pysimplesql.py
8876
8877
8878
8879
8880
8881
8882
8883
8884
8885
8886
8887
8888
8889
8890
8891
8892
8893
8894
def __init__(self, driver: SQLDriver, table: str) -> None:
    """Initilize a ColumnInfo instance."""
    self.driver = driver
    self.table = table

    # Defaults to use for Null values returned from the database. These can be
    # overwritten by the user and support function calls as well by using
    # ColumnInfo.set_null_default() and ColumnInfo.set_null_defaults()
    self.null_defaults = {
        "str": lang.description_column_str_null_default,
        "int": 0,
        "float": 0.0,
        "Decimal": Decimal(0),
        "bool": 0,
        "time": lambda: dt.datetime.now().strftime(TIME_FORMAT),
        "date": lambda: dt.date.today().strftime(DATE_FORMAT),
        "datetime": lambda: dt.datetime.now().strftime(DATETIME_FORMAT),
    }
    super().__init__()

pk_column: Union[str, None] property

Get the pk_column for this colection of column_info.

Returns:

Type Description
Union[str, None]

A string containing the column name of the PK column, or None if one was not

Union[str, None]

found

names: List[str] property

Return a List of column names from the Columns in this collection.

Returns:

Type Description
List[str]

List of column names

col_name(idx)

Get the column name located at the specified index in this collection of Columns.

Parameters:

Name Type Description Default
idx int

The index of the column to get the name from

required

Returns:

Type Description
str

The name of the column at the specified index

Source code in pysimplesql\pysimplesql.py
8928
8929
8930
8931
8932
8933
8934
8935
8936
8937
8938
def col_name(self, idx: int) -> str:
    """Get the column name located at the specified index in this collection of
    `Column`s.

    Args:
        idx: The index of the column to get the name from

    Returns:
        The name of the column at the specified index
    """
    return self[idx].name

default_row_dict(dataset)

Return a dictionary of a table row with all defaults assigned.

This is useful for inserting new records to prefill the GUI elements.

Parameters:

Name Type Description Default
dataset DataSet

a pysimplesql DataSet object

required

Returns:

Type Description
dict

dict

Source code in pysimplesql\pysimplesql.py
8940
8941
8942
8943
8944
8945
8946
8947
8948
8949
8950
8951
8952
8953
8954
8955
8956
8957
8958
8959
8960
8961
8962
8963
8964
8965
8966
8967
8968
8969
8970
8971
8972
8973
8974
8975
8976
8977
8978
8979
8980
8981
8982
8983
8984
8985
8986
8987
8988
8989
8990
8991
8992
8993
8994
8995
8996
8997
8998
8999
9000
9001
9002
9003
9004
9005
9006
9007
9008
9009
9010
9011
9012
9013
9014
9015
9016
9017
9018
9019
9020
9021
9022
9023
9024
9025
9026
9027
9028
def default_row_dict(self, dataset: DataSet) -> dict:
    """Return a dictionary of a table row with all defaults assigned.

    This is useful for inserting new records to prefill the GUI elements.

    Args:
        dataset: a pysimplesql DataSet object

    Returns:
        dict
    """
    d = {}
    for c in self:
        default = c.default
        python_type = c.python_type.__name__

        # First, check to see if the default might be a database function
        if self._looks_like_function(default):
            table = self.driver.quote_table(self.table)

            q = f"SELECT {default} AS val FROM {table};"

            rows = self.driver.execute(q)
            if rows.attrs["exception"] is None:
                try:
                    default = rows.iloc[0]["val"]
                except IndexError:
                    try:
                        default = rows.iloc[0]["VAL"]
                    except IndexError:
                        default = None
                if default is not None:
                    d[c.name] = default
                    continue
            logger.warning(
                "There was an exception getting the default: "
                f"{rows.attrs['exception']}"
            )

        # The stored default is a literal value, lets try to use it:
        if default in [None, "None"]:
            try:
                null_default = self.null_defaults[python_type]
            except KeyError:
                # Perhaps our default dict does not yet support this datatype
                null_default = None

            # return PK_PLACEHOLDER if this is a fk_relationships.
            # trick used in Combo for the pk to display placeholder
            rels = self.driver.relationships.get_rels_for(dataset.table)
            rel = next((r for r in rels if r.fk_column == c.name), None)
            if rel:
                null_default = PK_PLACEHOLDER

            # skip primary keys
            if not c.pk:
                # put default in description_column
                if c.name == dataset.description_column:
                    default = null_default

                # put defaults in other fields

                #  don't put txt in other txt fields
                elif c.python_type != str:
                    # If our default is callable, call it.
                    if callable(null_default):
                        default = null_default()
                    # Otherwise, assign it
                    else:
                        default = null_default
                # string-like, not description_column
                else:
                    default = ""
        else:
            # Load the default that was fetched from the database
            # during ColumnInfo creation
            if c.python_type == str:
                # strip quotes from default strings as they seem to get passed with
                # some database-stored defaults
                # strip leading and trailing quotes
                default = c.default.strip("\"'")

        d[c.name] = default
        logger.debug(
            f"Default fetched from database function. Default value is: {default}"
        )
    if dataset.transform is not None:
        dataset.transform(dataset, d, TFORM_DECODE)
    return d

set_null_default(python_type, value)

Set a Null default for a single python type.

Parameters:

Name Type Description Default
python_type str

This should be equal to what calling .__name__ on the Column [python_type][pysimplesql.pysimplesql.python_type] would equal: 'str', 'int', 'float', 'Decimal', 'bool', 'time', 'date', or 'datetime'.

required
value object

The new value to set the SQL type to. This can be a literal or even a callable

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
9030
9031
9032
9033
9034
9035
9036
9037
9038
9039
9040
9041
9042
9043
9044
9045
9046
9047
9048
9049
def set_null_default(self, python_type: str, value: object) -> None:
    """Set a Null default for a single python type.

    Args:
        python_type: This should be equal to what calling `.__name__` on the Column
            `python_type` would equal: 'str', 'int', 'float', 'Decimal', 'bool',
            'time', 'date', or 'datetime'.
        value: The new value to set the SQL type to. This can be a literal or even a
            callable

    Returns:
        None
    """
    if python_type not in self._python_types:
        RuntimeError(
            f"Unsupported SQL Type: {python_type}. Supported types are: "
            f"{self._python_types}"
        )

    self.null_defaults[python_type] = value

set_null_defaults(null_defaults)

Set Null defaults for all python types.

'str', 'int', 'float', 'Decimal', 'bool',

'time', 'date', or 'datetime'.

Parameters:

Name Type Description Default
null_defaults dict

A dict of python types and default values. This can be a literal or even a callable

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
9051
9052
9053
9054
9055
9056
9057
9058
9059
9060
9061
9062
9063
9064
9065
9066
9067
9068
9069
9070
9071
def set_null_defaults(self, null_defaults: dict) -> None:
    """Set Null defaults for all python types.

    Supported types: 'str', 'int', 'float', 'Decimal', 'bool',
        'time', 'date', or 'datetime'.

    Args:
        null_defaults: A dict of python types and default values. This can be a
            literal or even a callable

    Returns:
        None
    """
    # Check if the null_defaults dict has all the required keys:
    if not all(key in null_defaults for key in self._python_types):
        RuntimeError(
            f"The supplied null_defaults dictionary does not havle all required SQL"
            f" types. Required: {self._python_types}"
        )

    self.null_defaults = null_defaults

get_virtual_names()

Get a list of virtual column names.

Returns:

Type Description
List[str]

A List of column names that are virtual, or [] if none are present in this

List[str]

collections

Source code in pysimplesql\pysimplesql.py
9073
9074
9075
9076
9077
9078
9079
9080
def get_virtual_names(self) -> List[str]:
    """Get a list of virtual column names.

    Returns:
        A List of column names that are virtual, or [] if none are present in this
        collections
    """
    return [c.name for c in self if c.virtual]

Result

This is a "dummy" Result object that is a convenience for constructing a DataFrame that has the expected attrs set.

set(row_data=None, lastrowid=None, exception=None, column_info=None) classmethod

Create a pandas DataFrame with the row data and expected attrs set.

Parameters:

Name Type Description Default
row_data dict

A list of dicts of row data

None
lastrowid int

The inserted row ID from the last INSERT statement

None
exception Exception

Exceptions passed back from the SQLDriver

None
column_info ColumnInfo

(optional) ColumnInfo object

None
Source code in pysimplesql\pysimplesql.py
9119
9120
9121
9122
9123
9124
9125
9126
9127
9128
9129
9130
9131
9132
9133
9134
9135
9136
9137
9138
9139
9140
9141
9142
9143
@classmethod
def set(
    cls,
    row_data: dict = None,
    lastrowid: int = None,
    exception: Exception = None,
    column_info: ColumnInfo = None,
):
    """Create a pandas DataFrame with the row data and expected attrs set.

    Args:
        row_data: A list of dicts of row data
        lastrowid: The inserted row ID from the last INSERT statement
        exception: Exceptions passed back from the SQLDriver
        column_info: (optional) ColumnInfo object
    """
    rows = pd.DataFrame(row_data)
    rows.attrs["lastrowid"] = lastrowid
    rows.attrs["exception"] = exception
    rows.attrs["column_info"] = column_info
    rows.attrs["row_backup"] = None
    rows.attrs["virtual"] = []
    rows.attrs["sort_column"] = None
    rows.attrs["sort_reverse"] = None
    return rows

SqlChar dataclass

Container for passing database-specific characters.

Each database type expects their SQL prepared in a certain way. Defaults in this dataclass are set for how various elements in the SQL string should be quoted and represented as placeholders. Override these in the derived class as needed to satisfy SQL requirements

placeholder: str = '%s' class-attribute instance-attribute

The placeholder for values in the query string. This is typically '?' or'%s'

table_quote: str = '' class-attribute instance-attribute

Character to quote table. (defaults to no quotes)

column_quote: str = '' class-attribute instance-attribute

Chacter to quote column. (defaults to no quotes)

value_quote: str = "'" class-attribute instance-attribute

Character to quote value. (defaults to single quotes)

SQLDriver dataclass

Bases: ABC

Abstract SQLDriver class. Derive from this class to create drivers that conform to PySimpleSQL. This ensures that the same code will work the same way regardless of which database is used. There are a few important things to note: The commented code below is broken into methods that MUST be implemented in the derived class, methods that.

SHOULD be implemented in the derived class, and methods that MAY need to be implemented in the derived class for it to work as expected. Most derived drivers will at least partially work by implementing the MUST have methods.

See the source code for Sqlite, Mysql and Postgres for examples of how to construct your own driver.

NOTE: SQLDriver.execute() should return a pandas DataFrame. Additionally, by pysimplesql convention, the attrs["lastrowid"] should always be None unless and INSERT query is executed with SQLDriver.execute() or a record is inserted with SQLDriver.insert_record()

Parameters:

Name Type Description Default
host str

Host.

None
user str

User.

None
password str

Password.

None
database str

Name of database.

None
sql_script str

(optional) SQL script file to execute after opening the database.

None
sql_script_encoding str

The encoding of the SQL script file. Defaults to 'utf-8'.

'utf-8'
sql_commands str

(optional) SQL commands to execute after opening the database. Note: sql_commands are executed after sql_script.

None
update_cascade bool

(optional) Default:True. Requery and filter child table on selected parent primary key. (ON UPDATE CASCADE in SQL)

True
delete_cascade bool

(optional) Default:True. Delete the dependent child records if the parent table record is deleted. (ON UPDATE DELETE in SQL)

True
sql_char InitVar[SqlChar]

(optional) SqlChar object, if non-default chars desired.

SqlChar()

connect(*args, **kwargs) abstractmethod

Connect to a database.

Connect to a database in the connect() method, assigning the connection to self.con.

Implementation varies by database, you may need only one parameter, or several depending on how a connection is established with the target database.

Source code in pysimplesql\pysimplesql.py
9271
9272
9273
9274
9275
9276
9277
9278
9279
9280
@abstractmethod
def connect(self, *args, **kwargs):
    """Connect to a database.

    Connect to a database in the connect() method, assigning the connection to
    self.con.

    Implementation varies by database, you may need only one parameter, or several
    depending on how a connection is established with the target database.
    """

execute(query, values=None, column_info=None, auto_commit_rollback=False) abstractmethod

Execute a query.

Implements the native SQL implementation's execute() command.

Parameters:

Name Type Description Default
query

The query string to execute

required
values

Values to pass into the query to replace the placeholders

None
column_info ColumnInfo

An optional ColumnInfo object

None
auto_commit_rollback bool

Automatically commit or rollback depending on whether an exception was handled. Set to False by default. Set to True to have exceptions and commit/rollbacks happen automatically

False
Source code in pysimplesql\pysimplesql.py
9282
9283
9284
9285
9286
9287
9288
9289
9290
9291
9292
9293
9294
9295
9296
9297
9298
9299
9300
9301
@abstractmethod
def execute(
    self,
    query,
    values=None,
    column_info: ColumnInfo = None,
    auto_commit_rollback: bool = False,
):
    """Execute a query.

    Implements the native SQL implementation's execute() command.

    Args:
        query: The query string to execute
        values: Values to pass into the query to replace the placeholders
        column_info: An optional ColumnInfo object
        auto_commit_rollback: Automatically commit or rollback depending on whether
            an exception was handled. Set to False by default. Set to True to have
            exceptions and commit/rollbacks happen automatically
    """

check_keyword(keyword, key=None)

Check keyword to see if it is a reserved word. If it is raise a ReservedKeywordError. Checks to see if the database name is in keys and uses the database name for the key if it exists, otherwise defaults to 'common' in the RESERVED set. Override this with the specific key for the database if needed for best results.

Parameters:

Name Type Description Default
keyword str

the value to check against reserved words

required
key str

The key in the RESERVED set to check in

None

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
9338
9339
9340
9341
9342
9343
9344
9345
9346
9347
9348
9349
9350
9351
9352
9353
9354
9355
9356
9357
9358
9359
9360
9361
9362
9363
def check_keyword(self, keyword: str, key: str = None) -> None:
    """Check keyword to see if it is a reserved word.  If it is raise a
    ReservedKeywordError. Checks to see if the database name is in keys and uses the
    database name for the key if it exists, otherwise defaults to 'common' in the
    RESERVED set. Override this with the specific key for the database if needed for
    best results.

    Args:
        keyword: the value to check against reserved words
        key: The key in the RESERVED set to check in

    Returns:
        None
    """
    if not self.check_reserved_keywords:
        return

    if key is None:
        # First try using the name of the driver
        key = self.NAME.lower() if self.NAME.lower() in RESERVED else "common"

    if keyword.upper() in RESERVED[key] or keyword.upper in RESERVED["common"]:
        raise ReservedKeywordError(
            f"`{keyword}` is a reserved keyword and cannot be used for table or "
            f"column names."
        )

commit()

Commit a transaction.

Source code in pysimplesql\pysimplesql.py
9387
9388
9389
def commit(self) -> None:
    """Commit a transaction."""
    self.con.commit()

generate_join_clause(dataset)

Automatically generates a join clause from the Relationships that have been set.

This typically isn't used by end users.

Returns:

Name Type Description
str str

A join string to be used in a sqlite3 query

Source code in pysimplesql\pysimplesql.py
9425
9426
9427
9428
9429
9430
9431
9432
9433
9434
9435
9436
9437
9438
def generate_join_clause(self, dataset: DataSet) -> str:
    """Automatically generates a join clause from the Relationships that have been
    set.

    This typically isn't used by end users.

    Returns:
        str: A join string to be used in a sqlite3 query
    """
    join = ""
    for r in self.relationships:
        if dataset.table == r.child_table:
            join += f" {self.relationship_to_join_clause(r)}"
    return join if not dataset.join_clause else dataset.join_clause

generate_where_clause(dataset)

Generates a where clause from the Relationships that have been set, as well as the DataSet's where clause.

This is not typically used by end users.

Returns:

Name Type Description
str str

A where clause string to be used in a sqlite3 query

Source code in pysimplesql\pysimplesql.py
9440
9441
9442
9443
9444
9445
9446
9447
9448
9449
9450
9451
9452
9453
9454
9455
9456
9457
9458
9459
9460
9461
9462
9463
9464
9465
9466
9467
9468
9469
9470
9471
9472
def generate_where_clause(self, dataset: DataSet) -> str:
    """Generates a where clause from the Relationships that have been set, as well
    as the DataSet's where clause.

    This is not typically used by end users.

    Returns:
        str: A where clause string to be used in a sqlite3 query
    """
    where = ""
    for r in self.relationships:
        if dataset.table == r.child_table and r.on_update_cascade:
            table = dataset.table
            parent_pk = dataset.frm[r.parent_table].current.pk

            # Children without cascade-filtering parent aren't displayed
            if not parent_pk:
                parent_pk = PK_PLACEHOLDER

            clause = f" WHERE {table}.{r.fk_column}={parent_pk!s}"
            if where:
                clause = clause.replace("WHERE", "AND")
            where += clause

    if not where:
        # There was no where clause from Relationships..
        where = dataset.where_clause
    else:
        # There was an auto-generated portion of the where clause.
        # We will add the table's where clause to it
        where = where + " " + dataset.where_clause.replace("WHERE", "AND")

    return where

generate_query(dataset, join_clause=True, where_clause=True, order_clause=True) staticmethod

Generate a query string using the relationships that have been set.

Parameters:

Name Type Description Default
dataset DataSet

A DataSet object

required
join_clause bool

True to auto-generate 'join' clause, False to not

True
where_clause bool

True to auto-generate 'where' clause, False to not

True
order_clause bool

True to auto-generate 'order by' clause, False to not

True

Returns:

Type Description
str

a query string for use with sqlite3

Source code in pysimplesql\pysimplesql.py
9474
9475
9476
9477
9478
9479
9480
9481
9482
9483
9484
9485
9486
9487
9488
9489
9490
9491
9492
9493
9494
9495
9496
9497
@staticmethod
def generate_query(
    dataset: DataSet,
    join_clause: bool = True,
    where_clause: bool = True,
    order_clause: bool = True,
) -> str:
    """Generate a query string using the relationships that have been set.

    Args:
        dataset: A `DataSet` object
        join_clause: True to auto-generate 'join' clause, False to not
        where_clause: True to auto-generate 'where' clause, False to not
        order_clause: True to auto-generate 'order by' clause, False to not

    Returns:
        a query string for use with sqlite3
    """
    return (
        f"{dataset.query}"
        f' {dataset.join_clause if join_clause else ""}'
        f' {dataset.where_clause if where_clause else ""}'
        f' {dataset.order_clause if order_clause else ""}'
    )

duplicate_record(dataset, children)

Duplicates a record in a database table and optionally duplicates its dependent records.

The function uses all columns found in [column_info][pysimplesql.pysimplesql.DataSet.column_info] and select all except the primary key column, inserting a duplicate record with the same column values.

If the 'children' parameter is set to 'True', the function duplicates the dependent records by setting the foreign key column of the child records to the primary key value of the newly duplicated record before inserting them.

Note that this function assumes the primary key column is auto-incrementing and that no columns are set to unique.

Parameters:

Name Type Description Default
dataset DataSet

The DataSet of the the record to be duplicated.

required
children bool

(optional) Whether to duplicate dependent records. Defaults to False.

required
Source code in pysimplesql\pysimplesql.py
9576
9577
9578
9579
9580
9581
9582
9583
9584
9585
9586
9587
9588
9589
9590
9591
9592
9593
9594
9595
9596
9597
9598
9599
9600
9601
9602
9603
9604
9605
9606
9607
9608
9609
9610
9611
9612
9613
9614
9615
9616
9617
9618
9619
9620
9621
9622
9623
9624
9625
9626
9627
9628
9629
9630
9631
9632
9633
9634
9635
9636
9637
9638
9639
9640
9641
9642
9643
9644
9645
9646
9647
9648
9649
9650
9651
9652
9653
9654
9655
9656
9657
9658
9659
9660
9661
9662
9663
9664
9665
9666
9667
9668
9669
9670
9671
9672
9673
9674
9675
9676
9677
9678
def duplicate_record(self, dataset: DataSet, children: bool) -> pd.DataFrame:
    """Duplicates a record in a database table and optionally duplicates its
    dependent records.

    The function uses all columns found in `DataSet.column_info` and
    select all except the primary key column, inserting a duplicate record with the
    same column values.

    If the 'children' parameter is set to 'True', the function duplicates the
    dependent records by setting the foreign key column of the child records to the
    primary key value of the newly duplicated record before inserting them.

    Note that this function assumes the primary key column is auto-incrementing and
    that no columns are set to unique.

    Args:
        dataset: The `DataSet` of the the record to be duplicated.
        children: (optional) Whether to duplicate dependent records. Defaults to
            False.
    """
    # Get variables
    table = self.quote_table(dataset.table)
    columns = [
        self.quote_column(column.name)
        for column in dataset.column_info
        if column.name != dataset.pk_column and not column.generated
    ]
    columns = ", ".join(columns)
    pk_column = dataset.pk_column
    pk = dataset.current.pk

    # Insert new record
    res = self._insert_duplicate_record(table, columns, pk_column, pk)

    if res.attrs["exception"]:
        return res

    # Get pk of new record
    new_pk = res.attrs["lastrowid"]
    # now wrap pk_column
    pk_column = self.quote_column(dataset.pk_column)

    # Set description if TEXT
    if dataset.column_info[dataset.description_column].python_type == str:
        description_column = self.quote_column(dataset.description_column)
        description = (
            f"{lang.duplicate_prepend}{dataset.get_description_for_pk(pk)}"
        )
        query = (
            f"UPDATE {table} "
            f"SET {description_column} = {self.placeholder} "
            f"WHERE {pk_column} = {new_pk};"
        )
        res = self.execute(query, [description])
        if res.attrs["exception"]:
            return res

    # create list of which children we have duplicated
    child_duplicated = []
    # Next, duplicate the child records!
    if children:
        for _ in dataset.frm.datasets:
            for r in self.relationships:
                if (
                    r.parent_table == dataset.table
                    and r.on_update_cascade
                    and (r.child_table not in child_duplicated)
                ):
                    child = self.quote_table(r.child_table)
                    fk_column = self.quote_column(r.fk_column)

                    # all columns except pk_column
                    columns = [
                        self.quote_column(column.name)
                        for column in dataset.frm[r.child_table].column_info
                        if column.name != dataset.frm[r.child_table].pk_column
                        and not column.generated
                    ]

                    # replace fk_column value with pk of new parent
                    select_columns = [
                        str(new_pk)
                        if column == self.quote_column(r.fk_column)
                        else column
                        for column in columns
                    ]

                    # prepare query & execute
                    columns = ", ".join(columns)
                    select_columns = ", ".join(select_columns)
                    query = (
                        f"INSERT INTO {child} ({columns}) "
                        f"SELECT {select_columns} FROM {child} "
                        f"WHERE {fk_column} = {pk};"
                    )
                    res = self.execute(query)
                    if res.attrs["exception"]:
                        return res

                    child_duplicated.append(r.child_table)
    # If we made it here, we can return the pk.
    # Since the pk was stored earlier, we will just send an empty dataframe.
    return Result.set(lastrowid=new_pk)

add_relationship(join, child_table, fk_column, parent_table, pk_column, update_cascade, delete_cascade)

Add a foreign key relationship between two dataset of the database.

When you attach a database, PySimpleSQL isn't aware of the relationships contained until datasets are added via add_dataset, and the relationship of various tables is set with this function. Note that auto_add_relationships() will do this automatically from the schema of the database, which also happens automatically when a SQLDriver is created.

Parameters:

Name Type Description Default
join str

The join type of the relationship ('LEFT JOIN', 'INNER JOIN', 'RIGHT JOIN')

required
child_table str

The child table containing the foreign key

required
fk_column str

The foreign key column of the child table

required
parent_table str

The parent table containing the primary key

required
pk_column str

The primary key column of the parent table

required
update_cascade bool

Requery and filter child table results on selected parent primary key (ON UPDATE CASCADE in SQL)

required
delete_cascade bool

Delete the dependent child records if the parent table record is deleted (ON UPDATE DELETE in SQL)

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
9761
9762
9763
9764
9765
9766
9767
9768
9769
9770
9771
9772
9773
9774
9775
9776
9777
9778
9779
9780
9781
9782
9783
9784
9785
9786
9787
9788
9789
9790
9791
9792
9793
9794
9795
9796
9797
9798
9799
9800
9801
9802
9803
9804
9805
def add_relationship(
    self,
    join: str,
    child_table: str,
    fk_column: str,
    parent_table: str,
    pk_column: str,
    update_cascade: bool,
    delete_cascade: bool,
) -> None:
    """Add a foreign key relationship between two dataset of the database.

    When you attach a database, PySimpleSQL isn't aware of the relationships
    contained until datasets are added via `Form.add_dataset`, and the relationship
    of various tables is set with this function. Note that
    `SQLDriver.auto_add_relationships()` will do this automatically from the schema
    of the database, which also happens automatically when a `SQLDriver` is created.

    Args:
        join: The join type of the relationship ('LEFT JOIN', 'INNER JOIN', 'RIGHT
            JOIN')
        child_table: The child table containing the foreign key
        fk_column: The foreign key column of the child table
        parent_table: The parent table containing the primary key
        pk_column: The primary key column of the parent table
        update_cascade: Requery and filter child table results on selected parent
            primary key (ON UPDATE CASCADE in SQL)
        delete_cascade: Delete the dependent child records if the parent table
            record is deleted (ON UPDATE DELETE in SQL)

    Returns:
        None
    """
    self.relationships.append(
        Relationship(
            join,
            child_table,
            fk_column,
            parent_table,
            pk_column,
            update_cascade,
            delete_cascade,
            self,
        )
    )

auto_add_relationships()

Automatically add a foreign key relationship between tables of the database. This is done by foreign key constraints within the database. Automatically requery the child table if the parent table changes (ON UPDATE CASCADE in sql is set) When you attach a database, PySimpleSQL isn't aware of the relationships contained until tables are added and the relationship of various tables is set. This happens automatically during SQLDriver creation. Note that add_relationship() can do this manually.

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
9809
9810
9811
9812
9813
9814
9815
9816
9817
9818
9819
9820
9821
9822
9823
9824
9825
9826
9827
9828
9829
9830
9831
9832
9833
9834
9835
9836
9837
9838
9839
9840
def auto_add_relationships(self) -> None:
    """Automatically add a foreign key relationship between tables of the database.
    This is done by foreign key constraints within the database.  Automatically
    requery the child table if the parent table changes (ON UPDATE CASCADE in sql is
    set) When you attach a database, PySimpleSQL isn't aware of the relationships
    contained until tables are added and the relationship of various tables is set.
    This happens automatically during `SQLDriver` creation. Note that
    `SQLDriver.add_relationship()` can do this manually.

    Returns:
        None
    """
    logger.info("Automatically adding foreign key relationships")
    # Clear any current rels so that successive calls will not double the entries
    self.relationships = RelationshipStore(
        self
    )  # clear any relationships already stored
    relationships = self.get_relationships()
    for r in relationships:
        logger.debug(
            f'Adding relationship {r["from_table"]}.{r["from_column"]} = '
            f'{r["to_table"]}.{r["to_column"]}'
        )
        self.add_relationship(
            "LEFT JOIN",
            r["from_table"],
            r["from_column"],
            r["to_table"],
            r["to_column"],
            r["update_cascade"],
            r["delete_cascade"],
        )

check_reserved_keywords(value)

SQLDrivers can check to make sure that field names respect their own reserved keywords. By default, all SQLDrivers will check for their respective keywords. You can choose to disable this feature with this method.

Parameters:

Name Type Description Default
value bool

True to check for reserved keywords in field names, false to skip this check

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
9842
9843
9844
9845
9846
9847
9848
9849
9850
9851
9852
9853
9854
def check_reserved_keywords(self, value: bool) -> None:
    """SQLDrivers can check to make sure that field names respect their own reserved
    keywords.  By default, all SQLDrivers will check for their respective keywords.
    You can choose to disable this feature with this method.

    Args:
        value: True to check for reserved keywords in field names, false to skip
            this check

    Returns:
        None
    """
    self._CHECK_RESERVED_KEYWORDS = value

Sqlite(database=None, *, sql_script=None, sql_script_encoding='utf-8', sql_commands=None, update_cascade=True, delete_cascade=True, sql_char=sql_char, create_file=True, skip_sql_if_db_exists=True) dataclass

Bases: SQLDriver

The SQLite driver supports SQLite3 databases.

Parameters:

Name Type Description Default
database Union[str, Path, Literal[':memory:'], Connection]

Path to database file, ':memory:' in-memory database, or existing Sqlite3.Connection

None
sql_script

(optional) SQL script file to execute after opening the db.

None
sql_script_encoding str

(optional) The encoding of the SQL script file. Defaults to 'utf-8'.

'utf-8'
sql_commands

(optional) SQL commands to execute after opening the database. Note: sql_commands are executed after sql_script.

None
update_cascade bool

(optional) Default:True. Requery and filter child table on selected parent primary key. (ON UPDATE CASCADE in SQL)

True
delete_cascade bool

(optional) Default:True. Delete the dependent child records if the parent table record is deleted. (ON UPDATE DELETE in SQL)

True
sql_char SqlChar

(optional) SqlChar object, if non-default chars desired.

sql_char
create_file bool

(optional) default True. Create file if it doesn't exist.

True
skip_sql_if_db_exists bool

(optional) Skip both 'sql_file' and 'sql_commands' if database already exists.

True
Source code in pysimplesql\pysimplesql.py
9914
9915
9916
9917
9918
9919
9920
9921
9922
9923
9924
9925
9926
9927
9928
9929
9930
9931
9932
9933
9934
9935
9936
9937
9938
9939
9940
9941
9942
9943
9944
9945
9946
9947
9948
9949
9950
9951
9952
9953
9954
9955
9956
9957
9958
9959
9960
def __init__(
    self,
    database: Union[
        str,
        Path,
        Literal[":memory:"],
        sqlite3.Connection,
    ] = None,
    *,
    sql_script=None,
    sql_script_encoding: str = "utf-8",
    sql_commands=None,
    update_cascade: bool = True,
    delete_cascade: bool = True,
    sql_char: SqlChar = sql_char,
    create_file: bool = True,
    skip_sql_if_db_exists: bool = True,
) -> None:
    """Initilize a Sqlite instance.

    Args:
        database: Path to database file, ':memory:' in-memory database, or existing
            Sqlite3.Connection
        sql_script: (optional) SQL script file to execute after opening the db.
        sql_script_encoding: (optional) The encoding of the SQL script file.
            Defaults to 'utf-8'.
        sql_commands: (optional) SQL commands to execute after opening the database.
            Note: sql_commands are executed after sql_script.
        update_cascade: (optional) Default:True. Requery and filter child table on
            selected parent primary key. (ON UPDATE CASCADE in SQL)
        delete_cascade: (optional) Default:True. Delete the dependent child records
            if the parent table record is deleted. (ON UPDATE DELETE in SQL)
        sql_char: (optional) `SqlChar` object, if non-default chars desired.
        create_file: (optional) default True. Create file if it doesn't exist.
        skip_sql_if_db_exists: (optional) Skip both 'sql_file' and 'sql_commands' if
            database already exists.
    """
    self._database = str(database)
    self.sql_script = sql_script
    self.sql_script_encoding = sql_script_encoding
    self.sql_commands = sql_commands
    self.update_cascade = update_cascade
    self.delete_cascade = delete_cascade
    self.create_file = create_file
    self.skip_sql_if_db_exists = skip_sql_if_db_exists

    super().__post_init__(sql_char)

Flatfile(file_path, delimiter=',', quotechar='"', header_row_num=0, table=None, pk_col=None) dataclass

Bases: Sqlite

The Flatfile driver adds support for flatfile databases such as CSV files to pysimplesql.

The flatfile data is loaded into an internal SQlite database, where it can be used and manipulated like any other database file. Each timem records are saved, the contents of the internal SQlite database are written back out to the file. This makes working with flatfile data as easy and consistent as any other database.

Parameters:

Name Type Description Default
file_path str

The path to the flatfile

required
delimiter str

The delimiter for the flatfile. Defaults to ','. Tabs ('\t') are another popular option

','
quotechar str

The quoting character specified by the flatfile. Defaults to '"'

'"'
header_row_num int

The row containing the header column names. Defaults to 0

0
table str

The name to give this table in pysimplesql. Default is 'Flatfile'

None
pk_col str

The column name that acts as a primary key for the dataset. See below how to use this parameter: - If no pk_col parameter is supplied, then a generic primary key column named 'pk' will be generated with AUTO INCREMENT and PRIMARY KEY set. This is a virtual column and will not be written back out to the flatfile. - If the pk_col parameter is supplied, and it exists in the header row, then it will be used as the primary key for the dataset. If this column does not exist in the header row, then a virtual primary key column with this name will be created with AUTO INCREMENT and PRIMARY KEY set. As above, the virtual primary key column that was created will not be written to the flatfile.

None
Source code in pysimplesql\pysimplesql.py
10201
10202
10203
10204
10205
10206
10207
10208
10209
10210
10211
10212
10213
10214
10215
10216
10217
10218
10219
10220
10221
10222
10223
10224
10225
10226
10227
10228
10229
10230
10231
10232
10233
10234
10235
10236
10237
10238
10239
10240
10241
10242
10243
10244
10245
10246
def __init__(
    self,
    file_path: str,
    delimiter: str = ",",
    quotechar: str = '"',
    header_row_num: int = 0,
    table: str = None,
    pk_col: str = None,
) -> None:
    r"""Create a new Flatfile driver instance.

    Args:
        file_path: The path to the flatfile
        delimiter: The delimiter for the flatfile. Defaults to ','.  Tabs ('\t') are
            another popular option
        quotechar: The quoting character specified by the flatfile. Defaults to '"'
        header_row_num: The row containing the header column names. Defaults to 0
        table: The name to give this table in pysimplesql. Default is 'Flatfile'
        pk_col: The column name that acts as a primary key for the dataset. See
             below how to use this parameter:
            - If no pk_col parameter is supplied, then a generic primary key column
              named 'pk' will be generated with AUTO INCREMENT and PRIMARY KEY set.
              This is a virtual column and will not be written back out to the
              flatfile.
            - If the pk_col parameter is supplied, and it exists in the header row,
              then it will be used as the primary key for the dataset. If this
              column does not exist in the header row, then a virtual primary key
              column with this name will be created with AUTO INCREMENT and PRIMARY
              KEY set. As above, the virtual primary key column that was created
              will not be written to the flatfile.
    """
    self.file_path = file_path
    self.delimiter = delimiter
    self.quotechar = quotechar
    self.header_row_num = header_row_num
    self.pk_col = pk_col if pk_col is not None else "pk"
    self.pk_col_is_virtual = False
    self.table = table if table is not None else "Flatfile"

    # First up the SQLite driver that we derived from
    super().__init__(":memory:")  # use an in-memory database

    # Change Sqlite SQLDriver init set values to Flatfile-specific
    self.NAME = "Flatfile"
    self.REQUIRES = ["csv,sqlite3"]
    self.placeholder = "?"  # update

Mysql dataclass

Bases: SQLDriver

The Mysql driver supports MySQL databases.

tinyint1_is_boolean: bool = True class-attribute instance-attribute

Treat SQL column-type 'tinyint(1)' as Boolean

MySQL does not have a true 'Boolean' column. Instead, a column is declared as 'Boolean' will be stored as 'tinyint(1)'. Setting this arg as 'True' will map the [ColumnClass][pysimplesql.pysimplesql.ColumnClass] as a [BoolCol][pysimplesql.pysimplesql.BoolCol].

Mariadb dataclass

Bases: Mysql

The Mariadb driver supports MariaDB databases.

Postgres dataclass

Bases: SQLDriver

The Postgres driver supports PostgreSQL databases.

sync_sequences: bool = False class-attribute instance-attribute

Synchronize the sequences with the max pk for each table on database connection.

This is useful if manual records were inserted without calling nextval() to update the sequencer.

Sqlserver dataclass

Bases: SQLDriver

The Sqlserver driver supports Microsoft SQL Server databases.

MSAccess(database_file, *, overwrite_file=False, sql_script=None, sql_script_encoding='utf-8', sql_commands=None, update_cascade=True, delete_cascade=True, sql_char=sql_char, infer_datetype_from_default_function=True, use_newer_jackcess=False) dataclass

Bases: SQLDriver

The MSAccess driver supports Microsoft Access databases. Note that only database interactions are supported, including stored Queries, but not operations dealing with Forms, Reports, etc.

Note: Jackcess and UCanAccess libraries may not accurately report decimal places for "Number" or "Currency" columns. Manual configuration of decimal places may be required by replacing the placeholders as follows: frm[DATASET KEY].column_info[COLUMN NAME].scale = 2

Parameters:

Name Type Description Default
database_file Union[str, Path]

The path to the MS Access database file.

required
overwrite_file bool

If True, prompts the user if the file already exists. If the user declines to overwrite the file, the provided SQL commands or script will not be executed.

False
sql_script str

(optional) SQL script file to execute after opening the db.

None
sql_script_encoding str

The encoding of the SQL script file. Defaults to 'utf-8'.

'utf-8'
sql_commands str

(optional) SQL commands to execute after opening the database. Note: sql_commands are executed after sql_script.

None
update_cascade bool

(optional) Default:True. Requery and filter child table on selected parent primary key. (ON UPDATE CASCADE in SQL)

True
delete_cascade bool

(optional) Default:True. Delete the dependent child records if the parent table record is deleted. (ON UPDATE DELETE in SQL)

True
sql_char SqlChar

(optional) SqlChar object, if non-default chars desired.

sql_char
infer_datetype_from_default_function bool

If True, specializes a DateTime column by examining the column's default function. A DateTime column with '=Date()' will be treated as a 'DateCol', and '=Time()' will be treated as a 'TimeCol'. Defaults to True.

True
use_newer_jackcess bool

If True, uses a newer version of the Jackcess library for improved compatibility, specifically allowing handling of 'attachment' columns. Defaults to False.

False
Source code in pysimplesql\pysimplesql.py
11322
11323
11324
11325
11326
11327
11328
11329
11330
11331
11332
11333
11334
11335
11336
11337
11338
11339
11340
11341
11342
11343
11344
11345
11346
11347
11348
11349
11350
11351
11352
11353
11354
11355
11356
11357
11358
11359
11360
11361
11362
11363
11364
11365
11366
11367
11368
11369
11370
11371
def __init__(
    self,
    database_file: Union[str, Path],
    *,
    overwrite_file: bool = False,
    sql_script: str = None,
    sql_script_encoding: str = "utf-8",
    sql_commands: str = None,
    update_cascade: bool = True,
    delete_cascade: bool = True,
    sql_char: SqlChar = sql_char,
    infer_datetype_from_default_function: bool = True,
    use_newer_jackcess: bool = False,
) -> None:
    """Initialize the MSAccess class.

    Args:
        database_file: The path to the MS Access database file.
        overwrite_file: If True, prompts the user if the file already exists. If the
            user declines to overwrite the file, the provided SQL commands or script
            will not be executed.
        sql_script: (optional) SQL script file to execute after opening the db.
        sql_script_encoding: The encoding of the SQL script file. Defaults to
            'utf-8'.
        sql_commands: (optional) SQL commands to execute after opening the database.
            Note: sql_commands are executed after sql_script.
        update_cascade: (optional) Default:True. Requery and filter child table on
            selected parent primary key. (ON UPDATE CASCADE in SQL)
        delete_cascade: (optional) Default:True. Delete the dependent child records
            if the parent table record is deleted. (ON UPDATE DELETE in SQL)
        sql_char: (optional) `SqlChar` object, if non-default chars desired.
        infer_datetype_from_default_function: If True, specializes a DateTime column
            by examining the column's default function. A DateTime column with
            '=Date()' will be treated as a 'DateCol', and '=Time()' will be treated
            as a 'TimeCol'. Defaults to True.
        use_newer_jackcess: If True, uses a newer version of the Jackcess library
            for improved compatibility, specifically allowing handling of
            'attachment' columns. Defaults to False.
    """
    self.database_file = str(database_file)
    self.overwrite_file = overwrite_file
    self.sql_script = sql_script
    self.sql_script_encoding = sql_script_encoding
    self.sql_commands = sql_commands
    self.update_cascade = update_cascade
    self.delete_cascade = delete_cascade
    self.infer_datetype_from_default_function = infer_datetype_from_default_function
    self.use_newer_jackcess = use_newer_jackcess

    super().__post_init__(sql_char)

Driver

The Driver class allows for easy driver creation.

It is a simple wrapper around the various SQLDriver classes.

process_events(event, values)

Process mapped events for ALL Form instances.

Not to be confused with process_events(), which processes events for individual Form instances. This should be called once per iteration in your event loop. Note: Events handled are responsible for requerying and updating elements as needed.

Parameters:

Name Type Description Default
event str

The event returned by PySimpleGUI.read()

required
values list

the values returned by PySimpleGUI.read()

required

Returns:

Type Description
bool

True if an event was handled, False otherwise

Source code in pysimplesql\pysimplesql.py
4929
4930
4931
4932
4933
4934
4935
4936
4937
4938
4939
4940
4941
4942
4943
4944
4945
4946
4947
4948
def process_events(event: str, values: list) -> bool:
    """Process mapped events for ALL Form instances.

    Not to be confused with `Form.process_events()`, which processes events for
    individual `Form` instances. This should be called once per iteration in your event
    loop. Note: Events handled are responsible for requerying and updating elements as
    needed.

    Args:
        event: The event returned by PySimpleGUI.read()
        values: the values returned by PySimpleGUI.read()

    Returns:
        True if an event was handled, False otherwise
    """
    handled = False
    for i in Form.instances:
        if i.process_events(event, values):
            handled = True
    return handled

update_elements(data_key=None, edit_protect_only=False)

Updated the GUI elements to reflect values from the database for ALL Form instances. Not to be confused with update_elements(), which updates GUI elements for individual Form instances.

Parameters:

Name Type Description Default
data_key str

(optional) key of DataSet to update elements for, otherwise updates elements for all datasets.

None
edit_protect_only bool

(optional) If true, only update items affected by edit_protect.

False

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
4951
4952
4953
4954
4955
4956
4957
4958
4959
4960
4961
4962
4963
4964
4965
4966
def update_elements(data_key: str = None, edit_protect_only: bool = False) -> None:
    """Updated the GUI elements to reflect values from the database for ALL Form
    instances. Not to be confused with `Form.update_elements()`, which updates GUI
    elements for individual `Form` instances.

    Args:
        data_key: (optional) key of `DataSet` to update elements for, otherwise updates
            elements for all datasets.
        edit_protect_only: (optional) If true, only update items affected by
            edit_protect.

    Returns:
        None
    """
    for i in Form.instances:
        i.update_elements(data_key, edit_protect_only)

bind(win)

Bind all Form instances to specific window.

Not to be confused with bind(), which binds specific form to the window.

Parameters:

Name Type Description Default
win Window

The PySimpleGUI window to bind all forms to

required
Source code in pysimplesql\pysimplesql.py
4969
4970
4971
4972
4973
4974
4975
4976
4977
4978
def bind(win: sg.Window) -> None:
    """Bind all `Form` instances to specific window.

    Not to be confused with `Form.bind()`, which binds specific form to the window.

    Args:
        win: The PySimpleGUI window to bind all forms to
    """
    for i in Form.instances:
        i.bind(win)

simple_transform(dataset, row, encode)

Convenience transform function that makes it easier to add transforms to your records.

Source code in pysimplesql\pysimplesql.py
4981
4982
4983
4984
4985
4986
4987
4988
4989
4990
4991
4992
def simple_transform(dataset: DataSet, row, encode) -> None:
    """Convenience transform function that makes it easier to add transforms to your
    records.
    """
    for col, function in dataset._simple_transform.items():
        if col in row:
            msg = f"Transforming {col} from {row[col]}"
            if encode == TFORM_DECODE:
                row[col] = function["decode"](row, col)
            else:
                row[col] = function["encode"](row, col)
            logger.debug(f"{msg} to {row[col]}")

update_table_element(window, element, values, select_rows)

Updates a PySimpleGUI sg.Table with new data and suppresses extra events emitted.

Call this function instead of simply calling update() on a sg.Table element. Without unbinding the virtual "<>" event, updating the selection or values will in turn fire more changed events, creating an endless loop of events.

Parameters:

Name Type Description Default
window Window

A PySimpleGUI Window containing the sg.Table element to be updated.

required
element Type[Table]

The sg.Table element to be updated.

required
values List[TableRow]

A list of table rows to update the sg.Table with.

required
select_rows List[int]

List of rows to select as if user did.

required

Returns:

Type Description
None

None

Source code in pysimplesql\pysimplesql.py
4995
4996
4997
4998
4999
5000
5001
5002
5003
5004
5005
5006
5007
5008
5009
5010
5011
5012
5013
5014
5015
5016
5017
5018
5019
5020
5021
5022
5023
5024
5025
5026
5027
5028
def update_table_element(
    window: sg.Window,
    element: Type[sg.Table],
    values: List[TableRow],
    select_rows: List[int],
) -> None:
    """Updates a PySimpleGUI sg.Table with new data and suppresses extra events emitted.

    Call this function instead of simply calling update() on a sg.Table element.
    Without unbinding the virtual "<<TreeviewSelect>>" event, updating the selection or
    values will in turn fire more changed events, creating an endless loop of events.

    Args:
        window: A PySimpleGUI Window containing the sg.Table element to be updated.
        element: The sg.Table element to be updated.
        values: A list of table rows to update the sg.Table with.
        select_rows: List of rows to select as if user did.

    Returns:
        None
    """
    # Disable handling for "<<TreeviewSelect>>" event
    element.widget.unbind("<<TreeviewSelect>>")
    # update element
    element.update(values=values, select_rows=select_rows)

    # make sure row_iid is visible
    if not isinstance(element, LazyTable) and len(values) and select_rows:
        row_iid = element.tree_ids[select_rows[0]]
        element.widget.see(row_iid)

    window.refresh()  # Event handled and bypassed
    # Enable handling for "<<TreeviewSelect>>" event
    element.widget.bind("<<TreeviewSelect>>", element._treeview_selected)

checkbox_to_bool(value)

Allows a variety of checkbox values to still return True or False.

Parameters:

Name Type Description Default
value Union[str, int, bool]

Value to convert into True or False

required

Returns:

Type Description
bool

bool

Source code in pysimplesql\pysimplesql.py
5031
5032
5033
5034
5035
5036
5037
5038
5039
5040
5041
5042
5043
5044
5045
5046
5047
5048
5049
def checkbox_to_bool(value: Union[str, int, bool]) -> bool:
    """Allows a variety of checkbox values to still return True or False.

    Args:
        value: Value to convert into True or False

    Returns:
        bool
    """
    return str(value).lower() in [
        "y",
        "yes",
        "t",
        "true",
        "1",
        "on",
        "enabled",
        themepack.checkbox_true,
    ]

shake_widget(widget, pixels=4, delay_ms=50, repeat=2)

Shakes the given widget by modifying its padx attribute.

Parameters:

Name Type Description Default
widget Union[Element, Widget]

The widget to shake. Must be an instance of sg.Element or tk.Widget.

required
pixels int

The number of pixels by which to shake the widget horizontally.

4
delay_ms int

The delay in milliseconds between each shake movement.

50
repeat int

The number of times to repeat the shaking movement.

2
Source code in pysimplesql\pysimplesql.py
5052
5053
5054
5055
5056
5057
5058
5059
5060
5061
5062
5063
5064
5065
5066
5067
5068
5069
5070
5071
5072
5073
5074
5075
5076
5077
5078
5079
5080
5081
5082
5083
5084
5085
5086
5087
5088
5089
5090
5091
5092
def shake_widget(
    widget: Union[sg.Element, tk.Widget],
    pixels: int = 4,
    delay_ms: int = 50,
    repeat: int = 2,
) -> None:
    """Shakes the given widget by modifying its padx attribute.

    Args:
        widget: The widget to shake. Must be an instance of sg.Element or tk.Widget.
        pixels: The number of pixels by which to shake the widget horizontally.
        delay_ms: The delay in milliseconds between each shake movement.
        repeat: The number of times to repeat the shaking movement.
    """
    if isinstance(widget, sg.Element):
        widget = widget.Widget
    elif not isinstance(widget, tk.Widget):
        logger.debug(f"{widget} not a valid sg.Element or tk.Widget")
        return
    padx = widget.pack_info().get("padx", 0)

    # Adjust padx based on its current value
    if isinstance(padx, tuple):
        padx_left = padx[0] + pixels
        padx_right = padx[1] - pixels
        new_padx = (padx_left, padx_right)
    else:
        padx_left = padx + pixels
        padx_right = max(padx - pixels, 0)
        new_padx = (padx_left, padx_right)

    widget.update()

    # Perform the shaking movement
    for _ in range(repeat):
        widget.pack_configure(padx=new_padx)
        widget.update()
        widget.after(delay_ms)
        widget.pack_configure(padx=padx)
        widget.update()
        widget.after(delay_ms)

field(field, element=_EnhancedInput, size=None, label='', no_label=False, label_above=False, quick_editor=True, quick_editor_kwargs=None, filter=None, key=None, use_ttk_buttons=None, pad=None, **kwargs)

Convenience function for adding PySimpleGUI elements to the Window, so they are properly configured for pysimplesql. The automatic functionality of pysimplesql relies on accompanying metadata so that the auto_map_elements() can pick them up. This convenience function will create a text label, along with an element with the above metadata already set up for you. Note: The element key will default to the field name if none is supplied.

Parameters:

Name Type Description Default
field str

The database record in the form of table.column I.e. 'Journal.entry'

required
element Union[Type[Checkbox], Type[Combo], Type[Input], Type[Multiline]]

(optional) The element type desired (defaults to PySimpleGUI.Input)

_EnhancedInput
size Tuple[int, int]

Overrides the default element size for this element only.

None
label str

The text/label will automatically be generated from the column name. If a different text/label is desired, it can be specified here.

''
no_label bool

Do not automatically generate a label for this element

False
label_above bool

Place the label above the element instead of to the left.

False
quick_editor bool

For records that reference another table, place a quick edit button next to the element

True
quick_editor_kwargs dict

Additional keyword arguments to pass to quick editor.

None
filter

Can be used to reference different Forms in the same layout. Use a matching filter when creating the Form with the filter parameter.

None
key

(optional) The key to give this element. See note above about the default auto generated key.

None
use_ttk_buttons

Use ttk buttons for all action buttons. If None, defaults to setting [use_ttk_buttons][pysimplesql.pysimplesql.ThemePack.use_ttk_buttons].

None
pad

The padding to use for the generated elements. If None, defaults to setting [default_element_pad][pysimplesql.pysimplesql.ThemePack.default_element_pad].

None
**kwargs

Any additional arguments will be passed to the PySimpleGUI element.

{}

Returns:

Type Description
Column

Element(s) to be used in the creation of PySimpleGUI layouts. Note that this

Column

function actually creates multiple Elements wrapped in a PySimpleGUI Column, but

Column

can be treated as a single Element.

Source code in pysimplesql\pysimplesql.py
6513
6514
6515
6516
6517
6518
6519
6520
6521
6522
6523
6524
6525
6526
6527
6528
6529
6530
6531
6532
6533
6534
6535
6536
6537
6538
6539
6540
6541
6542
6543
6544
6545
6546
6547
6548
6549
6550
6551
6552
6553
6554
6555
6556
6557
6558
6559
6560
6561
6562
6563
6564
6565
6566
6567
6568
6569
6570
6571
6572
6573
6574
6575
6576
6577
6578
6579
6580
6581
6582
6583
6584
6585
6586
6587
6588
6589
6590
6591
6592
6593
6594
6595
6596
6597
6598
6599
6600
6601
6602
6603
6604
6605
6606
6607
6608
6609
6610
6611
6612
6613
6614
6615
6616
6617
6618
6619
6620
6621
6622
6623
6624
6625
6626
6627
6628
6629
6630
6631
6632
6633
6634
6635
6636
6637
6638
6639
6640
6641
6642
6643
6644
6645
6646
6647
6648
6649
6650
6651
6652
6653
6654
6655
6656
6657
6658
6659
6660
6661
6662
6663
6664
6665
6666
6667
6668
6669
6670
6671
6672
6673
6674
6675
6676
6677
6678
6679
6680
6681
6682
6683
6684
6685
6686
6687
6688
6689
6690
def field(
    field: str,
    element: Union[
        Type[sg.Checkbox],
        Type[sg.Combo],
        Type[sg.Input],
        Type[sg.Multiline],
    ] = _EnhancedInput,
    size: Tuple[int, int] = None,
    label: str = "",
    no_label: bool = False,
    label_above: bool = False,
    quick_editor: bool = True,
    quick_editor_kwargs: dict = None,
    filter=None,
    key=None,
    use_ttk_buttons=None,
    pad=None,
    **kwargs,
) -> sg.Column:
    """Convenience function for adding PySimpleGUI elements to the Window, so they are
    properly configured for pysimplesql. The automatic functionality of pysimplesql
    relies on accompanying metadata so that the `Form.auto_map_elements()` can pick them
    up. This convenience function will create a text label, along with an element with
    the above metadata already set up for you. Note: The element key will default to the
    field name if none is supplied.

    Args:
        field: The database record in the form of table.column I.e. 'Journal.entry'
        element: (optional) The element type desired (defaults to PySimpleGUI.Input)
        size: Overrides the default element size for this element only.
        label: The text/label will automatically be generated from the column name. If a
            different text/label is desired, it can be specified here.
        no_label: Do not automatically generate a label for this element
        label_above: Place the label above the element instead of to the left.
        quick_editor: For records that reference another table, place a quick edit
            button next to the element
        quick_editor_kwargs: Additional keyword arguments to pass to quick editor.
        filter: Can be used to reference different `Form`s in the same layout. Use a
            matching filter when creating the `Form` with the filter parameter.
        key: (optional) The key to give this element. See note above about the default
            auto generated key.
        use_ttk_buttons: Use ttk buttons for all action buttons. If None, defaults to
            setting `ThemePack.use_ttk_buttons`.
        pad: The padding to use for the generated elements. If None, defaults to setting
            `ThemePack.default_element_pad`.
        **kwargs: Any additional arguments will be passed to the PySimpleGUI element.

    Returns:
        Element(s) to be used in the creation of PySimpleGUI layouts.  Note that this
        function actually creates multiple Elements wrapped in a PySimpleGUI Column, but
        can be treated as a single Element.
    """
    # TODO: See what the metadata does after initial setup is complete - needed anymore?
    element = _EnhancedInput if element == sg.Input else element
    element = _EnhancedMultiline if element == sg.Multiline else element
    element = _AutocompleteCombo if element == sg.Combo else element

    if use_ttk_buttons is None:
        use_ttk_buttons = themepack.use_ttk_buttons
    if pad is None:
        pad = themepack.default_element_pad

    # if Record imply a where clause (indicated by ?) If so, strip out the info we need
    if "?" in field:
        table_info, where_info = field.split("?")
        label_text = (
            where_info.split("=")[1].replace("fk", "").replace("_", " ").capitalize()
            + ":"
        )
    else:
        table_info = field
        label_text = (
            table_info.split(".")[1].replace("fk", "").replace("_", " ").capitalize()
            + ":"
        )
    table, column = table_info.split(".")

    key = field if key is None else key
    key = keygen.get(key)

    if "values" in kwargs:
        first_param = kwargs["values"]
        del kwargs["values"]  # make sure we don't put it in twice
    else:
        first_param = ""

    if element == _EnhancedMultiline:
        layout_element = element(
            first_param,
            key=key,
            size=size or themepack.default_mline_size,
            metadata={
                "type": ElementType.FIELD,
                "Form": None,
                "filter": filter,
                "field": field,
                "data_key": key,
            },
            pad=pad,
            **kwargs,
        )
    else:
        layout_element = element(
            first_param,
            key=key,
            size=size or themepack.default_element_size,
            metadata={
                "type": ElementType.FIELD,
                "Form": None,
                "filter": filter,
                "field": field,
                "data_key": key,
            },
            pad=pad,
            **kwargs,
        )
    layout_label = sg.Text(
        label if label else label_text,
        size=themepack.default_label_size,
        key=f"{key}:label",
    )
    # Marker for required (notnull) records
    layout_marker = sg.Column(
        [
            [
                sg.T(
                    themepack.marker_required,
                    key=f"{key}:marker",
                    text_color=sg.theme_background_color(),
                    visible=True,
                )
            ]
        ],
        pad=(0, 0),
    )
    if no_label:
        layout = [[layout_marker, layout_element]]
    elif label_above:
        layout = [[layout_label], [layout_marker, layout_element]]
    else:
        layout = [[layout_label, layout_marker, layout_element]]
    # Add the quick editor button where appropriate
    if element == _AutocompleteCombo and quick_editor:
        meta = {
            "type": ElementType.EVENT,
            "event_type": EventType.QUICK_EDIT,
            "table": table,
            "column": column,
            "function": None,
            "Form": None,
            "filter": filter,
            "quick_editor_kwargs": quick_editor_kwargs,
        }
        if type(themepack.quick_edit) is bytes:
            layout[-1].append(
                sg.B(
                    "",
                    key=keygen.get(f"{key}.quick_edit"),
                    size=(1, 1),
                    image_data=themepack.quick_edit,
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                )
            )
        else:
            layout[-1].append(
                sg.B(
                    themepack.quick_edit,
                    key=keygen.get(f"{key}.quick_edit"),
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                )
            )
    # return layout
    return sg.Col(layout=layout, pad=(0, 0))

actions(table, key=None, default=True, edit_protect=None, navigation=None, insert=None, delete=None, duplicate=None, save=None, search=None, search_size=(30, 1), bind_return_key=True, filter=None, use_ttk_buttons=None, pad=None, **kwargs)

Allows for easily adding record navigation and record action elements to the PySimpleGUI window The navigation elements are generated automatically (first, previous, next, last and search). The action elements can be customized by selecting which ones you want generated from the parameters available. This allows full control over what is available to the user of your database application. Check out ThemePack to give any of these autogenerated controls a custom look!.

Note: By default, the base element keys generated for PySimpleGUI will be table:action using the name of the table passed in the table parameter plus the action strings below separated by a colon: (I.e. Journal:table_insert) edit_protect, db_save, table_first, table_previous, table_next, table_last, table_duplicate, table_insert, table_delete, search_input, search_button. If you supply a key with the key parameter, then these additional strings will be appended to that key. Also note that these autogenerated keys also pass through the KeyGen, so it's possible that these keys could be table_last:action!1, table_last:action!2, etc.

Parameters:

Name Type Description Default
table str

The table name that this "element" will provide actions for

required
key

(optional) The base key to give the generated elements

None
default bool

Default edit_protect, navigation, insert, delete, save and search to either true or false (defaults to True) The individual keyword arguments will trump the default parameter. This allows for starting with all actions defaulting to False, then individual ones can be enabled with True - or the opposite by defaulting them all to True, and disabling the ones not needed with False.

True
edit_protect bool

An edit protection mode to prevent accidental changes in the database. It is a button that toggles the ability on and off to prevent accidental changes in the database by enabling/disabling the insert, edit, duplicate, delete and save buttons.

None
navigation bool

The standard << < > >> (First, previous, next, last) buttons for navigation

None
insert bool

Button to insert new records

None
delete bool

Button to delete current record

None
duplicate bool

Button to duplicate current record

None
save bool

Button to save record. Note that the save button feature saves changes made to any table, therefore only one save button is needed per window.

None
search bool

A search Input element. Size can be specified with the 'search_size' parameter

None
search_size Tuple[int, int]

The size of the search input element

(30, 1)
bind_return_key bool

Bind the return key to the search button. Defaults to true.

True
filter str

Can be used to reference different Forms in the same layout. Use a matching filter when creating the Form with the filter parameter.

None
use_ttk_buttons bool

Use ttk buttons for all action buttons. If None, defaults to setting [use_ttk_buttons][pysimplesql.pysimplesql.ThemePack.use_ttk_buttons].

None
pad

The padding to use for the generated elements. If None, defaults to setting [action_button_pad][pysimplesql.pysimplesql.ThemePack.action_button_pad].

None
**kwargs

Any additional arguments will be passed to the PySimpleGUI element.

{}

Returns:

Type Description
Column

An element to be used in the creation of PySimpleGUI layouts. Note that this is

Column

technically multiple elements wrapped in a PySimpleGUI.Column, but acts as one

Column

element for the purpose of layout building.

Source code in pysimplesql\pysimplesql.py
6693
6694
6695
6696
6697
6698
6699
6700
6701
6702
6703
6704
6705
6706
6707
6708
6709
6710
6711
6712
6713
6714
6715
6716
6717
6718
6719
6720
6721
6722
6723
6724
6725
6726
6727
6728
6729
6730
6731
6732
6733
6734
6735
6736
6737
6738
6739
6740
6741
6742
6743
6744
6745
6746
6747
6748
6749
6750
6751
6752
6753
6754
6755
6756
6757
6758
6759
6760
6761
6762
6763
6764
6765
6766
6767
6768
6769
6770
6771
6772
6773
6774
6775
6776
6777
6778
6779
6780
6781
6782
6783
6784
6785
6786
6787
6788
6789
6790
6791
6792
6793
6794
6795
6796
6797
6798
6799
6800
6801
6802
6803
6804
6805
6806
6807
6808
6809
6810
6811
6812
6813
6814
6815
6816
6817
6818
6819
6820
6821
6822
6823
6824
6825
6826
6827
6828
6829
6830
6831
6832
6833
6834
6835
6836
6837
6838
6839
6840
6841
6842
6843
6844
6845
6846
6847
6848
6849
6850
6851
6852
6853
6854
6855
6856
6857
6858
6859
6860
6861
6862
6863
6864
6865
6866
6867
6868
6869
6870
6871
6872
6873
6874
6875
6876
6877
6878
6879
6880
6881
6882
6883
6884
6885
6886
6887
6888
6889
6890
6891
6892
6893
6894
6895
6896
6897
6898
6899
6900
6901
6902
6903
6904
6905
6906
6907
6908
6909
6910
6911
6912
6913
6914
6915
6916
6917
6918
6919
6920
6921
6922
6923
6924
6925
6926
6927
6928
6929
6930
6931
6932
6933
6934
6935
6936
6937
6938
6939
6940
6941
6942
6943
6944
6945
6946
6947
6948
6949
6950
6951
6952
6953
6954
6955
6956
6957
6958
6959
6960
6961
6962
6963
6964
6965
6966
6967
6968
6969
6970
6971
6972
6973
6974
6975
6976
6977
6978
6979
6980
6981
6982
6983
6984
6985
6986
6987
6988
6989
6990
6991
6992
6993
6994
6995
6996
6997
6998
6999
7000
7001
7002
7003
7004
7005
7006
7007
7008
7009
7010
7011
7012
7013
7014
7015
7016
7017
7018
7019
7020
7021
7022
7023
7024
7025
7026
7027
7028
7029
7030
7031
7032
7033
7034
7035
7036
7037
7038
7039
7040
7041
7042
7043
7044
7045
7046
7047
7048
7049
7050
7051
7052
7053
7054
7055
7056
7057
7058
7059
7060
7061
7062
7063
7064
7065
7066
7067
7068
7069
7070
7071
7072
7073
7074
7075
7076
7077
7078
7079
7080
7081
7082
7083
7084
7085
7086
7087
7088
7089
7090
7091
7092
7093
7094
7095
7096
7097
7098
7099
7100
7101
7102
7103
7104
7105
7106
7107
7108
7109
7110
7111
7112
7113
7114
7115
7116
7117
7118
7119
7120
7121
7122
7123
7124
def actions(
    table: str,
    key=None,
    default: bool = True,
    edit_protect: bool = None,
    navigation: bool = None,
    insert: bool = None,
    delete: bool = None,
    duplicate: bool = None,
    save: bool = None,
    search: bool = None,
    search_size: Tuple[int, int] = (30, 1),
    bind_return_key: bool = True,
    filter: str = None,
    use_ttk_buttons: bool = None,
    pad=None,
    **kwargs,
) -> sg.Column:
    """Allows for easily adding record navigation and record action elements to the
    PySimpleGUI window The navigation elements are generated automatically (first,
    previous, next, last and search).  The action elements can be customized by
    selecting which ones you want generated from the parameters available.  This allows
    full control over what is available to the user of your database application. Check
    out `ThemePack` to give any of these autogenerated controls a custom look!.

    Note: By default, the base element keys generated for PySimpleGUI will be
    `table:action` using the name of the table passed in the table parameter plus the
    action strings below separated by a colon: (I.e. Journal:table_insert) edit_protect,
    db_save, table_first, table_previous, table_next, table_last, table_duplicate,
    table_insert, table_delete, search_input, search_button. If you supply a key with
    the key parameter, then these additional strings will be appended to that key. Also
    note that these autogenerated keys also pass through the `KeyGen`, so it's possible
    that these keys could be table_last:action!1, table_last:action!2, etc.

    Args:
        table: The table name that this "element" will provide actions for
        key: (optional) The base key to give the generated elements
        default: Default edit_protect, navigation, insert, delete, save and search to
            either true or false (defaults to True) The individual keyword arguments
            will trump the default parameter.  This allows for starting with all actions
            defaulting to False, then individual ones can be enabled with True - or the
            opposite by defaulting them all to True, and disabling the ones not needed
            with False.
        edit_protect: An edit protection mode to prevent accidental changes in the
            database. It is a button that toggles the ability on and off to prevent
            accidental changes in the database by enabling/disabling the insert, edit,
            duplicate, delete and save buttons.
        navigation: The standard << < > >> (First, previous, next, last) buttons for
            navigation
        insert: Button to insert new records
        delete: Button to delete current record
        duplicate: Button to duplicate current record
        save: Button to save record.  Note that the save button feature saves changes
            made to any table, therefore only one save button is needed per window.
        search: A search Input element. Size can be specified with the 'search_size'
            parameter
        search_size: The size of the search input element
        bind_return_key: Bind the return key to the search button. Defaults to true.
        filter: Can be used to reference different `Form`s in the same layout.  Use a
            matching filter when creating the `Form` with the filter parameter.
        use_ttk_buttons: Use ttk buttons for all action buttons. If None, defaults to
            setting `ThemePack.use_ttk_buttons`.
        pad: The padding to use for the generated elements. If None, defaults to setting
            `ThemePack.action_button_pad`.
        **kwargs: Any additional arguments will be passed to the PySimpleGUI element.

    Returns:
        An element to be used in the creation of PySimpleGUI layouts.  Note that this is
        technically multiple elements wrapped in a PySimpleGUI.Column, but acts as one
        element for the purpose of layout building.
    """
    if use_ttk_buttons is None:
        use_ttk_buttons = themepack.use_ttk_buttons
    if pad is None:
        pad = themepack.action_button_pad

    edit_protect = default if edit_protect is None else edit_protect
    navigation = default if navigation is None else navigation
    insert = default if insert is None else insert
    delete = default if delete is None else delete
    duplicate = default if duplicate is None else duplicate
    save = default if save is None else save
    search = default if search is None else search
    key = f"{table}:" if key is None else f"{key}:"

    layout = []

    # Form-level events
    if edit_protect:
        meta = {
            "type": ElementType.EVENT,
            "event_type": EventType.EDIT_PROTECT_DB,
            "table": None,
            "column": None,
            "function": None,
            "Form": None,
            "filter": filter,
        }
        if type(themepack.edit_protect) is bytes:
            layout.append(
                sg.B(
                    "",
                    key=keygen.get(f"{key}edit_protect"),
                    size=(1, 1),
                    image_data=themepack.edit_protect,
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                )
            )
        else:
            layout.append(
                sg.B(
                    themepack.edit_protect,
                    key=keygen.get(f"{key}edit_protect"),
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                )
            )
    if save:
        meta = {
            "type": ElementType.EVENT,
            "event_type": EventType.SAVE_DB,
            "table": None,
            "column": None,
            "function": None,
            "Form": None,
            "filter": filter,
        }
        if type(themepack.save) is bytes:
            layout.append(
                sg.B(
                    "",
                    key=keygen.get(f"{key}db_save"),
                    image_data=themepack.save,
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                )
            )
        else:
            layout.append(
                sg.B(themepack.save, key=keygen.get(f"{key}db_save"), metadata=meta)
            )

    # DataSet-level events
    if navigation:
        # first
        meta = {
            "type": ElementType.EVENT,
            "event_type": EventType.FIRST,
            "table": table,
            "column": None,
            "function": None,
            "Form": None,
            "filter": filter,
        }
        if type(themepack.first) is bytes:
            layout.append(
                sg.B(
                    "",
                    key=keygen.get(f"{key}table_first"),
                    size=(1, 1),
                    image_data=themepack.first,
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                )
            )
        else:
            layout.append(
                sg.B(
                    themepack.first,
                    key=keygen.get(f"{key}table_first"),
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                )
            )
        # previous
        meta = {
            "type": ElementType.EVENT,
            "event_type": EventType.PREVIOUS,
            "table": table,
            "column": None,
            "function": None,
            "Form": None,
            "filter": filter,
        }
        if type(themepack.previous) is bytes:
            layout.append(
                sg.B(
                    "",
                    key=keygen.get(f"{key}table_previous"),
                    size=(1, 1),
                    image_data=themepack.previous,
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                )
            )
        else:
            layout.append(
                sg.B(
                    themepack.previous,
                    key=keygen.get(f"{key}table_previous"),
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                )
            )
        # next
        meta = {
            "type": ElementType.EVENT,
            "event_type": EventType.NEXT,
            "table": table,
            "column": None,
            "function": None,
            "Form": None,
            "filter": filter,
        }
        if type(themepack.next) is bytes:
            layout.append(
                sg.B(
                    "",
                    key=keygen.get(f"{key}table_next"),
                    size=(1, 1),
                    image_data=themepack.next,
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                )
            )
        else:
            layout.append(
                sg.B(
                    themepack.next,
                    key=keygen.get(f"{key}table_next"),
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                )
            )
        # last
        meta = {
            "type": ElementType.EVENT,
            "event_type": EventType.LAST,
            "table": table,
            "column": None,
            "function": None,
            "Form": None,
            "filter": filter,
        }
        if type(themepack.last) is bytes:
            layout.append(
                sg.B(
                    "",
                    key=keygen.get(f"{key}table_last"),
                    size=(1, 1),
                    image_data=themepack.last,
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                )
            )
        else:
            layout.append(
                sg.B(
                    themepack.last,
                    key=keygen.get(f"{key}table_last"),
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                )
            )
    if duplicate:
        meta = {
            "type": ElementType.EVENT,
            "event_type": EventType.DUPLICATE,
            "table": table,
            "column": None,
            "function": None,
            "Form": None,
            "filter": filter,
        }
        if type(themepack.duplicate) is bytes:
            layout.append(
                sg.B(
                    "",
                    key=keygen.get(f"{key}table_duplicate"),
                    size=(1, 1),
                    image_data=themepack.duplicate,
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                )
            )
        else:
            layout.append(
                sg.B(
                    themepack.duplicate,
                    key=keygen.get(f"{key}table_duplicate"),
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                )
            )
    if insert:
        meta = {
            "type": ElementType.EVENT,
            "event_type": EventType.INSERT,
            "table": table,
            "column": None,
            "function": None,
            "Form": None,
            "filter": filter,
        }
        if type(themepack.insert) is bytes:
            layout.append(
                sg.B(
                    "",
                    key=keygen.get(f"{key}table_insert"),
                    size=(1, 1),
                    image_data=themepack.insert,
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                )
            )
        else:
            layout.append(
                sg.B(
                    themepack.insert,
                    key=keygen.get(f"{key}table_insert"),
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                )
            )
    if delete:
        meta = {
            "type": ElementType.EVENT,
            "event_type": EventType.DELETE,
            "table": table,
            "column": None,
            "function": None,
            "Form": None,
            "filter": filter,
        }
        if type(themepack.delete) is bytes:
            layout.append(
                sg.B(
                    "",
                    key=keygen.get(f"{key}table_delete"),
                    size=(1, 1),
                    image_data=themepack.delete,
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                )
            )
        else:
            layout.append(
                sg.B(
                    themepack.delete,
                    key=keygen.get(f"{key}table_delete"),
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                )
            )
    if search:
        meta = {
            "type": ElementType.EVENT,
            "event_type": EventType.SEARCH,
            "table": table,
            "column": None,
            "function": None,
            "Form": None,
            "filter": filter,
        }
        if type(themepack.search) is bytes:
            layout += [
                _SearchInput(
                    "", key=keygen.get(f"{key}search_input"), size=search_size
                ),
                sg.B(
                    "",
                    key=keygen.get(f"{key}search_button"),
                    bind_return_key=bind_return_key,
                    size=(1, 1),
                    image_data=themepack.search,
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                ),
            ]
        else:
            layout += [
                _SearchInput(
                    "", key=keygen.get(f"{key}search_input"), size=search_size
                ),
                sg.B(
                    themepack.search,
                    key=keygen.get(f"{key}search_button"),
                    bind_return_key=bind_return_key,
                    metadata=meta,
                    use_ttk_buttons=use_ttk_buttons,
                    pad=pad,
                    **kwargs,
                ),
            ]
    return sg.Col(layout=[layout], pad=(0, 0))

selector(table, element=sg.Listbox, size=None, filter=None, key=None, **kwargs)

Selectors in pysimplesql are special elements that allow the user to change records in the database application. For example, Listboxes, Comboboxes and Tables all provide a convenient way to users to choose which record they want to select. This convenience function makes creating selectors very quick and as easy as using a normal PySimpleGUI element.

Parameters:

Name Type Description Default
table str

The table name that this selector will act on.

required
element Union[Type[Combo], Type[LazyTable], Type[Listbox], Type[Slider], Type[Table], TableBuilder]

The type of element you would like to use as a selector (defaults to a Listbox)

Listbox
size Tuple[int, int]

The desired size of this selector element

None
filter str

Can be used to reference different Forms in the same layout. Use a matching filter when creating the Form with the filter parameter.

None
key str

(optional) The key to give to this selector. If no key is provided, it will default to table:selector using the name specified in the table parameter. This is also passed through the keygen, so if selectors all use the default name, they will be made unique. ie: Journal:selector!1, Journal:selector!2, etc.

None
**kwargs

Any additional arguments supplied will be passed on to the PySimpleGUI element. Note: TableBuilder objects bring their own kwargs.

{}
Source code in pysimplesql\pysimplesql.py
7127
7128
7129
7130
7131
7132
7133
7134
7135
7136
7137
7138
7139
7140
7141
7142
7143
7144
7145
7146
7147
7148
7149
7150
7151
7152
7153
7154
7155
7156
7157
7158
7159
7160
7161
7162
7163
7164
7165
7166
7167
7168
7169
7170
7171
7172
7173
7174
7175
7176
7177
7178
7179
7180
7181
7182
7183
7184
7185
7186
7187
7188
7189
7190
7191
7192
7193
7194
7195
7196
7197
7198
7199
7200
7201
7202
7203
7204
7205
7206
7207
7208
7209
7210
7211
7212
7213
7214
7215
7216
7217
7218
7219
7220
7221
7222
7223
7224
7225
7226
7227
7228
7229
7230
7231
7232
7233
7234
7235
7236
7237
7238
7239
7240
7241
7242
7243
def selector(
    table: str,
    element: Union[
        Type[sg.Combo],
        Type[LazyTable],
        Type[sg.Listbox],
        Type[sg.Slider],
        Type[sg.Table],
        TableBuilder,
    ] = sg.Listbox,
    size: Tuple[int, int] = None,
    filter: str = None,
    key: str = None,
    **kwargs,
) -> sg.Element:
    """Selectors in pysimplesql are special elements that allow the user to change
    records in the database application. For example, Listboxes, Comboboxes and Tables
    all provide a convenient way to users to choose which record they want to select.
    This convenience function makes creating selectors very quick and as easy as using a
    normal PySimpleGUI element.

    Args:
        table: The table name that this selector will act on.
        element: The type of element you would like to use as a selector (defaults to a
            Listbox)
        size: The desired size of this selector element
        filter: Can be used to reference different `Form`s in the same layout. Use a
            matching filter when creating the `Form` with the filter parameter.
        key: (optional) The key to give to this selector. If no key is provided, it will
            default to table:selector using the name specified in the table parameter.
            This is also passed through the keygen, so if selectors all use the default
            name, they will be made unique. ie: Journal:selector!1, Journal:selector!2,
            etc.
        **kwargs: Any additional arguments supplied will be passed on to the PySimpleGUI
            element. Note: TableBuilder objects bring their own kwargs.
    """
    element = _AutocompleteCombo if element == sg.Combo else element

    key = f"{table}:selector" if key is None else key
    key = keygen.get(key)

    meta = {
        "type": ElementType.SELECTOR,
        "table": table,
        "Form": None,
        "filter": filter,
    }
    if element == sg.Listbox:
        layout = element(
            values=(),
            size=size or themepack.default_element_size,
            key=key,
            select_mode=sg.LISTBOX_SELECT_MODE_SINGLE,
            enable_events=True,
            metadata=meta,
        )
    elif element == sg.Slider:
        layout = element(
            enable_events=True,
            size=size or themepack.default_element_size,
            orientation="h",
            disable_number_display=True,
            key=key,
            metadata=meta,
        )
    elif element == _AutocompleteCombo:
        w = themepack.default_element_size[0]
        layout = element(
            values=(),
            size=size or (w, 10),
            enable_events=True,
            key=key,
            auto_size_text=False,
            metadata=meta,
        )
    elif element in [sg.Table, LazyTable]:
        required_kwargs = ["headings", "visible_column_map", "num_rows"]
        for kwarg in required_kwargs:
            if kwarg not in kwargs:
                raise RuntimeError(
                    f"DataSet selectors must use the {kwarg} keyword argument."
                )
        # Create a narrow column for displaying a * character for virtual rows.
        # This will be the 1st column
        kwargs["headings"].insert(0, "")
        kwargs["visible_column_map"].insert(0, 1)
        if "col_widths" in kwargs:
            kwargs["col_widths"].insert(0, themepack.unsaved_column_width)

        # Create other kwargs that are required
        kwargs["enable_events"] = True
        kwargs["select_mode"] = sg.TABLE_SELECT_MODE_BROWSE
        kwargs["justification"] = "left"

        # Make an empty list of values
        vals = [[""] * len(kwargs["headings"])]
        layout = element(values=vals, key=key, metadata=meta, **kwargs)
    elif isinstance(element, TableBuilder):
        table_builder = element
        element = table_builder.element
        lazy = table_builder.lazy_loading
        kwargs = table_builder.get_table_kwargs()

        meta["TableBuilder"] = table_builder
        # Make an empty list of values
        vals = [[""] * len(kwargs["headings"])]
        layout = element(
            vals,
            lazy_loading=lazy,
            key=key,
            metadata=meta,
            **kwargs,
        )
    else:
        raise RuntimeError(f'Element type "{element}" not supported as a selector.')

    return layout