Publish to git

master
Cole Deck 1 month ago
commit bfebba3e9e

1
.gitignore vendored

@ -0,0 +1 @@
*.pyc

@ -0,0 +1,12 @@
FROM python:3.11-slim
# Get runtime dependencies
# glx for OpenCV, ghostscript for datasheet PDF rendering, zbar for barcode scanning, git for cloning repos
#RUN apt-get update && apt-get install -y libgl1-mesa-glx ghostscript libzbar0 git && apt-get clean && rm -rf /var/lib/apt/lists
COPY requirements.txt ./
#COPY config-server.yml config.yml
RUN pip3 install -r requirements.txt
COPY *.py *.txt *.toml ./
COPY inventory ./inventory
CMD ["rio", "run", "--release", "--public", "--port", "8000"]
EXPOSE 8000

@ -0,0 +1,26 @@
# inventory
This is a placeholder README for your project. Use it to describe what your
project is about, to give new users a quick overview of what they can expect.
_Inventory_ was created using [Rio](http://rio.dev/), an easy to
use app & website framework for Python._
This project is based on the `Multipage Website` template.
## Multipage Website
This is a simple website which shows off how to add multiple pages to your Rio
app. The website comes with a custom navbar that allows you to switch between
the different pages.
The navbar is placed inside a `rio.Overlay` component, which makes it hover
above all other components. It contains buttons to switch between pages, and
also displays the currently active page by highlighting the corresponding
button.
To avoid placing the app on each page individually, this app makes use of the
app's build method. That's right, build functions aren't just for components!
The app's build creates an instance of `RootPage`, which in turn displays the
navbar and a `rio.PageView`. The currently active page is then always displayed
inside of that page view.

@ -0,0 +1,298 @@
from peewee import *
from search import InventorySearch as ivs
db = MySQLDatabase('inventory', thread_safe=True, user='inventory', password='nfrwnfprifbwef', host='database', port=3306)
search = None
class user(Model):
name = CharField()
username = CharField(unique=True, primary_key=True)
password = CharField() # replace with AD or something!!
changepw = BooleanField(default=False)
class Meta:
database = db
legacy_table_names = False
class office(Model):
name = CharField(unique=True)
officeid = AutoField()
class Meta:
database = db
legacy_table_names = False
class location(Model):
name = CharField()
locationid = AutoField()
description = CharField(null=True)
parent = ForeignKeyField('self', null=True, backref="sublocations")
class Meta:
database = db
legacy_table_names = False
class item(Model):
loc = ForeignKeyField(location, backref="items_here", null=True)
office = ForeignKeyField(office, backref="items_here", null=True)
fullname = CharField(null=True)
description = CharField(null=True)
serial = CharField(null=True)
checkout = BooleanField(default=False)
checkout_user = ForeignKeyField(user, backref="items_held", null=True)
checkout_start = DateTimeField(null=True)
checkout_end = DateTimeField(null=True)
mac = CharField(null=True)
barcode = CharField(unique=True, primary_key=True)
fwver = CharField(null=True)
manufacturer = CharField()
last_user = ForeignKeyField(user, null=True) # remove null=True once user auth works
class Meta:
database = db
legacy_table_names = False
class component(Model):
owner = ForeignKeyField(item, backref="components")
name = CharField()
description = CharField(null=True)
barcode = CharField(unique=True, primary_key=True)
serial = CharField(null=True)
class Meta:
database = db
legacy_table_names = False
def init():
print("Connecting to database...")
db.connect()
print("Checking & creating tables...")
db.create_tables([location, office, item, component, user])
print("Database initialized.")
global search
print("Creating cache index...")
search = ivs()
add = item.select().dicts()
#print(add)
#print(type(add))
for itm in add:
print(itm)
#print(type(itm))
search.add_document(itm)
print("Cache build complete.")
def search_item(query, filters: dict={}):
#print(filters)
if len(filters) > 0:
filt = ""
for key, val in filters.items():
if key == "office":
# convert to integer!
if val != 'all':
if len(office.select().where(office.name == val).dicts()) > 0:
val2 = str(office.select().where(office.name == val).dicts()[0]['officeid'])
#print(val2)
else:
# office does not have any parts
val2 = str(999999)
filt += key + " = " + val2 + " AND "
else:
continue
else:
filt += key + " = " + val + " AND "
filt = filt[0:-4] # remove extra and
#print(filt)
return search.search(query, filt)["hits"]
else:
return search.search(query, "")["hits"]
def find_item(barcode):
return search.get_barcode(barcode)
def create_item(fullname, serial, officename, barcode, location=None, description=None, manufacturer=None, mac=None, fwver=None):
try:
off = office(name=officename)
off.save(force_insert=True)
except IntegrityError:
pass
try:
off = office.select().where(office.name == officename)[0]
itm = item(office=off, barcode=barcode, fullname=fullname, description=description, loc=location, serial=serial, mac=mac, fwver=fwver, manufacturer=manufacturer)
itm.save(force_insert=True)
search.add_document(item.select().where(item.barcode==barcode).dicts()[0])
print("item: " + itm.fullname)
return itm
except IntegrityError:
print("Duplicate item " + fullname)
return False
def delete_item(itm):
#item.delete(itm)
itm.delete_instance()
def item_location_str(itm):
try:
return itm.loc.name
except:
if itm.checkout:
return "Checked out to unknown location"
else:
return "Unknown"
def create_component(parentitem, name, barcode, serial=None, description=None):
itm = parentitem
try:
cmp = component(owner=itm, name=name, barcode=barcode, description=description, serial=serial)
cmp.save(force_insert=True)
print("component: " + cmp.name)
return cmp
except IntegrityError:
print("Duplicate component " + name)
return False
def get_item(barcode):
query = item.select().where(item.barcode == barcode)
if len(query) == 1:
return query[0]
# check if component exists
return get_component(barcode)
def get_component(barcode):
query = component.select().where(component.barcode == barcode)
if len(query) == 1:
return query[0]
return False
def create_user(name, username, password, changepw=False):
try:
usr = user(username=username, name=name, password=password, changepw=changepw)
usr.save(force_insert=True)
return usr
except IntegrityError:
print("User " + username + " already exists.")
return False
def change_password(username, password):
usr = get_user(username)
if usr:
usr.password = password
user.changepw = False
usr.save(force_insert=True)
return True
else:
return False
def checkout(user, barcode, loc=None):
itm = get_item(barcode)
if itm:
itm.checkout = True
itm.checkout_user = user
itm.loc = loc
itm.save()
return itm
else:
return False
def checkin(user, barcode, loc=None):
itm = get_item(barcode)
if itm:
itm.checkout = False
itm.last_user = user
itm.loc = loc
itm.save()
return itm
else:
return False
def create_location(name, parent=None):
if parent is not None:
loc = location(name=name, parent=parent)
loc.save()
return loc
else:
return False
def _find_parent(loc, parent):
if hasattr(loc, 'parent'):
if loc.parent.locationid == parent.locationid:
return True
else:
return _find_parent(loc.parent, parent)
else:
return False
def get_location(name, parent=None):
try:
query = location.select().where(location.name == name)
if parent is not None:
for loc in query:
if _find_parent(loc, parent):
return loc
return False
else:
if len(query) == 1:
return query[0]
else:
return False
except:
return False
def get_user(name):
query = user.select().where(user.username == name)
if len(query) == 1:
return query[0]
query = user.select().where(user.name == name)
if len(query) == 1:
return query[0]
return False
def user_login(username, password):
user = get_user(username)
if user:
return user.password == password
else:
return False
def test():
costa = create_user("Costa Aralis", "caralpwtwfpis", "12345")
costa.username = "caralis"
costa.save(force_insert=True)
office = location(name="Chicago CIC")
office.save()
shelf2a = location(name="Shelf 2A", parent=office)
shelf2a.save()
create_item("BRS50", "BRS50-00242Q2Q-STCY99HHSESXX.X.XX", "12345678", location=shelf2a)
create_item("BRS50", "BRS50-00242W2W-STCY99HHSESXX.X.XX", "123456789", location=office)
#brs50 = part(name="BRS50", description="it's a frickin BRS dude", quantity=1)
#brs50.save()
#mybrs = item(owner=brs50, fullname="BRS50-00242Q2Q-STCY99HHSESXX.X.XX", description="This one has 6 dead ports. RIP", loc=shelf2a, barcode="tlg4276p4dj85697")
#mybrs.save()
print("Querying...")
#query = part.select()
query = item.select().where(item.name == "BRS50")
for brs in query:
for itm in brs.items:
print(itm.fullname)
if __name__ == "__main__":
init()
test()

@ -0,0 +1,36 @@
services:
meilisearch:
image: "getmeili/meilisearch:v1.10.1"
ports:
- "7700:7700"
environment:
MEILI_MASTER_KEY: fluffybunnyrabbit
MEILI_NO_ANALYTICS: true
# volumes:
# - "meili_data:/meili_data"
database:
image: mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: lrnigwurbuwfbiowfnwrf
MYSQL_USER: inventory
MYSQL_PASSWORD: nfrwnfprifbwef
MYSQL_DATABASE: inventory
ports:
- "3306:3306"
volumes:
- '/root/db:/var/lib/mysql'
inventory:
build: .
init: true
ports:
- "8000:8000"
environment:
- PYTHONUNBUFFERED=1
depends_on:
- meilisearch
- database
volumes:
meili_data:

@ -0,0 +1,72 @@
from __future__ import annotations
from pathlib import Path
from typing import * # type: ignore
import rio
from . import pages
from . import components as comps
from db_classes import *
# Define a theme for Rio to use.
#
# You can modify the colors here to adapt the appearance of your app or website.
# The most important parameters are listed, but more are available! You can find
# them all in the docs
#
# https://rio.dev/docs/api/theme
theme = rio.Theme.from_colors(
secondary_color=rio.Color.from_hex("004990ff"),
primary_color=rio.Color.from_hex("004990ff"),
neutral_color=rio.Color.from_hex("b1cad8FF"),
light=True,
)
init()
# Create a dataclass that inherits from rio.UserSettings. This indicates to
# Rio that these are settings and should be persisted.
# Create the Rio app
app = rio.App(
name='Inventory',
default_attachments=[
comps.Settings(),
],
pages=[
rio.Page(
name="Home",
page_url='',
build=pages.BrowsePage,
),
rio.Page(
name="AboutPage",
page_url='about-page',
build=pages.AboutPage,
),
rio.Page(
name="AddPage",
page_url='add',
build=pages.AddPage,
),
rio.Page(
name="ItemPage",
page_url='item',
build=pages.ItemPage,
),
],
# You can optionally provide a root component for the app. By default,
# a simple `rio.PageView` is used. By providing your own component, you
# can create components which stay put while the user navigates between
# pages, such as a navigation bar or footer.
#
# When you do this, make sure your component contains a `rio.PageView`
# so the currently active page is still visible.
build=pages.RootPage,
theme=theme,
assets_dir=Path(__file__).parent / "assets",
)

@ -0,0 +1,5 @@
from .footer import Footer
from .navbar import Navbar
from .testimonial import Testimonial
from .item import Item
from .settings import Settings

@ -0,0 +1,32 @@
from __future__ import annotations
from dataclasses import KW_ONLY, field
from typing import * # type: ignore
import rio
from .. import components as comps
class Footer(rio.Component):
"""
A simple, static component which displays a footer with the company name and
website name.
"""
def build(self) -> rio.Component:
return rio.Card(
content=rio.Column(
rio.Icon("rio/logo:fill", width=5, height=5),
rio.Text("Buzzwordz Inc."),
rio.Text(
"Hyper Dyper Website",
style="dim",
),
spacing=1,
margin=2,
align_x=0.5,
),
color="hud",
corner_radius=0,
)

@ -0,0 +1,24 @@
from __future__ import annotations
from dataclasses import KW_ONLY, field
from typing import * # type: ignore
import rio
from .. import components as comps
from db_classes import *
class Item(rio.Component):
itemcode: str = ""
def build(self) -> rio.Component:
if 'current_item' in self.session:
self.itemcode = self.session['current_item']
return rio.Card(
rio.Markdown(
self.markdown,
margin=2,
)
)

@ -0,0 +1,163 @@
from __future__ import annotations
from dataclasses import KW_ONLY, field
from typing import * # type: ignore
import rio
from .. import components as comps
class Navbar(rio.Component):
"""
A navbar with a fixed position and responsive width.
"""
# Make sure the navbar will be rebuilt when the app navigates to a different
# page. While Rio automatically detects state changes and rebuilds
# components as needed, navigating to other pages is not considered a state
# change, since it's not stored in the component.
#
# Instead, we can use Rio's `on_page_change` event to trigger a rebuild of
# the navbar when the page changes.
@rio.event.on_page_change
async def on_page_change(self) -> None:
# Rio comes with a function specifically for this. Whenever Rio is
# unable to detect a change automatically, use this function to force a
# refresh.
self.office = self.session[comps.Settings].office
#print(self.office)
await self.force_refresh()
checkpage: str = "in"
office: str = ""
def sub_page(self, event: rio.DropdownChangeEvent):
page = event.value
self.session.navigate_to("/" + page)
self.checkpage = "n/a"
def set_office(self, event: rio.DropdownChangeEvent):
settings = self.session[comps.Settings]
settings.office = event.value
self.session.attach(self.session[comps.Settings])
#print(settings.office)
self.office = event.value
@rio.event.on_populate
def set_office_init(self):
self.office = self.session[comps.Settings].office
print(self.office)
def build(self) -> rio.Component:
# Which page is currently active? This will be used to highlight the
# correct navigation button.
#
# `active_page_instances` contains the same `rio.Page` instances that
# you've passed the app during creation. Since multiple pages can be
# active at a time (e.g. /foo/bar/baz), this is a list.
active_page = self.session.active_page_instances[0]
active_page_url_segment = active_page.page_url
# The navbar should appear above all other components. This is easily
# done by using a `rio.Overlay` component.
return rio.Overlay(
rio.Row(
rio.Spacer(),
# Use a card for visual separation
rio.Rectangle(
content=rio.Row(
# Links can be used to navigate to other pages and
# external URLs. You can pass either a simple string, or
# another component as their content.
rio.Link(
rio.Button(
"Browse",
icon="material/info",
style=(
"major"
if active_page_url_segment == ""
else "plain"
),
),
"/",
),
# This spacer will take up any superfluous space,
# effectively pushing the subsequent buttons to the
# right.
rio.Spacer(),
# By sticking buttons into a `rio.Link`, we can easily
# make the buttons navigate to other pages, without
# having to write an event handler. Notice how there is
# no Python function called when the button is clicked.
rio.Dropdown(
options={
"ALL": "all",
"US-CHI": "us-chi",
"US-SC": "us-sc",
"DE-NT": "de-nt",
"CN-SHA": "cn-sha",
"IN-BAN": "in-ban"
},
on_change=self.set_office,
selected_value=self.bind().office,
),
rio.Dropdown(
options={
"Scan...": "n/a",
"Check in": "in",
"Check out": "out"
},
on_change=self.sub_page,
selected_value=self.bind().checkpage,
),
rio.Link(
rio.Button(
"Add",
icon="material/news",
style=(
"major"
if active_page_url_segment == "add"
else "plain"
),
),
"/add",
),
# Same game, different button
rio.Link(
rio.Button(
"About",
icon="material/info",
style=(
"major"
if active_page_url_segment == "about-page"
else "plain"
),
),
"/about-page",
),
spacing=1,
margin=1,
),
fill=self.session.theme.neutral_color,
corner_radius=self.session.theme.corner_radius_medium,
shadow_radius=0.8,
shadow_color=self.session.theme.shadow_color,
shadow_offset_y=0.2,
),
rio.Spacer(),
# Proportions are an easy way to make the navbar's size relative
# to the screen. This assigns 5% to the first spacer, 90% to the
# navbar, and 5% to the second spacer.
proportions=(0.5, 9, 0.5),
# Overlay assigns the entire screen to its child component.
# Since the navbar isn't supposed to take up all space, assign
# an alignment.
align_y=0,
margin=2,
)
)

@ -0,0 +1,14 @@
from __future__ import annotations
from dataclasses import KW_ONLY, field
from typing import * # type: ignore
import rio
from .. import components as comps
class Settings(rio.UserSettings):
language: str = "en"
office: str = "us-chi"
selected_item: str = ""

@ -0,0 +1,56 @@
from __future__ import annotations
from dataclasses import KW_ONLY, field
from typing import * # type: ignore
import rio
from .. import components as comps
class Testimonial(rio.Component):
"""
Displays 100% legitimate testimonials from real, totally not made-up people.
"""
# The quote somebody has definitely said about this company.
quote: str
# Who said the quote, probably Mark Twain.
name: str
# The company the person is from.
company: str
def build(self) -> rio.Component:
# Wrap everything in a card to make it stand out from the background.
return rio.Card(
# A second card, but this one is offset a bit. This allows the outer
# card to pop out a bit, displaying a nice colorful border at the
# bottom.
rio.Card(
# Combine the quote, name, and company into a column.
rio.Column(
rio.Markdown(self.quote),
rio.Text(
f"{self.name}",
justify="left",
),
rio.Text(
f"{self.company}",
# Dim text and icons are used for less important
# information and make the app more visually appealing.
style="dim",
justify="left",
),
spacing=0.4,
margin=2,
align_y=0.5,
),
margin_bottom=0.2,
),
# Important colors such as primary, secondary, neutral and
# background are available as string constants for easy access.
color="primary",
width=20,
)

@ -0,0 +1,7 @@
from .root_page import RootPage
from .about_page import AboutPage
from .add_page import AddPage
from .browse_page import BrowsePage
from .login_page import LoginPage
from .item_page import ItemPage

@ -0,0 +1,63 @@
from __future__ import annotations
from dataclasses import KW_ONLY, field
from typing import * # type: ignore
import rio
from .. import components as comps
class AboutPage(rio.Component):
"""
A sample page, which displays a humorous description of the company.
"""
def build(self) -> rio.Component:
return rio.Markdown(
"""
# About Us
Welcome to Buzzwordz Inc.! Unleashing Synergistic Paradigms for Unprecedented
Excellence since the day after yesterday.
## About Our Company
At buzzwordz, we are all talk and no action. Our mission is to be the vanguards
of industry-leading solutions, leveraging bleeding-edge technologies to catapult
your business into the stratosphere of success. Our unparalleled team of ninjas,
gurus, and rockstars is dedicated to disrupting the status quo and actualizing
your wildest business dreams. We live, breathe, and eat operational excellence
and groundbreaking innovation.
## Synergistic Consulting
Unlock your business's quantum potential with our bespoke, game-changing
strategies. Our consulting services synergize cross-functional paradigms to
create a holistic ecosystem of perpetual growth and exponential ROI. Did I
mention paradigm-shifts? We've got those too.
## Agile Hyper-Development
We turn moonshot ideas into reality with our agile, ninja-level development
techniques. Our team of coding wizards crafts robust, scalable, and future-proof
solutions that redefine industry standards. 24/7 Proactive Hyper-Support
Experience next-gen support that anticipates your needs before you do. Our
omnipresent customer happiness engineers ensure seamless integration,
frictionless operation, and infinite satisfaction, day and night.
Embark on a journey of transformational growth and stratospheric success. Don't
delay, give us your money today.
Phone: (123) 456-7890
Email: info@yourwebsite.com
Address: 123 Main Street, City, Country
""",
width=60,
margin_bottom=4,
align_x=0.5,
align_y=0,
)

@ -0,0 +1,247 @@
from __future__ import annotations
from dataclasses import KW_ONLY, field
from typing import * # type: ignore
import rio
import datetime
from mac_vendor_lookup import AsyncMacLookup
from db_classes import *
from .. import components as comps
class AddPage(rio.Component):
partnum: str = ""
mac: str = ""
serial: str = ""
fwver: str = ""
code: str = ""
popup_message: str = ""
popup_show: bool = False
popup_color: str = 'warning'
date: datetime.date = datetime.date.today()
tz: datetime.tzinfo = datetime.tzinfo()
time: datetime.datetime = datetime.datetime.now()
time_start: str = datetime.datetime.now().strftime(format="%H:%M")
macvendor: str = ""
manu: str = ""
manufield: str = ""
office: str = ""
@rio.event.periodic(1)
def set_office_init(self):
self.office = self.session[comps.Settings].office
#print("Populated:", self.office)
async def check_mac(self,mac):
print("Checking", mac)
try:
macvendor = await AsyncMacLookup().lookup(mac)
if self.manufield == "" or self.manufield == self.macvendor: # blank or set by MAC already
self.manu = macvendor
self.manufield = macvendor
self.macvendor = macvendor
#print(macvendor)
except:
pass
# not valid MAC?
async def check_all(self):
await self.check_mac(self.mac)
# check part number
# lookup in PL_Export_rel
async def add_part(self):
await self.check_all()
if self.code == "":
# FAIL
self.popup_message = "\n Missing barcode! \n\n"
self.popup_show = True
self.popup_color = 'danger'
else:
# OK, add part
if create_item(self.partnum, self.serial, self.office, self.code, location=None, description=None, manufacturer=self.manu, mac=self.mac, fwver=self.fwver) == False:
self.popup_message = "\n Duplicate barcode! \n\n"
self.popup_show = True
self.popup_color = 'warning'
else:
self.popup_message = "\n Part added! \n\n"
self.popup_show = True
self.popup_color = 'success'
self.name: str = ""
self.partnum: str = ""
self.mac: str = ""
self.serial: str = ""
self.fwver: str = ""
self.code: str = ""
self.macvendor: str = ""
self.manu: str = ""
self.manufield: str = ""
async def _add_part_enter(self, event: rio.TextInputConfirmEvent):
await self.add_part()
async def _add_part_button(self):
await self.add_part()
@rio.event.periodic(1)
def update_time_view(self):
self.time_start = self.session.timezone.fromutc(datetime.datetime.now()).now().strftime(format="%m/%d/%Y %H:%M:%S")
self.time = datetime.datetime.now()
def _set_time(self, event: rio.TextInputChangeEvent):
time_str = event.text
try:
time_obj = datetime.datetime.strptime(time_str, "%H:%M").time()
dt_with_time = datetime.datetime.combine(self.date, time_obj)
dt_with_tz = dt_with_time.replace(tzinfo=self.session.timezone)
self.time = dt_with_tz
#event.text =
#print(self.time)
except ValueError:
pass
async def _update_mac(self, event: rio.TextInputChangeEvent):
await self.check_mac(event.text)
def _update_partnum(self, event: rio.TextInputChangeEvent):
def __find_hm_header(txt):
searchlist = ["RSPS", "RSPE", "RSP", "RSB", "LRS", "RS", "OS", "RED", "MSP", "MSM", "MS", "MM", "EESX", "EES", "OZD", "OBR"]
for header in searchlist:
if txt.find(header) >= 0:
return txt.find(header) + len(header) + 2
def __find_header_general(txt):
acount = 0
ncount = 0
dash = False
for char in txt:
if char == '-':
dash = True
break
if char.isdigit():
ncount += 1
if char.isalpha():
acount += 1
if dash and acount <= 5 and ncount <= 5:
return acount+ncount
return -1
def __find_hm_fwver(txt):
a = txt.find(".")
if a > 5:
return a-2
return -1
def __find_hm_fwmode(txt):
a = __find_hm_fwver(txt)
#print(a)
if txt.find("BRS") == 0:
a -= 1
if txt[a] in ['S', 'A']:
return txt[a]
a = __find_hm_fwver(txt)
if txt.find("GRS") == 0:
a -= 4
if txt[a:a+2] in ['2S', '2A', '3S', '3A']: # 1040
return txt[a:a+2]
elif txt[a+2:a+4] in ['2S', '2A', '3S', '3A']: # 1020/30
return txt[a+2:a+4]
a = __find_hm_fwver(txt)
if txt.find("RSP") == 0 or txt.find("OS") == 0 or txt.find("MSP") == 0 or txt.find("EESX") == 0 or txt.find("RED"):
a -= 2
if txt[a:a+2] in ['2S', '2A', '3S', '3A']:
return txt[a:a+2]
a = __find_hm_fwver(txt)
if txt.find("RS") == 0 or txt.find("MS") == 0:
a -= 3
print(txt,txt[a])
if txt[a] in ['P', 'E']:
return txt[a]
a = __find_hm_fwver(txt)
if txt.find("EAGLE") == 0:
a -= 2
if txt[a:a+2] in ['3F', 'MB', 'IN', 'UN', 'OP', '01', 'SU', 'NF']:
return txt[a:a+2]
print("Failed to match software level", repr(txt))
return ""
pn = event.text
self.name = ""
if __find_header_general(pn) >= 0:
# hirschmann switch detected
self.name = pn[0:__find_header_general(pn)]
if __find_hm_fwver(pn) >= 0:
self.fwver = pn[__find_hm_fwver(pn):]
if len(__find_hm_fwmode(pn)) > 0:
self.fwver += " SWL-" + __find_hm_fwmode(pn)
def build(self) -> rio.Component:
return rio.Column(
rio.Popup(
anchor=rio.Text(
text="Add a part below:",
style='heading1',
align_x = 0.5
),
color=self.bind().popup_color,
is_open=self.bind().popup_show,
content=rio.Text(
text=self.bind().popup_message,
),
),
rio.TextInput(
label="Barcode",
text=self.bind().code
),
rio.TextInput(
label="Full part number",
text=self.bind().partnum,
on_change=self._update_partnum
),
rio.TextInput(
label="Serial",
text=self.bind().serial
),
rio.TextInput(
label="MAC",
text=self.bind().mac,
on_change=self._update_mac
),
rio.TextInput(
label="Manufacturer",
text=self.bind().manufield
),
rio.TextInput(
label="FW Ver",
text=self.bind().fwver,
on_confirm=self._add_part_enter
),
rio.Row(
# rio.DateInput(
# label="Timestamp",
# value=self.bind().date
# ),
rio.TextInput(
#text=
is_sensitive=False,
text = self.bind().time_start,
#on_change=self._set_time
)
),
rio.Button(
content="Add",
on_press=self._add_part_button
),
spacing=2,
width=60,
margin_bottom=4,
align_x=0.5,
align_y=0,
)

@ -0,0 +1,71 @@
from __future__ import annotations
from dataclasses import KW_ONLY, field
from typing import * # type: ignore
import rio
from .. import components as comps
from db_classes import *
import functools
class BrowsePage(rio.Component):
searchtext: str = ""
items: dict = {}
office: str = ""
filters: dict = {}
@rio.event.on_populate
async def _search(self, query=searchtext):
self.office = self.session[comps.Settings].office
self.filters['office'] = self.office
self.items = search_item(query, self.filters)
await self.force_refresh()
@rio.event.periodic(1)
async def set_office_init(self):
if self.office != self.session[comps.Settings].office:
self.office = self.session[comps.Settings].office
#print(self.office)
await self._search()
async def _search_trigger(self, event: rio.TextInputChangeEvent):
await self._search(event.text)
def click_item(self, code):
self.session[comps.Settings].selected_item = code
self.session.attach(self.session[comps.Settings])
self.session.navigate_to("/item")
def build(self) -> rio.Component:
searchview: rio.ListView = rio.ListView(height='grow')
for item in self.items:
if item["loc"] is not None:
loc = item["loc"]["name"]
else:
loc = ""
if item["checkout"]:
checkout = item["checkout_user"] + " - " + loc
else:
checkout = loc
searchview.add(rio.SimpleListItem(text=item["fullname"],secondary_text=(item["manufacturer"] + " - Serial: " + item["serial"] + "\n" + checkout), on_press=functools.partial(
self.click_item,
code=item["barcode"])))
return rio.Column(
rio.Row(
rio.TextInput(
text=self.bind().searchtext,
on_change=self._search_trigger,
label="Search"
)
),
searchview,
spacing=2,
width=60,
align_x=0.5,
align_y=0,
)

@ -0,0 +1,101 @@
from __future__ import annotations
from dataclasses import KW_ONLY, field
from typing import * # type: ignore
import rio
from .. import components as comps
from db_classes import *
import functools
class ItemPage(rio.Component):
itm: dict = {}
barcode: str = ""
@rio.event.on_populate
async def _get(self):
self.barcode = self.session[comps.Settings].selected_item
self.itm = find_item(self.barcode)
#print(find_item(self.barcode))
#print(self.itm)
await self.force_refresh()
def build(self) -> rio.Component:
if 'barcode' in self.itm:
if self.itm["loc"] is not None:
loc = self.itm["loc"]["name"]
else:
loc = ""
if self.itm["checkout"]:
checkout = self.itm["checkout_user"] + " - " + loc
else:
checkout = loc
# searchview.add(rio.SimpleListItem(text=item["fullname"],secondary_text=(item["manufacturer"] + " - Serial: " + item["serial"] + "\n" + checkout), on_press=functools.partial(
# self.click_item,
# file=item["barcode"])))
return rio.Column(
rio.Text(
text=str(self.itm["fullname"]),
style='heading1',
align_x = 0.5
),
rio.Text(
text=str(self.itm["manufacturer"]),
style='heading2',
align_x = 0.5
),
rio.Text(
text="Serial: " + str(self.itm["serial"]),
style='heading2',
align_x = 0.5
),
rio.Text(
text="MAC: " + str(self.itm["mac"]),
style='heading2',
align_x = 0.5
),
rio.Text(
text="FW Version: " + str(self.itm["fwver"]),
style='heading2',
align_x = 0.5
),
rio.Text(
text="Location: " + str(checkout),
style='heading2',
align_x = 0.5
),
rio.Text(
text="Checked out?: " + str(self.itm["checkout"]),
style='heading2',
align_x = 0.5
),
rio.Text(
text="Checkout start/end: " + str(self.itm["checkout_start"]) + " to " + str(self.itm["checkout_end"]),
style='heading2',
align_x = 0.5
),
rio.Text(
text="Office: " + str(self.itm["office"]),
style='heading2',
align_x = 0.5
),
rio.Text(
text="Barcode: " + str(self.itm["barcode"]),
style='heading2',
align_x = 0.5
),
rio.Text(
text="Description: " + str(self.itm["description"]),
style='heading2',
align_x = 0.5,
),
spacing=2,
width=60,
align_x=0.5,
align_y=0,
)
else:
return rio.Text("This item does not exist!")

@ -0,0 +1,49 @@
from __future__ import annotations
from dataclasses import KW_ONLY, field
from typing import * # type: ignore
import rio
import datetime
from .. import components as comps
class LoginPage(rio.Component):
name: str = ""
def build(self) -> rio.Component:
return rio.Column(
rio.Popup(
anchor=rio.Text(
text="Login",
style='heading1',
align_x = 0.5
),
color=self.bind().popup_color,
is_open=self.bind().popup_show,
content=rio.Text(
text=self.bind().popup_message,
),
),
rio.TextInput(
label="User",
text=self.bind().code
),
rio.TextInput(
label="Password",
text=self.bind().partnum
),
rio.Button(
content="Login",
on_press=self._add_part_button
),
spacing=2,
width=60,
margin_bottom=4,
align_x=0.5,
align_y=0,
)

@ -0,0 +1,39 @@
from __future__ import annotations
from dataclasses import KW_ONLY, field
from typing import * # type: ignore
import rio
from .. import components as comps
class RootPage(rio.Component):
"""
This page will be used as the root component for the app. This means, that
it will always be visible, regardless of which page is currently active.
This makes it the perfect place to put components that should be visible on
all pages, such as a navbar or a footer.
Additionally, the root page will contain a `rio.PageView`. Page views don't
have any appearance on their own, but they are used to display the content
of the currently active page. Thus, we'll always see the navbar and footer,
with the content of the current page in between.
"""
def build(self) -> rio.Component:
return rio.Column(
# The navbar contains a `rio.Overlay`, so it will always be on top
# of all other components.
comps.Navbar(),
# Add some empty space so the navbar doesn't cover the content.
rio.Spacer(height=10),
# The page view will display the content of the current page.
rio.PageView(
# Make sure the page view takes up all available space.
height="grow",
),
# The footer is also common to all pages, so place it here.
comps.Footer(),
)

@ -0,0 +1,6 @@
peewee
pymysql
flask
rio-ui
meilisearch
mac-vendor-lookup

@ -0,0 +1,6 @@
# This is the configuration file for Rio,
# an easy to use app & web framework for Python.
[app]
main-module = "inventory"
app-type = "website"

@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Interactions with the Meilisearch API for adding and searching cables."""
from meilisearch import Client
from meilisearch.task import TaskInfo
from meilisearch.errors import MeilisearchApiError
import time
DEFAULT_URL = "http://meilisearch:7700"
DEFAULT_APIKEY = "fluffybunnyrabbit" # I WOULD RECOMMEND SOMETHING MORE SECURE
DEFAULT_INDEX = "items"
DEFAULT_FILTERABLE_ATTRS = ["office", "loc", "fullname", "serial", "checkout_user", "checkout", "barcode", "manufacturer"] # default filterable attributes
class InventorySearch:
"""Class for interacting with the Meilisearch API."""
def __init__(self,
url: str = None,
api_key: str = None,
index: str = None,
filterable_attrs: list = None):
"""Connect to Meilisearch and perform first-run tasks as necessary.
:param url: Address of the Meilisearch server. Defaults to ``http://localhost:7700`` if unspecified.
:param api_key: API key used to authenticate with Meilisearch. It is highly recommended to set this as something
secure if you can access this endpoint publicly, but you can ignore this and set Meilisearch's default API key
to ``fluffybunnyrabbit``.
:param index: The name of the index to configure. Defaults to ``cables`` if unspecified.
:param filterable_attrs: List of all the attributes we want to filter by."""
# connect to Meilisearch
url = url or DEFAULT_URL
api_key = api_key or DEFAULT_APIKEY
filterable_attrs = filterable_attrs or DEFAULT_FILTERABLE_ATTRS
self.index = index or DEFAULT_INDEX
self.client = Client(url, api_key)
# create the index if it does not exist already
try:
self.client.get_index(self.index)
self.client.delete_index(self.index)
time.sleep(0.05)
self.client.create_index(self.index, {'primaryKey': 'barcode'})
time.sleep(0.05)
except MeilisearchApiError as _:
self.client.create_index(self.index, {'primaryKey': 'barcode'})
# make a variable to easily reference the index
self.idxref = self.client.index(self.index)
time.sleep(0.05)
# update filterable attributes if needed
self.idxref.update_distinct_attribute('barcode')
self.update_filterables(filterable_attrs)
time.sleep(0.05)
def add_document(self, document: dict) -> TaskInfo:
"""Add a cable to the Meilisearch index.
:param document: Dictionary containing all the cable data.
:returns: A TaskInfo object for the addition of the new document."""
return self.idxref.add_documents(document)
def add_documents(self, documents: list):
"""Add a list of cables to the Meilisearch index.
:param documents: List of dictionaries containing all the cable data.
:returns: A TaskInfo object for the last new document."""
taskinfo = None
for i in documents:
taskinfo = self.add_document(i)
return taskinfo
def update_filterables(self, filterables: list):
"""Update filterable attributes and wait for database to fully index. If the filterable attributes matches the
current attributes in the database, don't update (saves reindexing).
:param filterables: List of all filterable attributes"""
#existing_filterables = self.idxref.get_filterable_attributes()
#if len(set(existing_filterables).difference(set(filterables))) > 0:
taskref = self.idxref.update_filterable_attributes(filterables)
#self.client.wait_for_task(taskref.index_uid)
def search(self, query: str, filters: str = None):
"""Execute a search query on the Meilisearch index.
:param query: Seach query
:param filters: A meilisearch compatible filter statement.
:returns: The search results dict. Actual results are in a list under "hits", but there are other nice values that are useful in the root element."""
if filters:
q = self.idxref.search(query, {"filter": filters})
else:
q = self.idxref.search(query)
return q
def _filter_one(self, filter: str):
"""Get the first item to match a filter.
:param filter: A meilisearch compatible filter statement.
:returns: A dict containing the results; If no results found, an empty dict."""
q = self.search("", filter)
if q["estimatedTotalHits"] != 0:
return q["hits"][0]
else:
return dict()
def get_barcode(self, barcode: str):
"""Get a specific barcode.
:param uuid: The barcode to search for."""
return self._filter_one(f"barcode = {barcode}")
# entrypoint
if __name__ == "__main__":
jbs = InventorySearch()
Loading…
Cancel
Save