Coverage for farmbot_sidecar_starter_pack/functions/api.py: 100%
101 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-09-01 11:44 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-09-01 11:44 -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."""
18 def __init__(self):
19 super().__init__()
20 self.is_header = False
21 self.headers = []
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)
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
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
43class ApiConnect():
44 """Connect class for FarmBot API."""
45 def __init__(self, state):
46 self.state = state
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 timeout = self.state.timeout["api"]
55 response = requests.post(f'{server}/api/tokens', headers=headers, json=user, timeout=timeout)
56 # Handle HTTP status codes
57 if response.status_code == 200:
58 self.state.token = response.json()
59 self.state.error = None
60 self.state.print_status(description=f"Successfully fetched token from {server}.")
61 return response.json()
62 elif response.status_code == 404:
63 self.state.error = "HTTP ERROR: The server address does not exist."
64 elif response.status_code == 422:
65 self.state.error = "HTTP ERROR: Incorrect email address or password."
66 else:
67 self.state.error = f"HTTP ERROR: Unexpected status code {response.status_code}"
68 # Handle DNS resolution errors
69 except requests.exceptions.RequestException as e:
70 if isinstance(e, requests.exceptions.ConnectionError):
71 self.state.error = "DNS ERROR: The server address does not exist."
72 elif isinstance(e, requests.exceptions.Timeout):
73 self.state.error = "DNS ERROR: The request timed out."
74 elif isinstance(e, requests.exceptions.RequestException):
75 self.state.error = "DNS ERROR: There was a problem with the request."
76 except Exception as e:
77 self.state.error = f"DNS ERROR: An unexpected error occurred: {str(e)}"
79 self.state.token = None
80 self.state.print_status(description=self.state.error)
81 return self.state.error
83 @staticmethod
84 def parse_text(text):
85 """Parse response text."""
86 if '<html' in text:
87 parser = HTMLResponseParser()
88 return parser.read(text)
89 return text
91 def request_handling(self, response, make_request):
92 """Handle errors associated with different endpoint errors."""
94 error_messages = {
95 404: "The specified endpoint does not exist.",
96 400: "The specified ID is invalid or you do not have access to it.",
97 401: "The user`s token has expired or is invalid.",
98 502: "Please check your internet connection and try again."
99 }
101 text = self.parse_text(response.text)
103 # Handle HTTP status codes
104 if response.status_code == 200:
105 if not make_request:
106 description = "Editing disabled, request not sent."
107 else:
108 description = "Successfully sent request via API."
109 self.state.print_status(description=description)
110 return 200
111 if 400 <= response.status_code < 500:
112 self.state.error = f"CLIENT ERROR {response.status_code}: {error_messages.get(response.status_code, response.reason)}"
113 elif 500 <= response.status_code < 600:
114 self.state.error = f"SERVER ERROR {response.status_code}: {text}"
115 else:
116 self.state.error = f"UNEXPECTED ERROR {response.status_code}: {text}"
118 try:
119 response.json()
120 except requests.exceptions.JSONDecodeError:
121 self.state.error += f" ({text})"
122 else:
123 self.state.error += f" ({json.dumps(response.json(), indent=2)})"
125 self.state.print_status(description=self.state.error)
126 return response.status_code
128 def request(self, method, endpoint, database_id, payload=None):
129 """Make requests to API endpoints using different methods."""
131 self.state.check_token()
133 # use 'GET' method to view endpoint data
134 # use 'POST' method to overwrite/create new endpoint data
135 # use 'PATCH' method to edit endpoint data (used for new logs)
136 # use 'DELETE' method to delete endpoint data
138 token = self.state.token["token"]
139 iss = token["unencoded"]["iss"]
141 id_part = "" if database_id is None else f"/{database_id}"
142 http_part = "https" if self.state.ssl else "http"
143 url = f'{http_part}:{iss}/api/{endpoint}{id_part}'
145 headers = {'authorization': token['encoded'], 'content-type': 'application/json'}
146 make_request = not self.state.dry_run or method == "GET"
147 if make_request:
148 timeout = self.state.timeout["api"]
149 response = requests.request(method, url, headers=headers, json=payload, timeout=timeout)
150 else:
151 response = requests.Response()
152 response.status_code = 200
153 response._content = b'{"edit_requests_disabled": true}'
155 if self.request_handling(response, make_request) == 200:
156 self.state.error = None
157 self.state.print_status(description="Successfully returned request contents.")
158 return response.json()
159 self.state.print_status(description="There was an error processing the request...")
160 return self.state.error