PEP 691 – 基于 JSON 的 Python 包索引简单 API
- 作者:
- Donald Stufft <donald at stufft.io>,Pradyun Gedam <pradyunsg at gmail.com>,Cooper Lees <me at cooperlees.com>,Dustin Ingram <di at python.org>
- PEP 代理人:
- Brett Cannon <brett at python.org>
- 讨论至:
- Discourse 帖子
- 状态:
- 已接受
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建日期:
- 2022 年 5 月 4 日
- 发布历史:
- 2022 年 5 月 5 日
- 决议:
- Discourse 消息
摘要
在 PEP 503 中定义的“简单仓库 API”(并且使用时间远长于此)长期以来一直运行良好。然而,对使用 HTML 作为数据交换机制的依赖存在几个缺点。
基于 HTML 的 API 存在两个主要问题
- 虽然 HTML5 是一个标准,但它是一个极其复杂的标准,确保对其进行完全正确的解析涉及复杂的逻辑,而这些逻辑目前在 Python 标准库(以及许多其他语言的标准库)中都不存在。
这意味着为了实际接受所有技术上有效的内容,工具必须引入大型依赖项,或者它们必须依赖于标准库的
html.parser
库,该库更轻量级,但可能不完全支持 HTML5。 - HTML5 主要设计为一种用于向人类呈现文档的标记语言。我们对其的使用主要是出于历史和偶然原因,如果从头开始,没有人会设计一个依赖于它的 API。
使用为人类消费而设计的标记格式的主要问题是,在 HTML 中实际编码数据并没有很好的方法。我们通过限制我们放入此 API 的数据以及创造性地将数据塞入 API 来解决这个问题(例如,哈希值嵌入为 URL 片段,在 PEP 592 中添加
data-yanked
属性)。
PEP 503 主要是为了标准化已经在使用中的内容,因此它没有提议对 API 进行任何重大更改。
在过去的几年里,我们经常谈论一个将重新构想 PyPI 整个 API 的“API V2”。然而,由于时间限制,这项工作除了人们认为这样做会很好之外,没有获得太多(如果有的话)进展。
本 PEP 尝试走一条不同的路线。它没有从根本上改变整体 API 结构,而是指定了一种新的序列化,将现有 PEP 503 响应中包含的现有数据以一种更容易让软件解析的格式呈现,而不是使用以人为中心的文档格式。
目标
- 实现零配置发现。简单 API 的客户端必须能够优雅地确定目标仓库是否支持本 PEP,而无需依赖任何形式的带外通信(配置、先验知识等)。但是,单个客户端可以选择要求配置来启用此 API 的使用。
- 使客户端能够放弃对“传统”HTML 解析的支持。虽然预计大多数客户端将在一段时间内(如果不是永远)继续支持仅 HTML 仓库,但客户端应该能够选择仅支持新的 API 格式,并且不再调用 HTML 解析器。
- 使仓库能够放弃对“传统”HTML 格式的支持。与客户端类似,预计大多数仓库将长期(或永远)继续支持 HTML 响应。仓库应该能够选择仅支持新格式。
- 保持对现有仅 HTML 客户端的完全支持。我们不得破坏作为严格 PEP 503 API 访问 API 的现有客户端。唯一的例外是,如果仓库本身已选择不再支持 HTML 格式。
- 最小化额外的 HTTP 请求。使用此 API 不得大幅增加安装程序为了运行而必须执行的 HTTP 请求数量。理想情况下,它将需要 0 个额外请求,但如果需要,它可能需要一两个额外请求(总数,而不是每个依赖项)。
- 最小化额外的唯一响应。由于 PyPI 等大型仓库缓存响应的性质,本 PEP 不应引入显著或组合式大量的额外唯一响应,这些响应可能是仓库生成的。
- 支持 TUF。本 PEP 必须能够在 TUF 支持的范围内运行(PEP 458),并且必须能够使用它进行保护。
- 客户端只需标准库或少量外部依赖项。解析 API 响应理想情况下只需标准库,但需要一个小型纯 Python 依赖项也是可以接受的。
规范
为了仅使用标准库实现响应解析,本 PEP 指定所有响应(除了文件本身以及来自 PEP 503 的 HTML 响应)都应使用 JSON 序列化。
为了实现零配置发现并最大程度地减少额外的 HTTP 请求,本 PEP 扩展了 PEP 503,使得所有 API 端点(除了文件本身)都将利用 HTTP 内容协商,允许客户端和服务器选择正确的序列化格式来提供服务,即 HTML 或 JSON。
版本控制
版本控制将遵循 PEP 629 格式(Major.Minor
),其中已将现有 HTML 响应定义为 1.0
。由于本 PEP 未引入 API 的新功能,而是描述了现有功能的另一种序列化格式,因此本 PEP 不更改现有的 1.0
版本,而只是描述如何将其序列化为 JSON。
与 PEP 629 类似,如果对新格式的任何更改导致现有客户端无法有效理解该格式,则主要版本号必须递增。
同样,如果格式中添加或删除了功能,但现有客户端仍应能够有效理解该格式,则次要版本必须递增。
不会导致现有客户端无法有效理解格式且不代表添加或删除功能的更改可能发生,而无需更改版本号。
这有意含糊不清,因为本 PEP 认为最好留待未来的 PEP 来进行任何 API 更改时进行调查并决定该更改是否应增加主要或次要版本。
API 的未来版本可能会添加只能在该版本可用序列化子集中表示的内容。所有序列化版本号,在主要版本内,应保持同步,但功能序列化为每种格式的具体方式可能有所不同,包括该功能是否完全存在。
本 PEP 的意图是,API 应该被视为返回数据的 URL 端点,其解释由该数据的版本定义,然后序列化为目标序列化格式。
JSON 序列化
来自 PEP 503 的 URL 结构仍然适用,因为本 PEP 仅为现有 API 添加了一种额外的序列化格式。
以下限制适用于本 PEP 中描述的所有 JSON 序列化响应
- 所有 JSON 响应将 *始终* 是 JSON 对象,而不是数组或其他类型。
- 虽然 JSON 本身不支持 URL 类型,但此 API 中表示 URL 的任何值都可以是绝对或相对的,只要它们指向正确的位置。如果是相对的,则它们相对于当前 URL,就像它是 HTML 一样。
- 可以在 API 响应中的任何字典对象中添加额外的键,并且客户端必须忽略它们不理解的键。
- 所有 JSON 响应都将有一个
meta
键,其中包含与响应本身相关的信息,而不是响应的内容。 - 所有 JSON 响应都将包含
meta.api-version
键,它将是一个字符串,包含 PEP 629Major.Minor
版本号,具有与 PEP 629 中定义的相同的失败/警告语义。 - 所有 PEP 503 中不特定于 HTML 的要求仍然适用。
项目列表
本 PEP 的根 URL /
(表示基本 URL)将是一个 JSON 编码的字典,其中包含两个键
projects
:一个数组,其中每个条目都是一个字典,带有一个键name
,表示项目名称的字符串。meta
:如前所述的通用响应元数据。
举例来说:
{
"meta": {
"api-version": "1.0"
},
"projects": [
{"name": "Frob"},
{"name": "spamspamspam"}
]
}
注意
name
字段与 PEP 503 中的名称字段相同,它没有指定是未规范化的显示名称还是规范化的名称。实际上,这些 PEP 的不同实现在此处选择不同,因此依赖于它是未规范化还是规范化取决于相关仓库的实现细节。
注意
虽然 projects
键是一个数组,因此需要以某种顺序排列,但 PEP 503 和本 PEP 都没有要求任何特定的排序,也没有要求排序在不同请求之间保持一致。从概念上讲,最好将其视为一个集合,但 JSON 和 HTML 都缺少集合的功能。
项目详情
此 URL 的格式为 /<project>/
,其中 <project>
被该项目的 PEP 503 规范化名称替换,因此名为“Silly_Walk”的项目将具有类似 /silly-walk/
的 URL。
此 URL 必须响应一个 JSON 编码的字典,该字典具有三个键
name
:项目的规范化名称。files
:一个字典列表,每个字典代表一个单独的文件。meta
:如前所述的通用响应元数据。
每个单独的文件字典都具有以下键
filename
:所代表的文件名。url
:可以从中获取文件的 URL。hashes
:一个字典,将哈希名称映射到文件的十六进制编码摘要。可以包含多个哈希,由客户端决定如何处理多个哈希(它可以验证所有哈希或其子集,或完全不验证)。这些哈希名称应始终规范化为小写。hashes
字典必须存在,即使文件没有可用的哈希,但是强烈建议始终至少包含一个安全的、保证可用的哈希。默认情况下,可以通过 hashlib(特别是任何可以传递给
hashlib.new()
且不需要额外参数的算法)获得的任何哈希算法都可以用作哈希字典的键。应始终包含hashlib.algorithms_guaranteed
中的至少一个安全算法。在本 PEP 发布时,特别推荐使用sha256
。requires-python
:一个可选键,用于公开 PEP 345 中指定的 *Requires-Python* 元数据字段。如果存在此字段,则安装工具在安装到不满足要求的 Python 版本时应忽略下载。与 PEP 503 中的
data-requires-python
不同,requires-python
键除了 JSON 自然进行的任何转义之外,不需要任何特殊转义。dist-info-metadata
:一个可选键,表示此文件的元数据可用,通过与 PEP 658 中指定的位置相同({file_url}.metadata
)。如果存在,则必须是一个布尔值,表示文件是否有相关的元数据文件,或者是一个将哈希名称映射到元数据哈希的十六进制编码摘要的字典。当这是一个哈希字典而不是布尔值时,则与
hashes
键相同的所有要求和建议也适用于此键。如果此键缺失,则元数据文件可能存在也可能不存在。如果键值为真值,则元数据文件存在,如果为假值,则不存在。
建议服务器尽可能提供元数据文件的哈希值。
gpg-sig
:一个可选键,作为布尔值指示文件是否具有关联的 GPG 签名。签名文件的 URL 遵循 PEP 503 中指定的内容({file_url}.asc
)。如果此键不存在,则签名可能存在也可能不存在。yanked
:一个可选键,可以是布尔值,表示文件是否已被撤回,也可以是非空但任意的字符串,表示文件已被撤回并附有特定原因。如果yanked
键存在且为真值,则应解释为指示url
字段指向的文件已根据 PEP 592 被“撤回”。
举例来说:
{
"meta": {
"api-version": "1.0"
},
"name": "holygrail",
"files": [
{
"filename": "holygrail-1.0.tar.gz",
"url": "https://example.com/files/holygrail-1.0.tar.gz",
"hashes": {"sha256": "...", "blake2b": "..."},
"requires-python": ">=3.7",
"yanked": "Had a vulnerability"
},
{
"filename": "holygrail-1.0-py3-none-any.whl",
"url": "https://example.com/files/holygrail-1.0-py3-none-any.whl",
"hashes": {"sha256": "...", "blake2b": "..."},
"requires-python": ">=3.7",
"dist-info-metadata": true
}
]
}
注意
虽然 files
键是一个数组,因此需要以某种顺序排列,但 PEP 503 和本 PEP 都没有要求任何特定的排序,也没有要求排序在不同请求之间保持一致。从概念上讲,最好将其视为一个集合,但 JSON 和 HTML 都缺少集合的功能。
内容类型
本 PEP 提议,所有来自简单 API 的响应都应具有标准内容类型,该内容类型描述响应是什么(一个简单 API 响应)、它代表的 API 版本以及所使用的序列化格式。
这种内容类型的结构将是
application/vnd.pypi.simple.$version+format
由于只有主要版本才应干扰客户端试图理解这些 API 响应,因此内容类型中只包含主要版本,并以 v
为前缀,以明确表示它是版本号。
这意味着对于现有的 1.0 API,内容类型将是
- JSON:
application/vnd.pypi.simple.v1+json
- HTML:
application/vnd.pypi.simple.v1+html
除了以上内容,还支持一个名为 latest
的特殊“元”版本,其目的是允许客户端请求绝对最新版本,而无需事先知道该版本是什么。但是,建议客户端明确说明他们支持哪些版本。
为了支持期望现有 PEP 503 API 响应使用 text/html
内容类型的现有客户端,本 PEP 进一步定义 text/html
作为 application/vnd.pypi.simple.v1+html
内容类型的别名。
版本 + 格式选择
现在有多种可能的序列化方式,我们需要一种机制来允许客户端指示他们能够理解哪些序列化格式。此外,如果可以添加任何可能的 API 新主要版本而不会干扰期望先前 API 版本的现有客户端,那将是有益的。
为了实现这一点,本 PEP 将 HTTP 的服务器驱动内容协商标准化。
虽然本 PEP 不会完全描述服务器驱动内容协商的全部内容,但其流程大致如下
- 客户端发出一个 HTTP 请求,其中包含一个
Accept
头部,列出了他们能够理解的所有版本+格式内容类型。 - 服务器检查该头部,选择其中列出的内容类型之一,然后使用该内容类型返回响应(将缺少
Accept
头部视为Accept: */*
)。 - 如果服务器不支持
Accept
头部中的任何内容类型,则它们可以选择 3 种不同的响应方式- 选择一个不同于客户端请求的默认内容类型并返回带有该内容类型的响应。
- 返回 HTTP
406 不可接受
响应,表示所请求的内容类型均不可用,并且服务器无法或不愿选择默认内容类型进行响应。 - 返回 HTTP
300 多重选择
响应,其中包含所有可能选择的响应列表。
- 客户端解释响应,处理服务器可能响应的不同类型的响应。
本 PEP 未指定服务器在处理无法返回的内容类型时所做的选择,并且客户端应准备好以最适合该客户端的方式处理所有可能的响应。
然而,由于对于如何解释 300 多重选择
响应没有标准格式,本 PEP 强烈不鼓励服务器利用该选项,因为客户端将无法理解并选择不同的内容类型进行请求。此外,客户端可能无法理解不同的内容类型,因此最好的情况是此响应可能只被视为与 406 不可接受
错误相同。
本 PEP 确实要求,如果正在使用元版本 latest
,服务器必须使用响应中包含的实际版本的内容类型进行响应(即,请求 Accept: application/vnd.pypi.simple.latest+json
返回 v1.x
响应时,其 Content-Type
应为 application/vnd.pypi.simple.v1+json
)。
Accept
头部是一个逗号分隔的列表,其中包含客户端理解并能够处理的内容类型。它支持三种不同的格式来请求每种内容类型
$type/$subtype
$type/*
*/*
对于选择版本+格式,最有用的就是 $type/$subtype
,因为这是实际指定所需版本和格式的唯一方法。
Accept
头部中列出的内容类型的顺序没有特定含义,服务器应将它们都视为同样有效的响应。如果客户端希望指定他们偏好某个内容类型而不是另一个,他们可以使用 Accept
头部中的质量值语法。
这允许客户端通过附加 ;q=
后跟一个介于 0
和 1
之间(含)的值,最多 3 位小数,来指定 Accept
头部中特定条目的优先级。在解释此值时,质量较高的条目优先于质量较低的条目,任何未指定质量的条目将默认为 1
。
然而,客户端应记住,服务器可以自由选择他们要求的**任何**内容类型,无论其请求的优先级如何,甚至可能返回他们**未**要求的内容类型。
为了帮助客户端确定从 API 请求收到的响应内容类型,本 PEP 要求服务器始终包含 Content-Type
头部,指示响应的内容类型。这在技术上是一个向后不兼容的更改,但实际上 pip 一直在强制执行此要求,因此实际中断的风险很低。
客户端操作的示例如下所示
import email.message
import requests
def parse_content_type(header: str) -> str:
m = email.message.Message()
m["content-type"] = header
return m.get_content_type()
# Construct our list of acceptable content types, we want to prefer
# that we get a v1 response serialized using JSON, however we also
# can support a v1 response serialized using HTML. For compatibility
# we also request text/html, but we prefer it least of all since we
# don't know if it's actually a Simple API response, or just some
# random HTML page that we've gotten due to a misconfiguration.
CONTENT_TYPES = [
"application/vnd.pypi.simple.v1+json",
"application/vnd.pypi.simple.v1+html;q=0.2",
"text/html;q=0.01", # For legacy compatibility
]
ACCEPT = ", ".join(CONTENT_TYPES)
# Actually make our request to the API, requesting all of the content
# types that we find acceptable, and letting the server select one of
# them out of the list.
resp = requests.get("https://pypi.ac.cn/simple/", headers={"Accept": ACCEPT})
# If the server does not support any of the content types you requested,
# AND it has chosen to return a HTTP 406 error instead of a default
# response then this will raise an exception for the 406 error.
resp.raise_for_status()
# Determine what kind of response we've gotten to ensure that it is one
# that we can support, and if it is, dispatch to a function that will
# understand how to interpret that particular version+serialization. If
# we don't understand the content type we've gotten, then we'll raise
# an exception.
content_type = parse_content_type(resp.headers.get("content-type", ""))
match content_type:
case "application/vnd.pypi.simple.v1+json":
handle_v1_json(resp)
case "application/vnd.pypi.simple.v1+html" | "text/html":
handle_v1_html(resp)
case _:
raise Exception(f"Unknown content type: {content_type}")
如果客户端只想支持 HTML 或只支持 JSON,那么他们只需从 Accept
头部中移除他们不想要的内容类型,并将接收这些内容类型视为错误。
替代协商机制
虽然使用 HTTP 的内容协商被认为是客户端和服务器协调以确保客户端获得能够理解的 HTTP 响应的标准方式,但在某些情况下,该机制可能不足。对于这些情况,本 PEP 提供了可以可选使用的替代协商机制。
URL 参数
实现简单 API 的服务器可以选择支持一个名为 format
的 URL 参数,以允许客户端请求特定版本的 URL。
format
参数的值应该是一个有效的内容类型。不支持传递多个内容类型、通配符、质量值等。
支持此参数是可选的,客户端不应依赖它与 API 交互。此协商机制旨在允许更方便的人类在浏览器中探索 API,或允许文档或笔记链接到特定版本+格式。
不支持此参数的服务器在存在此参数时可能会选择返回错误,或者它们可能只是忽略其存在。
当服务器确实实现了此参数时,它应优先于客户端 Accept
头部中的任何值,并且如果服务器不支持所请求的格式,它可能会选择回退到 Accept
头部,或者选择标准服务器驱动内容协商通常具有的任何错误条件(例如 406 不可用
、303 多重选择
,或选择默认类型返回)。
端点配置
这个选项从技术上来说根本不是一个特殊选项,它只是使用内容协商并允许服务器选择哪些可用内容类型作为其默认设置的自然结果。
如果服务器不愿或无法实现服务器驱动内容协商,而宁愿要求用户显式配置其客户端来选择他们想要的版本,那么这是一种受支持的配置。
为此,服务器应为每个希望支持的版本+格式创建多个端点(例如,/simple/v1+html/
和/或 /simple/v1+json/
)。在该端点下,它们可以托管其仓库的副本,该副本仅支持一种(或子集)内容类型。当客户端使用 Accept
头部发出请求时,服务器可以忽略它并返回与该端点对应的内容类型。
对于希望要求特定配置的客户端,它们可以跟踪特定仓库 URL 配置的版本+格式,并在向该服务器发出请求时,发出一个Accept
头部,该头部只包含正确的内容类型。
TUF 支持 - PEP 458
PEP 458 要求所有 API 响应都是可哈希的,并且可以通过相对于仓库根目录的路径唯一标识。对于简单 API 仓库,目标路径是 API 的根目录(例如 PyPI 上的 /simple/
)。这在不直接使用标准 HTTP 客户端而使用 TUF 客户端访问 API 时带来了挑战,因为 TUF 客户端无法处理目标可能具有多个不同表示形式,所有这些表示形式都具有不同哈希值的事实。
PEP 458 没有指定简单 API 的目标路径应该是什么,但 TUF 要求目标路径是“文件式”的,换句话说,像 simple/PROJECT/
这样的路径是不可接受的,因为它从技术上指向一个目录。
值得庆幸的是,目标路径不必实际匹配从简单 API 获取的 URL,它只是一个获取代码知道如何转换为实际需要获取的 URL 的标记。同样的事情也适用于实际 HTTP 请求的其他方面,例如 Accept
头部。
最终,如何将目录映射到文件名超出了本 PEP 的范围(但它将在 PEP 458 的范围内),本 PEP 推迟对如何在 PEP 458 元数据中精确表示这一点做出决定。
然而,目前针对 pip 尝试实现 PEP 458 的 WIP 分支似乎正在使用 simple/PROJECT/index.html
这样的目标路径。这可以修改为包含 API 版本和序列化格式,使用类似 simple/PROJECT/vnd.pypi.simple.vN.FORMAT
的内容。因此,v1 HTML 格式将是 simple/PROJECT/vnd.pypi.simple.v1.html
,而 v1 JSON 格式将是 simple/PROJECT/vnd.pypi.simple.v1.json
。
在这种情况下,由于 text/html
是 application/vnd.pypi.simple.v1+html
的别名,当通过 TUF 交互时,它最有可能有意义地规范化为更明确的名称。
同样,latest
元版本不应包含在目标中,只应支持显式声明的版本。
建议
本节是非规范性的,代表了 PEP 作者认为对于实现本 PEP 的最佳默认实现决策,但它并不代表任何匹配这些决策的要求。
这些决策是为了最大限度地提高可以转移到最新版本 API 的请求数量,同时保持最大的兼容性。此外,它们还试图让使用 API 提供护栏,以促使客户端做出最佳选择。
建议服务器
- 只要合理可行,或者至少只要他们正在接收非琐碎的使用 HTML 响应的流量,就应支持本 PEP 中描述的所有 3 种内容类型,并使用服务器驱动的内容协商。
- 当遇到不包含任何它知道如何处理的内容类型的
Accept
头部时,服务器不应返回300 多重选择
响应,而应返回406 不可接受
响应。- 但是,如果选择使用端点配置,则应优先返回该端点预期内容类型的
200 OK
响应。
- 但是,如果选择使用端点配置,则应优先返回该端点预期内容类型的
- 在选择可接受的版本时,服务器应选择客户端支持的最高版本,并采用最具表达性/功能性的序列化格式,同时考虑客户端请求的特异性以及它们表达的任何质量优先级值,并且应仅将
text/html
内容类型作为最后的手段。
建议客户端
- 只要合理可行,就应支持本 PEP 中描述的所有 3 种内容类型,并使用服务器驱动的内容协商。
- 在构建
Accept
头部时,包含您支持的所有内容类型。除非您有希望服务器考虑的特定于实现的理由(例如,如果您正在使用标准库 HTML 解析器,并且担心在某些边缘情况下可能无法解析某些类型的 HTML 响应),否则您通常不应为您的内容类型包含质量优先级值。
此建议的一个例外是,建议您应在旧版
text/html
内容类型上包含;q=0.01
值,除非这是您请求的唯一内容类型。 - 在正常操作期间,明确选择他们正在寻找的版本,而不是使用
latest
元版本。 - 检查响应的
Content-Type
并确保它与您预期的内容匹配。
常见问题
这是否意味着 PyPI 计划放弃对 HTML/PEP 503 的支持?
不,PyPI 目前没有计划放弃对 PEP 503 或 HTML 响应的支持。
尽管本 PEP 确实赋予了仓库这样做的灵活性,但这主要是为了确保像使用端点配置机制这样的东西能够正常工作,并确保客户端不会做出任何可能在未来某个时候阻碍优雅地放弃 HTML 支持的假设。
现有的 HTML 响应几乎没有给 PyPI 带来维护负担,也没有迫切需要删除它们。删除它们唯一真正的好处是减少 CDN 中缓存的项目数量。
如果将来 PyPI *确实*希望放弃对它们的支持,那么这样做几乎肯定会成为 PEP 的主题,或者至少是一个公开的、开放的讨论,并且将由显示对最终用户任何影响的指标提供信息。
为什么是 JSON 而不是 X 格式?
JSON 解析器在大多数(如果不是所有)语言中都广泛可用。Python 标准库中也提供 JSON 解析器。它不是完美的格式,但足够好用。
为什么不添加 X 功能?
本 PEP 的总体目标是很少更改或添加。我们将主要侧重于将 HTML 响应中包含的现有信息转换为合理的 JSON 表示。这将包括打包工具所需的 PEP 658 元数据。
本 PEP 中唯一真正新增的功能是能够为一个文件拥有多个哈希。这样做是因为当前的机制仅限于单个哈希,这在过去迁移哈希(md5 到 sha256)时造成了痛苦,而将哈希设为字典并允许多个哈希的成本相当低。
API 的设计通常允许通过添加新键进行进一步扩展,因此如果安装程序可能需要一些新的数据片段,未来的 PEP 可以轻松地提供这些数据。
为什么 URL 中已包含文件名,还要包含文件名?
我们可以通过删除 filename
键并期望客户端从 URL 中提取该信息来减小响应的大小。
目前本 PEP 选择不这样做,主要是因为 PEP 503 明确要求文件名可通过链接的锚点标签获得,尽管这主要是因为必须存在一些东西。目前尚不清楚实际的仓库是否总是将文件名作为 URL 的最后一部分,或者它们是否依赖于锚点标签中的文件名。
它还使响应对人类来说更易读,因为您会得到一个简洁的唯一标识符。
如果我们有充分的信心要求文件名存在于 URL 中,那么我们可以删除此数据并减小 JSON 响应的大小。
为什么不从文件名中分离出其他信息?
目前,客户端需要从文件名中解析出一些信息,例如项目名称、版本、ABI 标签等。我们可以将这些信息分解出来,并将它们作为键添加到文件对象中。
本 PEP 选择不这样做,因为这样做会增加 API 响应的大小,而且大多数客户端无论 API 如何,都将需要从文件名中解析出这些信息。因此,将该功能保留在客户端内部是合理的。
为什么使用内容协商而不是多个 URL?
另一种合理的实现方式是复制 API 路由,并在 URL 本身中包含一些 JSON 标记。例如,将 URL 设置为 /simple/foo.json
、/simple/_index.json
等。
这使得一些事情变得更简单,例如 TUF 集成和仓库的完全静态服务(因为 .json
文件可以简单地写入)。
然而,这有两个相当大的问题
- 我们当前的 URL 结构依赖于一个表示“根”的 URL,即
/
,用于提供项目列表。如果我们要为 JSON 和 HTML 提供单独的 URL,我们需要想出一种方法来拥有两个根 URL。像
/
是 HTML,/_index.json
是 JSON,因为_index
不是有效的项目名称,这可能行得通。但是/
是 HTML 如果仓库想要删除对 HTML 的支持,效果就不太好。另一个选择是将所有现有的 HTML URL 移到一个命名空间下,同时为 JSON 创建一个新的命名空间。由于
/<project>/
已定义,我们必须使这些命名空间不是有效的项目名称,因此像/_html/
和/_json/
这样的名称可能会奏效,然后只需将非命名空间的 URL 重定向到该仓库的“默认”值(可能是 HTML,除非他们已禁用 HTML,那么就是 JSON)。 - 使用单独的 URL,无法很好地支持零配置发现仓库支持 JSON URL,而无需发出额外的 HTTP 请求来确定 JSON URL 是否存在。
最幼稚的实现方式是请求 JSON URL,然后对每个请求都回退到 HTML URL,但这会严重影响性能,并违反最小化额外 HTTP 请求的目标。
最可能的实现方式是创建一个某种仓库级别的配置文件,以某种方式指示支持什么。我们将面临与上述相同的命名空间问题,并采取相同的解决方案,像
/_config.json
之类的文件可以保存这些数据,客户端可以首先向其发出 HTTP 请求,如果存在则将其拉取并解析以了解此特定仓库的功能。 Accept
的使用也允许我们将版本控制添加到此字段中
总而言之,本 PEP 认为这三个问题相结合,使得使用单独的 API 路由不如依赖内容协商来选择数据最理想的表示形式。
这是否意味着不再支持静态服务器?
简而言之,不,静态服务器仍然(几乎)完全受本 PEP 支持。
它们如何得到支持的具体细节将取决于相关的静态服务器。例如
- S3: S3 完全支持自定义内容类型,但它不支持任何形式的内容协商。为了在 S3 上托管服务器,您必须使用“端点配置”样式的协商,并且用户必须明确配置其客户端。
- GitHub Pages: GitHub Pages 不支持自定义内容类型,因此 S3 解决方案目前不可行,这意味着只有
text/html
仓库才能正常运行。 - Apache: Apache 完全支持服务器驱动的内容协商,只需配置即可将自定义内容类型映射到特定的扩展名。
为什么不添加 application/json
别名,就像 text/html
一样?
本 PEP 认为,客户端和服务器最好都明确 API 响应的类型,而像 application/json
这样的内容类型恰恰相反。
text/html
别名的存在主要是一种折衷方案,以确保 API 的现有消费者继续像以前一样运行。对于使用 application/json
内容类型的现有客户端,没有这样的期望。
此外,application/json
中没有版本控制,这意味着如果将来出现简单 API 的 2.x
版本,我们将被迫做出决定。是 application/json
应保持向后兼容并继续作为 application/vnd.pypi.simple.v1+json
的别名,还是应更新为 application/vnd.pypi.simple.v2+json
的别名?
这个问题对于 text/html
不存在,因为假设 HTML 将保持为传统格式,并且可能不会获得任何新功能,更不用说需要破坏兼容性的功能。因此,将其作为 application/vnd.pypi.simple.v1+html
的别名实际上与将其作为 application/vnd.pypi.simple.latest+html
的别名相同,因为 1.x
可能将是唯一存在的 HTML 版本。
添加 application/json
内容类型的最大好处是,有些事物不允许您拥有自定义内容类型,并要求您选择其预设内容类型之一。这方面的主要例子是 GitHub Pages,本 PEP 中缺少 application/json
支持意味着静态仓库将无法再托管在 GitHub Pages 上,除非 GitHub 添加 application/vnd.pypi.simple.v1+json
内容类型。
本 PEP 认为,目前添加该内容类型别名的好处不足以弥补其弊端,并且其包含可能会成为一个“自挖陷阱”,让毫无戒心的用户不小心使用。特别是考虑到我们将来总是可以添加它,但删除它则要困难得多。
为什么添加 application/vnd.pypi.simple.v1+html
?
PEP 预计 API 的 HTML 版本将成为旧版,因此它可能采取的一个选项是不添加 application/vnd.pypi.simple.v1+html
内容类型,而只使用 text/html
。
本 PEP 决定添加新的内容类型总体来说更好,因为它使旧版格式更具自描述性,并使它们彼此之间更加一致。总的来说,我认为如果 +html
版本不存在会更令人困惑。
为什么是 v1.0 而不是 v1.1 或 v2.0?
本 PEP 仍然完全向后兼容能够读取现有 v1.0 API 的客户端,在进行这些更改后仍然可以继续读取 API。在 PEP 629 中,主要版本升级的条件是
增加主要版本用于表示向后不兼容的更改,使得现有客户端不再能够有效使用 API。
本 PEP 中的更改不符合该标准,没有任何更改导致现有客户端不再能够有效使用 API。
这意味着我们仍应在 v1.x 版本系列内。
关于我们应该是 v1.1 还是 v1.0 的问题更有趣,有几种看待方式
- 我们已经向 API 暴露了新功能(项目页面上的项目名称、多个哈希),这表明我们应该增加次要版本。
- 新功能完全存在于 JSON 序列化中,这意味着当前请求 HTML 1.0 页面的客户端无论如何都不会看到任何新功能,因此对它们来说,它实际上仍然是 v1.0。
- 目前还没有主要的客户端实现对 PEP 629 的支持,这意味着次要版本编号在这一点上很大程度上是学术性的,因为它旨在让客户端向最终用户提供反馈。
上述第二点和第三点最终使第一点变得毫无意义,因此,将所有内容都称为 v1.0,并在未来更严格地更新版本更有意义。
附录 1:要涵盖的用例调查
这是通过 pip
、PyPI
和 bandersnatch
维护者之间的讨论完成的,他们是新 API 的前两个潜在用户。以下是他们目前使用 Simple + JSON API 的方式或他们目前的计划
pip
:- 特定版本的所有文件列表
- 每个单独工件的元数据
- 是否已被撤回?(
data-yanked
) - Python 版本要求是什么?(
data-python-requires
) - 此文件的哈希值是多少?(目前是 URL 中的哈希值)
- 完整元数据 (
data-dist-info-metadata
) - [额外] 如果可用,声明的依赖项是什么(字符串列表,如果不可用则为 null)?
- 是否已被撤回?(
bandersnatch
- 目前只使用旧版 JSON API + XMLRPC- 生成简单 HTML 而不是从 PyPI 复制
- 也许这会随着新 API 的出现而改变,我们逐字从 PyPI 拉取这些 API 资产
- 特定版本的所有文件列表。
- 计算发布文件的 URL 以下载
- 每个独立工件的元数据。
- 将 JSON 写入镜像存储(磁盘/S3)
- 使用的所需元数据(通过Package 类)
元数据["info"]
元数据["last_serial"]
元数据["releases"]
- 摘要
- URL
- 使用的所需元数据(通过Package 类)
- 将 JSON 写入镜像存储(磁盘/S3)
- XML-RPC 调用(我们很乐意弃用 - 但我们认为不应放入简单 API 中)
- [额外] 获取序列 X 以来的包(或所有包)
- XML-RPC 调用:
changelog_since_serial
- XML-RPC 调用:
- [额外] 获取所有带序列的包
- XML-RPC 调用:
list_packages_with_serial
- XML-RPC 调用:
- [额外] 获取序列 X 以来的包(或所有包)
- 生成简单 HTML 而不是从 PyPI 复制
附录 2:粗略的底层数据模型
这些并非旨在完美匹配服务器、客户端或线缆格式。相反,这些是概念模型,通过代码使其更明确地说明了随着 PEP 503、PEP 592、PEP 629、PEP 658 以及现在的本 PEP PEP 691 的演变,仓库 API 所依据的抽象模型。
这些模型的现有 HTML 和新 JSON 序列化代表了这些底层概念模型如何映射到实际的线缆格式。
服务器或客户端如何选择建模此数据超出了本 PEP 的范围。
@dataclass
class Meta:
api_version: Literal["1.0"]
@dataclass
class Project:
# Normalized or Non-Normalized Name
name: str
# Computed in JSON, Included in HTML
url: str | None
@dataclass
class File:
filename: str
url: str
# Limited to a len() of 1 in HTML
hashes: dict[str, str]
gpg_sig: bool | None
requires_python: str | None
@dataclass
class PEP592File(File):
yanked: bool | str
@dataclass
class PEP658File(PEP592File):
# Limited to a len() of 1 in HTML
dist_info_metadata: bool | dict[str, str]
# Simple Index page (/simple/)
@dataclass
class PEP503_Index:
projects: set[Project]
@dataclass
class PEP629_Index(PEP503_Index):
meta: Meta
@dataclass
class Index(PEP629_Index):
pass
# Simple Detail page (/simple/$PROJECT/)
@dataclass
class PEP503_Detail:
files: set[File]
@dataclass
class PEP592_Detail(PEP503_Detail):
files: set[PEP592File]
@dataclass
class PEP629_Detail(PEP592_Detail):
meta: Meta
@dataclass
class PEP658_Detail(PEP629_Detail):
files: set[PEP658File]
@dataclass
class PEP691_Detail(PEP658_Detail):
name: str # Normalized Name
@dataclass
class Detail(PEP691_Detail):
pass
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0691.rst