Merry Chipmunks

This commit is contained in:
Reid 'arrdem' McKenzie 2024-12-26 03:33:13 -07:00
parent cd28e3dafe
commit adf58a2b7d
18 changed files with 878 additions and 253 deletions

1
.bazelignore Normal file
View file

@ -0,0 +1 @@
execroot

5
.gitignore vendored
View file

@ -25,3 +25,8 @@ tmp/
/**/config*.toml
/**/config*.yml
MODULE.bazel.lock
*.tar.zst
*.zapp
.BUILDINFO
.MTREE
.PKGINFO

View file

@ -0,0 +1,56 @@
#!/usr/bin/env python3
import sys
import logging
import socket
import socketserver
from pathlib import Path
from subprocess import check_output
import shlex
import os
logging.basicConfig(level=logging.INFO)
CONFIG = None
class RequestHandler(socketserver.BaseRequestHandler):
def setup(self) -> None:
logging.info("Start request.")
def handle(self) -> None:
conn = self.request
while True:
data = conn.recv(1024).decode("utf-8")
if not data:
break
data = shlex.split(data)
logging.info(f"recv: {data!r}")
if command_allowed(data):
resp = check_output()
conn.sendall(resp)
else:
logging.error("Invalid request %r", data)
def finish(self) -> None:
logging.info("Finish request.")
class Server(socketserver.ThreadingUnixStreamServer):
def server_activate(self) -> None:
logging.info("Server started on %s", self.server_address)
super().server_activate()
if __name__ == "__main__":
socket_path = Path(sys.argv[1])
if socket_path.exists():
socket_path.unlink()
with Server(str(socket_path), RequestHandler) as server:
server.serve_forever()

View file

@ -0,0 +1,7 @@
py_project(
name = "lcdbackpack",
lib_deps = [
py_requirement("click"),
py_requirement("pyserial"),
],
)

View file

@ -0,0 +1,347 @@
#!/usr/bin/env python3
import os
import serial
import sys
from time import sleep
from typing import Optional
import click
class LcdBackpack:
"""
The LcdBackpack class exposes the commands available on the Adafruit LCD USB/Serial Backpack via simple methods.
"""
# Quoting from the official firmware implementation -
CHANGESPLASH = 0x40 # COL * ROW chars!
DISPLAY_ON = 0x42 # backlight. 1 argument afterwards, in minutes
AUTOWRAPLINE_ON = 0x43
AUTOWRAPLINE_OFF = 0x44
DISPLAY_OFF = 0x46
SETCURSOR_POSITION = 0x47 # 2 args: col, row
SETCURSOR_HOME = 0x48
UNDERLINECURSOR_ON = 0x4A
UNDERLINECURSOR_OFF = 0x4B
MOVECURSOR_BACK = 0x4C
MOVECURSOR_FORWARD = 0x4D
CUSTOM_CHARACTER = 0x4E # 9 args: char #, 8 bytes data
SET_CONTRAST = 0x50 # 1 arg
AUTOSCROLL_ON = 0x51
AUTOSCROLL_OFF = 0x52
BLOCKCURSOR_ON = 0x53
BLOCKCURSOR_OFF = 0x54
GPO_OFF = 0x56
GPO_ON = 0x57
CLEAR = 0x58
SETSAVE_CONTRAST = 0x91 # 1 arg
SET_BRIGHTNESS = 0x99 # 1 arg: scale
SETSAVE_BRIGHTNESS = 0x98 # 1 arg: scale
LOADCUSTOMCHARBANK = 0xC0 # 9 args: char #, 8 bytes data
SAVECUSTOMCHARBANK = 0xC1 # 9 args: char #, 8 bytes data
GPO_START_ONOFF = 0xC3
RGBBACKLIGHT = 0xD0 # 3 args - R G B
SETSIZE = 0xD1 # 2 args - Cols & Rows
TESTBAUD = 0xD2 # zero args, prints baud rate to uart
STARTL_COMMAND = 0xFE # magic byte indicating a subsequent command list
def __init__(self, serial_device, baud_rate=115200, rows=16, cols=2):
self._baud_rate = baud_rate
self._serial_device = os.path.realpath(serial_device)
self._ser: Optional[serial.Serial] = None
self._rows = rows
self._cols = cols
def __enter__(self) -> "LcdBackpack":
"""
Connects to the serial port.
"""
self.connect()
return self
def __exit__(self, type, value, traceback):
if self.connected:
self.disconnect()
def connect(self):
"""
Manually open a connection.
"""
self._ser = serial.Serial(self._serial_device, self._baud_rate, timeout=1)
def disconnect(self):
"""
Closes the serial port connection.
"""
if self._ser is not None and self._ser.is_open:
self._ser.close()
self._ser = None
@property
def connected(self):
"""
Returns the state of the serial connection.
:return: True if the serial port is open (connected), False otherwise.
"""
return self._ser is not None and self._ser.is_open
def _write_command(self, command_list):
"""
Writes the given command list to the LCD back pack.
:param command_list: The commands to be written to the LCD back pack.
"""
assert self.connected, "Connection required"
self._ser.write(bytes([LcdBackpack.STARTL_COMMAND] + command_list))
def display_on(self):
"""
Switches the LCD backlight on.
"""
self._write_command([LcdBackpack.DISPLAY_ON, 0])
def display_off(self):
"""
Switches the LCD backlight off.
"""
self._write_command([LcdBackpack.DISPLAY_OFF])
def set_brightness(self, brightness: int):
"""
Sets the brightness of the LCD backlight.
:param brightness: integer value from 0 - 255
"""
assert 0 <= brightness <= 255
self._write_command([LcdBackpack.SET_BRIGHTNESS, brightness])
def set_contrast(self, contrast: int):
"""
Sets the contrast of the LCD character text.
:param contrast: integer value from 0 - 255
"""
assert 0 <= contrast <= 255
self._write_command([LcdBackpack.SET_CONTRAST, contrast])
def set_autoscroll(self, auto_scroll: bool):
"""
Sets the autoscrolling capability to the value provided.
:param auto_scroll: true/false
"""
if auto_scroll:
self._write_command([LcdBackpack.AUTOSCROLL_ON])
else:
self._write_command([LcdBackpack.AUTOSCROLL_OFF])
def set_autowrapline(self, auto_wrap: bool):
"""
Sets the autowrapping capability to the value provided.
:param auto_wrap: true/false
"""
if auto_wrap:
self._write_command([LcdBackpack.AUTOWRAPLINE_ON])
else:
self._write_command([LcdBackpack.AUTOWRAPLINE_OFF])
def set_cursor_position(self, column: int, row: int):
"""
Moves the cursor to the provided position.
:param column: integer value for column posititon starting from 1
:param row: integer value for row position starting from 1
"""
assert 1 <= column <= 255
assert 1 <= row <= 255
self._write_command([LcdBackpack.SETCURSOR_POSITION, column, row])
def set_cursor_home(self):
"""
Moves the cursor to the "home" positon: column = 1, row = 1.
"""
self._write_command([LcdBackpack.SETCURSOR_HOME])
def cursor_forward(self):
"""
Moves the cursor forward one character.
"""
self._write_command([LcdBackpack.MOVECURSOR_FORWARD])
def cursor_back(self):
"""
Moves the cursor backward one character.
"""
self._write_command([LcdBackpack.MOVECURSOR_BACK])
def set_underline_cursor(self, underline_cursor: bool):
"""
Enables/disables the underline cursor.
:param underline_cursor: true/false
"""
if underline_cursor:
self._write_command([LcdBackpack.UNDERLINECURSOR_ON])
else:
self._write_command([LcdBackpack.UNDERLINECURSOR_OFF])
def set_block_cursor(self, block_cursor: bool):
"""
Enables/disables the block cursor.
:param block_cursor:
"""
if block_cursor:
self._write_command([LcdBackpack.BLOCKCURSOR_OFF])
else:
self._write_command([LcdBackpack.BLOCKCURSOR_ON])
def set_backlight_rgb(self, red: int, green: int, blue: int):
"""
Sets the RGB LCD backlight to the colour provided by red, green and blue values.
:param red: integer value 0 - 255
:param green: integer value 0 - 255
:param blue: integer value 0 - 255
"""
assert 0 <= red <= 255
assert 0 <= green <= 255
assert 0 <= blue <= 255
self._write_command([LcdBackpack.RGBBACKLIGHT, red, green, blue])
def set_backlight_red(self):
"""
Sets the backlight of an RGB LCD display to be red.
"""
self.set_backlight_rgb(0xFF, 0, 0)
def set_backlight_green(self):
"""
Sets the backlight of an RGB LCD display to be green.
"""
self.set_backlight_rgb(0, 0xFF, 0)
def set_backlight_blue(self):
"""
Sets the backlight of an RGB LCD display to be blue.
"""
self.set_backlight_rgb(0, 0, 0xFF)
def set_backlight_white(self):
"""
Sets the backlight of an RGB LCD display to be white.
"""
self.set_backlight_rgb(0xFF, 0xFF, 0xFF)
def set_lcd_size(self, columns: int, rows: int):
"""
Sets the size of the LCD display (columns, rows).
:param columns: the number of columns
:param rows: the number of rows.
"""
assert 0 <= columns <= 255
assert 0 <= rows <= 255
self._write_command([LcdBackpack.SETSIZE, columns, rows])
def set_gpio_high(self, gpio: int):
"""
Sets the given GPIO pin on the LCD back pack HIGH (5V).
:param gpio: the GPIO pin to set HIGH (1 - 4)
"""
assert 1 <= gpio <= 4
self._write_command([LcdBackpack.GPO_ON, gpio])
def set_gpio_low(self, gpio: int):
"""
Sets the given GPIO pin on the LCD back pack LOW (5V).
:param gpio: the GPIO pin to set LOW (1 - 4)
"""
assert 1 <= gpio <= 4
self._write_command([LcdBackpack.GPO_OFF, gpio])
def clear(self):
"""
Clears all the characters from the display.
"""
self._write_command([LcdBackpack.CLEAR])
def write(self, string):
"""
Writes the given text on the LCD display.
:param string: the text to be written on the display.
"""
if self._ser is None or self._ser.closed:
raise serial.SerialException('Not connected')
self._ser.write(str.encode(string))
def set_splash_screen(self, string):
"""
Sets the LCD splash screen.
:param string: the text to be displayed at LCD start up
:param lcd_chars: the total characters of the LCD display
"""
self._write_command([LcdBackpack.CHANGESPLASH] + list(str.encode(string).ljust(self._cols*self._rows)))
@click.group()
@click.option("-d", "--device", "device", default="/dev/ttyACM0")
@click.option("-b", "--baud", "baud", type=int, default=115200)
@click.pass_context
def cli(ctx, device, baud):
ctx.obj = LcdBackpack(device, baud)
if ctx.invoked_subcommand is None:
ctx.invoke(do_write)
@cli.command("on")
@click.pass_context
def do_on(ctx):
with ctx.obj as dev:
dev.display_on()
@cli.command("off")
@click.pass_context
def do_off(ctx):
with ctx.obj as dev:
dev.display_off()
@cli.command("backlight")
@click.argument("brightness", type=int)
@click.pass_context
def do_backlight(ctx, brightness):
with ctx.obj as dev:
dev.set_brightness(brightness)
@cli.command("contrast")
@click.argument("contrast", type=int)
@click.pass_context
def do_contrast(ctx, contrast):
with ctx.obj as dev:
dev.set_contrast(contrast)
@cli.command("clear")
@click.pass_context
def do_clear(ctx):
with ctx.obj as dev:
dev.clear()
@cli.command("write")
@click.option("-d", "--delay", "delay", type=int, default=100, help="Number of ms to delay between sending lines")
@click.pass_context
def do_write(ctx, delay):
with ctx.obj as dev:
buff = [""] * dev._cols
for line in sys.stdin:
line = line[:dev._rows].strip()
buff = buff[1:] + [line]
dev.clear()
for l in buff:
dev.write(l)
dev.write("\r")
sleep(delay/1e3)
if __name__ == "__main__":
cli()

View file

@ -0,0 +1,9 @@
py_project(
name = "tickertape",
shebang = "/usr/bin/env python3",
main = "src/tickertape/__main__.py",
main_deps = [
"//projects/lcdbackpack",
py_requirement("click"),
],
)

View file

@ -0,0 +1,5 @@
FROM docker.io/library/python:3.13
COPY tickertape.zapp /usr/bin/tickertape
ENTRYPOINT ["/usr/bin/tickertape"]

View file

@ -0,0 +1,55 @@
pkgname=("tickertape")
pkgver="0.1.0"
pkgrel=1
epoch=1
pkgdesc="A LCD ticker for host details"
arch=("any")
depends=(
python
)
source=(
tickertape.service
config.toml
)
sha256sums=(
"SKIP"
"SKIP"
)
makedepends=(
python
bazel
)
# Input file verification
verify() {
true
}
# Input file patching
prepare() {
true
}
# Building
build() {
cd ..
bazel build :tickertape.zapp
cp -f $(bazel run --run_under=echo :tickertape.zapp) $srcdir/
}
# Post-build verification (tests)
check() {
true
}
# Emplacing artifacts
package() {
install -Dm755 tickertape.zapp "${pkgdir}"/usr/bin/tickertape
install -Dm644 tickertape.service "${pkgdir}"/usr/lib/systemd/system/tickertape.service
install -Dm644 config.toml "${pkgdir}"/etc/tickertape/config.toml
}

View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -eoux -o pipefail
cd "$(dirname "$(realpath "$0")")"
tmpdir=$(mktemp -d)
target="//projects/tickertape:tickertape.zapp"
bazel build "${target}"
zapp=$(realpath $(bazel run --run_under=echo "${target}"))
cp Dockerfile "$tmpdir/"
cp -r "$zapp" "$tmpdir/"
cd "${tmpdir}"
docker build "$@" -f Dockerfile -t registry.tirefireind.us/arrdem/tickertape:latest .
docker push registry.tirefireind.us/arrdem/tickertape:latest

Binary file not shown.

View file

@ -0,0 +1,8 @@
[Unit]
Description=A tickertape for Adafruit LCD displays
[Service]
ExecStart=/usr/bin/tickertape --config /etc/tickertape/config.toml
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1 @@
/home/arrdem/Documents/hobby/programming/source/projects/tickertape/tickertape.service

View file

@ -0,0 +1,247 @@
#!/usr/bin/env python3
from datetime import datetime
import ipaddress
from itertools import islice
import json
import os
from pathlib import Path
import re
from shutil import which
import signal
import socket
from subprocess import check_call, check_output
from time import sleep
import tomllib
from typing import Iterable
import click
from lcdbackpack import LcdBackpack
SPLASH_TEXT = "%-16s\r%-16s" % ("TireFire", " Industries")
DEFAULT_CONFIG = {
"tickertape": {
"splash_text": SPLASH_TEXT,
"clock_rate": 3,
"character_rate": 0.75,
},
"lcd": {
"path": "/dev/serial/by-id/usb-239a_Adafruit_Industries-if00",
"height": 2,
"width": 16,
"brightness": 16,
},
"proc": {"path": "/proc"},
"dev": {"path": "/dev"},
}
def hostname() -> str:
hn = os.getenv("TICKERTAPE_HOSTNAME") or socket.gethostname()
def _handler(m):
if m.group(2) and len(m.group(2)) > 3:
return f"{m.group(1)}..."
else:
return m.group(0)
return re.sub("(.{1,13})(.*)", _handler, hn)
def defaultaddr(
subnets: Iterable[ipaddress.IPv4Network] = (ipaddress.IPv4Network("10.0.0.0/8"),)
) -> str:
# Get all the IPv4 addresses for this host
addresses = [
ipaddress.IPv4Address(it)
for it in socket.gethostbyname_ex(socket.gethostname())[-1]
]
# Search out
addresses = [
it
for it in addresses
if any(net.network_address <= it <= net.broadcast_address for net in subnets)
]
for ip in addresses:
yield f"Addr: {ip}"
def render_duration(uptime_seconds):
uptime_seconds = int(uptime_seconds)
sec = uptime_seconds % 60
minutes = (uptime_seconds // 60) % 60
minutes = f"{minutes}m" if minutes > 0 else ""
hours = uptime_seconds // 60 // 60 % 24
hours = f"{hours}h" if hours > 0 else ""
days = uptime_seconds // 60 // 60 // 24
days = f"{days}d" if days > 0 else ""
return f"{days}{hours}{minutes}{sec}s"
def uptime(proc: Path):
with open(proc / "uptime", "r") as f:
uptime_seconds = int(float(f.readline().split()[0]))
yield f"Up: {render_duration(uptime_seconds)}"
def load():
load1, load5, load15 = os.getloadavg()
yield f"Load: 1m {load1:.2f}"
yield f"Load: 5m {load5:.2f}"
yield f"Load: 15m {load15:.2f}"
def df(
dirs: Iterable[Path] = (
Path("/"),
Path("/boot"),
Path("/srv"),
)
):
for dir in dirs:
if dir.is_dir():
dev, blocks, used, avail, pct, mtpt = re.split(
r"\s+", check_output(["df", str(dir)]).decode("utf-8").splitlines()[1]
)
yield f"{dir} usage: {pct}"
def zfs():
if zpool := which("zpool"):
status = json.loads(check_output([zpool, "status", "--json"]))
for pool_name, pool_stats in status["pools"].items():
yield f"Zpool {pool_name}:"
yield f" State: {pool_stats['state']}"
yield f" Errors: {pool_stats['error_count']}"
yield f" Used: {pool_stats['vdevs'][pool_name]['alloc_space']}"
yield f" Total: {pool_stats['vdevs'][pool_name]['total_space']}"
last_scan_date = datetime.strptime(
pool_stats["scan_stats"]["end_time"], "%a %b %d %H:%M:%S %p %Z %Y"
)
delta = datetime.now(tz=last_scan_date.tzinfo) - last_scan_date
yield f" Scanned: {render_duration(delta.total_seconds())} ago"
def window(text: str, n=2):
"""
Returns a sliding window (of width n) over data from the iterable.
s -> (s0,s1,...s[n-1]), (s1,s2,...,sn), ...
"""
offset = 0
while offset + n <= len(text):
yield text[offset : offset + n]
offset += 1
else:
if len(text) < n:
yield text
def recursive_merge(dict1, dict2):
for key, value in dict2.items():
if key in dict1 and isinstance(dict1[key], dict) and isinstance(value, dict):
dict1[key] = recursive_merge(dict1[key], value)
else:
dict1[key] = value
return dict1
@click.option(
"--config",
"config",
type=Path,
help="A configuration file",
default=Path("/etc/tickertape/config.toml"),
)
@click.command()
def main(config: Path):
# Config loading
config_data = {}
if config.exists():
with open(config, "rb") as fp:
config_data: Config = tomllib.load(fp)
config_data = recursive_merge(config_data, DEFAULT_CONFIG)
dev = Path(os.getenv("TICKERTAPE_DEVDIR") or config_data["dev"]["path"])
proc = Path(os.getenv("TICKERTAPE_PROCDIR") or config_data["proc"]["path"])
hn = hostname()
# Functions to iterators over lines (or scrolls )
lower = [
defaultaddr,
lambda: uptime(proc),
load,
df,
zfs,
]
# seconds between display refreshes
clock_rate = config_data["tickertape"]["clock_rate"]
character_rate = config_data["tickertape"]["character_rate"]
lcd_width = config_data["lcd"]["width"]
lcd_height = config_data["lcd"]["height"]
with LcdBackpack(config_data["lcd"]["path"]) as dev:
# Screen setup
dev.set_splash_screen(config_data["tickertape"]["splash_text"])
dev.set_lcd_size(
lcd_width,
lcd_height,
)
dev.set_brightness(config_data["lcd"]["brightness"])
dev.set_autoscroll(False)
# Display the splashscreen
dev.clear()
dev.set_cursor_position(1, 1)
dev.write(SPLASH_TEXT)
sleep(clock_rate)
# Set the hostname row
dev.set_cursor_position(1, 1)
dev.clear()
dev.write(hn)
def sigterm_handler(_signo, _stack_frame):
dev.clear()
exit(0)
signal.signal(signal.SIGTERM, sigterm_handler)
signal.signal(signal.SIGINT, sigterm_handler)
while True:
for h in lower:
# FIXME: Support dependency injection or args to the handlers?
for line in h():
# Lines could be longer than 16 characters and if they are
# we want to shift characters down the line. This is an
# inefficient but idiomatic way to achieve that.
for view in window(line, n=lcd_width):
# Reset the cursor
dev.set_cursor_position(1, 2)
# Lay down exactly 16 characters, space padded to flush any state
dev.write("%-16s" % view)
# Pause between sequential characters
sleep(character_rate)
else:
# And then wait the clock interval
sleep(clock_rate - character_rate)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,8 @@
[Unit]
Description=A tickertape for Adafruit LCD displays
[Service]
ExecStart=/usr/bin/tickertape --config /etc/tickertape/config.toml
[Install]
WantedBy=multi-user.target

View file

@ -4,7 +4,7 @@
# But ... that's exactly what we want to do.
# So this script exists to find a 'compliant' Python install and use that.
PYTHONREV="3.12"
PYTHONREV="3"
CMD="python${PYTHONREV}"
if [ -x "$(command -v "$CMD")" ]; then

View file

@ -56,3 +56,4 @@ yaspin
pytimeparse
git+https://github.com/arrdem/jaraco.text.git@0dd8d0b25a93c3fad896f3a92d11caff61ff273d#egg=jaraco.text
onepasswordconnectsdk
pyserial

View file

@ -1,147 +0,0 @@
aiohttp==3.9.5
aiohttp-basicauth==1.0.0
aiosignal==1.3.1
aiosql==10.1
alabaster==0.7.16
anyio==4.3.0
async-lru==2.0.4
attrs==23.2.0
autocommand==2.2.2
autoflake==2.3.1
Babel==2.15.0
beautifulsoup4==4.12.3
black==24.4.2
blinker==1.8.2
build==1.2.1
cachetools==5.3.3
certifi==2024.2.2
charset-normalizer==3.3.2
cheroot==10.0.1
CherryPy==18.8.0
click==8.1.7
colored==2.2.4
commonmark==0.9.1
coverage==7.5.1
decorator==5.1.1
deepmerge==1.1.1
docutils==0.21.2
ExifRead==3.0.0
flake8==7.0.0
Flask==3.0.3
frozenlist==1.4.1
h11==0.14.0
httpcore==0.16.3
httpx==0.23.3
hypothesis==6.102.1
ibis==3.3.0
icmplib==3.0.4
idna==3.7
imagesize==1.4.1
inflect==7.2.1
iniconfig==2.0.0
isort==5.13.2
itsdangerous==2.2.0
jaraco.collections==5.0.1
jaraco.context==5.3.0
jaraco.functools==4.0.1
jaraco.text @ git+https://github.com/arrdem/jaraco.text.git@0dd8d0b25a93c3fad896f3a92d11caff61ff273d
jedi==0.19.1
Jinja2==3.1.4
jsonschema==4.22.0
jsonschema-path==0.3.2
jsonschema-specifications==2023.12.1
lark==1.1.9
lazy-object-proxy==1.10.0
libsass==0.23.0
livereload==2.6.3
lxml==5.2.2
Markdown==3.6
MarkupSafe==2.1.5
mccabe==0.7.0
meraki==1.46.0
mirakuru==2.5.2
mistune==3.0.2
more-itertools==10.2.0
multidict==6.0.5
mypy-extensions==1.0.0
octorest==0.4
onepasswordconnectsdk==1.5.1
openapi-schema-validator==0.6.2
openapi-spec-validator==0.7.1
packaging==24.0
parso==0.8.4
pathable==0.4.3
pathspec==0.12.1
picobox==4.0.0
pip-tools==7.4.1
platformdirs==4.2.1
pluggy==1.5.0
port-for==0.7.2
portend==3.2.0
prompt-toolkit==3.0.43
proquint==0.2.1
psutil==5.9.8
psycopg==3.1.19
psycopg2==2.9.9
pudb==2024.1
py==1.11.0
pycodestyle==2.11.1
pycryptodome==3.20.0
pyflakes==3.2.0
Pygments==2.18.0
pyproject_hooks==1.1.0
pyrsistent==0.20.0
pytest==8.2.0
pytest-cov==5.0.0
pytest-postgresql==6.0.0
pytest-pudb==0.7.0
pytest-timeout==2.3.1
python-dateutil==2.9.0.post0
pytimeparse==1.1.8
pytz==2024.1
PyYAML==6.0.1
recommonmark==0.7.1
redis==5.0.4
referencing==0.31.1
requests==2.31.0
retry==0.9.2
rfc3339-validator==0.1.4
rfc3986==1.5.0
rpds-py==0.18.1
setuptools==69.5.1
six==1.16.0
smbus2==0.4.3
sniffio==1.3.1
snowballstemmer==2.2.0
sortedcontainers==2.4.0
soupsieve==2.5
Sphinx==7.3.7
sphinx_mdinclude==0.6.0
sphinxcontrib-applehelp==1.0.8
sphinxcontrib-devhelp==1.0.6
sphinxcontrib-htmlhelp==2.0.5
sphinxcontrib-httpdomain==1.8.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-openapi==0.8.4
sphinxcontrib-programoutput==0.17
sphinxcontrib-qthelp==1.0.7
sphinxcontrib-serializinghtml==1.1.10
tempora==5.5.1
termcolor==2.3.0
toml==0.10.2
tornado==6.4
typeguard==4.2.1
typing_extensions==4.11.0
unify==0.5
untokenize==0.1.1
urllib3==2.2.1
urwid==2.6.11
urwid_readline==0.14
wcwidth==0.2.13
websocket-client==1.8.0
Werkzeug==3.0.3
wheel==0.43.0
yamllint==1.35.1
yarl==1.9.4
yaspin==3.0.2
zc.lockfile==3.0.post1

View file

@ -1,144 +1,149 @@
aiohttp==3.9.3
aiohttp-basicauth==1.0.0
aiosignal==1.3.1
aiosql==9.4
alabaster==0.7.13
annotated-types==0.5.0
aiohappyeyeballs==2.4.4
aiohttp==3.11.11
aiohttp_basicauth==1.1.0
aiosignal==1.3.2
aiosql==13.0
alabaster==1.0.0
anyio==4.7.0
async-lru==2.0.4
async-timeout==4.0.2
attrs==23.2.0
attrs==24.3.0
autocommand==2.2.2
autoflake==2.2.1
Babel==2.12.1
autoflake==2.3.1
babel==2.16.0
beautifulsoup4==4.12.3
black==24.1.1
blinker==1.6.2
build==0.10.0
cachetools==5.3.2
certifi==2023.7.22
charset-normalizer==3.2.0
cheroot==10.0.0
black==24.10.0
blinker==1.9.0
build==1.2.2.post1
cachetools==5.5.0
certifi==2024.12.14
charset-normalizer==3.4.1
cheroot==10.0.1
CherryPy==18.8.0
click==8.1.7
click==8.1.8
colored==2.2.4
commonmark==0.9.1
coverage==7.2.7
coverage==7.6.9
decorator==5.1.1
deepmerge==1.1.0
docutils==0.20.1
deepmerge==2.0
docutils==0.21.2
ExifRead==3.0.0
flake8==7.0.0
Flask==3.0.2
frozenlist==1.4.0
hypothesis==6.98.2
flake8==7.1.1
Flask==3.1.0
frozenlist==1.5.0
h11==0.14.0
httpcore==0.16.3
httpx==0.23.3
hypothesis==6.123.1
ibis==3.3.0
icmplib==3.0.4
idna==3.4
idna==3.10
imagesize==1.4.1
inflect==7.0.0
inflect==7.4.0
iniconfig==2.0.0
isort==5.13.2
itsdangerous==2.1.2
jaraco.collections==4.3.0
jaraco.context==4.3.0
jaraco.functools==3.8.0
itsdangerous==2.2.0
jaraco.collections==5.1.0
jaraco.context==6.0.1
jaraco.functools==4.1.0
jaraco.text @ git+https://github.com/arrdem/jaraco.text.git@0dd8d0b25a93c3fad896f3a92d11caff61ff273d
jedi==0.18.2
Jinja2==3.1.3
jsonschema==4.18.4
jsonschema-path==0.3.2
jsonschema-spec==0.2.3
jsonschema-specifications==2023.7.1
lark==1.1.9
lazy-object-proxy==1.9.0
jedi==0.19.2
Jinja2==3.1.5
jsonschema==4.23.0
jsonschema-path==0.3.3
jsonschema-specifications==2023.12.1
lark==1.2.2
lazy-object-proxy==1.10.0
libsass==0.23.0
livereload==2.6.3
lxml==5.1.0
Markdown==3.5.2
MarkupSafe==2.1.3
livereload==2.7.1
lxml==5.3.0
Markdown==3.7
MarkupSafe==3.0.2
mccabe==0.7.0
meraki==1.42.0
mirakuru==2.5.1
mistune==2.0.5
more-itertools==10.0.0
multidict==6.0.4
meraki==1.53.0
mirakuru==2.5.3
mistune==3.0.2
more-itertools==10.5.0
multidict==6.1.0
mypy-extensions==1.0.0
octorest==0.4
openapi-schema-validator==0.6.0
onepasswordconnectsdk==1.5.1
openapi-schema-validator==0.6.2
openapi-spec-validator==0.7.1
packaging==23.1
parso==0.8.3
packaging==24.2
parso==0.8.4
pathable==0.4.3
pathspec==0.11.1
picobox==3.0.0
pip==23.1.2
pip-tools==7.3.0
platformdirs==3.9.1
pluggy==1.2.0
port-for==0.7.1
pathspec==0.12.1
picobox==4.0.0
pip-tools==7.4.1
platformdirs==4.3.6
pluggy==1.5.0
port-for==0.7.4
portend==3.2.0
prompt-toolkit==3.0.43
prompt_toolkit==3.0.48
propcache==0.2.1
proquint==0.2.1
psutil==5.9.5
psycopg==3.1.9
psycopg2==2.9.9
pudb==2022.1.3
psutil==6.1.1
psycopg==3.2.3
psycopg2==2.9.10
pudb==2024.1.3
py==1.11.0
pycodestyle==2.11.1
pycryptodome==3.20.0
pydantic==2.1.1
pydantic_core==2.4.0
pycodestyle==2.12.1
pycryptodome==3.21.0
pyflakes==3.2.0
Pygments==2.15.1
pyproject_hooks==1.0.0
Pygments==2.18.0
pyproject_hooks==1.2.0
pyrsistent==0.20.0
pytest==7.4.0
pytest-cov==4.1.0
pytest-postgresql==5.1.0
pyserial==3.5
pytest==8.3.4
pytest-cov==6.0.0
pytest-postgresql==6.1.1
pytest-pudb==0.7.0
pytest-timeout==2.2.0
pytest-timeout==2.3.1
python-dateutil==2.9.0.post0
pytimeparse==1.1.8
pytz==2023.3
PyYAML==6.0.1
PyYAML==6.0.2
recommonmark==0.7.1
redis==5.0.1
referencing==0.29.3
requests==2.31.0
redis==5.2.1
referencing==0.35.1
requests==2.32.3
retry==0.9.2
rfc3339-validator==0.1.4
rpds-py==0.9.2
setuptools==68.0.0
six==1.16.0
smbus2==0.4.3
rfc3986==1.5.0
rpds-py==0.22.3
setuptools==75.6.0
six==1.17.0
smbus2==0.5.0
sniffio==1.3.1
snowballstemmer==2.2.0
sortedcontainers==2.4.0
soupsieve==2.4.1
Sphinx==7.2.6
sphinx_mdinclude==0.5.3
sphinxcontrib-applehelp==1.0.4
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.1
soupsieve==2.6
Sphinx==8.1.3
sphinx_mdinclude==0.6.2
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-httpdomain==1.8.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-openapi==0.8.3
sphinxcontrib-programoutput==0.17
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.10
tempora==5.5.0
sphinxcontrib-openapi==0.8.4
sphinxcontrib-programoutput==0.18
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
tempora==5.7.0
termcolor==2.3.0
toml==0.10.2
tornado==6.3.2
typing_extensions==4.7.1
tornado==6.4.2
typeguard==4.4.1
typing_extensions==4.12.2
unify==0.5
untokenize==0.1.1
urllib3==2.0.4
urwid==2.1.2
urwid-readline==0.13
wcwidth==0.2.6
websocket-client==1.6.1
Werkzeug==3.0.1
wheel==0.40.0
yamllint==1.34.0
yarl==1.9.2
yaspin==3.0.1
urllib3==2.3.0
urwid==2.6.16
urwid_readline==0.15.1
wcwidth==0.2.13
websocket-client==1.8.0
Werkzeug==3.1.3
wheel==0.45.1
yamllint==1.35.1
yarl==1.18.3
yaspin==3.1.0
zc.lockfile==3.0.post1