From f4b07cf100a9ae0e8cf9dbba6c085bd3bbed5b7a Mon Sep 17 00:00:00 2001 From: Juliana Mashon Date: Thu, 25 Jul 2024 10:06:37 -0700 Subject: [PATCH] Bookmark --- README.md | 84 ++++++++++++++++++++++++-- api_connect.py | 63 +++++++++----------- api_functions.py | 14 +++-- broker_functions.py | 16 ----- testing.py | 141 ++++++-------------------------------------- 5 files changed, 133 insertions(+), 185 deletions(-) diff --git a/README.md b/README.md index 8854d86..ca899cc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,22 @@ # sidecar-starter-pack Authentication and communication utilities for FarmBot sidecars +## 📖 Contents + +* [Installation](#-installation-mac-os) +* [Getting Started](#-getting-started) +* [Functions](#-functions) + * [Setup](#setup) + * [Information](#information) + * [Messaging](#messaging) + * [Basic Commands](#basic-commands) + * [Movement](#movement) + * [Peripherals](#peripherals) + * [Broker Commands](#broker-commands) +* [Developer Info](#-developer-info) + * [api_connect.py](#api_connectpy) + * [broker_connect.py](#broker_connectpy) + ## 💻 Installation (Mac OS) To set up the project locally, follow these steps: @@ -33,7 +49,7 @@ python3 -m pip install paho-mqtt ## 🌱 Getting Started To generate your authorization token and get started: -(1) Import `farmbot_utilities` and create an instance. +(1) Import `main.py` and create an instance. ``` from farmbot_utilities import Farmbot bot = Farmbot() @@ -77,19 +93,77 @@ bot.move(30,40,10) bot.disconnect_broker() ``` +## 🧭 Functions + +### Setup + +`get_token()` generates user authentication token; call before any other function +`connect_broker()` establishes persistent connect to message broker +`disconnect_broker()` disconnects from the message broker +`listen_broker()` displays messages sent to/from message broker + +### Information + +`get_info()` returns information about a specific endpoint +`set_info()` edits information belonging to preexisting endpoint +env() +group() +curve() +read_status() +read_sensor() +safe_z() +garden_size() + +### Messaging + +`log()` sends a new log message via the API +`message()` sends a new log message via the message broker +`debug()` sends a log message of type 'debug' via the message broker +`toast()` sends a log message of type 'toast' via the message broker + +### Basic Commands + +wait() +e_stop() +unlock() +reboot() +shutdown() + +### Movement + +move() +set_home() +find_home() +axis_length() + +### Peripherals + +control_peripheral() +toggle_peripheral() +on() +off() + +### Broker Commands + +calibrate_camera() +control_servo() +take_photo() +soil_height() +detect_weeds() + ## 🧰 Developer Info -### farmbot_API +### api_connect.py Background: https://developer.farm.bot/v15/docs/web-app/rest-api -Formatting: functions in `farmbot_utilities` which interact with the API require an endpoint, which is truncated onto the HTTP request. +Formatting: functions in `api_functions.py` and `main.py` which interact with the API require an endpoint, which is truncated onto the HTTP request. List of endpoints: https://developer.farm.bot/v15/docs/web-app/api-docs > [!CAUTION] > Making requests other than GET to the API will permanently alter the data in your account. DELETE and POST requests may destroy data that cannot be recovered. Altering data through the API may cause account instability. -### farmbot_BROKER +### broker_connect.py Background: https://developer.farm.bot/v15/docs/message-broker -Formatting: functions in `farmbot_utilities` which interact with the message broker send a message containing CelerScript. The messages require the pre-formatted `RPC_request` included in `farmbot_utilities` as the first line of the message. \ No newline at end of file +Formatting: functions in `broker_functions.py` and `main.py` which interact with the message broker send a message containing CeleryScript. The messages require the pre-formatted `RPC_request` included in `broker_functions.py` as the first line of the message. \ No newline at end of file diff --git a/api_connect.py b/api_connect.py index a8ee7da..fc446a3 100644 --- a/api_connect.py +++ b/api_connect.py @@ -9,31 +9,6 @@ def __init__(self): ## ERROR HANDLING - def token_handling(self, response): - """Handle errors relating to bad user auth token requests.""" - - # Handle HTTP status codes - if response.status_code == 200: - return 200 - elif response.status_code == 404: - self.error = "ERROR: The server address does not exist." - elif response.status_code == 422: - self.error = "ERROR: Incorrect email address or password." - else: - self.error = f"ERROR: Unexpected status code {response.status_code}" - - # Handle DNS resolution errors - if response is None: - self.error = "ERROR: There was a problem with the request." - elif isinstance(response, requests.exceptions.ConnectionError): - self.error = "ERROR: The server address does not exist." - elif isinstance(response, requests.exceptions.Timeout): - self.error = "ERROR: The request timed out." - elif isinstance(response, requests.exceptions.RequestException): - self.error = "ERROR: There was a problem with the request." - - return 0 - def request_handling(self, response): """Handle errors relating to bad endpoints and user requests.""" @@ -54,23 +29,39 @@ def request_handling(self, response): else: self.error = json.dumps(f"UNEXPECTED ERROR {response.status_code}: {response.text}", indent=2) - return 0 - ## FUNCTIONS def get_token(self, email, password, server): """Fetch user authentication token via API.""" - headers = {'content-type': 'application/json'} - user = {'user': {'email': email, 'password': password}} - response = requests.post(f'{server}/api/tokens', headers=headers, json=user) + try: + headers = {'content-type': 'application/json'} + user = {'user': {'email': email, 'password': password}} + response = requests.post(f'{server}/api/tokens', headers=headers, json=user) + # Handle HTTP status codes + if response.status_code == 200: + token_data = response.json() + self.error = None + return token_data + elif response.status_code == 404: + self.error = "HTTP ERROR: The server address does not exist." + elif response.status_code == 422: + self.error = "HTTP ERROR: Incorrect email address or password." + else: + self.error = f"HTTP ERROR: Unexpected status code {response.status_code}" + # Handle DNS resolution errors + except requests.exceptions.RequestException as e: + if isinstance(e, requests.exceptions.ConnectionError): + self.error = "DNS ERROR: The server address does not exist." + elif isinstance(e, requests.exceptions.Timeout): + self.error = "DNS ERROR: The request timed out." + elif isinstance(e, requests.exceptions.RequestException): + self.error = "DNS ERROR: There was a problem with the request." + except Exception as e: + self.error = f"DNS ERROR: An unexpected error occurred: {str(e)}" - if self.token_handling(response) == 200: - token_data = response.json() - self.error = None - return token_data - else: - return self.error + self.token = None + return self.error def check_token(self): """Ensure user authentication token has been generated and persists.""" diff --git a/api_functions.py b/api_functions.py index 12429ce..a5dc8fc 100644 --- a/api_functions.py +++ b/api_functions.py @@ -8,13 +8,17 @@ def __init__(self): self.echo = True self.verbose = True - def return_config(self, return_value): # TODO: which functions return json() + def __return_config(self, return_value, json_val=False): # TODO: which functions return json() """Configure echo and verbosity of function returns.""" if self.echo is True and self.verbose is True: print('-' * 100) - print(f'FUNCTION: {return_value}\n') - return print(return_value) + if json_val is True: + print(f'FUNCTION: {return_value}\n') + return print(return_value) + else: + print(f'FUNCTION: {return_value}\n') + return print(return_value) elif self.echo is True and self.verbose is False: print('-' * 100) return print(return_value) @@ -42,13 +46,11 @@ def set_info(self, endpoint, field, value, id=None): self.api_connect.patch(endpoint, id, new_value) return self.api_connect.get(endpoint, id) - def env(self, id=None, field=None, new_val=None): # TODO: why are there '?' + def env(self, id=None, field=None, new_val=None): # TODO: Fix if id is None: data = self.api_connect.get('farmware_envs', id=None) - print(data) # ? else: data = self.api_connect.get('farmware_envs', id) - print(data) # ? # return ... def log(self, message, type=None, channel=None): diff --git a/broker_functions.py b/broker_functions.py index 6817810..3d9441c 100644 --- a/broker_functions.py +++ b/broker_functions.py @@ -21,22 +21,6 @@ def __init__(self): self.echo = True self.verbose = True - def return_config(self, return_value): # TODO: which functions return json() - """Configure echo and verbosity of function returns.""" - - if self.echo is True and self.verbose is True: - print('-' * 100) - print(f'FUNCTION: {return_value}\n') - return print(return_value) - elif self.echo is True and self.verbose is False: - print('-' * 100) - return print(return_value) - elif self.echo is False and self.verbose is False: - return return_value - else: - print('-' * 100) - return print("ERROR: Incompatible return configuration.") - ## INFORMATION def read_status(self): diff --git a/testing.py b/testing.py index f17bf82..a6e266e 100644 --- a/testing.py +++ b/testing.py @@ -1,7 +1,3 @@ -""" -Farmbot Unit Tests -""" - import unittest import json from unittest.mock import Mock, patch @@ -51,155 +47,56 @@ def test_get_token_custom_server(self, mock_post): @patch('requests.post') def test_get_token_bad_email(self, mock_post): - """NEGATIVE TEST: function called with bad email or password (HTTP error)""" + """NEGATIVE TEST: function called with incorrect email""" mock_response = Mock() + error_response = {'error': 'bad email or password'} + mock_response.json.return_value = error_response mock_response.status_code = 422 mock_post.return_value = mock_response fb = Farmbot() - # Call with bad email - fb.get_token('bad_email@gmail.com', 'test_pass_123', 'https://staging.farm.bot') + with self.assertRaises(Exception): + fb.get_token('bad_email@gmail.com', 'test_pass_123') mock_post.assert_called_once_with( - 'https://staging.farm.bot/api/tokens', + 'https://my.farm.bot/api/tokens', headers={'content-type': 'application/json'}, json={'user': {'email': 'bad_email@gmail.com', 'password': 'test_pass_123'}} ) - # self.assertEqual(fb.error, 'ERROR: Incorrect email address or password.') - self.assertIsNone(fb.token) self.assertEqual(mock_post.return_value.status_code, 422) @patch('requests.post') def test_get_token_bad_server(self, mock_post): - """NEGATIVE TEST: function called with bad server address (HTTP error)""" + """NEGATIVE TEST: function called with incorrect server""" mock_response = Mock() + error_response = {'error': 'bad server'} + mock_response.json.return_value = error_response mock_response.status_code = 404 mock_post.return_value = mock_response fb = Farmbot() - # Call with bad email - fb.get_token('test_email@gmail.com', 'test_pass_123', 'https://bad.farm.bot') + with self.assertRaises(Exception): + fb.get_token('test_email@gmail.com', 'test_pass_123', 'https://bad.farm.bot') mock_post.assert_called_once_with( 'https://bad.farm.bot/api/tokens', headers={'content-type': 'application/json'}, json={'user': {'email': 'test_email@gmail.com', 'password': 'test_pass_123'}} ) - # self.assertEqual(fb.error, 'ERROR: The server address does not exist.') - self.assertIsNone(fb.token) self.assertEqual(mock_post.return_value.status_code, 404) - @patch('requests.request') - def test_get_info_endpoint_only(self, mock_request): - """POSITIVE TEST: function called with endpoint only""" - mock_token = { - 'token': { - 'unencoded': {'iss': '//my.farm.bot'}, - 'encoded': 'encoded_token_value' - } - } - mock_response = Mock() - expected_response = {'device': 'info'} - mock_response.json.return_value = expected_response - mock_response.status_code = 200 - mock_request.return_value = mock_response - fb = Farmbot() - fb.api.api_connect.token = mock_token - # Call with endpoint only - response = fb.get_info('device') - mock_request.assert_called_once_with( - 'GET', - 'https://my.farm.bot/api/device', - headers={ - 'authorization': 'encoded_token_value', - 'content-type': 'application/json' - }, - json=None, - ) - self.assertEqual(response, expected_response) - self.assertEqual(mock_request.return_value.status_code, 200) - @patch('requests.get') def test_get_info_with_id(self, mock_get): - """POSITIVE TEST: function called with endpoint and ID value""" - mock_token = { - 'token': { - 'unencoded': {'iss': '//my.farm.bot'}, - 'encoded': 'encoded_token_value' - } - } + """POSITIVE TEST: function called with valid ID""" mock_response = Mock() - expected_response = {'peripheral': 'info'} - mock_response.json.return_value = expected_response + expected_info = {'id': '12345', 'info': 'farmbot_info'} + mock_response.json.return_value = expected_info mock_response.status_code = 200 mock_get.return_value = mock_response fb = Farmbot() - fb.token = mock_token - # Call with specific ID - response = fb.get_info('peripherals', '12345') + info = fb.get_info('12345') mock_get.assert_called_once_with( - 'https://my.farm.bot/api/peripherals/12345', - headers={ - 'authorization': 'encoded_token_value', - 'content-type': 'application/json' - } + 'https://my.farm.bot/api/resources/12345', + headers = {'authorization': self.token['token']['encoded'], 'content-type': 'application/json'} ) - self.assertEqual(response, json.dumps(expected_response, indent=2)) + self.assertEqual(info, expected_info) self.assertEqual(mock_get.return_value.status_code, 200) -# class TestFarmbot_api(unittest.TestCase): - -# def setUp(self): -# self.farmbot = Farmbot() - -# @patch('farmbot_util_PORT.FarmbotAPI.get_token') -# def test_get_token(self, mock_get_token): -# mock_get_token.return_value = 'fake_token' -# self.farmbot.get_token('test@example.com', 'password123') -# self.assertEqual(self.farmbot.token, 'fake_token') -# mock_get_token.assert_called_once_with('test@example.com', 'password123', 'https://my.farm.bot') - -# @patch('farmbot_util_PORT.FarmbotAPI.get_info') -# def test_get_info(self, mock_get_info): -# mock_get_info.return_value = {'info': 'fake_info'} -# result = self.farmbot.get_info() -# self.assertEqual(result, {'info': 'fake_info'}) -# mock_get_info.assert_called_once() - -# @patch('farmbot_util_PORT.FarmbotAPI.set_info') -# def test_set_info(self, mock_set_info): -# self.farmbot.set_info('label', 'value') -# mock_set_info.assert_called_once_with('label', 'value') - -# @patch('farmbot_util_PORT.FarmbotAPI.log') -# def test_log(self, mock_log): -# self.farmbot.log('message', 'info') -# mock_log.assert_called_once_with('message', 'info') - -# @patch('farmbot_util_PORT.FarmbotAPI.safe_z') -# def test_safe_z(self, mock_safe_z): -# mock_safe_z.return_value = 10 -# result = self.farmbot.safe_z() -# self.assertEqual(result, 10) -# mock_safe_z.assert_called_once() - -# @patch('farmbot_util_PORT.FarmbotAPI.garden_size') -# def test_garden_size(self, mock_garden_size): -# mock_garden_size.return_value = {'x': 1000, 'y': 2000} -# result = self.farmbot.garden_size() -# self.assertEqual(result, {'x': 1000, 'y': 2000}) -# mock_garden_size.assert_called_once() - -# @patch('farmbot_util_PORT.FarmbotAPI.group') -# def test_group(self, mock_group): -# sequences = ['seq1', 'seq2'] -# mock_group.return_value = {'grouped': True} -# result = self.farmbot.group(sequences) -# self.assertEqual(result, {'grouped': True}) -# mock_group.assert_called_once_with(sequences) - -# @patch('farmbot_util_PORT.FarmbotAPI.curve') -# def test_curve(self, mock_curve): -# mock_curve.return_value = {'curve': True} -# result = self.farmbot.curve('seq', 0, 0, 10, 10, 5, 5) -# self.assertEqual(result, {'curve': True}) -# mock_curve.assert_called_once_with('seq', 0, 0, 10, 10, 5, 5) - if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file