Coverage for functions/api.py: 100%
99 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-08-29 18:23 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-08-29 18:23 -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 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)}"
78 self.state.token = None
79 self.state.print_status(description=self.state.error)
80 return self.state.error
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
90 def request_handling(self, response, make_request):
91 """Handle errors associated with different endpoint errors."""
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 }
100 text = self.parse_text(response.text)
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}"
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)})"
124 self.state.print_status(description=self.state.error)
125 return response.status_code
127 def request(self, method, endpoint, database_id, payload=None):
128 """Make requests to API endpoints using different methods."""
130 self.state.check_token()
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
137 token = self.state.token["token"]
138 iss = token["unencoded"]["iss"]
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}'
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}'
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