commit a425964e4a2318f4ba8acea313d4887bf3cbb87a Author: vuko Date: Sun May 31 15:11:08 2020 +0200 adding initial version diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..9a725fc --- /dev/null +++ b/README.rst @@ -0,0 +1,32 @@ +web interface for accessing Hackerspace electronic parts information stored on +`inventory system`_ + +.. _inventory system: https://wiki.hackerspace.pl/members:services:inventory + +Status +------ +There is only one webpage listing SMD resistors in `BOXALL`_ container. Other functionality is not +there yet. + +.. _BOXALL: http://aidetek.com/mm5/merchant.mvc?Screen=PROD&Store_Code=A&Product_Code=BOXALL&Category_Code=Encl + +TODO +---- + +* form for adding new resistor (requires access token) +* generating pdf version +* toggle between metric and imperial units +* other component types +* integration into inventory (module?) + +Running +------- +.. code:: bash + + # prepare virtual environment + python3 -m venv venv + ./venv/bin/pip install "${PATH_TO_REPO}" + ./venv/bin/pip install gunicorn + + # run application + ./venv/bin/gunicorn 'electronics_inventory.webapp:app()' diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..c25d6c2 --- /dev/null +++ b/default.nix @@ -0,0 +1,16 @@ +{ pkgs ? import {} }: + +pkgs.python3Packages.buildPythonPackage { + pname = "hs-electronics-inventory"; + version = "1.0"; + + src = ./.; + + propagatedBuildInputs = with pkgs; [ + python3Packages.jinja2 + python3Packages.requests + python3Packages.setuptools + python3Packages.flask + ]; +} + diff --git a/electronics_inventory/__init__.py b/electronics_inventory/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/electronics_inventory/components.py b/electronics_inventory/components.py new file mode 100644 index 0000000..cabd937 --- /dev/null +++ b/electronics_inventory/components.py @@ -0,0 +1,108 @@ +import decimal +import re +import enum +import collections + +from . import si + +unit_ord = ("full", "utf", "ascii") +UnitDef = collections.namedtuple("UnitDef", unit_ord) +unit_defs = { + UnitDef("ohm", "Ω", "R"), + UnitDef("volt", "V", "V"), + UnitDef("amper", "A", "A"), +} + +units_dicts = [] +for r in unit_ord: + d = dict() + for u in unit_defs: + d[getattr(u, r)] = u + units_dicts.append(d) + + +def symbol_to_unitdef(symbol): + for ud in units_dicts: + if symbol in ud: + return ud[symbol] + return None + + +class Component: + class Unit: + def __init__(self, unit): + if isinstance(unit, type(self)): + self.u = unit.u + elif isinstance(unit, UnitDef): + self.u = unit + else: + self.u = symbol_to_unitdef(unit) + if self.u is None: + raise ValueError("invalid unit") + + def __str__(self, _ord="utf"): + return getattr(self.u, _ord) + + class Value: + def __init__(self, value, unit=None): + self.unit = Component.Unit(unit) + if isinstance(value, str): + self.value = decode_infix(value) + else: + self.value = decimal.Decimal(value) + + def prefixed(self, replace_separator=True): + return si.prefix(self.value, replace_separator=replace_separator) + + def __str__(self, unit_repr="utf8"): + if self.unit is None: + unit = None + elif unit_repr == "utf8": + unit = self.unit.u.utf + elif unit_repr == "ascii": + unit = self.unit.u.ascii + elif unit_repr == "full": + unit = self.unit.u.full + else: + raise ValueError("Invalid unit_repr: {!r:s}".format(unit_repr)) + s = "{!s:s}".format(si.prefix(self.value)) + if unit is not None: + s += " {:s}".format(unit) + return s + + def __init__(self, value=None, package=None): + self.value = value + self.package = package + + def to_item(): + pass + + @classmethod + def from_item(cls, item): + if re.match("rezystor", item.name): + return Resistor.from_item(item) + + +class Resistor(Component): + def __init__(self, resistance, package, tolerance): + if isinstance(resistance, str): + self.resistance = si.decode_infix(resistance) + else: + self.resistance = decimal.Decimal(value) + self.tolerance = str(tolerance) + self.package = str(package) + + @property + def value(self): + return self.Value(self.resistance, "ohm") + + @classmethod + def from_item(cls, item): + m = re.match("rezystor ([^ ]+) ([^ ]+)", item.name) + tolerance = item.props.get("tolerance", "unknown") + return cls(m.group(1), m.group(2), tolerance) + + def __repr__(self): + return """""".format( + self.value, self.package, self.tolerance + ) diff --git a/electronics_inventory/resistors_boxall.html.jinja2 b/electronics_inventory/resistors_boxall.html.jinja2 new file mode 100644 index 0000000..4be6334 --- /dev/null +++ b/electronics_inventory/resistors_boxall.html.jinja2 @@ -0,0 +1,40 @@ + + + + + title + + + + +
+ {% for rt in columns %} +
+ + + + + + + + + + {% for r in rt %} + + + + + + + {% endfor %} + +
resistance [Ω]package
{{ r.compartment }}{{ r.resistance }}{{ r.package }}{{ r.tolerance }}
+
+ {% endfor %} +
+ + diff --git a/electronics_inventory/resistors_boxall.py b/electronics_inventory/resistors_boxall.py new file mode 100644 index 0000000..a8a0411 --- /dev/null +++ b/electronics_inventory/resistors_boxall.py @@ -0,0 +1,85 @@ +import collections +from dataclasses import dataclass +from jinja2 import Environment, FileSystemLoader +import math +import os +from typing import List, Tuple +from jinja2 import Template +from pkg_resources import resource_string + +from . import si +from .spaceventory import Inventory +from .components import Component + + +RESISTORS_BOXALL = "28f37f99-45b1-4e85-940e-06273f786e59" +TEMPLATE = Template(resource_string(__name__, "resistors_boxall.html.jinja2").decode()) + + +def fetch_container(inventory: Inventory, uuid) -> Tuple[str, Component]: + """fetch boxall resistors from inventory""" + l = [] + for c in inventory.get_children(uuid): + try: + item = Component.from_item(c) + except Exception: + logging.exception(f"parsing component {c} failed") + continue + l.append((item, c.props["compartment"])) + return sorted(l, key=lambda r: r[0].resistance) + + +@dataclass +class ResistorCompartment: + compartment: str + resistance: str + package: str + tolerance: str + + +def fetch_resistors(inventory: Inventory) -> List[ResistorCompartment]: + rs = fetch_container(inventory, RESISTORS_BOXALL) + compartments = [] + for resistor, compartment in rs: + value, prefix = si.prefix_tuple(resistor.value.value) + # unit = resistor.value.unit.u.utf + resistance = f"{value:.2f} {prefix}" + if resistor.tolerance != "unknown": + tolerance = resistor.tolerance + else: + tolerance = "" + compartments.append( + ResistorCompartment( + compartment=compartment, + resistance=resistance, + package=resistor.package, + tolerance=tolerance, + ) + ) + return compartments + + +def render(compartments: List[ResistorCompartment], column_count: int = 3) -> str: + # divide resistor list into columns + rows_count = math.ceil(len(compartments) / column_count) + columns = [ + compartments[i : i + rows_count] + for i in range(0, len(compartments), rows_count) + ] + + return TEMPLATE.render(columns=columns) + + +def print_resistors(): + import getpass + + token = os.environ.get("INVENTORY_TOKEN", getpass.getpass("Inventory token: ")) + compartments = fetch_resistors(Inventory(token=token)) + for cp in compartments: + print( + f"{cp.compartment:>4s} {cp.resistance:>8s} {cp.package:>4s} {cp.tolerance:>4s}" + ) + + +if __name__ == "__main__": + print_resistors() diff --git a/electronics_inventory/si.py b/electronics_inventory/si.py new file mode 100644 index 0000000..ce3dbec --- /dev/null +++ b/electronics_inventory/si.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 + +import math +import re +import decimal +from typing import Set + +si_table = { + -8: ("yocto", "y", "y", 10e-24), + -7: ("zepto", "z", "z", 10e-21), + -6: ("atto", "a", "a", 10e-18), + -5: ("femto", "f", "f", 10e-15), + -4: ("pico", "p", "p", 10e-12), + -3: ("nano", "n", "n", 10e-9), + -2: ("micro", "u", "μ", 10e-6), + -1: ("milli", "m", "m", 10e-3), + 0: ("", "", "", 0), + 1: ("kilo", "k", "k", 10e3), + 2: ("mega", "M", "M", 10e6), + 3: ("giga", "G", "G", 10e9), + 4: ("tera", "T", "T", 10e12), + 5: ("peta", "P", "P", 10e15), + 6: ("exa", "E", "E", 10e18), + 7: ("zetta", "Z", "Z", 10e21), + 8: ("yotta", "Y", "Y", 10e24), +} + +_si_letters_set: Set[str] = set() +for n in si_table: + _si_letters_set.add(si_table[n][1]) + _si_letters_set.add(si_table[n][2]) + +si_letters = "".join(sorted(_si_letters_set)) + + +def prefix_tuple(value, utf=True): + value = float(value) + ex = math.log(abs(value * 1.0000001), 10) + nex = math.floor(ex) // 3 + nval = value / 10 ** (nex * 3) + if nex not in si_table: + raise ValueError("No suitable SI prefix found") + if utf == True: + px = si_table[nex][2] + else: + px = si_table[nex][1] + return (nval, px) + + +def prefix(value, utf=True, replace_separator=True): + v, px = prefix_tuple(value, utf) + if px == "": + return "{:1.2f}".format(v).rstrip("0").rstrip(".") + elif replace_separator == True: + s = "{:1.2f}".format(v).replace(".", px).rstrip("0").rstrip(".") + return s + else: + s = "{:1.2f}".format(v).rstrip("0").rstrip(".") + return s + " {:s}".format(px) + + +def power_by_prefix(string): + for v in si_table: + if string in si_table[v]: + return v + raise ValueError() + + +def decode_infix(string): + np = "^(([1-9][0-9]*)?[0-9](\.[0-9]+)?) ?([{:s}])?$".format(si_letters) + ni = "^(([1-9][0-9]*)?[0-9](([{:s}])[0-9]+)?)$".format(si_letters) + rp = re.match(np, string) + ri = re.match(ni, string) + if rp is not None: + if rp.groups()[3] is not None: + power = power_by_prefix(rp.groups()[3]) + else: + power = 0 + base = decimal.Decimal(rp.groups()[0]) + return base * decimal.Decimal("10") ** (power * 3) + elif ri is not None: + base = decimal.Decimal(str(ri.groups()[0]).replace(ri.groups()[3], ".")) + power = power_by_prefix(ri.groups()[3]) + return base * decimal.Decimal("10") ** (power * 3) + else: + raise ValueError() + + +if __name__ == "__main__": + print(prefix(1.5632e11)) + print(prefix(12.32e-9)) + print(prefix(47000)) + print(prefix(4700)) + print(prefix(470)) + + print(si_letters) + print(decode_infix("0.23")) + print(decode_infix("470M")) + print(prefix(float(decode_infix("0k23")))) diff --git a/electronics_inventory/spaceventory.py b/electronics_inventory/spaceventory.py new file mode 100644 index 0000000..445e0d8 --- /dev/null +++ b/electronics_inventory/spaceventory.py @@ -0,0 +1,153 @@ +#!/usr/bin/python3 + +import requests +import json +import enum +import os +from typing import List, Dict, Union, Optional +from dataclasses import dataclass +from uuid import UUID + + +@dataclass +class Item: + class State(enum.Enum): + PRESENT = "present" + MISSING = "missing" + TAKEN = "taken" + + description: str + parent: str + uuid: str + labels: List[str] + state: State + props: Dict[str, str] + + def __init__( + self, + name, + description=None, + parent=None, + uuid=None, + state=State.PRESENT, + labels=None, + props={}, + ): + self.name = str(name) + + self.description = None + self.parent = None + self.uuid = None + self.labels = None + + if description is not None: + self.description = str(description) + if parent is not None: + self.parent = str(parent) + if uuid is not None: + self.uuid = str(uuid) + if labels is not None: + self.labels = [str(label) for label in labels] + + self.state = self.State(state) + self.props = dict(props) + + # def __repr__(self): + # return f"<{self.__class__.__name__!s}(id='{self.uuid[:8]!s}', name='{self.name}')>" + + @classmethod + def from_dict(cls, _dict: Dict[str, str]): + name = _dict["name"] + description = _dict["description"] + parent = _dict["parent"] + _uuid = _dict["uuid"] + state = _dict.get("state", cls.State.PRESENT) + props = _dict.get("props", dict()) + labels = _dict.get("labels", []) + return cls( + name=name, + description=description, + parent=parent, + uuid=_uuid, + state=state, + labels=labels, + props=props, + ) + + +API_URL = "https://inventory.waw.hackerspace.pl/api/1/" + + +class Inventory: + def __init__(self, token: Optional[str] = None, api_url: str = API_URL): + self.token = token + self.headers = { + "content-type": "application/json", + } + if token is not None: + self.headers["Authorization"] = f"Token {token}" + self.api_url = api_url + + def _check_response(self, r): + if not r.ok: + raise Exception(str(r.text)) + + def get(self, path): + ret = requests.get( + self.api_url + str(path) + "?format=json", headers=self.headers + ) + self._check_response(ret) + return ret.json() + + def post(self, path, data): + ret = requests.post( + self.api_url + str(path), data=json.dumps(data), headers=self.headers + ) + self._check_response(ret) + return ret.json() + + def put(self, path, data): + ret = requests.put( + self.api_url + str(path), data=json.dumps(data), headers=self.headers + ) + self._check_response(ret) + return ret.json() + + def patch(self, path, data): + ret = requests.patch( + self.api_url + str(path), data=json.dumps(data), headers=self.headers + ) + self._check_response(ret) + return ret.json() + + def delete(self, path): + ret = requests.delete(self.api_url + str(path), headers=self.headers) + self._check_response(ret) + + def delete_item(self, uuid): + ret = self.delete("items/{:s}".format(uuid)) + self._check_response(ret) + + def _get_uuid(self, i): + if isinstance(i, Item): + _uuid = i.uuid + elif isinstance(i, str): + _uuid = str(i) + else: + raise ValueError() + UUID(_uuid) + return _uuid + + def get_item(self, item: Union[str, Item]): + _uuid = self._get_uuid(item) + r = self.get("items/{:s}".format(_uuid)) + return Item.from_dict(r) + + def get_children(self, parent: Union[str, Item]): + """return list of children of item""" + _uuid = self._get_uuid(parent) + r = self.get("items/{:s}/children".format(_uuid)) + cs = [] + for c in r: + cs.append(Item.from_dict(c)) + return cs diff --git a/electronics_inventory/webapp.py b/electronics_inventory/webapp.py new file mode 100644 index 0000000..e6eac4c --- /dev/null +++ b/electronics_inventory/webapp.py @@ -0,0 +1,16 @@ +from flask import Flask, Response, jsonify +from .spaceventory import Inventory +from .resistors_boxall import fetch_resistors, render +import os + + +def app(): + app = Flask(__name__) + + @app.route("/index.html") + def resistors(): + token = os.environ.get("INVENTORY_TOKEN", None) + compartments = fetch_resistors(Inventory(token=token)) + return render(compartments) + + return app diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d453f38 --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +from setuptools import setup + +setup( + name="hs-electronics-inventory", + version="1.0", + description="simple interface for electronic components backed by spacestore api", + author="Jan Wiśniewski", + author_email="vuko@hackerspace.pl", + classifiers=[ + "License :: OSI Approved :: zlib/libpng License", + "Programming Language :: Python :: 3.7", + ], + packages=["electronics_inventory"], + python_requires=">=3.7,", + package_data={"electronics_inventory": ["resistors_boxall.html.jinja2"]}, + install_requires=["jinja2", "setuptools", "requests", "flask"], + entry_points={ + "console_scripts": [ + "hs-boxall=electronics_inventory.resistors_boxall:print_resistors" + ] + }, +)