adding initial version

master
vuko 2020-05-31 15:11:08 +02:00
commit a425964e4a
10 changed files with 571 additions and 0 deletions

32
README.rst Normal file
View File

@ -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()'

16
default.nix Normal file
View File

@ -0,0 +1,16 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.python3Packages.buildPythonPackage {
pname = "hs-electronics-inventory";
version = "1.0";
src = ./.;
propagatedBuildInputs = with pkgs; [
python3Packages.jinja2
python3Packages.requests
python3Packages.setuptools
python3Packages.flask
];
}

View File

View File

@ -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 """<Resistor('{!s:s}', '{!s:s}', '{:s}')>""".format(
self.value, self.package, self.tolerance
)

View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>title</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<style type="text/css">
.compartment {
font-family: monospace;
}
</style>
</head>
<body>
<div class="container"><div class="row">
{% for rt in columns %}
<div class="col-sm">
<table class="table table-sm table-bordered table-striped" style="width: auto;">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">resistance [Ω]</th>
<th scope="col">package</th>
</tr>
</thead>
<tbody>
{% for r in rt %}
<tr class='text-right'>
<th scope="row" class="compartment">{{ r.compartment }}</th>
<td>{{ r.resistance }}</td>
<td>{{ r.package }}</td>
<td>{{ r.tolerance }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
</div> </div>
</body>
</html>

View File

@ -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()

View File

@ -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"))))

View File

@ -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

View File

@ -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

22
setup.py Normal file
View File

@ -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"
]
},
)