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

1 import logging

2 import math

3 import os

4 import re

5 from copy import copy

6 from inspect import getsourcefile

7

8 from googleapiclient.discovery import build

9 from openpyxl import load_workbook

10 from openpyxl.drawing.image import Image

11 from openpyxl.styles import Alignment

12 from openpyxl.utils import get_column_letter

13 from openpyxl.utils.exceptions import InvalidFileException

14 from openpyxl.worksheet.dimensions import ColumnDimension, DimensionHolder

15 from PIL import Image as PIL_Image

16 from PIL import UnidentifiedImageError

17

18 from artemis_sg import app_creds, vendor

19 from artemis_sg.config import CFG

20

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

22

23

24 def get_worksheet(wb_obj, worksheet):

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

26 return ws

27

28

29 def 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

61 def 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

166 def 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

181 def 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

191 def 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

236 def 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

295 def 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