Coverage for amazonorders/entity/order.py: 94.53%

128 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-16 23:55 +0000

1import logging 

2from datetime import datetime, date 

3from typing import List, Optional, TypeVar 

4from urllib.parse import parse_qs 

5from urllib.parse import urlparse 

6 

7from bs4 import BeautifulSoup, Tag 

8 

9from amazonorders.entity.item import Item 

10from amazonorders.entity.parsable import Parsable 

11from amazonorders.entity.recipient import Recipient 

12from amazonorders.entity.shipment import Shipment 

13from amazonorders.session import BASE_URL 

14 

15__author__ = "Alex Laird" 

16__copyright__ = "Copyright 2024, Alex Laird" 

17__version__ = "0.0.7" 

18 

19logger = logging.getLogger(__name__) 

20 

21Entity = TypeVar('Entity', bound='Order') 

22 

23 

24class Order(Parsable): 

25 def __init__(self, 

26 parsed: Tag, 

27 full_details: bool = False, 

28 clone: Optional[Entity] = None) -> None: 

29 super().__init__(parsed) 

30 

31 self.full_details: bool = full_details 

32 

33 self.shipments: List[Shipment] = clone.shipments if clone else self._parse_shipments() 

34 self.items: List[Item] = clone.items if clone else self._parse_items() 

35 self.order_details_link: Optional[str] = clone.order_details_link if clone else self.safe_parse( 

36 self._parse_order_details_link) 

37 self.order_number: str = clone.order_number if clone else self.safe_parse(self._parse_order_number) 

38 self.grand_total: float = clone.grand_total if clone else self.safe_parse(self._parse_grand_total) 

39 self.order_placed_date: date = clone.order_placed_date if clone else self.safe_parse( 

40 self._parse_order_placed_date) 

41 self.recipient: Recipient = clone.recipient if clone else self.safe_parse(self._parse_recipient) 

42 

43 if self.full_details: 

44 self.items: List[Item] = self._parse_items() 

45 self.payment_method: Optional[str] = self._parse_payment_method() 

46 self.payment_method_last_4: Optional[str] = self._parse_payment_method_last_4() 

47 self.subtotal: Optional[float] = self._parse_subtotal() 

48 self.shipping_total: Optional[float] = self._parse_shipping_total() 

49 self.subscription_discount: Optional[float] = self._parse_subscription_discount() 

50 self.total_before_tax: Optional[float] = self._parse_total_before_tax() 

51 self.estimated_tax: Optional[float] = self._parse_estimated_tax() 

52 self.refund_total: Optional[float] = self._parse_refund_total() 

53 self.order_shipped_date: Optional[date] = self._parse_order_shipping_date() 

54 self.refund_completed_date: Optional[date] = self._parse_refund_completed_date() 

55 

56 def __repr__(self) -> str: 

57 return "<Order #{}: \"{}\">".format(self.order_number, self.items) 

58 

59 def __str__(self) -> str: # pragma: no cover 

60 return "Order #{}: \"{}\"".format(self.order_number, self.items) 

61 

62 def _parse_shipments(self) -> List[Shipment]: 

63 return [Shipment(x) for x in self.parsed.find_all("div", {"class": "shipment"})] 

64 

65 def _parse_items(self) -> List[Item]: 

66 return [Item(x) for x in self.parsed.find_all("div", {"class": "yohtmlc-item"})] 

67 

68 def _parse_order_details_link(self) -> Optional[str]: 

69 tag = self.parsed.find("a", {"class": "yohtmlc-order-details-link"}) 

70 if tag: 

71 return "{}{}".format(BASE_URL, tag.attrs["href"]) 

72 else: 

73 return None 

74 

75 def _parse_order_number(self) -> str: 

76 if self.order_details_link: 

77 parsed_url = urlparse(self.order_details_link) 

78 return parse_qs(parsed_url.query)["orderID"][0] 

79 else: 

80 tag = self.parsed.find("bdi", dir="ltr") 

81 return tag.text.strip() 

82 

83 def _parse_grand_total(self) -> float: 

84 tag = self.parsed.find("div", {"class": "yohtmlc-order-total"}) 

85 if tag: 

86 tag = tag.find("span", {"class": "value"}) 

87 else: 

88 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}): 

89 if "grand total" in tag.text.lower(): 

90 tag = tag.find("div", {"class": "a-span-last"}) 

91 break 

92 return float(tag.text.strip().replace("$", "")) 

93 

94 def _parse_order_placed_date(self) -> date: 

95 tag = self.parsed.find("span", {"class": "order-date-invoice-item"}) 

96 if tag: 

97 date_str = tag.text.split("Ordered on")[1].strip() 

98 else: 

99 tag = self.parsed.find("div", {"class": "a-span3"}).find_all("span") 

100 date_str = tag[1].text.strip() 

101 return datetime.strptime(date_str, "%B %d, %Y").date() 

102 

103 def _parse_recipient(self) -> Recipient: 

104 tag = self.parsed.find("div", {"class": "displayAddressDiv"}) 

105 if not tag: 

106 script_id = self.parsed.find("div", 

107 id=lambda value: value and value.startswith("shipToInsertionNode")).attrs[ 

108 "id"] 

109 tag = self.parsed.find("script", 

110 id="shipToData-shippingAddress-{}".format(script_id.split("-")[2])) 

111 tag = BeautifulSoup(str(tag.contents[0]).strip(), "html.parser") 

112 return Recipient(tag) 

113 

114 def _parse_payment_method(self) -> Optional[str]: 

115 tag = self.parsed.find("img", {"class": "pmts-payment-credit-card-instrument-logo"}) 

116 if tag: 

117 return tag.attrs["alt"] 

118 else: 

119 return None 

120 

121 def _parse_payment_method_last_4(self) -> Optional[str]: 

122 tag = self.parsed.find("img", {"class": "pmts-payment-credit-card-instrument-logo"}) 

123 if tag: 

124 ending_sibling = tag.find_next_siblings()[-1] 

125 return ending_sibling.text.split("ending in")[1].strip() 

126 else: 

127 return None 

128 

129 def _parse_subtotal(self) -> Optional[float]: 

130 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}): 

131 if "subtotal" in tag.text.lower(): 

132 return float(tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", "")) 

133 

134 return None 

135 

136 def _parse_shipping_total(self) -> Optional[float]: 

137 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}): 

138 if "shipping" in tag.text.lower(): 

139 return float(tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", "")) 

140 

141 return None 

142 

143 def _parse_subscription_discount(self) -> Optional[float]: 

144 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}): 

145 if "subscribe" in tag.text.lower(): 

146 return float(tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", "")) 

147 

148 return None 

149 

150 def _parse_total_before_tax(self) -> Optional[float]: 

151 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}): 

152 if "before tax" in tag.text.lower(): 

153 return float(tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", "")) 

154 

155 return None 

156 

157 def _parse_estimated_tax(self) -> Optional[float]: 

158 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}): 

159 if "estimated tax" in tag.text.lower(): 

160 return float(tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", "")) 

161 

162 return None 

163 

164 def _parse_refund_total(self) -> Optional[float]: 

165 for tag in self.parsed.find("div", id="od-subtotals").find_all("div", {"class": "a-row"}): 

166 if "refund total" in tag.text.lower() and "tax refund" not in tag.text.lower(): 

167 return float(tag.find("div", {"class": "a-span-last"}).text.strip().replace("$", "")) 

168 

169 return None 

170 

171 def _parse_order_shipping_date(self) -> Optional[date]: 

172 # TODO: find a better way to do this 

173 if "Items shipped:" in self.parsed.text: 

174 date_str = self.parsed.text.split("Items shipped:")[1].strip().split("-")[0].strip() 

175 return datetime.strptime(date_str, "%B %d, %Y").date() 

176 else: 

177 return None 

178 

179 def _parse_refund_completed_date(self) -> Optional[date]: 

180 # TODO: find a better way to do this 

181 if "Refund: Completed" in self.parsed.text: 

182 date_str = self.parsed.text.split("Refund: Completed")[1].strip().split("-")[0].strip() 

183 return datetime.strptime(date_str, "%B %d, %Y").date() 

184 else: 

185 return None