"""
Class for working with omex files.
"""
from __future__ import print_function, division, absolute_import
import os
import re
import tempfile
import json
import getpass
try:
import libcombine
except ImportError:
import tecombine as libcombine
try:
from .convert_phrasedml import phrasedmlImporter
except:
pass
from .convert_antimony import antimonyConverter
[docs]
def readCreator(file=None):
from .. import getAppDir
if file is None:
file = os.path.join(getAppDir(), 'telocal', getpass.getuser() + '.vcard')
if not os.path.exists(file) or not os.path.isfile(file):
return None
with open(file, 'rb') as f:
return json.load(f)
[docs]
class OmexAsset(object):
[docs]
def getLocation(self):
return self.location
[docs]
def getFileName(self):
return os.path.split(self.getLocation())[-1]
[docs]
def getContent(self):
return self.content
[docs]
def getMaster(self):
master = False
if self.master is not None:
master = self.master
return master
[docs]
class SbmlAsset(OmexAsset):
def __init__(self, location, content, master=False):
self.location = location
self.content = content
self.master = master
[docs]
def getModuleName(self):
return os.path.splitext(self.getFileName())[0]
def __repr__(self):
return 'SbmlAsset(location={}, master={})'.format(self.getLocation(), self.getMaster())
[docs]
class SedmlAsset(OmexAsset):
def __init__(self, location, content, master=False):
self.location = location
self.content = content
self.master = master
def __repr__(self):
return 'SedmlAsset(location={}, master={})'.format(self.getLocation(), self.getMaster())
[docs]
class Omex(object):
""" Wrapper for Combine archives. """
def __init__(self,
description='',
creator=None):
self.about = '.'
self.description = description
self.creator = creator
self.sbml_assets = []
self.sedml_assets = []
[docs]
def addSbmlAsset(self, asset):
self.sbml_assets.append(asset)
[docs]
def addSedmlAsset(self, asset):
self.sedml_assets.append(asset)
[docs]
def getSbmlAssets(self):
return self.sbml_assets
[docs]
def getSedmlAssets(self):
return self.sedml_assets
[docs]
def writeFiles(self, dir):
filenames = []
for t in self.getSedmlAssets():
fname = os.path.join(dir, os.path.normpath(t.getLocation()))
dname = os.path.dirname(fname)
if not os.path.exists(dname):
os.makedirs(dname)
filenames.append(fname)
with open(fname, 'w') as f:
f.write(t.getContent())
for t in self.getSbmlAssets():
fname = os.path.join(dir, os.path.normpath(t.getLocation()))
dname = os.path.dirname(fname)
if not os.path.exists(dname):
os.makedirs(dname)
filenames.append(fname)
with open(fname, 'w') as f:
f.write(t.getContent())
return filenames
[docs]
def executeOmex(self):
""" Executes this Omex instance.
:return:
"""
try:
import phrasedml
phrasedml.clearReferencedSBML()
except:
pass
workingDir = tempfile.mkdtemp(suffix="_sedml")
self.writeFiles(workingDir)
from tellurium import executeSEDML
for sedml_asset in self.getSedmlAssets():
if sedml_asset.getMaster():
sedml_path = os.path.join(workingDir, sedml_asset.getLocation())
executeSEDML(sedml_path,
workingDir=os.path.dirname(sedml_path))
# shutil.rmtree(workingDir)
[docs]
def exportToCombine(self, outfile):
""" Export Omex instance as combine archive.
:param outfile: A path to the output file"""
try:
import phrasedml
phrasedml.clearReferencedSBML()
except:
pass
archive = libcombine.CombineArchive()
description = libcombine.OmexDescription()
description.setAbout(self.about)
description.setDescription(self.description)
time_now = libcombine.OmexDescription.getCurrentDateAndTime()
description.setCreated(time_now)
# TODO: pass in creator
if self.creator is not None:
creator = libcombine.VCard()
creator.setFamilyName(self.creator['last_name'])
creator.setGivenName(self.creator['first_name'])
creator.setEmail(self.creator['email'])
creator.setOrganization(self.creator['organization'])
description.addCreator(creator)
archive.addMetadata('.', description)
# Write out to temporary files
# TODO: can add content via strings now
workingDir = tempfile.mkdtemp(suffix="_sedml")
files = [] # Keep a list of files to remove
def addAssetToArchive(asset, format):
""" Helper to add asset of given format. """
fname = os.path.join(workingDir, os.path.normpath(asset.getLocation()))
dname = os.path.dirname(fname)
if not os.path.exists(dname):
os.makedirs(dname)
with open(fname, 'w') as f:
files.append(fname)
f.write(t.getContent())
archive.addFile(fname,
asset.getLocation(),
libcombine.KnownFormats.lookupFormat(format),
asset.getMaster())
try:
for t in self.getSedmlAssets():
addAssetToArchive(t, 'sedml')
for t in self.getSbmlAssets():
addAssetToArchive(t, 'sbml')
archive.writeToFile(outfile)
finally:
# put this in finally to make sure files are removed even under Exception
for f in files:
os.remove(f)
[docs]
class inlineOmexImporter:
# Set to false to disable "Converted from ...xml" comments
__write_block_delimiter_comments = True
[docs]
@classmethod
def fromFile(cls, path):
""" Initialize from a combine archive.
:param path: The path to the omex file
"""
if not os.path.isfile(path):
raise IOError('No such file: {}'.format(path))
if not os.access(os.getcwd(), os.W_OK):
os.chdir(tempfile.gettempdir())
omex = libcombine.CombineArchive()
if not omex.initializeFromArchive(path):
raise IOError('Could not read COMBINE archive.')
importer = inlineOmexImporter(omex)
return importer
def __init__(self, omex):
""" Initialize from a CombineArchive instance
(https://sbmlteam.github.io/libCombine/html/class_combine_archive.html).
:param omex: A CombineArchive instance
"""
self.omex = omex
self.write_block_delimiter_comments = inlineOmexImporter.__write_block_delimiter_comments
self.n_master_sedml = 0
self.sedml_entries = []
self.sbml_entries = []
detector = OmexFormatDetector(self.omex)
# Prevents %antimony and %phrasedml headers from
# being written when all entries are in root of archive
# and no sedml entries have master=False.
self.headerless = True
for entry in self.getEntries():
# shouldn't happen
if not entry.isSetLocation():
raise RuntimeError('Entry has no location')
if not self.isInRootDir(entry.getLocation()):
# must write headers to specify entry paths
self.headerless = False
# count number of master sedml entries
if detector.isSEDMLEntry(entry):
if entry.isSetMaster():
if entry.getMaster():
self.n_master_sedml += 1
if self.n_master_sedml > 1:
# must write headers to specify non-master sedml
self.headerless = False
self.sedml_entries.append(entry)
else:
# must write headers to specify non-master sedml
self.headerless = False
elif detector.isSBMLEntry(entry):
self.sbml_entries.append(entry)
# check whether the model id matches the file name - if it doesn't, we need headers
module_name = antimonyConverter().sbmlToAntimony(self.omex.extractEntryToString(entry.getLocation()))[0]
file_name_normalized = os.path.splitext(os.path.split(entry.getLocation())[-1])[0]
if module_name != file_name_normalized:
self.headerless = False
self.BioModHackRemoveDuplicates()
[docs]
def getEntries(self):
for k in range(self.omex.getNumEntries()):
yield self.omex.getEntry(k)
[docs]
def numEntries(self):
return self.omex.getNumEntries()
[docs]
def numSBMLEntries(self):
return len(self.sbml_entries)
[docs]
def containsSBMLOnly(self):
""" Return true if this is a SBML-only archive (no SED-ML). """
if len(self.sbml_entries) > 0 and len(self.sedml_entries) == 0:
return True
else:
return False
[docs]
def BioModHackRemoveDuplicates(self):
""" A hack to remove duplicates (urn/url) in BioModels archives. """
if len(self.sbml_entries) == 2:
n_urn = 0
n_url = 0
for entry in self.sbml_entries:
if '_urn.xml' in entry.getLocation():
n_urn += 1
if '_url.xml' in entry.getLocation():
n_url += 1
if n_urn == 1 and n_url == 1:
del self.sbml_entries[-1]
[docs]
def isInRootDir(self, path):
""" Returns true if path specififies a root location like ./file.ext."""
d = os.path.split(path)[0]
return d == '' or d == '.'
[docs]
def normalizePath(self, path):
return os.path.normpath(path)
[docs]
def fixExt(self, path):
""" Ensures all extensions are .xml."""
p = os.path.splitext(path)[0]
return ''.join([p, '.xml'])
[docs]
def fixSep(self, path):
""" Converts Windows-style separators to Unix separators."""
if os.path.sep == '\\':
return path.replace(os.path.sep, '/')
else:
return path
# return os.path.splitext(self.normalizePath(path))[0]
[docs]
def makeSBMLResourceMap(self, relative_to=None):
result = {}
for entry in self.sbml_entries:
if relative_to is None:
relpath = entry.getLocation()
else:
relpath = self.fixSep(os.path.relpath(entry.getLocation(), relative_to))
result[self.formatPhrasedmlResource(relpath)] = self.omex.extractEntryToString(entry.getLocation())
return result
[docs]
def toInlineOmex(self, detailedErrors=True):
""" Converts a COMBINE archive into an inline phrasedml / antimony string.
:returns: A string with the inline phrasedml / antimony source
"""
output = ''
# try to read the author information
desc = self.omex.getMetadataForLocation('.')
if desc and desc.getNumCreators() > 0:
# just get first one
vcard = desc.getCreator(0)
output += '// Archive author information:\n'
first_name = vcard.getGivenName()
last_name = vcard.getFamilyName()
name = ' '.join([first_name, last_name])
email = vcard.getEmail()
org = vcard.getOrganization()
if name:
output += '// - Name: {}\n'.format(name.replace('\n', ' ').replace('\r', ''))
if email:
output += '// - Email: {}\n'.format(email.replace('\n', ' ').replace('\r', ''))
if org:
output += '// - Organization: {}\n'.format(org.replace('\n', ' ').replace('\r', ''))
# convert sbml entries to antimony
for entry in self.sbml_entries:
output += (self.makeHeader(entry, 'sbml') +
antimonyConverter().sbmlToAntimony(self.omex.extractEntryToString(entry.getLocation()))[
1].rstrip() + '\n'
+ self.makeFooter(entry, 'sbml'))
# convert sedml entries to phrasedml
for entry in self.sedml_entries:
sedml_str = self.omex.extractEntryToString(entry.getLocation()).replace('BIOMD0000000012,xml',
'BIOMD0000000012.xml')
phrasedml_output = ""
try:
phrasedml_output = phrasedmlImporter.fromContent(
sedml_str,
self.makeSBMLResourceMap(self.fixSep(os.path.dirname(entry.getLocation())))
).toPhrasedml().rstrip().replace('compartment', 'compartment_')
except Exception as e:
errmsg = 'Could not read embedded SED-ML file or could not convert to phraSED-ML: {}.\n{}'.format(entry.getLocation(), e.what())
try:
try:
import libsedml
except ImportError:
import tesedml as libsedml
s = libsedml.readSedMLFromString(sedml_str)
if s.getNumErrors() > 0:
if detailedErrors:
for k in range(s.getNumErrors()):
errmsg += 'Error {}:\n{}'.format(k + 1, s.getError(k).getMessage())
else:
errmsg += ' Run te.convertCombineArchive for more info.'
else:
errmsg += ' Not supported by PhraSEDML.'
except Exception as e:
if detailedErrors:
errmsg += ' '+str(e)
else:
errmsg += ' Invalid SED-ML.'
raise RuntimeError(errmsg)
output += (self.makeHeader(entry, 'sedml') +
phrasedml_output + '\n'
+ self.makeFooter(entry, 'sedml'))
return output.rstrip()