"""
Tellurium utilities for working with COMBINE archives.
Open Modeling EXchange format (OMEX) is the basis of the COMBINE Archive, a single file that supports
the exchange of all the information necessary for a modeling and simulation experiment in biology.
An OMEX file is a ZIP container that includes a manifest file, listing the content of the archive,
an optional metadata file adding information about the archive and its content, and the files describing
the model. The content of a COMBINE Archive consists of files encoded in COMBINE standards whenever
possible, but may include additional files defined by an Internet Media Type.
The COMBINE Archive facilitates the reproduction of modeling and simulation experiments in biology by
embedding all the relevant information in one file. Having all the information stored and exchanged at
once also helps in building activity logs and audit trails.
The COMBINE archive is described in the following publication
BMC Bioinformatics. 2014 Dec 14;15:369. doi: 10.1186/s12859-014-0369-z.
COMBINE archive and OMEX format: one file to share all information to reproduce a modeling project.
The specification is available from
http://co.mbine.org/specifications/omex.version-1
The archive contains
1. a mandatory manifest file, called manifest.xml, always located at the root of the archive, that describes the
location and the type of each data file contained in the archive plus an entry describing the archive itself.
The location of those files is defined by a relative path.
2. metadata files containing clerical information about the various files contained in the archive, and the archive
itself. A best practice is to include only one file for each file format metadata called metadata.* (where *
means the suitable file extension)
3. all remaining files necessary to the model and simulation project
An entry in the OmexManifest is represented by the Content class. It declares a file in the COMBINE archive. A
content element possesses two required attributes, location and format, as well as an optional one, master.
The location attribute of type string is required. It represents the relative location of an entry within the archive.
The archive is represented by a dot '.'.
The format attribute of type string is required. It indicates the file type of the Content element. The allowed
values fall in two categories. Either the format denotes one of the COMBINE standards, in which case the format is
the corresponding Identifiers.org URI. Or the format represents an InternetMedia type (Freed and Borenstein,
1996) (previously known as "MIME type"), in which case the format indicates this Media type.
The master attribute of type boolean optional. When set to "true", it indicates that the file declared by the content
element should be used first when processing the content of the archive. The file can be for instance the description
of an upper model in a composed model, itself declaring the various submodels; a simulation description,
declaring the different model descriptions and data sources used in the experiment. In most cases, one content
element per archive will have its master attribute set to true.
"""
# TODO: handle real world archives from other tools
# TODO: metaData
# TODO: list of Entries, entry.getFormat(), entry.getLocation()
from __future__ import print_function, division
import os
import shutil
import warnings
from zipfile import ZipFile
import phrasedml
import re
import tellurium as te
import roadrunner
try:
import tesedml as libsedml
except ImportError:
import libsedml
from xml.etree import ElementTree as et
[docs]def combine(combinePath):
""" Open a combine archive from local directory.
:param combinePath: Path to a combine archive
:type combinePath: str
:returns: OpenCombine instance
"""
warnings.warn('Use inline_omex instead.', DeprecationWarning)
return OpenCombine(combinePath)
[docs]class Asset(object):
COMBINE_PREFIX = 'http://identifiers.org/combine.specifications/'
MEDIATYPE_PREFIX = 'http://purl.org/NET/mediatypes/'
FORMATS = {
# combine formats
# http://co.mbine.org/standards/specifications/
'biopax': COMBINE_PREFIX + 'biopax', # Biological Pathway Exchange format
'cellml': COMBINE_PREFIX + 'cellml', # CellML
'omex': COMBINE_PREFIX + 'omex', # COMBINE archive
'omex-manifest': COMBINE_PREFIX + 'omex-manifest', # COMBINE archive manifest
'omex-metadata': COMBINE_PREFIX + 'omex-metadata', # COMBINE archive metadata
'gpml': COMBINE_PREFIX + 'gpml', # GenMAPP Pathway Markup Language
'sbgn': COMBINE_PREFIX + 'sbgn', # Systems Biology Graphical Notation
'sbml': COMBINE_PREFIX + 'sbml', # Systems Biology Markup Language
'sbol': COMBINE_PREFIX + 'sbol', # Synthetic Biology Open Language
'sed-ml': COMBINE_PREFIX + 'sed-ml', # Simulation Experiment Description Markup Language
'teddy': COMBINE_PREFIX + 'teddy', # TErminology for the Description of DYnamics
# File assets
'png': MEDIATYPE_PREFIX + 'image/png',
'jpg': MEDIATYPE_PREFIX + 'image/jpg',
'jpeg': MEDIATYPE_PREFIX + 'image/jpg',
'svg': MEDIATYPE_PREFIX + 'image/svg+xml',
'pdf': MEDIATYPE_PREFIX + 'application/pdf',
'txt': MEDIATYPE_PREFIX + 'text/plain',
'csv': MEDIATYPE_PREFIX + 'text/csv',
'dat': MEDIATYPE_PREFIX + 'text/dat',
'md': MEDIATYPE_PREFIX + 'text/x-markdown',
}
def __init__(self, raw=None, location=None, filetype=None, master=False):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
self.raw = raw
self.location = location
self.master = master
self.format = Asset.formatFromFiletype(filetype)
# subfolders for figures and data
if filetype in ['png', 'jpg', 'jpeg', 'svg']:
self.location = 'fig/{}'.format(self.location)
if filetype in ['txt', 'csv', 'dat']:
self.location = 'data/{}'.format(self.location)
[docs] @classmethod
def fromRaw(cls, raw, location, filetype, master=None):
""" Create asset from raw string.
:param raw: raw string content
:type raw: str
:param location: ??
:type location: ??
:return:
:rtype:
"""
warnings.warn('Use inline_omex instead.', DeprecationWarning)
return cls(raw=raw, location=location, filetype=filetype, master=master)
[docs] @classmethod
def fromFile(cls, filename, filetype, master=None):
""" Create asset from filename.
:param filename:
:type filename:
:return:
:rtype:
"""
warnings.warn('Use inline_omex instead.', DeprecationWarning)
location = os.path.basename(filename)
with open(filename) as f:
raw = f.read()
return cls(raw=raw, location=location, filetype=filetype, master=master)
[docs] @classmethod
def fromAntimony(cls, antimonyStr, location, master=None):
""" Create SBMLAsset from antimonyStr
:param antimonyStr:
:type antimonyStr:
:param location:
:type location:
:return:
:rtype:
"""
warnings.warn('Use inline_omex instead.', DeprecationWarning)
r = te.loada(antimonyStr)
raw = r.getSBML()
return cls.fromRaw(raw=raw, location=location, filetype='sbml', master=master)
[docs] @classmethod
def fromPhrasedML(cls, phrasedmlStr, location, master=None):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
sedmlStr = phrasedml.convertString(phrasedmlStr)
# necessary to add xml extensions to antimony models
phrasedml.addDotXMLToModelSources()
sedmlStr = phrasedml.getLastSEDML()
if sedmlStr is None:
raise Exception(phrasedml.getLastError())
return cls.fromRaw(raw=sedmlStr, location=location, filetype='sed-ml', master=master)
[docs]class CombineArchive(object):
""" Class for creating combine archives.
Raw and file assets are added to the empty COMBINE archive via
the respective add*Str and add*File methods.
The archive is written to file via the write function.
"""
def __init__(self):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
self.assets = []
[docs] def checkfile(self, filename):
""" Check that file exists. """
warnings.warn('Use inline_omex instead.', DeprecationWarning)
if not os.path.exists(filename) or not os.path.isfile(filename):
raise RuntimeError('No such file: {}'.format(filename))
[docs] def addAsset(self, asset):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
self.assets.append(asset)
[docs] def addSBMLStr(self, sbmlStr, location, master=None):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
self.addAsset(Asset.fromRaw(sbmlStr, location, filetype='sbml', master=master))
[docs] def addAntimonyStr(self, antimonyStr, location, master=None):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
self.addAsset(Asset.fromAntimony(antimonyStr, location, master=master))
[docs] def addSEDMLStr(self, sedmlStr, location, master=None):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
self.addAsset(Asset.fromRaw(sedmlStr, location, filetype='sed-ml', master=master))
[docs] def addPhraSEDMLStr(self, phrasedmlStr, location, master=None):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
self.addAsset(Asset.fromPhrasedML(phrasedmlStr, location, master=master))
[docs] def addSBMLFile(self, filename, master=None):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
self.checkfile(filename)
self.addAsset(Asset.fromFile(filename, filetype='sbml', master=master))
[docs] def addSEDMLFile(self, filename, master=None):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
self.checkfile(filename)
self.addAsset(Asset.fromFile(filename, filetype='sed-ml', master=master))
[docs] def addFile(self, filename, filetype, master=None):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
self.addAsset(Asset.fromFile(filename=filename, filetype=filetype, master=master))
[docs] def addStr(self, textStr, location, filetype):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
self.addAsset(Asset.fromRaw(raw=textStr, location=location, filetype=filetype))
[docs] def write(self, outfile):
""" Write the combine archive to the outfile.
Writes all assets and creates the manifest.xml.
"""
warnings.warn('Use inline_omex instead.', DeprecationWarning)
manifestStr = self._createManifestString()
with ZipFile(outfile, 'w') as zf:
# write assets
for a in self.assets:
zf.writestr(a.location, a.raw)
# write manifest
zf.writestr('manifest.xml', manifestStr)
def _createManifestString(self):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
lines = ['<?xml version="1.0" encoding="utf-8"?>',
'<omexManifest xmlns="http://identifiers.org/combine.specifications/omex-manifest">',
' <content location="./manifest.xml" format="http://identifiers.org/combine.specifications/omex-manifest"/>']
for a in self.assets:
if a.master:
lines.append(' <content location="./{}" format="{}" master="true"/>'.format(a.location, a.format))
else:
lines.append(' <content location="./{}" format="{}"/>'.format(a.location, a.format))
lines.append('</omexManifest>')
return "\n".join(lines)
[docs] @staticmethod
def readContentsFromManifest(manifestPath):
""" Reads the manifest information.
:param manifestPath:
:type manifestPath:
:return:
:rtype:
"""
warnings.warn('Use inline_omex instead.', DeprecationWarning)
from collections import namedtuple
Content = namedtuple('Content', 'location format master')
contents = []
tree = et.parse(manifestPath)
root = tree.getroot()
for child in root:
location = child.attrib['location']
format = child.attrib['format']
master = child.attrib.get('master', None)
contents.append(Content(location, format, master))
return contents
###########################################################################################
# working with existing combine archives
[docs]class OpenCombine(object):
""" Main class for handling COMBINE Archives. """
def __init__(self, combinePath):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
# open existing
if os.path.exists(combinePath):
self.combinePath = combinePath
# create new archive
else:
raise Exception("Invalid path for combine archive")
# <SBML>
[docs] def addSBML(self, sbmlPath, filename=None):
""" Adds SBML file into COMBINE archive.
:param sbmlPath: path to SBML file or full SBML file string
"""
warnings.warn('Use inline_omex instead.', DeprecationWarning)
modelname = self.getModelName(sbmlPath)
contents = self.listContents()
zf = ZipFile(self.combinePath, 'a')
if os.path.exists(sbmlPath):
numSame = 0
while contents.count(modelname + '.xml') == 1:
# This should never happen (breaks the SEDML files)
modelname = modelname + '_' + str(numSame) + '.xml'
numSame += 1
if filename != None:
if filename[-3:] != ('xml' or 'sbml'):
filename = filename + '.xml'
modelname = filename
zf.write(sbmlPath, arcname=modelname)
elif sbmlPath.startswith(r'<?xml'):
numSame = 0
while contents.count(modelname + '.xml') == 1:
modelname = modelname + '_' + str(numSame) + '.xml'
numSame += 1
if filename != None:
if filename[-3:] != ('xml' or 'sbml'):
filename = filename + '.xml'
modelname = filename
zf.writestr(modelname, sbmlPath)
else:
raise Exception("Invalid string for sbml: Check the path of the file")
zf.close()
self.updateManifest(modelname, 'sbml')
[docs] def getModelName(self, sbmlfile):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
r = roadrunner.RoadRunner(sbmlfile)
return r.getModel().getModelName()
[docs] def addAntimony(self, antimonyStr, filename=None):
""" Adds SBML file as Antimony into COMBINE archive.
:param antimonyStr: antimony string
"""
warnings.warn('Use inline_omex instead.', DeprecationWarning)
sbmlStr = te.antimonyToSBML(antimonyStr)
self.addSBML(sbmlStr, filename)
# <SEDML>
[docs] def addSEDML(self, sedmlPath, arcname=None):
""" Adds SEDML file into COMBINE archive.
:param sedmlPath: path to SEDML file
:param arcname: (optional) desired name of SEDML file
"""
warnings.warn('Use inline_omex instead.', DeprecationWarning)
contents = self.listContents()
sedmlBase = os.path.basename(sedmlPath)
try:
arcname, arcFormat = os.path.splitext(arcname)
except AttributeError:
pass
zf = ZipFile(self.combinePath, 'a')
if os.path.exists(sedmlPath):
if arcname == None:
sedmlname, sedmlFormat = os.path.splitext(sedmlBase)
numSame = 0
while contents.count(sedmlname + '.xml') == 1:
sedmlname = sedmlname + '_' + str(numSame)
numSame += 1
zf.write(sedmlPath, arcname=sedmlname + '.xml')
else:
if arcname + '.xml' in contents:
raise Exception('Combine archive contains a file with the same name. Please try different name.')
else:
zf.write(sedmlPath, arcname=arcname + '.xml')
elif sedmlPath.startswith(r'<?xml'):
if arcname == None:
raise Exception("Name of sedml file not defined.")
else:
if arcname + '.xml' in contents:
raise Exception('Combine archive contains a file with the same name. Please try different name.')
else:
zf.writestr(arcname + '.xml', sedmlPath)
else:
raise Exception("Invalid string for sbml: Check the path of the file")
zf.close()
if arcname == None:
self.updateManifest(sedmlname + '.xml', 'sedml')
else:
self.updateManifest(arcname + '.xml', 'sedml')
[docs] def addPhrasedml(self, phrasedmlStr, antimonyStr, arcname=None):
""" Adds SEDML file as phraSEDML string into COMBINE archive.
:param phrasedmlStr: phraSEDML string
:param antimonyStr: antimony string to be referenced
:param arcname: (optional) desired name of SEDML file
"""
warnings.warn('Use inline_omex instead.', DeprecationWarning)
# FIXME: This does not work for multiple referenced models !.
reModel = r"""(\w*) = model ('|")(.*)('|")"""
phrasedmllines = phrasedmlStr.splitlines()
for k, line in enumerate(phrasedmllines):
reSearchModel = re.split(reModel, line)
if len(reSearchModel) > 1:
modelsource = str(reSearchModel[3])
modelname = os.path.basename(modelsource)
modelname = str(modelname).replace(".xml", '')
phrasedml.setReferencedSBML(modelsource, te.antimonyToSBML(antimonyStr))
sedmlstr = phrasedml.convertString(phrasedmlStr)
if sedmlstr is None:
raise Exception(phrasedml.getLastError())
phrasedml.clearReferencedSBML()
self.addSEDML(sedmlstr, arcname)
# Other file
[docs] def addFile(self, filePath):
""" Adds other file into COMBINE archive.
Currently, png, jpg, pdf, txt, csv, dat formats are supported.
:param filePath: path to the file
"""
warnings.warn('Use inline_omex instead.', DeprecationWarning)
acceptedFormats = ('png','jpg','jpeg','pdf','txt','csv','dat')
contents = self.listContents()
fileBase = os.path.basename(filePath)
fileName, fileFormat = os.path.splitext(fileBase)
zf = ZipFile(self.combinePath, 'a')
if os.path.exists(filePath):
if filePath.lower().endswith(acceptedFormats):
if filePath.lower().endswith(('png', 'jpg', 'jpeg')):
numSame = 0
while contents.count("fig/" + fileName + fileFormat) == 1:
fileName = fileName + '_' + str(numSame)
numSame += 1
zf.write(filePath, arcname="fig/" + fileName + fileFormat)
if filePath.lower().endswith(('pdf', 'txt', 'csv', 'dat')):
numSame = 0
while contents.count("data/" + fileName + fileFormat) == 1:
fileName = fileName + '_' + str(numSame)
numSame += 1
zf.write(filePath, arcname="data/" + fileName + fileFormat)
else:
raise Exception("Unsupported file format")
else:
raise Exception("Cannot find the file")
zf.close()
self.updateManifest(fileName + fileFormat, fileFormat[1:])
[docs] def getSBML(self, sbmlfile):
""" returns SBML
:param sbmlfile: filename of SBML file in COMBINE archive
:return: SBML string
:rtype: str
"""
warnings.warn('Use inline_omex instead.', DeprecationWarning)
zf = ZipFile(self.combinePath, 'r')
sbmlStr = zf.read(sbmlfile)
zf.close()
return sbmlStr
[docs] def getSBMLAsAntimony(self, sbmlfile):
""" returns SBML as antimony
:param sbmlfile: filename of SBML file in COMBINE archive
:return: antimony string
:rtype: str
"""
warnings.warn('Use inline_omex instead.', DeprecationWarning)
zf = ZipFile(self.combinePath, 'r')
sbmlStr = zf.read(sbmlfile)
zf.close()
antStr = te.sbmlToAntimony(sbmlStr)
return antStr
[docs] def getSEDML(self, sedmlfile):
""" returns SEDML
:param sbmlfile: filename of SEDML file in COMBINE archive
:return: SEDML string
:rtype: str
"""
zf = ZipFile(self.combinePath, 'r')
sedmlStr = zf.read(sedmlfile)
zf.close()
return sedmlStr
[docs] def getSEDMLAsPhrasedml(self, sedmlfile):
""" returns SEDML as phraSEDML
:param sbmlfile: filename of SEDML file in COMBINE archive
:return: phraSEDML string
:rtype: str
"""
warnings.warn('Use inline_omex instead.', DeprecationWarning)
zf = ZipFile(self.combinePath, 'r')
sedmlStr = zf.read(sedmlfile)
zf.close()
phrasedmlStr = phrasedml.convertString(sedmlStr)
return phrasedmlStr
[docs] def listContents(self):
""" returns simplified content list
"""
warnings.warn('Use inline_omex instead.', DeprecationWarning)
zf = ZipFile(self.combinePath, 'r')
temp = zf.namelist()
zf.close()
return temp
[docs] def listDetailedContents(self):
""" List contents of the archive from the manifest.xml
:return: contentList of dictionaries with keys: filename, type
:rtype: [{}]
"""
warnings.warn('Use inline_omex instead.', DeprecationWarning)
contents = []
man = self.readManifest()
xml = et.ElementTree(et.fromstring(man))
root = xml.getroot()
for child in root:
# get content for xml element
content = None
attribute = child.attrib
formtype = attribute.get('format')
loc = attribute.get('location')
# sbml
if formtype == "http://identifiers.org/combine.specifications/sbml":
if loc.startswith('http') or loc.startswith('www'):
content = {'filename': loc, 'type': 'sbml'}
else:
content = {'filename': os.path.basename(loc), 'type': 'sbml'}
# sedml
elif formtype == "http://identifiers.org/combine.specifications/sed-ml":
zf = ZipFile(self.combinePath, 'r')
try:
sedmlRaw = zf.read(loc)
except KeyError as e:
try:
sedmlRaw = zf.read(os.path.basename(loc))
except KeyError as e:
raise e
sedmlDoc = libsedml.readSedMLFromString(sedmlRaw)
tempSedmlSource = []
for model in sedmlDoc.getListOfModels():
if os.path.splitext(os.path.basename(model.getSource()))[1] == '':
pass
else:
tempSedmlSource.append(os.path.basename(model.getSource()))
content = {'filename': os.path.basename(loc),
'type': 'sedml',
'modelsource': tempSedmlSource}
zf.close()
# manifest
elif formtype == "http://identifiers.org/combine.specifications/omex-manifest":
content = {'filename': os.path.basename(loc), 'type': 'manifest'}
# other formats
elif formtype == "image/png":
content = {'filename': os.path.basename(loc), 'type': 'png'}
elif formtype == "image/jpg" or formtype == "image/jpeg":
content = {'filename': os.path.basename(loc), 'type': 'jpg'}
elif formtype == "application/pdf":
content = {'filename': os.path.basename(loc), 'type': 'pdf'}
elif formtype == "plain/text":
content = {'filename': os.path.basename(loc), 'type': 'txt'}
elif formtype == "plain/csv":
content = {'filename': os.path.basename(loc), 'type': 'csv'}
elif formtype == "plain/dat":
content = {'filename': os.path.basename(loc), 'type': 'dat'}
if content != None:
contents.append(content)
return contents
[docs] def removeFile(self, fileName):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
""" remove specified file from COMBINE archive
:param fileName: name of the file to be removed
"""
baseName, fileFormat = os.path.splitext(fileName)
tempPath = os.path.join(os.path.dirname(self.combinePath), os.path.splitext(
os.path.basename(self.combinePath))[0] + '_tempcombine' + os.path.splitext(os.path.basename(self.combinePath))[1])
zin = ZipFile(self.combinePath, 'r')
zout = ZipFile(tempPath, 'w')
for item in zin.infolist():
buffer = zin.read(item.filename)
if (item.filename != fileName):
zout.writestr(item, buffer)
zout.close()
zin.close()
shutil.move(tempPath, self.combinePath)
if fileName == 'manifest.xml':
pass
else:
self.updateManifest(baseName + fileFormat, fileFormat[1:], delete=True)
[docs] def update(self, targetFile, change):
""" update the contents of SBML or SEDML files in COMBINE archive
Only supports antimony or phrasedml string. For other input types, use removeFile().
:param targetFile: SBML or SEDML file to be updated
:param change: antimony or phrasedml string
"""
warnings.warn('Use inline_omex instead.', DeprecationWarning)
contentList = self.listDetailedContents()
try:
filetype = (item for item in contentList if item["filename"] == targetFile).next().get('type')
except TypeError:
raise Exception('There is no file name called ' + targetFile + 'in COMBINE archive')
if filetype == 'sbml':
self.removeFile(targetFile)
self.addAntimony(change, filename=targetFile)
elif filetype == 'sedml':
modelSource = (item for item in contentList if item["filename"] == targetFile).next().get('modelsource')[0]
refAntStr = self.getSBMLAsAntimony(modelSource)
self.removeFile(targetFile)
self.addPhrasedml(change, refAntStr, arcname=targetFile)
else:
raise Exception('Unsupported file type')
[docs] def readManifest(self):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
zf = ZipFile(self.combinePath, 'r')
try:
man = zf.read(r'manifest.xml')
except KeyError as e:
raise e
zf.close()
return man
[docs] def updateManifest(self, fileName, fileType, delete=False):
warnings.warn('Use inline_omex instead.', DeprecationWarning)
man = self.readManifest()
man = man.splitlines()
if delete == True:
matching = [s for s in man if fileName in s]
man.remove(matching[0])
else:
if fileType == 'sbml':
value = r' <content location="./' + fileName + '" format="http://identifiers.org/combine.specifications/sbml"/>'
elif fileType == 'sedml':
value = r' <content location="./' + fileName + '" master="true" format="http://identifiers.org/combine.specifications/sed-ml"/>'
elif fileType == 'png':
value = r' <content location="./fig/'+ fileName + '" format="image/png"/>'
elif fileType == 'jpg' or fileType == '.jpeg':
value = r' <content location="./fig/'+ fileName + '" format="image/jpg"/>'
elif fileType == 'pdf':
value = r' <content location="./data/'+ fileName + '" format="application/pdf"/>'
elif fileType == 'txt':
value = r' <content location="./data/'+ fileName + '" format="plain/text"/>'
elif fileType == 'csv':
value = r' <content location="./data/'+ fileName + '" format="plain/csv"/>'
elif fileType == 'dat':
value = r' <content location="./data/'+ fileName + '" format="plain/dat"/>'
else:
raise Exception("Unsupported file type")
man.insert(-1, value)
man = "\n".join([str(item) for item in man])
self.removeFile(r'manifest.xml')
zf = ZipFile(self.combinePath, 'a')
zf.writestr(r'manifest.xml', man)
zf.close()