"""
The module PySecondo implements an API for interaction between Python and Secondo.
After stablishing connections to Secondo server and Optimizer server reading the cofiguration parameters from a file,
the queries canbe executed.
"""
import asyncio
import nest_asyncio
nest_asyncio.apply()
import time
from config_pkg.load_secondo_config import *
from optimizer_pkg.optimizer_server import *
from optimizer_pkg.optimize_query import *
from libs_pkg.nested_list import *
from libs_pkg.command_execution import *
from libs_pkg.exception_handler import *
from secondo_datatypes_pkg.secondo_int import *
from secondo_datatypes_pkg.secondo_real import *
from secondo_datatypes_pkg.Attribute import *
from secondo_datatypes_pkg.Tuple import *
from secondo_datatypes_pkg.Relation import *
class Secondo():
"""
This class contains attributes and methods for connecting to a running Secondo server,
executing commands and queries, returning the result sets and parsing the results to implemented
data structurs in python.
"""
def __init__(self, secondo_server='127.0.0.1', secondo_port=1234, optimizer_host='127.0.0.1',
optimizer_port=1235, username='user', password='secret', bin_format = True):
"""
This constructor which reads from configuration file, connects to secondo server
and initialises the object to connect to Optimizer server.
"""
self.server = secondo_server
self.port = secondo_port
self.user = username
self.password = password
self.bin_format = bin_format
self.initialized = False
self.conn = None
self.reader = None
self.writer = None
self.opendb = ""
#loop = asyncio.get_event_loop()
#loop.run_until_complete(self.connect())
asyncio.run(self.connect())
self.opt = Optimizer(optimizer_host, optimizer_port)
self.opt_reader, self.opt_writer = self.opt.get_opt_streams()
self.result_error_code = None
self.result_error_message = None
self.result = []
self.stream_result = []
async def connect(self):
"""
This method connects to Secondo server using asyncio methods and sets the connection,
stream- reader and writer attributes of the Secondo class.
"""
if self.initialized == True:
raise SecondoAPI_Error('Secondo already initialised.')
try:
self.conn = asyncio.open_connection(self.server, int(self.port))
except ConnectionRefusedError as e:
raise SecondoAPI_Error(e.args[1] + ' - Connection to Secondo server refused.')
except OSError as e:
raise SecondoAPI_Error(e.args[1] + ' - Connection to Secondo couldnt be stablished.')
try:
self.reader, self.writer = await asyncio.wait_for(self.conn, timeout = 10)
except ConnectionRefusedError as e:
raise SecondoAPI_Error(e.args[1] + ' - Stream reader and stream writer for Secondo couldnt be initialised.')
except OSError as e:
raise SecondoAPI_Error(e.args[1] + ' - Stream reader and stream writer for Secondo couldnt be initialised.')
message = "\n" + self.user + "\n" + self.password + "\n" + "\n"
while True:
line = await self.reader.readline()
if not line:
raise SecondoAPI_Error('An empty line was read from Secondo - Connection to Server failed.')
break
line = line.decode()
if line != '\n':
#print(f'Received: {line!r}')
break
if line == '\n':
#print(f'Received: {line}')
#print(message)
self.writer.write(message.encode())
await self.writer.drain()
while True:
line = await self.reader.readline()
if not line:
raise SecondoAPI_Error('An empty line was read from Secondo - Connection to Server failed.')
break
line = line.decode()
if line == '\n':
#print(f'Received: {line!r}')
while True:
line = await self.reader.readline()
if not line:
raise SecondoAPI_Error('An empty line was read from Secondo - Connection to Server failed.')
line = line.decode()
if line == '\n':
#print(f'Received: {line!r}')
self.initialized = True
print("Connection To Secondo Server established...")
break
elif line == '\n':
raise SecondoAPI_Error('Error Tag received from Secondo- Connection to Server failed.')
#print(f'Received: {line!r}')
#else:
#print(f'Received: {line!r}')
break
break
#print(self.initialized)
async def optimization_check(self, command):
"""
This method checks if the query need to be optimized then returns the optimized command
by calling the function opt_comm_exec() otherwise returns the unchanged query.
:param command: The command or query to be executed.
:return: The optimized query.
"""
update_comm = False
catalog_comm = False
select_comm = True
if command.startswith('sql') or command.startswith('sql\n'):
update_comm = True
elif "insert into " in command:
update_comm = True
elif "delete from " in command:
update_comm = True
elif "update " in command:
update_comm = True
elif "create table" in command:
update_comm = True
catalog_comm = True
select_comm = False
elif "drop" in command:
update_comm = True
catalog_comm = True
select_comm = False
elif "delete" in command:
update_comm = True
catalog_comm = True
select_comm = False
elif "select" in command:
update_comm = True
if update_comm:
if len(self.get_opendb()) == 0:
raise SecondoError("No open Database.")
if not self.opt.get_opt_conn():
raise SecondoAPI_Error('Connection to OptimizerServer reset.')
if catalog_comm:
opt_res = await opt_comm_exec(self.opt_reader, self.opt_writer, command, self.get_opendb(), True)
if opt_res:
await self.reopen_db()
return None
else:
raise SecondoError("Optimization failed.")
elif select_comm:
opt_res = await opt_comm_exec(self.opt_reader, self.opt_writer, command, self.get_opendb(), False)
if opt_res:
return "query " + opt_res
else:
raise SecondoError("Optimization failed.")
else:
return command
async def reopen_db(self):
"""
This method closes the currently open database and opens it again.
It is needed after executing some query types like catalog updates.
:return: True/ False according to the success of operation.
"""
if len(self.get_opendb()) == 0:
return False
db = self.get_opendb()
if not self.initialized:
raise SecondoAPI_Error('Connection to ScondoServer has not been initialised.')
if self.conn is None:
raise SecondoAPI_Error('Connection to ScondoServer reset.')
res = await self.command_exec("close database")
self.set_opendb('')
if not self.initialized:
raise SecondoAPI_Error('Connection to ScondoServer has not been initialised.')
if self.conn is None:
raise SecondoAPI_Error('Connection to ScondoServer reset.')
res = await self.command_exec("open database " + db)
self.set_opendb(db)
return res[0] == 0
async def command_exec(self, command, tupel_source = None):
"""
This method executes the command/query in three categories (save, restore, general)
by calling relevant functions, sets the attributes result_error_code,
result_error_message and result of the Secondo object.
:param command: The command or query to be executed.
:param tupel_source: the source for producing a stream of tupels, needed by queries like pyreceive
which send a stream of tuples to Secondo server.
:return: the result of command/ query as a python nested list.
"""
self.result_error_code = None
self.result_error_message = None
self.result = []
self.stream_result = []
if 'open database ' in command.lower() and len(self.opendb) > 0:
raise SecondoError('A database is already open, close the database before openning a new one.')
if 'close database' in command.lower() and len(self.opendb) == 0:
raise SecondoError('No database is open.')
if not self.initialized:
raise SecondoAPI_Error('Connection to ScondoServer has not been initialised.')
if self.conn is None:
raise SecondoAPI_Error('Connection to ScondoServer reset.')
if not self.opt.get_opt_initialized():
raise SecondoAPI_Error('Connection to OptimizerServer has not been initialised.')
if self.opt.get_opt_conn() is None:
raise SecondoAPI_Error('Connection to OptimizerServer reset.')
if self.get_sec_streams() is None:
raise SecondoAPI_Error('Connection to Secondo Server reset!')
command = await self.optimization_check(command)
if command:
#handliung restore-command
if (((command.lstrip()).lower()).startswith('(restore') or ((command.lstrip()).lower()).startswith('( restore') or ((command.lstrip()).lower()).startswith('restore')):
if not self.initialized:
raise SecondoAPI_Error('Connection to ScondoServer has not been initialised.')
if self.conn is None:
raise SecondoAPI_Error('Connection to ScondoServer reset.')
result = await restore_command(self.reader, self.writer, self.get_bin_format(), command)
self.result_error_code = result[0]
self.result_error_message = result[2]
self.result = result[3]
if result is None:
raise SecondoAPI_Error('Command execution returned an empty list.')
if result[0] != 0 :
raise SecondoError(secondo_errors[result[0]] + ' Command execution was not successful.')
if result[0] == 0 :
print('Restore was successful.')
return result
#handliung save-command
if (((command.lstrip()).lower()).startswith('(save') or ((command.lstrip()).lower()).startswith('( save') or ((command.lstrip()).lower()).startswith('save')):
if not self.initialized:
raise SecondoAPI_Error('Connection to ScondoServer has not been initialised.')
if self.conn == None:
raise SecondoAPI_Error('Connection to ScondoServer reset.')
result = await save_command(self.reader, self.writer, self.get_bin_format(), command)
self.result_error_code = result[0]
self.result_error_message = result[2]
self.result = result[3]
if result is None:
raise SecondoAPI_Error('Command execution returned an empty list.')
if result[0] != 0 :
raise SecondoError(secondo_errors[result[0]] + ' Command execution was not successful.')
if result[0] == 0 :
print('Save was successful.')
return result
#handling general-commands
if not self.initialized:
raise SecondoAPI_Error('Connection to ScondoServer has not been initialised.')
if self.conn is None:
raise SecondoAPI_Error('Connection to ScondoServer reset.')
if 'pysend[' in command.lower():
result, self.stream_result = await general_command(self.reader, self.writer, self.get_bin_format(), command)
if 'pyreceive[' in command.lower():
if tupel_source is None:
raise SecondoAPI_Error('No Tupel source has been given.')
result = await general_command(self.reader, self.writer, self.get_bin_format(), command, stream_source = tupel_source)
if not ('pysend[' in command.lower() or 'pyreceive[' in command.lower()):
result = await general_command(self.reader, self.writer, self.get_bin_format(), command)
self.result_error_code = result[0]
self.result_error_message = result[2]
self.result = result[3]
if result is None:
raise SecondoAPI_Error('Command execution returned an empty list.')
if result[0] != 0 :
print(self.get_result_error_message())
raise SecondoError(secondo_errors[result[0]] + ' Command execution was not successful.')
if result[0] == 0 :
print('Command execution was successful.')
if result[0] == 0 and "open database " in command:
self.set_opendb(command[14:])
if result[0] == 0 and "close database" in command.lower():
self.opendb = ''
return result
else:
return None
def close(self):
"""
This method closes the connections to Secondo server and consequently to Optimizer server.
"""
if not self.initialized:
raise SecondoAPI_Error('The connection to Secondo has not been initialised.')
self.initialized = False
self.opt.close()
if self.writer is not None:
self.writer.close()
if self.conn is not None:
self.conn.close()
def get_sec_conn(self):
"""
This method returns the connection attribute of Secondo object.
:return: The connection attribute of Secondo object.
"""
if self.conn is not None:
return self.conn
else:
raise SecondoAPI_Error('No connection to SecondoServer.')
def get_sec_streams(self):
"""
This method returns the stream reader/ writer attributes of Secondo object.
:return: The stream reader/ writer attributes of Secondo object.
"""
if self.reader is not None and self.writer is not None:
return self.reader, self.writer
else:
raise SecondoAPI_Error("Connection Error, no open stream reade/writer!")
def get_server(self):
"""
This method returns the server attributes of Secondo object.
:return: The server attributes of Secondo object, which consists of an IP-address eg. 127.0.0.0.
"""
return self.server
def get_port(self):
"""
This method returns the port attributes of Secondo object.
:return: The port attributes of Secondo object, which consists of an integer number as port eg. 5678.
"""
return self.port
def get_user(self):
"""
This method returns the user attributes of secondo object.
:return: The user attributes of secondo object needed for authentication in Secondo.
"""
return self.user
def get_password(self):
"""
This method returns the password attributes of secondo object.
:return: The password attributes of secondo object, which is given as
plain text and needed for authentication in Secondo.
"""
return self.password
def get_opendb(self):
"""
This method returns the open database attribute of secondo object.
:return: The currently open database in Secondo server as string.
"""
return self.opendb
def get_bin_format(self):
"""
This method returns the format of returned result of queries.
:return: True if the result is a nested list in binary formet/ False if in plain text nested list.
"""
return self.bin_format
def get_opt(self):
"""
This method returns the opt attribute of secondo object.
:return: The opt attribute of secondo represents an optimizer object.
"""
return self.opt
def fetch_result_type(self):
"""
This method returns the result_type attribute of secondo object.
:return: The result_type attribute of secondo represents the datatype
of result set e.g. int or nested list.
"""
if self.result:
return self.result[0]
return self.result
def fetch_result_rows(self):
"""
This method returns the result attribute of secondo object.
:return: The result attribute of secondo represents the result as a list.
"""
if self.result:
if isinstance(self.result[1], list):
return self.result[1]
else:
res = [self.result[1]]
return res
return self.result
def fetch_relation_header(self):
"""
This method returns the header of the relation in result set of secondo object.
:return: The header of the relation in result set as a nested list.
"""
return self.fetch_result_type()[1][1]
def parse_result_to_secondo_int(self):
"""
This method parses the integer result of a query and converts it to a secondo_int type.
:return: An object of type secondo_int.
"""
int_result = secondo_int()
int_result.in_from_list(self.fetch_result_rows())
print('int object: ')
print(int_result.get_value())
return int_result
def parse_result_to_secondo_real(self):
"""
This method parses the float result of a query and converts it to a secondo_real type.
:return: An object of type secondo_real.
"""
real_result = secondo_real()
real_result.in_from_list(self.fetch_result_rows())
print('real object: ')
print(real_result.get_value())
return real_result
def parse_result_to_relation(self):
"""
This method parses the relation result of a query and converts it to a Relation type.
:return: An object of type Relation.
"""
header = []
for attrib in self.fetch_relation_header():
att = Attribute()
att.in_from_list(attrib)
header.append(att)
rel = []
rel.append(header)
rel.append(self.fetch_result_rows())
relation = Relation()
relation.in_from_list(rel)
print('header: ')
for item in relation.get_header():
print (item.get_Identifier() + '-' + item.get_type_name() + '-' + item.get_type_class())
print(relation.get_tuples())
return relation
def fetch_result(self):
"""
This method returns the result attribute of the Secondo object.
:return: The result attribute of the Secondo object as list.
"""
return self.result
def fetch_stream_result(self):
"""
This method returns the attribute containing the stream of tupels received from
Secondo server when the query contains the operator pysend.
:return: The stream of tuples collected in a list of tuples after receiving.
"""
return self.stream_result
def parse_stream_result_to_relation(self):
"""
This method parses the stream of tuples received from Secondo server
when the query contains the operator pysend and converts them to a relation.
:return: A Relation containing the stream of tuples received from Secondo server.
"""
header = []
for attrib in self.stream_result[0]:
att = Attribute()
att.in_from_list(attrib)
header.append(att)
rel = []
rel.append(header)
records = []
for i in range(1, len(self.stream_result)):
records.append(self.stream_result[i])
rel.append(records)
relation = Relation()
relation.in_from_list(rel)
print('header: ')
for item in relation.get_header():
print (item.get_Identifier() + '-' + item.get_type_name() + '-' + item.get_type_class())
print(relation.get_tuples())
return relation
def get_result_error_code(self):
"""
This method returns the result_error_code attribute of the Secondo object.
:return: An integer representing the error code after executing a command/ query.
"""
return self.result_error_code
def get_result_error_message(self):
"""
This method returns the result_error_message attribute of the Secondo object.
:return: A string representing the error message after executing a command/ query.
"""
return self.result_error_message
def set_opendb(self, db):
"""
This method returns the opendb attribute of the Secondo object.
:return: A string representing the currently open database.
"""
self.opendb = db
def set_bin_format(self, val):
"""
This method returns the bin_format attribute of the Secondo object.
:return: A Boolean representing the format of received result from Secondo,
when True corresponds to Binary nested list, when False textual nested list.
"""
self.bin_format = val