Coverage for src/artemis_sg/spreadsheet.py: 86%

220 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-10-12 06:30 -0700

1import logging 

2import math 

3import os 

4import re 

5from copy import copy 

6from inspect import getsourcefile 

7 

8from googleapiclient.discovery import build 

9from openpyxl import load_workbook 

10from openpyxl.drawing.image import Image 

11from openpyxl.styles import Alignment 

12from openpyxl.utils import get_column_letter 

13from openpyxl.utils.exceptions import InvalidFileException 

14from openpyxl.worksheet.dimensions import ColumnDimension, DimensionHolder 

15from PIL import Image as PIL_Image 

16from PIL import UnidentifiedImageError 

17 

18from artemis_sg import app_creds, vendor 

19from artemis_sg.config import CFG 

20 

21MODULE = os.path.splitext(os.path.basename(__file__))[0] 

22 

23 

24def get_worksheet(wb_obj, worksheet): 

25 ws = wb_obj.worksheets[0] if not worksheet else wb_obj[worksheet] 

26 return ws 

27 

28 

29def insert_image(image_directory, ws, isbn_cell, image_cell): 

30 namespace = f"{MODULE}.{insert_image.__name__}" 

31 image_row_height = CFG["asg"]["spreadsheet"]["sheet_image"]["image_row_height"] 

32 if isbn_cell.value: 32 ↛ 46line 32 didn't jump to line 46, because the condition on line 32 was never false

33 isbn = isbn_cell.value 

34 if isinstance(isbn, float): 

35 isbn = int(isbn) 

36 elif isinstance(isbn, str): 36 ↛ 37line 36 didn't jump to line 37, because the condition on line 36 was never true

37 m = re.search('="(.*)"', isbn) 

38 if m: 

39 isbn = m.group(1) 

40 try: 

41 isbn = str(isbn).strip() 

42 except Exception as e: 

43 logging.error(f"{namespace}: Err reading isbn '{isbn}', err: '{e}'") 

44 isbn = "" 

45 else: 

46 isbn = "" 

47 # Set row height 

48 row_dim = ws.row_dimensions[image_cell.row] 

49 row_dim.height = image_row_height 

50 

51 # Insert image into cell 

52 filename = f"{isbn}.jpg" 

53 filepath = os.path.join(image_directory, filename) 

54 logging.debug(f"{namespace}: Attempting to insert '{filepath}'.") 

55 if os.path.isfile(filepath): 

56 img = Image(filepath) 

57 ws.add_image(img, f"{image_cell.column_letter}{image_cell.row}") 

58 logging.info(f"{namespace}: Inserted '{filepath}'.") 

59 

60 

61def sheet_image(vendor_code, workbook, worksheet, image_directory, out): 

62 namespace = f"{MODULE}.{sheet_image.__name__}" 

63 

64 # get vendor info from database 

65 logging.debug(f"{namespace}: Instantiate vendor.") 

66 vendr = vendor.Vendor(vendor_code) 

67 vendr.set_vendor_data() 

68 

69 isbn_key = vendr.isbn_key 

70 logging.debug(f"{namespace}: Setting ISBN_KEY to '{isbn_key}'.") 

71 

72 # Load worksheet 

73 logging.info(f"{namespace}: Workbook is {workbook}") 

74 wb = load_workbook(workbook) 

75 ws = get_worksheet(wb, worksheet) 

76 logging.info(f"{namespace}: Worksheet is {ws.title}") 

77 

78 # TODO: (#163) Add column order preference to CFG 

79 # Insert column "A" for "ISBN" 

80 # Insert column "B" for "Image" 

81 # Insert column "C" for "Order" 

82 ws.insert_cols(1) 

83 ws.insert_cols(1) 

84 ws.insert_cols(1) 

85 ws["B1"] = "Image" 

86 ws["C1"] = "Order" 

87 

88 # Move ISBN colum to "A" 

89 # Find ISBN column 

90 row01 = ws[1] 

91 for cell in row01: 91 ↛ 102line 91 didn't jump to line 102, because the loop on line 91 didn't complete

92 if isinstance(cell.value, str) and cell.value.upper() == isbn_key.upper(): 

93 # Copy formatting 

94 h_font = copy(cell.font) 

95 h_border = copy(cell.border) 

96 h_fill = copy(cell.fill) 

97 h_number_format = copy(cell.number_format) 

98 h_protection = copy(cell.protection) 

99 h_alignment = copy(cell.alignment) 

100 break 

101 # Move it to A 

102 isbn_idx = cell.column 

103 ws.move_range( 

104 f"{cell.column_letter}{cell.row}:{cell.column_letter}{ws.max_row}", 

105 cols=-(cell.column - 1), 

106 ) 

107 ws.delete_cols(isbn_idx) 

108 

109 # Copy ISBN header format to B1, C1 

110 cell = ws["A1"] 

111 if cell.has_style: 111 ↛ 122line 111 didn't jump to line 122, because the condition on line 111 was never false

112 # fmt: off 

113 ws["B1"].font = ws["C1"].font = h_font 

114 ws["B1"].border = ws["C1"].border = h_border 

115 ws["B1"].fill = ws["C1"].fill = h_fill 

116 ws["B1"].number_format = ws["C1"].number_format = h_number_format 

117 ws["B1"].protection = ws["C1"].protection = h_protection 

118 ws["B1"].alignment = ws["C1"].alignment = h_alignment 

119 # fmt: on 

120 

121 # Create column widths 

122 dim_holder = DimensionHolder(worksheet=ws) 

123 

124 # Set column A isbn_col_width for ISBN numbers 

125 dim_holder["A"] = ColumnDimension( 

126 ws, 

127 index="A", 

128 width=math.ceil( 

129 CFG["asg"]["spreadsheet"]["sheet_image"]["isbn_col_width"] 

130 * CFG["asg"]["spreadsheet"]["sheet_image"]["col_buffer"] 

131 ), 

132 ) 

133 # Set column B image_col_width for images 

134 dim_holder["B"] = ColumnDimension( 

135 ws, index="B", width=CFG["asg"]["spreadsheet"]["sheet_image"]["image_col_width"] 

136 ) 

137 

138 # Dynamically set remaining columns 

139 for col in range(3, ws.max_column + 1): 

140 col_letter = get_column_letter(col) 

141 width = ( 

142 max(len(str(cell.value)) for cell in ws[col_letter]) 

143 * CFG["asg"]["spreadsheet"]["sheet_image"]["col_buffer"] 

144 ) 

145 if width > CFG["asg"]["spreadsheet"]["sheet_image"]["max_col_width"]: 145 ↛ 146line 145 didn't jump to line 146, because the condition on line 145 was never true

146 width = CFG["asg"]["spreadsheet"]["sheet_image"]["max_col_width"] 

147 dim_holder[col_letter] = ColumnDimension(ws, index=col_letter, width=width) 

148 

149 ws.column_dimensions = dim_holder 

150 

151 # Prepare column "B" for "Image" 

152 col_b = ws["B"] 

153 for cell in col_b: 

154 # Format to center content 

155 cell.alignment = Alignment(horizontal="center") 

156 

157 # Insert images in column 2, (i.e. "B") 

158 for row in ws.iter_rows(min_row=2, max_col=2): 

159 isbn_cell, image_cell = row 

160 insert_image(image_directory, ws, isbn_cell, image_cell) 

161 

162 # Save workbook 

163 wb.save(out) 

164 

165 

166def validate_isbn(isbn): 

167 namespace = f"{MODULE}.{validate_isbn.__name__}" 

168 valid_isbn = "" 

169 if isinstance(isbn, str): 169 ↛ 170line 169 didn't jump to line 170, because the condition on line 169 was never true

170 m = re.search('="(.*)"', isbn) 

171 if m: 

172 isbn = m.group(1) 

173 try: 

174 valid_isbn = str(int(isbn)).strip() 

175 except Exception as e: 

176 logging.error(f"{namespace}: Err reading isbn '{isbn}', err: '{e}'") 

177 valid_isbn = "" 

178 return valid_isbn 

179 

180 

181def validate_qty(qty): 

182 namespace = f"{MODULE}.{validate_qty.__name__}" 

183 try: 

184 valid_qty = str(int(qty)).strip() 

185 except Exception as e: 

186 logging.error(f"{namespace}: Err reading Order qty '{qty}', err: '{e}'") 

187 valid_qty = None 

188 return valid_qty 

189 

190 

191def get_order_items(vendor_code, workbook, worksheet): 

192 namespace = f"{MODULE}.{get_order_items.__name__}" 

193 

194 order_items = [] 

195 # get vendor info from database 

196 logging.debug(f"{namespace}: Instantiate vendor.") 

197 vendr = vendor.Vendor(vendor_code) 

198 vendr.set_vendor_data() 

199 

200 isbn_key = vendr.isbn_key 

201 logging.debug(f"{namespace}: Setting ISBN_KEY to '{isbn_key}'.") 

202 

203 # Load worksheet 

204 logging.info(f"{namespace}: Workbook is {workbook}") 

205 wb = load_workbook(workbook) 

206 ws = get_worksheet(wb, worksheet) 

207 logging.info(f"{namespace}: Worksheet is {ws.title}") 

208 

209 # Find Isbn and Order column letters 

210 row01 = ws[1] 

211 for cell in row01: 

212 if cell.value == isbn_key: 

213 isbn_column_letter = cell.column_letter 

214 if cell.value == "Order": 

215 order_column_letter = cell.column_letter 

216 

217 for row in ws.iter_rows(min_row=2): 

218 for cell in row: 

219 if cell.column_letter == isbn_column_letter: 

220 isbn_cell = cell 

221 if cell.column_letter == order_column_letter: 

222 order_cell = cell 

223 # Validate ISBN 

224 isbn = validate_isbn(isbn_cell.value) 

225 if not isbn: 225 ↛ 226line 225 didn't jump to line 226, because the condition on line 225 was never true

226 continue 

227 # Validate Order Qty 

228 qty = validate_qty(order_cell.value) 

229 if not qty: 229 ↛ 230line 229 didn't jump to line 230, because the condition on line 229 was never true

230 continue 

231 order_items.append((isbn, qty)) 

232 

233 return order_items 

234 

235 

236def mkthumbs(image_directory): 

237 namespace = f"{MODULE}.{mkthumbs.__name__}" 

238 

239 thumb_width = CFG["asg"]["spreadsheet"]["mkthumbs"]["width"] 

240 thumb_height = CFG["asg"]["spreadsheet"]["mkthumbs"]["height"] 

241 

242 here = os.path.dirname(getsourcefile(lambda: 0)) 242 ↛ exitline 242 didn't run the lambda on line 242

243 data = os.path.abspath(os.path.join(here, "data")) 

244 logo = os.path.join(data, "artemis_logo.png") 

245 logging.debug(f"{namespace}: Found image for thumbnail background at '{logo}'") 

246 sub_dir = "thumbnails" 

247 back = PIL_Image.open(logo) 

248 thumb_dir = os.path.join(image_directory, sub_dir) 

249 logging.debug(f"{namespace}: Defining thumbnail directory as '{thumb_dir}'") 

250 if not os.path.isdir(thumb_dir): 250 ↛ 260line 250 didn't jump to line 260, because the condition on line 250 was never false

251 logging.debug(f"{namespace}: Creating directory '{thumb_dir}'") 

252 os.mkdir(thumb_dir) 

253 if os.path.isdir(thumb_dir): 253 ↛ 256line 253 didn't jump to line 256, because the condition on line 253 was never false

254 logging.info(f"{namespace}: Successfully created directory '{thumb_dir}'") 

255 else: 

256 logging.error( 

257 f"{namespace}: Failed to create directory '{thumb_dir}'. Aborting." 

258 ) 

259 raise Exception 

260 files = os.listdir(image_directory) 

261 for f in files: 

262 # Valid files are JPG or PNG that are not supplemental images. 

263 image = re.match(r"^.+\.(?:jpg|png)$", f) 

264 if not image: 

265 continue 

266 # Supplemental images have a "-[0-9]+" suffix before the file type. 

267 # AND a file without that suffix exists int he image_directory. 

268 suffix = re.match(r"(^.+)-[0-9]+(\.(?:jpg|png))$", f) 

269 if suffix: 

270 primary = suffix.group(1) + suffix.group(2) 

271 primary_path = os.path.join(image_directory, primary) 

272 if os.path.isfile(primary_path): 

273 continue 

274 thumb_file = os.path.join(thumb_dir, f) 

275 # don't remake thumbnails 

276 if os.path.isfile(thumb_file): 276 ↛ 277line 276 didn't jump to line 277, because the condition on line 276 was never true

277 continue 

278 bk = back.copy() 

279 try: 

280 file_path = os.path.join(image_directory, f) 

281 fg = PIL_Image.open(file_path) 

282 except UnidentifiedImageError: 

283 logging.error(f"{namespace}: Err reading '{f}', deleting '{file_path}'") 

284 os.remove(file_path) 

285 continue 

286 fg.thumbnail((thumb_width, thumb_height)) 

287 size = (int((bk.size[0] - fg.size[0]) / 2), int((bk.size[1] - fg.size[1]) / 2)) 

288 bk.paste(fg, size) 

289 logging.debug(f"{namespace}: Attempting to save thumbnail '{thumb_file}'") 

290 bkn = bk.convert("RGB") 

291 bkn.save(thumb_file) 

292 logging.info(f"{namespace}: Successfully created thumbnail '{thumb_file}'") 

293 

294 

295def get_sheet_data(workbook, worksheet=None): 

296 namespace = f"{MODULE}.{get_sheet_data.__name__}" 

297 ######################################################################### 

298 # Try to open sheet_id as an Excel file 

299 sheet_data = [] 

300 try: 

301 wb = load_workbook(workbook) 

302 ws = get_worksheet(wb, worksheet) 

303 for row in ws.values: 

304 sheet_data.append(row) 

305 except (FileNotFoundError, InvalidFileException): 

306 ######################################################################### 

307 # Google specific stuff 

308 # authenticate to google sheets 

309 logging.info(f"{namespace}: Authenticating to google api.") 

310 creds = app_creds.app_creds() 

311 sheets_api = build("sheets", "v4", credentials=creds) 

312 # get sheet data 

313 if not worksheet: 313 ↛ 322line 313 didn't jump to line 322, because the condition on line 313 was never false

314 sheets = ( 

315 sheets_api.spreadsheets() 

316 .get(spreadsheetId=workbook) 

317 .execute() 

318 .get("sheets", "") 

319 ) 

320 ws = sheets.pop(0).get("properties", {}).get("title") 

321 else: 

322 ws = worksheet 

323 sheet_data = ( 

324 sheets_api.spreadsheets() 

325 .values() 

326 .get(range=ws, spreadsheetId=workbook) 

327 .execute() 

328 .get("values") 

329 ) 

330 ######################################################################### 

331 return sheet_data