关闭

2017年3月24日

只需几步,列出你最喜欢的锻造炉的内容

早在2016年11月,尼古拉斯·丹德里蒙特(Nicolas Dandrimont)就写过关于结构代码更改的文章导致Software Heritage归档的存储库数量大幅增加(+1500万!)通过列表和加载调度程序之间的自动链接,对如何处理超大存储库主机有了新的理解github,并激活以前跳过的一组新存储库。

在帖子中,尼古拉斯概述了软件遗产保护过程中的三个主要阶段(列表、计划更新、加载),并强调了保护世界自由软件遗产的能力取决于我们查找和列出存储库的能力。

当时,Software Heritage只能在GitHub上列出项目。及早专注于GitHub,这是世界上最大、最活跃的锻造厂之一,可以实现巨大的物有所值比,并快速启动存档。正如古老的意大利谚语所说,“Il meglioènemico del bene”,或者用现代英语的说法,“完美是善的敌人”,对吗?正确的。因此,从一开始的计划就是为GitHub实现一个列表器,然后可能再实现一个,然后后退几步,眯着眼睛看。

为什么?因为源代码托管服务的行为不符合统一标准。每个新服务都需要专门的开发时间来实现一个新的刮取客户端,以满足该服务API的不可传输需求和复杂性。当时,以一种可扩展和可适应的方式进行这项工作需要一定程度的暴露这些服务之间的无数差异,而我们只是认为我们还没有做到。

Nicolas的帖子以这样一句话结束:“我们还没有开发出一个稳定的API,它只允许您填补空白,因为我们目前只有GitHub列表器,只有当我们拥有一些多样性时,一个经过验证的API才会有机地出现。”

自那以后,情况发生了变化。截至2017年3月6日,软件遗产列表器代码已经过积极的重组、抽象和注释以使创建新列表更加容易。可能还有一些问题需要解决,但是现在制作一个新的列表实际上就像填补空白

基本上,基本列表器必须遵循以下步骤:

  1. 为服务端点发出网络请求。
  2. 将响应转换为规范格式。
  3. 填充用于提取和接收源存储库的工作队列。

步骤1和3是一般性问题,因此它们可以获得隐藏在基本代码中的一般性解决方案,其中大多数永远不需要更改。这让我们需要实现步骤2,对于使用干净的web API的服务,现在可以完成这一步骤。

在新代码中,我们试图隐藏尽可能多的通用功能,将其转换为几个简单的自定义元素之间的设置和忘记管道。不同的托管服务可能使用不同的网络协议、速率限制消息或分页方案,但是,只要有某种方法可以获得托管存储库的列表,我们认为新的基本代码将使获取这些存储库变得更加容易。

首先让我给你30000英尺的视野…

旧的GitHub-specific列表器代码如下所示(265行Python):

相比之下,新的GitHub特定代码看起来是这样的(Python的34行):

新的特定于BitBucket的代码甚至更短,如下所示(24行Python):

现在,这是几个抽象基类中常见的共享代码,带有一些新功能和大量文档字符串注释(红色):

那么列表器代码现在是如何工作的,以及一个有贡献的开发人员如何着手创建一个新的开发人员?

首先要知道的是,我们现在有了一个通用的lister基类和ORM模型。列表器库的子类应该已经能够完成完成单个服务请求/响应周期的列表任务所需的几乎所有操作,实现要求如下:

  1. 必须声明调用成员变量模型,它等于基本ORM模型的子类(注意:类型,而不是实例)。使用子类的原因主要是因为不同的服务对其存储库使用不同的不兼容主标识符。模型子类通常只是一个或两个附加变量声明。
  2. 一个名为运输请求必须实现,它获取完整的目标标识符(例如URL),并尝试使用与服务交互所需的任何传输协议一次性请求它。它不应该尝试在超时时重试或对响应执行任何其他操作(这已经为您完成了)。它应该只返回响应或引发获取错误例外。
  3. 一个名为传输响应字符串必须实现,这将获取(1)中请求的整个响应,并将其转换为用于日志记录的字符串。
  4. 一个名为运输配额检查必须实现,它获取(1)中请求的整个响应,并检查进程是否违反了任何查询配额或速率限制。如果服务说在发出更多请求之前等待,则该方法应返回真的以及等待的秒数,否则返回False(错误)
  5. 一个名为运输响应简化必须实现,它还接受(1)中请求的整个响应,并将其转换为一个由dict组成的Python列表(每个存储库一个dict),其中的键是根据前面提到的MODEL类成员给定的。

因为1、2、3和4基本上只依赖于所选的网络协议,所以我们还有一个HTTP mix-in模块,它补充了列表器基础,并为这些方法提供了默认实现,以及使用Python Requests库的可选请求头注入。这个运输配额检查提供的方法遵循IETF标准,用于与HTTP代码429一些托管服务选择不遵循,因此可能需要特定的列表器覆盖它。

除此之外,我们还为基类lister提供了另一层,该层增加了对索引顺序循环的支持。什么是指数?嗯,一些服务(比特桶例如,GitHub)不会一次向您发送所有存储库的完整列表,因为服务器的响应会很难处理。相反,它们对结果进行分页,还允许您查询它们的API,如下所示:https://server_address.tld/query_type?起始列表源id=foo公司。更改“foo”的值可以从那里获取一组存储库。我们将“foo”称为索引,并将以这种方式工作的服务称为索引服务。GitHub使用存储库唯一标识符,BitBucket使用存储库创建时间,但只要值随新存储库单调增加,服务就可以真正使用任何东西。一个好的索引服务还包括下一个页面的URL,并在其响应中包含后面的“foo”。对于这些索引服务,我们提供了另一个名为索引列表器的中间列表器。而不是继承自SWH柱基,lister类将继承自SWH索引列表器。除了列表器库的要求外,索引列表器库还增加了一个额外的要求:

  1. 一个名为获取扩展目标来自响应必须定义,它接受完整的请求响应并返回下一页的索引(上面的“oo”)。

这些都是基本要求。当然,还有一些其他的小细节(现在在代码的文档字符串注释中介绍),但大部分都是这样。听起来需要吸收和实现的信息很多,但请记住,上面提到的大多数实现要求已经由HTTP混合模块为99%的服务提供了。当我们看看我们目前拥有的两种新型索引列表器的实际实现时,它看起来要简单得多…

这是BitBucket存储库列表器的全部源代码。

#版权所有(C)2017软件遗产开发者#许可证:GNU通用公共许可证第3版或更高版本#有关详细信息,请参阅顶级许可证文件从urllib导入解析从swh.lister.bitbucket.models导入BitBucketModel从swh.lister.core.indexing_lister导入SWHIndexingHttpLister类BitBucketLister(SWHIndexingHttpLister):PATH_TEMPLATE='/repositories?在=%s'之后MODEL=BitBucket模型定义get_model_from_repo(self,repo):return{'uid':报告['uuid'],“可索引”:repo['created_on'],'name':回购['name'],“完整名称”:repo['full_name'],'html_url':回购['links']['html']['href'],'origin_url':回购['links']['clone'][0]['href'],'origin_type':repo['scm'],“description”:回购['description']}定义get_next_target_from_response(自我,响应):body=响应.json()如果正文中有“next”:return parse.uncote(body['next'].split('after=')[1])其他:return无定义transport_response_simplified(self,response):repos=response.json()['values']return[self.get_model_from_repo(repo)for repo in repo]

这是GitHub存储库列表器的全部源代码。

#版权所有(C)2017软件遗产开发者#许可证:GNU通用公共许可证版本3或更高版本#有关详细信息,请参阅顶级许可证文件导入时间从swh.lister.core.indexing_lister导入SWHIndexingHttpLister从swh.lister.github.models导入GitHubModel类GitHubLister(SWHIndexingHttpLister):PATH_TEMPLATE='/repositories?因为=%d'模型=GitHubModel定义get_model_from_repo(self,repo):return{'uid‘:repo〔'id‘〕,“可索引”:repo['id'],'name':回购['name'],“完整名称”:repo['full_name'],“html_url”:报告[“html_ url”],“原始url”:repo[“html_url”],“origin_type”:“git”,“description”:回购['description']}定义get_next_target_from_response(自我,响应):如果“下一步”响应。链接:next_url=response.links['next']['url']return int(next_url.split('sine=')[1])其他:return无定义transport_response_simplified(self,response):repos=响应.json()return[self.get_model_from_repo(repo)for repo in repo]def请求标头(自身):return{“接受”:“应用程序/vnd.github.v3+json”}def transport_quota_check(自我,响应):remain=int(response.headers['X-RateLimit-Remaining'])如果response.status_code==403并且保持==0:reset_at=int(响应.headers['X-RateLimit-reset'])延迟=分钟(reset_at-time.time(),3600)return True,延迟其他:返回False,0

我们可以看到,有一些共同的要素:

  • 两者都使用HTTP传输混合(SWH索引HttpLister联合收割机SWH寄存器HttpTransportSWH索引列表器)免费获得大部分网络请求功能。
  • 两者都定义了模型路径_模板变量。开发人员应该清楚路径_模板,当与基本服务URL(例如“https://some_service.com“)并传递一个值(前面描述的'foo'索引),从而生成一个完整的标识符,用于向这些服务发出API请求。它是我们的HTTP模块所必需的。
  • 这两个服务都使用JSON进行响应,因此运输响应简化都很相似而且很短。

我们还可以看到一些差异:

  • GitHub将下一个URL作为响应头的一部分发送,而BitBucket将其发送到响应正文中。
  • GitHub通过请求头区分API版本(我们的HTTP传输混合将自动使用可选的请求标头方法),而BitBucket将其作为其基本服务URL的一部分。
  • BitBucket使用IETF标准的HTTP 429响应代码来发送速率限制通知(HTTP传输混入自动处理),而GitHub使用需要特殊处理的自定义响应标头。

但看看他们!58行Python代码,合并,从两个最大、最有影响力的源代码托管服务中吸收所有存储库。

好吧,那么幕后发生了什么?

为了跟踪代码的操作,让我们从一个示例实例化开始,并从那里开始查看何时调用了哪些方法。接下来将是一系列极端简化的伪代码方法。这不是代码实际的样子(它甚至不是真正的代码),但它有相同的基本流。请耐心听我说,我试着以准线性的方式安排lister操作…

#主要任务ghl=GitHubLister(lister_name='github.com',api_baseurl='https://github.com')ghl.run()

(SWHIndexingLister.run)

#SWHIndexingLister.run软件标识符=无response,repos=SWHListerBase.ingute_data(标识符)标识符=GitHubLister.get_next_target_from_response(响应)while(标识符)

(SWHListerBase.inset_data)

#SWH列表基础最大数据响应=SWHListerBase.safely_issue_request(标识符)repos=GitHubLister.transport_response_simplified(响应)injected=SWHListerBase.inject_repo_data_into_db(repo)返回响应,注入

(SWHListerBase.safely_issue_request)

#SWHListerBase.safely_问题_请求重复:resp=SWHListerHttpTransport.transport_request(标识符)retry,delay=SWHListerHttpTransport.transport_quota_check(resp)如果重试:睡眠(延迟)直到(不重试)或too_many_retries)返回resp

(SWHListerHttpTransport.transport_request)

#SWH列表HttpTransport.transport_request路径=SWHListerBase.api_baseurl+SWHListerHttpTransport。PATH_TEMPLATE%标识符headers=SWHListerHttpTransport.request_headers()return http.get(路径,头)

(哦,看,这是我们的PATH_TEMPLATE)
(SWHListerHttpTransport.request_headers)

#SWHListerHttpTransport.request_headersoverride→GitHubLister.request_headers

↑↑ (SWHListerBase.safely_issue_request)⇓(SWHLesterHttpTransport.transport_quota_check)

#SWH列表HttpTransport.transport_quota_check覆盖→GitHubLister.transport_quota_check

然后我们就完成了。从头到尾,我希望这有助于您了解少数定制部件如何适合新的共享管道。

现在你可以为我们还没有的代码托管网站写一个列表了!
我们针对列表者的git存储库可在线访问https://forge.softwareheritage.org/source/swh-lister网站/


-阿维·凯尔曼