diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..37d69e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea/ +*.png +*.jpg +*.gif +*.csv +*.pdf +**/__pycache__/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..52a94a2 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Package Measurement Conversion + +## Overview + +This Measurement Conversion application allows users to input a character string and receive the output in JSON format. +The input is processed into a list showing the total values of measured inflows. +Accessible via a RESTful API endpoint, the application is designed with scalability and extensibility in mind, facilitating easy modifications and expansions. + +## 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. +- **Handling Special Cases:** Handle special cases, such as, the charecter "z" which acts differently. + +## Installation +- Clone the following url in your command prompt: https://github.com/Abbas-Abdulrab/PackageMeasurementConversionAPI.git + +### Prerequisites +- Python 3.x +- CherryPy + +### Setup +1. Clone the repository (see the Installation section above). +2. Install required Python packages from requirements.txt: + ```pip install -r requirements.txt``` +## Usage +- Get a specific string: + + Default: http://localhost:8080/api/sequence/ + Specific port: http://localhost:/api/sequence/ + +- Get history of all conversions: + + Default: http://localhost:8080/api/sequence + + Specific port: http://localhost:/api/sequence + +### Running the Program +- **Script**: + + Default: ```python main.py``` + + Specific port: ```python main_app.py ``` +- **Access URL**: + + Default: http://localhost:8080/ + + Specific port: http://localhost:/ + +## Contributing +Contributions to this project are prohibited due to the course restrictions. + +## License +This project is licensed under the MIT License. + +## Contact +For any queries, please contact Abbas@gmail.com + +## Acknowledgements +Project by Abbas Abdul Rab diff --git a/cherrypy.log b/cherrypy.log new file mode 100644 index 0000000..e69de29 diff --git a/main_app.py b/main_app.py new file mode 100644 index 0000000..014c65c --- /dev/null +++ b/main_app.py @@ -0,0 +1,66 @@ +import cherrypy +import os +import sys + +from src.controller.sequence_controller import SequenceAPI + + +class Root(object): + @cherrypy.expose + def index(self): + """ + This function is going to return index.html files present in the static directory. + We don't need to implement it since it is configured and cherrypy is going to handle it. + """ + pass + + +def setup_server(port=8080): + # Set up logging + log_file = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'cherrypy.log') + cherrypy.log.error_file = log_file + cherrypy.log.access_file = log_file + + # Configure the global settings for CherryPy + cherrypy.config.update({ + 'server.socket_host': '0.0.0.0', + 'server.socket_port': port, + 'log.error_file': log_file, + 'log.screen': True, + 'log.access_file': log_file + }) + + # Mount the ContactsAPI application + cherrypy.tree.mount(SequenceAPI(), '/api/sequence', { + '/': { + 'request.dispatch': cherrypy.dispatch.MethodDispatcher(), # Use method-based dispatching + 'tools.sessions.on': False, + 'tools.response_headers.on': True, + 'tools.response_headers.headers': [('Content-Type', 'text/plain')] + } + }) + + cherrypy.tree.mount(Root(), '/', { + '/': { + 'tools.staticdir.root': os.path.abspath(os.path.dirname(__file__)), + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.index': 'index.html', + } + }) + + # Start the CherryPy server + cherrypy.engine.start() + cherrypy.engine.block() + + +if __name__ == "__main__": + # Default port is 8080 unless specified + port = 8080 + if len(sys.argv) > 1: + try: + port = int(sys.argv[1]) + except ValueError: + print("Invalid port number. Using default port 8080.") + + setup_server(port) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8115f67 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +CherryPy==18.9.0 +requests==2.31.0 + diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/abstraction/__init__.py b/src/abstraction/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/abstraction/dataformat.py b/src/abstraction/dataformat.py new file mode 100644 index 0000000..09ce802 --- /dev/null +++ b/src/abstraction/dataformat.py @@ -0,0 +1,15 @@ +""" +DataFormat is an abstract class that has three subclasses, which minuplate the data into the chosen format, +such as, JSON, SQL +""" + +from abc import ABC, abstractmethod + + +class DataFormat(ABC): + def __init__(self): + self.data_formatter = None + + @abstractmethod + def format(self, input_string): + pass diff --git a/src/controller/__init__.py b/src/controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/controller/sequence_controller.py b/src/controller/sequence_controller.py new file mode 100644 index 0000000..5868620 --- /dev/null +++ b/src/controller/sequence_controller.py @@ -0,0 +1,41 @@ +import cherrypy +import traceback + +from src.models.sequence import Sequence +from src.services.sequence_service import SequenceService +from src.utils.File_Handler import FileHandler +from src.services.sequence_processor import SequenceProcessor +from src.utils.sequence_history import SequenceHistoryModel + + +class SequenceAPI(object): + exposed = True + + @cherrypy.tools.json_out() + def GET(self, input_string: str = ""): + res_msg = {"status": "FAIL", "result": []} + storage_type = "db" + file_handler = FileHandler(storage_type) + sequence = SequenceService(storage_type) + + try: + if input_string: + string_instance = Sequence(input_string) + processor = SequenceProcessor(string_instance) + processed_results = processor.create_result() + + res_msg = {"status": "SUCCESS", "err_msg": "", "result": processed_results} + sequence.input_service(input_string) + + else: + sequencehistory = SequenceHistoryModel() + file_data = [seq.to_dict() for seq in sequencehistory.data] + res_msg = {"status": "SUCCESS", "err_msg": "", "result": file_data} + except Exception as e: + print(traceback.format_exc(e)) + res_msg = {"status": "fail", "err_msg": "invalid sequence", "result": []} + + return res_msg + + + diff --git a/src/data/store_sequence.db b/src/data/store_sequence.db new file mode 100644 index 0000000..e69de29 diff --git a/src/deserializers/__init__.py b/src/deserializers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/models/sequence.py b/src/models/sequence.py new file mode 100644 index 0000000..083f0c1 --- /dev/null +++ b/src/models/sequence.py @@ -0,0 +1,18 @@ +import time +from datetime import datetime + + +class Sequence: + def __init__(self, input_string, created_at_epoch_time=time.time()): + self.input_string = input_string + self.created_at = created_at_epoch_time + + def get_string(self): + return self.input_string + + def to_dict(self): + """Convert the Sequence object attributes to a dictionary for easy JSON serialization.""" + return { + 'input_string': self.input_string, + 'created_at': self.created_at + } diff --git a/src/serializers/JSONdataformat.py b/src/serializers/JSONdataformat.py new file mode 100644 index 0000000..8ae7197 --- /dev/null +++ b/src/serializers/JSONdataformat.py @@ -0,0 +1,18 @@ +import json + +from src.abstraction.dataformat import DataFormat +from src.models.sequence import Sequence + + +class JSONDataFormat(DataFormat): + def format(self, input_string: Sequence): + """ + Formats input data with created time into JSON format. + :param input_string: contains the sequence the user inputted + :return: formatted in JSON data to be saved to JSON file + """ + return json.dumps({ + "input_string": input_string.get_string(), + "input_time": input_string.created_at + }) + "\n" + diff --git a/src/serializers/SQLDataFormat.py b/src/serializers/SQLDataFormat.py new file mode 100644 index 0000000..bdccc5f --- /dev/null +++ b/src/serializers/SQLDataFormat.py @@ -0,0 +1,33 @@ +import sqlite3 + +from src.abstraction.dataformat import DataFormat +from src.models.sequence import Sequence + + +class SQLDataFormat(DataFormat): + db_path = './src/data/sequence_history.db' # Define the database path at the class level if constant + + @staticmethod + def connect(): + """ + Establish a connection to the SQLite database. + """ + return sqlite3.connect(SQLDataFormat.db_path) + + def format(self, input_string: Sequence): + """ + Insert the input string into the SQLite database with the current timestamp. + """ + with self.connect() as conn: + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS sequence_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + input_string TEXT, + created_at TEXT + ); + ''') + cursor.execute('INSERT INTO sequence_history (input_string, created_at) VALUES (?, ?)', + (input_string.get_string(), input_string.created_at)) + conn.commit() + return f"Data inserted: {input_string.get_string()} at {input_string.created_at}" + "\n" diff --git a/src/serializers/__init__.py b/src/serializers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/services/__init__.py b/src/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/services/sequence_processor.py b/src/services/sequence_processor.py new file mode 100644 index 0000000..afa0c68 --- /dev/null +++ b/src/services/sequence_processor.py @@ -0,0 +1,112 @@ +from src.models.sequence import Sequence + + +class SequenceProcessor: + def __init__(self, string_instance: Sequence): + self.input_string = string_instance.get_string() + self.results = [] + self.converted_list = [] + self.combined_list = [] + + def process_string_to_list(self): + """ + Convert the input string to a list of integers based on character positions, + where 'a' is 1, 'z' is 26, and '_' is 0. + """ + for char in self.input_string: + if 'a' <= char <= 'z': + num_chars_to_consider = ord(char) - ord('a') + 1 + self.append_converted_list(num_chars_to_consider) + elif char == '_': + self.append_converted_list(0) + + def append_converted_list(self, value): + """ + :param value: Is the converted chars to position nums from Process_string_to_list func + :return: appends value to converted list. + """ + self.converted_list.append(value) + return self.converted_list + + def append_combined_list(self, num): + """ + + :param num: contains list processed by handle z_case func. + :return: appends new list to combined_list + """ + self.combined_list.append(num) + return self.combined_list + + def append_result(self, seq): + """ + + :param seq: contains value of each sequence. + :return: appends sequence to result. + """ + self.results.append(seq) + return self.results + + def handle_z_case(self): + """ + Combine all consecutive 26s in the list with the next non-26 value. + In order to handle Z cases as intended. + """ + i = 0 + while i < len(self.converted_list): + if self.converted_list[i] == 26: + sum_26 = 0 + while i < len(self.converted_list) and self.converted_list[i] == 26: + sum_26 += self.converted_list[i] + i += 1 + if i < len(self.converted_list): + sum_26 += self.converted_list[i] + self.append_combined_list(sum_26) + i += 1 + else: + self.append_combined_list(self.converted_list[i]) + i += 1 + return self.combined_list + + def create_result(self): + """ + Process the input string to combine consecutive 26s with the next value, + and calculate sums. + """ + if self.input_string.startswith('_'): + return [0] + + self.results = [] + converted_list = self.process_string_to_list() + combined_list = self.handle_z_case() + i = 0 + + while i < len(combined_list): + count = combined_list[i] if i < len(combined_list) else 0 + sum_sequence = 0 + if count + i + 1 > len(combined_list): + self.results = [] + break + elif count > 0 and i + 1 < len(combined_list): + sum_sequence = sum(combined_list[i + 1:i + 1 + count]) + self.append_result(sum_sequence) + i += count + 1 + + if i >= len(combined_list) and (count == 0 or i + 1 >= len(combined_list)): + break + + return self.results + + +# Example usage +if __name__ == "__main__": + test_strings = [ + 'aaa', 'dz_a_aazzaaa', 'a_', 'abcdabcdab', 'abcdabcdab_', + 'zdaaaaaaaabaaaaaaaabaaaaaaaabbaa', + '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_', + 'za_a_a_a_a_a_a_a_a_a_a_a_a_azaaa', '_a', '_', '_ad', '_zzzb', '__' + ] + + for test_string in test_strings: + processor = SequenceProcessor(test_string) + result = processor.create_result() + print(f"Input: {test_string} -> Got: {result}") diff --git a/src/services/sequence_service.py b/src/services/sequence_service.py new file mode 100644 index 0000000..4fdaba3 --- /dev/null +++ b/src/services/sequence_service.py @@ -0,0 +1,32 @@ +from src.models.sequence import Sequence +from src.services.sequence_processor import SequenceProcessor +from src.utils.File_Handler import FileHandler + + +class SequenceService: + def __init__(self, storage_format): + self.file_handler = FileHandler(storage_format=storage_format) + + def input_service(self, input_string): + """ + Takes an input string, processes it using StringProcessor, and stores the results. + """ + # Process the string using StringProcessor + sequence = Sequence(input_string) + processor = SequenceProcessor(sequence) + processed_results = processor.create_result() + + # Convert results to the desired format (JSON, CSV, TXT) + formatted_data = self.file_handler.dataformat.format(sequence) + + # Store the formatted data + self.file_handler.open_write_file(data=formatted_data, state="a") + return processed_results, formatted_data + + +# Example usage +if __name__ == "__main__": + sequence_service = SequenceService(storage_format='db') + test_string = 'dz_a_aazzaaa' + results, formatted = sequence_service.input_service(test_string) + print(f"Processed Results: {results}, Formatted and Stored: {formatted}") diff --git a/src/services/unittest/test_sequence_processor.py b/src/services/unittest/test_sequence_processor.py new file mode 100644 index 0000000..9ee9dc2 --- /dev/null +++ b/src/services/unittest/test_sequence_processor.py @@ -0,0 +1,41 @@ +import unittest + +from src.models.sequence import Sequence +from src.services.sequence_processor import SequenceProcessor + +# tests to run +CURRENT_VS_EXPECTED_VALUE_MAPPING = ( + ("aa", [1]), + ("abbcc", [2, 6]), + ("dz_a_aazzaaa", [28, 53, 1]), + ("a_", [0]), + ("abcdabcdab", [2, 7, 7]), + ("abcdabcdab_", [2, 7, 7, 0]), + ("zdaaaaaaaabaaaaaaaabaaaaaaaabbaa", [34]), + ("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]), + ("za_a_a_a_a_a_a_a_a_a_a_a_a_azaaa", [40, 1]), + ("_", [0]), + ("_ad", [0]), + ("a_", [0]), + ("_zzzb", [0]), + ("__", [0]) +) + + +class TestSequenceProcessor(unittest.TestCase): + + def test_sequence_processing(self): + """ + Tests the logic of the Sequence processor and checks whether the result is as expected + :return: Test OK + """ + for test_string, expected in CURRENT_VS_EXPECTED_VALUE_MAPPING: + sequence_instance = Sequence(test_string) + processor = SequenceProcessor(sequence_instance) + result = processor.create_result() + print(f"result {result} expected {expected}") + self.assertEqual(result, expected) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/utils/File_Handler.py b/src/utils/File_Handler.py new file mode 100644 index 0000000..0d22d38 --- /dev/null +++ b/src/utils/File_Handler.py @@ -0,0 +1,47 @@ +from src.serializers.JSONdataformat import JSONDataFormat +from src.models.sequence import Sequence +from src.serializers.SQLDataFormat import SQLDataFormat + + +class FileHandler: + BASE_FILE_PATH = "./src/data/store_sequence" + + def __init__(self, storage_format): + self.set_datastore(storage_format) + self.storage_format = storage_format + self.dataformat = self.set_datastore(storage_format) + self.INPUT_FILE_PATH = f"{self.BASE_FILE_PATH}.{storage_format}" + + def format_record(self, input_string): + """ + :param input_string: input_string + :return: chooses the format of file based on user input + """ + inputstring = Sequence(input_string) + return self.dataformat.format(inputstring) + + def set_datastore(self, storage_format): + """ + + :param storage_format: changes format type based on what's written in the + controller. + :return: If it's db, then the db format is chosen, if JSON, the JSON format is chosen. + """ + if storage_format == 'db': + return SQLDataFormat() + elif storage_format == 'json': + return JSONDataFormat() + self.INPUT_FILE_PATH = f"{self.BASE_FILE_PATH}.{storage_format}" + + def open_write_file(self, data="", state="r"): + """ + contains all file related functions such as, Write, Read, Append to main file + :param data: user inputted record + :param state: a, r, or w + :return: Data Saved Successfully! + """ + with open(self.INPUT_FILE_PATH, state) as input_file: + if state in ["a", "w"]: + input_file.write(data) + elif state == "r": + return [x.strip() for x in input_file.readlines()] diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/sequence_history.py b/src/utils/sequence_history.py new file mode 100644 index 0000000..03ab12e --- /dev/null +++ b/src/utils/sequence_history.py @@ -0,0 +1,49 @@ +from src.models.sequence import Sequence + + +import sqlite3 +from datetime import datetime + + +class SequenceHistoryModel: + def __init__(self, db_path="./src/data/sequence_history.db"): + self.db_path = db_path + self.data = [] + self.load_sequences() + + def connect(self): + """ + Establish a connection to the SQLite database. + """ + return sqlite3.connect(self.db_path) + + def fetch_sequences_from_db(self): + """ + Fetches all sequences from the database and returns them as a list of dictionaries. + """ + sequences = [] + try: + with self.connect() as conn: + cursor = conn.cursor() + cursor.execute("SELECT input_string, created_at FROM sequence_history") + rows = cursor.fetchall() + for row in rows: + formatted = datetime.fromtimestamp(float(row[1])).strftime("%Y-%m-%d %H:%M:%S") + sequences.append({'input_string': row[0], 'created_at': formatted}) + return sequences + except FileNotFoundError as file: + print("File Not found") + except Exception as ex: + print("Error", ex) + + def load_sequences(self): + """ + Load sequences from the database into the model. + """ + try: + sequences_from_db = self.fetch_sequences_from_db() + for seq in sequences_from_db: + sequence = Sequence(seq['input_string'], seq['created_at']) + self.data.append(sequence) + except Exception as ex: + print("Error", ex)