在 Python 中与 Jira API 集成_python集成开发环境有哪两个
在 Python 中连接到 Jira。
概述
继续介绍有关Zato 3.2中最新云连接的系列文章,这一集从调用其 API 以构建 Jira 与其他系统之间的集成的角度介绍了 Atlassian Jira。
与 Jira 集成基本上有两种使用模式:
- Jira 对您项目中发生的事件做出反应,并通过 WebHooks 相应地调用您的端点。在这种情况下,Jira 明确地与您的 API 建立连接并向其发送请求。
- Jira 项目会定期或作为 Jira 使用 WebHook 以外的方式触发的事件的结果进行查询。
第一种情况通常更易于概念化 - 您在 Jira 中创建一个 WebHook,将其指向您的端点,当出现感兴趣的情况时,Jira 会调用它,例如打开或更新新票证。我将在以后的文章中讨论与 Jira 集成的这种变体,因为当前的版本是关于另一种情况,即您的系统与 Jira 建立连接。
首先讨论第二种形式更实际的原因是,即使 WebHooks 更容易推理,它们也有自己的分支。
首先,假设您使用基于云的 Jira 版本(例如https://example.atlassian.net),您需要有一个公开可用的端点供 Jira 通过 WebHooks 调用。很多时候,这是不可取的,因为您需要集成的系统可能是内部系统,绝不会暴露于公共网络。
其次,您的端点需要有一个由公共证书颁发机构签名的 TLS 证书,并且它们需要可以在端口 443 上访问。同样,这两者都是大多数企业系统根本不允许的,或者可能需要数月或数年才能完成在涉及的各个公司部门内部处理此类变更。
最后,即使可以使用 WebHook,您在来自 WebHook 的请求中收到的初始信息也不一定已经包含您在特定集成服务中需要的所有内容。因此,您仍然需要向 Jira 发出请求以查找特定对象的详细信息(例如票证),这样可以将 WebHook 减少为与 Jira 交互的初始触发器的角色,例如 WebHook 调用您的端点,您在输入时有一个工单 ID,然后无论如何调用 Jira 以获取您在业务集成中实际需要的所有详细信息。
最终情况是,虽然 WebHooks 是一个有用的概念,我将在以后的文章中写到,但对于许多集成用例来说,它们可能还不够。这就是为什么我从作为 WebHooks 替代方案的集成方法开始。
WebHooks 的替代品
如果在我们的例子中,我们不能使用 WebHooks,那么接下来怎么办?两个好的方法是:
- 预定作业
- 对电子邮件做出反应(通过 IMAP)
计划的作业将让您定期向 Jira 询问您尚未处理的更改。例如,作业定义如下:
现在,为此作业配置的服务将每分钟调用一次,以执行所需的任何集成工作。例如,它可以获取自上次运行以来的工单列表,根据业务上下文的需要处理每个工单,并使用刚刚完成的信息更新数据库——数据库可以基于 Redis、MongoDB , SQL 或其他任何东西。
当您需要定期扫描大量业务数据时,围绕计划作业构建的集成最有意义,当您不确切知道有多少数据时,这些是“给我上一期更改的所有内容”类型的交互你将收到。
但是,在 Jira 票证的特定情况下,一个有趣的替代方案可能是将计划作业与 IMAP 连接结合起来:
这里的想法是,当打开新票证或对现有票证进行更新时,Jira 将向特定电子邮件地址发送通知,我们可以利用它。
例如,您可以告诉 Jira 给 CC 或 BCC 一个地址。现在,Zato 仍将运行计划作业,但不是直接与 Jira 连接,而是该作业将为其收件箱查找未读电子邮件(根据相关RFC为“UNSEEN” )。
自上次迭代以来,任何未读的内容都必须是新的,这意味着我们可以处理收件箱中的每封此类电子邮件,这样可以保证我们只处理最新的更新,而无需我们自己的已处理票证数据库。我们可以从电子邮件中提取票证 ID 或其他详细信息,在 Jira 中查找其详细信息,然后根据需要继续。
文档中提供了有关如何使用 IMAP 电子邮件的所有详细信息,但归结为:
Python
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class MyService(Service):
def handle(self):
conn = self.email.imap.get('My Jira Inbox').conn
for msg_id, msg in conn.get():
# Process the message here ..
process_message(msg.data)
# .. and mark it as seen in IMAP.
? ? ? ? ? ? msg.mark_seen()
自然的问题是 - “process_message”函数如何从电子邮件中提取工单的详细信息?
有几种方法:
- 每封电子邮件都有一个固定形式的主题 - “[JIRA] (ABC-123) Here go description”。在本例中,ABC-123 是工单 ID。
- 每封电子邮件都会包含一个摘要,例如下面的,也可以解析:
纯文本
Summary: Here goes description
Key: ABC-123
URL: https://example.atlassian.net/browse/ABC-123
Project: My Project
Issue Type: Improvement
Affects Versions: 1.3.17
Environment: Production
Reporter: Reporter Name
? ? ? ? Assignee: Assignee Name
- 最后,每封电子邮件都会有一个“X-Atl-Mail-Meta”标头,其中包含有趣的元数据,这些元数据也可以被解析和提取:
纯文本
X-Atl-Mail-Meta: user_id="123456:12d80508-dcd0-42a2-a2cd-c07f230030e5",
event_type="Issue Created",
? ? ? ? ? ? ? ? ?tenant="https://example.atlassian.net"
第一个选项是最直接且可能最方便的选项 - 只需解析出票证 ID,并在输入时使用该 ID 调用 Jira 以获取有关票证的所有其他信息。如何准确地做到这一点将在下一章中介绍。
无论我们如何解析电子邮件,重要的是我们知道只有在有新的或更新的票证时才会调用 Jira——否则不会有任何新的电子邮件需要处理。此外,因为调用 Jira 的是我们这边,所以我们不会将我们的内部系统直接暴露给公共网络。
但是,从整体安全架构的角度来看,电子邮件仍然是攻击面的一部分,因此我们需要确保在阅读和解析电子邮件时考虑到这一点。换句话说,无论是 Jira 调用我们还是我们阅读来自 Jira 的电子邮件,所有关于 API 集成和接受外部资源输入的常见安全预防措施,所有这些仍然存在并且需要成为集成工作流设计的一部分.
创建 Jira 连接
上面介绍了我们可以到达何时调用 Jira 的步骤,现在我们已经准备好实际执行它了。
与其他类型的连接一样,Jira 连接是在 Zato Dashboard 中创建的,如下所示。请注意,您使用的是您代表其连接到 Jira 的用户的电子邮件地址,但唯一的其他凭据是该用户先前在 Jira 中生成的 API 令牌,而不是用户的密码。
调用 Jira
有了 Jira 连接,我们现在可以创建 Python API 服务。在这种情况下,我们在输入时接受票证 ID(在 Jira 中称为“密钥”),并将票证的一些详细信息返回给调用者。
这是一种可以从由计划作业触发的服务中调用的服务。也就是说,我们将任务分开,一项服务将负责打开 IMAP 收件箱和解析电子邮件,而下面的一项将负责与 Jira 通信。
多亏了这种松耦合,我们让一切变得更加可重用——服务可以独立更改只是其中的一部分,更重要的是,通过这种分离,它们都可以被未来的服务重用,而无需绑定他们死板地把这一单整合在一起。
Python
# -*- coding: utf-8 -*-
# stdlib
from dataclasses import dataclass
# Zato
from zato.common.typing_ import cast_, dictnone
from zato.server.service import Model, Service
# ###########################################################################
if 0:
from zato.server.connection.jira_ import JiraClient
# ###########################################################################
@dataclass(init=False)
class GetTicketDetailsRequest(Model):
key: str
@dataclass(init=False)
class GetTicketDetailsResponse(Model):
assigned_to: str = ''
progress_info: dictnone = None
# ###########################################################################
class GetTicketDetails(Service):
class SimpleIO:
input = GetTicketDetailsRequest
output = GetTicketDetailsResponse
def handle(self):
# This is our input data
input = self.request.input # type: GetTicketDetailsRequest
# .. create a reference to our connection definition ..
jira = self.cloud.jira['My Jira Connection']
# .. obtain a client to Jira ..
with jira.conn.client() as client: # type: JiraClient
# Cast to enable code completion
client = cast_('JiraClient', client)
# Get details of a ticket (issue) from Jira
ticket = client.get_issue(input.key)
# Observe that ticket may be None (e.g. invalid key), hence this 'if' guard ..
if ticket:
# .. build a shortcut reference to all the fields in the ticket ..
fields = ticket['fields']
# .. build our response object ..
response = GetTicketDetailsResponse()
response.assigned_to = fields['assignee']['emailAddress']
response.progress_info = fields['progress']
# .. and return the response to our caller.
self.response.payload = response
# ###########################################################################
创建 REST 通道并对其进行测试
最后剩下的部分是一个 REST 通道,用于调用我们的服务。我们将在输入时提供票证 ID(密钥),服务将回复在 Jira 中为该票证找到的内容。
我们现在准备好进行最后一步 - 我们调用通道,该通道调用与 Jira 通信的服务,将来自 Jira 的响应转换为我们需要的输出:
壳
$ curl localhost:17010/jira1 -d '{"key":"ABC-123"}'
{
"assigned_to":"zato@example.com",
"progress_info": {
"progress": 10,
"total": 30
}
}
$
这就是今天的一切——请记住,这只是与 Jira 集成的一种方式。另一种是使用 WebHooks,我将在以后的一篇文章中介绍。