Coverage for functions/authentication.py: 100%

85 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-08-28 12:30 -0700

1""" 

2Authentication class. 

3""" 

4 

5# └── functions/authentication.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 Authentication(): 

44 """Authentication 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 

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 def request_handling(self, response): 

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

84 

85 error_messages = { 

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

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

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

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

90 } 

91 

92 # Handle HTTP status codes 

93 if response.status_code == 200: 

94 self.state.print_status(description="Successfully sent request via API.") 

95 return 200 

96 if 400 <= response.status_code < 500: 

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

98 elif 500 <= response.status_code < 600: 

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

100 else: 

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

102 

103 try: 

104 response.json() 

105 except requests.exceptions.JSONDecodeError: 

106 if '<html>' in response.text: 

107 parser = HTMLResponseParser() 

108 self.state.error += f" ({parser.read(response.text)})" 

109 else: 

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

111 else: 

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

113 

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

115 return response.status_code 

116 

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

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

119 

120 self.state.check_token() 

121 

122 # use 'GET' method to view endpoint data 

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

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

125 # use 'DELETE' method to delete endpoint data (hidden) 

126 

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

128 iss = token["unencoded"]["iss"] 

129 

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

131 url = f'https:{iss}/api/{endpoint}{id_part}' 

132 

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

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

135 

136 if self.request_handling(response) == 200: 

137 self.state.error = None 

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

139 return response.json() 

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

141 return self.state.error