diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2483976 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +__pycache__/ diff --git a/Controllers/sequence_measuremnet_conversion_api.py b/Controllers/sequence_measuremnet_conversion_api.py new file mode 100644 index 0000000..6465cbd --- /dev/null +++ b/Controllers/sequence_measuremnet_conversion_api.py @@ -0,0 +1,27 @@ +import cherrypy +from Services.sequence_processor import sequence_compressor +from Models.sequence_history import SequenceHistory + +class SequenceConverterAPI: + def __init__(self, db_file): + # Initialize SequenceHistory with the provided database file + self.sequence_history = SequenceHistory(db_file) + + @cherrypy.expose + @cherrypy.tools.json_out() + def convert_measurements(self, input): + # Call sequence_compressor to process the input and get compressed values + compressed_values = sequence_compressor(input) + + # Save the sequence and its compressed values to the database + self.sequence_history.save_sequence({'input': input, 'compressed_values': compressed_values}) + + # Return the compressed values in JSON format + return compressed_values + + @cherrypy.expose + @cherrypy.tools.json_out() + def get_history(self): + # Get the history of sequences from the database + return self.sequence_history.get_history() + diff --git a/Logs/logs.log b/Logs/logs.log new file mode 100644 index 0000000..cb34fe0 --- /dev/null +++ b/Logs/logs.log @@ -0,0 +1,13 @@ +2024-05-01 11:03:09,106:INFO:[01/May/2024:11:03:09] ENGINE Listening for SIGTERM. +2024-05-01 11:03:09,107:INFO:[01/May/2024:11:03:09] ENGINE Bus STARTING +2024-05-01 11:03:09,108:INFO:[01/May/2024:11:03:09] ENGINE Started monitor thread 'Autoreloader'. +2024-05-01 11:03:09,324:INFO:[01/May/2024:11:03:09] ENGINE Serving on http://0.0.0.0:8080 +2024-05-01 11:03:09,324:INFO:[01/May/2024:11:03:09] ENGINE Bus STARTED +2024-05-01 11:03:12,099:INFO:[01/May/2024:11:03:12] ENGINE Keyboard Interrupt: shutting down bus +2024-05-01 11:03:12,099:INFO:[01/May/2024:11:03:12] ENGINE Bus STOPPING +2024-05-01 11:03:12,274:INFO:[01/May/2024:11:03:12] ENGINE HTTP Server cherrypy._cpwsgi_server.CPWSGIServer(('0.0.0.0', 8080)) shut down +2024-05-01 11:03:12,274:INFO:[01/May/2024:11:03:12] ENGINE Stopped thread 'Autoreloader'. +2024-05-01 11:03:12,287:INFO:[01/May/2024:11:03:12] ENGINE Bus STOPPED +2024-05-01 11:03:12,288:INFO:[01/May/2024:11:03:12] ENGINE Bus EXITING +2024-05-01 11:03:12,288:INFO:[01/May/2024:11:03:12] ENGINE Bus EXITED +2024-05-01 11:03:12,289:INFO:[01/May/2024:11:03:12] ENGINE Waiting for child threads to terminate... diff --git a/Models/sequence_history.py b/Models/sequence_history.py new file mode 100644 index 0000000..e941e2a --- /dev/null +++ b/Models/sequence_history.py @@ -0,0 +1,36 @@ +# sequence_history.py +import sqlite3 + +class SequenceHistory: + def __init__(self, db_file): + # Store the path to the SQLite database file + self.db_file = db_file + + def save_sequence(self, sequence_data): + # Connect to the SQLite database and save the sequence data + with sqlite3.connect(self.db_file) as conn: + cursor = conn.cursor() + sequence = sequence_data['input'] + compressed_values = ','.join(map(str, sequence_data['compressed_values'])) + # Insert the sequence and compressed values into the 'sequences' table + cursor.execute("INSERT INTO sequences (sequence, compressed_values) VALUES (?, ?)", + (sequence, compressed_values)) + conn.commit() + + def get_history(self): + # Connect to the SQLite database and retrieve sequence history + with sqlite3.connect(self.db_file) as conn: + cursor = conn.cursor() + # Execute SQL query to select sequences and order by timestamp in descending order + cursor.execute("SELECT sequence, compressed_values, timestamp FROM sequences ORDER BY timestamp DESC") + rows = cursor.fetchall() + history = [] + # Iterate over rows and format data into a list of dictionaries + for row in rows: + history.append({ + 'sequence': row[0], + 'compressed_values': list(map(int, row[1].split(','))), + 'timestamp': row[2] + }) + return history + diff --git a/Services/sequence_processor.py b/Services/sequence_processor.py new file mode 100644 index 0000000..847389d --- /dev/null +++ b/Services/sequence_processor.py @@ -0,0 +1,90 @@ +# sequence_processor.py +from abc import ABC, abstractmethod + +class SequenceProcessorBase(ABC): + @abstractmethod + def process_sequence(self): + # Abstract method to process a sequence of numbers + pass + +class SequenceConverter: + def __init__(self, sequence): + # Initialize with the input sequence and an empty list for numerical values + self.sequence = sequence + self.numerical_values = [] + + def convert_to_numeric_list(self): + # Convert the input sequence into a list of numerical values + in_z_sequence = False + z_accumulated_sum = 0 + + for char in self.sequence: + # Determine the offset value for ASCII mapping + offset_val = 97 if char != "_" else 96 + # Convert character to numerical value based on ASCII and 'z' sequence logic + char_value = ord(char) - offset_val + 1 + + if char == "z": + # Track if in 'z' sequence and accumulate sum + in_z_sequence = True + z_accumulated_sum += char_value + else: + in_z_sequence = False + if not in_z_sequence: + if z_accumulated_sum > 0: + # Add accumulated sum and reset if not in 'z' sequence + z_accumulated_sum += char_value + self.numerical_values.append(z_accumulated_sum) + z_accumulated_sum = 0 + else: + # Add character value if not in 'z' sequence + self.numerical_values.append(char_value) + elif char == "_": + # Append accumulated sum if encounter '_' and reset + self.numerical_values.append(z_accumulated_sum) + z_accumulated_sum = 0 + in_z_sequence = False + + return self.numerical_values + +class CompressedSequenceProcessor(SequenceProcessorBase): + def __init__(self, numerical_values): + # Initialize with the numerical values + self.numerical_values = numerical_values + + def process_sequence(self): + # Process numerical values to generate compressed sequence + compressed_values = [] + is_step = True + step_remaining = 0 + current_sum = 0 + + for num in self.numerical_values: + if is_step: + # Set current number as a step if True + step_remaining = num + is_step = False + elif step_remaining > 0: + # Accumulate numbers until step_remaining reaches 0 + current_sum += num + step_remaining -= 1 + + if step_remaining == 0: + # Append the result and reset cycle when step_remaining is 0 + compressed_values.append(current_sum) + current_sum = 0 + is_step = True + + return compressed_values + +def sequence_compressor(sequence): + # Main function to convert a sequence into compressed values + converter = SequenceConverter(sequence) + numerical_values = converter.convert_to_numeric_list() + + if not numerical_values: + return ["Invalid Input"] + print("Numerical Values:", numerical_values) + processor = CompressedSequenceProcessor(numerical_values) + print("Measured Inflows:") + return processor.process_sequence() diff --git a/Utillities/README.md b/Utillities/README.md new file mode 100644 index 0000000..a7cdbaf --- /dev/null +++ b/Utillities/README.md @@ -0,0 +1,141 @@ +# Package Measurement Conversion API + +## Introduction + +This API converts measurement input strings into a list of total values of measured inflows for each package. It follows a specific encoding format where a number represents the count of values measured in each cycle, followed by measured values encoded using alphabetical characters ('a' for 1, 'b' for 2, and so on). + +### Algorithm Summary + +- Each character in the input sequence corresponds to a specific value. +- The algorithm processes the input sequence character by character. +- Special cases, such as 'z', are handled where a single character may represent multiple characters. +- The algorithm calculates the sum of values based on specific rules for each character. +- The sequence restarts after each calculation until the end of the input sequence is reached. + +## Operation + +1. Start with the first character in the input sequence. +2. Determine the value of the character based on predefined rules. +3. If a special case like 'z' is encountered, treat it and the following characters as a single continuous character. +4. Calculate the sum of values for a specified number of characters after the current character. +5. Repeat the process until the end of the input sequence is reached. + +## Examples + +1. **Input:** abbcc + - **Response:** [2, 6] + - **Explanation:** + - 'a' -> 1 + - 'b' -> 2 (consider 2 characters after 'b': 'c', 'c') + - 'c' -> 3 (consider 2 characters after 'c': 'c') + - Sum: 2 + 6 = 8 + +2. **Input:** abcdabcdab + - **Response:** [2, 7, 7] + - **Explanation:** + - 'a' -> 1 + - 'b' -> 2 (consider 2 characters after 'b': 'c', 'd') + - 'c' -> 3 (consider 3 characters after 'c': 'd', 'a', 'b') + - 'd' -> 4 + - Sum: 2 + 7 + 7 = 16 + +3. **Input:** dz_a_aazzaaa + - **Response:** [28, 53, 1] + - **Explanation:** (Include the detailed explanation of the 'z' case here) + +4. **Input:** zd_aaaaaaaaaabaaaaaaaabaaaaaaaabbaa + - **Response:** [34] + - **Explanation:** (Include the detailed explanation of the 'z' case here) + +This algorithm effectively encodes or decodes input sequences by assigning values to characters and summing them according to specified rules, providing a concise representation of the sequence. + + +## Prerequisites + +- Python 3.x +- CherryPy + +## Installation + +1. Clone the repository to your local machine: + +git clone https://github.com/jnulia/PackageMeasurementConversionAPI.git + + +2. Install dependencies: + +pip install -r requirements.txt + + +## Running the Application + +1. Navigate to the project directory. +2. Run the main application file with Python: + +python main_app.py + + +You can specify a custom port using the --port argument: + +python main_app.py --port + + +## API Usage + +### Conversion Endpoint + +- **Endpoint:** /convert-measurements +- **Method:** GET +- **Query Parameter:** +- input: Measurement input string +- **Response:** JSON array containing the total values of measured inflows for each package + +#### Example Usage: + +Assuming the API is running locally on http://localhost:8080: + +1. Convert measurements: + +GET http://localhost:8080/convert-measurements?input=dz_a_aazzaaa + + +**Response:** + +[28, 53, 1] + + +### History Endpoint + +- **Endpoint:** /get-history +- **Method:** GET +- **Response:** JSON array containing the history of all data from requests + +#### Example Usage: + +Assuming the API is running locally on http://localhost:8080: + +2. Retrieve history: + +GET http://localhost:8080/get-history + + +**Response:** +[ +{ +"sequence": "dz_a_aazzaaa", +"compressed_values": [28, 53, 1], +"timestamp": "2024-04-29 12:00:00" +}, +... +] + + +## Testing + +1. Execute the test cases in the following: + +``` python -m unittest .\tests\test_sequence_processor.py + + +License +This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/Utillities/requirements.txt b/Utillities/requirements.txt new file mode 100644 index 0000000..cdd578d --- /dev/null +++ b/Utillities/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/Utillities/sequence_history.db b/Utillities/sequence_history.db new file mode 100644 index 0000000..62cd92d Binary files /dev/null and b/Utillities/sequence_history.db differ diff --git a/main_app.py b/main_app.py new file mode 100644 index 0000000..049327b --- /dev/null +++ b/main_app.py @@ -0,0 +1,24 @@ +# main_app.py +import cherrypy +from Controllers.sequence_measuremnet_conversion_api import SequenceConverterAPI +import argparse +import logging + +if __name__ == '__main__': + # Configure LOGGING to file + logging.basicConfig(filename='./Logs/logs.log', level=logging.CRITICAL, + format='%(asctime)s:%(levelname)s:%(message)s') + + # Parse command-line arguments + parser = argparse.ArgumentParser(description='Package Measurement Conversion API') + parser.add_argument('--port', type=int, default=8080, help='Port number to run the server on') + args = parser.parse_args() + + # Path to SQLite database file + db_file = 'Utillities/sequence_history.db' + + # Update CherryPy configuration with host and port + cherrypy.config.update({'server.socket_host': '0.0.0.0', 'server.socket_port': args.port}) + + # Start the CherryPy server with the SequenceConverterAPI instance + cherrypy.quickstart(SequenceConverterAPI(db_file)) diff --git a/tests/test_sequence_processor.py b/tests/test_sequence_processor.py new file mode 100644 index 0000000..509cd3e --- /dev/null +++ b/tests/test_sequence_processor.py @@ -0,0 +1,28 @@ +import unittest +from Services.sequence_processor import sequence_compressor + +class TestSequenceProcessor(unittest.TestCase): + def test_sequence_compressor(self): + # Define test cases with input sequences and expected outputs + test_cases = [ + ("dz_a_aazzaaa", [28, 53, 1]), + ("za_a_a_a_a_a_a_a_a_a_a_a_a_azaaa", [40, 1]), + ("aa", [1]), + ("abbcc", [2, 6]), + ("a_", [0]), + ("abcdabcdab", [2, 7, 7]), + ("zdaaaaaaaabaaaaaaaabaaaaaaaabbaa", [34]), + ("za_a_a_a_a_a_a_a_a_a_a_a_a_azaaa", [40, 1]), + ("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]) + ] + + # Iterate through test cases and run sub-tests + for sequence, expected_output in test_cases: + with self.subTest(sequence=sequence): + # Call sequence_compressor function with each sequence + result = sequence_compressor(sequence) + # Assert the result matches the expected output + self.assertEqual(result, expected_output) + +if __name__ == '__main__': + unittest.main()