跳转到主要内容

极其轻量级的命令行界面

项目描述

radicli:极其轻量级的命令行界面

radicli 是一个小的、零依赖的Python包,用于创建命令行界面,基于Python的 argparse 模块。它引入了最小开销,保留了您的原始Python函数,并使用 类型提示 解析CLI上提供的值。它支持所有常见类型,包括像 List[str]LiteralEnum 这样复杂类型,并允许通过自定义转换器注册 自定义类型,以及自定义仅CLI的 错误处理,导出 静态表示 以加快 --help 和错误以及自动生成的 Markdown文档

重要说明: 此包旨在根据我们库的需求提供一个简单的选项。如果您正在寻找更全面的CLI工具包,请查看 typerclickplac

GitHub Actions Current Release Version pypi Version

⏳ 安装

请注意,radicli 目前需要 Python 3.8+

pip install radicli

👩‍💻 使用方法

Radicli 类设置 CLI 并提供用于命令和子命令的装饰器。可以使用 Arg 数据类来描述如何在 CLI 上展示参数。类型和默认值从 Python 函数中读取。通常,您不需要更改 Python 函数的实现方式,就可以将其作为 CLI 命令使用。

# cli.py
from radicli import Radicli, Arg

cli = Radicli()

@cli.command(
    "hello",
    name=Arg(help="Your name"),
    age=Arg("--age", "-a", help="Your age"),
    greet=Arg("--greet", "-G", help="Whether to greet"),
)
def hello(name: str, age: int, greet: bool = False):
    """Description of the function for help text."""
    if greet:
        print(f"Hello {name} ({age})!")

if __name__ == "__main__":
    cli.run()
$ python cli.py hello Alex --age 35 --greet
Hello Alex (35)!

如果一个文件只指定了 单个命令(带或不带子命令),您可以省略命令名称。所以上面的示例脚本也可以这样调用

$ python cli.py Alex --age 35 --greet
Hello Alex (35)!

或者,您还可以使用 Radicli.call

# cli.py
from radicli import Radicli, Arg

def hello(name: str, age: int):
     print(f"Hello {name} ({age})!")

if __name__ == "__main__":
    args = dict(name=Arg(help="Your name"), age=Arg("--age", "-a", help="Your age"))
    command = Command.from_function("hello", args, hello)
    Radicli().call(command)
$ python cli.py Alex --age 35
Hello Alex (35)!

子命令

radicli 支持一层嵌套子命令。父命令可以独立存在,但不必如此。

@cli.subcommand("parent", "child1", name=Arg("--name", help="Your name"))
def parent_child1(name: str):
    ...

@cli.subcommand("parent", "child2", name=Arg("--age", help="Your age"))
def parent_child2(age: int):
    ...
$ python cli.py parent child1 --name Alex
$ python cli.py parent child2 --age 35

与类型一起工作

对于像 strintfloat 这样的内置可调用类型,从 CLI 接收的字符串值会被传递给可调用对象,例如 int(value)。更复杂、嵌套的类型会递归解析。库还提供了一些内置的 自定义类型,用于处理诸如文件路径等事项。

⚠️ 注意,CLI 接口能够合理支持的功能有限,因此建议避免过度复杂的类型。对于 Union 类型,使用联合中的 第一个 类型。期望 Optional 类型未设置,默认为 None。如果提供了值,则使用标记为可选的类型,例如 str 用于 Optional[str]

列表

默认情况下,列表类型通过允许 CLI 参数出现多次来实现。每个元素的值使用为列表成员定义的类型进行解析。

@cli.command("hello", fruits=Arg("--fruits", help="One or more fruits"))
def hello(fruits: List[str]):
    print(fruits)
$ python cli.py hello --fruits apple --fruits banana --fruits cherry
['apple', 'banana', 'cherry']

如果您不喜欢这种语法,也可以在 Arg 定义中添加一个 converter,以不同的方式处理值,例如通过分割逗号分隔的字符串。这允许用户编写 --fruits apple,banana,cherry,同时将列表传递给 Python 函数。

文字和枚举

只能取给定集合中值的参数可以定义为 Literal 类型。任何不在列表中的值都会引发 CLI 错误。

@cli.command("hello", color=Arg("--color", help="Pick a color"))
def hello(color: Literal["red", "blue", "green"]):
    print(color)  # this will be a string

Enum 也受支持,在这种情况下,可以在 CLI 上提供枚举键,函数将接收选定的枚举成员。

class ColorEnum(Enum):
    red = "the color red"
    blue = "the color blue"
    green = "the color green"

@cli.command("hello", color=Arg("--color", help="Pick a color"))
def hello(color: ColorEnum):
    print(color)  # this will be the enum, e.g. ColorEnum.red

使用自定义类型和转换器

radicli 支持定义自定义转换函数来处理单个参数,以及全局处理给定类型的所有实例。转换器接收 CLI 提供的字符串值,并应返回传递给函数的值,与类型一致。它们还可以引发验证错误。

format_name = lambda value: value.upper()

@cli.command("hello", name=Arg("--name", converter=format_name))
def hello(name: str):
    print(f"Hello {name}"!)
$ python cli.py hello --name Alex
Hello ALEX!

全局自定义类型转换器

通过初始化 Radicli 时提供的 converters 参数,您可以提供映射到转换函数的类型字典。如果遇到目标类型的参数,输入字符串值会自动转换。这确保了您的 Python 函数保持可组合性,不需要额外的逻辑来满足 CLI 使用。

以下示例展示了如何注册一个自定义转换器,该转换器从字符串名称加载 spaCy 管道,同时允许函数本身需要 Language 对象。

import radicli
import spacy

def load_spacy_model(name: str) -> spacy.language.Language:
    return spacy.load(name)

converters = {spacy.language.Language: load_spacy_model}
cli = Radicli(converters=converters)

@cli.command(
    "process",
    nlp=Arg(help="The spaCy pipeline to use"),
    name=Arg("--text", help="The text to process")
)
def process_text(nlp: spacy.language.Language, text: str):
    doc = nlp(text)
    print(doc.text, [token.pos_ for token in doc])
$ python test.py process en_core_web_sm --text Hello world!
Hello world! ['INTJ', 'NOUN', 'PUNCT']

如果您想为现有类型添加自定义处理并将其别名为新类型,可以创建一个 NewType。这也是内置的 Path 转换器 的实现方式。在帮助消息中,将显示基于的类型以及自定义名称。

from typing import NewType
from pathlib import Path

ExistingPath = NewType("ExistingPath", Path)

def convert_existing_path(path_str: str) -> Path:
    path = Path(path_str)
    if not path.exists():
        raise ValueError(f"path does not exist: {path_str}")
    return path

converters = {ExistingPath: convert_existing_path}

对于可以接受参数的泛型,例如 ListList[str],转换器会检查确切的类型以及来源。这意味着可以为不同的泛型拥有多个转换器,以及一个后备

converters = {
    List[str]: convert_string_list,
    List[int]: convert_int_list,
    List: convert_other_lists,
}

允许额外的参数

如果您想捕获并消耗函数和参数注解中未定义的额外参数,可以使用 command_with_extrasubcommand_with_extra 装饰器。额外参数以字符串列表的形式传递给函数的参数 _extra(您可以通过初始化 CLI 时的 extra_key 设置来更改它)。spaCy 使用此功能在 download 命令 中传递设置,或者在训练期间允许任意的 配置覆盖

@cli.command_with_extra("hello", name=Arg("--name", help="Your name"))
def hello(name: str, _extra: List[str] = []):
    print(f"Hello {name}!", _extra)
$ python cli.py hello --name Alex --age 35 --color blue
Hello Alex! ['--age', '35', '--color', 'blue']

通过堆叠装饰器创建命令别名

可以通过堆叠命令和子命令装饰器来使同一函数通过不同的命令别名可用。在这种情况下,您只需确保所有装饰器接收相同的参数注解即可,例如,将它们移动到变量中。

args = dict(
    name=Arg(help="Your name"),
    age=Arg("--age", "-a", help="Your age")
)

@cli.command("hello", **args)
@cli.command("hey", **args)
@cli.subcommand("greet", "person", **args)
def hello(name: str, age: int):
    print(f"Hello {name} ({age})!")
$ python cli.py hello --name Alex --age 35
$ python cli.py hey --name Alex --age 35
$ python cli.py greet person --name Alex --age 35

错误处理

将 CLIs 添加到代码库时常见的难题之一是错误处理。在 CLI 上下文中调用时,通常希望格式化打印任何错误并避免长跟踪回溯。然而,您不希望在用于其他地方或直接从 Python 或测试期间调用 CLI 函数的辅助函数中使用这些错误和平凡的 SystemExit(没有跟踪回溯)。

为了解决这个问题,radicli 允许您通过初始化时的 errors 参数提供错误映射。它将 Exception 类型如 ValueError 或完全定制的错误子类映射到处理函数。如果抛出了该类型的错误,则调用处理程序并将错误传递给它。处理程序可以选择返回一个退出代码——在这种情况下,radicli 将使用该代码执行 sys.exit。如果没有返回错误代码,则不执行退出,处理程序可以自行处理退出或选择不退出。

from radicli import Radicli
from termcolor import colored

def pretty_print_error(error: Exception) -> int:
    print(colored(f"🚨 {error}", "red"))
    return 1

cli = Radicli(errors={ValueError: handle_error})

@cli.command("hello", name=Arg("--name"))
def hello(name: str):
    if name == "Alex":
        raise ValueError("Invalid name")
$ python cli.py hello --name Alex
🚨 Invalid name
>>> hello("Alex")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Invalid name

此方法与自定义错误子类特别强大。在这里,您可以决定错误应接受哪些参数以及这些信息应在 CLI 上还是在一个常规的非 CLI 上下文中显示。

class CustomError(Exception):
    def __init__(self, text: str, additional_info: Any = "") -> None:
        self.text = text
        self.additional_info
        self.message = f"{self.text} {self.additional_info}"
        super().__init__(self.message)

def handle_custom_error(error: CustomError) -> int:
    print(colored(error.text, "red"))
    print(error.additional_info)
    return 1

使用静态数据以加快帮助和错误处理

CLIs 通常需要其他各种 Python 包,这些包需要导入——例如,您可能需要导入 pytorchtensorflow,或者在全局范围内加载其他资源。所有这些都增加了 CLI 的加载时间,因此即使显示 --help 信息也可能需要几秒钟才能运行。这些都是不必要的,会让开发者感到沮丧。

radicli 允许您生成 CLI 的静态表示,并将其作为 JSON 文件,包括输出帮助信息以及检查命令是否存在以及是否提供了正确和必需的参数所需的一切。如果静态 CLI 没有通过打印帮助信息或引发错误来执行系统退出,则可以导入并运行“实时”CLI 以继续。这可以让您 延迟导入直到真正需要时,即转换参数到预期的类型并执行命令函数。

cli.to_static("./static.json")
from radicli import StaticRadicli

static = StaticRadicli.load("./static.json")

if __name__ == "__main__":
    static.run()

    # This only runs if the static CLI doesn't error or print help
    from .cli import cli
    cli.run()

如果 CLI 是 Python 包的一部分,您可以在构建过程中生成静态 JSON 文件,并将预生成的 JSON 文件与您的包一起分发。

StaticRadicli 还提供了一个 disable 参数来禁用开发期间(或如果设置了某些环境变量)的静态解析。设置 debug=True 将打印一个额外的起始标记和可选的结束标记(如果静态 CLI 在之前没有退出),以指示静态 CLI 已运行。

自动记录 CLI

《Radicli.document》方法允许您为您的CLI生成简单的Markdown格式文档,并在顶部可选地添加titledescription。您还可以将此调用包含在CI或构建过程中,以确保文档始终保持最新。

with Path("README.md").open("w", encoding="utf8") as f:
    f.write(cli.document())

path_root允许您提供一个自定义的Path,该路径用作所有指定为默认参数的路径的相对根。这意味着绝对路径不会出现在您的README中。

🎛 API

dataclass Arg

描述参数元信息的Dataclass。这通常用于命令装饰器中,仅包含关于如何在CLI上处理参数的信息。参数类型和默认值从Python函数中读取。

参数 类型 描述
选项 Optional[str] CLI上使用的选项,例如--arg。如果未设置,参数将被视为位置参数。
Optional[str] 选项的简写,例如-A
帮助 Optional[str] 参数的帮助文本,用于--help
计数 bool 仅计数并返回参数被使用的次数,例如--verbose-vvv(对于简写-v)。
转换器 Optional[Callable[[str], Any]] 转换函数,它接受CLI值中的字符串并返回传递给函数的值。

dataclass Command

CLI命令的内部表示。可以通过Radicli.commandsRadicli.subcommands访问。

名称 类型 描述
name str 命令的名称。
func Callable 装饰的命令函数。
args List[ArgparseArg] 参数注解的内部表示。Argparse.arg允许您访问原始的Arg
描述 Optional[str] 命令描述,来自函数文档字符串。
允许额外 bool 是否允许额外参数。
父命令 Optional[str] 如果命令是子命令,则为其命名的父命令。
占位符 bool 命令是否为占位符,由Radicli.placeholder创建。有时检查这可能是有用的,例如用于测试。默认为False
显示名称 str 如果可用,包括父命令的显示名称,例如parent child

classmethod Command.from_function

从函数及其参数注解创建命令,并使用函数中定义的类型提示和默认值生成参数。这是命令装饰器内部发生的事情,如果需要手动构造Command,则可以使用。

def hello(name: str, age: int):
    print(f"Hello {name} ({age})!")

args = {"name": Arg(), "age": Arg("--age", help="Your age")}
command = Command.from_function("hello", args, hello)
参数 类型 描述
name str 命令的名称。
args Dict[str, Arg] 命令参数注解,定义为Arg数据类。
func Callable 命令函数。
父命令 Optional[str] 如果命令是子命令,则为其命名的父命令。
允许额外 bool 是否允许额外参数。
额外键 str 如果allow_extraTrue,则接收额外参数的函数参数的名称。默认为"_extra"
转换器 Dict[Type, Callable[[str], Any]] 将类型映射到全局转换函数的字典。
返回 Command 命令。

class Radicli

属性

名称 类型 描述
prog Optional[str] 显示在--help提示使用示例中的程序名称,例如"python -m spacy"
帮助 str CLI的帮助文本,显示在顶级--help。默认为""
version Optional[str] 如果设置,则通过--version提供版本。
转换器 Dict[Type, Callable[[str], Any]] 将类型映射到全局转换函数的字典。
errors Dict[Type[Exception], Callable[[Exception], Optional[int]]] 将错误类型映射到全局错误处理器的字典。如果处理程序返回退出码,则将使用该代码引发一个sys.exit。有关详细信息,请参阅错误处理
commands Dict[str, Command] 添加到CLI中的命令,按名称键。
子命令 Dict[str, Dict[str, Command]] 添加到CLI中的子命令,按父名称键,然后按子命令名称键。

方法 Radicli.__init__

初始化CLI并创建注册表。

from radicli import Radicli

cli = Radicli(prog="python -m spacy")
参数 类型 描述
prog Optional[str] 显示在--help提示使用示例中的程序名称,例如"python -m spacy"
帮助 str CLI的帮助文本,显示在顶级--help。默认为""
version Optional[str] 如果设置,则通过--version提供版本。
转换器 Dict[Type, Callable[[str], Any]] 将类型映射到转换函数的字典。所有具有这些类型的参数都将传递到相应的转换器。
errors Dict[Type[Exception], Callable[[Exception], Optional[int]]] 将错误类型映射到全局错误处理器的字典。如果处理程序返回退出码,则将使用该代码引发一个sys.exit。有关详细信息,请参阅错误处理
额外键 str 如果使用 command_with_extrasubcommand_with_extra 装饰器,则接收额外参数的函数参数名称。默认为 "_extra"

装饰器 Radicli.commandRadicli.command_with_extra

用于包装顶层命令函数的装饰器。

@cli.command(
    "hello",
    name=Arg(help="Your name"),
    age=Arg("--age", "-a", help="Your age"),
    greet=Arg("--greet", "-G", help="Whether to greet"),
)
def hello(name: str, age: int, greet: bool = False) -> None:
    if greet:
        print(f"Hello {name} ({age})")
$ python cli.py hello Alex --age 35 --greet
Hello Alex (35)
@cli.command_with_extra(
    "hello",
    name=Arg(help="Your name"),
    age=Arg("--age", "-A", help="Your age"),
)
def hello(name: str, age: int, _extra: List[str]) -> None:
    print(f"Hello {name} ({age})", _extra)
$ python cli.py hello Alex --age 35 --color red
Hello Alex (35) ['--color', 'red']
参数 类型 描述
name str 命令的名称。
**args Arg 定义参数信息的键值参数。名称需要与函数参数匹配。如果没有定义参数注解,则所有参数都视为位置参数。
返回 Callable 包装的函数。

装饰器 Radicli.subcommandRadicli.subcommand_with_extra

用于包装一级子命令函数的装饰器。

@cli.subcommand("hello", "world", name=Arg(help="Your name"))
def hello_world(name: str) -> None:
    print(f"Hello world, {name}!")
$ python cli.py hello world Alex
Hello world, Alex!
@cli.subcommand_with_extra("hello", "world", name=Arg(help="Your name"))
def hello_world(name: str, _extra: List[str]) -> None:
    print(f"Hello world, {name}!", _extra)
$ python cli.py hello world Alex --color blue
Hello world, Alex! ['--color', 'blue']
参数 类型 描述
父命令 str 父命令的名称(不需要存在)。
name str 子命令的名称。
**args Arg 定义参数信息的键值参数。名称需要与函数参数匹配。
返回 Callable 包装的函数。

方法 Radicli.placeholder

添加空父命令,并带有自定义描述文本,用于没有可执行父命令的子命令。

cli.placeholder("parent", description="This is the top-level command description")

@cli.subcommand("parent", "child", name=Arg("--name", help="Your name"))
def child(name: str) -> None:
    print(f"Hello {name}!")
参数 类型 描述
name str 命令的名称。
描述 Optional[str] 用于帮助文本的命令描述。

方法 Radicli.run

运行CLI。通常在文件末尾或包的 __main__.py 中的 if __name__ == "__main__": 块中调用,以便通过 python -m [package] 执行CLI。

if __name__ == "__main__":
    cli.run()
参数 类型 描述
args Optional[List[str]] Optional命令传入。如果没有设置,将从中读取 sys.argv(标准用法)。

方法 Radicli.call

使用args调用命令。

command = cli.commands["hello"]
cli.call(command, ["Alex", "--age", "35"])
参数 类型 描述
command Command 命令。
args Optional[List[str]] Optional命令传入。如果没有设置,将从中读取 sys.argv(标准用法)。

方法 Radicli.parse

解析给定命令的参数列表。通常是内部使用,但也可以用于测试。

command = cli.commands["hello"]
values = cli.parse(["Alex", "--age", "35"], command)
command.func(**values)
参数 类型 描述
args List[str] 字符串参数,例如从命令行接收到的参数。
command Command 命令。
子命令 Dict[str, Command] 父命令的子命令(如果有的话),按子命令名称键。默认为 {}
allow_partial bool 允许部分解析并仍然返回解析的值,即使缺少必需的参数。默认为 False
返回 Dict[str, Any] 按参数名称键入的解析值,可以传递给命令函数。

方法 Radicli.document

为CLI生成Markdown格式的文档。

with Path("README.md").open("w", encodig="utf8") as f:
    f.write(cli.document())
参数 类型 描述
title Optional[str] 添加到文件顶部的标题。默认为 None
描述 Optional[str] 添加到文件顶部的描述。默认为 None
comment Optional[str] 添加到文件顶部的HTML注释文本,通常指示它是自动生成的。如果 None,则不会添加注释。默认为 "This file is auto-generated"
path_root Optional[Path] 用作类型为 Path 的参数默认值的相对根路径,以防止本地绝对路径出现在文档中。默认为 None
返回 str Markdown格式的文档。

方法 Radicli.to_static

导出CLI的静态JSON表示形式,用于 StaticRadicli

cli.to_static("./static.json")
参数 类型 描述
file_path Union[str, Path] JSON文件的路径。
返回 Path 数据保存到的路径。

方法 Radicli.to_static_json

将CLI的静态表示形式作为可序列化为JSON的字典生成,用于 StaticRadicli

data = cli.to_static_json()
参数 类型 描述
返回 Dict[str, Any] 静态数据。

StaticRadicli

Radicli 的子类,同时也是 CLI 的静态版本,可以从 CLI 的静态表示形式中加载,该表示形式由 Radicli.to_static 生成。静态 CLI 可以在导入并运行实时 CLI 之前运行,并负责显示帮助信息以及进行基本的参数检查,例如确保所有参数都正确且存在。这可以通过将实时 CLI 的导入推迟到真正需要时(即转换值并执行函数)来显著加快 CLI 帮助的加载速度。

static = StaticRadicli.load("./static.json")

if __name__ == "__main__":
    static.run()
    # This only runs if the static CLI doesn't error or print help
    from .cli import cli
    cli.run()

类方法 StaticRadicli.load

从使用 Radicli.to_static 生成的 JSON 文件中加载静态 CLI。

static = StaticRadicli.load("./static.json")
参数 类型 描述
file_path Union[str, Path] 要加载的 JSON 文件。
disable bool 是否禁用静态解析。在开发期间可能很有用。默认值为 False
debug bool 启用调试模式并打印额外的开始和可选结束标记(如果静态 CLI 在退出之前没有退出)以指示静态 CLI 已运行。默认值为 False
转换器 Dict[Type, Callable[[str], Any]] 将类型映射到全局转换函数的字典,这些函数将用于反序列化类型。

方法 StaticRadicli.__init__

使用可 JSON 序列化的静态表示形式初始化静态 CLI。

data = cli.to_static_json()
static = StaticRadicli(data)
参数 类型 描述
data Dict[str, Any] 静态数据。
disable bool 是否禁用静态解析。在开发期间可能很有用。默认值为 False
debug bool 启用调试模式并打印额外的开始和可选结束标记(如果静态 CLI 在退出之前没有退出)以指示静态 CLI 已运行。默认值为 False
转换器 Dict[Type, Callable[[str], Any]] 将类型映射到全局转换函数的字典,这些函数将用于反序列化类型。

方法 StaticRadicli.run

运行静态 CLI。通常在运行实时 CLI 之前调用,如果打印了帮助信息(0)或参数名称缺失或不正确(1),则将执行系统退出。这意味着可以将实时 CLI 的加载推迟到真正需要时,即转换值并执行函数。

if __name__ == "__main__":
    static.run()

    from .cli import cli
    cli.run()
参数 类型 描述
args Optional[List[str]] Optional命令传入。如果没有设置,将从中读取 sys.argv(标准用法)。

自定义类型和转换器

该软件包包含默认启用的几个转换器,以及作为 NewType 实现的自定义类型,并具有预定义的转换函数。如果这些类型在装饰的函数中使用,则从 CLI 收到的值将相应地进行转换和验证。

名称 类型 描述
ExistingPath Path 返回一个路径并检查其是否存在。
ExistingFilePath Path 返回一个路径并检查其是否存在且为文件。
ExistingDirPath Path 返回一个路径并检查其是否存在且为目录。
ExistingPathOrDash Union[Path, Literal["-"]] 返回一个现有路径,但也接受 "-"(通常用于指示函数应从标准输入读取)。
ExistingFilePathOrDash Union[Path, Literal["-"]] 返回一个现有文件路径,但也接受 "-"(通常用于指示函数应从标准输入读取)。
ExistingDirPathOrDash Union[Path, Literal["-"]] 返回一个现有目录路径,但也接受 "-"(通常用于指示函数应从标准输入读取)。
PathOrDash Union[Path, Literal["-"]] 返回一个路径,但也接受 "-"(通常用于指示函数应从标准输入读取)。
UUID UUID 将值转换为 UUID。
StrOrUUID Union[str, UUID] 如果有效,将值转换为 UUID,否则返回字符串。

get_list_converter

辅助函数,创建一个列表转换器,它接受由分隔符分隔的项目字符串,并返回给定类型的项目列表。如果您更喜欢在 CLI 上将列表定义为逗号分隔的字符串,而不是重复参数,则这可能很有用。

@cli.command("hello", items=Arg("--items", converter=get_list_converter(str)))
def hello(items: List[str]) -> None:
    print(items)
参数 类型 描述
type_func Callable[[Any], Union[bool, int, float]] 转换列表项的函数。可以是内置函数,如 strint,也可以是自定义转换函数。
delimiter str 字符串的分隔符。默认为 ","
返回 Callable[[str], List] 转换函数,将字符串转换为给定类型的列表。

项目详细信息


下载文件

下载适合您平台文件。如果您不确定选择哪个,请了解更多关于安装包的信息。

源代码分发

radicli-0.0.25.tar.gz (49.3 kB 查看哈希)

上传时间 源代码

构建分发

radicli-0.0.25-py3-none-any.whl (35.0 kB 查看哈希)

上传时间 Python 3

支持者