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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.idea/
*.png
*.jpg
*.gif
*.csv
*.pdf
**/__pycache__/
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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/<enter_string_here>
Specific port: http://localhost:<enter_port_here>/api/sequence/<enter_string_here>

- Get history of all conversions:

Default: http://localhost:8080/api/sequence

Specific port: http://localhost:<enter_port_here>/api/sequence

### Running the Program
- **Script**:

Default: ```python main.py```

Specific port: ```python main_app.py <enter_port>```
- **Access URL**:

Default: http://localhost:8080/

Specific port: http://localhost:<enter_port>/

## 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 [email protected]

## Acknowledgements
Project by Abbas Abdul Rab
Empty file added cherrypy.log
Empty file.
66 changes: 66 additions & 0 deletions main_app.py
Original file line number Diff line number Diff line change
@@ -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)
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

Empty file added src/__init__.py
Empty file.
Empty file added src/abstraction/__init__.py
Empty file.
15 changes: 15 additions & 0 deletions src/abstraction/dataformat.py
Original file line number Diff line number Diff line change
@@ -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
Empty file added src/controller/__init__.py
Empty file.
41 changes: 41 additions & 0 deletions src/controller/sequence_controller.py
Original file line number Diff line number Diff line change
@@ -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



Empty file added src/data/store_sequence.db
Empty file.
Empty file added src/deserializers/__init__.py
Empty file.
Empty file added src/models/__init__.py
Empty file.
18 changes: 18 additions & 0 deletions src/models/sequence.py
Original file line number Diff line number Diff line change
@@ -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
}
18 changes: 18 additions & 0 deletions src/serializers/JSONdataformat.py
Original file line number Diff line number Diff line change
@@ -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"

33 changes: 33 additions & 0 deletions src/serializers/SQLDataFormat.py
Original file line number Diff line number Diff line change
@@ -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"
Empty file added src/serializers/__init__.py
Empty file.
Empty file added src/services/__init__.py
Empty file.
112 changes: 112 additions & 0 deletions src/services/sequence_processor.py
Original file line number Diff line number Diff line change
@@ -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}")
Loading