diff --git a/.gitignore b/.gitignore index 8779740..860eff4 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ dist .envrc codegen.log Brewfile.lock.json +screenshot.png +openapi.v1.yaml diff --git a/README.md b/README.md index 2880764..41f865c 100644 --- a/README.md +++ b/README.md @@ -50,17 +50,18 @@ so that your API Key is not stored in source control. See the [examples](examples) directory for more usage examples. +> [!NOTE] +> Running the examples requires [Rye](https://rye.astral.sh/) to be installed. + To run the examples, clone this repository and run the following commands from the project root (this directory): ```bash -python3 -m venv .venv -source .venv/bin/activate -python3 -m pip install . -python3 -m examples/02_create_session.py # replace with the example you want to run +rye sync +rye run example playwright_basic # replace with the example you want to run ``` -!!! note - Make sure you have a `.env` file that matches the [.env.example](.env.example) file in the root of this repository. +> [!NOTE] +> Make sure you have a `.env` file that matches the [.env.example](.env.example) file in the root of this repository. ## Async usage diff --git a/examples/01_quickstart.py b/examples/01_quickstart.py deleted file mode 100644 index 6fb908c..0000000 --- a/examples/01_quickstart.py +++ /dev/null @@ -1,28 +0,0 @@ -import os - -from dotenv import load_dotenv -from playwright.sync_api import Playwright, sync_playwright - -load_dotenv(override=True) -BROWSERBASE_API_KEY = os.environ["BROWSERBASE_API_KEY"] -BROWSERBASE_PROJECT_ID = os.environ["BROWSERBASE_PROJECT_ID"] - - -def run(playwright: Playwright): - print("Api Key: " + os.environ["BROWSERBASE_API_KEY"]) - connect_url = "wss://connect.browserbase.com?apiKey=" + BROWSERBASE_API_KEY - print("connect_url: " + connect_url) - chromium = playwright.chromium - browser = chromium.connect_over_cdp( - "wss://connect.browserbase.com?apiKey=" + BROWSERBASE_API_KEY - ) - context = browser.contexts[0] - page = context.pages[0] - - page.goto("https://www.google.com") - page.screenshot(path="screenshot.png") - print(page.title) - - -with sync_playwright() as playwright: - run(playwright) diff --git a/examples/02_create_session.py b/examples/02_create_session.py deleted file mode 100644 index ec5d3b6..0000000 --- a/examples/02_create_session.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -from pprint import pprint - -from dotenv import load_dotenv -from playwright.sync_api import Playwright, sync_playwright - -from browserbase import Browserbase - -load_dotenv(override=True) -BROWSERBASE_API_KEY = os.environ["BROWSERBASE_API_KEY"] -BROWSERBASE_PROJECT_ID = os.environ["BROWSERBASE_PROJECT_ID"] - - -bb = Browserbase( - api_key=BROWSERBASE_API_KEY -) # can be omitted if in environment variable - -session = bb.sessions.create(project_id=BROWSERBASE_PROJECT_ID) -pprint({"session": session}) - - -def run(playwright: Playwright): - print("Api Key: " + os.environ["BROWSERBASE_API_KEY"]) - connect_url = session.connectUrl - print("connect_url: " + connect_url) - chromium = playwright.chromium - browser = chromium.connect_over_cdp(connect_url) - context = browser.contexts[0] - page = context.pages[0] - - page.goto("https://www.google.com") - page.screenshot(path="screenshot.png") - print(page.title) - - -with sync_playwright() as playwright: - run(playwright) - -update_session_resp = bb.sessions.update( - id=session.id, status="REQUEST_RELEASE", project_id=BROWSERBASE_PROJECT_ID -) -pprint({"update_session_resp": update_session_resp}) - -print("done") diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..baf9883 --- /dev/null +++ b/examples/__init__.py @@ -0,0 +1,24 @@ +import os + +from dotenv import load_dotenv + +from browserbase import Browserbase + +# Load our environment variables +load_dotenv(override=True) + +# Make sure we have the required environment variables +BROWSERBASE_CONNECT_URL = os.environ.get( + "BROWSERBASE_CONNECT_URL", "wss://connect.browserbase.com" +) +_BROWSERBASE_API_KEY = os.environ.get("BROWSERBASE_API_KEY") +if not _BROWSERBASE_API_KEY: + raise ValueError("BROWSERBASE_API_KEY is not set in environment") +BROWSERBASE_API_KEY: str = _BROWSERBASE_API_KEY +_BROWSERBASE_PROJECT_ID = os.environ.get("BROWSERBASE_PROJECT_ID") +if not _BROWSERBASE_PROJECT_ID: + raise ValueError("BROWSERBASE_PROJECT_ID is not set in environment") +BROWSERBASE_PROJECT_ID = _BROWSERBASE_PROJECT_ID or "" + +# Instantiate our Browserbase client +bb = Browserbase(api_key=BROWSERBASE_API_KEY) diff --git a/examples/e2e/README.md b/examples/e2e/README.md new file mode 100644 index 0000000..0443cc8 --- /dev/null +++ b/examples/e2e/README.md @@ -0,0 +1,26 @@ +# End-to-end tests + +This directory contains end-to-end tests that run against a real Browserbase instance. + +## Running the tests + +To run the tests, you will need to set the following environment variables: + +- `BROWSERBASE_API_KEY`: Your Browserbase API key +- `BROWSERBASE_PROJECT_ID`: The ID of the project you want to use for the tests + +You can set these variables in a `.env` file in the root of this directory. + +Then, run the tests with: + +```sh +$ rye run test:e2e +``` + +## Writing tests + +The tests are written using pytest and the [pytest-playwright](https://playwright.dev/python/docs/pytest) plugin. + +You can find more information about writing tests in the [pytest documentation](https://docs.pytest.org/en/7.1.x/). + +To submit a test, create a new file in the `e2e` directory with a name that describes the test and starts with `test_`. diff --git a/examples/e2e/__init__.py b/examples/e2e/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/e2e/__init__.py @@ -0,0 +1 @@ + diff --git a/examples/e2e/test_playwright_basic.py b/examples/e2e/test_playwright_basic.py new file mode 100644 index 0000000..aba94d4 --- /dev/null +++ b/examples/e2e/test_playwright_basic.py @@ -0,0 +1,21 @@ +import pytest +from playwright.sync_api import Playwright, sync_playwright + +from browserbase import Browserbase + +from .. import ( + BROWSERBASE_API_KEY, + playwright_basic, +) + +bb = Browserbase(api_key=BROWSERBASE_API_KEY) + + +@pytest.fixture(scope="session") +def playwright(): + with sync_playwright() as p: + yield p + + +def test_playwright_basic(playwright: Playwright): + playwright_basic.run(playwright) diff --git a/examples/playwright_basic.py b/examples/playwright_basic.py new file mode 100644 index 0000000..e909f9d --- /dev/null +++ b/examples/playwright_basic.py @@ -0,0 +1,41 @@ +from playwright.sync_api import Playwright, sync_playwright + +from examples import ( + BROWSERBASE_API_KEY, + BROWSERBASE_PROJECT_ID, + BROWSERBASE_CONNECT_URL, + bb, +) + + +def run(playwright: Playwright): + # Create a session on Browserbase + session = bb.sessions.create(project_id=BROWSERBASE_PROJECT_ID) + assert session.id is not None + assert session.status == "RUNNING", f"Session status is {session.status}" + + # Connect to the remote session + connect_url = ( + f"{BROWSERBASE_CONNECT_URL}?sessionId={session.id}&apiKey={BROWSERBASE_API_KEY}" + ) + chromium = playwright.chromium + browser = chromium.connect_over_cdp(connect_url) + context = browser.contexts[0] + page = context.pages[0] + + # Execute Playwright actions on the remote browser tab + page.goto("https://news.ycombinator.com/") + page_title = page.title() + assert ( + page_title == "Hacker News" + ), f"Page title is not 'Hacker News', it is '{page_title}'" + page.screenshot(path="screenshot.png") + + page.close() + browser.close() + print("Done!") + + +if __name__ == "__main__": + with sync_playwright() as playwright: + run(playwright) diff --git a/pyproject.toml b/pyproject.toml index e0a6c1f..28a35a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,12 +50,15 @@ dev-dependencies = [ "respx", "pytest", "pytest-asyncio", + "pytest-playwright", "ruff", "time-machine", "nox", "dirty-equals>=0.6.0", "importlib-metadata>=6.7.0", "rich>=13.7.1", + "python-dotenv", + "playwright", ] [tool.rye.scripts] @@ -79,10 +82,15 @@ format = { chain = [ "check:importable" = "python -c 'import browserbase'" +"example" = "python -c 'import sys; from pathlib import Path; example = Path(\"examples\") / (sys.argv[1] + \".py\"); exec(open(example).read())'" + +"test:e2e" = "python -m pytest examples/e2e" + typecheck = { chain = [ "typecheck:pyright", "typecheck:mypy" ]} + "typecheck:pyright" = "pyright" "typecheck:verify-types" = "pyright --verifytypes browserbase --ignoreexternal" "typecheck:mypy" = "mypy ." diff --git a/requirements-dev.lock b/requirements-dev.lock index b6be61c..3bd5de5 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -7,6 +7,7 @@ # all-features: true # with-sources: false # generate-hashes: false +# universal: false -e file:. annotated-types==0.6.0 @@ -21,6 +22,9 @@ attrs==23.1.0 certifi==2023.7.22 # via httpcore # via httpx + # via requests +charset-normalizer==3.4.0 + # via requests colorlog==6.7.0 # via nox dirty-equals==0.6.0 @@ -32,6 +36,8 @@ exceptiongroup==1.1.3 # via anyio filelock==3.12.4 # via virtualenv +greenlet==3.1.1 + # via playwright h11==0.14.0 # via httpcore httpcore==1.0.2 @@ -42,6 +48,7 @@ httpx==0.25.2 idna==3.4 # via anyio # via httpx + # via requests importlib-metadata==7.0.0 iniconfig==2.0.0 # via pytest @@ -60,6 +67,8 @@ packaging==23.2 # via pytest platformdirs==3.11.0 # via virtualenv +playwright==1.48.0 + # via pytest-playwright pluggy==1.3.0 # via pytest py==1.11.0 @@ -68,16 +77,28 @@ pydantic==2.7.1 # via browserbase pydantic-core==2.18.2 # via pydantic +pyee==12.0.0 + # via playwright pygments==2.18.0 # via rich pyright==1.1.380 pytest==7.1.1 # via pytest-asyncio + # via pytest-base-url + # via pytest-playwright pytest-asyncio==0.21.1 +pytest-base-url==2.1.0 + # via pytest-playwright +pytest-playwright==0.5.2 python-dateutil==2.8.2 # via time-machine +python-dotenv==1.0.1 +python-slugify==8.0.4 + # via pytest-playwright pytz==2023.3.post1 # via dirty-equals +requests==2.32.3 + # via pytest-base-url respx==0.20.2 rich==13.7.1 ruff==0.6.9 @@ -89,6 +110,8 @@ sniffio==1.3.0 # via anyio # via browserbase # via httpx +text-unidecode==1.3 + # via python-slugify time-machine==2.9.0 tomli==2.0.1 # via mypy @@ -99,6 +122,9 @@ typing-extensions==4.8.0 # via mypy # via pydantic # via pydantic-core + # via pyee +urllib3==2.2.3 + # via requests virtualenv==20.24.5 # via nox zipp==3.17.0 diff --git a/requirements.lock b/requirements.lock index 44b55d0..b71b55a 100644 --- a/requirements.lock +++ b/requirements.lock @@ -7,6 +7,7 @@ # all-features: true # with-sources: false # generate-hashes: false +# universal: false -e file:. annotated-types==0.6.0