diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b29db5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +.idea/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f1a02ea --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# Package Converter API +## Overview +The Package Converter API is a web service that +provides functionality to convert package measurements +based on user input. It exposes endpoints for converting +measurements and retrieving conversion history. + +## Features +- **Sequence Input Handling:** Accepts a sequence of characters and underscores as input for conversion. +- **Efficient Conversion Algorithms:** Implements efficient algorithms for converting the input sequence into a list of measurements. +- **Clear and adaptable:** Easily modified and extended to include additional functionalities. + +## Installation +Clone the repository: +``` +git clone https://github.com/Rudainasaleh/PackageMeasurementConversionAPI.git +``` + +## Install dependencies: +``` +pip install -r requirements.txt +``` + +## Usage +- Start the server: +```python main_app.py``` +- ```GET http://localhost:8080/convert_measurements?input=aa``` Convert package measurements based on user input +- ```GET http://localhost:8080/get_history``` Retrieve conversion history. + +## Testing + +- To run unit tests: + +``` python -m unittest ./services/test/test_package_conversion.py``` \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/controller/__init__.py b/controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/controller/converter_controller.py b/controller/converter_controller.py new file mode 100644 index 0000000..a4e70a5 --- /dev/null +++ b/controller/converter_controller.py @@ -0,0 +1,65 @@ +import cherrypy +from datetime import datetime + +from services.package_converter import PackageConverter +from utilis.db_operator import PackageMeasurementHistory +from models.sequence import Sequence + + +class ConverterAPI: + def __init__(self): + self.converter = PackageConverter() + self.sequence_history = PackageMeasurementHistory('./utilis/converter.db') + + @cherrypy.expose + @cherrypy.tools.json_out() + def convert_measurements(self, input=None): + """ + Function that exposes an API endpoint to convert the measurements given by user and handles the errors + :param input: sequence of characters from user + :return: a JSON response containing the status, response, measurements, and error + """ + try: + if input is None: + input_string = cherrypy.request.params.get("input", "") + + input_string = input + measurement = self.converter.package_measurement_conversion(input_string) + time = datetime.now() + if measurement != "Invalid": + # Assuming no error message or response for now + response = "Measurements saved successfully" + sequence = Sequence(input_string, measurement, time, response) + self.sequence_history.save_curr_seq(sequence) + return {"status": "success", "err_msg": "", "result": measurement} + else: + error_message = "invalid sequence" + response = "cant convert measurement input string" + measurement = [] + sequence = Sequence(input_string, measurement, time, response) + self.sequence_history.save_curr_seq(sequence) + return {"status": "fail", "err_msg": error_message, "result": measurement} + + except Exception as e: + cherrypy.response.status = 500 + error_message = str(e) + response = 500 + sequence = Sequence(input_string, measurement=None, time=time, response=response) + self.sequence_history.save_curr_seq(sequence) + return {"status": "error", "err_msg": error_message, "result": None} + + @cherrypy.expose + @cherrypy.tools.json_out() + def get_history(self): + """ + Function that exposes an API endpoint to retrieve measurements history + :return: a JSON response containing the history data, or an error message + """ + try: + history = self.sequence_history.get_history() + return history + except Exception as e: + cherrypy.response.status = 500 # Internal Server Error + return {"status": "error", "err_msg": str(e), "result": None} + + diff --git a/main_app.py b/main_app.py new file mode 100644 index 0000000..81404a3 --- /dev/null +++ b/main_app.py @@ -0,0 +1,23 @@ +import sys +import logging +import cherrypy +from controller.converter_controller import ConverterAPI + + +if __name__ == '__main__': + + # Configure LOGGING to file + logging.basicConfig(filename='utilis/error_log.log', level=logging.CRITICAL, + format='%(asctime)s:%(levelname)s:%(message)s') + port = 8080 # Default port + + if len(sys.argv) > 1: + try: + port = int(sys.argv[1]) + except ValueError: + print("Invalid port number. Using the default port (8080).") + + cherrypy.config.update({'server.socket_host': '0.0.0.0', 'server.socket_port': port}) + cherrypy.tree.mount(ConverterAPI(), '/') + cherrypy.engine.start() + cherrypy.engine.block() diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/sequence.py b/models/sequence.py new file mode 100644 index 0000000..93ff16a --- /dev/null +++ b/models/sequence.py @@ -0,0 +1,9 @@ + + +class Sequence: + def __init__(self, input_string, measurement, time, response: str = None): + self.input_string = input_string + self.measurement = measurement + self.response = response + self.time = time + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cdd578d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +CherryPy==18.9.0 +requests==2.31.0 +setuptools==69.2.0 \ No newline at end of file diff --git a/services/package_converter.py b/services/package_converter.py new file mode 100644 index 0000000..aaaf3e6 --- /dev/null +++ b/services/package_converter.py @@ -0,0 +1,124 @@ +import string + + +class PackageConverter: + def __init__(self): + pass + + def search(self, lst): + """ + Function to count the number of the letter z + :param lst: a list containing numbers + :return: an integer indicating the occurrences of z + """ + count = 0 + for i in lst: + if i != 26: + return count + elif i == "invalid": + return "invalid" + count += 1 + + def calc(self, lst): + """ + Function to return the sum of a given list + :param lst: a list containing integers + :return: an integer indicating the sum of a list + """ + return sum(lst) + + def is_empty(self, lst): + """ + Function to check if the list is empty + :param lst: a list of integers + :return: a boolean (True/False) + """ + return len(lst) < 1 + + def convert_char_to_conversion(self, char): + """ + Function to convert characters to numeric values + :param char: a single character either a letter or an underscore + :return: an integer value of the character + """ + if char == '_': + return 0 + elif char.lower() in string.ascii_lowercase: + return string.ascii_lowercase.index(char.lower()) + 1 + else: + return "invalid" + + def process_list(self, lst): + """ + Function to process the list and add values of z (26) together. It also checks if z is the last character + :param lst: a list containing integers + :return: returns the processed list, or returns invalid in case z was the last character + """ + i = 0 + while i < len(lst): + + if lst[i] == 26: + if i < len(lst) - 1: + new_num = self.search(lst[i:]) + lst[i + new_num] += lst[i] * new_num + + del lst[i:i + new_num] + i += new_num + i += 1 + else: + return "Invalid" + elif lst[i] == "invalid": + return "Invalid" + else: + i += 1 + return lst + + def package_measurement_conversion(self, input_string): + """ + Function to convert a sequence of characters to a list of the total measurements + :param input_string: a string inputted by the user + :return: a list of the measurements calculated from the input string + """ + package_list = [self.convert_char_to_conversion(char) for char in input_string] + + list_to_measure = self.process_list(package_list) + measurements = [] + if list_to_measure == "Invalid": + measurements = "Invalid" + else: + while not self.is_empty(list_to_measure): + n = list_to_measure[0] + if n == 0: + measurements.append(0) + break + elif n >= len(list_to_measure) or self.is_empty(list_to_measure): + measurements = "Invalid" + break + else: + list_to_measure.pop(0) + measurements.append(self.calc(list_to_measure[:n])) + list_to_measure = list_to_measure[n:] + return measurements + + + +# # # Test the function +converter = PackageConverter() +print(converter.package_measurement_conversion("@@@")) # [1] +# converter.package_measurement_conversion("__") # [0] +# converter.package_measurement_conversion("a_") # [0] +# converter.package_measurement_conversion("abz") # invalid +# converter.package_measurement_conversion("abc") # invalid +# +# converter.package_measurement_conversion("baaca") # invalid +# converter.package_measurement_conversion("aaa") # invalid +# converter.package_measurement_conversion("bbb") # [4] +# converter.package_measurement_conversion("ccc") # invalid +# converter.package_measurement_conversion('abbcc') # [2,6] +# converter.package_measurement_conversion('abcdabcdab') # [2,7,7] +# converter.package_measurement_conversion('abcdabcdab_') # [[2, 7, 7, 0 ] +# converter.package_measurement_conversion('dz_a_aazzaaa') # [28, 53, 1] +# converter.package_measurement_conversion('zdaaaaaaaabaaaaaaaabaaaaaaaabbaa') # [34] +# converter.package_measurement_conversion('za_a_a_a_a_a_a_a_a_a_a_a_a_azaaa') # [40,1] +# converter.package_measurement_conversion('zza_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_') # [26] +# # diff --git a/services/test/test_package_conversion.py b/services/test/test_package_conversion.py new file mode 100644 index 0000000..21ef026 --- /dev/null +++ b/services/test/test_package_conversion.py @@ -0,0 +1,71 @@ +import unittest +from services.package_converter import PackageConverter + + +class TestPackageConverter(unittest.TestCase): + """ + Two functions containing multiple test cases for valid and invalid user inputs + """ + + def setUp(self): + self.converter = PackageConverter() + + def test_aa_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion("aa"), [1]) + + def test_double_underscore_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion("__"), [0]) + + def test_single_underscore_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion("a_"), [0]) + + def test_abbcc_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion('abbcc'), [2, 6]) + + def test_abcdabcdab_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion('abcdabcdab'), [2, 7, 7]) + + def test_abcdabcdab_with_underscore_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion('abcdabcdab_'), [2, 7, 7, 0]) + + def test_dz_a_aazzaaa_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion('dz_a_aazzaaa'), [28, 53, 1]) + + def test_zdaaaaaaaabaaaaaaaabaaaaaaaabbaa_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion('zdaaaaaaaabaaaaaaaabaaaaaaaabbaa'), [34]) + + def test_za_a_a_a_a_a_a_a_a_a_a_a_a_azaaa_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion('za_a_a_a_a_a_a_a_a_a_a_a_a_azaaa'), [40, 1]) + + def test_zza_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_underscore_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion('zza_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_'), [26]) + + def test_single_underscore_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion("_"), [0]) + + def test_underscore_ad_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion("_ad"), [0]) + + def test_a_underscore_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion("a_"), [0]) + + def test_underscore_zzzb_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion("_zzzb"), [0]) + + def test_abz_invalid_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion("abz"), "Invalid") + + def test_aaa_invalid_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion("aaa"), "Invalid") + + def test_abc_invalid_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion("abc"), "Invalid") + + def test_baaca_invalid_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion("baaca"), "Invalid") + + def test_ccc_invalid_conversion(self): + self.assertEqual(self.converter.package_measurement_conversion("ccc"), "Invalid") + +if __name__ == '__main__': + unittest.main() diff --git a/utilis/db_operator.py b/utilis/db_operator.py new file mode 100644 index 0000000..aee2084 --- /dev/null +++ b/utilis/db_operator.py @@ -0,0 +1,78 @@ +import json +import sqlite3 +from models.sequence import Sequence + + +class PackageMeasurementHistory: + def __init__(self, db_path): + self.db_path = db_path + self.create_table() + + def create_table(self): + """ + Function to create the SQL database + :return: the database connection object + """ + try: + connection = sqlite3.connect(self.db_path) + cursor = connection.cursor() + cursor.execute('''CREATE TABLE IF NOT EXISTS sequences ( + id INTEGER PRIMARY KEY, + input_string TEXT, + measurements TEXT, + response TEXT, + time timestamp + )''') + connection.commit() + except sqlite3.Error as e: + print(f"Error creating table: {e}") + finally: + if connection: + connection.close() + + def save_curr_seq(self, sequence: Sequence) -> bool: + """ + Function to insert the data coming from the requests to the SQL database + :param sequence: the sequence of characters from user + :return: a boolean value indicating whether the sequence was saved or not + """ + try: + connection = sqlite3.connect(self.db_path) + cursor = connection.cursor() + cursor.execute( + "INSERT INTO sequences (input_string, measurements, response, time) VALUES (?, ?, ?, ?)", + (sequence.input_string, json.dumps(sequence.measurement), + sequence.response, sequence.time)) + connection.commit() + + # Clear input_string and measurements lists + Sequence.input_string = "" + Sequence.measurement = [] + + return True + except sqlite3.Error as e: + print(f"Error saving sequence: {e}") + return False + finally: + if connection: + connection.close() + + def get_history(self) -> list: + """ + Function to return the stored history from the SQL database + :return: a list of history data including the string, measurements, error message, and response + """ + try: + connection = sqlite3.connect(self.db_path) + cursor = connection.cursor() + cursor.execute("SELECT input_string, measurements, time FROM sequences") + rows = cursor.fetchall() + + return rows + except sqlite3.Error as e: + print(f"Error getting history: {e}") + return {"status": "error", "data": None, "error": str(e)} + finally: + if connection: + connection.close() +