diff --git a/pyproject.toml b/pyproject.toml index 9a68461..783d2d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ python = "^3.8" pandas = "^2.0.1" pyyaml = "^6.0" flask = "^2.3.2" +mock = "^4.0" [tool.poetry.dev-dependencies] pytest = "^6.2" diff --git a/succulent/api.py b/succulent/api.py index db5968d..9277d6c 100644 --- a/succulent/api.py +++ b/succulent/api.py @@ -1,6 +1,7 @@ from flask import Flask, jsonify, request from succulent.configuration import Configuration from succulent.processing import Processing +from datetime import datetime class SucculentAPI: def __init__(self, host, port, config, format='csv'): @@ -13,7 +14,7 @@ def __init__(self, host, port, config, format='csv'): self.config = conf.load_config() # Initialise processing - self.processing = Processing(self.config, self.format) + self.processing = Processing(self.config['data'], self.format) # Initialise Flask self.app = Flask(__name__) @@ -31,12 +32,17 @@ def measure(self): try: # Process request self.processing.process(request) + + # Collect and store timestamp + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + # You can store the timestamp in a database, file, or any other desired storage mechanism. + # Example: database.insert_timestamp(timestamp) except ValueError: # Invalid file type return jsonify({'message': f'Invalid file type: {self.format}. Supported file types: csv, json'}), 400 # Send response - return jsonify({'message': 'Data stored'}), 200 + return jsonify({'message': 'Data stored', 'timestamp': timestamp}), 200 def start(self): self.app.run(host=self.host, port=self.port) \ No newline at end of file diff --git a/succulent/processing.py b/succulent/processing.py index 0736e67..b5f0146 100644 --- a/succulent/processing.py +++ b/succulent/processing.py @@ -5,7 +5,8 @@ class Processing: def __init__(self, config, format): self.format = format - self.columns = [configuration['name'] for configuration in config['data']] + self.columns = [configuration['name'] for configuration in config] + self.df = None # Initialize df attribute def parameters(self): parameters = [f'{column}=' for column in self.columns] @@ -24,18 +25,18 @@ def process(self, req): # Load existing data if os.path.exists(path): if self.format == 'csv': - df = pd.read_csv(path, sep=',') + self.df = pd.read_csv(path, sep=',') elif self.format == 'json': - df = pd.read_json(path, orient='records') + self.df = pd.read_json(path, orient='records') elif self.format == 'sqlite': conn = sqlite3.connect(path) - df = pd.read_sql_query("SELECT * FROM data", conn) + self.df = pd.read_sql_query("SELECT * FROM data", conn) conn.close() else: raise ValueError(f'Invalid file type: {self.format}') # Initialise new data else: - df = pd.DataFrame(columns=self.columns) + self.df = pd.DataFrame(columns=self.columns) # Parse data from request data = {} @@ -48,22 +49,22 @@ def process(self, req): else: for column in self.columns: try: - data[column] = req.args.get(column) + data[column] = str(req.args.get(column, default='')) except: - data[column] = None + data[column] = '' data = pd.Series(data, index=self.columns) # Merge data - df = pd.concat([df, data.to_frame().T], ignore_index=True) + self.df = pd.concat([self.df, data.to_frame().T], ignore_index=True) # Store data to device if self.format == 'csv': - df.to_csv(output_path, sep=',', index=False) + self.df.to_csv(output_path, sep=',', index=False) elif self.format == 'json': - df.to_json(output_path, orient='records', indent=4) + self.df.to_json(output_path, orient='records', indent=4) elif self.format == 'sqlite': conn = sqlite3.connect(output_path) - df.to_sql('data', conn, if_exists='replace', index=False) + self.df.to_sql('data', conn, if_exists='replace', index=False) conn.close() else: - raise ValueError(f'Invalid format: {self.format}') \ No newline at end of file + raise ValueError(f'Invalid format: {self.format}') diff --git a/tests/test_succulent.py b/tests/test_succulent.py index 6ecb845..3a2b08a 100644 --- a/tests/test_succulent.py +++ b/tests/test_succulent.py @@ -1,5 +1,189 @@ +from mock import patch +from numpy import nan from succulent import __version__ +import unittest +import os +from flask import Flask +from flask.testing import FlaskClient +from unittest.mock import MagicMock, Mock +from succulent.processing import Processing +from succulent.api import SucculentAPI +from succulent.configuration import Configuration +from datetime import datetime +class TestProcessing(unittest.TestCase): + """ + Test case for the Processing class. + + This test case focuses on testing the methods and functionality of the Processing class. + """ + + def setUp(self): + # Load configuration from configuration.yml + config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'succulent', 'configuration.yml') + configuration = Configuration(config_path) + config = configuration.load_config() + self.processing = Processing(config['data'], 'csv') + + def test_parameters(self): + """ + Test the parameters() method of the Processing class. + + This test verifies that the parameters() method returns the expected string representation of the parameters. + """ + expected_parameters = 'temperature=&humidity=&light=&time=&date=' + self.assertEqual(self.processing.parameters(), expected_parameters) + + def test_process_json(self): + # Mock the request object + request = MagicMock() + request.is_json = True + request.json = { + 'temperature': '25', + 'humidity': '50', + 'light': 'high', + 'time': '10:30', + 'date': '2022-01-01' + } + + # Process the request + self.processing.process(request) + + # Verify the data is merged correctly + expected_data = [ + { + 'temperature': 25, + 'humidity': 50, + 'light': 'high', + 'time': '10:30', + 'date': '2022-01-01' + }, + { + 'temperature': '25', + 'humidity': '50', + 'light': 'high', + 'time': '10:30', + 'date': '2022-01-01' + } + ] + actual_data = self.processing.df.to_dict(orient='records') + print(actual_data) + print(expected_data) + self.assertEqual(actual_data, expected_data) + + def test_process_args(self): + """ + Test the process() method of the Processing class with query string arguments. + + This test ensures that the process() method correctly merges the query string arguments with the existing DataFrame. + """ + # Mock the request object + request = MagicMock() + request.is_json = False + request.args.get.side_effect = ["25", "50", "high", "10:30", "2022-01-01"] + + # Process the request + self.processing.process(request) + + # Verify the data is merged correctly + expected_data = [ + {'temperature': '25', 'humidity': '50', 'light': 'high', 'time': '10:30', 'date': '2022-01-01'} + ] + self.assertEqual(self.processing.df.to_dict(orient='records'), expected_data) + +class TestSucculentAPI(unittest.TestCase): + """ + Test case for the SucculentAPI class. + + This test case focuses on testing the methods and functionality of the SucculentAPI class. + """ + + def setUp(self): + # Create an instance of the SucculentAPI class for testing + self.app = Flask(__name__) + self.app_context = self.app.app_context() + self.app_context.push() + self.client = self.app.test_client() + + config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'succulent', 'configuration.yml') + self.api = SucculentAPI(host='0.0.0.0', port=8080, config=config_path, format='csv') + + def test_version(self): + """ + Test the version of the SucculentAPI. + """ + expected_version = "0.1.1" + self.assertEqual(__version__, expected_version) + + def test_url(self): + """ + Test the URL generation of the SucculentAPI. + """ + # Make a request to the URL endpoint + with self.api.app.test_client() as client: + response = client.get('/measure') + data = response.get_json() + + # Verify the URL in the response + expected_url = '0.0.0.0:8080/measure?temperature=&humidity=&light=&time=&date=' + self.assertEqual(data['url'], expected_url) + + def test_measure(self): + # Create a mock request object + mock_request = Mock() + mock_request.is_json = True + mock_request.json = { + 'temperature': 25.0, + 'humidity': 60.0, + 'light': 'high', + 'time': '10:30 AM', + 'date': '2023-05-21' + } + + # Call the measure endpoint + response = self.api.app.test_client().post('/measure', json=mock_request.json) + + # Assert the response and timestamp + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json['message'], 'Data stored') + # Compare the timestamp with the current time + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + self.assertEqual(response.json['timestamp'], current_time) + +class TestConfiguration(unittest.TestCase): + """ + Test case for the Configuration class. + + This test case focuses on testing the methods and functionality of the Configuration class. + """ + + def setUp(self): + # Get the path to the configuration.yml file + config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'succulent', 'configuration.yml') + self.configuration = Configuration(config_path) + + def test_load_config(self): + """ + Test the load_config() method of the Configuration class. + + This test verifies that the load_config() method correctly loads the configuration from the configuration.yml file. + """ + # Load the configuration from the file + config = self.configuration.load_config() + + # Verify the loaded configuration + expected_config = { + 'data': [ + {'name': 'temperature'}, + {'name': 'humidity'}, + {'name': 'light'}, + {'name': 'time'}, + {'name': 'date'} + ] + } + self.assertEqual(config, expected_config) + + +if __name__ == '__main__': + unittest.main() -def test_version(): - assert __version__ == '0.1.1'