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,工具必须引入大型依赖项,或者它们必须依赖于标准库的
html.parser
库,该库更轻量级,但可能无法完全支持 HTML5。 - HTML5 主要被设计为一种标记语言,用于呈现供人类阅读的文档。我们使用它的原因主要是出于历史和偶然的原因,如果他们从头开始设计 API,不太可能有人会设计一个依赖于它的 API。
使用为人类阅读而设计的标记格式的主要问题是,没有很好的方法可以在 HTML 中真正编码数据。我们通过限制我们在此 API 中放置的数据并发挥创意来解决这个问题,例如,散列被嵌入为 URL 片段,在 PEP 592 中添加了
data-yanked
属性。
PEP 503 在很大程度上是试图标准化已经使用的东西,因此它没有提出对 API 的任何重大更改。
在过去的几年里,我们经常讨论过一个“API V2”,它将重新构思 PyPI 的整个 API。但是,由于时间限制,除了人们认为这样做很好之外,这项工作并没有获得太多(如果有的话)的吸引力。
本 PEP 尝试采用不同的路线。它并没有从根本上改变整体 API 结构,而是指定了现有 PEP 503 响应中包含的现有数据的新的序列化格式,这种格式更容易被软件解析,而不是使用以人为中心的文档格式。
目标
- **启用零配置发现。**简单 API 的客户端**必须**能够优雅地确定目标存储库是否支持此 PEP,而无需依赖任何形式的带外通信(配置、先验知识等)。但是,各个客户端**可以选择**要求配置以启用此 API 的使用。
- **使客户端能够放弃对“遗留”HTML 解析的支持。**虽然预计大多数客户端会在一段时间内(如果不是永远的话)继续支持仅 HTML 的存储库,但客户端应该可以选择仅支持新的 API 格式,并且不再调用 HTML 解析器。
- **使存储库能够放弃对“遗留”HTML 格式的支持。**与客户端类似,预计大多数存储库将在很长一段时间内(或永远)继续支持 HTML 响应。存储库应该可以选择仅支持新的格式。
- **保持对现有仅 HTML 客户端的完全支持。**我们**必须**不破坏作为严格的 PEP 503 API 访问 API 的现有客户端。唯一的例外是,如果存储库本身选择不再支持 HTML 格式。
- **最少的额外 HTTP 请求。**使用此 API**必须**不会大幅增加安装程序必须执行的 HTTP 请求数量才能正常运行。理想情况下,它不需要额外的请求,但如果需要,它可能需要一个或两个额外的请求(总计,而不是每个依赖项)。
- **最少的额外唯一响应。**由于像 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 认为最好留给将来对 API 进行任何更改的 PEP 来调查并决定该更改是否应该递增主版本或次版本。
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 中定义的相同失败/警告语义。 - 所有不特定于 HTML 的 PEP 503 要求仍然适用。
项目列表
此 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
:一个**可选**键,用于公开Requires-Python 元数据字段,该字段在 PEP 345 中指定。如果存在此键,则安装程序工具**应该**在安装到不满足要求的 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 建议 Simple API 的所有响应都将具有一个标准内容类型,该类型描述响应是什么(Simple 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 不会完全描述服务器驱动的内容协商的全部内容,但流程大致如下
- 客户端发出包含
Accept
标头的 HTTP 请求,该标头列出他们能够理解的所有版本+格式内容类型。 - 服务器检查该标头,选择列出的内容类型之一,然后使用该内容类型返回响应(将缺少
Accept
标头视为Accept: */*
)。 - 如果服务器不支持
Accept
标头中的任何内容类型,则可以选择 3 种不同的响应方式- 选择客户端请求之外的默认内容类型并使用该类型返回响应。
- 返回 HTTP
406 Not Acceptable
响应以指示没有可用的请求内容类型,并且服务器无法或不愿选择默认内容类型进行响应。 - 返回 HTTP
300 Multiple Choices
响应,其中包含可能已选择的响应列表。
- 客户端解释响应,处理服务器可能已响应的不同类型的响应。
此 PEP 没有指定服务器在处理无法返回的内容类型时做出的选择,并且客户端**应该**准备好以对该客户端最有意义的方式处理所有可能的响应。
然而,由于没有关于如何解释300 Multiple Choices
响应的标准格式,此PEP强烈建议服务器不要使用该选项,因为客户端将无法理解并选择不同的内容类型来请求。此外,客户端不太可能理解其他内容类型,因此,此响应充其量可能只会像406 Not Acceptable
错误一样处理。
此PEP**确实**要求,如果正在使用元版本latest
,则服务器**必须**以响应中包含的实际版本的content-type进行响应(例如,一个返回v1.x
响应的Accept: application/vnd.pypi.simple.latest+json
请求应该具有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
参数的值应该是有效的content-type之一。传递多个内容类型、通配符、质量值等**不支持**。
支持此参数是可选的,客户端**不应该**依赖它来与API交互。此协商机制旨在允许更轻松地以人为基础在浏览器中浏览API,或者允许文档或注释链接到特定版本+格式。
不支持此参数的服务器可以选择在出现此参数时返回错误,或者可以选择忽略其存在。
当服务器确实实现了此参数时,它**应该**优先于客户端Accept
头中的任何值,如果服务器不支持请求的格式,则可以选择回退到Accept
头,或者选择标准服务器驱动的内容协商通常具有的任何错误条件(例如406 Not Available
、303 Multiple Choices
或选择要返回的默认类型)。
端点配置
从技术上讲,此选项根本不是一个特殊选项,它只是使用内容协商并允许服务器选择其可用内容类型中的哪一个是其默认值的自然结果。
如果服务器不愿意或无法实现服务器驱动的内容协商,而是希望要求用户显式配置其客户端以选择他们想要的版本,那么这是一种受支持的配置。
为了启用此功能,服务器应该为他们希望支持的每个版本+格式创建多个端点(例如,/simple/v1+html/
和/或/simple/v1+json/
)。在该端点下,他们可以托管其存储库的副本,该副本仅支持一种(或子集)内容类型。当客户端使用Accept
头发出请求时,服务器可以忽略它并返回与该端点对应的content-type。
对于希望需要特定配置的客户端,他们可以跟踪特定存储库URL配置的版本+格式,并在向该服务器发出请求时,发出一个Accept
头,该头**仅**包含正确的内容类型。
TUF 支持 - PEP 458
PEP 458要求所有API响应都是可哈希的,并且可以通过相对于存储库根目录的路径唯一标识。对于简单API存储库,目标路径是我们的API根目录(例如,PyPI上的/simple/
)。当使用TUF客户端而不是直接使用标准HTTP客户端访问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
的目标路径。这可以通过使用类似simple/PROJECT/vnd.pypi.simple.vN.FORMAT
的内容来修改,以包含API版本和序列化格式。因此,v1 HTML格式将是simple/PROJECT/vnd.pypi.simple.v1.html
,v1 JSON格式将是simple/PROJECT/vnd.pypi.simple.v1.json
。
在这种情况下,由于在通过TUF交互时,text/html
是application/vnd.pypi.simple.v1+html
的别名,因此规范化为更明确的名称可能最有意义。
同样,元版本latest
不应包含在目标中,仅应支持显式声明的版本。
建议
本节是非规范性的,代表PEP作者认为实现此PEP的最佳默认实现决策,但它**不**代表任何必须匹配这些决策的要求。
这些决策是为了最大化可以迁移到最新API版本的请求数量,同时保持最大的兼容性。此外,他们还试图使使用API提供一些防护措施,这些措施试图推动客户端做出最佳选择。
建议服务器
- 在合理的情况下,或至少在接收使用HTML响应的非微不足道的流量时,支持此PEP中描述的所有三种内容类型,使用服务器驱动的内容协商。
- 当遇到
Accept
头中不包含任何已知内容类型时,服务器永远不应该返回300 Multiple Choice
响应,而应该返回406 Not Acceptable
响应。- 但是,如果选择使用端点配置,则应优先返回该端点预期内容类型的
200 OK
响应。
- 但是,如果选择使用端点配置,则应优先返回该端点预期内容类型的
- 在选择可接受的版本时,服务器应该选择客户端支持的最高版本,以及最具表现力/功能丰富的序列化格式,同时考虑客户端请求的具体情况以及他们表达的任何质量优先级值,并且它应该只将
text/html
内容类型作为最后的手段。
建议客户端
- 在合理的情况下,支持此PEP中描述的所有三种内容类型,使用服务器驱动的内容协商。
- 在构造
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 创建一个新的命名空间。由于
/
已定义,我们需要使这些命名空间成为无效的项目名称,因此类似于 /_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 完全支持服务器驱动的内容协商,只需要将其配置为将自定义内容类型映射到特定扩展名。
为什么不添加一个像 text/html
这样的 application/json
别名?
此 PEP 认为,对于客户端和服务器来说,明确使用 API 响应的类型是最好的,而像 application/json
这样的内容类型与明确性完全相反。
text/html
别名的存在主要是为了确保 API 的现有使用者能够像以前一样继续工作。没有期望现有客户端使用具有 application/json
内容类型的简单 API。
此外,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
和 bandersnarch
维护者之间的讨论完成的,他们是新 API 的前两个潜在用户。这是他们今天使用 Simple + JSON API 的方式,或者他们目前计划使用它的方式
pip
:- 特定版本的全部文件列表
- 每个单独工件的元数据
- 它是否被撤回?(
data-yanked
) - python-requires 是什么?(
data-python-requires
) - 此文件的哈希值是什么?(目前,URL 中的哈希值)
- 完整元数据(
data-dist-info-metadata
) - [额外] 如果可用,声明的依赖项是什么?(字符串列表,如果不可用则为 null)
- 它是否被撤回?(
bandersnatch
- 今天仅使用遗留 JSON API + XMLRPC- 生成 Simple HTML 而不是从 PyPI 复制
- 也许这会随着新 API 而改变,我们会逐字从 PyPI 中提取这些 API 资源
- 特定版本的全部文件列表。
- 用于下载发行版文件的网址
- 每个单独工件的元数据。
- 将 JSON 写入到今天镜像存储(磁盘/S3)
- 使用的必需元数据(通过 Package 类)
metadata["info"]
metadata["last_serial"]
metadata["releases"]
- digests
- URL
- 使用的必需元数据(通过 Package 类)
- 将 JSON 写入到今天镜像存储(磁盘/S3)
- XML-RPC 调用(我们希望弃用 - 但我们认为不应该进入 Simple API)
- [额外] 获取自序列号 X 以来(或全部)的软件包
- XML-RPC 调用:
changelog_since_serial
- XML-RPC 调用:
- [额外] 获取所有带有序列号的软件包
- XML-RPC 调用:
list_packages_with_serial
- XML-RPC 调用:
- [额外] 获取自序列号 X 以来(或全部)的软件包
- 生成 Simple HTML 而不是从 PyPI 复制
附录 2:粗略的基础数据模型
这些并非旨在完美匹配服务器、客户端或线格式。相反,这些是概念模型,将其转换为代码以使其更明确地说明在存储库 API 下的抽象模型,因为它通过 PEP 503、PEP 529、PEP 629、PEP 658 以及现在这个 PEP,PEP 691 演变而来。
然后,现有的 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 PEP529File(File):
yanked: bool | str
@dataclass
class PEP658File(PEP529File):
# 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 PEP529_Detail(PEP503_Detail):
files: set[PEP529File]
@dataclass
class PEP629_Detail(PEP529_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
上次修改时间:2023-09-09 17:39:29 GMT