diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ae8ce5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..c366fbb --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +## Package Conversion API Overview +The Package Conversion API functions is a web-based service designed to facilitate the conversion of package measurements as per user specifications. It offers endpoints specifically for converting measurements and accessing conversion records. + +## Features +- **Seamless Handling of Sequence Input:** Capable of processing sequences comprising characters and underscores, facilitating smooth conversion. +- **Optimized Conversion Algorithms:** Incorporates streamlined algorithms for the swift transformation of input sequences into lists of measurements. +- **Transparent and Flexible:** Exhibits clarity in design and is easily customizable, allowing for seamless integration of supplementary features. + +## Installation +Clone the repository: +``` +git clone https://github.com/aljab017/PackageMeasurementConversionAPI.git +``` + +## Install dependencies: +``` +pip install -r requirements.txt +``` + +## Usage +- Start the server: +```python main.py``` +- Convert measurement: +```GET http://127.0.0.1:8080/convert_measurements?input_str=INPUT STRING HERE``` +- Get conversion history +```GET http://127.0.0.1:8080/get_history``` +## Testing + +- To run tests: +``` python -m unittest .\models\tests\measurement_converter_tests.py ``` \ No newline at end of file diff --git a/controller/measurement_converter_api.py b/controller/measurement_converter_api.py new file mode 100644 index 0000000..14dd75f --- /dev/null +++ b/controller/measurement_converter_api.py @@ -0,0 +1,42 @@ +import cherrypy +import json +from services.measurement_converter import MeasurementConverter +from utils.measurement_converter_db import MeasurementConverterDB +from models.measurement_history import MeasurementHistory + +class MeasurementConverterAPI(object): + + @cherrypy.expose + @cherrypy.tools.json_in() + @cherrypy.tools.json_out() + def convert_measurements(self, input_str=None): + """ + Convert the measurements to the sum of letter values for each segment. + """ + db_entry = MeasurementHistory + db_entry.input = input_str + try: + if input_str is None: + raise cherrypy.HTTPError(400, "Missing input parameter") + db_entry.output = MeasurementConverter().package_measurement_converter(input_str) + MeasurementConverterDB().save_to_history(db_entry.input, db_entry.output) + error_msg = "Invalid string input" if db_entry.output == "Invalid string input" else "" + status = "SUCCESS" if error_msg == "" else "FAIL" + result = db_entry.output if error_msg == "" else [] + return {"status": status, "error_msg": error_msg, "result": result} + except Exception as e: + return {"status": "FAIL", "error_msg": str(e), "result": []} + + @cherrypy.expose + @cherrypy.tools.json_out() + def get_history(self): + """ + Get the history of the measurements from the database. + """ + try: + db_instance = MeasurementConverterDB() + except: + raise cherrypy.HTTPError(500, "Database connection error") + + return db_instance.get_history() + diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py new file mode 100644 index 0000000..ae873b7 --- /dev/null +++ b/main.py @@ -0,0 +1,15 @@ +import cherrypy +import logging +from controller.measurement_converter_api import MeasurementConverterAPI + + +if __name__ == '__main__': + # Configure logging + logging.basicConfig(filename='./logs/measurement_log.log', level=logging.CRITICAL, + format='%(asctime)s:%(levelname)s:%(message)s') + + cherrypy.tree.mount(MeasurementConverterAPI(), '/') + cherrypy.config.update({'server.socket_host': '0.0.0.0'}) + cherrypy.config.update({'server.socket_port': 8080}) + cherrypy.engine.start() + cherrypy.engine.block() \ No newline at end of file diff --git a/models/measurement_history.py b/models/measurement_history.py new file mode 100644 index 0000000..e563e94 --- /dev/null +++ b/models/measurement_history.py @@ -0,0 +1,7 @@ +class MeasurementHistory: + """ + Class to represent a measurement history object + """ + def __init__(self, input_str, output): + self.input = input_str + self.output = output 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/measurement_converter.py b/services/measurement_converter.py new file mode 100644 index 0000000..3139ac3 --- /dev/null +++ b/services/measurement_converter.py @@ -0,0 +1,71 @@ +class MeasurementConverter: + def __init__(self, string=None): + self.string = string + + def get_letter_value(self, letter): + """ + Get the numerical value of a letter. + """ + if letter == 'z': + return 26 + else: + return ord(letter) - 96 + + def package_measurement_converter(self, string): + """ + Parse the string and calculate the sum of letter values for each segment. + """ + self.string = string + result = [] + index = 0 + try: + # Iterate through the string + while index < len(string): + if string[index].isalpha(): + count = 0 + # If the letter is z, add 26 to the count until you find the first non-z letter/char + if string[index] == 'z': + while string[index] == 'z': + count += self.get_letter_value(string[index]) + index += 1 + if string[index].isalpha(): + count += self.get_letter_value(string[index]) + index += 1 + else: + count = self.get_letter_value(string[index]) + index += 1 + + sum_value = 0 + i = index # i determains the start of the segment while index is a pointer to the current character + range = index + count + + # Extract the sum of letter values for the segment + while i < (range): + if string[i].isalpha(): + if string[i] == 'z': + range += 1 + while string[i] == 'z': + sum_value += self.get_letter_value(string[i]) + index += 1 + i += 1 + if string[i].isalpha(): + sum_value += self.get_letter_value(string[i]) + else: + sum_value += self.get_letter_value(string[i]) + i += 1 + + result.append(sum_value) + index += count + + elif string[index] == '_': + # Append 0 and break the loop if the first character is '_' + result.append(0) + break + + else: + # For any other character that is not a letter or '_', return "Invalid string input" + return "Invalid string input" + + except: + return "Invalid string input" + return result diff --git a/services/tests/measurement_converter_tests.py b/services/tests/measurement_converter_tests.py new file mode 100644 index 0000000..7893917 --- /dev/null +++ b/services/tests/measurement_converter_tests.py @@ -0,0 +1,39 @@ +import unittest +from services.measurement_converter import MeasurementConverter + + +class TestMeasurementConverter(unittest.TestCase): + + def test_non_z_strings(self): + """ + Test valid non z strings. + """ + non_z_inputs = ["aa", "abbcc", "abcdabcdab", "a_" ] + non_z_outputs = [[1], [2, 6], [2, 7, 7], [0]] + + for i in range(len(non_z_inputs)): + self.assertEqual(MeasurementConverter().package_measurement_converter(non_z_inputs[i]), non_z_outputs[i]) + + + def test_z_strings(self): + """ + Test valid z strings. + """ + z_inputs = ["dz_a_aazzaaa", "za_a_a_a_a_a_a_a_a_a_a_a_a_azaaa", "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_", "_zzzb", "_"] + z_outputs = [[28, 53, 1], [40, 1], [26], [0], [0]] + + for i in range(len(z_inputs)): + self.assertEqual(MeasurementConverter().package_measurement_converter(z_inputs[i]), z_outputs[i]) + + def test_invalid_strings(self): + """ + Test invalid strings. + """ + invalid_inputs = ["abc", "aaa", "z", "123", "+!@#"] + + for i in range(len(invalid_inputs)): + self.assertEqual(MeasurementConverter().package_measurement_converter(invalid_inputs[i]), "Invalid string input") + + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/utils/measurement_converter_db.py b/utils/measurement_converter_db.py new file mode 100644 index 0000000..c645903 --- /dev/null +++ b/utils/measurement_converter_db.py @@ -0,0 +1,41 @@ +import sqlite3 +import json + +class MeasurementConverterDB(object): + def __init__(self, db_name='./data/history.db'): + self.db_name = db_name + self.conn = sqlite3.connect(self.db_name) + self.cursor = self.conn.cursor() + self.create_table() + + def create_table(self): + """ + Create the table for the history of the measurements. + """ + self.cursor.execute(''' + CREATE TABLE IF NOT EXISTS history ( + id INTEGER PRIMARY KEY, + input TEXT, + output TEXT, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + self.conn.commit() + + def save_to_history(self, input_str, output_str): + """ + Save the input and output to the history table. + """ + output_str = str(output_str) + self.cursor.execute('INSERT INTO history (input, output) VALUES (?, ?)', (input_str, output_str)) + self.conn.commit() + + def get_history(self): + """ + Get the history of the measurements from the database. + """ + self.cursor.execute("SELECT input, output FROM history ORDER BY timestamp DESC") + rows = self.cursor.fetchall() + history = [{'input': row[0], 'output': row[1]} for row in rows] + return history +