diff --git a/.github/workflows/README.md b/.github/workflows/README.md index f64f95c..f88495c 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 - 单元测试 + 集成测试 **测试步骤**: diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index bb67d3c..8f629da 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'] + python-version: ['3.11'] env: HF_TOKEN: ${{ secrets.HF_TOKEN }} @@ -67,16 +68,26 @@ jobs: with: python-version: ${{ matrix.python-version }} cache: 'pip' + cache-dependency-path: 'pyproject.toml' - name: Install system dependencies run: | sudo apt-get update sudo apt-get install -y build-essential - - name: Install Python dependencies + - name: Install CI test dependencies run: | python -m pip install --upgrade pip - pip install -e ".[dev]" + python -m pip install --no-cache-dir -r requirements-ci.txt + + - name: Verify key dependencies + run: | + python -c "from jose import JWTError, jwt; print('jose OK')" + python -c "from passlib.context import CryptContext; print('passlib OK')" + python -c "import fastapi; print('fastapi OK')" + + - name: Install package (best-effort, private deps may be unavailable) + run: python -m pip install --no-cache-dir -e ".[dev]" || true - name: Run unit tests if: ${{ github.event.inputs.test_type == 'unit' || github.event.inputs.test_type == 'all' || github.event.inputs.test_type == '' }} @@ -123,11 +134,12 @@ jobs: with: python-version: '3.11' cache: 'pip' + cache-dependency-path: 'pyproject.toml' - name: Install dependencies run: | pip install --upgrade pip - pip install -e ".[dev]" + pip install --no-cache-dir -e ".[dev]" - 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/README.md b/README.md index 81acc0e..65a023c 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) @@ -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+ +python --version # 需要 3.11+ # 检查 SAGE 安装 pip list | grep isage diff --git a/pyproject.toml b/pyproject.toml index 16e8ecf..10ca8c9 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" +requires-python = ">=3.11" authors = [{ name = "IntelliStream Team", email = "shuhao_zhang@hust.edu.cn" }] keywords = [ "sage", @@ -74,15 +75,8 @@ dependencies = [ # SAGE kernel integration (includes sage.middleware.* pipeline layer) "isage-kernel>=0.2.5.0", "isage-vida>=0.1.0", -] - -license = { text = "MIT" } -[project.optional-dependencies] -full = [ - # Continual-learning / coreset selection for tool-use features - "isage-sias>=0.1.0", - # Backend API / auth / config helpers (optional for lightweight studio installs) + # Backend API / auth / config helpers (required by services layer) "aiofiles>=23.0.0", "configparser>=5.3.0", "markdown>=3.4.4,<4.0.0", @@ -92,11 +86,25 @@ full = [ "structlog>=23.0.0", ] +license = { text = "MIT" } + +[project.optional-dependencies] +full = [ + # Continual-learning / coreset selection for tool-use features + "isage-sias>=0.1.0", +] + dev = [ "pytest>=7.4.0", "pytest-asyncio>=0.21.0", - "ruff==0.15.4", + "ruff==0.15.8", "isage-pypi-publisher>=0.2.1.0", + # Auth/backend deps required by unit tests (duplicated from core for CI reliability) + "python-jose[cryptography]>=3.5.0,<4.0.0", + "passlib[argon2]>=1.7.4,<2.0.0", + "aiofiles>=23.0.0", + "pydantic-settings>=2.0.0", + "structlog>=23.0.0", ] [project.urls] @@ -143,7 +151,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"] +target-version = ["py311", "py312"] include = '\.pyi?$' [tool.isort] @@ -187,7 +196,8 @@ filterwarnings = [ [tool.ruff] line-length = 100 -target-version = "py310" +# target-version = "py310" +target-version = "py311" [tool.ruff.lint] select = ["E", "F", "W", "I", "N", "UP"] diff --git a/quickstart.sh b/quickstart.sh index 114a191..326a57e 100755 --- a/quickstart.sh +++ b/quickstart.sh @@ -83,10 +83,13 @@ 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}" +# if python3 -c "import sys; exit(0 if sys.version_info >= (3,10) else 1)" 2>/dev/null; then +if python3 -c "import sys; exit(0 if sys.version_info >= (3,11) else 1)" 2>/dev/null; then + # echo -e " ${GREEN}✓ Python ≥ 3.10${NC}" + echo -e " ${GREEN}✓ Python ≥ 3.11${NC}" else - echo -e " ${RED}✗ Python 3.10+ required (found ${PYTHON_VERSION})${NC}" + # 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/requirements-ci.txt b/requirements-ci.txt new file mode 100644 index 0000000..ea8bffd --- /dev/null +++ b/requirements-ci.txt @@ -0,0 +1,31 @@ +# CI test requirements - public PyPI packages only +# Used by GitHub Actions to bootstrap test environment independently +# of private SAGE packages (isage, isagellm, etc.) + +# Test framework +pytest>=7.4.0 +pytest-asyncio>=0.21.0 + +# Auth (required by services/auth_service.py) +python-jose[cryptography]>=3.5.0,<4.0.0 +passlib[argon2]>=1.7.4,<2.0.0 + +# Backend API +pydantic[email]>=2.10.0,<3.0.0 +pydantic-settings>=2.0.0 +fastapi>=0.115.0,<1.0.0 +starlette>=0.40,<0.53 +uvicorn[standard]>=0.34.0,<1.0.0 +httpx>=0.28.0,<1.0.0 +python-multipart>=0.0.6 +websockets>=11.0 + +# Utilities +aiofiles>=23.0.0 +configparser>=5.3.0 +markdown>=3.4.4,<4.0.0 +structlog>=23.0.0 +jinja2>=3.1.0,<4.0.0 +markupsafe>=2.0.1 +PyYAML>=6.0.1 +packaging>=24.0,<27.0 diff --git a/src/sage/studio/_version.py b/src/sage/studio/_version.py index 57d195e..aa627d9 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.44" __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..675bdfc 100644 --- a/tests/integration/test_studio_cli.py +++ b/tests/integration/test_studio_cli.py @@ -5,14 +5,18 @@ 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 +from sage.studio.cli import register_studio_command + # Test runner runner = CliRunner() +# Build a minimal root CLI app for testing plugin registration. +sage_app = typer.Typer() +register_studio_command(sage_app) + class FakeStudioManager: """Mock StudioManager for testing."""