bump version to 0.2.7
Release / Pre-release Check (push) Failing after 35s
Release / Build Artifacts (push) Has been skipped
Release / Publish to PyPI (push) Has been skipped
Release / Publish Release (push) Has been skipped

This commit is contained in:
2026-06-27 15:23:48 +08:00
parent 1e23c48efc
commit 2b3f4b82d3
10 changed files with 7736 additions and 298 deletions
+4 -2
View File
@@ -6,6 +6,7 @@ classifiers = [
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Application Frameworks",
@@ -20,7 +21,7 @@ license = { text = "MIT" }
name = "pyflowx" name = "pyflowx"
readme = "README.md" readme = "README.md"
requires-python = ">=3.8" requires-python = ">=3.8"
version = "0.2.6" version = "0.2.7"
[project.scripts] [project.scripts]
autofmt = "pyflowx.cli.autofmt:main" autofmt = "pyflowx.cli.autofmt:main"
@@ -65,6 +66,7 @@ dev = [
"tox-uv>=1.13.1", "tox-uv>=1.13.1",
"tox>=4.25.0", "tox>=4.25.0",
] ]
llm = ["sglang[all]==0.5.10rc0; python_version >= '3.10'"]
office = [ office = [
"pillow>=10.4.0", "pillow>=10.4.0",
"pymupdf>=1.24.11", "pymupdf>=1.24.11",
@@ -90,7 +92,7 @@ packages = ["src/pyflowx"]
pyflowx = { workspace = true } pyflowx = { workspace = true }
[dependency-groups] [dependency-groups]
dev = ["pyflowx[dev,office]"] dev = ["pyflowx[dev,office,llm]"]
[tool.coverage.run] [tool.coverage.run]
branch = true branch = true
+1 -1
View File
@@ -95,7 +95,7 @@ from .task import (
task_template, task_template,
) )
__version__ = "0.3.0" __version__ = "0.3.1"
__all__ = [ __all__ = [
"IS_LINUX", "IS_LINUX",
+21 -13
View File
@@ -4,19 +4,23 @@ import argparse
from pathlib import Path from pathlib import Path
import pyflowx as px import pyflowx as px
from pyflowx.conditions import BuiltinConditions from pyflowx.conditions import BuiltinConditions, Constants
def main(): def main():
parser = argparse.ArgumentParser(description="Run a local model using SGLang.") parser = argparse.ArgumentParser(description="启动 SGLang 服务")
parser.add_argument("name", help="Model name.") parser.add_argument("--model", default="~/.models/Qwen2.5-Coder-32B-Instruct-AWQ", help="模型路径")
parser.add_argument("--dir", default=None, help="Directory of model.") parser.add_argument("--port", type=int, default=8000, help="服务端口")
parser.add_argument("--ctx-len", type=int, default=28672, help="最大上下文长度")
parser.add_argument("--mem", type=float, default=0.75, help="显存占比 (0-1)")
parser.add_argument("--host", default="0.0.0.0", help="主机地址")
parser.add_argument("--log-level", default="info", help="日志级别")
args = parser.parse_args() args = parser.parse_args()
if not args.name: if not args.model:
parser.error("name is required") parser.error("model is required")
model_dir = Path(args.dir) if args.dir else Path.home() / ".models" / args.name.split("/")[-1] model_dir = Path(args.model).expanduser()
if not model_dir.exists(): if not model_dir.exists():
parser.error(f"Model directory {model_dir} does not exist.") parser.error(f"Model directory {model_dir} does not exist.")
@@ -34,22 +38,26 @@ def main():
px.TaskSpec( px.TaskSpec(
name="run", name="run",
cmd=[ cmd=[
"uvx", "python" if Constants.IS_WINDOWS else "python3",
"sglang", "-m",
"serve", "sglang.launch_server",
"--model-path", "--model-path",
str(model_dir), str(model_dir),
"--host", "--host",
"0.0.0.0", str(args.host),
"--port", "--port",
"8000", "8000",
"--mem-fraction-static", "--mem-fraction-static",
"0.88", str(args.mem),
"--context-length", "--context-length",
"32768", "32768",
"--tool-call-parser",
"qwen",
"--log-level",
str(args.log_level),
], ],
verbose=True, verbose=True,
), ),
]) ])
px.run(graph, verbose=True) px.run(graph, strategy="sequential", verbose=True)
+2
View File
@@ -134,7 +134,9 @@ def _evaluate_conditions(spec: TaskSpec[Any], context: Mapping[str, Any]) -> str
failed_conditions.append(getattr(condition, "__name__", None) or "匿名条件") failed_conditions.append(getattr(condition, "__name__", None) or "匿名条件")
if failed_conditions: if failed_conditions:
if len(failed_conditions) <= 2:
return f"条件不满足: {', '.join(failed_conditions)}" return f"条件不满足: {', '.join(failed_conditions)}"
return f"条件不满足: {', '.join(failed_conditions[:2])}{len(failed_conditions)}个条件"
if spec.skip_if_missing and not spec._is_cmd_available(): if spec.skip_if_missing and not spec._is_cmd_available():
cmd_name = spec.cmd[0] if isinstance(spec.cmd, list) and spec.cmd else "unknown" cmd_name = spec.cmd[0] if isinstance(spec.cmd, list) and spec.cmd else "unknown"
+33 -35
View File
@@ -35,8 +35,6 @@ from typing import (
Iterator, Iterator,
List, List,
Mapping, Mapping,
Optional,
Tuple,
Union, Union,
cast, cast,
) )
@@ -107,7 +105,7 @@ class RetryPolicy:
delay: float = 0.0 delay: float = 0.0
backoff: float = 1.0 backoff: float = 1.0
jitter: float = 0.0 jitter: float = 0.0
retry_on: Tuple[type[BaseException], ...] = (Exception,) retry_on: tuple[type[BaseException], ...] = (Exception,)
def __post_init__(self) -> None: def __post_init__(self) -> None:
if self.max_attempts < 1: if self.max_attempts < 1:
@@ -151,9 +149,9 @@ class TaskHooks:
钩子异常不会影响任务状态,仅记录日志。 钩子异常不会影响任务状态,仅记录日志。
""" """
pre_run: Optional[Callable[[TaskSpec[Any]], None]] = None pre_run: Callable[[TaskSpec[Any]], None] | None = None
post_run: Optional[Callable[[TaskSpec[Any], Any], None]] = None post_run: Callable[[TaskSpec[Any], Any], None] | None = None
on_failure: Optional[Callable[[TaskSpec[Any], BaseException], None]] = None on_failure: Callable[[TaskSpec[Any], BaseException], None] | None = None
class TaskStatus(Enum): class TaskStatus(Enum):
@@ -248,27 +246,27 @@ class TaskSpec(Generic[T]):
""" """
name: str name: str
fn: Optional[TaskFn[T]] = None fn: TaskFn[T] | None = None
cmd: Optional[TaskCmd] = None cmd: TaskCmd | None = None
depends_on: Tuple[str, ...] = () depends_on: tuple[str, ...] = ()
soft_depends_on: Tuple[str, ...] = () soft_depends_on: tuple[str, ...] = ()
defaults: Mapping[str, Any] = field(default_factory=dict) defaults: Mapping[str, Any] = field(default_factory=dict)
args: Tuple[Any, ...] = () args: tuple[Any, ...] = ()
kwargs: Mapping[str, Any] = field(default_factory=dict) kwargs: Mapping[str, Any] = field(default_factory=dict)
retry: RetryPolicy = field(default_factory=RetryPolicy) retry: RetryPolicy = field(default_factory=RetryPolicy)
timeout: Optional[float] = None timeout: float | None = None
tags: Tuple[str, ...] = () tags: tuple[str, ...] = ()
conditions: Tuple[Condition, ...] = () conditions: tuple[Condition, ...] = ()
cwd: Optional[Path] = None cwd: Path | None = None
env: Optional[Mapping[str, str]] = None env: Mapping[str, str] | None = None
verbose: bool = False verbose: bool = False
skip_if_missing: bool = False skip_if_missing: bool = False
allow_upstream_skip: bool = False allow_upstream_skip: bool = False
strategy: Optional[str] = None strategy: str | None = None
priority: int = 0 priority: int = 0
concurrency_key: Optional[str] = None concurrency_key: str | None = None
continue_on_error: bool = False continue_on_error: bool = False
cache_key: Optional[CacheKeyFn] = None cache_key: CacheKeyFn | None = None
hooks: TaskHooks = field(default_factory=TaskHooks) hooks: TaskHooks = field(default_factory=TaskHooks)
def __post_init__(self) -> None: def __post_init__(self) -> None:
@@ -310,7 +308,7 @@ class TaskSpec(Generic[T]):
_run.__name__ = spec.name _run.__name__ = spec.name
return _run # type: ignore[return-value] return _run # type: ignore[return-value]
def should_execute(self, context: Context) -> Tuple[bool, Optional[str]]: def should_execute(self, context: Context) -> tuple[bool, str | None]:
"""检查任务是否应执行。 """检查任务是否应执行。
Returns Returns
@@ -367,12 +365,12 @@ class TaskSpec(Generic[T]):
@contextmanager @contextmanager
def _env_and_cwd( def _env_and_cwd(
env: Optional[Mapping[str, str]], env: Mapping[str, str] | None,
cwd: Optional[Path], cwd: Path | None,
) -> Iterator[None]: ) -> Iterator[None]:
"""临时设置环境变量与工作目录。""" """临时设置环境变量与工作目录。"""
saved_env: dict[str, str] = {} saved_env: dict[str, str] = {}
saved_cwd: Optional[str] = None saved_cwd: str | None = None
if env: if env:
for k, v in env.items(): for k, v in env.items():
if k in os.environ: if k in os.environ:
@@ -431,7 +429,7 @@ def _run_command(spec: TaskSpec[Any]) -> Any: # noqa: PLR0912
print(f"[verbose] 工作目录: {cwd}", flush=True) print(f"[verbose] 工作目录: {cwd}", flush=True)
# 合并环境变量 # 合并环境变量
run_env: Optional[dict[str, str]] = None run_env: dict[str, str] | None = None
if env_override: if env_override:
run_env = dict(os.environ) run_env = dict(os.environ)
run_env.update(env_override) run_env.update(env_override)
@@ -470,8 +468,8 @@ def _run_command(spec: TaskSpec[Any]) -> Any: # noqa: PLR0912
# 任务模板:批量生成相似 TaskSpec 的工厂 # 任务模板:批量生成相似 TaskSpec 的工厂
# ---------------------------------------------------------------------- # # ---------------------------------------------------------------------- #
def task_template( def task_template(
fn: Optional[TaskFn[Any]] = None, fn: TaskFn[Any] | None = None,
cmd: Optional[TaskCmd] = None, cmd: TaskCmd | None = None,
**defaults: Any, **defaults: Any,
) -> Callable[..., TaskSpec[Any]]: ) -> Callable[..., TaskSpec[Any]]:
"""创建任务模板工厂。 """创建任务模板工厂。
@@ -505,15 +503,15 @@ class TaskResult(Generic[T]):
spec: TaskSpec[T] spec: TaskSpec[T]
status: TaskStatus = TaskStatus.PENDING status: TaskStatus = TaskStatus.PENDING
value: Optional[T] = None value: T | None = None
error: Optional[BaseException] = None error: BaseException | None = None
attempts: int = 0 attempts: int = 0
started_at: Optional[datetime] = None started_at: datetime | None = None
finished_at: Optional[datetime] = None finished_at: datetime | None = None
reason: Optional[str] = None # 跳过原因 reason: str | None = None # 跳过原因
@property @property
def duration(self) -> Optional[float]: def duration(self) -> float | None:
"""从开始到结束的耗时(秒),未开始/未结束则为 ``None``。""" """从开始到结束的耗时(秒),未开始/未结束则为 ``None``。"""
if self.started_at is None or self.finished_at is None: if self.started_at is None or self.finished_at is None:
return None return None
@@ -527,6 +525,6 @@ class TaskEvent:
task: str task: str
status: TaskStatus status: TaskStatus
attempts: int = 0 attempts: int = 0
error: Optional[str] = None error: str | None = None
duration: Optional[float] = None duration: float | None = None
reason: Optional[str] = None reason: str | None = None
+1
View File
@@ -238,6 +238,7 @@ class TestPdfInfo:
class TestPdfOcr: class TestPdfOcr:
"""Test pdf_ocr function.""" """Test pdf_ocr function."""
@pytest.mark.slow
def test_pdf_ocr_file(self, tmp_path: Path) -> None: def test_pdf_ocr_file(self, tmp_path: Path) -> None:
"""Should OCR PDF.""" """Should OCR PDF."""
pytest.importorskip("fitz") pytest.importorskip("fitz")
+1
View File
@@ -529,6 +529,7 @@ class TestDependencyDrivenScheduling:
class TestConcurrencyLimits: class TestConcurrencyLimits:
"""测试并发限制:相同 concurrency_key 的任务串行执行。""" """测试并发限制:相同 concurrency_key 的任务串行执行。"""
@pytest.mark.slow
def test_concurrency_key_serializes_tasks(self) -> None: def test_concurrency_key_serializes_tasks(self) -> None:
"""相同 key 的任务不应并发执行。""" """相同 key 的任务不应并发执行。"""
running: list[int] = [] running: list[int] = []
+1
View File
@@ -264,6 +264,7 @@ def test_skip_if_missing_with_fn_not_checked():
assert spec.should_execute({})[0] is True assert spec.should_execute({})[0] is True
@pytest.mark.slow
def test_skip_if_missing_with_empty_cmd_list(): def test_skip_if_missing_with_empty_cmd_list():
"""skip_if_missing=True 时,空命令列表应返回 True(不检查).""" """skip_if_missing=True 时,空命令列表应返回 True(不检查)."""
spec = TaskSpec("test", cmd=[""], skip_if_missing=True) spec = TaskSpec("test", cmd=[""], skip_if_missing=True)
+1 -1
View File
@@ -1,6 +1,6 @@
[tox] [tox]
isolated_build = true isolated_build = true
envlist = py38, py39, py310, py311, py312, py313 envlist = py38, py39, py310, py311, py312, py313, py314
min_version = 4.0 min_version = 4.0
requires = tox-uv requires = tox-uv
skipsdist = true skipsdist = true
Generated
+7670 -245
View File
File diff suppressed because it is too large Load Diff