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 import artemis_sg.app_creds as app_creds

19 import artemis_sg.vendor as vendor

20 from artemis_sg.config import CFG

21

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

23

24

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

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

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

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

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