Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/
.idea/
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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```
Empty file added __init__.py
Empty file.
Empty file added controller/__init__.py
Empty file.
65 changes: 65 additions & 0 deletions controller/converter_controller.py
Original file line number Diff line number Diff line change
@@ -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}


23 changes: 23 additions & 0 deletions main_app.py
Original file line number Diff line number Diff line change
@@ -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()
Empty file added models/__init__.py
Empty file.
9 changes: 9 additions & 0 deletions models/sequence.py
Original file line number Diff line number Diff line change
@@ -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

3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CherryPy==18.9.0
requests==2.31.0
setuptools==69.2.0
124 changes: 124 additions & 0 deletions services/package_converter.py
Original file line number Diff line number Diff line change
@@ -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]
# #
71 changes: 71 additions & 0 deletions services/test/test_package_conversion.py
Original file line number Diff line number Diff line change
@@ -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()
Loading