adding initial version
commit
a425964e4a
|
@ -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()'
|
|
@ -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
|
||||
];
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
|
@ -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>
|
|
@ -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()
|
|
@ -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"))))
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
]
|
||||
},
|
||||
)
|
Loading…
Reference in New Issue