#!/usr/bin/env python3
# File: library_info_cli.py
# Author: bgstack15
# Startdate: 2024-07-06-7 15:19
# Title: CLI for library_info
# Project: library_info
# Purpose: cli client for library_info_lib
# History:
# Usage: --help
# Reference:
# Improve:
# Dependencies:
# dep-almalinux8: python3-requests, python3-dateutil
import argparse, sys, json, datetime, base64, os
import library_info_lib
# FUNCTIONS
def prn(*args,**kwargs):
kwargs["end"] = ""
print(*args,**kwargs)
def eprint(*args,**kwargs):
kwargs["file"] = sys.stderr
print(*args,**kwargs)
def serialize(item):
eprint(f"DEBUG: trying to serialize {item}, type {type(item)}")
if type(item) == datetime.datetime:
# We know library due dates are just a day, and not a time.
return item.strftime("%F")
else:
eprint(f"WARNING: unknown type {type(item)} for json-serializing object {item}")
return item
def html_td_img(i, imagepath, imagerelativepath):
"""
Given the item, imagepath, and imagerelativepath, prepare the table data object in html.
It is possible for imagepath and imagerelativepath to be None, in which case the image will be stored inline in the html.
"""
if "img" in i:
if imagepath and imagerelativepath:
ext = i["img_type"].split("/")[-1]
img_contents = base64.b64decode(i["img"])
# use chars -25 to -5 (so 20 characters long, 5 from the right end)
# to avoid the == at the end
# also img50 was annoying.
filename = f"{i['img'][-25:-5:]}.{ext}".replace("/","_")
filepath = os.path.join(imagepath, filename)
relativefilepath = os.path.join(imagerelativepath, filename)
if debuglevel >= 3:
eprint(f"Writing to file {filepath} for {i['title']}")
with open(filepath,"wb") as w:
w.write(img_contents)
return f"
"
else:
# Just print it inline in the html.
img_src = i["img"]
return "
"
else:
# No image available. This might happen?
return "
none
"
# failsafe
return "
none
"
def html(checkouts, reservations, imagepath, imagerelativepath):
"""
Make a pretty html page of the items. If imagepath and imagerelativepath are defined, save the images to imagepath, and set the html img tags src attribute to the imagerelativepath.
"""
# Uses https://datatables.net/download/builder?dt/jq-3.7.0/dt-2.0.8/cr-2.0.3/fh-4.0.1
# with css corrected by having the utf-8 line at the top of the .min.css file.
prn("\n")
prn("\n")
prn('\n')
prn('\n')
prn('\n')
prn('')
# inline css, if any
prn('\n')
prn("Library checkouts\n")
prn("\n")
prn("\n")
seen_accounts = []
for i in checkouts:
if i["patron"] not in seen_accounts:
seen_accounts.append(i["patron"])
if seen_accounts:
prn("
Accounts: ")
prn(", ".join(seen_accounts))
prn("
\n")
try:
# fails on python36 on almalinux8
now = datetime.datetime.now(datetime.UTC)
except AttributeError:
now = datetime.datetime.utcnow()
prn("Last modified: " + now.strftime("%FT%TZ") + "\n")
prn("
Checkouts
\n")
if len(checkouts):
# for some reason, DataTables will let a table resize with browser window resize only if the table object itself has the style. A css entry for "table" does not affect DataTables.
prn("
\n")
else:
prn("No reservations.\n")
prn(f"\n")
prn(f"\n")
prn('')
# disable paging, because I dislike it.
# Use column 2 (due date) ascending as initial sort.
prn("""
""")
prn(f"")
# ARGUMENTS
parser = argparse.ArgumentParser(description="Shows currently checked out items from the configured libraries.")
parser.add_argument("-d","--debug", nargs='?', default=0, type=int, choices=range(0,11), help="Set debug level. >=5 adds verbose=True to get_checkouts().")
# add output option: html, raw, json
# add mutual: all or single.
group1 = parser.add_mutually_exclusive_group()
group1.add_argument("-s","--single", help="Show this single account.")
group1.add_argument("-a","--all", action='store_true', default=True, help="Check all accounts")
parser.add_argument("-o","--output", choices=["html","json","raw"], default="raw", help="Output format.")
try:
# This was only added in python 3.9
parser.add_argument("-f","--full", action=argparse.BooleanOptionalAction, default=True, help="Use full image objects or not. They are huge and during debugging it is useful to turn off.")
except AttributeError:
group2 = parser.add_mutually_exclusive_group()
group2.add_argument("-f","--full", action="store_true", default=True, help="Use full image objects or not. They are huge and during debugging it is useful to turn off.")
group2.add_argument("--no-f","--no-full", action="store_true", default=False)
parser.add_argument("-i","--imagepath", default=None, help="Affects html output only. Places images in this directory on the filesystem.")
parser.add_argument("-r","--imagerelativepath", default=None, help="Affects html output only. Use this relative path in the img src attribute. Required if -i is used.")
args = parser.parse_args()
# PARSE ARGUMENTS
debuglevel = 0
if args.debug is None:
# -d was used but no value provided
debuglevel = 10
elif args.debug:
debuglevel = args.debug
full_images = args.full
single = args.single
output = args.output
imagepath = args.imagepath
imagerelativepath = args.imagerelativepath
# Both are required together.
if imagepath is not None and imagerelativepath is None:
eprint("Fatal! If --imagepath is used, you must also use --imagerelativepath")
sys.exit(1)
if imagerelativepath is not None and imagepath is None:
eprint("Fatal! If --imagerelativepath is used, you must also use --imagepath")
sys.exit(1)
# MAIN
if "__main__" == __name__:
if debuglevel >= 1:
eprint(args)
if single:
checkouts, reservations = library_info_lib.get_single_configitem(alias = single, full_images = full_images, verbose = debuglevel >= 5)
else:
checkouts, reservations = library_info_lib.get_all_configitems(full_images = full_images, verbose = debuglevel >= 5)
if "raw" == output:
print(checkouts)
print(reservations)
elif "json" == output:
print(json.dumps(checkouts,default=serialize))
elif "html" == output:
html(checkouts, reservations, imagepath, imagerelativepath)
else:
print(f"Error! Invalid choice for output format {output}.")