diff --git a/.github/workflows/README.md b/.github/workflows/README.md index f64f95c..da38b2e 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -14,7 +14,8 @@ Studio 的持续集成和持续部署工作流。 - 手动触发 (workflow_dispatch) **测试矩阵**: -- Python 3.10, 3.11 + +- Python 3.11 - 单元测试 + 集成测试 **测试步骤**: @@ -22,6 +23,12 @@ Studio 的持续集成和持续部署工作流。 #### Job: test 运行快速单元测试和集成测试(不需要 LLM 引擎) +安装依赖使用: + +```bash +pip install -e ".[dev,full]" +``` + ```bash # 单元测试 pytest tests/unit/ -v -m "not slow" diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index bb67d3c..df9ebc8 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -49,7 +49,8 @@ jobs: strategy: matrix: - python-version: ['3.10', '3.11'] + # python-version: ['3.10', '3.11'] # deprecated: 3.10 support removed + python-version: ['3.11'] env: HF_TOKEN: ${{ secrets.HF_TOKEN }} @@ -76,7 +77,7 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip - pip install -e ".[dev]" + pip install -e ".[dev,full]" - name: Run unit tests if: ${{ github.event.inputs.test_type == 'unit' || github.event.inputs.test_type == 'all' || github.event.inputs.test_type == '' }} @@ -127,7 +128,7 @@ jobs: - name: Install dependencies run: | pip install --upgrade pip - pip install -e ".[dev]" + pip install -e ".[dev,full]" - name: Run LLM 集成测试 (CPU backend) run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d12694..7adac8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,8 @@ Source: `https://pypi.org/pypi/isage-studio/json` (checked on 2026-02-14, UTC). ### Added - CI/CD workflow for automated testing (`.github/workflows/ci-test.yml`) - - Unit tests on Python 3.10 and 3.11 + + - Unit tests on Python 3.11 - Integration tests with CPU backend - E2E tests for LLM integration - Code quality checks (Ruff, Mypy) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8bddd79..a1c46cc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ cd sage-studio 2. **Install dependencies**: ```bash -pip install -e . +pip install -e ".[dev,full]" ``` 3. **Install SAGE core dependencies** (if needed): diff --git a/README.md b/README.md index 81acc0e..313b8d4 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,8 @@ Studio 采用**前后端分离**架构,直接接入 SAGE 核心引擎: ### Environment Requirements -- **Python**: 3.10+ (必需) + +- **Python**: 3.11+ (必需) - **Node.js**: 18+ (推荐 LTS) - **SAGE**: 完整安装 (包括 kernel, middleware, libs) @@ -97,7 +98,7 @@ pip install isage # 自动包含 isage-studio # 方式 2: 开发模式安装 cd sage-studio -pip install -e ".[dev]" +pip install -e ".[dev,full]" # 安装前端依赖 cd src/sage/studio/frontend @@ -525,7 +526,8 @@ React 18.2 + TypeScript 5.2 #### 后端 ``` -FastAPI + Python 3.10+ + +FastAPI + Python 3.11+ ├── Pydantic 2.0 # 数据验证 ├── Uvicorn # ASGI 服务器 ├── sage-kernel # Environment, DataStream API @@ -870,7 +872,8 @@ sage studio start --dev ```bash # 检查 Python 版本 -python --version # 需要 3.10+ +# python --version # 需要 3.10+ [deprecated] +python --version # 需要 3.11+ # 检查 SAGE 安装 pip list | grep isage diff --git a/pyproject.toml b/pyproject.toml index 16e8ecf..46951b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,8 @@ name = "isage-studio" dynamic = ["version"] description = "SAGE Studio - Visual workflow builder and LLM playground for SAGE AI pipelines" readme = "README.md" -requires-python = ">=3.10" +# requires-python = ">=3.10" # deprecated: use 3.11+ +requires-python = ">=3.11" authors = [{ name = "IntelliStream Team", email = "shuhao_zhang@hust.edu.cn" }] keywords = [ "sage", @@ -143,7 +144,8 @@ version = { attr = "sage.studio._version.__version__" } # Development tools configuration [tool.black] line-length = 100 -target-version = ["py310", "py311", "py312"] +# target-version = ["py310", "py311", "py312"] # deprecated: py310 removed +target-version = ["py311", "py312"] include = '\.pyi?$' [tool.isort] @@ -187,7 +189,8 @@ filterwarnings = [ [tool.ruff] line-length = 100 -target-version = "py310" +# target-version = "py310" # deprecated: use py311 +target-version = "py311" [tool.ruff.lint] select = ["E", "F", "W", "I", "N", "UP"] diff --git a/quickstart.sh b/quickstart.sh index 114a191..48aff11 100755 --- a/quickstart.sh +++ b/quickstart.sh @@ -2,14 +2,14 @@ # quickstart.sh — sage-studio dev environment setup # # Usage: -# ./quickstart.sh # dev mode (default): hooks + .[dev] (includes [full]) +# ./quickstart.sh # dev mode (default): hooks + .[dev,full] # ./quickstart.sh --full # optional backends only: .[full] # ./quickstart.sh --standard # core deps only: no extras # ./quickstart.sh --yes # non-interactive (assume yes) # ./quickstart.sh --doctor # diagnose environment issues # # Install matrix: -# (default / --dev) pip install -e .[dev] ← includes [full] via self-ref +# (default / --dev) pip install -e .[dev,full] # --full pip install -e .[full] # --standard pip install -e . # @@ -29,7 +29,7 @@ BOLD='\033[1m' NC='\033[0m' # ─── Arguments ──────────────────────────────────────────────────────────────── -EXTRAS="[dev]" # default — dev includes [full] via pyproject self-reference +EXTRAS="[dev,full]" # default — install both dev and full extras DOCTOR=false YES=false for arg in "$@"; do @@ -37,7 +37,7 @@ for arg in "$@"; do --doctor) DOCTOR=true ;; --standard) EXTRAS="" ;; --full) EXTRAS="[full]" ;; - --dev) EXTRAS="[dev]" ;; + --dev) EXTRAS="[dev,full]" ;; --yes|-y) YES=true ;; esac done @@ -83,10 +83,19 @@ fi echo -e "${YELLOW}${BOLD}Step 1/3: Checking Python environment${NC}" PYTHON_VERSION=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null || echo "unknown") echo -e " Python version: ${CYAN}${PYTHON_VERSION}${NC}" -if python3 -c "import sys; exit(0 if sys.version_info >= (3,10) else 1)" 2>/dev/null; then - echo -e " ${GREEN}✓ Python ≥ 3.10${NC}" +# DEPRECATED: Python 3.10 support +# if python3 -c "import sys; exit(0 if sys.version_info >= (3,10) else 1)" 2>/dev/null; then +# echo -e " ${GREEN}✓ Python ≥ 3.10${NC}" +# else +# echo -e " ${RED}✗ Python 3.10+ required (found ${PYTHON_VERSION})${NC}" +# exit 1 +# fi + +# ACTIVE: Python 3.11+ required +if python3 -c "import sys; exit(0 if sys.version_info >= (3,11) else 1)" 2>/dev/null; then + echo -e " ${GREEN}✓ Python ≥ 3.11${NC}" else - echo -e " ${RED}✗ Python 3.10+ required (found ${PYTHON_VERSION})${NC}" + echo -e " ${RED}✗ Python 3.11+ required (found ${PYTHON_VERSION})${NC}" exit 1 fi echo "" diff --git a/src/sage/studio/_version.py b/src/sage/studio/_version.py index 57d195e..fc6e1fe 100644 --- a/src/sage/studio/_version.py +++ b/src/sage/studio/_version.py @@ -1,6 +1,6 @@ """Version information for sage-studio package.""" # 独立硬编码版本 -__version__ = "0.2.4.37" +__version__ = "0.2.4.39" __author__ = "IntelliStream Team" __email__ = "shuhao_zhang@hust.edu.cn" diff --git a/src/sage/studio/api/auth.py b/src/sage/studio/api/auth.py index 000ed8b..5ee77ae 100644 --- a/src/sage/studio/api/auth.py +++ b/src/sage/studio/api/auth.py @@ -7,7 +7,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from fastapi import APIRouter from pydantic import BaseModel @@ -26,7 +26,7 @@ class TokenResponse(BaseModel): def _now_iso() -> str: - return datetime.now(timezone.utc).isoformat() + return datetime.now(UTC).isoformat() def build_auth_router() -> APIRouter: diff --git a/src/sage/studio/api/canvas.py b/src/sage/studio/api/canvas.py index 49a2135..59a33d6 100644 --- a/src/sage/studio/api/canvas.py +++ b/src/sage/studio/api/canvas.py @@ -1,7 +1,7 @@ from __future__ import annotations import json -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path from typing import Any @@ -144,8 +144,8 @@ async def publish_canvas_graph_route( "metadata": req.metadata, "params_schema": {}, "dataset_requirements": {}, - "created_at": datetime.now(timezone.utc).isoformat(), - "updated_at": datetime.now(timezone.utc).isoformat(), + "created_at": datetime.now(UTC).isoformat(), + "updated_at": datetime.now(UTC).isoformat(), }, compiled_payload=compiled_payload, ) @@ -184,7 +184,7 @@ async def export_flow(flow_id: str): export_data = { "version": "1.0.0", - "exportTime": str(datetime.now(timezone.utc)), + "exportTime": str(datetime.now(UTC)), "flowId": flow_id, "flow": flow_data, } diff --git a/src/sage/studio/contracts/models.py b/src/sage/studio/contracts/models.py index af85035..f8ce4a6 100644 --- a/src/sage/studio/contracts/models.py +++ b/src/sage/studio/contracts/models.py @@ -1,7 +1,7 @@ from __future__ import annotations -from datetime import datetime, timezone -from enum import Enum +from datetime import UTC, datetime +from enum import StrEnum from typing import Any from pydantic import BaseModel, ConfigDict, Field @@ -9,13 +9,13 @@ CONTRACT_SCHEMA_VERSION = "v1" -class RunKind(str, Enum): +class RunKind(StrEnum): CHAT = "chat" EXPERIMENT = "experiment" SWARM = "swarm" -class StageEventState(str, Enum): +class StageEventState(StrEnum): CREATED = "created" QUEUED = "queued" RUNNING = "running" @@ -24,7 +24,7 @@ class StageEventState(str, Enum): CANCELLED = "cancelled" -class ArtifactKind(str, Enum): +class ArtifactKind(StrEnum): INPUT = "input" OUTPUT = "output" LOG = "log" @@ -39,7 +39,7 @@ class RunRef(BaseModel): request_id: str workspace_id: str kind: RunKind - created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) class StageEvent(BaseModel): @@ -50,7 +50,7 @@ class StageEvent(BaseModel): request_id: str stage: str state: StageEventState - timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC)) message: str | None = None metrics: dict[str, Any] | None = None diff --git a/src/sage/studio/runtime/endpoints/contracts.py b/src/sage/studio/runtime/endpoints/contracts.py index b386459..6cec62a 100644 --- a/src/sage/studio/runtime/endpoints/contracts.py +++ b/src/sage/studio/runtime/endpoints/contracts.py @@ -1,11 +1,11 @@ from __future__ import annotations from dataclasses import dataclass, field -from datetime import datetime, timezone -from enum import Enum +from datetime import UTC, datetime +from enum import StrEnum -class EndpointProvider(str, Enum): +class EndpointProvider(StrEnum): ALIBABA_DASHSCOPE = "alibaba_dashscope" OPENAI = "openai" ANTHROPIC = "anthropic" @@ -39,8 +39,8 @@ class ManagedEndpoint: is_default: bool = False extra_headers: tuple[tuple[str, str], ...] = () api_key: str | None = None - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @dataclass(slots=True, frozen=True) diff --git a/src/sage/studio/runtime/endpoints/registry.py b/src/sage/studio/runtime/endpoints/registry.py index d223d85..88c7bb9 100644 --- a/src/sage/studio/runtime/endpoints/registry.py +++ b/src/sage/studio/runtime/endpoints/registry.py @@ -2,7 +2,7 @@ import threading from dataclasses import replace -from datetime import datetime, timezone +from datetime import UTC, datetime from sage.studio.runtime.endpoints.contracts import ( PROVIDER_PRESETS, @@ -59,7 +59,7 @@ def update_endpoint(self, endpoint_id: str, command: EndpointUpdate) -> ManagedE if current is None: raise KeyError(endpoint_id) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) updated = replace( current, display_name=command.display_name @@ -92,7 +92,7 @@ def set_enabled(self, endpoint_id: str, enabled: bool) -> ManagedEndpoint: current = self._records.get(endpoint_id) if current is None: raise KeyError(endpoint_id) - updated = replace(current, enabled=enabled, updated_at=datetime.now(timezone.utc)) + updated = replace(current, enabled=enabled, updated_at=datetime.now(UTC)) self._records[endpoint_id] = updated return updated @@ -140,7 +140,7 @@ def reset(self) -> None: self._records.clear() def _set_default_locked(self, endpoint_id: str) -> None: - now = datetime.now(timezone.utc) + now = datetime.now(UTC) for record_id, record in list(self._records.items()): self._records[record_id] = replace( record, diff --git a/src/sage/studio/services/auth_service.py b/src/sage/studio/services/auth_service.py index 5d79ac0..851dc9c 100644 --- a/src/sage/studio/services/auth_service.py +++ b/src/sage/studio/services/auth_service.py @@ -1,6 +1,6 @@ import os import sqlite3 -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from jose import JWTError, jwt from passlib.context import CryptContext @@ -82,7 +82,7 @@ def create_user(self, username: str, password: str) -> User: cursor = conn.cursor() cursor.execute( "INSERT INTO users (username, hashed_password, created_at, is_guest) VALUES (?, ?, ?, 0)", - (username, hashed_password, datetime.now(timezone.utc)), + (username, hashed_password, datetime.now(UTC)), ) user_id = cursor.lastrowid conn.commit() @@ -107,7 +107,7 @@ def create_guest_user(self) -> User: cursor = conn.cursor() cursor.execute( "INSERT INTO users (username, hashed_password, created_at, is_guest) VALUES (?, ?, ?, 1)", - (username, hashed_password, datetime.now(timezone.utc)), + (username, hashed_password, datetime.now(UTC)), ) user_id = cursor.lastrowid conn.commit() @@ -145,9 +145,9 @@ def get_user(self, username: str) -> UserInDB | None: def create_access_token(self, data: dict, expires_delta: timedelta | None = None) -> str: to_encode = data.copy() if expires_delta: - expire = datetime.now(timezone.utc) + expires_delta + expire = datetime.now(UTC) + expires_delta else: - expire = datetime.now(timezone.utc) + timedelta(minutes=15) + expire = datetime.now(UTC) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/tests/integration/test_studio_cli.py b/tests/integration/test_studio_cli.py index 4f655a0..7870fba 100644 --- a/tests/integration/test_studio_cli.py +++ b/tests/integration/test_studio_cli.py @@ -5,15 +5,24 @@ from unittest.mock import patch import pytest - -# Import from sage-cli (which hosts the studio command) -from sage.cli.main import app as sage_app +import typer from typer.testing import CliRunner +# Import Studio plugin surfaces directly; SAGE core CLI is argparse-based now. +from sage.studio.cli import register_studio_command + # Test runner runner = CliRunner() +@pytest.fixture +def sage_app() -> typer.Typer: + """Build a root Typer app and register the Studio plugin commands.""" + app = typer.Typer() + register_studio_command(app) + return app + + class FakeStudioManager: """Mock StudioManager for testing.""" @@ -84,7 +93,7 @@ def mock_studio_manager(): return FakeStudioManager() -def test_studio_start_command(mock_studio_manager): +def test_studio_start_command(mock_studio_manager, sage_app): """Test that 'sage studio start' command works.""" # Patch at the import location before CLI invocation with patch("sage.studio.cli.studio_manager", mock_studio_manager): @@ -105,14 +114,14 @@ def test_studio_start_command(mock_studio_manager): assert mock_studio_manager._config["host"] == "127.0.0.1" -def test_studio_status_command(mock_studio_manager): +def test_studio_status_command(mock_studio_manager, sage_app): """Test that 'sage studio status' command works.""" with patch("sage.studio.cli.studio_manager", mock_studio_manager): result = runner.invoke(sage_app, ["studio", "status"]) assert result.exit_code == 0 -def test_studio_stop_command(mock_studio_manager): +def test_studio_stop_command(mock_studio_manager, sage_app): """Test that 'sage studio stop' command works.""" # Start the manager first mock_studio_manager._running = True @@ -124,28 +133,28 @@ def test_studio_stop_command(mock_studio_manager): assert mock_studio_manager._running is False -def test_studio_install_command(mock_studio_manager): +def test_studio_install_command(mock_studio_manager, sage_app): """Test that 'sage studio install' command works.""" with patch("sage.studio.cli.studio_manager", mock_studio_manager): result = runner.invoke(sage_app, ["studio", "install"]) assert result.exit_code == 0 -def test_studio_build_command(mock_studio_manager): +def test_studio_build_command(mock_studio_manager, sage_app): """Test that 'sage studio build' command works.""" with patch("sage.studio.cli.studio_manager", mock_studio_manager): result = runner.invoke(sage_app, ["studio", "build"]) assert result.exit_code == 0 -def test_studio_help_command(): +def test_studio_help_command(sage_app): """Test that 'sage studio --help' command works.""" result = runner.invoke(sage_app, ["studio", "--help"]) assert result.exit_code == 0 assert "Studio" in result.stdout or "studio" in result.stdout -def test_studio_npm_command(mock_studio_manager): +def test_studio_npm_command(mock_studio_manager, sage_app): """Test that 'sage studio npm install' command works.""" with patch("sage.studio.cli.studio_manager", mock_studio_manager): result = runner.invoke(sage_app, ["studio", "npm", "install"])