Skip to content

Commit a1063fe

Browse files
authored
Merge pull request #81 from chilli-axe/desktop-client-improvements
Desktop client improvements
2 parents 2ffbb44 + 2415010 commit a1063fe

File tree

5 files changed

+112
-68
lines changed

5 files changed

+112
-68
lines changed

autofill/src/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ class GoogleScriptsAPIs(str, Enum):
107107
"https://script.google.com/macros/s/AKfycbzzCWc2x3tfQU1Zp45LB1P19FNZE-4njwzfKT5_Rx399h-5dELZWyvf/exec"
108108
)
109109

110+
def __str__(self) -> str:
111+
return str(self.value)
112+
110113

111114
BRACKETS = [18, 36, 55, 72, 90, 108, 126, 144, 162, 180, 198, 216, 234, 396, 504, 612]
112115
THREADS = 5 # shared between CardImageCollections

autofill/src/driver.py

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,34 +26,7 @@
2626

2727

2828
@attr.s
29-
class OrderStatusBarBaseClass:
30-
order: CardOrder = attr.ib(default=attr.Factory(CardOrder.from_xml_in_folder))
31-
state: str = attr.ib(init=False, default=States.initialising)
32-
33-
manager: enlighten.Manager = attr.ib(init=False, default=attr.Factory(enlighten.get_manager))
34-
status_bar: enlighten.StatusBar = attr.ib(init=False, default=False)
35-
download_bar: enlighten.Counter = attr.ib(init=False, default=None)
36-
upload_bar: enlighten.Counter = attr.ib(init=False, default=None)
37-
38-
def configure_bars(self) -> None:
39-
num_images = len(self.order.fronts.cards) + len(self.order.backs.cards)
40-
status_format = "State: {state}, Action: {action}"
41-
self.status_bar = self.manager.status_bar(
42-
status_format=status_format,
43-
state=f"{TEXT_BOLD}{self.state}{TEXT_END}",
44-
action=f"{TEXT_BOLD}N/A{TEXT_END}",
45-
position=1,
46-
)
47-
self.download_bar = self.manager.counter(total=num_images, desc="Images Downloaded", position=2)
48-
self.upload_bar = self.manager.counter(total=num_images, desc="Images Uploaded", position=3)
49-
50-
self.status_bar.refresh()
51-
self.download_bar.refresh()
52-
self.upload_bar.refresh()
53-
54-
55-
@attr.s
56-
class AutofillDriver(OrderStatusBarBaseClass):
29+
class AutofillDriver:
5730
driver: webdriver.remote.webdriver.WebDriver = attr.ib(
5831
default=None
5932
) # delay initialisation until XML is selected and parsed
@@ -63,7 +36,13 @@ class AutofillDriver(OrderStatusBarBaseClass):
6336
init=False,
6437
default="https://www.makeplayingcards.com/design/custom-blank-card.html",
6538
)
39+
order: CardOrder = attr.ib(default=attr.Factory(CardOrder.from_xml_in_folder))
40+
state: str = attr.ib(init=False, default=States.initialising)
6641
action: Optional[str] = attr.ib(init=False, default=None)
42+
manager: enlighten.Manager = attr.ib(init=False, default=attr.Factory(enlighten.get_manager))
43+
status_bar: enlighten.StatusBar = attr.ib(init=False, default=False)
44+
download_bar: enlighten.Counter = attr.ib(init=False, default=None)
45+
upload_bar: enlighten.Counter = attr.ib(init=False, default=None)
6746

6847
# region initialisation
6948
def initialise_driver(self) -> None:
@@ -80,6 +59,22 @@ def initialise_driver(self) -> None:
8059

8160
self.driver = driver
8261

62+
def configure_bars(self) -> None:
63+
num_images = len(self.order.fronts.cards) + len(self.order.backs.cards)
64+
status_format = "State: {state}, Action: {action}"
65+
self.status_bar = self.manager.status_bar(
66+
status_format=status_format,
67+
state=f"{TEXT_BOLD}{self.state}{TEXT_END}",
68+
action=f"{TEXT_BOLD}N/A{TEXT_END}",
69+
position=1,
70+
)
71+
self.download_bar = self.manager.counter(total=num_images, desc="Images Downloaded", position=2)
72+
self.upload_bar = self.manager.counter(total=num_images, desc="Images Uploaded", position=3)
73+
74+
self.status_bar.refresh()
75+
self.download_bar.refresh()
76+
self.upload_bar.refresh()
77+
8378
def __attrs_post_init__(self) -> None:
8479
self.configure_bars()
8580
self.order.print_order_overview()
@@ -277,7 +272,7 @@ def upload_and_insert_image(self, image: CardImage) -> None:
277272
self.insert_image(pid, image, slots=unfilled_slot_numbers)
278273

279274
def upload_and_insert_images(self, images: CardImageCollection) -> None:
280-
for i in range(len(images.cards)):
275+
for _ in range(len(images.cards)):
281276
image: CardImage = images.queue.get()
282277
if image.downloaded:
283278
self.upload_and_insert_image(image)

autofill/src/order.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def from_element(cls, element: Element) -> "CardImage":
117117
return card_image
118118

119119
def download_image(self, queue: Queue["CardImage"], download_bar: enlighten.Counter) -> None:
120-
if not self.file_exists() and not self.errored:
120+
if not self.file_exists() and not self.errored and self.file_path is not None:
121121
self.errored = not download_google_drive_file(self.drive_id, self.file_path)
122122

123123
if self.file_exists() and not self.errored:

autofill/src/pdf_maker.py

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22
from concurrent.futures import ThreadPoolExecutor
33

44
import attr
5+
import enlighten
56
import InquirerPy
67
from fpdf import FPDF
7-
from src.constants import THREADS
8-
from src.driver import OrderStatusBarBaseClass
8+
from src.constants import THREADS, States
9+
from src.order import CardOrder
10+
from src.utils import TEXT_BOLD, TEXT_END
911

1012

1113
@attr.s
12-
class PdfExporter(OrderStatusBarBaseClass):
14+
class PdfExporter:
15+
order: CardOrder = attr.ib(default=attr.Factory(CardOrder.from_xml_in_folder))
16+
state: str = attr.ib(init=False, default=States.initialising)
1317
pdf: FPDF = attr.ib(default=None)
1418
card_width_in_inches: float = attr.ib(default=2.73)
1519
card_height_in_inches: float = attr.ib(default=3.71)
@@ -19,6 +23,29 @@ class PdfExporter(OrderStatusBarBaseClass):
1923
save_path: str = attr.ib(default="")
2024
separate_faces: bool = attr.ib(default=False)
2125
current_face: str = attr.ib(default="all")
26+
manager: enlighten.Manager = attr.ib(init=False, default=attr.Factory(enlighten.get_manager))
27+
status_bar: enlighten.StatusBar = attr.ib(init=False, default=False)
28+
download_bar: enlighten.Counter = attr.ib(init=False, default=None)
29+
processed_bar: enlighten.Counter = attr.ib(init=False, default=None)
30+
31+
def configure_bars(self) -> None:
32+
num_images = len(self.order.fronts.cards) + len(self.order.backs.cards)
33+
status_format = "State: {state}"
34+
self.status_bar = self.manager.status_bar(
35+
status_format=status_format,
36+
state=f"{TEXT_BOLD}{self.state}{TEXT_END}",
37+
position=1,
38+
)
39+
self.download_bar = self.manager.counter(total=num_images, desc="Images Downloaded", position=2)
40+
self.processed_bar = self.manager.counter(total=num_images, desc="Images Processed", position=3)
41+
42+
self.download_bar.refresh()
43+
self.processed_bar.refresh()
44+
45+
def set_state(self, state: str) -> None:
46+
self.state = state
47+
self.status_bar.update(state=f"{TEXT_BOLD}{self.state}{TEXT_END}")
48+
self.status_bar.refresh()
2249

2350
def __attrs_post_init__(self) -> None:
2451
self.ask_questions()
@@ -45,22 +72,24 @@ def ask_questions(self) -> None:
4572
+ "the longer the processing will take and the larger the file size will be.",
4673
"default": 60,
4774
"when": lambda result: result["split_faces"] is False,
48-
"transformer": lambda result: 1 if int(result) < 1 else int(result),
75+
"transformer": lambda result: 1 if (int_result := int(result)) < 1 else int_result,
4976
},
5077
]
5178
answers = InquirerPy.prompt(questions)
5279
if answers["split_faces"]:
5380
self.separate_faces = True
5481
self.number_of_cards_per_file = 1
5582
else:
56-
self.number_of_cards_per_file = 1 if int(answers["cards_per_file"]) < 1 else int(answers["cards_per_file"])
83+
self.number_of_cards_per_file = (
84+
1 if (int_cards_per_file := int(answers["cards_per_file"])) < 1 else int_cards_per_file
85+
)
5786

5887
def generate_file_path(self) -> None:
5988
basename = os.path.basename(str(self.order.name))
6089
if not basename:
6190
basename = "cards.xml"
6291
file_name = os.path.splitext(basename)[0]
63-
self.save_path = "export/%s/" % file_name
92+
self.save_path = f"export/{file_name}/"
6493
os.makedirs(self.save_path, exist_ok=True)
6594
if self.separate_faces:
6695
for face in ["backs", "fronts"]:
@@ -77,8 +106,8 @@ def add_image(self, image_path: str) -> None:
77106
def save_file(self) -> None:
78107
extra = ""
79108
if self.separate_faces:
80-
extra = "%s/" % self.current_face
81-
self.pdf.output(self.save_path + extra + str(self.file_num) + ".pdf")
109+
extra = f"{self.current_face}/"
110+
self.pdf.output(f"{self.save_path}{extra}{self.file_num}.pdf")
82111

83112
def download_and_collect_images(self) -> None:
84113
with ThreadPoolExecutor(max_workers=THREADS) as pool:
@@ -107,37 +136,38 @@ def execute(self) -> None:
107136
else:
108137
self.export()
109138

110-
print("Finished exporting files! They should be accessible at %s" % self.save_path)
139+
print(f"Finished exporting files! They should be accessible at {self.save_path}.")
111140

112141
def export(self) -> None:
113142
for slot in sorted(self.paths_by_slot.keys()):
114143
(back_path, front_path) = self.paths_by_slot[slot]
115-
print("Working on slot %s" % str(slot))
144+
self.set_state(f"Working on slot {slot}")
116145
if slot == 0:
117146
self.generate_pdf()
118147
elif slot % self.number_of_cards_per_file == 0:
119-
print("Saving PDF #%s" % str(self.file_num))
148+
self.set_state(f"Saving PDF #{slot}")
120149
self.save_file()
121150
self.file_num = self.file_num + 1
122151
self.generate_pdf()
123-
print("Adding images for slot %s" % str(slot))
152+
self.set_state(f"Adding images for slot {slot}")
124153
self.add_image(back_path)
125154
self.add_image(front_path)
155+
self.processed_bar.update()
126156

127-
print("Saving PDF #%s" % str(self.file_num))
157+
self.set_state(f"Saving PDF #{self.file_num}")
128158
self.save_file()
129159

130160
def export_separate_faces(self) -> None:
131161
all_faces = ["backs", "fronts"]
132162
for slot in sorted(self.paths_by_slot.keys()):
133163
image_paths_tuple = self.paths_by_slot[slot]
134-
print("Working on slot " + str(slot))
164+
self.set_state(f"Working on slot {slot}")
135165
for face in all_faces:
136166
face_index = all_faces.index(face)
137167
self.current_face = face
138168
self.generate_pdf()
139169
self.add_image(image_paths_tuple[face_index])
140-
print("Saving %s PDF for slot %s" % (face, str(slot)))
170+
self.set_state(f"Saving {face} PDF for slot {slot}")
141171
self.save_file()
142172
if face_index == 1:
143173
self.file_num = self.file_num + 1

autofill/src/utils.py

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,46 +41,62 @@ class ValidationException(Exception):
4141

4242
@ratelimit.sleep_and_retry # type: ignore # `ratelimit` does not implement decorator typing correctly
4343
@ratelimit.limits(calls=1, period=0.1) # type: ignore # `ratelimit` does not implement decorator typing correctly
44+
def rate_limit_post_api_call(url: str, data: dict[str, Any], timeout: Optional[int] = None) -> requests.Response:
45+
with requests.post(url, data=data, timeout=timeout) as r_info:
46+
return r_info
47+
48+
49+
def safe_post_api_call(
50+
url: str, data: dict[str, Any], expected_keys: set[str], max_tries: int = 3, timeout: Optional[int] = None
51+
) -> Optional[dict[str, Any]]:
52+
tries = 0
53+
while True:
54+
try:
55+
r_info = rate_limit_post_api_call(url=url, data=data, timeout=timeout)
56+
r_json = r_info.json()
57+
# validate contents of response
58+
if (
59+
r_info.status_code != 500
60+
and len(expected_keys - r_json.keys()) == 0
61+
and not any([bool(r_json[x]) is False for x in expected_keys])
62+
):
63+
return r_json
64+
except (requests.exceptions.RequestException, TimeoutError):
65+
pass
66+
67+
tries += 1
68+
if tries >= max_tries:
69+
return None
70+
71+
4472
def get_google_drive_file_name(drive_id: str) -> Optional[str]:
4573
"""
4674
Retrieve the name for the Google Drive file identified by `drive_id`.
4775
"""
4876

4977
if not drive_id:
5078
return None
51-
try:
52-
with requests.post(
53-
constants.GoogleScriptsAPIs.image_name.value,
54-
data={"id": drive_id},
55-
timeout=30,
56-
) as r_info:
57-
if r_info.status_code == 500:
58-
return None
59-
return r_info.json()["name"]
60-
except requests.exceptions.Timeout:
61-
return None
79+
response = safe_post_api_call(
80+
url=constants.GoogleScriptsAPIs.image_name, data={"id": drive_id}, timeout=30, expected_keys={"name"}
81+
)
82+
return response["name"] if response is not None else None
6283

6384

64-
@ratelimit.sleep_and_retry # type: ignore # `ratelimit` does not implement decorator typing correctly
65-
@ratelimit.limits(calls=1, period=0.1) # type: ignore # `ratelimit` does not implement decorator typing correctly
6685
def download_google_drive_file(drive_id: str, file_path: str) -> bool:
6786
"""
6887
Download the Google Drive file identified by `drive_id` to the specified `file_path`.
6988
Returns whether the request was successful or not.
7089
"""
7190

72-
with requests.post(
73-
constants.GoogleScriptsAPIs.image_content.value,
74-
data={"id": drive_id},
75-
) as r_contents:
76-
if "<title>Error</title>" in r_contents.text:
77-
# error occurred while attempting to retrieve from Google API
78-
return False
79-
filecontents = r_contents.json()["result"]
80-
if len(filecontents) > 0:
91+
response = safe_post_api_call(
92+
url=constants.GoogleScriptsAPIs.image_content, data={"id": drive_id}, timeout=5 * 60, expected_keys={"result"}
93+
)
94+
if response is not None:
95+
file_contents = response["result"]
96+
if len(file_contents) > 0:
8197
# Download the image
8298
with open(file_path, "wb") as f:
83-
f.write(np.array(filecontents, dtype=np.uint8))
99+
f.write(np.array(file_contents, dtype=np.uint8))
84100
return True
85101
return False
86102

0 commit comments

Comments
 (0)