# Record
# - id
# - parent id
# - datetime
# - name
# - payload type name
# - pickled payload data (dict, list, tuple)
# Payload types
# - id
# - type name
# Sample record sequence
# Task (projectData, mpiData, vmLayout)
# Verification OptIter
# Folder
# OptIter
# OptIter
# ...
# OptIter
# Verification OptIter
# Folder
# OptIter
# OptIter
# ...
# OptIter
# ...
# Verification OptIter
# Conclusion
import sqlite3 as lite
import pickle as pickle
import time, uuid, pprint, datetime
import numpy as np
from ..evaluator.aggregate import *
from .. import PyOpusError
from ..misc.format import NA
from ..misc.debug import DbgMsgOut
__all__ = [
'PyOpusSqliteError',
'SQLiteRecord', 'SQLiteDatabase',
'SQLDataRoot', 'SQLDataTask', 'SQLDataTaskCBD',
'SQLDataFolder', 'SQLDataCorners', 'SQLDataOptIter',
'SQLDataOptIter', 'SQLDataConclusion'
]
[docs]
class PyOpusSqliteError(PyOpusError):
def __init__(self, message, *args):
super(PyOpusSqliteError, self).__init__(message, *args)
class SQLData(object):
def __init__(self):
pass
def typeString(self):
return type(self).__name__[7:]
def textAspects(self):
return [ ]
def getAuxiliaryData(self, sqldb, recordId):
aux={}
return aux
def formatStr(self, aspect, auxData):
return None
class SQLDataRoot(SQLData):
def __init__(self):
SQLData.__init__(self)
self.uuid=str(uuid.uuid4())
def textAspects(self):
return [ "uuid" ]
def formatStr(self, aspect, auxData):
if aspect=='uuid':
return self.uuid
else:
return None
class SQLDataTask(SQLData):
def __init__(self, project, task):
SQLData.__init__(self)
self.project=project
self.task=task
def textAspects(self):
return [ "project", "task" ]
def formatStr(self, aspect, auxData):
if aspect=='project':
return pprint.pformat(self.project)
elif aspect=="task":
return pprint.pformat(self.task)
else:
return None
class SQLDataTaskCBD(SQLData):
# First child of a CBD task
def __init__(self, aggregatorSetup):
SQLData.__init__(self)
self.aggregatorSetup=aggregatorSetup
def textAspects(self):
return [ "aggregator" ]
def formatStr(self, aspect, auxData):
if aspect=='aggregator':
nameW=max([len(c['measure']) for c in self.aggregatorSetup]+[2])
goalW=max([len(str(c['norm'])) for c in self.aggregatorSetup]+[2])
reduceW=max([len(str(c['reduce'])) for c in self.aggregatorSetup]+[2])
txt=""
for c in self.aggregatorSetup:
txt += f"{c['measure']:{nameW}s} {str(c['norm']):{goalW}s}"
txt += f" norm={NA(c['norm'].norm):e} fp={NA(c['norm'].failure):e}"
txt += f" {str(c['reduce']):{reduceW}s}"
txt+=" "+str(c['shape'])
txt+="\n"
return txt
else:
return None
class SQLDataFolder(SQLData):
def __init__(self):
SQLData.__init__(self)
class SQLDataCorners(SQLData):
def __init__(self, measureCorners, addedCorners):
SQLData.__init__(self)
self.measureCorners=measureCorners
self.addedCorners=addedCorners
def getAuxiliaryData(self, sqldb, recordId):
aux={}
# Get task record
taskRec=sqldb.getAncestorByType(recordId, SQLDataTask)
aux['project']=taskRec.payload.project
aux['task']=taskRec.payload.task
return aux
def textAspects(self):
return [ "corners" ]
def formatStr(self, aspect, auxData):
task=auxData['task']
if aspect=="corners":
reqLen=max([len(s) for s in task['requirementNames']]+[1])
nameList=task['requirementNames']
txt=""
for name in nameList:
txt1 = f"{name:<{reqLen}s} : "
if len(self.measureCorners[name])>0:
txt1+=" ".join([c for c in self.measureCorners[name]])
else:
txt1+="no corners"
if len(self.addedCorners[name]):
txt2 = f"{'':<{reqLen}s} + "
txt2+=" ".join([c for c in self.addedCorners[name]])+"\n"
else:
txt2=""
txt+=txt1+"\n"+txt2
return txt
else:
return None
class SQLDataOptIter(SQLData):
def __init__(self, parameters, aggregatorData, evaluatorData, componentNames, waveformData=None):
SQLData.__init__(self)
self.parameters=parameters
self.aggregatorData=aggregatorData
self.componentNames=componentNames
self.evaluatorData=evaluatorData
# List of corner names refered to by indices in aggregator is
# passed to CBD as cornerOrder and forwarded to PerformanceEvaluator.
# From there it is read by the Aggregator.
self.waveformData=waveformData
def getAuxiliaryData(self, sqldb, recordId):
aux={}
# Get task record
taskRec=sqldb.getAncestorByType(recordId, SQLDataTask)
aux['project']=taskRec.payload.project
aux['task']=taskRec.payload.task
# Get first child of task record (task setup record)
tsrRec=sqldb.getFirstChild(taskRec.recordId)
aux['aggregatorSetup']=tsrRec.payload.aggregatorSetup
return aux
def textAspects(self):
return [ "parameters", "performance", "cost" ]
def boundText(self, task, name, ev):
if ev is None:
return "Failed"
if (
name in task['requirements']['lower'] and
ev<task['requirements']['lower'][name]
):
return "Low"
if (
name in task['requirements']['upper'] and
ev>task['requirements']['upper'][name]
):
return "High"
return ""
def formatStr(self, aspect, auxData):
project=auxData['project']
task=auxData['task']
aggregatorSetup=auxData['aggregatorSetup']
# Length of parameter, measure, and corner names
parLen=max([len(s) for s in task['parameterNames']]+[1])
reqLen=max([len(s) for s in task['requirementNames']]+[1])
cLen=max([len(s) for s in task['cornerNames']]+[1])
# Length of component names
cnLen=0
for m in self.componentNames.keys():
if self.componentNames[m] is not None:
for cn in self.componentNames[m]:
cn1=len(cn)
if cn1>cnLen:
cnLen=cn1
# Length of default component names (indices)
for measureName, row in self.evaluatorData.items():
for ev in row.values():
if project['measures'][measureName]['vector']:
if type(ev) is np.ndarray:
cn1=int(np.ceil(np.log10(ev.shape[0])))
else:
cn1=1
if cn1>cnLen:
cnLen=cn1
if aspect=="parameters":
txt=""
for name in task['parameterNames']:
txt1 = f"{name:<{parLen}s} = {NA(self.parameters[name]):e}\n"
txt+=txt1
return txt
elif aspect=="performance":
nameList=task['requirementNames']
txt=""
# Evaluator results
for name in nameList:
firstName=True
if name not in self.evaluatorData:
continue
evMeas=self.evaluatorData[name]
for cName in task['cornerNames']:
firstCorner=True
if cName not in evMeas:
continue
ev=evMeas[cName]
# Prefix with name and corner
txt1 = f"{(name if firstName else ''):<{reqLen}s}: {(cName if firstCorner else ''):<{cLen}s}"
if ev is None:
# No result
bt=self.boundText(task, name, ev)
txt1+=(" "*(cnLen+2))+" = "+(" "*(14))+" "+bt+"\n"
elif project['measures'][name]['vector']:
# Vector by definition
if type(ev) is np.ndarray:
# Vector is ndarray
txt1=""
for ii in range(ev.shape[0]):
# Prefix (again, skip name and corner when not needed)
txt1 += f"{(name if firstName else ''):<{reqLen}s}: {(cName if firstCorner else ''):<{cLen}s}"
# Component name
lst=self.componentNames[name] if name in self.componentNames else None
if lst is not None and len(lst)>ii:
compName = f"{lst[ii]:<{cnLen}s}"
else:
compName = f"{ii:<{cnLen}d}"
# Value
valTxt = f"{ev[ii]:14e}" if ev[ii] is not None else " " * 14
# Put together
bt=self.boundText(task, name, ev[ii])
txt1 += f"[{compName}] = {valTxt} {bt}\n"
firstName=False
firstCorner=False
else:
# Vector is not ndarray (is scalar)
lst=self.componentNames[name] if name in self.componentNames else None
if lst is not None and len(lst)>0:
compName = f"{lst[0]:<{cnLen}s}"
else:
compName = f"{0:<{cnLen}d}"
valTxt = f"{ev:14e}" if ev is not None else " " * 14
bt=self.boundText(task, name, ev)
txt1 += f"[{compName}] = {valTxt} {bt}\n"
else:
# Scalar by definition
valTxt = f"{ev:14e}" if ev is not None else " " * 14
bt=self.boundText(task, name, ev)
txt1+=(" "*(cnLen+2))+" = "+valTxt+" "+bt+"\n"
firstName=False
firstCorner=False
txt+=txt1
return txt
elif aspect=="cost":
txt=""
names=task['requirementNames']
cost=0.0
for ii in range(len(aggregatorSetup)):
name=aggregatorSetup[ii]['measure']
norm=aggregatorSetup[ii]['norm']
reduction=aggregatorSetup[ii]['reduce']
data=self.aggregatorData[ii]
goal=norm.goal
typeChar=">" if type(norm)is Nabove else "<"
txt1 = f"{name:{reqLen}s} {typeChar} {NA(goal):e}"
contrib=data['contribution']
cost+=contrib
if data['worst'] is None:
statusText=reduction.flagFailure()
failedCount=len(data['worst_corner_vector'])
worst = f"in {failedCount} corner(s)"
cornerList=[task['cornerNames'][ci] for ci in data['worst_corner_vector']]
if len(cornerList)>3:
cornerText=(" ".join(cornerList[:3]))+" ..."
else:
cornerText=(" ".join(cornerList))
else:
statusText=reduction.flagSuccess(data['fulfilled'])
worst = f"worst={NA(data['worst']):e}"
cornerText=task['cornerNames'][data['worst_corner']]
txt1 += f" {statusText} {worst:<20s} cf={NA(contrib):<14e} {cornerText}"
txt+=txt1+"\n"
txt += f"\ncost function value = {cost:e}\n"
return txt
else:
return None
class SQLDataConclusion(SQLData):
def __init__(self, analysisCount, time):
SQLData.__init__(self)
self.analysisCount=analysisCount
self.time=time
def textAspects(self):
return [ "summary" ]
def formatStr(self, aspect, auxData):
if aspect=="summary":
anLen=max([ len(a) for a in self.analysisCount.keys() ]+[1])
txt="Analysis counts\n"
for name in self.analysisCount.keys():
txt += f"{name:<{anLen}s} : {self.analysisCount[name]}\n"
txt += f"\nTask took {self.time} seconds\n"
return txt
else:
return None
class SQLiteRecord(object):
def __init__(self, sqldb=None, recordId=None, parent=None, timestamp=None, name=None, typename=None, payload=None):
self.sqldb=sqldb
self.auxData=None
self.waveforms={}
if type(recordId) is str:
raise PyOpusError(DbgMsg("SQLITE", "Record is a string."))
self.recordId=recordId
self.parent=parent
self.timestamp=timestamp
self.name=name
if typename is None:
self.typename=type(payload).__name__
else:
self.typename=typename
self.payload=payload
def getAuxiliaryData(self):
if self.auxData is None:
if self.sqldb is None:
raise PyOpusSqliteError("No database specified. Cannot retrieve auxiliary data.")
self.auxData=self.payload.getAuxiliaryData(self.sqldb, self.recordId)
self.waveforms=self.sqldb.getWaveforms(self.recordId)
def textAspects(self):
return self.payload.textAspects()
def formatHead(self):
dateStr=datetime.datetime.fromtimestamp(self.timestamp).strftime('%Y-%m-%d %H:%M:%S')
tstr=self.payload.typeString() if self.payload is not None else self.typename
txt = f"Id : {self.recordId}\n"
txt += f"Parent : {self.parent}\n"
txt += f"Name : {self.name}\n"
txt += f"Type : {tstr}\n"
txt += f"Time : {self.timestamp:.2f} ({dateStr})\n"
return txt
def formatStr(self, aspect=None):
self.getAuxiliaryData()
aspects=self.textAspects()
# Default aspect
if aspect is None and len(aspects)>0:
aspect=aspects[0]
if aspect is not None:
txtpl=self.payload.formatStr(aspect, self.auxData)
else:
txtpl=None
txt=""
if txtpl is not None:
# txt1 = f"Content ({aspect})"
# txt+=txt1+"\n"
# txt+=("-"*len(txt1))+"\n"
txt+=txtpl
else:
if len(aspects)>0:
txt += f"No text found for aspect '{aspect}'."
else:
txt+="No text aspects available."
return txt
class SQLiteDatabase(object):
def __init__(self, fpath):
self.fpath=fpath
self.rootId=None
self.rootTypeId=None
self.uuid=None
self.processedPayloadTypes={}
def payloadTypeId(self, typeName, con):
cur=con.cursor()
# Did we see it
if typeName not in self.processedPayloadTypes:
# No, check if it is in the database
cur.execute("SELECT id FROM payloads WHERE typename=?", (typeName,))
row=cur.fetchone()
if row is None:
# No it is not, add it
cur.execute("INSERT INTO payloads (typename) VALUES (?)", (typeName,))
typeId=cur.lastrowid
else:
# Yes it is there, get it
typeId=row[0]
self.processedPayloadTypes[typeName]=typeId
else:
# Yes, we saw it
typeId=self.processedPayloadTypes[typeName]
con.commit()
return typeId
# Drop all tables, rebuild database
def reset(self):
with lite.connect(self.fpath) as con:
con.isolation_level = None
cur=con.cursor()
# Drop tables
cur.execute("DROP TABLE IF EXISTS payloads")
cur.execute("DROP TABLE IF EXISTS data")
cur.execute("DROP TABLE IF EXISTS waveforms")
# Create tables
cur.execute("""
CREATE TABLE payloads(
id INTEGER PRIMARY KEY,
typename TEXT NOT NULL
)
""")
cur.execute("CREATE UNIQUE INDEX idx_typename ON payloads(typename)")
cur.execute("""
CREATE TABLE data(
id INTEGER PRIMARY KEY,
parent INTEGER,
timestamp TIMESTAMP,
name TEXT,
type INTEGER,
data BLOB
)
""")
cur.execute("CREATE INDEX idx_parent ON data(parent)")
cur.execute("CREATE INDEX idx_timestamp ON data(timestamp)")
cur.execute("CREATE INDEX idx_name ON data(name)")
cur.execute("CREATE INDEX idx_type ON data(type)")
cur.execute("""
CREATE TABLE waveforms(
id INTEGER,
corner TEXT NOT NULL,
analysis TEXT NOT NULL,
filename TEXT NOT NULL
)
""")
cur.execute("CREATE INDEX idx_id ON waveforms(id)")
cur.execute("CREATE INDEX idx_corner ON waveforms(corner)")
cur.execute("CREATE INDEX idx_analysis ON waveforms(analysis)")
cur.execute("CREATE UNIQUE INDEX idx_filename ON waveforms(filename)")
# Add root type
self.rootTypeId=self.payloadTypeId(SQLDataRoot.__name__, con)
# Insert root record
payload=SQLDataRoot()
tname=type(payload).__name__
t=time.time()
cur.execute(
"INSERT INTO data (id, parent, timestamp, name, type, data) VALUES (0,-1,?,'root',(SELECT rowid FROM payloads WHERE typename=?),?)",
(t, tname, lite.Binary(pickle.dumps(payload)))
)
con.commit()
self.rootId=cur.lastrowid
return self.rootId
# Get root entry type id and id
def root(self):
# Did we see it
if self.rootId is None:
# No, it must already be there. Get it.
with lite.connect(self.fpath) as con:
con.isolation_level = None
cur=con.cursor()
cur.execute("""
SELECT payloads.id, data.id, data.data FROM payloads, data
WHERE payloads.typename=? AND data.type=payloads.id
""", (SQLDataRoot.__name__,))
row=cur.fetchone()
self.rootTypeId, self.rootId = row[0], row[1]
obj=pickle.loads(row[2])
self.uuid=obj.uuid
return self.rootTypeId, self.rootId
def getAncestorByType(self, recordId, ancestorType):
with lite.connect(self.fpath) as con:
con.isolation_level = None
cur=con.cursor()
atId=recordId
while True:
cur.execute(
"SELECT data.parent, data.timestamp, data.name, payloads.typename, data.data "
"FROM data, payloads WHERE data.type=payloads.id AND data.id=?",
(atId, )
)
row=cur.fetchone()
if row is None:
return None
if row[3]==ancestorType.__name__:
# Found it
obj=pickle.loads(row[4])
return SQLiteRecord(self,
atId, parent=row[0], timestamp=row[1], name=row[2],
typename=row[3],
payload=obj
)
else:
# Go to parent
atId=row[0]
if atId<0:
return None
def getFirstChild(self, recordId):
with lite.connect(self.fpath) as con:
con.isolation_level = None
cur=con.cursor()
cur.execute(
"SELECT data.id, data.parent, data.timestamp, data.name, payloads.typename, data.data "
"FROM data, payloads WHERE data.type=payloads.id AND data.parent=? "
"ORDER BY data.id ASC LIMIT 1",
(recordId, )
)
row=cur.fetchone()
if row is None:
return None
else:
obj=pickle.loads(row[5])
return SQLiteRecord(self,
recordId=row[0], parent=row[1], timestamp=row[2], name=row[3],
typename=row[4],
payload=obj
)
# Get a record
def get(self, recordId, getPayload=True):
with lite.connect(self.fpath) as con:
con.isolation_level = None
cur=con.cursor()
if getPayload:
cur.execute(
"SELECT data.parent, data.timestamp, data.name, payloads.typename, data.data "
"FROM data, payloads WHERE data.id=? AND data.type=payloads.id",
(recordId, )
)
row=cur.fetchone()
if row is None:
return None
else:
obj=pickle.loads(row[4])
return SQLiteRecord(self,
recordId=recordId, parent=row[0],
timestamp=row[1], name=row[2],
typename=row[3],
payload=obj
)
else:
cur.execute(
"SELECT data.parent, data.timestamp, data.name, payloads.typename "
"FROM data, payloads WHERE data.id=? AND data.type=payloads.id",
(recordId, )
)
row=cur.fetchone()
if row is None:
return None
else:
return SQLiteRecord(self,
recordId=recordId, parent=row[0],
timestamp=row[1], name=row[2],
typename=row[3], payload=None
)
def getWaveforms(self, recordId):
with lite.connect(self.fpath) as con:
con.isolation_level = None
cur=con.cursor()
cur.execute(
"SELECT waveforms.corner, waveforms.analysis, waveforms.filename "
"FROM waveforms WHERE waveforms.id=?",
(recordId, )
)
files={}
while True:
row=cur.fetchone()
if row is None:
break
an=row[1]
# Convert empty string to None
if an=='':
an=None
key=(row[0], an)
files[key]=row[2]
return files
# Get children with child counts. Does not fetch payloads.
def getChildren(self, recordId):
with lite.connect(self.fpath) as con:
con.isolation_level = None
cur=con.cursor()
cur.execute(
"SELECT dp.id, dp.name, payloads.typename, "
" (SELECT COUNT(id) FROM data dc WHERE dc.parent=dp.id)"
"FROM data dp, payloads "
"WHERE dp.type=payloads.id "
"AND dp.parent=?",
(recordId, )
)
ids=[]
names=[]
types=[]
childCounts=[]
while True:
row=cur.fetchone()
if row is None:
break
ids.append(row[0])
names.append(row[1])
types.append(row[2])
childCounts.append(row[3])
return ids, names, types, childCounts
# Get children with child counts. Does not fetch payloads.
def getNewNodes(self, startId):
with lite.connect(self.fpath) as con:
con.isolation_level = None
cur=con.cursor()
cur.execute(
"SELECT data.id, data.parent, data.name, payloads.typename "
"FROM data, payloads "
"WHERE data.type=payloads.id "
"AND data.id>=?",
(startId, )
)
ids=[]
parents=[]
names=[]
types=[]
while True:
row=cur.fetchone()
if row is None:
break
ids.append(row[0])
parents.append(row[1])
names.append(row[2])
types.append(row[3])
return ids, parents, names, types
def lastId(self):
with lite.connect(self.fpath) as con:
con.isolation_level = None
cur=con.cursor()
cur.execute(
"SELECT max(id) FROM data"
)
row=cur.fetchone()
if row is None:
return 1
else:
return row[0]
# Commit a record
def commit(self, record):
# Add a record
if record.timestamp is None:
record.timestamp=time.time()
tname=type(record.payload).__name__
# Default parent is root
if record.parent is None:
_, record.parent = self.root()
with lite.connect(self.fpath) as con:
con.isolation_level = None
cur=con.cursor()
# Add type
self.payloadTypeId(tname, con)
# Add entry
cur.execute(
"INSERT INTO data (parent, timestamp, name, type, data) VALUES (?,?,?,(SELECT rowid FROM payloads WHERE typename=?),?)",
(record.parent, record.timestamp, record.name, tname, pickle.dumps(record.payload))
)
con.commit()
record.recordId=cur.lastrowid
# DbgMsgOut("SQLITE", f"Commit record, id={record.recordId}")
return record.recordId
def commitWaveform(self, recordId, corner, analysis, fileName):
with lite.connect(self.fpath) as con:
con.isolation_level = None
cur=con.cursor()
# Convert None to ''
if analysis is None:
analysis=''
# DbgMsgOut("SQLITE", f"Commit waveform, id={recordId} co={corner} an={analysis} file={fileName}")
cur.execute(
"INSERT INTO waveforms (id, corner, analysis, filename) VALUES (?,?,?,?)",
(recordId, corner, analysis, fileName)
)
con.commit()
# files is a dictionary with (corner, analysis) for key and filename for value
def commitWaveforms(self, recordId, files):
with lite.connect(self.fpath) as con:
con.isolation_level = None
cur=con.cursor()
for (corner, analysis), fileName in files.items():
# Convert None to ''
if analysis is None:
analysis=''
# DbgMsgOut("SQLITE", f"Commit waveforms, mul id={recordId} co={corner} an={analysis} file={fileName}")
cur.execute(
"INSERT INTO waveforms (id, corner, analysis, filename) VALUES (?,?,?,?)",
(recordId, corner, analysis, fileName)
)
con.commit()
#
# Main function
#
def dumpChildren(sqldb, recId, level=0, idLen=6):
ids, names, types, childCounts = sqldb.getChildren(recId)
indent=" "*(level*2)
for ii in range(len(ids)):
txt = f"{ids[ii]:<{idLen}d}: {indent}{names[ii]}"
if childCounts[ii]>0:
txt += f" ({types[ii][7:]}, children={childCounts[ii]})"
else:
txt += f" ({types[ii][7:]})"
print(txt)
if childCounts[ii]>0:
dumpChildren(sqldb, ids[ii], level+1, idLen)
def main():
import sys, math
appName="pyori"
nArgs=len(sys.argv)
ok=True
dbfile=None
action="tree"
recId=None
aspect=None
if nArgs>=2:
dbfile=sys.argv[1]
if nArgs>=3:
action=sys.argv[2]
if nArgs>=4:
try:
recId=int(sys.argv[3])
except:
print("Record id must be an integer")
if nArgs>=5:
aspect=sys.argv[4]
ok=True
# Check dbFile
if dbfile is None:
print("Need an sqlite database.\n")
ok=False
# Check action
if action not in [ "aspects", "print", "tree" ]:
print("Unknown action '"+action+"'.\n")
ok=False
# Check aspects and print record id
if action in ["aspects", "print"] and recId is None:
print("No record id given.\n")
ok=False
if not ok:
print("PyOPUS results database inspector usage:")
print(" "+appName+" <sqlite file> <action> <option1> <option2> ...")
print("")
print("Print the tree")
print(" "+appName+" <sqlite file>")
print(" "+appName+" <sqlite file> tree")
print("")
print("List available aspects of node <id>")
print(" "+appName+" <sqlite file> aspects <id>")
print("")
print("Print all aspects of node <id>")
print(" "+appName+" <sqlite file> print <id>")
print("")
print("Print <aspect> of node <id>")
print(" "+appName+" <sqlite file> print <id> <aspect>")
print("")
sys.exit(1)
try:
sqldb=SQLiteDatabase(dbfile)
# Handle tree
if action=="tree":
# Default to root node
if recId is None:
_, recId = sqldb.root()
txt="Children of root record"
else:
txt = f"Children of record {recId}"
rec=sqldb.get(recId, getPayload=False)
if rec is None:
raise PyOpusSqliteError(f"Record {recId} not found.")
lastId=sqldb.lastId()
idLen=int(math.ceil(math.log10(lastId+1)))+1
print(txt)
print("-"*len(txt))
dumpChildren(sqldb, recId, 0, idLen)
# Handle aspects
if action=="aspects":
rec=sqldb.get(recId)
if rec is None:
raise PyOpusSqliteError(f"Record {recId} not found.")
aspects=rec.textAspects()
if len(aspects)>0:
print("\n".join(aspects))
else:
print("No aspects available.")
# Handle print
if action=="print":
rec=sqldb.get(recId)
if rec is None:
raise PyOpusSqliteError(f"Record {recId} not found.")
aspects=rec.textAspects()
if aspect is None:
aList=aspects
elif aspect not in aspects:
raise PyOpusSqliteError(f"Unknown aspect of record {recId}")
else:
aList=[aspect]
print(rec.formatHead())
if aspect is None:
n=len(aList)
for ii in range(n):
aspect=aList[ii]
print(rec.formatStr(aspect), end='')
if ii<n-1:
print()
else:
print(rec.formatStr(aspect))
except PyOpusSqliteError as e:
print("Failed:", str(e))
sys.exit(1)
sys.exit(0)
if __name__ == '__main__':
main()