feat(stream-gears): ship stream_gears package with accurate stubs (#1732)

* feat(stream-gears): ship stream_gears package with accurate stubs

The stream_gears/ package (__init__.py, stub, py.typed) never made it into
the wheel: pyproject only listed python-packages = ["biliup"], so maturin
generated its own __init__.py and the stub drifted from the implementation
(7 of 16 upload lines, login_by_qrcode typed as bool, missing upload
parameters, pyobject.Segment/Credit not accepted by the Rust side).

- Move the package to the repo root next to biliup/ (maturin resolves
  python-packages relative to python-source) and set
  module-name = "stream_gears.stream_gears" for a mixed layout.
- __init__.py re-exports the native module, adds Segment = PySegment and a
  Credit TypedDict; pyobject.py is removed.
- Rewrite stream_gears.pyi from the implementation, including
  StreamGearsError and the proxy/file parameters.
- Give upload an explicit text_signature so its string and list defaults
  show up in inspect.signature/help() instead of Ellipsis.

Top-level names only grow: Credit and Segment are added.

* ci(desktop): keep PyInstaller off the source stream_gears package

PyInstaller puts the repo root first on its search path, so the source
stream_gears/ (which has no native module) shadowed the installed package
and the frozen biliup failed to import stream_gears.stream_gears. Remove
the source copy after installing the wheel.

* test(stream-gears): check package layout and run stubtest in CI

test_package.py covers the new top-level names, the native submodule,
__all__ staying in sync with the native module, py.typed and the stub in
the wheel, upload's text_signature against PyO3's argument parsing, and
mypy --strict on a valid and an invalid example.

CI runs python -m mypy.stubtest stream_gears against the installed wheel
so the stub cannot drift from the implementation again.

* build(stream-gears): drop unused danmaku-client and async-trait deps

Neither is referenced anywhere in crates/stream-gears. danmaku is still
built through biliup-cli, so this only trims the manifest and lockfile
entries.

* refactor(stream-gears): keep the package in crates/stream-gears via python-source

Point maturin's python-source at crates/stream-gears instead of moving
stream_gears/ to the repository root. python-packages is resolved under
python-source, so the root biliup/ package is shipped through include.
The wheel contents are unchanged.

* ci: drop workarounds for a root-level stream_gears package

With stream_gears/ back in crates/stream-gears, nothing at the repository
root shadows the installed package, so PyInstaller and stubtest no longer
need to avoid it.
This commit is contained in:
Noah authored and GitHub committed 2026-09-24 06:48:40 +00:00
1 parent 67e49354ad
commit 3f4583b40d
11 files changed
+430 -215

No files matched your search

+5 -1
View File
@@ -64,8 +64,12 @@ jobs:
if: matrix.platform.target == 'x86_64'
run: |
python -m venv /tmp/smoke-venv
/tmp/smoke-venv/bin/pip install dist/biliup-*-abi3-*.whl pytest
# mypy is pinned because new stubtest releases add checks.
/tmp/smoke-venv/bin/pip install dist/biliup-*-abi3-*.whl pytest mypy==2.3.1
/tmp/smoke-venv/bin/pytest crates/stream-gears/tests -v
- name: Check type stubs against the wheel
if: matrix.platform.target == 'x86_64'
run: /tmp/smoke-venv/bin/python -m mypy.stubtest stream_gears
- name: Build free-threaded wheels
uses: PyO3/maturin-action@v1
with:
+3 -1
View File
@@ -56,4 +56,6 @@ cookies.json
.cargo/
logs/
CLAUDE.md
.claude/
.claude/
/crates/stream-gears/stream_gears/*.so
/crates/stream-gears/stream_gears/*.pyd
Generated
-2
View File
@@ -4412,12 +4412,10 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
name = "stream-gears"
version = "1.2.8"
dependencies = [
"async-trait",
"axum",
"biliup",
"biliup-cli",
"bon",
"danmaku",
"error-stack",
"nom 8.0.0",
"pyo3",
-2
View File
@@ -26,10 +26,8 @@ tracing-subscriber = { workspace = true, features = ["env-filter"] }
tracing-appender = { workspace = true }
biliup = { path = "../biliup"}
biliup-cli = { path = "../biliup-cli"}
danmaku-client = { package = "danmaku", path = "../danmaku"}
time = { workspace = true }
bon = { workspace = true }
async-trait = { workspace = true }
error-stack = { workspace = true }
[build-dependencies]
+3
View File
@@ -431,6 +431,9 @@ fn login_by_web_qrcode(
#[allow(clippy::too_many_arguments)]
#[pyfunction]
#[pyo3(signature = (video_path, cookie_file, title, tid=171, tid_v2=None, tag="".to_string(), copyright=2, source="".to_string(), desc="".to_string(), dynamic="".to_string(), cover="".to_string(), dolby=0, lossless_music=0, no_reprint=0, charging_pay=0, up_close_reply=false, up_selection_reply=false, up_close_danmu=false, limit=3, desc_v2=vec![], dtime=None, line=None, extra_fields="".to_string(), submit=None, proxy=None))]
#[pyo3(
text_signature = "(video_path, cookie_file, title, tid=171, tid_v2=None, tag='', copyright=2, source='', desc='', dynamic='', cover='', dolby=0, lossless_music=0, no_reprint=0, charging_pay=0, up_close_reply=False, up_selection_reply=False, up_close_danmu=False, limit=3, desc_v2=[], dtime=None, line=None, extra_fields='', submit=None, proxy=None)"
)]
fn upload(
py: Python<'_>,
video_path: Vec<PathBuf>,
+39 -5
View File
@@ -1,7 +1,41 @@
from .stream_gears import *
from .pyobject import Segment, Credit
import typing as _typing
from . import stream_gears
from .stream_gears import *
from .stream_gears import PySegment
__all__ = ['Segment', 'Credit']
__doc__ = stream_gears.__doc__
if hasattr(stream_gears, "__all__"):
__all__.extend(stream_gears.__all__)
# Literal so type checkers see the exports; the tests keep it in sync with the
# native module's `__all__`.
__all__ = [
"Credit",
"PySegment",
"Segment",
"StreamGearsError",
"UploadLine",
"config_bindings",
"download",
"download_with_callback",
"get_qrcode",
"login_by_cookies",
"login_by_qrcode",
"login_by_sms",
"login_by_web_cookies",
"login_by_web_qrcode",
"main_loop",
"send_sms",
"upload",
]
Segment = PySegment
class _CreditRequired(_typing.TypedDict):
type: int
raw_text: str
class Credit(_CreditRequired, total=False):
"""`upload` 的 `desc_v2` 元素:`type` 为 1 时是普通文本,为 2 时 `raw_text` 是被 @ 的用户名、`biz_id` 是其 mid。"""
biz_id: _typing.Optional[str]
@@ -1,36 +0,0 @@
from typing import Optional
class Segment:
"""视频分段设置"""
@staticmethod
def by_time(time: int) -> 'Segment':
"""
按时长分段
:param int time: 分段时长, 单位为秒
:return: 视频分段设置
"""
segment = Segment()
segment.time = time
return segment
@staticmethod
def by_size(size: int) -> 'Segment':
"""
按大小分段
:param int size: 分段大小, 单位为字节
:return: 视频分段设置
"""
segment = Segment()
segment.size = size
return segment
class Credit:
# FIXME: docstring
type_id: int
raw_text: str
biz_id: Optional[str]
+243 -167
View File
@@ -1,185 +1,261 @@
from typing import Dict, List, Optional, Callable
from enum import Enum
import os
from typing import Any, Callable, ClassVar, Mapping, Optional, Protocol, Sequence, Union, final
from .pyobject import Segment, Credit
from typing_extensions import TypeAlias, deprecated
from stream_gears import Credit
def download(url: str,
header_map: Dict[str, str],
file_name: str,
segment: Segment,
proxy: Optional[str]) -> None:
"""
下载视频
__all__ = [
"PySegment",
"StreamGearsError",
"UploadLine",
"config_bindings",
"download",
"download_with_callback",
"get_qrcode",
"login_by_cookies",
"login_by_qrcode",
"login_by_sms",
"login_by_web_cookies",
"login_by_web_qrcode",
"main_loop",
"send_sms",
"upload",
]
:param str url: 视频地址
:param Dict[str, str] header_map: HTTP请求头
:param str file_name: 文件名格式
:param Segment segment: 视频分段设置
:param Optional[str] proxy: 代理
_StrPath: TypeAlias = Union[str, "os.PathLike[str]"]
class StreamGearsError(RuntimeError):
"""下载、登录或上传失败时抛出。是 `RuntimeError` 的子类。"""
@final
class PySegment:
"""视频分段设置。`time`(秒)和 `size`(字节)都为 `None` 时不分段。"""
time: Optional[int]
size: Optional[int]
def __init__(self) -> None: ...
class _SegmentObject(Protocol):
@property
def time(self) -> Optional[int]: ...
@property
def size(self) -> Optional[int]: ...
_SegmentArg: TypeAlias = Union[PySegment, Mapping[str, Optional[int]], _SegmentObject]
class _CreditObject(Protocol):
@property
def type_id(self) -> int: ...
@property
def raw_text(self) -> str: ...
@final
class UploadLine:
"""上传线路。`upload` 不传 `line` 时自动测速选择。
`Cn*`、`An*`、`At*` 是同一 CDN 经不同接入域名的变体。
"""
def download_with_callback(url: str,
header_map: Dict[str, str],
file_name: str,
segment: Segment,
file_name_callback_fn: Callable[[str], None],
proxy: Optional[str]) -> None:
"""
下载视频
:param str url: 视频地址
:param Dict[str, str] header_map: HTTP请求头
:param str file_name: 文件名格式
:param Segment segment: 视频分段设置
:param Callable[[str], None] file_name_callback_fn: 回调已下载完成文件名
:param Optional[str] proxy: 代理
"""
def login_by_cookies(proxy: Optional[str]) -> bool:
"""
cookie登录
:param Optional[str] proxy: 代理
:return: 是否登录成功
"""
def send_sms(country_code: int, phone: int, proxy: Optional[str]) -> str:
"""
发送短信验证码
:param int country_code: 国家/地区代码
:param int phone: 手机号
:param Optional[str] proxy: 代理
:return: 短信登录JSON信息
"""
def login_by_sms(code: int, ret: str, proxy: Optional[str]) -> bool:
"""
短信登录
:param int code: 验证码
:param str ret: 短信登录JSON信息
:param Optional[str] proxy: 代理
:return: 是否登录成功
"""
def get_qrcode(proxy: Optional[str]) -> str:
"""
获取二维码
:param Optional[str] proxy: 代理
:return: 二维码登录JSON信息
"""
def login_by_qrcode(ret: str, proxy: Optional[str]) -> bool:
"""
二维码登录
:param str ret: 二维码登录JSON信息
:param Optional[str] proxy: 代理
:return: 是否登录成功
"""
def login_by_web_cookies(sess_data: str, bili_jct: str, proxy: Optional[str]) -> bool:
"""
网页Cookie登录1
:param str sess_data: SESSDATA
:param str bili_jct: bili_jct
:param Optional[str] proxy: 代理
:return: 是否登录成功
"""
def login_by_web_qrcode(sess_data: str, dede_user_id: str, proxy: Optional[str]) -> bool:
"""
网页Cookie登录2
:param str sess_data: SESSDATA
:param str dede_user_id: DedeUserID
:param Optional[str] proxy: 代理
:return: 是否登录成功
"""
class UploadLine(Enum):
"""上传线路"""
Bda2 = 1
Bldsa: ClassVar[UploadLine]
"""B站大陆动态加速"""
Cnbldsa: ClassVar[UploadLine]
Andsa: ClassVar[UploadLine]
Atdsa: ClassVar[UploadLine]
Bda2: ClassVar[UploadLine]
"""百度云"""
Tx = 4
Cnbd: ClassVar[UploadLine]
Anbd: ClassVar[UploadLine]
Atbd: ClassVar[UploadLine]
Tx: ClassVar[UploadLine]
"""腾讯云EO"""
Txa = 5
Cntx: ClassVar[UploadLine]
Antx: ClassVar[UploadLine]
Attx: ClassVar[UploadLine]
Txa: ClassVar[UploadLine]
"""腾讯云EO海外"""
Bldsa = 6
"""Bilibili大陆动态加速"""
Alia = 7
Alia: ClassVar[UploadLine]
"""阿里云海外"""
Estx = 8
Estx: ClassVar[UploadLine]
"""B站自建"""
Akbd = 9
Akbd: ClassVar[UploadLine]
"""B站自建"""
def __int__(self) -> int: ...
class _ConfigState:
def get(self, key: str, default: Any = None) -> Any:
"""`key` 不存在且未给 `default` 时抛 `AttributeError`。"""
def download(
url: str,
header_map: dict[str, str],
file_name: str,
segment: _SegmentArg,
proxy: Optional[str] = None,
) -> None:
"""下载 FLV 或 HLS 直播流。
def upload(video_path: List[str],
cookie_file: str,
title: str,
tid: int,
tag: str,
copyright: int,
source: str,
desc: str,
dynamic: str,
cover: str,
dolby: int,
lossless_music: int,
no_reprint: int,
charging_pay: int,
limit: int,
desc_v2: List[Credit],
dtime: Optional[int],
line: Optional[UploadLine],
extra_fields: Optional[str],
submit: Optional[str],
proxy: Optional[str]) -> None:
:param url: 流地址
:param header_map: HTTP 请求头
:param file_name: 文件名格式,不含扩展名,支持 strftime 占位符(如 `%Y-%m-%d`)
:param segment: 分段设置:`PySegment`、带 `time` / `size` 键的 dict,或带这两个属性的对象
:param proxy: 代理
:raises StreamGearsError: 连接失败、HTTP 错误状态、流数据不完整或解析失败
"""
上传视频稿件
:param List[str] video_path: 视频文件路径
:param str cookie_file: cookie文件路径
:param str title: 视频标题
:param int tid: 投稿分区
:param str tag: 视频标签, 英文逗号分隔多个tag
:param int copyright: 是否转载, 1-自制 2-转载
:param str source: 转载来源
:param str desc: 视频简介
:param str dynamic: 空间动态
:param str cover: 视频封面
:param int dolby: 是否开启杜比音效, 0-关闭 1-开启
:param int lossless_music: 是否开启Hi-Res, 0-关闭 1-开启
:param int no_reprint: 是否禁止转载, 0-允许 1-禁止
:param int charging_pay: 是否开启充电, 0-关闭 1-开启
:param int limit: 单视频文件最大并发数
:param List[Credit] desc_v2: 视频简介v2
:param Optional[dtime] int dtime: 定时发布时间, 距离提交大于2小时小于15天, 格式为10位时间戳
:param Optional[UploadLine] line: 上传线路
:param Optional[ExtraFields] line: 上传额外参数
:param Optional[str] submit: 提交接口, 可选值: BCutAndroid, App(默认)
:param Optional[str] proxy: 代理
def download_with_callback(
url: str,
header_map: dict[str, str],
file_name: str,
segment: _SegmentArg,
file_name_callback_fn: Optional[Callable[[str], object]] = None,
proxy: Optional[str] = None,
) -> None:
"""下载 FLV 或 HLS 直播流,每个分段文件写完时调用回调。
回调抛出的异常会立即记进日志,下载继续;下载结束后重新抛出第一个回调异常。
下载本身也失败时抛 `StreamGearsError`,回调异常挂在它的 `__cause__` 上。
:param file_name_callback_fn: 以写完的文件名为参数的回调
:raises StreamGearsError: 同 `download`
:raises TypeError: `file_name_callback_fn` 不可调用
"""
def login_by_cookies(file: str, proxy: Optional[str] = None) -> bool:
"""用 cookie 文件登录。
:param file: cookie 文件路径
:return: 成功时为 `True`
:raises StreamGearsError: 登录失败
"""
def send_sms(country_code: int, phone: int, proxy: Optional[str] = None) -> str:
"""发送短信验证码。
:return: 传给 `login_by_sms` 的 JSON 字符串
"""
def login_by_sms(
code: int,
ret: str,
proxy: Optional[str] = None,
file: _StrPath = "cookies.json",
) -> bool:
"""短信验证码登录,成功后把登录信息写入 `file`。
:param code: 验证码
:param ret: `send_sms` 的返回值
:return: 成功时为 `True`
:raises StreamGearsError: 登录失败
:raises ValueError: `ret` 不是合法 JSON
"""
def get_qrcode(proxy: Optional[str] = None) -> str:
"""获取登录二维码。
:return: 传给 `login_by_qrcode` 的 JSON 字符串
"""
def login_by_qrcode(ret: str, proxy: Optional[str] = None, file: Optional[_StrPath] = None) -> str:
"""等待扫码登录。
:param ret: `get_qrcode` 的返回值
:param file: 给了就同时把登录信息写入该文件
:return: 登录信息的 JSON 字符串
:raises StreamGearsError: 登录失败
:raises ValueError: `ret` 不是合法 JSON
"""
def login_by_web_cookies(
sess_data: str,
bili_jct: str,
proxy: Optional[str] = None,
file: _StrPath = "cookies.json",
) -> bool:
"""用网页 Cookie(`SESSDATA`、`bili_jct`)登录,成功后把登录信息写入 `file`。
:return: 成功时为 `True`
:raises StreamGearsError: 登录失败
"""
def login_by_web_qrcode(
sess_data: str,
dede_user_id: str,
proxy: Optional[str] = None,
file: _StrPath = "cookies.json",
) -> bool:
"""用网页 Cookie(`SESSDATA`、`DedeUserID`)登录,成功后把登录信息写入 `file`。
:return: 成功时为 `True`
:raises StreamGearsError: 登录失败
"""
def upload(
video_path: Sequence[_StrPath],
cookie_file: _StrPath,
title: str,
tid: int = 171,
tid_v2: Optional[int] = None,
tag: str = "",
copyright: int = 2,
source: str = "",
desc: str = "",
dynamic: str = "",
cover: str = "",
dolby: int = 0,
lossless_music: int = 0,
no_reprint: int = 0,
charging_pay: int = 0,
up_close_reply: bool = False,
up_selection_reply: bool = False,
up_close_danmu: bool = False,
limit: int = 3,
desc_v2: Sequence[Union[Credit, _CreditObject]] = [],
dtime: Optional[int] = None,
line: Optional[UploadLine] = None,
extra_fields: Optional[str] = "",
submit: Optional[str] = None,
proxy: Optional[str] = None,
) -> None:
"""上传视频并投稿。
:param video_path: 视频文件路径,每个文件是一个分P
:param cookie_file: cookie 文件路径
:param title: 标题
:param tid: 分区
:param tid_v2: 新版分区
:param tag: 标签,英文逗号分隔
:param copyright: 1 自制,2 转载
:param source: 转载来源
:param desc: 简介
:param dynamic: 空间动态
:param cover: 封面图片路径,空串表示不设置
:param dolby: 杜比音效,0 关闭,1 开启
:param lossless_music: Hi-Res 无损音质,0 关闭,1 开启
:param no_reprint: 禁止转载,0 允许,1 禁止
:param charging_pay: 充电专属,0 关闭,1 开启
:param up_close_reply: 关闭评论
:param up_selection_reply: 开启精选评论
:param up_close_danmu: 关闭弹幕
:param limit: 单个文件的并发上传数
:param desc_v2: 带 @ 的简介,元素见 `Credit`;也接受带 `type_id` / `raw_text` / `biz_id` 属性的对象
:param dtime: 定时发布的 10 位时间戳,须在提交后 2 小时到 15 天之间
:param line: 上传线路,`None` 时自动测速
:param extra_fields: 追加到投稿请求里的 JSON 对象字符串;`None` 或空串表示没有
:param submit: 投稿接口:`app`(默认)、`web`、`bcutandroid`,不区分大小写,无法识别时用 `app`
:param proxy: 代理
:raises StreamGearsError: 登录、上传或投稿失败
:raises ValueError: `extra_fields` 不是合法的 JSON 对象
:raises TypeError: `desc_v2` 元素缺少必填字段
"""
@deprecated("config_bindings() always returns the built-in default configuration and will be removed.")
def config_bindings() -> _ConfigState:
"""总是返回内置的默认配置;运行中的服务不读写它。"""
def main_loop() -> None:
"""`biliup` 命令行入口,参数取自 `sys.argv`。无参数或 `start` 时按 `server` 运行。
:raises SystemExit: 命令行参数错误
:raises RuntimeError: 服务运行出错
"""
+128
View File
@@ -0,0 +1,128 @@
"""Layout and type stubs of the installed ``stream_gears`` package.
Run against an installed wheel: ``pytest crates/stream-gears/tests``.
"""
import importlib.machinery
import importlib.metadata
import inspect
import subprocess
import sys
import textwrap
from pathlib import Path
import pytest
import stream_gears
from stream_gears import Credit, Segment, StreamGearsError, UploadLine
PACKAGE_FILES = {
"stream_gears/__init__.py",
"stream_gears/py.typed",
"stream_gears/stream_gears.pyi",
}
def test_top_level_names():
assert Segment is stream_gears.PySegment
assert UploadLine is stream_gears.UploadLine
assert issubclass(StreamGearsError, RuntimeError)
assert Credit(type=1, raw_text="hi") == {"type": 1, "raw_text": "hi"}
assert Credit.__required_keys__ == {"type", "raw_text"}
assert Credit.__optional_keys__ == {"biz_id"}
def test_native_module_is_a_submodule():
native = stream_gears.stream_gears
assert native.__name__ == "stream_gears.stream_gears"
assert native.__file__.endswith(tuple(importlib.machinery.EXTENSION_SUFFIXES))
assert Path(native.__file__).parent == Path(stream_gears.__file__).parent
def test_all_matches_native_module():
assert set(stream_gears.__all__) == set(stream_gears.stream_gears.__all__) | {
"Credit",
"Segment",
}
def test_wheel_ships_stubs():
files = {str(f).replace("\\", "/") for f in importlib.metadata.files("biliup")}
assert PACKAGE_FILES <= files
package_dir = Path(stream_gears.__file__).parent
assert (package_dir / "py.typed").is_file()
assert (package_dir / "stream_gears.pyi").is_file()
def test_upload_text_signature_is_accepted():
"""`upload` has a hand-written `text_signature`; every name and default in it
must go through PyO3's argument parsing. An invalid `extra_fields` raises
ValueError before anything else happens; a wrong name would raise TypeError."""
params = inspect.signature(stream_gears.upload).parameters
required = {"video_path": ["a.mp4"], "cookie_file": "cookies.json", "title": "t"}
kwargs = {
name: required[name] if p.default is inspect.Parameter.empty else p.default
for name, p in params.items()
}
assert kwargs["tag"] == "" and kwargs["desc_v2"] == [] and kwargs["tid"] == 171
kwargs["extra_fields"] = "{"
with pytest.raises(ValueError, match="extra_fields"):
stream_gears.upload(**kwargs)
GOOD = """
import stream_gears
from stream_gears import Credit, Segment, StreamGearsError, UploadLine
segment = Segment()
segment.time = 60
credit: Credit = {"type": 2, "raw_text": "name", "biz_id": "1"}
qrcode: str = stream_gears.login_by_qrcode(stream_gears.get_qrcode())
ok: bool = stream_gears.login_by_cookies("cookies.json")
try:
stream_gears.download("http://x/live.flv", {}, "out", {"time": 60})
stream_gears.download_with_callback("http://x/live.flv", {}, "out", segment, print)
stream_gears.upload(
["a.mp4"], "cookies.json", "title", desc_v2=[credit], line=UploadLine.Cnbldsa
)
except StreamGearsError as e:
print(e)
"""
BAD = """
import stream_gears
a: bool = stream_gears.login_by_qrcode("{}")
stream_gears.upload(["a.mp4"], "cookies.json", title=1)
stream_gears.UploadLine.Nope
stream_gears.download("http://x/live.flv", {}, "out", 60)
stream_gears.upload(["a.mp4"], "cookies.json", "t", desc_v2=[{"raw_text": "x"}])
"""
def run_mypy(source, tmp_path):
pytest.importorskip("mypy")
(tmp_path / "example.py").write_text(textwrap.dedent(source))
return subprocess.run(
[sys.executable, "-m", "mypy", "--strict", "--no-incremental", "example.py"],
cwd=tmp_path,
capture_output=True,
text=True,
timeout=300,
)
def test_stubs_accept_valid_usage(tmp_path):
result = run_mypy(GOOD, tmp_path)
assert result.returncode == 0, result.stdout + result.stderr
def test_stubs_reject_invalid_usage(tmp_path):
result = run_mypy(BAD, tmp_path)
assert result.returncode == 1, result.stdout + result.stderr
lines = {
int(line.split(":")[1])
for line in result.stdout.splitlines()
if ": error:" in line
}
assert lines == {4, 5, 6, 7, 8}, result.stdout
+2
View File
@@ -14,7 +14,9 @@ import pytest
import stream_gears
EXPORTS = {
"Credit",
"PySegment",
"Segment",
"StreamGearsError",
"UploadLine",
"config_bindings",
+7 -1
View File
@@ -8,7 +8,13 @@ features = ["pyo3/extension-module"]
# Include additional files
#include = [ "biliup/web/public/**/*" ]
#data = "biliup/web/public"
python-packages = ["biliup"]
# Mixed layout: `crates/stream-gears/stream_gears/` (`__init__.py`, stubs, `py.typed`)
# ships alongside the native module, which becomes `stream_gears.stream_gears`.
python-source = "crates/stream-gears"
module-name = "stream_gears.stream_gears"
# `python-packages` is resolved under `python-source`, so the root `biliup/`
# package goes in through `include` instead.
include = [{ path = "biliup/**/*.py", format = ["sdist", "wheel"] }]
manifest-path = "crates/stream-gears/Cargo.toml"
[project]