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

217 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-05 09:33 -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 

18import artemis_sg.app_creds as app_creds 

19import artemis_sg.vendor as vendor 

20from artemis_sg.config import CFG 

21 

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

23 

24 

25def get_worksheet(wb_obj, worksheet): 

26 if not worksheet: 

27 ws = wb_obj.worksheets[0] 

28 else: 

29 ws = wb_obj[worksheet] 

30 return ws 

31 

32 

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

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

35 

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

37 image_col_width = CFG["asg"]["spreadsheet"]["sheet_image"]["image_col_width"] 

38 isbn_col_width = CFG["asg"]["spreadsheet"]["sheet_image"]["isbn_col_width"] 

39 max_col_width = CFG["asg"]["spreadsheet"]["sheet_image"]["max_col_width"] 

40 col_buffer = CFG["asg"]["spreadsheet"]["sheet_image"]["col_buffer"] 

41 

42 # get vendor info from database 

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

44 vendr = vendor.Vendor(vendor_code) 

45 vendr.set_vendor_data() 

46 

47 isbn_key = vendr.isbn_key 

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

49 

50 # Load worksheet 

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

52 wb = load_workbook(workbook) 

53 ws = get_worksheet(wb, worksheet) 

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

55 

56 # Insert column "A" for "ISBN" 

57 # Insert column "B" for "Image" 

58 # Insert column "C" for "Order" 

59 ws.insert_cols(1) 

60 ws.insert_cols(1) 

61 ws.insert_cols(1) 

62 ws["B1"] = "Image" 

63 ws["C1"] = "Order" 

64 

65 # Move ISBN colum to "A" 

66 # Find ISBN column 

67 row01 = ws[1] 

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

69 if type(cell.value) is str and cell.value.upper() == isbn_key.upper(): 

70 # Copy formatting 

71 h_font = copy(cell.font) 

72 h_border = copy(cell.border) 

73 h_fill = copy(cell.fill) 

74 h_number_format = copy(cell.number_format) 

75 h_protection = copy(cell.protection) 

76 h_alignment = copy(cell.alignment) 

77 break 

78 # Move it to A 

79 isbn_idx = cell.column 

80 ws.move_range( 

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

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

83 ) 

84 ws.delete_cols(isbn_idx) 

85 

86 # Copy ISBN header format to B1, C1 

87 cell = ws["A1"] 

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

89 # fmt: off 

90 ws["B1"].font = ws["C1"].font = h_font # noqa: E221, E501 

91 ws["B1"].border = ws["C1"].border = h_border # noqa: E221, E501 

92 ws["B1"].fill = ws["C1"].fill = h_fill # noqa: E221, E501 

93 ws["B1"].number_format = ws["C1"].number_format = h_number_format # noqa: E501 

94 ws["B1"].protection = ws["C1"].protection = h_protection # noqa: E221, E501 

95 ws["B1"].alignment = ws["C1"].alignment = h_alignment # noqa: E221, E501 

96 # fmt: on 

97 

98 # Create column widths 

99 dim_holder = DimensionHolder(worksheet=ws) 

100 

101 # Set column A isbn_col_width for ISBN numbers 

102 dim_holder["A"] = ColumnDimension( 

103 ws, index="A", width=math.ceil(isbn_col_width * col_buffer) 

104 ) 

105 # Set column B image_col_width for images 

106 dim_holder["B"] = ColumnDimension(ws, index="B", width=image_col_width) 

107 

108 # Dynamically set remaining columns 

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

110 col_letter = get_column_letter(col) 

111 width = max(len(str(cell.value)) for cell in ws[col_letter]) * col_buffer 

112 if width > max_col_width: 112 ↛ 113line 112 didn't jump to line 113, because the condition on line 112 was never true

113 width = max_col_width 

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

115 

116 ws.column_dimensions = dim_holder 

117 

118 # Prepare column "B" for "Image" 

119 colB = ws["B"] 

120 for cell in colB: 

121 # Format to center content 

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

123 

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

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

126 isbn_cell, image_cell = row 

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

128 isbn = isbn_cell.value 

129 if isinstance(isbn, float): 

130 isbn = int(isbn) 

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

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

133 if m: 

134 isbn = m.group(1) 

135 try: 

136 isbn = str(isbn).strip() 

137 except Exception as e: 

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

139 isbn = "" 

140 else: 

141 isbn = "" 

142 # Set row height 

143 row_dim = ws.row_dimensions[image_cell.row] 

144 row_dim.height = image_row_height 

145 

146 # Insert image into cell 

147 filename = f"{isbn}.jpg" 

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

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

150 if os.path.isfile(filepath): 

151 img = Image(filepath) 

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

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

154 

155 # Save workbook 

156 wb.save(out) 

157 

158 

159def get_order_items(vendor_code, workbook, worksheet): 

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

161 

162 order_items = [] 

163 # get vendor info from database 

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

165 vendr = vendor.Vendor(vendor_code) 

166 vendr.set_vendor_data() 

167 

168 isbn_key = vendr.isbn_key 

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

170 

171 # Load worksheet 

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

173 wb = load_workbook(workbook) 

174 ws = get_worksheet(wb, worksheet) 

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

176 

177 # Find Isbn and Order column letters 

178 row01 = ws[1] 

179 for cell in row01: 

180 if cell.value == isbn_key: 

181 isbn_column_letter = cell.column_letter 

182 if cell.value == "Order": 

183 order_column_letter = cell.column_letter 

184 

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

186 for cell in row: 

187 if cell.column_letter == isbn_column_letter: 

188 isbn_cell = cell 

189 if cell.column_letter == order_column_letter: 

190 order_cell = cell 

191 # Validate ISBN 

192 if not isbn_cell.value: 192 ↛ 193line 192 didn't jump to line 193, because the condition on line 192 was never true

193 continue 

194 else: 

195 isbn = isbn_cell.value 

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

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

198 if m: 

199 isbn = m.group(1) 

200 try: 

201 isbn = str(int(isbn)).strip() 

202 except Exception as e: 

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

204 continue 

205 # Validate Order Qty 

206 if not order_cell.value: 206 ↛ 207line 206 didn't jump to line 207, because the condition on line 206 was never true

207 continue 

208 else: 

209 qty = order_cell.value 

210 try: 

211 qty = str(int(qty)).strip() 

212 except Exception as e: 

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

214 continue 

215 order_items.append((isbn, qty)) 

216 

217 return order_items 

218 

219 

220def mkthumbs(image_directory): 

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

222 

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

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

225 

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

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

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

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

230 sub_dir = "thumbnails" 

231 back = PIL_Image.open(logo) 

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

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

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

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

236 os.mkdir(thumb_dir) 

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

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

239 else: 

240 logging.error( 

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

242 ) 

243 raise Exception 

244 files = os.listdir(image_directory) 

245 for f in files: 

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

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

248 if not image: 

249 continue 

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

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

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

253 if suffix: 

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

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

256 if os.path.isfile(primary_path): 

257 continue 

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

259 # don't remake thumbnails 

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

261 continue 

262 bk = back.copy() 

263 try: 

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

265 fg = PIL_Image.open(file_path) 

266 except UnidentifiedImageError: 

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

268 os.remove(file_path) 

269 continue 

270 fg.thumbnail((thumb_width, thumb_height)) 

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

272 bk.paste(fg, size) 

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

274 bkn = bk.convert("RGB") 

275 bkn.save(thumb_file) 

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

277 

278 

279def get_sheet_data(workbook, worksheet=None): 

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

281 ######################################################################### 

282 # Try to open sheet_id as an Excel file 

283 sheet_data = [] 

284 try: 

285 wb = load_workbook(workbook) 

286 ws = get_worksheet(wb, worksheet) 

287 for row in ws.values: 

288 sheet_data.append(row) 

289 except (FileNotFoundError, InvalidFileException): 

290 ######################################################################### 

291 # Google specific stuff 

292 # authenticate to google sheets 

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

294 creds = app_creds.app_creds() 

295 SHEETS = build("sheets", "v4", credentials=creds) 

296 # get sheet data 

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

298 sheets = ( 

299 SHEETS.spreadsheets() 

300 .get(spreadsheetId=workbook) 

301 .execute() 

302 .get("sheets", "") 

303 ) 

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

305 else: 

306 ws = worksheet 

307 sheet_data = ( 

308 SHEETS.spreadsheets() 

309 .values() 

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

311 .execute() 

312 .get("values") 

313 ) 

314 ######################################################################### 

315 return sheet_data