378 lines
9.9 KiB
Python
378 lines
9.9 KiB
Python
import enum as _enum
|
|
import io as _io
|
|
import typing as _typing
|
|
|
|
from lxml import etree as _etree
|
|
|
|
XML_ENCODING = "UTF-8"
|
|
XML_DOCTYPE = "<!DOCTYPE BrickStockXML>"
|
|
XML_ROOT_TAG = "BrickStockXML"
|
|
|
|
OBJ_GUISTATES = "GuiStates"
|
|
|
|
|
|
def _enum_ensurer(enumtype: _enum.EnumMeta
|
|
) -> _typing.Callable[[_enum.Enum], str]:
|
|
return lambda v: enumtype(v).value
|
|
|
|
|
|
def _is_binary(fp: _typing.io) -> bool:
|
|
# https://stackoverflow.com/a/44584871/2334951
|
|
if hasattr(fp, "mode"):
|
|
return "b" in fp.mode
|
|
else:
|
|
return isinstance(fp, (_io.RawIOBase, _io.BufferedIOBase))
|
|
|
|
|
|
class Condition(_enum.Enum):
|
|
# taken from BrickStock source
|
|
# (bricklink.h #254; bricklink.cpp #989..)
|
|
NEW = "N"
|
|
USED = "U"
|
|
|
|
|
|
class SubCondition(_enum.Enum):
|
|
# taken from BrickStock source
|
|
# (bricklink.h #255; bricklink.cpp #991..)
|
|
NONE = "?"
|
|
COMPLETE = "C"
|
|
INCOMPLETE = "I"
|
|
MISB = "M"
|
|
|
|
|
|
class Status(_enum.Enum):
|
|
# taken from BrickStock source
|
|
# (bricklink.h #316; bricklink.cpp #1019..)
|
|
INCLUDE = "I"
|
|
EXCLUDE = "X"
|
|
EXTRA = "E"
|
|
UNKNOWN = "?"
|
|
|
|
|
|
class RootChildren(_enum.Enum):
|
|
INVENTORY = "Inventory"
|
|
GUISTATE = "GuiState"
|
|
|
|
|
|
class InventoryChildren(_enum.Enum):
|
|
ITEM = "Item"
|
|
|
|
|
|
class ItemChildren(_enum.Enum):
|
|
# taken from BrickStock source (bricklink.cpp #1124..)
|
|
ITEMID = "ItemID"
|
|
ITEMTYPEID = "ItemTypeID"
|
|
COLORID = "ColorID"
|
|
ITEMNAME = "ItemName"
|
|
ITEMTYPENAME = "ItemTypeName"
|
|
COLORNAME = "ColorName"
|
|
CATEGORYID = "CategoryID"
|
|
CATEGORYNAME = "CategoryName"
|
|
STATUS = "Status"
|
|
QTY = "Qty"
|
|
PRICE = "Price"
|
|
CONDITION = "Condition"
|
|
SUBCONDITION = "SubCondition"
|
|
ALTERNATE = "Alternate" # not suppoerted by BrickStore
|
|
COUNTERPART = "Counterpart" # not suppoerted by BrickStore
|
|
IMAGE = "Image"
|
|
BULK = "Bulk"
|
|
SALE = "Sale"
|
|
COMMENTS = "Comments"
|
|
REMARKS = "Remarks"
|
|
RETAIN = "Retain"
|
|
STOCKROOM = "StockRoom"
|
|
RESERVED = "Reserved"
|
|
LOTID = "LotID"
|
|
TQ1 = "TQ1"
|
|
TP1 = "TP1"
|
|
TQ2 = "TQ2"
|
|
TP2 = "TP2"
|
|
TQ3 = "TQ3"
|
|
TP3 = "TP3"
|
|
TOTALWEIGHT = "TotalWeight"
|
|
ORIGPRICE = "OrigPrice"
|
|
ORIGQTY = "OrigQty"
|
|
|
|
|
|
itemchildrencast = {
|
|
ItemChildren.COLORID: int,
|
|
ItemChildren.CATEGORYID: int,
|
|
ItemChildren.STATUS: _enum_ensurer(Status),
|
|
ItemChildren.QTY: int,
|
|
ItemChildren.PRICE: float,
|
|
ItemChildren.CONDITION: _enum_ensurer(Condition),
|
|
ItemChildren.SUBCONDITION: _enum_ensurer(SubCondition),
|
|
ItemChildren.ALTERNATE: bool,
|
|
ItemChildren.COUNTERPART: bool,
|
|
ItemChildren.BULK: int,
|
|
ItemChildren.SALE: int,
|
|
ItemChildren.RETAIN: bool,
|
|
ItemChildren.STOCKROOM: bool,
|
|
ItemChildren.LOTID: int,
|
|
ItemChildren.TQ1: int,
|
|
ItemChildren.TP1: float,
|
|
ItemChildren.TQ2: int,
|
|
ItemChildren.TP2: float,
|
|
ItemChildren.TQ3: int,
|
|
ItemChildren.TP3: float,
|
|
ItemChildren.ORIGPRICE: float,
|
|
ItemChildren.ORIGQTY: int,
|
|
}
|
|
|
|
|
|
class GuiStateAttribute(_enum.Enum):
|
|
APPLICATION = "Application"
|
|
VERSION = "Version"
|
|
|
|
|
|
class GuiStateChildren(_enum.Enum):
|
|
ITEMVIEW = "ItemView"
|
|
|
|
|
|
class ItemViewChildren(_enum.Enum):
|
|
COLUMNORDER = "ColumnOrder"
|
|
COLUMNWIDTHS = "ColumnWidths"
|
|
COLUMNWIDTHSHIDDEN = "ColumnWidthsHidden"
|
|
SORTCOLUMN = "SortColumn"
|
|
SORTDIRECTION = "SortDirection"
|
|
|
|
|
|
class Field(_enum.IntEnum):
|
|
# taken from BrickStock source (cdocument.h #44..)
|
|
Status = 0
|
|
Picture = 1
|
|
PartNo = 2
|
|
Description = 3
|
|
Condition = 4
|
|
Color = 5
|
|
Quantity = 6
|
|
Price = 7
|
|
Total = 8
|
|
Bulk = 9
|
|
Sale = 10
|
|
Comments = 11
|
|
Remarks = 12
|
|
Category = 13
|
|
ItemType = 14
|
|
TierQ1 = 15
|
|
TierP1 = 16
|
|
TierQ2 = 17
|
|
TierP2 = 18
|
|
TierQ3 = 19
|
|
TierP3 = 20
|
|
LotId = 21
|
|
Retain = 22
|
|
Stockroom = 23
|
|
Reserved = 24
|
|
Weight = 25
|
|
YearReleased = 26
|
|
QuantityOrig = 27
|
|
QuantityDiff = 28
|
|
PriceOrig = 29
|
|
PriceDiff = 30
|
|
|
|
|
|
class SortDirection(_enum.Enum):
|
|
Ascending = "A"
|
|
Descending = "D"
|
|
|
|
|
|
def dump(obj: dict, fp: _typing.io, *,
|
|
pretty_print = True,
|
|
) -> None:
|
|
s = dumps(obj, pretty_print = pretty_print)
|
|
if _is_binary(fp):
|
|
return fp.write(skip_decode = True)
|
|
else:
|
|
return fp.write(s)
|
|
|
|
|
|
def dumps(obj: dict, *,
|
|
pretty_print = True,
|
|
skip_decode = False,
|
|
) -> _typing.Union[str, bytes]:
|
|
root = obj2root(obj)
|
|
b = b'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
b += _etree.tostring(
|
|
root,
|
|
pretty_print = pretty_print,
|
|
xml_declaration = False,
|
|
encoding = XML_ENCODING,
|
|
doctype = XML_DOCTYPE,
|
|
)
|
|
if skip_decode:
|
|
return b
|
|
else:
|
|
s = b.decode(XML_ENCODING)
|
|
return s
|
|
|
|
|
|
def load(fp: _typing.io) -> dict:
|
|
tree = _etree.parse(fp)
|
|
root = tree.getroot()
|
|
return root2obj(root)
|
|
|
|
|
|
def loads(s: str) -> dict:
|
|
try:
|
|
root = _etree.fromstring(s)
|
|
except ValueError:
|
|
root = _etree.fromstring(s.encode(XML_ENCODING))
|
|
return root2obj(root)
|
|
|
|
|
|
def obj2root(obj: dict) -> _etree.Element:
|
|
root = _etree.Element(XML_ROOT_TAG)
|
|
inventory_o = obj.get(RootChildren.INVENTORY.value)
|
|
if inventory_o:
|
|
inventory_e = inventory_o2e(inventory_o)
|
|
root.append(inventory_e)
|
|
for guistate_o in obj.get(OBJ_GUISTATES, []):
|
|
root.append(guistate_o2e(guistate_o))
|
|
return root
|
|
|
|
|
|
def root2obj(root: _etree.Element) -> dict:
|
|
result = {}
|
|
inventory_e = root.find(RootChildren.INVENTORY.value)
|
|
if inventory_e is not None:
|
|
inventory_o = inventory_e2o(inventory_e)
|
|
if inventory_o:
|
|
result[RootChildren.INVENTORY.value] = inventory_o
|
|
guistate_seq = root.findall(RootChildren.GUISTATE.value)
|
|
if guistate_seq:
|
|
guistate_o_seq = []
|
|
for guistate_e in guistate_seq:
|
|
guistate_o = guistate_e2o(guistate_e)
|
|
if guistate_o:
|
|
guistate_o_seq.append(guistate_o)
|
|
if guistate_o_seq:
|
|
result[OBJ_GUISTATES] = guistate_o_seq
|
|
return result
|
|
|
|
|
|
def inventory_e2o(inventory_e: _etree.Element) -> list:
|
|
result = []
|
|
for item_e in inventory_e:
|
|
item_o = {}
|
|
for property_tag in ItemChildren.__members__.values():
|
|
property_name = property_tag.value
|
|
property_e = item_e.find(property_name)
|
|
if property_e is not None:
|
|
cast_func = itemchildrencast.get(property_tag, str)
|
|
if cast_func is bool:
|
|
item_o[property_name] = True
|
|
else:
|
|
property_v = cast_func(property_e.text)
|
|
item_o[property_name] = property_v
|
|
if item_o:
|
|
result.append(item_o)
|
|
return result
|
|
|
|
|
|
def inventory_o2e(inventory_o: dict) -> _etree.Element:
|
|
result = _etree.Element(RootChildren.INVENTORY.value)
|
|
for item_o in inventory_o:
|
|
item_e = _etree.Element(InventoryChildren.ITEM.value)
|
|
result.append(item_e)
|
|
for property_tag in ItemChildren.__members__.values():
|
|
property_v = item_o.get(property_tag.value)
|
|
if property_v is None:
|
|
continue
|
|
cast_func = itemchildrencast.get(property_tag, str)
|
|
if cast_func is bool and not property_v:
|
|
continue
|
|
property_e = _etree.Element(property_tag.value)
|
|
item_e.append(property_e)
|
|
if cast_func is float:
|
|
property_e.text = f'{cast_func(property_v):.3f}'
|
|
elif cast_func is not bool:
|
|
property_e.text = str(cast_func(property_v))
|
|
return result
|
|
|
|
|
|
def guistate_e2o(guistate_e: _etree.Element) -> dict:
|
|
result = {}
|
|
for prop in GuiStateAttribute.__members__.values():
|
|
if prop.value in guistate_e.attrib:
|
|
result[prop.value] = guistate_e.attrib[prop.value]
|
|
for e1 in guistate_e.iterchildren():
|
|
e1name = GuiStateChildren(e1.tag)
|
|
if e1name is GuiStateChildren.ITEMVIEW:
|
|
itemview_o = {}
|
|
for e2 in e1.iterchildren():
|
|
e2name = ItemViewChildren(e2.tag)
|
|
if e2name is ItemViewChildren.COLUMNORDER:
|
|
columorder_o = [
|
|
Field(int(s)).name
|
|
for s in e2.text.split(",")
|
|
]
|
|
itemview_o[e2name.value] = columorder_o
|
|
elif e2name in (
|
|
ItemViewChildren.COLUMNWIDTHS,
|
|
ItemViewChildren.COLUMNWIDTHSHIDDEN,
|
|
):
|
|
widths_o = {name: 0 for name in Field.__members__}
|
|
for i, s in enumerate(e2.text.split(",")):
|
|
widths_o[Field(i).name] = int(s)
|
|
itemview_o[e2name.value] = widths_o
|
|
elif e2name is ItemViewChildren.SORTCOLUMN:
|
|
itemview_o[e2name.value] = Field(int(e2.text)).name
|
|
elif e2name is ItemViewChildren.SORTDIRECTION:
|
|
f_ensure = _enum_ensurer(SortDirection)
|
|
itemview_o[e2name.value] = f_ensure(e2.text)
|
|
if itemview_o:
|
|
result[e1name.value] = itemview_o
|
|
return result
|
|
|
|
|
|
def guistate_o2e(guistate_o: dict) -> _etree.Element:
|
|
result = _etree.Element(RootChildren.GUISTATE.value)
|
|
for attribname in GuiStateAttribute.__members__.values():
|
|
if attribname.value in guistate_o:
|
|
value = guistate_o[attribname.value]
|
|
if value:
|
|
result.attrib[attribname.value] = str(value)
|
|
for e1name in GuiStateChildren.__members__.values():
|
|
if e1name.value not in guistate_o:
|
|
continue
|
|
obj1 = guistate_o[e1name.value]
|
|
e1 = _etree.Element(e1name.value)
|
|
result.append(e1)
|
|
if e1name is GuiStateChildren.ITEMVIEW:
|
|
columnwidths_key = ItemViewChildren.COLUMNWIDTHS.value
|
|
columswidths_obj = obj1.get(columnwidths_key, {})
|
|
for e2name in ItemViewChildren.__members__.values():
|
|
if e2name.value not in obj1:
|
|
continue
|
|
obj2 = obj1[e2name.value]
|
|
e2 = _etree.Element(e2name.value)
|
|
e1.append(e2)
|
|
if e2name is ItemViewChildren.COLUMNORDER:
|
|
e2.text = ",".join(
|
|
str(int(Field[name])) for name in obj2
|
|
)
|
|
elif e2name in (
|
|
ItemViewChildren.COLUMNWIDTHS,
|
|
ItemViewChildren.COLUMNWIDTHSHIDDEN,
|
|
):
|
|
vals2 = []
|
|
for field in Field.__members__.values():
|
|
val2 = obj2.get(field.name)
|
|
if val2 is None:
|
|
if e2name is ItemViewChildren.COLUMNWIDTHSHIDDEN:
|
|
val2 = columswidths_obj.get(field.name, 0)
|
|
else:
|
|
val2 = 0
|
|
vals2.append(val2)
|
|
e2.text = ",".join(str(int(v)) for v in vals2)
|
|
elif e2name is ItemViewChildren.SORTCOLUMN:
|
|
e2.text = str(int(Field[obj2]))
|
|
elif e2name is ItemViewChildren.SORTDIRECTION:
|
|
f_ensure = _enum_ensurer(SortDirection)
|
|
e2.text = f_ensure(obj2)
|
|
else:
|
|
e2.text = str(obj2)
|
|
return result
|