#!/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 f"" else: # No image available. This might happen? return "none" # failsafe return "none" def html(checkouts, reservations, imagepath, imagerelativepath, card_expiration_dates): # WORKHERE: need to revise this to take a list of config entries to load, so we can collect card expiration info """ 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") prn(f"\n") prn("\n") for i in checkouts: prn(f"") prn(f"") prn(f"") due = i["due"].strftime("%F") prn(f"") prn(f"") prn(html_td_img(i, imagepath, imagerelativepath)) prn(f"") prn(f"\n") prn(f"\n") prn(f"
PatronbarcodedueformatcoverTitle
{i['patron']}{i['barcode']}{due}{i['format']}{i['title']}
\n") else: prn(f"No checkouts.\n") prn("

Reservations

\n") if len(reservations): prn(f"\n") prn(f"\n") prn("\n") for i in reservations: prn(f"") prn(f"") prn(f"") prn(f"") prn(f"") prn(f"") prn(html_td_img(i, imagepath, imagerelativepath)) prn(f"") prn(f"\n") prn(f"\n") prn(f"
Patrondatelocationpositionstatuscovertitle
{i['patron']}{i['date_placed']}{i['location']}{i['position']}{i['status']}{i['title']}
\n") else: prn("No reservations.\n") prn(f"

Card expiration dates

\n") prn(f"\n") prn(f"\n") for i in card_expiration_dates: expires = i["expires"].strftime("%F") prn(f"\n") prn(f"
Patrondate
{i['patron']}{expires}
\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) card_expiration_dates = [] # will hold objects of {"alias": "aspen 1", "expires": datetime.date(2022,11,4)} if single: checkouts, reservations, card_expiration_dates = library_info_lib.get_single_configitem(alias = single, full_images = full_images, verbose = debuglevel >= 5) else: checkouts, reservations, card_expiration_dates = library_info_lib.get_all_configitems(full_images = full_images, verbose = debuglevel >= 5) if "raw" == output: print(checkouts) print(reservations) print(card_expiration_dates) elif "json" == output: output_json = { "checkouts": checkouts, "reservations": reservations, "card_expiration_dates": card_expiration_dates, } print(json.dumps(output_json,default=serialize)) elif "html" == output: html(checkouts, reservations, imagepath, imagerelativepath, card_expiration_dates) else: print(f"Error! Invalid choice for output format {output}.")