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

99 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-08-30 13:00 -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 def __init__(self): 

19 super().__init__() 

20 self.is_header = False 

21 self.headers = [] 

22 

23 def read(self, data): 

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

25 self.is_header = False 

26 self.headers = [] 

27 self.reset() 

28 self.feed(data) 

29 return " ".join(self.headers) 

30 

31 def handle_starttag(self, tag, attrs): 

32 """Detect headers.""" 

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

34 self.is_header = True 

35 

36 def handle_data(self, data): 

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

38 if self.is_header: 

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

40 self.is_header = False 

41 

42 

43class ApiConnect(): 

44 """Connect class for FarmBot API.""" 

45 def __init__(self, state): 

46 self.state = state 

47 

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

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

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

51 try: 

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

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

54 response = requests.post(f'{server}/api/tokens', headers=headers, json=user) 

55 # Handle HTTP status codes 

56 if response.status_code == 200: 

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

58 self.state.error = None 

59 self.state.print_status(description=f"Successfully fetched token from {server}.") 

60 return response.json() 

61 elif response.status_code == 404: 

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

63 elif response.status_code == 422: 

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

65 else: 

66 self.state.error = f"HTTP ERROR: Unexpected status code {response.status_code}" 

67 # Handle DNS resolution errors 

68 except requests.exceptions.RequestException as e: 

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

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

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

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

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

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

75 except Exception as e: 

76 self.state.error = f"DNS ERROR: An unexpected error occurred: {str(e)}" 

77 

78 self.state.token = None 

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

80 return self.state.error 

81 

82 @staticmethod 

83 def parse_text(text): 

84 """Parse response text.""" 

85 if '<html' in text: 

86 parser = HTMLResponseParser() 

87 return parser.read(text) 

88 return text 

89 

90 def request_handling(self, response, make_request): 

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

92 

93 error_messages = { 

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

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

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

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

98 } 

99 

100 text = self.parse_text(response.text) 

101 

102 # Handle HTTP status codes 

103 if response.status_code == 200: 

104 if not make_request: 

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

106 else: 

107 description = "Successfully sent request via API." 

108 self.state.print_status(description=description) 

109 return 200 

110 if 400 <= response.status_code < 500: 

111 self.state.error = f"CLIENT ERROR {response.status_code}: {error_messages.get(response.status_code, response.reason)}" 

112 elif 500 <= response.status_code < 600: 

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

114 else: 

115 self.state.error = f"UNEXPECTED ERROR {response.status_code}: {text}" 

116 

117 try: 

118 response.json() 

119 except requests.exceptions.JSONDecodeError: 

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

121 else: 

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

123 

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

125 return response.status_code 

126 

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

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

129 

130 self.state.check_token() 

131 

132 # use 'GET' method to view endpoint data 

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

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

135 # use 'DELETE' method to delete endpoint data 

136 

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

138 iss = token["unencoded"]["iss"] 

139 

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

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

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

143 

144 headers = {'authorization': token['encoded'], 'content-type': 'application/json'} 

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

146 if make_request: 

147 response = requests.request(method, url, headers=headers, json=payload) 

148 else: 

149 response = requests.Response() 

150 response.status_code = 200 

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

152 

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

154 self.state.error = None 

155 self.state.print_status(description="Successfully returned request contents.") 

156 return response.json() 

157 self.state.print_status(description="There was an error processing the request...") 

158 return self.state.error