Coverage for farmbot/functions/api.py: 100%
107 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-09-12 12:03 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-09-12 12:03 -0700
1"""
2ApiConnect class.
3"""
5# └── functions/api.py
6# ├── [API] get_token()
7# ├── [API] check_token()
8# ├── [API] request_handling()
9# └── [API] request()
11import json
12from html.parser import HTMLParser
13import requests
16class HTMLResponseParser(HTMLParser):
17 """Response parser for HTML content."""
19 def __init__(self):
20 super().__init__()
21 self.is_header = False
22 self.headers = []
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)
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
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
44class ApiConnect():
45 """Connect class for FarmBot API."""
47 def __init__(self, state):
48 self.state = state
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}"
87 self.state.token = None
88 self.state.print_status(description=self.state.error)
89 return self.state.error
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
99 def request_handling(self, response, make_request):
100 """Handle errors associated with different endpoint errors."""
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 }
109 text = self.parse_text(response.text)
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}"
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)})"
135 self.state.print_status(description=self.state.error)
136 return response.status_code
138 def request(self, method, endpoint, database_id, payload=None):
139 """Make requests to API endpoints using different methods."""
141 self.state.check_token()
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
148 token = self.state.token["token"]
149 iss = token["unencoded"]["iss"]
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}'
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}'
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