feat(server): 平台默认组合——Windows granian+winloop,Unix granian+rloop
- granian[reload,rloop,uvloop,winloop] 改为默认依赖,uv sync 即具备完整启动栈 - auto 在启动器内解析为具体循环:granian Windows→winloop / Unix→rloop(缺省回退 uvloop/asyncio); uvicorn Unix→uvloop / Windows→asyncio - 启动日志打印实际 server+loop;测试覆盖平台默认与回退;README 默认矩阵更新
This commit is contained in:
1 parent
0cae770ace
commit
bcfc1022eb
5 files changed
+114
-51
No files matched your search
@@ -53,29 +53,37 @@ Node/TypeScript 版(v1.x,`master` 分支):`pnpm install && pnpm start`
|
|||||||
|
|
||||||
### 服务器与事件循环(uvicorn / granian)
|
### 服务器与事件循环(uvicorn / granian)
|
||||||
|
|
||||||
`scripts/serve.py` 统一启动入口:默认 `--server auto`(装了 granian 就用 granian,否则 uvicorn),
|
`scripts/serve.py` 统一启动入口。默认组合按平台确定:
|
||||||
|
|
||||||
|
| 平台 | 默认服务器 | 默认事件循环 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Windows | granian | winloop(未安装则 asyncio) |
|
||||||
|
| Linux / macOS | granian | rloop(未安装则 uvloop → asyncio) |
|
||||||
|
| Windows + `--server uvicorn` | uvicorn | asyncio(uvicorn 不支持 winloop) |
|
||||||
|
| Linux / macOS + `--server uvicorn` | uvicorn | uvloop(未安装则 asyncio) |
|
||||||
|
|
||||||
事件循环可用 `auto / asyncio / rloop / uvloop / winloop`,参数与环境变量等价
|
事件循环可用 `auto / asyncio / rloop / uvloop / winloop`,参数与环境变量等价
|
||||||
(`SERVER` / `LOOP` / `HOST` / `PORT` / `WORKERS` / `RELOAD`)。
|
(`SERVER` / `LOOP` / `HOST` / `PORT` / `WORKERS` / `RELOAD`)。`auto` 会在启动器内解析成上表中的具体循环,
|
||||||
|
启动日志会打印实际使用的组合;显式请求当前平台不可用的循环会直接报错并列出可用项(不会静默降级)。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv sync --extra granian # Granian + rloop/uvloop/winloop + watchfiles
|
uv sync # 默认依赖已含 granian[rloop/uvloop/winloop/reload]
|
||||||
uv run python scripts/serve.py # auto:granian(若已安装),loop=auto
|
uv run python scripts/serve.py # Windows → granian+winloop;Unix → granian+rloop
|
||||||
uv run python scripts/serve.py --server granian --loop rloop # 仅 Unix(Rust 事件循环)
|
uv run python scripts/serve.py --server uvicorn # Unix → uvicorn+uvloop;Windows → uvicorn+asyncio
|
||||||
uv run python scripts/serve.py --server granian --loop uvloop # 仅 Unix
|
uv run python scripts/serve.py --loop rloop # 仅 Unix(Rust 事件循环)
|
||||||
uv run python scripts/serve.py --server granian --loop winloop # 仅 Windows
|
uv run python scripts/serve.py --loop uvloop # 仅 Unix
|
||||||
uv run python scripts/serve.py --server uvicorn --loop uvloop # 仅 Unix
|
uv run python scripts/serve.py --loop winloop # 仅 Windows
|
||||||
uv run python scripts/serve.py --server granian --reload # 热重载(强制单 worker)
|
uv run python scripts/serve.py --server granian --reload # 热重载(强制单 worker)
|
||||||
```
|
```
|
||||||
|
|
||||||
| 事件循环 | 平台 | 说明 |
|
| 事件循环 | 平台 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `asyncio` | 全平台 | 标准库,始终可用 |
|
| `asyncio` | 全平台 | 标准库,始终可用 |
|
||||||
| `rloop` | Unix | granian 的 Rust 事件循环(`rloop` 包,Windows 无 wheel) |
|
| `rloop` | Unix | Rust 事件循环(`rloop` 包,Windows 无 wheel) |
|
||||||
| `uvloop` | Unix | libuv 事件循环 |
|
| `uvloop` | Unix | libuv 事件循环 |
|
||||||
| `winloop` | Windows | uvloop 的 Windows 实现 |
|
| `winloop` | Windows | uvloop 的 Windows 实现 |
|
||||||
| `auto` | 全平台 | granian:rloop → uvloop → winloop → asyncio;uvicorn:uvloop → asyncio |
|
|
||||||
|
|
||||||
显式请求当前平台不可用的循环会直接报错并列出可用项(不会静默降级)。
|
granian 也支持显式指定与默认不同的循环(如 Unix 上 `--loop uvloop`、Windows 上 `--loop asyncio`)。
|
||||||
|
|
||||||
## HTTP 接口
|
## HTTP 接口
|
||||||
|
|
||||||
@@ -204,7 +212,7 @@ scripts/
|
|||||||
serve.py 启动器(uvicorn / granian,事件循环 rloop/uvloop/winloop 选择与校验)
|
serve.py 启动器(uvicorn / granian,事件循环 rloop/uvloop/winloop 选择与校验)
|
||||||
docs/
|
docs/
|
||||||
PORTING.md TS → Python 平台移植契约(目录/子模块协议/签名差异/验收要求)
|
PORTING.md TS → Python 平台移植契约(目录/子模块协议/签名差异/验收要求)
|
||||||
pyproject.toml uv 项目定义(依赖、granian 可选 extra、ruff、pyright、pytest 配置)
|
pyproject.toml uv 项目定义(默认 granian/uvicorn 依赖、ruff、pyright、pytest 配置)
|
||||||
tests/
|
tests/
|
||||||
test_serve.py 启动器服务器/循环选择逻辑测试(TS 版 smoke 测试尚未移植)
|
test_serve.py 启动器服务器/循环选择逻辑测试(TS 版 smoke 测试尚未移植)
|
||||||
```
|
```
|
||||||
|
|||||||
+3
-12
@@ -5,6 +5,9 @@ description = "多平台音乐聚合服务器"
|
|||||||
requires-python = ">=3.12,<3.13"
|
requires-python = ">=3.12,<3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi>=0.115",
|
"fastapi>=0.115",
|
||||||
|
# 默认服务器:granian;uvicorn 作为备选(--server uvicorn)
|
||||||
|
# extras 按平台自动选择循环:rloop/uvloop(Unix)、winloop(Windows)、reload(watchfiles)
|
||||||
|
"granian[reload,rloop,uvloop,winloop]>=2.0",
|
||||||
"uvicorn[standard]>=0.30",
|
"uvicorn[standard]>=0.30",
|
||||||
"aiohttp>=3.10",
|
"aiohttp>=3.10",
|
||||||
"brotli>=1.1",
|
"brotli>=1.1",
|
||||||
@@ -13,18 +16,6 @@ dependencies = [
|
|||||||
"pillow>=10",
|
"pillow>=10",
|
||||||
]
|
]
|
||||||
|
|
||||||
# 可选:Granian 服务器(Rust HTTP 实现)与高性能事件循环
|
|
||||||
# rloop —— Granian 内置的 Rust 事件循环,跨平台
|
|
||||||
# uvloop —— POSIX 上的 libuv 循环
|
|
||||||
# winloop —— Windows 上的 libuv 循环(uvloop 的 Windows 对应实现)
|
|
||||||
# 安装:uv sync --extra granian
|
|
||||||
[project.optional-dependencies]
|
|
||||||
granian = [
|
|
||||||
# granian 自带的 extras 已按平台配好 marker:
|
|
||||||
# rloop(Unix)、uvloop(Unix)、winloop(Windows)、reload(watchfiles)
|
|
||||||
"granian[reload,rloop,uvloop,winloop]>=2.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=8",
|
"pytest>=8",
|
||||||
|
|||||||
+41
-18
@@ -1,21 +1,22 @@
|
|||||||
"""启动器 —— uvicorn / granian,可选事件循环 rloop / uvloop / winloop。
|
"""启动器 —— 默认 granian + 平台首选事件循环(Windows: winloop / Unix: rloop)。
|
||||||
|
|
||||||
用法(在仓库根目录执行):
|
用法(在仓库根目录执行):
|
||||||
|
|
||||||
uv run python scripts/serve.py # 默认:装了 granian 就优先 granian,否则 uvicorn
|
uv run python scripts/serve.py # Windows: granian+winloop;Unix: granian+rloop
|
||||||
uv run python scripts/serve.py --server granian # Rust HTTP 服务器
|
uv run python scripts/serve.py --server uvicorn # Unix 默认 uvloop,Windows 默认 asyncio
|
||||||
uv run python scripts/serve.py --server granian --loop rloop # 仅 Unix(granian[rloop])
|
uv run python scripts/serve.py --loop rloop # 显式指定(平台/安装不支持时直接报错)
|
||||||
uv run python scripts/serve.py --server granian --loop uvloop # 仅 Unix(granian[uvloop])
|
uv run python scripts/serve.py --loop uvloop
|
||||||
uv run python scripts/serve.py --server granian --loop winloop # 仅 Windows(granian[winloop])
|
uv run python scripts/serve.py --loop winloop
|
||||||
uv run python scripts/serve.py --server uvicorn --loop uvloop # uvicorn 在 Unix 上等价
|
|
||||||
|
|
||||||
事件循环可用性(与 granian 的 extras / 平台 marker 一致):
|
事件循环可用性(与依赖的平台 marker 一致):
|
||||||
asyncio 始终可用
|
asyncio 始终可用
|
||||||
rloop rloop 包,仅 Unix(granian 内置的 Rust 循环)
|
rloop rloop 包,仅 Unix(Rust 事件循环)
|
||||||
uvloop uvloop 包,仅 Unix
|
uvloop uvloop 包,仅 Unix
|
||||||
winloop winloop 包,仅 Windows(uvloop 的 Windows 实现)
|
winloop winloop 包,仅 Windows(uvloop 的 Windows 实现)
|
||||||
`auto` 交给服务器自行选择:granian 优先 rloop → uvloop → winloop → asyncio;
|
|
||||||
uvicorn 优先 uvloop(已安装时)→ asyncio。
|
`auto` 在启动器内解析为具体循环,不交给服务器:
|
||||||
|
granian: Windows → winloop → asyncio;Unix → rloop → uvloop → asyncio
|
||||||
|
uvicorn: Windows → asyncio;Unix → uvloop → asyncio
|
||||||
|
|
||||||
环境变量(参数优先):SERVER / LOOP / HOST / PORT / WORKERS / RELOAD。
|
环境变量(参数优先):SERVER / LOOP / HOST / PORT / WORKERS / RELOAD。
|
||||||
PORT 默认读取 .env 后的 16688;HOST 默认 0.0.0.0(局域网播放器可直连)。
|
PORT 默认读取 .env 后的 16688;HOST 默认 0.0.0.0(局域网播放器可直连)。
|
||||||
@@ -42,6 +43,7 @@ LOOPS = ("auto", "asyncio", "rloop", "uvloop", "winloop")
|
|||||||
|
|
||||||
#: 每个循环对应的可导入包(asyncio 无依赖)
|
#: 每个循环对应的可导入包(asyncio 无依赖)
|
||||||
LOOP_PACKAGES = {"rloop": "rloop", "uvloop": "uvloop", "winloop": "winloop"}
|
LOOP_PACKAGES = {"rloop": "rloop", "uvloop": "uvloop", "winloop": "winloop"}
|
||||||
|
IS_WINDOWS = sys.platform.startswith("win")
|
||||||
|
|
||||||
|
|
||||||
def has_module(name: str) -> bool:
|
def has_module(name: str) -> bool:
|
||||||
@@ -53,7 +55,7 @@ def has_module(name: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def pick_server(requested: str) -> str:
|
def pick_server(requested: str) -> str:
|
||||||
"""auto:安装了 granian 就用 granian,否则 uvicorn。"""
|
"""auto:默认 granian(uv sync 已包含);未安装时回退 uvicorn。"""
|
||||||
if requested not in SERVERS:
|
if requested not in SERVERS:
|
||||||
raise SystemExit(f"不支持的 server: {requested}(可选:{', '.join(SERVERS)})")
|
raise SystemExit(f"不支持的 server: {requested}(可选:{', '.join(SERVERS)})")
|
||||||
if requested != "auto":
|
if requested != "auto":
|
||||||
@@ -71,18 +73,36 @@ def available_loops(server: str) -> list[str]:
|
|||||||
return loops
|
return loops
|
||||||
|
|
||||||
|
|
||||||
|
def default_loop(server: str) -> str:
|
||||||
|
"""平台默认循环:granian = Windows winloop / Unix rloop;uvicorn = Unix uvloop / Windows asyncio。
|
||||||
|
|
||||||
|
依赖未安装(例如只用 uvloop 时没装 rloop)则按优先级回退,直到 asyncio。
|
||||||
|
"""
|
||||||
|
if server == "granian":
|
||||||
|
order = ("winloop", "asyncio") if IS_WINDOWS else ("rloop", "uvloop", "asyncio")
|
||||||
|
else:
|
||||||
|
order = ("asyncio",) if IS_WINDOWS else ("uvloop", "asyncio")
|
||||||
|
for loop in order:
|
||||||
|
if loop == "asyncio":
|
||||||
|
return loop
|
||||||
|
package = LOOP_PACKAGES.get(loop)
|
||||||
|
if package and has_module(package):
|
||||||
|
return loop
|
||||||
|
return "asyncio"
|
||||||
|
|
||||||
|
|
||||||
def resolve_loop(server: str, loop: str) -> str:
|
def resolve_loop(server: str, loop: str) -> str:
|
||||||
"""校验事件循环可用性;显式请求不可用时给出可操作的原因(不做静默降级)。"""
|
"""auto → 平台默认循环;显式请求不可用时给出可操作的原因(不做静默降级)。"""
|
||||||
if loop not in LOOPS:
|
if loop not in LOOPS:
|
||||||
raise SystemExit(f"不支持的 loop: {loop}(可选:{', '.join(LOOPS)})")
|
raise SystemExit(f"不支持的 loop: {loop}(可选:{', '.join(LOOPS)})")
|
||||||
if loop == "auto":
|
if loop == "auto":
|
||||||
return loop
|
return default_loop(server)
|
||||||
if server == "uvicorn":
|
if server == "uvicorn":
|
||||||
if loop == "asyncio":
|
if loop == "asyncio":
|
||||||
return loop
|
return loop
|
||||||
if loop == "uvloop":
|
if loop == "uvloop":
|
||||||
if not has_module("uvloop"):
|
if not has_module("uvloop"):
|
||||||
raise SystemExit("uvloop 未安装(仅 Unix 可用):uv sync --extra granian 或改用 --loop auto")
|
raise SystemExit("uvloop 未安装(仅 Unix 可用):uv sync 或改用 --loop auto")
|
||||||
return loop
|
return loop
|
||||||
raise SystemExit(f"uvicorn 不支持 --loop {loop};请改用 --server granian(granian 支持 rloop/uvloop/winloop)")
|
raise SystemExit(f"uvicorn 不支持 --loop {loop};请改用 --server granian(granian 支持 rloop/uvloop/winloop)")
|
||||||
|
|
||||||
@@ -115,7 +135,7 @@ def build_command(
|
|||||||
if reload:
|
if reload:
|
||||||
command.append("--reload")
|
command.append("--reload")
|
||||||
if reload and not has_module("watchfiles"):
|
if reload and not has_module("watchfiles"):
|
||||||
print("[serve] 警告:granian --reload 需要 watchfiles(uv sync --extra granian 已包含)", flush=True)
|
print("[serve] 警告:granian --reload 需要 watchfiles(uv sync 已包含)", flush=True)
|
||||||
command.append(APP_TARGET)
|
command.append(APP_TARGET)
|
||||||
else:
|
else:
|
||||||
command += [APP_TARGET, "--host", host, "--port", str(port)]
|
command += [APP_TARGET, "--host", host, "--port", str(port)]
|
||||||
@@ -141,9 +161,12 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
parser.add_argument("--reload", action="store_true", help="开发热重载(强制单 worker)")
|
parser.add_argument("--reload", action="store_true", help="开发热重载(强制单 worker)")
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
server = pick_server(str(_arg_or_env(args.server, "SERVER", "auto")))
|
requested_server = str(_arg_or_env(args.server, "SERVER", "auto"))
|
||||||
|
server = pick_server(requested_server)
|
||||||
if not has_module(server):
|
if not has_module(server):
|
||||||
raise SystemExit(f"未安装 {server}:uv sync --extra granian(或 uv add {server})")
|
raise SystemExit(f"未安装 {server}:uv sync(或 uv add {server})")
|
||||||
|
if requested_server == "auto" and server == "uvicorn" and not has_module("granian"):
|
||||||
|
print("[serve] 未安装 granian,回退 uvicorn;uv sync 可安装默认的 granian 依赖", flush=True)
|
||||||
|
|
||||||
loop = resolve_loop(server, str(_arg_or_env(args.loop, "LOOP", "auto")))
|
loop = resolve_loop(server, str(_arg_or_env(args.loop, "LOOP", "auto")))
|
||||||
host = str(_arg_or_env(args.host, "HOST", "0.0.0.0"))
|
host = str(_arg_or_env(args.host, "HOST", "0.0.0.0"))
|
||||||
|
|||||||
+48
-2
@@ -45,11 +45,57 @@ def test_uvicorn_rejects_granian_only_loops() -> None:
|
|||||||
serve.resolve_loop("uvicorn", "rloop")
|
serve.resolve_loop("uvicorn", "rloop")
|
||||||
|
|
||||||
|
|
||||||
def test_uvicorn_auto_and_asyncio_allowed() -> None:
|
def test_uvicorn_auto_and_asyncio_allowed(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
assert serve.resolve_loop("uvicorn", "auto") == "auto"
|
monkeypatch.setattr(serve, "IS_WINDOWS", True)
|
||||||
|
assert serve.resolve_loop("uvicorn", "auto") == "asyncio"
|
||||||
assert serve.resolve_loop("uvicorn", "asyncio") == "asyncio"
|
assert serve.resolve_loop("uvicorn", "asyncio") == "asyncio"
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_loop_granian_windows(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(serve, "IS_WINDOWS", True)
|
||||||
|
monkeypatch.setattr(serve, "has_module", lambda name: name == "winloop")
|
||||||
|
assert serve.default_loop("granian") == "winloop"
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_loop_granian_unix_prefers_rloop(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(serve, "IS_WINDOWS", False)
|
||||||
|
monkeypatch.setattr(serve, "has_module", lambda name: name in ("rloop", "uvloop"))
|
||||||
|
assert serve.default_loop("granian") == "rloop"
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_loop_granian_unix_falls_back(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(serve, "IS_WINDOWS", False)
|
||||||
|
monkeypatch.setattr(serve, "has_module", lambda name: name == "uvloop")
|
||||||
|
assert serve.default_loop("granian") == "uvloop"
|
||||||
|
|
||||||
|
monkeypatch.setattr(serve, "has_module", lambda name: False)
|
||||||
|
assert serve.default_loop("granian") == "asyncio"
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_loop_uvicorn(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(serve, "IS_WINDOWS", False)
|
||||||
|
monkeypatch.setattr(serve, "has_module", lambda name: name == "uvloop")
|
||||||
|
assert serve.default_loop("uvicorn") == "uvloop"
|
||||||
|
|
||||||
|
# Windows 上 uvicorn 只有 asyncio(即使装了 winloop 也不会用)
|
||||||
|
monkeypatch.setattr(serve, "IS_WINDOWS", True)
|
||||||
|
monkeypatch.setattr(serve, "has_module", lambda name: name == "winloop")
|
||||||
|
assert serve.default_loop("uvicorn") == "asyncio"
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_loop_windows_without_winloop(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(serve, "IS_WINDOWS", True)
|
||||||
|
monkeypatch.setattr(serve, "has_module", lambda name: False)
|
||||||
|
assert serve.default_loop("granian") == "asyncio"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_loop_auto_uses_platform_default(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(serve, "IS_WINDOWS", False)
|
||||||
|
monkeypatch.setattr(serve, "has_module", lambda name: name in ("rloop", "uvloop"))
|
||||||
|
assert serve.resolve_loop("granian", "auto") == "rloop"
|
||||||
|
assert serve.resolve_loop("uvicorn", "auto") == "uvloop"
|
||||||
|
|
||||||
|
|
||||||
def test_build_command_granian(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_build_command_granian(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
monkeypatch.setattr(serve, "has_module", lambda name: True)
|
monkeypatch.setattr(serve, "has_module", lambda name: True)
|
||||||
command = serve.build_command("granian", host="0.0.0.0", port=16688, workers=2, loop="winloop", reload=True)
|
command = serve.build_command("granian", host="0.0.0.0", port=16688, workers=2, loop="winloop", reload=True)
|
||||||
|
|||||||
@@ -418,16 +418,12 @@ dependencies = [
|
|||||||
{ name = "brotli" },
|
{ name = "brotli" },
|
||||||
{ name = "cryptography" },
|
{ name = "cryptography" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
|
{ name = "granian", extra = ["reload", "rloop", "uvloop", "winloop"] },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
{ name = "qrcode" },
|
{ name = "qrcode" },
|
||||||
{ name = "uvicorn", extra = ["standard"] },
|
{ name = "uvicorn", extra = ["standard"] },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
|
||||||
granian = [
|
|
||||||
{ name = "granian", extra = ["reload", "rloop", "uvloop", "winloop"] },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
@@ -443,12 +439,11 @@ requires-dist = [
|
|||||||
{ name = "brotli", specifier = ">=1.1" },
|
{ name = "brotli", specifier = ">=1.1" },
|
||||||
{ name = "cryptography", specifier = ">=43" },
|
{ name = "cryptography", specifier = ">=43" },
|
||||||
{ name = "fastapi", specifier = ">=0.115" },
|
{ name = "fastapi", specifier = ">=0.115" },
|
||||||
{ name = "granian", extras = ["reload", "rloop", "uvloop", "winloop"], marker = "extra == 'granian'", specifier = ">=2.0" },
|
{ name = "granian", extras = ["reload", "rloop", "uvloop", "winloop"], specifier = ">=2.0" },
|
||||||
{ name = "pillow", specifier = ">=10" },
|
{ name = "pillow", specifier = ">=10" },
|
||||||
{ name = "qrcode", specifier = ">=8" },
|
{ name = "qrcode", specifier = ">=8" },
|
||||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.30" },
|
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.30" },
|
||||||
]
|
]
|
||||||
provides-extras = ["granian"]
|
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
|
|||||||
Reference in new issue
Block a user