wrolpi/modules/inventory/common.py

183 lines
6 KiB
Python
Raw Normal View History

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)