Brenda-利用SOAP API访问Brenda及本地保存

Target

  1. 使用Brenda的SOAP API来获取JSON格式的信息,并建立本地的文件夹来保存信息,构建日志文件记录更新与获取历史。
  2. 利用SOAP API获取与Txtfile相同的内容,整理为同样格式的TSV文件。

Brenda SOAP API

官方介绍:Brenda - SOAP access help
使用Python首先需要安装Zeep。我在Anaconda中更新了lxml库(4.6.2),然后用pip方法安装了zeep。

客户端配置

官网的示例代码中配置了客户端并给出一个简单的案例(略有改动)。

from zeep import Client
import hashlib
# 客户端
wsdl = "https://www.brenda-enzymes.org/soap/brenda_zeep.wsdl"
client = Client(wsdl)
# 用户名和密码
email = "j.doe@example.edu" # 首先在Brenda上用邮箱注册一个账号。
password = hashlib.sha256("myPassword".encode("utf-8")).hexdigest()
# 获取ecNumber = 1.1.1.1 & organism = Homo sapiens的kmValue的案例。(后详述)
parameters = (email,password,"ecNumber*1.1.1.1","organism*Homo sapiens","kmValue*", "kmValueMaximum*","substrate*","commentary*","ligandStructureId*","literature*" )
resultString = client.service.getKmValue(*parameters)
print (resultString)
SOAP API

下面谈一谈SOAP API的组织逻辑。页面上给出了众多的SOAP methods,它们在函数名、参数和返回值上有微小的差别。
SOAP methods
我们以Reaction为例,单击Reaction跳转到Reaction部分。Reaction部分有以下三个方法(109-111):

getEcNumbersFromReaction()
getOrganismsFromReaction()
getReaction()

它们的Output有说明,分别是:

String containing an array of the different EC Numbers linked to Reaction, e.g. “[‘EC Number1’,‘EC Number2’,‘EC Number3’,‘EC Number4’]”

String containing an array of Organisms (Python 3) linked to Reaction, e.g. “[‘Organism1’,‘Organism2’,‘Organism3’,‘Organism4’]”

String containing an array of Reaction objects, e.g. “[{‘ecNumber’:string, ‘reaction’:string, ‘commentary’:string, ‘literature’:[int1,int2,int3,…], ‘organism’:string},{‘ecNumber’:string, ‘reaction’:string, ‘commentary’:string, ‘literature’:[int1,int2,int3,…], ‘organism’:string},…]”

由上,三个方法的含义是:从“Reaction”表中获得EcNumber(get EcNumbers From Reaction Table);从“Reaction”表中获得Organisms;从“Reaction”表中根据 String 参数获取表的内容。

参数

Brenda以EC号和来源生物作为酶的定义,实际上前两个方法是获取酶的自变量。因此,我们只需要提供用户名和密码参数即可。

parameters = (email, password)
EcNumbersOfReaction = client.service.getEcNumbersFromReaction(*parameters)
OrganismsOfReaction = client.service.getOrganismsFromReaction(*parameters)

对于getReaction(string)方法。

Either the key field ecNumber (e.g. “ecNumber1.1.1.2"), the key field organism (e.g. "organismHomo sapiens”) or both (e.g. “ecNumber1.1.1.2#organismHomo sapiens”) have to be specified. If none of these key fields is used as an input parameter, an empty String will be returned by the SOAP query. In addition, the following optional parameters can be specified: “reaction”, “commentary”, “literature”

参数中需要给出ecNumber或者organism,或者二者均给定。返回Reaction表中对应的记录。
代码如下:

parameters = (email, password, "ecNumber*1.1.1.1", "reaction*Mus musculus", "commentary*", "literature*", "organism*")
Reactions = client.service.getReaction(*parameters)

另外,这里的输入必须是完整的:

# 去掉了参数中的"literature*"
parameters_notcomplete = (email, password, "ecNumber*1.1.1.1", "reaction*", "commentary*", "organism*Mus musculus")
Reactions_null = client.service.getReaction(*parameters_notcomplete)

结果会报错:

ValidationError: Missing element literature (getReaction.literature)

其余参数同理。

输出

EcNumbersOfReaction 和 OrganismsOfReaction 是List of str:

type(EcNumbersOfReaction) # zeep.objects.ArrayOfStrings
isinstance(EcNumbersOfReaction, list) # True
len(EcNumbersOfReaction) # 7006
EcNumbersOfReaction[0:2] # ['0.0.0.0', '1.1.1.1']
type(EcNumbersOfReaction[0]) # str
len(OrganismsOfReaction) # 2492
OrganismsOfReaction[0:2] # ['Abies grandis', 'Abrus precatorius']

Reactions 是List of dict-like-records:

type(Reactions) # zeep.objects.ArrayOfReactions
isinstance(Reactions, list) # True
type(Reactions[0]) # zeep.objects.reaction # 是内部定义的一个类
isinstance(Reactions[0], collections.abc.Mapping) # False
isinstance(Reactions[0], str) # False
dir(Reactions[0]) # ['commentary', 'ecNumber', 'literature', 'organism', 'reaction']
help(Reactions[0]) 
'''
 |  Method resolution order:
 |      reaction
 |      zeep.xsd.valueobjects.CompoundValue
 |      builtins.object
'''
Reactions[0]
'''
{
    'reaction': 'a primary alcohol + NAD+ = an aldehyde + NADH + H+',
    'commentary': 'ordered bibi mechanism, structural and functional implications of amino acid residue 47',
    'literature': [
        654730
    ],
    'organism': 'Mus musculus',
    'ecNumber': '1.1.1.1'
}
'''
Reactions[0]['reaction'] # 'a primary alcohol + NAD+ = an aldehyde + NADH + H+'
isinstance(Reactions[0]['literature'], list) # True
isinstance(Reactions[0]['literature'][0], int) # True

获取其它内容的方式与Reaction一致,需要哪些内容,便去查阅帮助文档获取对应的参数写法。

其它事项
  1. 是否有访问次数和访问频率限制?
    目前感觉没有。
  2. 访问速度?
    按照全库下载来看,使用EC号进行访问要比Organism来得更好,并且不会报错。
命令时间(s)规模(访问次数, 结果长度)sleep time(s)
(‘KmValue’, ‘1.1.1.1’)2.88(1,1057)-
(‘Reaction’, ‘’, ‘’)1065(2492,10271)0.1

TODO List

Brenda类
Brenda_SOAP

写一个封装类Brenda_SOAP,利用"策略"设计模式以统一接口来调用SOAP API。

from zeep import Client
import hashlib
from operator import methodcaller

class Brenda_SOAP:
    def __init__(self, username, password):
        self.username = username
        self.password = hashlib.sha256(password.encode("utf-8")).hexdigest()
        self.wsdl = "https://www.brenda-enzymes.org/soap/brenda_zeep.wsdl"
        self.client = Client(self.wsdl)

    def getEcNumbers(self, table):
        return EcNumbers
    
    def getOrganisms(self, table):
        return Organisms
        
    def getContent(self, table, ec = '', organism = ''):
        '''

        从 table 中读取给定 ec 和/或 organism 的记录;如果二者均未指定,那么返回所有内容。

        Returns
        -------
        list of dict

        '''
        return content
Brenda_local

实现本地文件的“缓存”机制。

  • 在文件夹内创建Content_[table] / EcNumber_[table] / Organism_[table]文件保存网络内容,实现其读取
  • 构建log文件记录通过API获取内容写入文件的记录
  • 构建schema.pkl文件记录各个[table]的格式
class Brenda_local:
    def __init__(self, path):
        self.path = path
        # self.logger # pass
        
    def writeEcNumbers(self, table, EcNumbers):
        pass
    def writeOrganisms(self, table, Organisms):
        pass
    def writeContent(self, table, content):
        pass    
        
    def readEcNumbers(self, table):
        return EcNumbers
    def readOrganisms(self, table):
        return Organisms
    def readContent(self, table, ec = '', organism = ''):
    	'''
        从 table 中读取给定 ec 和/或 organism 的记录;如果二者均未指定,那么返回所有内容。
        Parameters
        ----------
        table : str
        ec : str, optional. The default is ''.
        organism : str, optional. The default is ''.
        
        Returns
        -------
        list of dict

        '''
        return content
        
Brenda
  • 封装上述二者,实现内容获取(来自网络或本地)和保存,由一个参数控制:‘default’,‘soap’,‘local’。
  • 维护一个日志文件记录获取网络内容的时间,用于流程控制。
  • get方法实现批量获取,即 ec 和 organism 均不提供时表明获取全表内容(与SOAP API 的接口不同)。
class Brenda:
    def __init__(self, email, password, path):
        self.bs = Brenda_SOAP(email, password)
        self.bl = Brenda_local(path)
        self.mode = 'default' # 'soap' or 'local'
    
    def getEcNumbers(self, table):
        return EcNumbers
    
    def getOrganisms(self, table):
        return Organisms
    
    def getContent(self, table, ec = '', organism = ''):
    	'''
        从 table 中读取给定 ec 和/或 organism 的记录;如果二者均未指定,那么返回所有内容。

        Returns
        -------
        list of dict

        '''
        return content
利用 Brenda_SOAP来制作TSV表格

这个再写一篇博文来说明吧。

表格schema
重构genTSVs(brenda)

代码工作

Brenda_SOAP
要点
  1. 实现“策略”模式,主要是对SOAP API实现接口的统一。对于EcNumbers和Organisms可以通过getattr()方法将table(str)转化为函数名。
   def _get(self, directory, table):
        return getattr(self.client.service, 'get'+ directory +'From' + table)(*(self.email, self.password))
  1. 由于Brenda_SOAP作为Brenda的一个子类,对Brenda而言,被调用的接口也应该实现接口的统一。

‘get’ + ‘EcNumber’ (*parameter), paramter[0] = table

   def getEcNumbers(self, table)
   def getOrganisms(self, table)
   def getContent(self, table, ec = '', organism = '')
  1. 对于getContent()方法,修改了默认的返回,即对于ec和organism不给定时,返回全表。这通过循环 ec 或 organism 列表来实现。
				#### 略 ####
				content = []
                ecs = self.getEcNumbers(table)
                for ec_i in ecs:
                    parameter = self._up + ('ecNumber*'+ec, 'organism*') + tuple(p+'*' for p in self._parameter_dict[table])
                    content += getFromTable(*parameter)
  1. 对于 SOAP API 的get方法,面临的问题是对于不同的表格获取内容时,其输入参数有些微差别,如:

KmValue
(email,password,“ecNumber*”, “kmValue*”, “kmValueMaximum*”, “substrate*”, “commentary*”, “organism*”, “ligandStructureId*”, “literature*”)
Reaction
(email,password,“ecNumber*”, “reaction*”, “commentary*”, “literature*”, “organism*”)

测试结果表明:1. 参数传入次序无关;2.参数的名称特别重要;3.必须恰好是所需的几个参数名称。
能想到的思路直接但比较费力的解决方案就是将所有方法的配置写到一个文件里;然后从这个文件里读参数配置方法。
这个文件可以人工录入:把目前感兴趣的几个量写进去。做了一个接口更新该文件及变量,可以直接复制粘贴网页上的内容。

class Brenda_SOAP:
    def __init__(self, email, password):
    	#### 略 ####
        self._parameter_schema_file = os.path.join(PATH, 'schema.pkl')
        self._parameter_dict = self._getParameterSchema()
        
    def _getParameterSchema(self):
        with open(self._parameter_schema_file, 'rb') as f:
            parameter_dict = pickle.load(f)
        return parameter_dict
    
    def refreshParameterSchema(self, table, list_of_para):
        '''
        将 https://www.brenda-enzymes.org/soap.php 上的【表名称】和【标黄的参数】作为参数传入,更新 schema.pkl 文件
        self.refreshParameterSchema('KmValue',["kmValue", "kmValueMaximum", "substrate", 
                                                         "commentary", "ligandStructureId", "literature"])
        Parameters
        ----------
        table : str
        list_of_para : list of str

        '''
    	#### 略 ####

schema是一个不包括’ecNumber’和’organism’的 dict of {table: list of str}。

{‘Reaction’:[‘reaction’,‘commentary’,‘literature’]}

  1. import mylog 来做网络访问的日志记录。
import mylog
ml = mylog.mylog(os.path.join(PATH, 'log.txt'))
logfunc = ml.info
						#### 略 ####
                        logfunc("{}: Ec = {}".format(i, ec_i))

其内容如下:

[Sat 06 Mar 2021 08:53:42] 0: Ec = Abies grandis

  1. 网络问题与异常处理
    大规模非并行访问时测试一些time.sleep(time)的参数。发现 time 为0和1时会报错。

XMLSyntaxError: PCDATA invalid Char value 2, line 2, column 785

这个错误不是网络问题,是lxml解析网页时报错(参考)。下述语句也会报错:

contents_97 = bs.getContent(table, '', 'Allochromatium vinosum')

输出中主要嵌套的报错有

lxml.etree.XMLSyntaxError,
zeep.exceptions.XMLSyntaxError,
zeep.exceptions.TransportError

测试了一些异常处理语句,下述语句可以正确处理异常。

from zeep import exceptions
try:
    contents_97 = bs.getContent(table, '', 'Allochromatium vinosum')
except (exceptions.TransportError):
    print('Yes!')

一些报上述词错误的命令:

测试代码
table = 'Reaction'
ec = '1.1.1.1'
org = 'Mus musculus'
bs = Brenda_SOAP(EMAIL, PASSWORD)
ecs = bs.getEcNumbers(table)
len(ecs) # 7006
orgs = bs.getOrganisms(table)
len(orgs) # 2492
## getContent 的各种调用方法
contents = bs.getContent(table, ec, org)
len(content) # 1
contents = bs.getContent('KmValue', '1.1.1.1')
len(contents) # 1057
contents = bs.getContent('KmValue', '', org)
len(contents) # 3022
contents = bs.getContent(table, '', '')
len(contents) # 10271
bs.refreshParameterSchema("Engineering", ["ecNumber*", "engineering*", "commentary*", "organism*", "literature*"])
Brenda_local
要点
  1. 将EcNumbers写入EcNumbers文件夹中的table.pkl中,以list of str形式保存;同理,Organisms写入同名文件夹的table.pkl中,以list of str保存;为保持代码的一致性,将表的内容也pickle.dump到Content文件夹中。这通过内置 write 和 read 函数来实现。
class Brenda_local:
	#### 略 ####
	def _write(self, directory, table, content):
        with open(os.path.join(self.path, directory, table + '.pkl'), 'wb') as f:
            pickle.dump(self._formalize(directory, content), f)        
    
    def _read(self, directory, table):
        with open(os.path.join(self.path, directory, table + '.pkl'), 'rb') as f:
            content = pickle.load(f)
        return content
  1. 由于zeep有其自身实现的类,因此需要一个规范化函数,将使用的数据类型转化为list,str 和 dict。
    def _formalize(self, directory, content):
    	#### 略 ####
    	return list_content
  1. 实现函数名称对外的接口一致性。
  2. readContent() 对于 ec 和 organism 参数的处理与 Brenda_SOAP.getContent() 一致。
测试代码
bl = Brenda_local(PATH)
bl.writeEcNumbers(table, ecs)
ecs_local = bl.readEcNumbers(table)
ecs_local == list(ecs) # True
bl.writeOrganisms(table, orgs)
orgs_local = bl.readOrganisms(table)
orgs_local == list(orgs) # True
bl.writeContent(table, contents)
content_local = bl.readContent(table, '', org)
len(content_local) # 281
content_local = bl.readContent(table, ec, org)
len(content_local) # 1
content_local = bl.readContent(table, ec, '')
len(content_local) # 23
content_local = bl.readContent(table)
len(content_local) # 10271
Brenda_logger
要点
  1. 为了流程控制,首先写了个logger类(为什么不用logging呢?— log记录的信息是给人读的,而这里的logger相当于代码读的),其数据保存在一个字典(dict of {tuple: float})里。具有初始化从文件读入,向文件写入,向logger写入,判断是否在logger中四个方法。判断是否在logger中使用特殊方法__contains__实现。
class Brenda_logger:
    def __init__(self, path):
        self.path = path
        self._records = self.init()
    
    def init(self):
        if not os.path.exists(os.path.join(self.path, 'log.pkl')):
            record = {}
        else:
            with open(os.path.join(self.path,'log.pkl'), 'rb') as f:
                record = pickle.load(f)
        return record
    def save(self):
        with open(os.path.join(self.path,'log.pkl'), 'wb') as f:
            pickle.dump(self._record, f)
            
    def log(self, *key):
        self._record[parameters] = time.time() # 以浮点数形式记录获取内容的时刻
        self.save()
    
    def __contains__(self, key):
        if len(key) == 2: # 通过长度来识别key
            return key in self._records.keys()
        elif len(key) == 3:
            return key in self._records.keys() or key[:2] in self._records.keys()
        elif len(key) == 4:
            return any((key in self._records.keys(), key[:2] in self._records.keys(),
                        key[:3] in self._records.keys(), (key[0], key[1], key[3]) in self._records.keys()))
        else:
            return False  
  1. 这个字典的键可以是以下的值,用于获取内容时的流程判断。
logger的键含义
(‘Content’, table, ‘’, ‘’)全表内容
(‘Content’, table, ec, ‘’)table 中 ecNumber = ec 的内容
(‘Content’, table, ‘’, org)table 中 organism = org 的内容
(‘Content’, table, ec, org)table 中 ecNumber= ec & organism = org 的内容
(‘EcNumbers’, table)getEcNumbers 所返回的 EcNumbers 列表
(‘Organisms’, table)getOrganisms 所返回的 Organisms 列表
测试代码
logger = Brenda_logger(PATH)       
logger._records = {}
logger.save()
logger.log('Content', table)
('EcNumbers', table) in logger # False
logger.log('EcNumbers', table)
('EcNumbers', table) in logger # True
logger.log('Organisms', table)
('Content', table, ec) in logger # False
('Content', table, ec, org) in logger # False
logger.log('Content', table, ec, '')
logger.log('Content', table, '', org)
('Content', table, ec, org) in logger # True
logger.log('Content', table, ec, org)
('Content', table) in logger # False
('EcNumbers', table) in logger # True
('Organisms', table) in logger # True
('Content', 'KmValue') in logger # False
logger.log('Content', table, '', '')
('Content', table, '1.1.1.2') in logger # True
('Content', table, 'Homo Sapiens') in logger # True
('Content', table, ec, org) in logger # True 
Brenda
主要问题
  1. Brenda的get函数的逻辑:
    根据模式设定(‘default’, ‘local’, ‘soap’) 和 logger的记录判断获取内容的方式;
    自网络获取内容,那么要更新本地的logger记录并保存文件。
    自本地获取内容。
  2. Brenda_SOAP和Brenda_local 实现名称与参数一致的接口,最大限度的减少模板代码。
    def _get(self, *key):
        if ((self.mode != 'local') and (not key in self.logger)) or (self.mode == 'soap'):
            results = getattr(self.bs, 'get' + key[0])(*key[1:])
            getattr(self.bl, 'write' + key[0])(key[1], results)
            self.logger.log(*key)
        elif self.mode != 'soap' and key in self.logger:
            results = getattr(self.bl, 'read' + key[0])(*key[1:])
        else:
            results = None
        return results
  1. 有一些边缘情况
    返回的表中不是所有的项都有organism这个数据的。
contents_ec = brenda.getContent(table, ec, '')
'''
[{'reaction': 'a primary alcohol + NAD+ = an aldehyde + NADH + H+',
  'commentary': 'ordered bi-bi mechanism',
  'literature': [285598],
  'organism': 'Drosophila melanogaster',
  'ecNumber': '1.1.1.1'},
……,
 {'reaction': 'a secondary alcohol + NAD+ = a ketone + NADH + H+',
  'literature': [0],
  'ecNumber': '1.1.1.1'}]
'''

给Brenda_local的判断上加补丁:

                return [ c for c in content if ('organism' in c and c['organism'] == organism) ]

测试代码
brenda = Brenda(EMAIL, PASSWORD, PATH)
brenda.logger._records = {} # 为了测试,将记录清空
brenda.logger.save()
ecs = brenda.getEcNumbers(table) # 默认方法
# [Sat 06 Mar 2021 11:50:36] (EcNumbers,Reaction)
len(ecs) # 7006
('EcNumbers', table) in brenda.logger # True
ecs_local = brenda.getEcNumbers(table)
len(ecs_local) # 7006
brenda.mode = 'soap'
ecs_local = brenda.getEcNumbers(table) # 网络
# [Sat 06 Mar 2021 11:50:53] (EcNumbers,Reaction)
brenda.mode = 'local'
orgs = brenda.getOrganisms(table) # 本地
orgs == None # True
brenda.mode = 'default'
orgs = brenda.getOrganisms(table) # 默认方法
# [Sat 06 Mar 2021 11:52:49] (Organisms,Reaction)
len(orgs) # 2492
## 测试getContent 接口
contents_ec_org = brenda.getContent(table, ec, org)
len(contents_ec_org) # 1
contents_ec = brenda.getContent(table, ec, '')
len(contents_ec) # 29
contents_ec_Dm = brenda.getContent(table, ec, 'Drosophila melanogaster')
len(contents_ec_Dm) # 2
contents_Dm = brenda.getContent(table, '', 'Drosophila melanogaster')
len(contents_Dm) # 78
contents_ec_Dm = brenda.getContent(table, ec, 'Drosophila melanogaster')
len(contents_ec_Dm) # 2
## 测试全表获取 接口
contents_Reaction = brenda.getContent(table)
# 输出 1mol log 信息。
len(contents_Reaction) # 10271
contents_Reaction_ec = brenda.getContent(table, ec)
contents_Reaction_local = brenda.getContent(table)
len(contents_Reaction_local) # 10271

More to do

代码规范化
  • 利用doctest方法规范化上述测试代码部分。
  • Brenda_SOAP中可以再抽象一下_get方法。
  • 重新打包代码。
  • 更详细的接口说明。
  • 代码打包放到Github上。
其它功能
  • 有一些其它的方法被忽略了,例如:getLigandStructureIdByCompoundName(string)
    getReferenceById(String)
    getReferenceByPubmedId(String)
  • 有很多表根本没有 organism 这个entry,需要再修改一下代码,加一些判断逻辑。

总结

其实还是写了很多没用的东西,还有一些有用的(可能更重要的)功能没有写。上述实际上是假设了工作流中,下载一部分数据库,另一部分在网上,按需取用。但是实际上的工作流是不管三七二十一,先全部下载下来,直接在本地使用(串行的网络访问只需要不到20小时)。

参考资料

python 使用函数名的字符串调用函数(4种方法) - CSDN, 2018-11
Programming FAQ - python官方文档, 2021-03

class Foo:
    def do_foo(self):
        ...
    def do_bar(self):
        ...
f = getattr(foo_instance, 'do_' + opname)
f()

Spyder returning TypeError after updating to Spyder 4.1.4
python保存字典和读取字典pickle

import pickle

def save_obj(obj, name):
    with open(name + '.pkl', 'wb') as f:
        pickle.dump(obj, f, pickle.HIGHEST_PROTOCOL)
def load_obj(name):
    with open(name + '.pkl', 'rb') as f:
        return pickle.load(f)
  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值