"""
Module containing classes that read, manipulate, and write slots.
.. module:: slot
:synopsis:
.. moduleauthor: Paul Bromwell Jr.
"""
from datetime import datetime
from decimal import Decimal
from typing import Any, Dict, Optional, List, Union
from xml.etree import ElementTree
from gnewcash.file_formats import GnuCashXMLObject, GnuCashSQLiteObject
[docs]class Slot(GnuCashXMLObject, GnuCashSQLiteObject):
"""Represents a slot in GnuCash."""
sqlite_slot_type_mapping = {
1: 'integer',
2: 'double',
3: 'numeric',
4: 'string',
5: 'guid',
9: 'guid',
10: 'gdate'
}
def __init__(self, key: str, value: Any, slot_type: str) -> None:
self.key: str = key
self.value: Any = value
self.type: str = slot_type
@property
def as_xml(self) -> ElementTree.Element:
"""
Returns the current slot as GnuCash-compatible XML.
:return: Current slot as XML
:rtype: xml.etree.ElementTree.Element
"""
slot_node: ElementTree.Element = ElementTree.Element('slot')
ElementTree.SubElement(slot_node, 'slot:key').text = self.key
slot_value_node = ElementTree.SubElement(slot_node, 'slot:value', {'type': self.type})
if self.type == 'gdate':
ElementTree.SubElement(slot_value_node, 'gdate').text = datetime.strftime(self.value, '%Y-%m-%d')
elif self.type in ['string', 'guid', 'numeric']:
slot_value_node.text = self.value
elif self.type in ['integer', 'double']:
slot_value_node.text = str(self.value)
elif isinstance(self.value, list) and self.value:
for sub_slot in self.value:
slot_value_node.append(sub_slot.as_xml)
elif self.type == 'frame':
pass # Empty frame element, just leave it
else:
raise NotImplementedError('Slot type {} is not implemented.'.format(self.type))
return slot_node
[docs] @classmethod
def from_xml(cls, slot_node: ElementTree.Element, namespaces: Dict[str, str]) -> 'Slot':
"""
Creates a Slot object from the GnuCash XML.
:param slot_node: XML node for the slot
:type slot_node: ElementTree.Element
:param namespaces: XML namespaces for GnuCash elements
:type namespaces: dict[str, str]
:return: Slot object from XML
:rtype: Slot
"""
key_node: Optional[ElementTree.Element] = slot_node.find('slot:key', namespaces)
if key_node is None or not key_node.text:
raise ValueError('slot:key missing or empty in slot node')
key: str = key_node.text
value_node: Optional[ElementTree.Element] = slot_node.find('slot:value', namespaces)
if value_node is None:
raise ValueError('slot:value missing in slot node')
slot_type = value_node.attrib['type']
value: Any = None
if slot_type == 'gdate':
value_gdate_node: Optional[ElementTree.Element] = value_node.find('gdate')
if value_gdate_node is None:
raise ValueError('slot type is gdate but missing gdate node')
if not value_gdate_node.text:
raise ValueError('slot type is gdate but gdate node is empty')
value = datetime.strptime(value_gdate_node.text, '%Y-%m-%d')
elif slot_type in ['string', 'guid', 'numeric']:
value = value_node.text
elif slot_type == 'integer' and value_node.text:
value = int(value_node.text)
elif slot_type == 'double' and value_node.text:
value = Decimal(value_node.text)
else:
child_tags: List[str] = list(set(map(lambda x: x.tag, value_node)))
if len(child_tags) == 1 and child_tags[0] == 'slot':
value = [Slot.from_xml(x, namespaces) for x in value_node]
elif slot_type == 'frame':
value = None # Empty frame element, just leave it
else:
raise NotImplementedError('Slot type {} is not implemented.'.format(slot_type))
return cls(key, value, slot_type)
[docs] @classmethod
def from_sqlite(cls, sqlite_cursor, object_id):
"""
Creates Slot objects from the GnuCash SQLite database.
:param sqlite_cursor: Open cursor to the SQLite database
:type sqlite_cursor: sqlite3.Cursor
:param object_id: ID of the object that the slot belongs to
:type object_id: str
:return: Slot objects from SQLite
:rtype: list[Slot]
"""
slot_info = cls.get_sqlite_table_data(sqlite_cursor, 'slots', 'obj_guid = ?', (object_id,))
new_slots = []
for slot in slot_info:
slot_type = cls.sqlite_slot_type_mapping[slot['slot_type']]
slot_name = slot['name']
if slot_type == 'guid':
slot_value = slot['guid_val']
elif slot_type == 'string':
slot_value = slot['string_val']
elif slot_type == 'gdate':
slot_value = datetime.strptime(slot['gdate_val'], '%Y%m%d')
else:
raise NotImplementedError('Slot type {} is not implemented.'.format(slot['slot_type']))
new_slot = cls(slot_name, slot_value, slot_type)
new_slots.append(new_slot)
return new_slots
[docs] def to_sqlite(self, sqlite_cursor):
# slot_action = self.get_db_action(sqlite_cursor, 'slots', )
# TODO: Slots don't have GUIDs. Need to store the DB ID in the object.
raise NotImplementedError
[docs]class SlottableObject(object):
"""Class used to consolidate storing and retrieving slot values."""
def __init__(self) -> None:
super(SlottableObject, self).__init__()
self.slots: List[Slot] = []
[docs] def get_slot_value(self, key: str) -> Any:
"""
Retrieves the value of the slot given a certain key.
:param key: Name of the slot
:type key: str
:return: Slot value
:rtype: Any
"""
if not self.slots:
return None
target_slot: List[Slot] = list(filter(lambda x: x.key == key, self.slots))
if not target_slot:
return None
return target_slot[0].value
[docs] def set_slot_value(self, key: str, value: Any, slot_type: str) -> None:
"""
Sets the value of the slot given a certain key and slot type.
:param key: Name of the slot
:type key: str
:param value: New value of the slot
:type value: Any
:param slot_type: Type of slot
:type slot_type: str
"""
target_slot: List[Slot] = list(filter(lambda x: x.key == key, self.slots))
if target_slot:
target_slot[0].value = value
else:
self.slots.append(Slot(key, value, slot_type))
[docs] def set_slot_value_bool(self, key: str, value: Union[str, bool], slot_type: str) -> None:
"""
Helper function for slots that expect "true" or "false" GnuCash-side.
Converts "true" (case insensitive) and True to "true".
Converts "false" (case insensitive) and False to "false".
:param key:
:type key: str
:param value: New value of the slot
:type value: bool|str
:param slot_type: Type of slot
:type slot_type: str
"""
if isinstance(value, str) and value.lower() == 'true':
value = True
elif isinstance(value, str) and value.lower() == 'false':
value = False
elif isinstance(value, bool):
value = value
else:
raise ValueError('"bool" slot values must be "true", "false", True, or False.')
value = 'true' if value else 'false'
self.set_slot_value(key, value, slot_type)