Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 8 additions & 2 deletions succulent/api.py
Original file line number Diff line number Diff line change
@@ -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'):
Expand All @@ -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__)
Expand All @@ -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)
25 changes: 13 additions & 12 deletions succulent/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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 = {}
Expand All @@ -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}')
raise ValueError(f'Invalid format: {self.format}')
188 changes: 186 additions & 2 deletions tests/test_succulent.py
Original file line number Diff line number Diff line change
@@ -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'