wrolpi/modules/inventory/common.py
Roland 9b899f6fc0
A new start
Signed-off-by: Roland <roland@learningselfreliance.com>
2021-11-01 16:43:09 -06:00

182 lines
6 KiB
Python

from decimal import Decimal
from functools import partial
from pathlib import Path
from typing import List, Tuple
import yaml
from pint import Quantity
from wrolpi import before_startup
from wrolpi.common import logger, Base
from wrolpi.db import get_db_session
from wrolpi.errors import NoInventories, InventoriesVersionMismatch
from wrolpi.vars import CONFIG_DIR
from .inventory import unit_registry, get_items, get_inventories, increment_inventories_version, \
get_inventories_version
from .models import Inventory, Item
MY_DIR: Path = Path(__file__).parent
logger = logger.getChild(__name__)
def sum_by_key(items: List[Base], key: callable):
"""
Sum the total size of each item by the provided key function. Returns a dict containing the key, and the total_size
for that key. Combine total of like units. This means ounces and pounds will be in the same total.
"""
summed = dict()
for item in items:
k = key(item)
item_size, count, unit = item.item_size, item.count, unit_registry(item.unit)
key_dim = (k, unit.dimensionality)
total_size = item_size * count * unit
try:
summed[key_dim] += total_size
except KeyError:
summed[key_dim] = total_size
summed = {k: compact_unit(v) for k, v in summed.items()}
return summed
def get_inventory_by_keys(keys: Tuple, inventory_id: int):
items = get_items(inventory_id)
summed = sum_by_key(items, lambda i: tuple(getattr(i, k) or '' for k in keys))
inventory = []
for key, quantity in sorted(summed.items(), key=lambda i: i[0][0]):
quantity = cleanup_quantity(quantity)
total_size, units = quantity.to_tuple()
unit = units[0][0]
d = dict(total_size=total_size, unit=unit)
d.update(dict(zip(keys, key[0])))
inventory.append(d)
return inventory
get_inventory_by_category = partial(get_inventory_by_keys, ('category',))
get_inventory_by_subcategory = partial(get_inventory_by_keys, ('category', 'subcategory'))
get_inventory_by_name = partial(get_inventory_by_keys, ('brand', 'name'))
INVENTORY_UNITS = {
('ounce', 1): (16, unit_registry.pound),
('pound', 1): (2000, unit_registry.ton),
}
UNIT_PRECISION = 5
def compact_unit(quantity: unit_registry.Quantity) -> unit_registry.Quantity:
"""
Convert a Quantity to it's more readable format. Such as 2000 pounds to 1 ton.
"""
number, (units,) = quantity.to_tuple()
next_unit = INVENTORY_UNITS.get(units)
if not next_unit:
# No units after this one, return as is.
return round(quantity, UNIT_PRECISION)
max_quantity, next_unit = next_unit
if number >= max_quantity:
# Number is too high for this unit, move to the next unit.
return compact_unit(quantity.to(next_unit))
# No units were necessary, return as is.
quantity = round(quantity, UNIT_PRECISION)
return quantity
def quantity_to_tuple(quantity: unit_registry.Quantity) -> Tuple[Decimal, Quantity]:
decimal, (units,) = quantity.to_tuple()
unit, _ = units
quantity, unit = round(decimal, UNIT_PRECISION), unit_registry(unit)
return quantity, unit
def cleanup_quantity(quantity: Quantity) -> Quantity:
"""
Remove trailing zeros from a Quantity.
"""
num, unit = quantity_to_tuple(quantity)
num = round(num, UNIT_PRECISION)
num = str(num).rstrip('0').rstrip('.')
return Decimal(num) * unit
DEFAULT_SAVE_PATH = CONFIG_DIR / 'inventories.yaml'
def save_inventories_file(path: str = None):
"""
Write all inventories and their respective items to a YAML file.
"""
path: Path = Path(path) if path else DEFAULT_SAVE_PATH
inventories = []
for inventory in get_inventories():
inventories.append(inventory.dict())
if not inventories:
raise NoInventories('No Inventories are in the database!')
with increment_inventories_version() as version:
if path.is_file():
# Check that we aren't overwriting our inventories with empty inventories.
with open(path, 'rt') as fh:
old = yaml.load(fh, Loader=yaml.Loader)
if old and not inventories:
raise FileExistsError(f'Refusing to overwrite non-empty inventories.yaml with empty inventories.'
f' {path}')
if old and old.get('version') > version:
raise InventoriesVersionMismatch(
f'Inventories config version is {old["version"]} but DB version is {get_inventories_version()}')
with open(path, 'wt') as fh:
contents = dict(
version=version,
inventories=inventories,
)
yaml.dump(contents, fh)
@before_startup
def import_inventories_file(path: str = None):
path: Path = Path(path) if path else DEFAULT_SAVE_PATH
if not path.is_file():
logger.warning(f'No inventories config file at {path}')
return
with open(path, 'rt') as fh:
contents = yaml.load(fh, Loader=yaml.Loader)
if not contents or 'inventories' not in contents:
raise ValueError('Inventories file does not contain the expected "inventories" list.')
inventories = get_inventories()
inventories_names = {i.name for i in inventories}
new_inventories = [i for i in contents['inventories'] if i['name'] not in inventories_names]
with get_db_session(commit=True) as session:
for inventory in new_inventories:
items = inventory['items']
inventory = Inventory(
name=inventory['name'],
created_at=inventory['created_at'],
deleted_at=inventory['deleted_at'],
)
session.add(inventory)
# Get the Inventory from the DB so we can use it's ID.
session.flush()
session.refresh(inventory)
for item in items:
del item['inventory_id']
item = Item(inventory_id=inventory.id, **item)
item.inventory_id = inventory.id
session.add(item)