Coverage for farmbot_sidecar_starter_pack/functions/api.py: 100%

107 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-09-11 15:43 -0700

1""" 

2ApiConnect class. 

3""" 

4 

5# └── functions/api.py 

6# ├── [API] get_token() 

7# ├── [API] check_token() 

8# ├── [API] request_handling() 

9# └── [API] request() 

10 

11import json 

12from html.parser import HTMLParser 

13import requests 

14 

15 

16class HTMLResponseParser(HTMLParser): 

17 """Response parser for HTML content.""" 

18 

19 def __init__(self): 

20 super().__init__() 

21 self.is_header = False 

22 self.headers = [] 

23 

24 def read(self, data): 

25 """Read the headers from the HTML content.""" 

26 self.is_header = False 

27 self.headers = [] 

28 self.reset() 

29 self.feed(data) 

30 return " ".join(self.headers) 

31 

32 def handle_starttag(self, tag, attrs): 

33 """Detect headers.""" 

34 if tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']: 

35 self.is_header = True 

36 

37 def handle_data(self, data): 

38 """Add header data to the list.""" 

39 if self.is_header: 

40 self.headers.append(data.strip()) 

41 self.is_header = False 

42 

43 

44class ApiConnect(): 

45 """Connect class for FarmBot API.""" 

46 

47 def __init__(self, state): 

48 self.state = state 

49 

50 def get_token(self, email, password, server="https://my.farm.bot"): 

51 """Get FarmBot authorization token. Server is 'https://my.farm.bot' by default.""" 

52 self.state.ssl = "https" in server 

53 try: 

54 headers = {'content-type': 'application/json'} 

55 user = {'user': {'email': email, 'password': password}} 

56 timeout = self.state.timeout["api"] 

57 response = requests.post( 

58 url=f'{server}/api/tokens', 

59 headers=headers, 

60 json=user, 

61 timeout=timeout) 

62 # Handle HTTP status codes 

63 if response.status_code == 200: 

64 self.state.token = response.json() 

65 self.state.error = None 

66 description = f"Successfully fetched token from {server}." 

67 self.state.print_status(description=description) 

68 return response.json() 

69 elif response.status_code == 404: 

70 self.state.error = "HTTP ERROR: The server address does not exist." 

71 elif response.status_code == 422: 

72 self.state.error = "HTTP ERROR: Incorrect email address or password." 

73 else: 

74 code = response.status_code 

75 self.state.error = f"HTTP ERROR: Unexpected status code {code}" 

76 # Handle DNS resolution errors 

77 except requests.exceptions.RequestException as e: 

78 if isinstance(e, requests.exceptions.ConnectionError): 

79 self.state.error = "DNS ERROR: The server address does not exist." 

80 elif isinstance(e, requests.exceptions.Timeout): 

81 self.state.error = "DNS ERROR: The request timed out." 

82 elif isinstance(e, requests.exceptions.RequestException): 

83 self.state.error = "DNS ERROR: There was a problem with the request." 

84 except Exception as e: 

85 self.state.error = f"DNS ERROR: An unexpected error occurred: {e}" 

86 

87 self.state.token = None 

88 self.state.print_status(description=self.state.error) 

89 return self.state.error 

90 

91 @staticmethod 

92 def parse_text(text): 

93 """Parse response text.""" 

94 if '<html' in text: 

95 parser = HTMLResponseParser() 

96 return parser.read(text) 

97 return text 

98 

99 def request_handling(self, response, make_request): 

100 """Handle errors associated with different endpoint errors.""" 

101 

102 error_messages = { 

103 404: "The specified endpoint does not exist.", 

104 400: "The specified ID is invalid or you do not have access to it.", 

105 401: "The user`s token has expired or is invalid.", 

106 502: "Please check your internet connection and try again." 

107 } 

108 

109 text = self.parse_text(response.text) 

110 

111 # Handle HTTP status codes 

112 if response.status_code == 200: 

113 if not make_request: 

114 description = "Editing disabled, request not sent." 

115 else: 

116 description = "Successfully sent request via API." 

117 self.state.print_status(description=description) 

118 return 200 

119 if 400 <= response.status_code < 500: 

120 err = error_messages.get(response.status_code, response.reason) 

121 self.state.error = f"CLIENT ERROR {response.status_code}: {err}" 

122 elif 500 <= response.status_code < 600: 

123 self.state.error = f"SERVER ERROR {response.status_code}: {text}" 

124 else: 

125 code = response.status_code 

126 self.state.error = f"UNEXPECTED ERROR {code}: {text}" 

127 

128 try: 

129 response.json() 

130 except requests.exceptions.JSONDecodeError: 

131 self.state.error += f" ({text})" 

132 else: 

133 self.state.error += f" ({json.dumps(response.json(), indent=2)})" 

134 

135 self.state.print_status(description=self.state.error) 

136 return response.status_code 

137 

138 def request(self, method, endpoint, database_id, payload=None): 

139 """Make requests to API endpoints using different methods.""" 

140 

141 self.state.check_token() 

142 

143 # use 'GET' method to view endpoint data 

144 # use 'POST' method to overwrite/create new endpoint data 

145 # use 'PATCH' method to edit endpoint data (used for new logs) 

146 # use 'DELETE' method to delete endpoint data 

147 

148 token = self.state.token["token"] 

149 iss = token["unencoded"]["iss"] 

150 

151 id_part = "" if database_id is None else f"/{database_id}" 

152 http_part = "https" if self.state.ssl else "http" 

153 url = f'{http_part}:{iss}/api/{endpoint}{id_part}' 

154 

155 headers = {'authorization': token['encoded'], 

156 'content-type': 'application/json'} 

157 make_request = not self.state.dry_run or method == "GET" 

158 if make_request: 

159 timeout = self.state.timeout["api"] 

160 response = requests.request( 

161 method=method, 

162 url=url, 

163 headers=headers, 

164 json=payload, 

165 timeout=timeout) 

166 else: 

167 response = requests.Response() 

168 response.status_code = 200 

169 response._content = b'{"edit_requests_disabled": true}' 

170 

171 if self.request_handling(response, make_request) == 200: 

172 self.state.error = None 

173 description = "Successfully fetched request contents." 

174 self.state.print_status(description=description) 

175 return response.json() 

176 description = "There was an error processing the request..." 

177 self.state.print_status(description=description) 

178 return self.state.error