diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 61ca40bf..d1c662bc 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,11 +1,11 @@ [bumpversion] -current_version = 0.3.8 +current_version = 0.0.1 commit = True tag = True -[bumpversion:file:setup.py] -search = version="{current_version}" -replace = version="{new_version}" +[bumpversion:file:pyproject.toml] +search = version = "{current_version}" +replace = version = "{new_version}" [bumpversion:file:stagehand/__init__.py] search = __version__ = "{current_version}" diff --git a/.github/README_PUBLISHING.md b/.github/README_PUBLISHING.md index 2b4ec69f..ae5d5211 100644 --- a/.github/README_PUBLISHING.md +++ b/.github/README_PUBLISHING.md @@ -53,21 +53,7 @@ This project uses Ruff for linting and formatting. The workflow enforces these s To run the same checks locally: ```bash -# Install Ruff -pip install ruff - -# Run linting -ruff check . - -# Check formatting -ruff format --check . - -# Auto-fix issues where possible -ruff check --fix . -ruff format . - -# Use Black to format the code -black . +./format.sh ``` ## Troubleshooting diff --git a/.github/pull_request_template b/.github/pull_request_template new file mode 100644 index 00000000..cd3a8bd7 --- /dev/null +++ b/.github/pull_request_template @@ -0,0 +1,5 @@ +# why + +# what changed + +# test plan diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml new file mode 100644 index 00000000..f20977b5 --- /dev/null +++ b/.github/workflows/linting.yml @@ -0,0 +1,36 @@ +name: Format Check + +on: + pull_request: + branches: [ main, master ] + paths: + - '**.py' + - 'stagehand/**' + - 'pyproject.toml' + +jobs: + format-check: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install formatting dependencies + run: | + python -m pip install --upgrade pip + pip install black ruff + + - name: Check Black formatting + run: | + echo "Checking Black formatting..." + black --check --diff stagehand + + - name: Run Ruff linting + run: | + echo "Running Ruff linting..." + ruff check stagehand \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ae0a9c12..b6d7d589 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,7 +35,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install build twine wheel setuptools bumpversion ruff + pip install build twine wheel setuptools ruff pip install -r requirements.txt if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi @@ -51,11 +51,59 @@ jobs: run: | pytest - - name: Update version + - name: Calculate new version + id: version + run: | + # Get current version from pyproject.toml + CURRENT_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") + echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + + # Parse version components + IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" + + # Calculate new version based on release type + case "${{ github.event.inputs.release_type }}" in + "major") + NEW_MAJOR=$((MAJOR + 1)) + NEW_MINOR=0 + NEW_PATCH=0 + ;; + "minor") + NEW_MAJOR=$MAJOR + NEW_MINOR=$((MINOR + 1)) + NEW_PATCH=0 + ;; + "patch") + NEW_MAJOR=$MAJOR + NEW_MINOR=$MINOR + NEW_PATCH=$((PATCH + 1)) + ;; + esac + + NEW_VERSION="${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}" + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "Bumping version from $CURRENT_VERSION to $NEW_VERSION" + + - name: Update version files + run: | + CURRENT_VERSION="${{ steps.version.outputs.current_version }}" + NEW_VERSION="${{ steps.version.outputs.new_version }}" + + # Update pyproject.toml + sed -i "s/version = \"$CURRENT_VERSION\"/version = \"$NEW_VERSION\"/" pyproject.toml + + # Update __init__.py + sed -i "s/__version__ = \"$CURRENT_VERSION\"/__version__ = \"$NEW_VERSION\"/" stagehand/__init__.py + + echo "Updated version to $NEW_VERSION in pyproject.toml and __init__.py" + + - name: Commit version bump run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - bumpversion ${{ github.event.inputs.release_type }} + git add pyproject.toml stagehand/__init__.py + git commit -m "Bump version to ${{ steps.version.outputs.new_version }}" + git tag "v${{ steps.version.outputs.new_version }}" - name: Build package run: | @@ -63,7 +111,7 @@ jobs: - name: Upload to PyPI env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: | twine upload dist/* @@ -77,6 +125,6 @@ jobs: if: ${{ github.event.inputs.create_release == 'true' }} uses: softprops/action-gh-release@v1 with: - tag_name: v$(python setup.py --version) - name: Release v$(python setup.py --version) + tag_name: v${{ steps.version.outputs.new_version }} + name: Release v${{ steps.version.outputs.new_version }} generate_release_notes: true \ No newline at end of file diff --git a/format.sh b/format.sh index fb408fd1..b0451ae0 100755 --- a/format.sh +++ b/format.sh @@ -1,18 +1,14 @@ #!/bin/bash # Define source directories (adjust as needed) -SOURCE_DIRS="evals stagehand" +SOURCE_DIRS="stagehand" -# Apply Black formatting only to source directories +# Apply Black formatting first echo "Applying Black formatting..." black $SOURCE_DIRS -# Fix import sorting (addresses I001 errors) -echo "Sorting imports..." -isort $SOURCE_DIRS - -# Apply Ruff with autofix for remaining issues -echo "Applying Ruff autofixes..." +# Apply Ruff with autofix for all issues (including import sorting) +echo "Applying Ruff autofixes (including import sorting)..." ruff check --fix $SOURCE_DIRS echo "Checking for remaining issues..." diff --git a/pyproject.toml b/pyproject.toml index 6ef4d251..7080ba31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,8 +3,8 @@ requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] -name = "stagehand-py" -version = "0.3.10" +name = "stagehand" +version = "0.0.1" description = "Python SDK for Stagehand" readme = "README.md" license = {text = "MIT"} diff --git a/stagehand/__init__.py b/stagehand/__init__.py index 1d14d2b2..3a40ef94 100644 --- a/stagehand/__init__.py +++ b/stagehand/__init__.py @@ -19,7 +19,7 @@ ) from .utils import configure_logging -__version__ = "0.3.9" #for pypi "stagehand" +__version__ = "0.0.1" __all__ = [ "Stagehand", diff --git a/stagehand/client.py b/stagehand/client.py index c6b7ecc8..37b35ed7 100644 --- a/stagehand/client.py +++ b/stagehand/client.py @@ -76,23 +76,23 @@ def __init__( # Start with provided config or default config if config is None: config = default_config - + # Apply any overrides overrides = {} if api_url is not None: # api_url isn't in config, handle separately pass if model_api_key is not None: - # model_api_key isn't in config, handle separately + # model_api_key isn't in config, handle separately pass if session_id is not None: - overrides['browserbase_session_id'] = session_id + overrides["browserbase_session_id"] = session_id if env is not None: - overrides['env'] = env - + overrides["env"] = env + # Add any additional config overrides overrides.update(config_overrides) - + # Create final config with overrides if overrides: self.config = config.with_overrides(**overrides) @@ -102,10 +102,14 @@ def __init__( # Handle non-config parameters self.api_url = api_url or os.getenv("STAGEHAND_API_URL") self.model_api_key = model_api_key or os.getenv("MODEL_API_KEY") - + # Extract frequently used values from config for convenience - self.browserbase_api_key = self.config.api_key or os.getenv("BROWSERBASE_API_KEY") - self.browserbase_project_id = self.config.project_id or os.getenv("BROWSERBASE_PROJECT_ID") + self.browserbase_api_key = self.config.api_key or os.getenv( + "BROWSERBASE_API_KEY" + ) + self.browserbase_project_id = self.config.project_id or os.getenv( + "BROWSERBASE_PROJECT_ID" + ) self.session_id = self.config.browserbase_session_id self.model_name = self.config.model_name self.dom_settle_timeout_ms = self.config.dom_settle_timeout_ms @@ -114,7 +118,9 @@ def __init__( self.system_prompt = self.config.system_prompt self.verbose = self.config.verbose self.env = self.config.env.upper() if self.config.env else "BROWSERBASE" - self.local_browser_launch_options = self.config.local_browser_launch_options or {} + self.local_browser_launch_options = ( + self.config.local_browser_launch_options or {} + ) # Handle model-related settings self.model_client_options = {} diff --git a/stagehand/config.py b/stagehand/config.py index e37fead9..4bbf68ec 100644 --- a/stagehand/config.py +++ b/stagehand/config.py @@ -88,10 +88,10 @@ class StagehandConfig(BaseModel): def with_overrides(self, **overrides) -> "StagehandConfig": """ Create a new config instance with the specified overrides. - + Args: **overrides: Key-value pairs to override in the config - + Returns: StagehandConfig: New config instance with overrides applied """ diff --git a/stagehand/context.py b/stagehand/context.py index 84fb3731..51c0a06c 100644 --- a/stagehand/context.py +++ b/stagehand/context.py @@ -87,6 +87,7 @@ async def wrapped_new_page(*args, **kwargs): return wrapped_new_page elif name == "pages": + async def wrapped_pages(): pw_pages = self._context.pages # Return StagehandPage objects diff --git a/stagehand/handlers/act_handler_utils.py b/stagehand/handlers/act_handler_utils.py index 395376d9..6506ab5f 100644 --- a/stagehand/handlers/act_handler_utils.py +++ b/stagehand/handlers/act_handler_utils.py @@ -444,7 +444,7 @@ async def handle_possible_page_navigation( }, ) - if new_opened_tab and new_opened_tab.url != 'about:blank': + if new_opened_tab and new_opened_tab.url != "about:blank": logger.info( message="new page detected (new tab) with URL", category="action", diff --git a/stagehand/utils.py b/stagehand/utils.py index 4b252cf9..9ef5278d 100644 --- a/stagehand/utils.py +++ b/stagehand/utils.py @@ -680,6 +680,7 @@ def convert_dict_keys_to_camel_case(data: dict[str, Any]) -> dict[str, Any]: return result + def format_simplified_tree(node: AccessibilityNode, level: int = 0) -> str: """Formats a node and its children into a simplified string representation.""" indent = " " * level @@ -1134,6 +1135,8 @@ def inject_url_at_path(obj, segments, id_to_url_mapping): else: # Continue traversing the path inject_url_at_path(obj[key], rest, id_to_url_mapping) + + # Convert any non-serializable objects to plain Python objects def make_serializable(obj): """Recursively convert non-JSON-serializable objects to serializable ones."""