aboutsummaryrefslogtreecommitdiff
path: root/library_info_cli.py
blob: 3c6174b6931419c606754ee15e7e2a314f5d7e28 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
#!/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"<td><img class='thumb' src='{relativefilepath}' /></td>"
      else:
         # Just print it inline in the html.
         img_src = i["img"]
         return f"<td><img class='thumb' src='data:{i['img_type']};base64, {img_src}' /></td>"
   else:
      # No image available. This might happen?
      return "<td>none</td>"
   # failsafe
   return "<td>none</td>"

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("<html>\n")
   prn("<head>\n")
   prn('<meta name="viewport" content="width=device-width, initial-scale=1">\n')
   prn('<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">\n')
   prn('<link href="DataTables/datatables.min.css" rel="stylesheet">\n')
   prn('<link rel=stylesheet type="text/css" href="library.css">')
   # inline css, if any
   prn('<style type="text/css">\n')
   prn('</style>\n')
   prn("<title>Library checkouts</title>\n")
   prn("</head>\n")
   prn("<body>\n")
   seen_accounts = []
   for i in checkouts:
      if i["patron"] not in seen_accounts:
         seen_accounts.append(i["patron"])
   if seen_accounts:
      prn("<h2>Accounts: ")
      prn(", ".join(seen_accounts))
      prn("</h2>\n")
   try:
      # fails on python36 on almalinux8
      now = datetime.datetime.now(datetime.UTC)
   except AttributeError:
      now = datetime.datetime.utcnow()
   prn("<span class='eighty'>Last modified: " + now.strftime("%FT%TZ") + "</span>\n")
   prn("<h2>Checkouts</h2>\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("<table style='width: 100%;' class='display' id='checkouts'>\n")
      prn(f"<thead><tr><th>Patron</th><th>barcode</th><th>due</th><th>format</th><th>cover</th><th>Title</th></tr></thead>\n")
      prn("<tbody>\n")
      for i in checkouts:
         prn(f"<tr>")
         prn(f"<td>{i['patron']}</td>")
         prn(f"<td>{i['barcode']}</td>")
         due = i["due"].strftime("%F")
         prn(f"<td>{due}</td>")
         prn(f"<td>{i['format']}</td>")
         prn(html_td_img(i, imagepath, imagerelativepath))
         prn(f"<td>{i['title']}</td>")
         prn(f"</tr>\n")
      prn(f"</tbody>\n")
      prn(f"</table>\n")
   else:
      prn(f"No checkouts.\n")
   prn("<h2>Reservations</h2>\n")
   if len(reservations):
      prn(f"<table style='width: 100%;' class='display' id='reservations'>\n")
      prn(f"<thead><tr><th>Patron</th><th>date</th><th>location</th><th>position</th><th>status</th><th>cover</th><th>title</th></thead>\n")
      prn("<tbody>\n")
      for i in reservations:
         prn(f"<tr>")
         prn(f"<td>{i['patron']}</td>")
         prn(f"<td>{i['date_placed']}</td>")
         prn(f"<td>{i['location']}</td>")
         prn(f"<td>{i['position']}</td>")
         prn(f"<td>{i['status']}</td>")
         prn(html_td_img(i, imagepath, imagerelativepath))
         prn(f"<td>{i['title']}</td>")
         prn(f"</tr>\n")
      prn(f"</tbody>\n")
      prn(f"</table>\n")
   else:
      prn("No reservations.\n")
   prn(f"<h2>Card expiration dates</h2>\n")
   prn(f"<table>\n")
   prn(f"<tr><th>Patron</th><th>date</th></tr>\n")
   for i in card_expiration_dates:
      expires = i["expires"].strftime("%F")
      prn(f"<tr><td>{i['patron']}</td><td>{expires}</td>\n")
   prn(f"</table>\n")
   prn(f"</body>\n")
   prn(f"<footer>\n")
   prn(f"</footer>\n")
   prn('<script src="DataTables/datatables.min.js"></script>')
   # disable paging, because I dislike it.
   # Use column 2 (due date) ascending as initial sort.
   prn("""
<script>$(document).ready(
   function () {
      var table1 = $('#checkouts').DataTable({paging: false});
      table1.column('2:visible').order('asc').draw();
      var table2 = $('#reservations').DataTable({paging: false});
      table2.column('0:visible').order('desc').draw();
   }
);</script>
""")
   prn(f"</html>")

# 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}.")
bgstack15