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
« 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
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
18from artemis_sg import app_creds, vendor
19from artemis_sg.config import CFG
21MODULE = os.path.splitext(os.path.basename(__file__))[0]
24def get_worksheet(wb_obj, worksheet):
25 ws = wb_obj.worksheets[0] if not worksheet else wb_obj[worksheet]
26 return ws
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
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}'.")
61def sheet_image(vendor_code, workbook, worksheet, image_directory, out):
62 namespace = f"{MODULE}.{sheet_image.__name__}"
64 # get vendor info from database
65 logging.debug(f"{namespace}: Instantiate vendor.")
66 vendr = vendor.Vendor(vendor_code)
67 vendr.set_vendor_data()
69 isbn_key = vendr.isbn_key
70 logging.debug(f"{namespace}: Setting ISBN_KEY to '{isbn_key}'.")
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}")
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"
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)
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
121 # Create column widths
122 dim_holder = DimensionHolder(worksheet=ws)
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 )
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)
149 ws.column_dimensions = dim_holder
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")
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)
162 # Save workbook
163 wb.save(out)
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
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
191def get_order_items(vendor_code, workbook, worksheet):
192 namespace = f"{MODULE}.{get_order_items.__name__}"
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()
200 isbn_key = vendr.isbn_key
201 logging.debug(f"{namespace}: Setting ISBN_KEY to '{isbn_key}'.")
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}")
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
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))
233 return order_items
236def mkthumbs(image_directory):
237 namespace = f"{MODULE}.{mkthumbs.__name__}"
239 thumb_width = CFG["asg"]["spreadsheet"]["mkthumbs"]["width"]
240 thumb_height = CFG["asg"]["spreadsheet"]["mkthumbs"]["height"]
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}'")
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