From e3a607184425815dd1e5534518c686ad4e5e7e00 Mon Sep 17 00:00:00 2001 From: "fern-api[bot]" <115122769+fern-api[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 09:38:28 +0200 Subject: [PATCH 01/13] :herb: Fern Regeneration -- September 2, 2025 (#23) * SDK regeneration * stash --------- Co-authored-by: fern-api <115122769+fern-api[bot]@users.noreply.github.com> Co-authored-by: Matic Zavadlal --- .devcontainer/Dockerfile | 9 - .devcontainer/devcontainer.json | 43 - .fernignore | 6 + .github/workflows/ci.yml | 117 +- .github/workflows/publish-pypi.yml | 31 - .github/workflows/release-doctor.yml | 21 - .gitignore | 23 +- .python-version | 1 - .release-please-manifest.json | 3 - .stats.yml | 4 - .vscode/settings.json | 6 +- Brewfile | 2 - CHANGELOG.md | 68 - CONTRIBUTING.md | 128 -- LICENSE | 201 -- README.md | 154 +- SECURITY.md | 27 - api.md | 115 - bin/check-release-environment | 21 - bin/publish-pypi | 6 - examples/.keep | 4 - mypy.ini | 50 - noxfile.py | 9 - poetry.lock | 550 +++++ pyproject.toml | 251 +-- reference.md | 1606 +++++++++++++ release-please-config.json | 66 - requirements-dev.lock | 135 -- requirements.lock | 72 - requirements.txt | 4 + scripts/bootstrap | 19 - scripts/format | 8 - scripts/lint | 11 - scripts/mock | 41 - scripts/test | 61 - scripts/utils/ruffen-docs.py | 167 -- scripts/utils/upload-artifact.sh | 27 - src/browser_use/__init__.py | 9 + src/browser_use/accounts/__init__.py | 4 + src/browser_use/accounts/client.py | 100 + src/browser_use/accounts/raw_client.py | 114 + src/browser_use/client.py | 165 ++ src/browser_use/core/__init__.py | 7 + src/browser_use/core/api_error.py | 23 + src/browser_use/core/client_wrapper.py | 78 + src/browser_use/core/datetime_utils.py | 28 + src/browser_use/core/file.py | 67 + src/browser_use/core/force_multipart.py | 16 + src/browser_use/core/http_client.py | 543 +++++ src/browser_use/core/http_response.py | 55 + src/browser_use/core/jsonable_encoder.py | 100 + src/browser_use/core/pydantic_utilities.py | 255 +++ src/browser_use/core/query_encoder.py | 58 + src/browser_use/core/remove_none_from_dict.py | 11 + src/browser_use/core/request_options.py | 35 + src/browser_use/core/serialization.py | 276 +++ src/browser_use/core/unchecked_base_model.py | 341 +++ src/browser_use/environment.py | 7 + src/browser_use/errors/__init__.py | 4 + src/browser_use/errors/bad_request_error.py | 10 + .../errors/internal_server_error.py | 10 + src/browser_use/errors/not_found_error.py | 10 + .../errors/payment_required_error.py | 11 + .../errors/unprocessable_entity_error.py | 10 + src/browser_use/files/__init__.py | 4 + src/browser_use/files/client.py | 245 ++ src/browser_use/files/raw_client.py | 387 ++++ src/browser_use/files/types/__init__.py | 4 + .../types/upload_file_request_content_type.py | 23 + src/browser_use/profiles/__init__.py | 4 + src/browser_use/profiles/client.py | 335 +++ src/browser_use/profiles/raw_client.py | 471 ++++ src/{browser_use_sdk => browser_use}/py.typed | 0 src/browser_use/sessions/__init__.py | 4 + src/browser_use/sessions/client.py | 648 ++++++ src/browser_use/sessions/raw_client.py | 990 ++++++++ src/browser_use/tasks/__init__.py | 4 + src/browser_use/tasks/client.py | 610 +++++ src/browser_use/tasks/raw_client.py | 933 ++++++++ src/browser_use/types/__init__.py | 4 + .../types/account_not_found_error.py | 24 + src/browser_use/types/account_view.py | 54 + .../types/bad_request_error_body.py | 8 + .../types/credits_deduction_error.py | 24 + .../types/download_url_generation_error.py | 24 + src/browser_use/types/file_view.py | 34 + .../types/http_validation_error.py | 21 + .../types/insufficient_credits_error.py | 24 + .../types/internal_server_error_body.py | 24 + src/browser_use/types/not_found_error_body.py | 8 + .../types/output_file_not_found_error.py | 24 + .../types/profile_list_response.py | 45 + .../types/profile_not_found_error.py | 24 + src/browser_use/types/profile_view.py | 49 + src/browser_use/types/proxy_country_code.py | 5 + .../types/session_has_running_task_error.py | 24 + src/browser_use/types/session_item_view.py | 55 + .../types/session_list_response.py | 45 + .../types/session_not_found_error.py | 24 + src/browser_use/types/session_status.py | 5 + .../types/session_stopped_error.py | 24 + .../types/session_update_action.py | 5 + src/browser_use/types/session_view.py | 68 + .../types/share_not_found_error.py | 24 + src/browser_use/types/share_view.py | 47 + src/browser_use/types/supported_ll_ms.py | 20 + .../types/task_created_response.py | 27 + src/browser_use/types/task_item_view.py | 88 + src/browser_use/types/task_list_response.py | 45 + .../types/task_log_file_response.py | 29 + src/browser_use/types/task_not_found_error.py | 24 + .../types/task_output_file_response.py | 39 + src/browser_use/types/task_status.py | 5 + src/browser_use/types/task_step_view.py | 63 + src/browser_use/types/task_update_action.py | 5 + src/browser_use/types/task_view.py | 92 + .../types/unsupported_content_type_error.py | 24 + .../upload_file_presigned_url_response.py | 49 + src/browser_use/types/validation_error.py | 23 + .../types/validation_error_loc_item.py | 5 + src/browser_use/version.py | 3 + src/browser_use/wrapper/browser_use_client.py | 0 src/browser_use/wrapper/parse.py | 80 + src/browser_use/wrapper/tasks/client.py | 8 + .../lib => browser_use/wrapper}/webhooks.py | 0 src/browser_use_sdk/__init__.py | 100 - src/browser_use_sdk/_base_client.py | 1995 ----------------- src/browser_use_sdk/_client.py | 439 ---- src/browser_use_sdk/_compat.py | 219 -- src/browser_use_sdk/_constants.py | 14 - src/browser_use_sdk/_exceptions.py | 108 - src/browser_use_sdk/_files.py | 123 - src/browser_use_sdk/_models.py | 829 ------- src/browser_use_sdk/_qs.py | 150 -- src/browser_use_sdk/_resource.py | 43 - src/browser_use_sdk/_response.py | 832 ------- src/browser_use_sdk/_streaming.py | 333 --- src/browser_use_sdk/_types.py | 219 -- src/browser_use_sdk/_utils/__init__.py | 57 - src/browser_use_sdk/_utils/_logs.py | 25 - src/browser_use_sdk/_utils/_proxy.py | 65 - src/browser_use_sdk/_utils/_reflection.py | 42 - .../_utils/_resources_proxy.py | 24 - src/browser_use_sdk/_utils/_streams.py | 12 - src/browser_use_sdk/_utils/_sync.py | 86 - src/browser_use_sdk/_utils/_transform.py | 447 ---- src/browser_use_sdk/_utils/_typing.py | 151 -- src/browser_use_sdk/_utils/_utils.py | 422 ---- src/browser_use_sdk/_version.py | 4 - src/browser_use_sdk/lib/.keep | 4 - src/browser_use_sdk/lib/parse.py | 35 - src/browser_use_sdk/resources/__init__.py | 75 - .../resources/agent_profiles.py | 748 ------ .../resources/browser_profiles.py | 764 ------- .../resources/sessions/__init__.py | 33 - .../resources/sessions/public_share.py | 429 ---- .../resources/sessions/sessions.py | 618 ----- src/browser_use_sdk/resources/tasks.py | 1741 -------------- .../resources/users/__init__.py | 33 - .../resources/users/me/__init__.py | 33 - .../resources/users/me/files.py | 269 --- src/browser_use_sdk/resources/users/me/me.py | 207 -- src/browser_use_sdk/resources/users/users.py | 102 - src/browser_use_sdk/types/__init__.py | 33 - .../types/agent_profile_create_params.py | 30 - .../types/agent_profile_list_params.py | 15 - .../types/agent_profile_list_response.py | 20 - .../types/agent_profile_update_params.py | 30 - .../types/agent_profile_view.py | 36 - .../types/browser_profile_create_params.py | 32 - .../types/browser_profile_list_params.py | 15 - .../types/browser_profile_list_response.py | 20 - .../types/browser_profile_update_params.py | 33 - .../types/browser_profile_view.py | 38 - src/browser_use_sdk/types/file_view.py | 13 - .../types/proxy_country_code.py | 7 - .../types/session_list_params.py | 24 - .../types/session_list_response.py | 38 - src/browser_use_sdk/types/session_status.py | 7 - .../types/session_update_params.py | 16 - src/browser_use_sdk/types/session_view.py | 35 - .../types/sessions/__init__.py | 5 - .../types/sessions/share_view.py | 20 - .../types/task_create_params.py | 64 - .../types/task_create_response.py | 13 - .../types/task_get_logs_response.py | 11 - .../types/task_get_output_file_response.py | 15 - .../task_get_user_uploaded_file_response.py | 15 - src/browser_use_sdk/types/task_item_view.py | 44 - src/browser_use_sdk/types/task_list_params.py | 33 - .../types/task_list_response.py | 20 - src/browser_use_sdk/types/task_status.py | 7 - src/browser_use_sdk/types/task_step_view.py | 25 - .../types/task_update_params.py | 17 - src/browser_use_sdk/types/task_view.py | 79 - src/browser_use_sdk/types/users/__init__.py | 5 - .../types/users/me/__init__.py | 6 - .../me/file_create_presigned_url_params.py | 37 - .../me/file_create_presigned_url_response.py | 22 - .../types/users/me_retrieve_response.py | 22 - tests/__init__.py | 1 - tests/api_resources/__init__.py | 1 - tests/api_resources/sessions/__init__.py | 1 - .../sessions/test_public_share.py | 276 --- tests/api_resources/test_agent_profiles.py | 487 ---- tests/api_resources/test_browser_profiles.py | 491 ---- tests/api_resources/test_sessions.py | 363 --- tests/api_resources/test_tasks.py | 692 ------ tests/api_resources/users/__init__.py | 1 - tests/api_resources/users/me/__init__.py | 1 - tests/api_resources/users/me/test_files.py | 104 - tests/api_resources/users/test_me.py | 80 - tests/conftest.py | 84 - tests/custom/test_client.py | 7 + tests/sample_file.txt | 1 - tests/test_client.py | 1736 -------------- tests/test_deepcopy.py | 58 - tests/test_extract_files.py | 64 - tests/test_files.py | 51 - tests/test_models.py | 963 -------- tests/test_qs.py | 78 - tests/test_required_args.py | 111 - tests/test_response.py | 277 --- tests/test_streaming.py | 250 --- tests/test_transform.py | 453 ---- tests/test_utils/test_proxy.py | 34 - tests/test_utils/test_typing.py | 73 - tests/test_webhooks.py | 151 -- tests/utils.py | 159 -- tests/utils/__init__.py | 2 + tests/utils/assets/models/__init__.py | 21 + tests/utils/assets/models/circle.py | 11 + tests/utils/assets/models/color.py | 7 + .../assets/models/object_with_defaults.py | 15 + .../models/object_with_optional_field.py | 35 + tests/utils/assets/models/shape.py | 28 + tests/utils/assets/models/square.py | 11 + .../assets/models/undiscriminated_shape.py | 10 + tests/utils/test_http_client.py | 61 + tests/utils/test_query_encoding.py | 37 + tests/utils/test_serialization.py | 72 + 241 files changed, 11089 insertions(+), 21423 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json create mode 100644 .fernignore delete mode 100644 .github/workflows/publish-pypi.yml delete mode 100644 .github/workflows/release-doctor.yml delete mode 100644 .python-version delete mode 100644 .release-please-manifest.json delete mode 100644 .stats.yml delete mode 100644 Brewfile delete mode 100644 CHANGELOG.md delete mode 100644 CONTRIBUTING.md delete mode 100644 LICENSE delete mode 100644 SECURITY.md delete mode 100644 api.md delete mode 100644 bin/check-release-environment delete mode 100644 bin/publish-pypi delete mode 100644 examples/.keep delete mode 100644 mypy.ini delete mode 100644 noxfile.py create mode 100644 poetry.lock create mode 100644 reference.md delete mode 100644 release-please-config.json delete mode 100644 requirements-dev.lock delete mode 100644 requirements.lock create mode 100644 requirements.txt delete mode 100755 scripts/bootstrap delete mode 100755 scripts/format delete mode 100755 scripts/lint delete mode 100755 scripts/mock delete mode 100755 scripts/test delete mode 100644 scripts/utils/ruffen-docs.py delete mode 100755 scripts/utils/upload-artifact.sh create mode 100644 src/browser_use/__init__.py create mode 100644 src/browser_use/accounts/__init__.py create mode 100644 src/browser_use/accounts/client.py create mode 100644 src/browser_use/accounts/raw_client.py create mode 100644 src/browser_use/client.py create mode 100644 src/browser_use/core/__init__.py create mode 100644 src/browser_use/core/api_error.py create mode 100644 src/browser_use/core/client_wrapper.py create mode 100644 src/browser_use/core/datetime_utils.py create mode 100644 src/browser_use/core/file.py create mode 100644 src/browser_use/core/force_multipart.py create mode 100644 src/browser_use/core/http_client.py create mode 100644 src/browser_use/core/http_response.py create mode 100644 src/browser_use/core/jsonable_encoder.py create mode 100644 src/browser_use/core/pydantic_utilities.py create mode 100644 src/browser_use/core/query_encoder.py create mode 100644 src/browser_use/core/remove_none_from_dict.py create mode 100644 src/browser_use/core/request_options.py create mode 100644 src/browser_use/core/serialization.py create mode 100644 src/browser_use/core/unchecked_base_model.py create mode 100644 src/browser_use/environment.py create mode 100644 src/browser_use/errors/__init__.py create mode 100644 src/browser_use/errors/bad_request_error.py create mode 100644 src/browser_use/errors/internal_server_error.py create mode 100644 src/browser_use/errors/not_found_error.py create mode 100644 src/browser_use/errors/payment_required_error.py create mode 100644 src/browser_use/errors/unprocessable_entity_error.py create mode 100644 src/browser_use/files/__init__.py create mode 100644 src/browser_use/files/client.py create mode 100644 src/browser_use/files/raw_client.py create mode 100644 src/browser_use/files/types/__init__.py create mode 100644 src/browser_use/files/types/upload_file_request_content_type.py create mode 100644 src/browser_use/profiles/__init__.py create mode 100644 src/browser_use/profiles/client.py create mode 100644 src/browser_use/profiles/raw_client.py rename src/{browser_use_sdk => browser_use}/py.typed (100%) create mode 100644 src/browser_use/sessions/__init__.py create mode 100644 src/browser_use/sessions/client.py create mode 100644 src/browser_use/sessions/raw_client.py create mode 100644 src/browser_use/tasks/__init__.py create mode 100644 src/browser_use/tasks/client.py create mode 100644 src/browser_use/tasks/raw_client.py create mode 100644 src/browser_use/types/__init__.py create mode 100644 src/browser_use/types/account_not_found_error.py create mode 100644 src/browser_use/types/account_view.py create mode 100644 src/browser_use/types/bad_request_error_body.py create mode 100644 src/browser_use/types/credits_deduction_error.py create mode 100644 src/browser_use/types/download_url_generation_error.py create mode 100644 src/browser_use/types/file_view.py create mode 100644 src/browser_use/types/http_validation_error.py create mode 100644 src/browser_use/types/insufficient_credits_error.py create mode 100644 src/browser_use/types/internal_server_error_body.py create mode 100644 src/browser_use/types/not_found_error_body.py create mode 100644 src/browser_use/types/output_file_not_found_error.py create mode 100644 src/browser_use/types/profile_list_response.py create mode 100644 src/browser_use/types/profile_not_found_error.py create mode 100644 src/browser_use/types/profile_view.py create mode 100644 src/browser_use/types/proxy_country_code.py create mode 100644 src/browser_use/types/session_has_running_task_error.py create mode 100644 src/browser_use/types/session_item_view.py create mode 100644 src/browser_use/types/session_list_response.py create mode 100644 src/browser_use/types/session_not_found_error.py create mode 100644 src/browser_use/types/session_status.py create mode 100644 src/browser_use/types/session_stopped_error.py create mode 100644 src/browser_use/types/session_update_action.py create mode 100644 src/browser_use/types/session_view.py create mode 100644 src/browser_use/types/share_not_found_error.py create mode 100644 src/browser_use/types/share_view.py create mode 100644 src/browser_use/types/supported_ll_ms.py create mode 100644 src/browser_use/types/task_created_response.py create mode 100644 src/browser_use/types/task_item_view.py create mode 100644 src/browser_use/types/task_list_response.py create mode 100644 src/browser_use/types/task_log_file_response.py create mode 100644 src/browser_use/types/task_not_found_error.py create mode 100644 src/browser_use/types/task_output_file_response.py create mode 100644 src/browser_use/types/task_status.py create mode 100644 src/browser_use/types/task_step_view.py create mode 100644 src/browser_use/types/task_update_action.py create mode 100644 src/browser_use/types/task_view.py create mode 100644 src/browser_use/types/unsupported_content_type_error.py create mode 100644 src/browser_use/types/upload_file_presigned_url_response.py create mode 100644 src/browser_use/types/validation_error.py create mode 100644 src/browser_use/types/validation_error_loc_item.py create mode 100644 src/browser_use/version.py create mode 100644 src/browser_use/wrapper/browser_use_client.py create mode 100644 src/browser_use/wrapper/parse.py create mode 100644 src/browser_use/wrapper/tasks/client.py rename src/{browser_use_sdk/lib => browser_use/wrapper}/webhooks.py (100%) delete mode 100644 src/browser_use_sdk/__init__.py delete mode 100644 src/browser_use_sdk/_base_client.py delete mode 100644 src/browser_use_sdk/_client.py delete mode 100644 src/browser_use_sdk/_compat.py delete mode 100644 src/browser_use_sdk/_constants.py delete mode 100644 src/browser_use_sdk/_exceptions.py delete mode 100644 src/browser_use_sdk/_files.py delete mode 100644 src/browser_use_sdk/_models.py delete mode 100644 src/browser_use_sdk/_qs.py delete mode 100644 src/browser_use_sdk/_resource.py delete mode 100644 src/browser_use_sdk/_response.py delete mode 100644 src/browser_use_sdk/_streaming.py delete mode 100644 src/browser_use_sdk/_types.py delete mode 100644 src/browser_use_sdk/_utils/__init__.py delete mode 100644 src/browser_use_sdk/_utils/_logs.py delete mode 100644 src/browser_use_sdk/_utils/_proxy.py delete mode 100644 src/browser_use_sdk/_utils/_reflection.py delete mode 100644 src/browser_use_sdk/_utils/_resources_proxy.py delete mode 100644 src/browser_use_sdk/_utils/_streams.py delete mode 100644 src/browser_use_sdk/_utils/_sync.py delete mode 100644 src/browser_use_sdk/_utils/_transform.py delete mode 100644 src/browser_use_sdk/_utils/_typing.py delete mode 100644 src/browser_use_sdk/_utils/_utils.py delete mode 100644 src/browser_use_sdk/_version.py delete mode 100644 src/browser_use_sdk/lib/.keep delete mode 100644 src/browser_use_sdk/lib/parse.py delete mode 100644 src/browser_use_sdk/resources/__init__.py delete mode 100644 src/browser_use_sdk/resources/agent_profiles.py delete mode 100644 src/browser_use_sdk/resources/browser_profiles.py delete mode 100644 src/browser_use_sdk/resources/sessions/__init__.py delete mode 100644 src/browser_use_sdk/resources/sessions/public_share.py delete mode 100644 src/browser_use_sdk/resources/sessions/sessions.py delete mode 100644 src/browser_use_sdk/resources/tasks.py delete mode 100644 src/browser_use_sdk/resources/users/__init__.py delete mode 100644 src/browser_use_sdk/resources/users/me/__init__.py delete mode 100644 src/browser_use_sdk/resources/users/me/files.py delete mode 100644 src/browser_use_sdk/resources/users/me/me.py delete mode 100644 src/browser_use_sdk/resources/users/users.py delete mode 100644 src/browser_use_sdk/types/__init__.py delete mode 100644 src/browser_use_sdk/types/agent_profile_create_params.py delete mode 100644 src/browser_use_sdk/types/agent_profile_list_params.py delete mode 100644 src/browser_use_sdk/types/agent_profile_list_response.py delete mode 100644 src/browser_use_sdk/types/agent_profile_update_params.py delete mode 100644 src/browser_use_sdk/types/agent_profile_view.py delete mode 100644 src/browser_use_sdk/types/browser_profile_create_params.py delete mode 100644 src/browser_use_sdk/types/browser_profile_list_params.py delete mode 100644 src/browser_use_sdk/types/browser_profile_list_response.py delete mode 100644 src/browser_use_sdk/types/browser_profile_update_params.py delete mode 100644 src/browser_use_sdk/types/browser_profile_view.py delete mode 100644 src/browser_use_sdk/types/file_view.py delete mode 100644 src/browser_use_sdk/types/proxy_country_code.py delete mode 100644 src/browser_use_sdk/types/session_list_params.py delete mode 100644 src/browser_use_sdk/types/session_list_response.py delete mode 100644 src/browser_use_sdk/types/session_status.py delete mode 100644 src/browser_use_sdk/types/session_update_params.py delete mode 100644 src/browser_use_sdk/types/session_view.py delete mode 100644 src/browser_use_sdk/types/sessions/__init__.py delete mode 100644 src/browser_use_sdk/types/sessions/share_view.py delete mode 100644 src/browser_use_sdk/types/task_create_params.py delete mode 100644 src/browser_use_sdk/types/task_create_response.py delete mode 100644 src/browser_use_sdk/types/task_get_logs_response.py delete mode 100644 src/browser_use_sdk/types/task_get_output_file_response.py delete mode 100644 src/browser_use_sdk/types/task_get_user_uploaded_file_response.py delete mode 100644 src/browser_use_sdk/types/task_item_view.py delete mode 100644 src/browser_use_sdk/types/task_list_params.py delete mode 100644 src/browser_use_sdk/types/task_list_response.py delete mode 100644 src/browser_use_sdk/types/task_status.py delete mode 100644 src/browser_use_sdk/types/task_step_view.py delete mode 100644 src/browser_use_sdk/types/task_update_params.py delete mode 100644 src/browser_use_sdk/types/task_view.py delete mode 100644 src/browser_use_sdk/types/users/__init__.py delete mode 100644 src/browser_use_sdk/types/users/me/__init__.py delete mode 100644 src/browser_use_sdk/types/users/me/file_create_presigned_url_params.py delete mode 100644 src/browser_use_sdk/types/users/me/file_create_presigned_url_response.py delete mode 100644 src/browser_use_sdk/types/users/me_retrieve_response.py delete mode 100644 tests/__init__.py delete mode 100644 tests/api_resources/__init__.py delete mode 100644 tests/api_resources/sessions/__init__.py delete mode 100644 tests/api_resources/sessions/test_public_share.py delete mode 100644 tests/api_resources/test_agent_profiles.py delete mode 100644 tests/api_resources/test_browser_profiles.py delete mode 100644 tests/api_resources/test_sessions.py delete mode 100644 tests/api_resources/test_tasks.py delete mode 100644 tests/api_resources/users/__init__.py delete mode 100644 tests/api_resources/users/me/__init__.py delete mode 100644 tests/api_resources/users/me/test_files.py delete mode 100644 tests/api_resources/users/test_me.py delete mode 100644 tests/conftest.py create mode 100644 tests/custom/test_client.py delete mode 100644 tests/sample_file.txt delete mode 100644 tests/test_client.py delete mode 100644 tests/test_deepcopy.py delete mode 100644 tests/test_extract_files.py delete mode 100644 tests/test_files.py delete mode 100644 tests/test_models.py delete mode 100644 tests/test_qs.py delete mode 100644 tests/test_required_args.py delete mode 100644 tests/test_response.py delete mode 100644 tests/test_streaming.py delete mode 100644 tests/test_transform.py delete mode 100644 tests/test_utils/test_proxy.py delete mode 100644 tests/test_utils/test_typing.py delete mode 100644 tests/test_webhooks.py delete mode 100644 tests/utils.py create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/assets/models/__init__.py create mode 100644 tests/utils/assets/models/circle.py create mode 100644 tests/utils/assets/models/color.py create mode 100644 tests/utils/assets/models/object_with_defaults.py create mode 100644 tests/utils/assets/models/object_with_optional_field.py create mode 100644 tests/utils/assets/models/shape.py create mode 100644 tests/utils/assets/models/square.py create mode 100644 tests/utils/assets/models/undiscriminated_shape.py create mode 100644 tests/utils/test_http_client.py create mode 100644 tests/utils/test_query_encoding.py create mode 100644 tests/utils/test_serialization.py diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index ff261ba..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -ARG VARIANT="3.9" -FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} - -USER vscode - -RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.44.0" RYE_INSTALL_OPTION="--yes" bash -ENV PATH=/home/vscode/.rye/shims:$PATH - -RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index c17fdc1..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,43 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/debian -{ - "name": "Debian", - "build": { - "dockerfile": "Dockerfile", - "context": ".." - }, - - "postStartCommand": "rye sync --all-features", - - "customizations": { - "vscode": { - "extensions": [ - "ms-python.python" - ], - "settings": { - "terminal.integrated.shell.linux": "/bin/bash", - "python.pythonPath": ".venv/bin/python", - "python.defaultInterpreterPath": ".venv/bin/python", - "python.typeChecking": "basic", - "terminal.integrated.env.linux": { - "PATH": "/home/vscode/.rye/shims:${env:PATH}" - } - } - } - }, - "features": { - "ghcr.io/devcontainers/features/node:1": {} - } - - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Configure tool-specific properties. - // "customizations": {}, - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" -} diff --git a/.fernignore b/.fernignore new file mode 100644 index 0000000..a5e6117 --- /dev/null +++ b/.fernignore @@ -0,0 +1,6 @@ +# Specify files that shouldn't be modified by Fern +.gitignore +.vscode/ + +src/browser_use/client.py +src/browser_use/wrapper/ \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ccb20d4..66780cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,98 +1,37 @@ -name: CI -on: - push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' - pull_request: - branches-ignore: - - 'stl-preview-head/**' - - 'stl-preview-base/**' +name: ci +on: [push] jobs: - lint: - timeout-minutes: 10 - name: lint - runs-on: ${{ github.repository == 'stainless-sdks/browser-use-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + compile: + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' - - - name: Install dependencies - run: rye sync --all-features - - - name: Run lints - run: ./scripts/lint - - build: - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork - timeout-minutes: 10 - name: build - permissions: - contents: read - id-token: write - runs-on: depot-ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - - name: Install Rye + - name: Checkout repo + uses: actions/checkout@v4 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' - + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 - name: Install dependencies - run: rye sync --all-features - - - name: Run build - run: rye build - - - name: Get GitHub OIDC Token - if: github.repository == 'stainless-sdks/browser-use-python' - id: github-oidc - uses: actions/github-script@v6 - with: - script: core.setOutput('github_token', await core.getIDToken()); - - - name: Upload tarball - if: github.repository == 'stainless-sdks/browser-use-python' - env: - URL: https://pkg.stainless.com/s - AUTH: ${{ steps.github-oidc.outputs.github_token }} - SHA: ${{ github.sha }} - run: ./scripts/utils/upload-artifact.sh - + run: poetry install + - name: Compile + run: poetry run mypy . test: - timeout-minutes: 10 - name: test - runs-on: ${{ github.repository == 'stainless-sdks/browser-use-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - name: Install Rye + - name: Checkout repo + uses: actions/checkout@v4 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' - - - name: Bootstrap - run: ./scripts/bootstrap + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install - - name: Run tests - run: ./scripts/test + - name: Test + run: poetry run pytest -rP . diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml deleted file mode 100644 index d917646..0000000 --- a/.github/workflows/publish-pypi.yml +++ /dev/null @@ -1,31 +0,0 @@ -# This workflow is triggered when a GitHub release is created. -# It can also be run manually to re-publish to PyPI in case it failed for some reason. -# You can run this workflow by navigating to https://www.github.com/browser-use/browser-use-python/actions/workflows/publish-pypi.yml -name: Publish PyPI -on: - workflow_dispatch: - - release: - types: [published] - -jobs: - publish: - name: publish - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' - - - name: Publish to PyPI - run: | - bash ./bin/publish-pypi - env: - PYPI_TOKEN: ${{ secrets.BROWSER_USE_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml deleted file mode 100644 index 8d69a5b..0000000 --- a/.github/workflows/release-doctor.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Release Doctor -on: - pull_request: - branches: - - main - workflow_dispatch: - -jobs: - release_doctor: - name: release doctor - runs-on: ubuntu-latest - if: github.repository == 'browser-use/browser-use-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') - - steps: - - uses: actions/checkout@v4 - - - name: Check release environment - run: | - bash ./bin/check-release-environment - env: - PYPI_TOKEN: ${{ secrets.BROWSER_USE_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.gitignore b/.gitignore index 95ceb18..7979467 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,8 @@ -.prism.log -_dev - -__pycache__ -.mypy_cache - -dist - -.venv -.idea - -.env -.envrc -codegen.log -Brewfile.lock.json +.mypy_cache/ +.ruff_cache/ +__pycache__/ +dist/ +poetry.toml + +.env* +!.env.example \ No newline at end of file diff --git a/.python-version b/.python-version deleted file mode 100644 index 43077b2..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.9.18 diff --git a/.release-please-manifest.json b/.release-please-manifest.json deleted file mode 100644 index 06d6df2..0000000 --- a/.release-please-manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - ".": "1.0.2" -} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml deleted file mode 100644 index 808f944..0000000 --- a/.stats.yml +++ /dev/null @@ -1,4 +0,0 @@ -configured_endpoints: 26 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browser-use%2Fbrowser-use-814bdd9f98b750d42a2b713a0a12b14fc5a0241ff820b2fbc7666ab2e9a5443f.yml -openapi_spec_hash: 0dae4d4d33a3ec93e470f9546e43fad3 -config_hash: dd3e22b635fa0eb9a7c741a8aaca2a7f diff --git a/.vscode/settings.json b/.vscode/settings.json index 5b01030..63139ca 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,7 @@ { - "python.analysis.importFormat": "relative", + "python.analysis.importFormat": "relative", + "editor.codeActionsOnSave": { + "source.organizeImports": "always", + "source.fixAll.ruff": "always" + } } diff --git a/Brewfile b/Brewfile deleted file mode 100644 index 492ca37..0000000 --- a/Brewfile +++ /dev/null @@ -1,2 +0,0 @@ -brew "rye" - diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 284fac7..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,68 +0,0 @@ -# Changelog - -## 1.0.2 (2025-08-22) - -Full Changelog: [v1.0.1...v1.0.2](https://github.com/browser-use/browser-use-python/compare/v1.0.1...v1.0.2) - -### Chores - -* update github action ([655a660](https://github.com/browser-use/browser-use-python/commit/655a6600972aee2cefcf83c73fdfd9e1ae68b852)) - -## 1.0.1 (2025-08-21) - -Full Changelog: [v1.0.0...v1.0.1](https://github.com/browser-use/browser-use-python/compare/v1.0.0...v1.0.1) - -### Bug Fixes - -* Improve Quick Start Section ([c994ab2](https://github.com/browser-use/browser-use-python/commit/c994ab27ce570d95961f84d8e54a48dd04bd3dfc)) - -## 1.0.0 (2025-08-20) - -Full Changelog: [v0.3.0...v1.0.0](https://github.com/browser-use/browser-use-python/compare/v0.3.0...v1.0.0) - -## 0.3.0 (2025-08-20) - -Full Changelog: [v0.2.0...v0.3.0](https://github.com/browser-use/browser-use-python/compare/v0.2.0...v0.3.0) - -### Features - -* LLM key strings over LLM model enum ([0f5930a](https://github.com/browser-use/browser-use-python/commit/0f5930a7760190523f1d3e969c66e0a34ff075b3)) - -## 0.2.0 (2025-08-19) - -Full Changelog: [v0.1.0...v0.2.0](https://github.com/browser-use/browser-use-python/compare/v0.1.0...v0.2.0) - -### Features - -* **api:** manual updates ([6266282](https://github.com/browser-use/browser-use-python/commit/6266282a615344fdab0737d29adc9124a3bf8b8d)) -* **api:** manual updates ([2d9ba52](https://github.com/browser-use/browser-use-python/commit/2d9ba52b23e53c581360afc655fa8d665a106814)) -* Improve Docs ([6e79b7c](https://github.com/browser-use/browser-use-python/commit/6e79b7c5cfc7cf54f1474521025fa713f200bc3b)) - -## 0.1.0 (2025-08-18) - -Full Changelog: [v0.0.2...v0.1.0](https://github.com/browser-use/browser-use-python/compare/v0.0.2...v0.1.0) - -### Features - -* Add start_url ([2ede0a9](https://github.com/browser-use/browser-use-python/commit/2ede0a9089bfbba1eca207508a52ee36b4ef18ac)) -* Align Task Filtering by Status with `status` Field ([29b4590](https://github.com/browser-use/browser-use-python/commit/29b4590c69f13fbf7f855888862ef77a9e704172)) -* **api:** api update ([5867532](https://github.com/browser-use/browser-use-python/commit/58675327b6a0e7ba41f312e4887062a9b6dc2852)) -* **api:** manual updates ([78727c0](https://github.com/browser-use/browser-use-python/commit/78727c02cefa53fd0dd877e137b7b6f92e14fce8)) -* **api:** update via SDK Studio ([b283386](https://github.com/browser-use/browser-use-python/commit/b283386b805435a87114e807f8919185cb6a5b7b)) -* Fix Stainless GitHub Action ([5dcf360](https://github.com/browser-use/browser-use-python/commit/5dcf360ccfe40f45962ecaa64b8a5aacf55778d4)) -* Update param and response views ([44b4c5d](https://github.com/browser-use/browser-use-python/commit/44b4c5d7ed416f9f5c37afb3287cdaa6f22a30cd)) - - -### Chores - -* **internal:** codegen related update ([151d56b](https://github.com/browser-use/browser-use-python/commit/151d56ba67c2d09970ff415472c0a1d259716bbc)) - -## 0.0.2 (2025-08-09) - -Full Changelog: [v0.0.1...v0.0.2](https://github.com/browser-use/browser-use-python/compare/v0.0.1...v0.0.2) - -### Chores - -* configure new SDK language ([af51d4f](https://github.com/browser-use/browser-use-python/commit/af51d4f1d2ff224d0a2cba426b28d540d74f63ce)) -* update SDK settings ([4fcafb0](https://github.com/browser-use/browser-use-python/commit/4fcafb0a1cbd6fda1c28c0996fe3de4eb033b107)) -* update SDK settings ([20019d1](https://github.com/browser-use/browser-use-python/commit/20019d1ec80d3c75dfb7ca54131b66e9dc0dd542)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 5338abc..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,128 +0,0 @@ -## Setting up the environment - -### With Rye - -We use [Rye](https://rye.astral.sh/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: - -```sh -$ ./scripts/bootstrap -``` - -Or [install Rye manually](https://rye.astral.sh/guide/installation/) and run: - -```sh -$ rye sync --all-features -``` - -You can then run scripts using `rye run python script.py` or by activating the virtual environment: - -```sh -# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work -$ source .venv/bin/activate - -# now you can omit the `rye run` prefix -$ python script.py -``` - -### Without Rye - -Alternatively if you don't want to install `Rye`, you can stick with the standard `pip` setup by ensuring you have the Python version specified in `.python-version`, create a virtual environment however you desire and then install dependencies using this command: - -```sh -$ pip install -r requirements-dev.lock -``` - -## Modifying/Adding code - -Most of the SDK is generated code. Modifications to code will be persisted between generations, but may -result in merge conflicts between manual patches and changes from the generator. The generator will never -modify the contents of the `src/browser_use_sdk/lib/` and `examples/` directories. - -## Adding and running examples - -All files in the `examples/` directory are not modified by the generator and can be freely edited or added to. - -```py -# add an example to examples/.py - -#!/usr/bin/env -S rye run python -… -``` - -```sh -$ chmod +x examples/.py -# run the example against your api -$ ./examples/.py -``` - -## Using the repository from source - -If you’d like to use the repository from source, you can either install from git or link to a cloned repository: - -To install via git: - -```sh -$ pip install git+ssh://git@github.com/browser-use/browser-use-python.git -``` - -Alternatively, you can build from source and install the wheel file: - -Building this package will create two files in the `dist/` directory, a `.tar.gz` containing the source files and a `.whl` that can be used to install the package efficiently. - -To create a distributable version of the library, all you have to do is run this command: - -```sh -$ rye build -# or -$ python -m build -``` - -Then to install: - -```sh -$ pip install ./path-to-wheel-file.whl -``` - -## Running tests - -Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. - -```sh -# you will need npm installed -$ npx prism mock path/to/your/openapi.yml -``` - -```sh -$ ./scripts/test -``` - -## Linting and formatting - -This repository uses [ruff](https://github.com/astral-sh/ruff) and -[black](https://github.com/psf/black) to format the code in the repository. - -To lint: - -```sh -$ ./scripts/lint -``` - -To format and fix all ruff issues automatically: - -```sh -$ ./scripts/format -``` - -## Publishing and releases - -Changes made to this repository via the automated release PR pipeline should publish to PyPI automatically. If -the changes aren't made through the automated pipeline, you may want to make releases manually. - -### Publish with a GitHub workflow - -You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/browser-use/browser-use-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. - -### Publish manually - -If you need to manually release a package, you can run the `bin/publish-pypi` script with a `PYPI_TOKEN` set on -the environment. diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 6eff678..0000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2025 Browser Use - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/README.md b/README.md index 79ee251..0f4433d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ -Browser Use Python +# BrowserUse Python Library -[![PyPI version]()](https://pypi.org/project/browser-use-sdk/) +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=https%3A%2F%2Fgithub.com%2Fbrowser-use%2Fbrowser-use-python) +[![pypi](https://img.shields.io/pypi/v/browser-use)](https://pypi.python.org/pypi/browser-use) -```sh -pip install browser-use-sdk -``` +The BrowserUse Python library provides convenient access to the BrowserUse APIs from Python. ## Two-Step QuickStart @@ -200,6 +199,80 @@ asyncio.run(main()) ## Advanced +### Access Raw Response Data + +The SDK provides access to raw response data, including headers, through the `.with_raw_response` property. +The `.with_raw_response` property returns a "raw" client that can be used to access the `.headers` and `.data` attributes. + +```python +from browser_use import BrowserUse + +client = BrowserUse( + ..., +) +response = client.tasks.with_raw_response.create_task(...) +print(response.headers) # access the response headers +print(response.data) # access the underlying object +``` + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retryable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). + +A request is deemed retryable when any of the following HTTP status codes is returned: + +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors) + +Use the `max_retries` request option to configure this behavior. + +```python +client.tasks.create_task(..., request_options={ + "max_retries": 1 +}) +``` + +### Timeouts + +The SDK defaults to a 60 second timeout. You can configure this with a timeout option at the client or request level. + +```python + +from browser_use import BrowserUse + +client = BrowserUse( + ..., + timeout=20.0, +) + + +# Override timeout for a specific method +client.tasks.create_task(..., request_options={ + "timeout_in_seconds": 1 +}) +``` + +### Custom Client + +You can override the `httpx` client to customize it for your use-case. Some common use-cases include support for proxies +and transports. + +```python +import httpx +from browser_use import BrowserUse + +client = BrowserUse( + ..., + httpx_client=httpx.Client( + proxies="http://my.test.proxy.example.com", + transport=httpx.HTTPTransport(local_address="0.0.0.0"), + ), +) +``` + ## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `browser_use_sdk.APIConnectionError` is raised. @@ -388,4 +461,73 @@ Python 3.8 or higher. ## Contributing -See [the contributing documentation](./CONTRIBUTING.md). +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! +## Installation + +```sh +pip install browser-use +``` + +## Reference + +A full reference for this library is available [here](https://github.com/browser-use/browser-use-python/blob/HEAD/./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.tasks.create_task( + task="task", +) +``` + +## Async Client + +The SDK also exports an `async` client so that you can make non-blocking calls to our API. + +```python +import asyncio + +from browser_use import AsyncBrowserUse + +client = AsyncBrowserUse( + api_key="YOUR_API_KEY", +) + + +async def main() -> None: + await client.tasks.create_task( + task="task", + ) + + +asyncio.run(main()) +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error +will be thrown. + +```python +from browser_use.core.api_error import ApiError + +try: + client.tasks.create_task(...) +except ApiError as e: + print(e.status_code) + print(e.body) +``` + diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index fa6b52c..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,27 +0,0 @@ -# Security Policy - -## Reporting Security Issues - -This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. - -To report a security issue, please contact the Stainless team at security@stainless.com. - -## Responsible Disclosure - -We appreciate the efforts of security researchers and individuals who help us maintain the security of -SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible -disclosure practices by allowing us a reasonable amount of time to investigate and address the issue -before making any information public. - -## Reporting Non-SDK Related Security Issues - -If you encounter security issues that are not directly related to SDKs but pertain to the services -or products provided by Browser Use, please follow the respective company's security reporting guidelines. - -### Browser Use Terms and Policies - -Please contact support@browser-use.com for any questions or concerns regarding the security of our services. - ---- - -Thank you for helping us keep the SDKs and systems they interact with secure. diff --git a/api.md b/api.md deleted file mode 100644 index 9c56e23..0000000 --- a/api.md +++ /dev/null @@ -1,115 +0,0 @@ -# Users - -## Me - -Types: - -```python -from browser_use_sdk.types.users import MeRetrieveResponse -``` - -Methods: - -- client.users.me.retrieve() -> MeRetrieveResponse - -### Files - -Types: - -```python -from browser_use_sdk.types.users.me import FileCreatePresignedURLResponse -``` - -Methods: - -- client.users.me.files.create_presigned_url(\*\*params) -> FileCreatePresignedURLResponse - -# Tasks - -Types: - -```python -from browser_use_sdk.types import ( - FileView, - TaskItemView, - TaskStatus, - TaskStepView, - TaskView, - TaskCreateResponse, - TaskListResponse, - TaskGetLogsResponse, - TaskGetOutputFileResponse, - TaskGetUserUploadedFileResponse, -) -``` - -Methods: - -- client.tasks.create(\*\*params) -> TaskCreateResponse -- client.tasks.retrieve(task_id) -> TaskView -- client.tasks.update(task_id, \*\*params) -> TaskView -- client.tasks.list(\*\*params) -> TaskListResponse -- client.tasks.get_logs(task_id) -> TaskGetLogsResponse -- client.tasks.get_output_file(file_id, \*, task_id) -> TaskGetOutputFileResponse -- client.tasks.get_user_uploaded_file(file_id, \*, task_id) -> TaskGetUserUploadedFileResponse - -# Sessions - -Types: - -```python -from browser_use_sdk.types import SessionStatus, SessionView, SessionListResponse -``` - -Methods: - -- client.sessions.retrieve(session_id) -> SessionView -- client.sessions.update(session_id, \*\*params) -> SessionView -- client.sessions.list(\*\*params) -> SessionListResponse -- client.sessions.delete(session_id) -> None - -## PublicShare - -Types: - -```python -from browser_use_sdk.types.sessions import ShareView -``` - -Methods: - -- client.sessions.public_share.create(session_id) -> ShareView -- client.sessions.public_share.retrieve(session_id) -> ShareView -- client.sessions.public_share.delete(session_id) -> None - -# BrowserProfiles - -Types: - -```python -from browser_use_sdk.types import BrowserProfileView, ProxyCountryCode, BrowserProfileListResponse -``` - -Methods: - -- client.browser_profiles.create(\*\*params) -> BrowserProfileView -- client.browser_profiles.retrieve(profile_id) -> BrowserProfileView -- client.browser_profiles.update(profile_id, \*\*params) -> BrowserProfileView -- client.browser_profiles.list(\*\*params) -> BrowserProfileListResponse -- client.browser_profiles.delete(profile_id) -> None - -# AgentProfiles - -Types: - -```python -from browser_use_sdk.types import AgentProfileView, AgentProfileListResponse -``` - -Methods: - -- client.agent_profiles.create(\*\*params) -> AgentProfileView -- client.agent_profiles.retrieve(profile_id) -> AgentProfileView -- client.agent_profiles.update(profile_id, \*\*params) -> AgentProfileView -- client.agent_profiles.list(\*\*params) -> AgentProfileListResponse -- client.agent_profiles.delete(profile_id) -> None diff --git a/bin/check-release-environment b/bin/check-release-environment deleted file mode 100644 index b845b0f..0000000 --- a/bin/check-release-environment +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -errors=() - -if [ -z "${PYPI_TOKEN}" ]; then - errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") -fi - -lenErrors=${#errors[@]} - -if [[ lenErrors -gt 0 ]]; then - echo -e "Found the following errors in the release environment:\n" - - for error in "${errors[@]}"; do - echo -e "- $error\n" - done - - exit 1 -fi - -echo "The environment is ready to push releases!" diff --git a/bin/publish-pypi b/bin/publish-pypi deleted file mode 100644 index 826054e..0000000 --- a/bin/publish-pypi +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -set -eux -mkdir -p dist -rye build --clean -rye publish --yes --token=$PYPI_TOKEN diff --git a/examples/.keep b/examples/.keep deleted file mode 100644 index d8c73e9..0000000 --- a/examples/.keep +++ /dev/null @@ -1,4 +0,0 @@ -File generated from our OpenAPI spec by Stainless. - -This directory can be used to store example files demonstrating usage of this SDK. -It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index d128e00..0000000 --- a/mypy.ini +++ /dev/null @@ -1,50 +0,0 @@ -[mypy] -pretty = True -show_error_codes = True - -# Exclude _files.py because mypy isn't smart enough to apply -# the correct type narrowing and as this is an internal module -# it's fine to just use Pyright. -# -# We also exclude our `tests` as mypy doesn't always infer -# types correctly and Pyright will still catch any type errors. -exclude = ^(src/browser_use_sdk/_files\.py|_dev/.*\.py|tests/.*)$ - -strict_equality = True -implicit_reexport = True -check_untyped_defs = True -no_implicit_optional = True - -warn_return_any = True -warn_unreachable = True -warn_unused_configs = True - -# Turn these options off as it could cause conflicts -# with the Pyright options. -warn_unused_ignores = False -warn_redundant_casts = False - -disallow_any_generics = True -disallow_untyped_defs = True -disallow_untyped_calls = True -disallow_subclassing_any = True -disallow_incomplete_defs = True -disallow_untyped_decorators = True -cache_fine_grained = True - -# By default, mypy reports an error if you assign a value to the result -# of a function call that doesn't return anything. We do this in our test -# cases: -# ``` -# result = ... -# assert result is None -# ``` -# Changing this codegen to make mypy happy would increase complexity -# and would not be worth it. -disable_error_code = func-returns-value,overload-cannot-match - -# https://github.com/python/mypy/issues/12162 -[mypy.overrides] -module = "black.files.*" -ignore_errors = true -ignore_missing_imports = true diff --git a/noxfile.py b/noxfile.py deleted file mode 100644 index 53bca7f..0000000 --- a/noxfile.py +++ /dev/null @@ -1,9 +0,0 @@ -import nox - - -@nox.session(reuse_venv=True, name="test-pydantic-v1") -def test_pydantic_v1(session: nox.Session) -> None: - session.install("-r", "requirements-dev.lock") - session.install("pydantic<2") - - session.run("pytest", "--showlocals", "--ignore=tests/functional", *session.posargs) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..c2ddd86 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,550 @@ +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} + +[[package]] +name = "anyio" +version = "4.5.2" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, + {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "certifi" +version = "2025.8.3" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +files = [ + {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, + {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "mypy" +version = "1.13.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pydantic" +version = "2.10.6" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, + {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.27.2" +typing-extensions = ">=4.12.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, + {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.23.8" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, + {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "ruff" +version = "0.11.5" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b"}, + {file = "ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077"}, + {file = "ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783"}, + {file = "ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe"}, + {file = "ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800"}, + {file = "ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e"}, + {file = "ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef"}, +] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20241206" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, + {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "8551b871abee465e23fb0966d51f2c155fd257b55bdcb0c02d095de19f92f358" diff --git a/pyproject.toml b/pyproject.toml index cbc8b6a..b3ac656 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,211 +1,84 @@ [project] -name = "browser-use-sdk" -version = "1.0.2" -description = "The official Python library for the browser-use API" -dynamic = ["readme"] -license = "Apache-2.0" -authors = [ -{ name = "Browser Use", email = "support@browser-use.com" }, -] -dependencies = [ - "httpx>=0.23.0, <1", - "pydantic>=1.9.0, <3", - "typing-extensions>=4.10, <5", - "anyio>=3.5.0, <5", - "distro>=1.7.0, <2", - "sniffio", -] -requires-python = ">= 3.8" -classifiers = [ - "Typing :: Typed", - "Intended Audience :: Developers", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Operating System :: OS Independent", - "Operating System :: POSIX", - "Operating System :: MacOS", - "Operating System :: POSIX :: Linux", - "Operating System :: Microsoft :: Windows", - "Topic :: Software Development :: Libraries :: Python Modules", - "License :: OSI Approved :: Apache Software License" -] +name = "browser-use" -[project.urls] -Homepage = "https://github.com/browser-use/browser-use-python" -Repository = "https://github.com/browser-use/browser-use-python" - -[project.optional-dependencies] -aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"] +[tool.poetry] +name = "browser-use" +version = "0.0.0" +description = "" +readme = "README.md" +authors = [] +keywords = [] -[tool.rye] -managed = true -# version pins are in requirements-dev.lock -dev-dependencies = [ - "pyright==1.1.399", - "mypy", - "respx", - "pytest", - "pytest-asyncio", - "ruff", - "time-machine", - "nox", - "dirty-equals>=0.6.0", - "importlib-metadata>=6.7.0", - "rich>=13.7.1", - "nest_asyncio==1.6.0", - "pytest-xdist>=3.6.1", +classifiers = [ + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed" ] - -[tool.rye.scripts] -format = { chain = [ - "format:ruff", - "format:docs", - "fix:ruff", - # run formatting again to fix any inconsistencies when imports are stripped - "format:ruff", -]} -"format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" -"format:ruff" = "ruff format" - -"lint" = { chain = [ - "check:ruff", - "typecheck", - "check:importable", -]} -"check:ruff" = "ruff check ." -"fix:ruff" = "ruff check --fix ." - -"check:importable" = "python -c 'import browser_use_sdk'" - -typecheck = { chain = [ - "typecheck:pyright", - "typecheck:mypy" -]} -"typecheck:pyright" = "pyright" -"typecheck:verify-types" = "pyright --verifytypes browser_use_sdk --ignoreexternal" -"typecheck:mypy" = "mypy ." - -[build-system] -requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"] -build-backend = "hatchling.build" - -[tool.hatch.build] -include = [ - "src/*" +packages = [ + { include = "browser_use", from = "src"} ] -[tool.hatch.build.targets.wheel] -packages = ["src/browser_use_sdk"] - -[tool.hatch.build.targets.sdist] -# Basically everything except hidden files/directories (such as .github, .devcontainers, .python-version, etc) -include = [ - "/*.toml", - "/*.json", - "/*.lock", - "/*.md", - "/mypy.ini", - "/noxfile.py", - "bin/*", - "examples/*", - "src/*", - "tests/*", -] - -[tool.hatch.metadata.hooks.fancy-pypi-readme] -content-type = "text/markdown" - -[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] -path = "README.md" - -[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] -# replace relative links with absolute links -pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' -replacement = '[\1](https://github.com/browser-use/browser-use-python/tree/main/\g<2>)' +[project.urls] +Repository = 'https://github.com/browser-use/browser-use-python' + +[tool.poetry.dependencies] +python = "^3.8" +httpx = ">=0.21.2" +pydantic = ">= 1.9.2" +pydantic-core = ">=2.18.2" +typing_extensions = ">= 4.0.0" + +[tool.poetry.group.dev.dependencies] +mypy = "==1.13.0" +pytest = "^7.4.0" +pytest-asyncio = "^0.23.5" +python-dateutil = "^2.9.0" +types-python-dateutil = "^2.9.0.20240316" +ruff = "==0.11.5" [tool.pytest.ini_options] -testpaths = ["tests"] -addopts = "--tb=short -n auto" -xfail_strict = true +testpaths = [ "tests" ] asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "session" -filterwarnings = [ - "error" -] - -[tool.pyright] -# this enables practically every flag given by pyright. -# there are a couple of flags that are still disabled by -# default in strict mode as they are experimental and niche. -typeCheckingMode = "strict" -pythonVersion = "3.8" -exclude = [ - "_dev", - ".venv", - ".nox", -] - -reportImplicitOverride = true -reportOverlappingOverload = false - -reportImportCycles = false -reportPrivateUsage = false +[tool.mypy] +plugins = ["pydantic.mypy"] [tool.ruff] line-length = 120 -output-format = "grouped" -target-version = "py38" - -[tool.ruff.format] -docstring-code-format = true [tool.ruff.lint] select = [ - # isort - "I", - # bugbear rules - "B", - # remove unused imports - "F401", - # bare except statements - "E722", - # unused arguments - "ARG", - # print statements - "T201", - "T203", - # misuse of typing.TYPE_CHECKING - "TC004", - # import rules - "TID251", + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort ] ignore = [ - # mutable defaults - "B006", + "E402", # Module level import not at top of file + "E501", # Line too long + "E711", # Comparison to `None` should be `cond is not None` + "E712", # Avoid equality comparisons to `True`; use `if ...:` checks + "E721", # Use `is` and `is not` for type comparisons, or `isinstance()` for insinstance checks + "E722", # Do not use bare `except` + "E731", # Do not assign a `lambda` expression, use a `def` + "F821", # Undefined name + "F841" # Local variable ... is assigned to but never used ] -unfixable = [ - # disable auto fix for print statements - "T201", - "T203", -] - -[tool.ruff.lint.flake8-tidy-imports.banned-api] -"functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" [tool.ruff.lint.isort] -length-sort = true -length-sort-straight = true -combine-as-imports = true -extra-standard-library = ["typing_extensions"] -known-first-party = ["browser_use_sdk", "tests"] +section-order = ["future", "standard-library", "third-party", "first-party"] -[tool.ruff.lint.per-file-ignores] -"bin/**.py" = ["T201", "T203"] -"scripts/**.py" = ["T201", "T203"] -"tests/**.py" = ["T201", "T203"] -"examples/**.py" = ["T201", "T203"] +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/reference.md b/reference.md new file mode 100644 index 0000000..33df870 --- /dev/null +++ b/reference.md @@ -0,0 +1,1606 @@ +# Reference +## Accounts +
client.accounts.get_account_me() +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get authenticated account information including credit balances and account details. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.accounts.get_account_me() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Tasks +
client.tasks.list_tasks(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get paginated list of AI agent tasks with optional filtering by session and status. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.tasks.list_tasks() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**page_size:** `typing.Optional[int]` + +
+
+ +
+
+ +**page_number:** `typing.Optional[int]` + +
+
+ +
+
+ +**session_id:** `typing.Optional[str]` + +
+
+ +
+
+ +**filter_by:** `typing.Optional[TaskStatus]` + +
+
+ +
+
+ +**after:** `typing.Optional[dt.datetime]` + +
+
+ +
+
+ +**before:** `typing.Optional[dt.datetime]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.tasks.create_task(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +You can either: +1. Start a new task (auto creates a new simple session) +2. Start a new task in an existing session (you can create a custom session before starting the task and reuse it for follow-up tasks) +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.tasks.create_task( + task="task", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**task:** `str` — The task prompt/instruction for the agent. + +
+
+ +
+
+ +**llm:** `typing.Optional[SupportedLlMs]` — The LLM model to use for the agent. + +
+
+ +
+
+ +**start_url:** `typing.Optional[str]` — The URL to start the task from. + +
+
+ +
+
+ +**max_steps:** `typing.Optional[int]` — Maximum number of steps the agent can take before stopping. + +
+
+ +
+
+ +**structured_output:** `typing.Optional[str]` — The stringified JSON schema for the structured output. + +
+
+ +
+
+ +**session_id:** `typing.Optional[str]` — The ID of the session where the task will run. + +
+
+ +
+
+ +**metadata:** `typing.Optional[typing.Dict[str, typing.Optional[str]]]` — The metadata for the task. + +
+
+ +
+
+ +**secrets:** `typing.Optional[typing.Dict[str, typing.Optional[str]]]` — The secrets for the task. + +
+
+ +
+
+ +**allowed_domains:** `typing.Optional[typing.Sequence[str]]` — The allowed domains for the task. + +
+
+ +
+
+ +**highlight_elements:** `typing.Optional[bool]` — Tells the agent to highlight interactive elements on the page. + +
+
+ +
+
+ +**flash_mode:** `typing.Optional[bool]` — Whether agent flash mode is enabled. + +
+
+ +
+
+ +**thinking:** `typing.Optional[bool]` — Whether agent thinking mode is enabled. + +
+
+ +
+
+ +**vision:** `typing.Optional[bool]` — Whether agent vision capabilities are enabled. + +
+
+ +
+
+ +**system_prompt_extension:** `typing.Optional[str]` — Optional extension to the agent system prompt. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.tasks.get_task(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get detailed task information including status, progress, steps, and file outputs. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.tasks.get_task( + task_id="task_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**task_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.tasks.update_task(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Control task execution with stop, pause, resume, or stop task and session actions. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.tasks.update_task( + task_id="task_id", + action="stop", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**task_id:** `str` + +
+
+ +
+
+ +**action:** `TaskUpdateAction` — The action to perform on the task + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.tasks.get_task_logs(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get secure download URL for task execution logs with step-by-step details. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.tasks.get_task_logs( + task_id="task_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**task_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Sessions +
client.sessions.list_sessions(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get paginated list of AI agent sessions with optional status filtering. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.sessions.list_sessions() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**page_size:** `typing.Optional[int]` + +
+
+ +
+
+ +**page_number:** `typing.Optional[int]` + +
+
+ +
+
+ +**filter_by:** `typing.Optional[SessionStatus]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.create_session(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create a new session with a new task. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.sessions.create_session() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**profile_id:** `typing.Optional[str]` — The ID of the profile to use for the session + +
+
+ +
+
+ +**proxy_country_code:** `typing.Optional[ProxyCountryCode]` — Country code for proxy location. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.get_session(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get detailed session information including status, URLs, and task details. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.sessions.get_session( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.delete_session(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Permanently delete a session and all associated data. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.sessions.delete_session( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.update_session(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Stop a session and all its running tasks. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.sessions.update_session( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.get_session_public_share(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get public share information including URL and usage statistics. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.sessions.get_session_public_share( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.create_session_public_share(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create or return existing public share for a session. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.sessions.create_session_public_share( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sessions.delete_session_public_share(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Remove public share for a session. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.sessions.delete_session_public_share( + session_id="session_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Files +
client.files.user_upload_file_presigned_url(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Generate a secure presigned URL for uploading files that AI agents can use during tasks. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.files.user_upload_file_presigned_url( + session_id="session_id", + file_name="fileName", + content_type="image/jpg", + size_bytes=1, +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` + +
+
+ +
+
+ +**file_name:** `str` — The name of the file to upload + +
+
+ +
+
+ +**content_type:** `UploadFileRequestContentType` — The content type of the file to upload + +
+
+ +
+
+ +**size_bytes:** `int` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.files.get_task_output_file_presigned_url(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get secure download URL for an output file generated by the AI agent. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.files.get_task_output_file_presigned_url( + task_id="task_id", + file_id="file_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**task_id:** `str` + +
+
+ +
+
+ +**file_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Profiles +
client.profiles.list_profiles(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get paginated list of profiles. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.profiles.list_profiles() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**page_size:** `typing.Optional[int]` + +
+
+ +
+
+ +**page_number:** `typing.Optional[int]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.profiles.create_profile() +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Profiles allow you to preserve the state of the browser between tasks. + +They are most commonly used to allow users to preserve the log-in state in the agent between tasks. +You'd normally create one profile per user and then use it for all their tasks. + +You can create a new profile by calling this endpoint. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.profiles.create_profile() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.profiles.get_profile(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get profile details. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.profiles.get_profile( + profile_id="profile_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**profile_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.profiles.delete_browser_profile(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Permanently delete a browser profile and its configuration. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from browser_use import BrowserUse + +client = BrowserUse( + api_key="YOUR_API_KEY", +) +client.profiles.delete_browser_profile( + profile_id="profile_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**profile_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ diff --git a/release-please-config.json b/release-please-config.json deleted file mode 100644 index 1225639..0000000 --- a/release-please-config.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "packages": { - ".": {} - }, - "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", - "include-v-in-tag": true, - "include-component-in-tag": false, - "versioning": "prerelease", - "prerelease": true, - "bump-minor-pre-major": true, - "bump-patch-for-minor-pre-major": false, - "pull-request-header": "Automated Release PR", - "pull-request-title-pattern": "release: ${version}", - "changelog-sections": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "perf", - "section": "Performance Improvements" - }, - { - "type": "revert", - "section": "Reverts" - }, - { - "type": "chore", - "section": "Chores" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "style", - "section": "Styles" - }, - { - "type": "refactor", - "section": "Refactors" - }, - { - "type": "test", - "section": "Tests", - "hidden": true - }, - { - "type": "build", - "section": "Build System" - }, - { - "type": "ci", - "section": "Continuous Integration", - "hidden": true - } - ], - "release-type": "python", - "extra-files": [ - "src/browser_use_sdk/_version.py" - ] -} \ No newline at end of file diff --git a/requirements-dev.lock b/requirements-dev.lock deleted file mode 100644 index 57a6b07..0000000 --- a/requirements-dev.lock +++ /dev/null @@ -1,135 +0,0 @@ -# generated by rye -# use `rye lock` or `rye sync` to update this lockfile -# -# last locked with the following flags: -# pre: false -# features: [] -# all-features: true -# with-sources: false -# generate-hashes: false -# universal: false - --e file:. -aiohappyeyeballs==2.6.1 - # via aiohttp -aiohttp==3.12.8 - # via browser-use-sdk - # via httpx-aiohttp -aiosignal==1.3.2 - # via aiohttp -annotated-types==0.6.0 - # via pydantic -anyio==4.4.0 - # via browser-use-sdk - # via httpx -argcomplete==3.1.2 - # via nox -async-timeout==5.0.1 - # via aiohttp -attrs==25.3.0 - # via aiohttp -certifi==2023.7.22 - # via httpcore - # via httpx -colorlog==6.7.0 - # via nox -dirty-equals==0.6.0 -distlib==0.3.7 - # via virtualenv -distro==1.8.0 - # via browser-use-sdk -exceptiongroup==1.2.2 - # via anyio - # via pytest -execnet==2.1.1 - # via pytest-xdist -filelock==3.12.4 - # via virtualenv -frozenlist==1.6.2 - # via aiohttp - # via aiosignal -h11==0.16.0 - # via httpcore -httpcore==1.0.9 - # via httpx -httpx==0.28.1 - # via browser-use-sdk - # via httpx-aiohttp - # via respx -httpx-aiohttp==0.1.8 - # via browser-use-sdk -idna==3.4 - # via anyio - # via httpx - # via yarl -importlib-metadata==7.0.0 -iniconfig==2.0.0 - # via pytest -markdown-it-py==3.0.0 - # via rich -mdurl==0.1.2 - # via markdown-it-py -multidict==6.4.4 - # via aiohttp - # via yarl -mypy==1.14.1 -mypy-extensions==1.0.0 - # via mypy -nest-asyncio==1.6.0 -nodeenv==1.8.0 - # via pyright -nox==2023.4.22 -packaging==23.2 - # via nox - # via pytest -platformdirs==3.11.0 - # via virtualenv -pluggy==1.5.0 - # via pytest -propcache==0.3.1 - # via aiohttp - # via yarl -pydantic==2.10.3 - # via browser-use-sdk -pydantic-core==2.27.1 - # via pydantic -pygments==2.18.0 - # via rich -pyright==1.1.399 -pytest==8.3.3 - # via pytest-asyncio - # via pytest-xdist -pytest-asyncio==0.24.0 -pytest-xdist==3.7.0 -python-dateutil==2.8.2 - # via time-machine -pytz==2023.3.post1 - # via dirty-equals -respx==0.22.0 -rich==13.7.1 -ruff==0.9.4 -setuptools==68.2.2 - # via nodeenv -six==1.16.0 - # via python-dateutil -sniffio==1.3.0 - # via anyio - # via browser-use-sdk -time-machine==2.9.0 -tomli==2.0.2 - # via mypy - # via pytest -typing-extensions==4.12.2 - # via anyio - # via browser-use-sdk - # via multidict - # via mypy - # via pydantic - # via pydantic-core - # via pyright -virtualenv==20.24.5 - # via nox -yarl==1.20.0 - # via aiohttp -zipp==3.17.0 - # via importlib-metadata diff --git a/requirements.lock b/requirements.lock deleted file mode 100644 index bd72e76..0000000 --- a/requirements.lock +++ /dev/null @@ -1,72 +0,0 @@ -# generated by rye -# use `rye lock` or `rye sync` to update this lockfile -# -# last locked with the following flags: -# pre: false -# features: [] -# all-features: true -# with-sources: false -# generate-hashes: false -# universal: false - --e file:. -aiohappyeyeballs==2.6.1 - # via aiohttp -aiohttp==3.12.8 - # via browser-use-sdk - # via httpx-aiohttp -aiosignal==1.3.2 - # via aiohttp -annotated-types==0.6.0 - # via pydantic -anyio==4.4.0 - # via browser-use-sdk - # via httpx -async-timeout==5.0.1 - # via aiohttp -attrs==25.3.0 - # via aiohttp -certifi==2023.7.22 - # via httpcore - # via httpx -distro==1.8.0 - # via browser-use-sdk -exceptiongroup==1.2.2 - # via anyio -frozenlist==1.6.2 - # via aiohttp - # via aiosignal -h11==0.16.0 - # via httpcore -httpcore==1.0.9 - # via httpx -httpx==0.28.1 - # via browser-use-sdk - # via httpx-aiohttp -httpx-aiohttp==0.1.8 - # via browser-use-sdk -idna==3.4 - # via anyio - # via httpx - # via yarl -multidict==6.4.4 - # via aiohttp - # via yarl -propcache==0.3.1 - # via aiohttp - # via yarl -pydantic==2.10.3 - # via browser-use-sdk -pydantic-core==2.27.1 - # via pydantic -sniffio==1.3.0 - # via anyio - # via browser-use-sdk -typing-extensions==4.12.2 - # via anyio - # via browser-use-sdk - # via multidict - # via pydantic - # via pydantic-core -yarl==1.20.0 - # via aiohttp diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e80f640 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +httpx>=0.21.2 +pydantic>= 1.9.2 +pydantic-core>=2.18.2 +typing_extensions>= 4.0.0 diff --git a/scripts/bootstrap b/scripts/bootstrap deleted file mode 100755 index e84fe62..0000000 --- a/scripts/bootstrap +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -if ! command -v rye >/dev/null 2>&1 && [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then - brew bundle check >/dev/null 2>&1 || { - echo "==> Installing Homebrew dependencies…" - brew bundle - } -fi - -echo "==> Installing Python dependencies…" - -# experimental uv support makes installations significantly faster -rye config --set-bool behavior.use-uv=true - -rye sync --all-features diff --git a/scripts/format b/scripts/format deleted file mode 100755 index 667ec2d..0000000 --- a/scripts/format +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -echo "==> Running formatters" -rye run format diff --git a/scripts/lint b/scripts/lint deleted file mode 100755 index 3094ad3..0000000 --- a/scripts/lint +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -echo "==> Running lints" -rye run lint - -echo "==> Making sure it imports" -rye run python -c 'import browser_use_sdk' diff --git a/scripts/mock b/scripts/mock deleted file mode 100755 index 0b28f6e..0000000 --- a/scripts/mock +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -if [[ -n "$1" && "$1" != '--'* ]]; then - URL="$1" - shift -else - URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" -fi - -# Check if the URL is empty -if [ -z "$URL" ]; then - echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" - exit 1 -fi - -echo "==> Starting mock server with URL ${URL}" - -# Run prism mock on the given spec -if [ "$1" == "--daemon" ]; then - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & - - # Wait for server to come online - echo -n "Waiting for server" - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do - echo -n "." - sleep 0.1 - done - - if grep -q "✖ fatal" ".prism.log"; then - cat .prism.log - exit 1 - fi - - echo -else - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" -fi diff --git a/scripts/test b/scripts/test deleted file mode 100755 index dbeda2d..0000000 --- a/scripts/test +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' # No Color - -function prism_is_running() { - curl --silent "http://localhost:4010" >/dev/null 2>&1 -} - -kill_server_on_port() { - pids=$(lsof -t -i tcp:"$1" || echo "") - if [ "$pids" != "" ]; then - kill "$pids" - echo "Stopped $pids." - fi -} - -function is_overriding_api_base_url() { - [ -n "$TEST_API_BASE_URL" ] -} - -if ! is_overriding_api_base_url && ! prism_is_running ; then - # When we exit this script, make sure to kill the background mock server process - trap 'kill_server_on_port 4010' EXIT - - # Start the dev server - ./scripts/mock --daemon -fi - -if is_overriding_api_base_url ; then - echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" - echo -elif ! prism_is_running ; then - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" - echo -e "running against your OpenAPI spec." - echo - echo -e "To run the server, pass in the path or url of your OpenAPI" - echo -e "spec to the prism command:" - echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" - echo - - exit 1 -else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" - echo -fi - -export DEFER_PYDANTIC_BUILD=false - -echo "==> Running tests" -rye run pytest "$@" - -echo "==> Running Pydantic v1 tests" -rye run nox -s test-pydantic-v1 -- "$@" diff --git a/scripts/utils/ruffen-docs.py b/scripts/utils/ruffen-docs.py deleted file mode 100644 index 0cf2bd2..0000000 --- a/scripts/utils/ruffen-docs.py +++ /dev/null @@ -1,167 +0,0 @@ -# fork of https://github.com/asottile/blacken-docs adapted for ruff -from __future__ import annotations - -import re -import sys -import argparse -import textwrap -import contextlib -import subprocess -from typing import Match, Optional, Sequence, Generator, NamedTuple, cast - -MD_RE = re.compile( - r"(?P^(?P *)```\s*python\n)" r"(?P.*?)" r"(?P^(?P=indent)```\s*$)", - re.DOTALL | re.MULTILINE, -) -MD_PYCON_RE = re.compile( - r"(?P^(?P *)```\s*pycon\n)" r"(?P.*?)" r"(?P^(?P=indent)```.*$)", - re.DOTALL | re.MULTILINE, -) -PYCON_PREFIX = ">>> " -PYCON_CONTINUATION_PREFIX = "..." -PYCON_CONTINUATION_RE = re.compile( - rf"^{re.escape(PYCON_CONTINUATION_PREFIX)}( |$)", -) -DEFAULT_LINE_LENGTH = 100 - - -class CodeBlockError(NamedTuple): - offset: int - exc: Exception - - -def format_str( - src: str, -) -> tuple[str, Sequence[CodeBlockError]]: - errors: list[CodeBlockError] = [] - - @contextlib.contextmanager - def _collect_error(match: Match[str]) -> Generator[None, None, None]: - try: - yield - except Exception as e: - errors.append(CodeBlockError(match.start(), e)) - - def _md_match(match: Match[str]) -> str: - code = textwrap.dedent(match["code"]) - with _collect_error(match): - code = format_code_block(code) - code = textwrap.indent(code, match["indent"]) - return f"{match['before']}{code}{match['after']}" - - def _pycon_match(match: Match[str]) -> str: - code = "" - fragment = cast(Optional[str], None) - - def finish_fragment() -> None: - nonlocal code - nonlocal fragment - - if fragment is not None: - with _collect_error(match): - fragment = format_code_block(fragment) - fragment_lines = fragment.splitlines() - code += f"{PYCON_PREFIX}{fragment_lines[0]}\n" - for line in fragment_lines[1:]: - # Skip blank lines to handle Black adding a blank above - # functions within blocks. A blank line would end the REPL - # continuation prompt. - # - # >>> if True: - # ... def f(): - # ... pass - # ... - if line: - code += f"{PYCON_CONTINUATION_PREFIX} {line}\n" - if fragment_lines[-1].startswith(" "): - code += f"{PYCON_CONTINUATION_PREFIX}\n" - fragment = None - - indentation = None - for line in match["code"].splitlines(): - orig_line, line = line, line.lstrip() - if indentation is None and line: - indentation = len(orig_line) - len(line) - continuation_match = PYCON_CONTINUATION_RE.match(line) - if continuation_match and fragment is not None: - fragment += line[continuation_match.end() :] + "\n" - else: - finish_fragment() - if line.startswith(PYCON_PREFIX): - fragment = line[len(PYCON_PREFIX) :] + "\n" - else: - code += orig_line[indentation:] + "\n" - finish_fragment() - return code - - def _md_pycon_match(match: Match[str]) -> str: - code = _pycon_match(match) - code = textwrap.indent(code, match["indent"]) - return f"{match['before']}{code}{match['after']}" - - src = MD_RE.sub(_md_match, src) - src = MD_PYCON_RE.sub(_md_pycon_match, src) - return src, errors - - -def format_code_block(code: str) -> str: - return subprocess.check_output( - [ - sys.executable, - "-m", - "ruff", - "format", - "--stdin-filename=script.py", - f"--line-length={DEFAULT_LINE_LENGTH}", - ], - encoding="utf-8", - input=code, - ) - - -def format_file( - filename: str, - skip_errors: bool, -) -> int: - with open(filename, encoding="UTF-8") as f: - contents = f.read() - new_contents, errors = format_str(contents) - for error in errors: - lineno = contents[: error.offset].count("\n") + 1 - print(f"{filename}:{lineno}: code block parse error {error.exc}") - if errors and not skip_errors: - return 1 - if contents != new_contents: - print(f"{filename}: Rewriting...") - with open(filename, "w", encoding="UTF-8") as f: - f.write(new_contents) - return 0 - else: - return 0 - - -def main(argv: Sequence[str] | None = None) -> int: - parser = argparse.ArgumentParser() - parser.add_argument( - "-l", - "--line-length", - type=int, - default=DEFAULT_LINE_LENGTH, - ) - parser.add_argument( - "-S", - "--skip-string-normalization", - action="store_true", - ) - parser.add_argument("-E", "--skip-errors", action="store_true") - parser.add_argument("filenames", nargs="*") - args = parser.parse_args(argv) - - retv = 0 - for filename in args.filenames: - retv |= format_file(filename, skip_errors=args.skip_errors) - return retv - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh deleted file mode 100755 index 145854e..0000000 --- a/scripts/utils/upload-artifact.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -set -exuo pipefail - -FILENAME=$(basename dist/*.whl) - -RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \ - -H "Authorization: Bearer $AUTH" \ - -H "Content-Type: application/json") - -SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url') - -if [[ "$SIGNED_URL" == "null" ]]; then - echo -e "\033[31mFailed to get signed URL.\033[0m" - exit 1 -fi - -UPLOAD_RESPONSE=$(curl -v -X PUT \ - -H "Content-Type: binary/octet-stream" \ - --data-binary "@dist/$FILENAME" "$SIGNED_URL" 2>&1) - -if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then - echo -e "\033[32mUploaded build to Stainless storage.\033[0m" - echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/browser-use-python/$SHA/$FILENAME'\033[0m" -else - echo -e "\033[31mFailed to upload artifact.\033[0m" - exit 1 -fi diff --git a/src/browser_use/__init__.py b/src/browser_use/__init__.py new file mode 100644 index 0000000..b415d1a --- /dev/null +++ b/src/browser_use/__init__.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +from . import accounts, files, profiles, sessions, tasks +from .client import AsyncBrowserUse, BrowserUse +from .version import __version__ + +__all__ = ["AsyncBrowserUse", "BrowserUse", "__version__", "accounts", "files", "profiles", "sessions", "tasks"] diff --git a/src/browser_use/accounts/__init__.py b/src/browser_use/accounts/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/browser_use/accounts/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/browser_use/accounts/client.py b/src/browser_use/accounts/client.py new file mode 100644 index 0000000..ba05a0e --- /dev/null +++ b/src/browser_use/accounts/client.py @@ -0,0 +1,100 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.account_view import AccountView +from .raw_client import AsyncRawAccountsClient, RawAccountsClient + + +class AccountsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawAccountsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawAccountsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawAccountsClient + """ + return self._raw_client + + def get_account_me(self, *, request_options: typing.Optional[RequestOptions] = None) -> AccountView: + """ + Get authenticated account information including credit balances and account details. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AccountView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.accounts.get_account_me() + """ + _response = self._raw_client.get_account_me(request_options=request_options) + return _response.data + + +class AsyncAccountsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawAccountsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawAccountsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawAccountsClient + """ + return self._raw_client + + async def get_account_me(self, *, request_options: typing.Optional[RequestOptions] = None) -> AccountView: + """ + Get authenticated account information including credit balances and account details. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AccountView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.accounts.get_account_me() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_account_me(request_options=request_options) + return _response.data diff --git a/src/browser_use/accounts/raw_client.py b/src/browser_use/accounts/raw_client.py new file mode 100644 index 0000000..a391e4b --- /dev/null +++ b/src/browser_use/accounts/raw_client.py @@ -0,0 +1,114 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.not_found_error import NotFoundError +from ..types.account_view import AccountView + + +class RawAccountsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_account_me(self, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[AccountView]: + """ + Get authenticated account information including credit balances and account details. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[AccountView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + "accounts/me", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + AccountView, + construct_type( + type_=AccountView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawAccountsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_account_me( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[AccountView]: + """ + Get authenticated account information including credit balances and account details. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[AccountView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + "accounts/me", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + AccountView, + construct_type( + type_=AccountView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/browser_use/client.py b/src/browser_use/client.py new file mode 100644 index 0000000..675e144 --- /dev/null +++ b/src/browser_use/client.py @@ -0,0 +1,165 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from .accounts.client import AccountsClient, AsyncAccountsClient +from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .environment import BrowserUseEnvironment +from .files.client import AsyncFilesClient, FilesClient +from .profiles.client import AsyncProfilesClient, ProfilesClient +from .sessions.client import AsyncSessionsClient, SessionsClient +from .tasks.client import AsyncTasksClient, TasksClient + + +class BrowserUse: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : typing.Optional[str] + The base url to use for requests from the client. + + environment : BrowserUseEnvironment + The environment to use for requests from the client. from .environment import BrowserUseEnvironment + + + + Defaults to BrowserUseEnvironment.PRODUCTION + + + + api_key : str + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.Client] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + """ + + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: BrowserUseEnvironment = BrowserUseEnvironment.PRODUCTION, + api_key: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.Client] = None, + ): + _defaulted_timeout = ( + timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read + ) + self._client_wrapper = SyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + api_key=api_key, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.Client(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + self.accounts = AccountsClient(client_wrapper=self._client_wrapper) + self.tasks = TasksClient(client_wrapper=self._client_wrapper) + self.sessions = SessionsClient(client_wrapper=self._client_wrapper) + self.files = FilesClient(client_wrapper=self._client_wrapper) + self.profiles = ProfilesClient(client_wrapper=self._client_wrapper) + + +class AsyncBrowserUse: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : typing.Optional[str] + The base url to use for requests from the client. + + environment : BrowserUseEnvironment + The environment to use for requests from the client. from .environment import BrowserUseEnvironment + + + + Defaults to BrowserUseEnvironment.PRODUCTION + + + + api_key : str + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.AsyncClient] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + """ + + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: BrowserUseEnvironment = BrowserUseEnvironment.PRODUCTION, + api_key: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.AsyncClient] = None, + ): + _defaulted_timeout = ( + timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read + ) + self._client_wrapper = AsyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + api_key=api_key, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.AsyncClient(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + self.accounts = AsyncAccountsClient(client_wrapper=self._client_wrapper) + self.tasks = AsyncTasksClient(client_wrapper=self._client_wrapper) + self.sessions = AsyncSessionsClient(client_wrapper=self._client_wrapper) + self.files = AsyncFilesClient(client_wrapper=self._client_wrapper) + self.profiles = AsyncProfilesClient(client_wrapper=self._client_wrapper) + + +def _get_base_url(*, base_url: typing.Optional[str] = None, environment: BrowserUseEnvironment) -> str: + if base_url is not None: + return base_url + elif environment is not None: + return environment.value + else: + raise Exception("Please pass in either base_url or environment to construct the client") diff --git a/src/browser_use/core/__init__.py b/src/browser_use/core/__init__.py new file mode 100644 index 0000000..73955ba --- /dev/null +++ b/src/browser_use/core/__init__.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +from .file import File, with_content_type + +__all__ = ["File", "with_content_type"] diff --git a/src/browser_use/core/api_error.py b/src/browser_use/core/api_error.py new file mode 100644 index 0000000..6f850a6 --- /dev/null +++ b/src/browser_use/core/api_error.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Optional + + +class ApiError(Exception): + headers: Optional[Dict[str, str]] + status_code: Optional[int] + body: Any + + def __init__( + self, + *, + headers: Optional[Dict[str, str]] = None, + status_code: Optional[int] = None, + body: Any = None, + ) -> None: + self.headers = headers + self.status_code = status_code + self.body = body + + def __str__(self) -> str: + return f"headers: {self.headers}, status_code: {self.status_code}, body: {self.body}" diff --git a/src/browser_use/core/client_wrapper.py b/src/browser_use/core/client_wrapper.py new file mode 100644 index 0000000..3cb5004 --- /dev/null +++ b/src/browser_use/core/client_wrapper.py @@ -0,0 +1,78 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from .http_client import AsyncHttpClient, HttpClient + + +class BaseClientWrapper: + def __init__( + self, + *, + api_key: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + ): + self.api_key = api_key + self._headers = headers + self._base_url = base_url + self._timeout = timeout + + def get_headers(self) -> typing.Dict[str, str]: + headers: typing.Dict[str, str] = { + "X-Fern-Language": "Python", + "X-Fern-SDK-Name": "browser-use", + "X-Fern-SDK-Version": "0.0.0", + **(self.get_custom_headers() or {}), + } + headers["X-Browser-Use-API-Key"] = self.api_key + return headers + + def get_custom_headers(self) -> typing.Optional[typing.Dict[str, str]]: + return self._headers + + def get_base_url(self) -> str: + return self._base_url + + def get_timeout(self) -> typing.Optional[float]: + return self._timeout + + +class SyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + api_key: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + httpx_client: httpx.Client, + ): + super().__init__(api_key=api_key, headers=headers, base_url=base_url, timeout=timeout) + self.httpx_client = HttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, + ) + + +class AsyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + api_key: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + httpx_client: httpx.AsyncClient, + ): + super().__init__(api_key=api_key, headers=headers, base_url=base_url, timeout=timeout) + self.httpx_client = AsyncHttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, + ) diff --git a/src/browser_use/core/datetime_utils.py b/src/browser_use/core/datetime_utils.py new file mode 100644 index 0000000..7c9864a --- /dev/null +++ b/src/browser_use/core/datetime_utils.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt + + +def serialize_datetime(v: dt.datetime) -> str: + """ + Serialize a datetime including timezone info. + + Uses the timezone info provided if present, otherwise uses the current runtime's timezone info. + + UTC datetimes end in "Z" while all other timezones are represented as offset from UTC, e.g. +05:00. + """ + + def _serialize_zoned_datetime(v: dt.datetime) -> str: + if v.tzinfo is not None and v.tzinfo.tzname(None) == dt.timezone.utc.tzname(None): + # UTC is a special case where we use "Z" at the end instead of "+00:00" + return v.isoformat().replace("+00:00", "Z") + else: + # Delegate to the typical +/- offset format + return v.isoformat() + + if v.tzinfo is not None: + return _serialize_zoned_datetime(v) + else: + local_tz = dt.datetime.now().astimezone().tzinfo + localized_dt = v.replace(tzinfo=local_tz) + return _serialize_zoned_datetime(localized_dt) diff --git a/src/browser_use/core/file.py b/src/browser_use/core/file.py new file mode 100644 index 0000000..44b0d27 --- /dev/null +++ b/src/browser_use/core/file.py @@ -0,0 +1,67 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import IO, Dict, List, Mapping, Optional, Tuple, Union, cast + +# File typing inspired by the flexibility of types within the httpx library +# https://github.com/encode/httpx/blob/master/httpx/_types.py +FileContent = Union[IO[bytes], bytes, str] +File = Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], FileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[ + Optional[str], + FileContent, + Optional[str], + Mapping[str, str], + ], +] + + +def convert_file_dict_to_httpx_tuples( + d: Dict[str, Union[File, List[File]]], +) -> List[Tuple[str, File]]: + """ + The format we use is a list of tuples, where the first element is the + name of the file and the second is the file object. Typically HTTPX wants + a dict, but to be able to send lists of files, you have to use the list + approach (which also works for non-lists) + https://github.com/encode/httpx/pull/1032 + """ + + httpx_tuples = [] + for key, file_like in d.items(): + if isinstance(file_like, list): + for file_like_item in file_like: + httpx_tuples.append((key, file_like_item)) + else: + httpx_tuples.append((key, file_like)) + return httpx_tuples + + +def with_content_type(*, file: File, default_content_type: str) -> File: + """ + This function resolves to the file's content type, if provided, and defaults + to the default_content_type value if not. + """ + if isinstance(file, tuple): + if len(file) == 2: + filename, content = cast(Tuple[Optional[str], FileContent], file) # type: ignore + return (filename, content, default_content_type) + elif len(file) == 3: + filename, content, file_content_type = cast(Tuple[Optional[str], FileContent, Optional[str]], file) # type: ignore + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type) + elif len(file) == 4: + filename, content, file_content_type, headers = cast( # type: ignore + Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], file + ) + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type, headers) + else: + raise ValueError(f"Unexpected tuple length: {len(file)}") + return (None, file, default_content_type) diff --git a/src/browser_use/core/force_multipart.py b/src/browser_use/core/force_multipart.py new file mode 100644 index 0000000..ae24ccf --- /dev/null +++ b/src/browser_use/core/force_multipart.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + + +class ForceMultipartDict(dict): + """ + A dictionary subclass that always evaluates to True in boolean contexts. + + This is used to force multipart/form-data encoding in HTTP requests even when + the dictionary is empty, which would normally evaluate to False. + """ + + def __bool__(self): + return True + + +FORCE_MULTIPART = ForceMultipartDict() diff --git a/src/browser_use/core/http_client.py b/src/browser_use/core/http_client.py new file mode 100644 index 0000000..e4173f9 --- /dev/null +++ b/src/browser_use/core/http_client.py @@ -0,0 +1,543 @@ +# This file was auto-generated by Fern from our API Definition. + +import asyncio +import email.utils +import re +import time +import typing +import urllib.parse +from contextlib import asynccontextmanager, contextmanager +from random import random + +import httpx +from .file import File, convert_file_dict_to_httpx_tuples +from .force_multipart import FORCE_MULTIPART +from .jsonable_encoder import jsonable_encoder +from .query_encoder import encode_query +from .remove_none_from_dict import remove_none_from_dict +from .request_options import RequestOptions +from httpx._types import RequestFiles + +INITIAL_RETRY_DELAY_SECONDS = 0.5 +MAX_RETRY_DELAY_SECONDS = 10 +MAX_RETRY_DELAY_SECONDS_FROM_HEADER = 30 + + +def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait. + + Inspired by the urllib3 retry implementation. + """ + retry_after_ms = response_headers.get("retry-after-ms") + if retry_after_ms is not None: + try: + return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0 + except Exception: + pass + + retry_after = response_headers.get("retry-after") + if retry_after is None: + return None + + # Attempt to parse the header as an int. + if re.match(r"^\s*[0-9]+\s*$", retry_after): + seconds = float(retry_after) + # Fallback to parsing it as a date. + else: + retry_date_tuple = email.utils.parsedate_tz(retry_after) + if retry_date_tuple is None: + return None + if retry_date_tuple[9] is None: # Python 2 + # Assume UTC if no timezone was specified + # On Python2.7, parsedate_tz returns None for a timezone offset + # instead of 0 if no timezone is given, where mktime_tz treats + # a None timezone offset as local time. + retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:] + + retry_date = email.utils.mktime_tz(retry_date_tuple) + seconds = retry_date - time.time() + + if seconds < 0: + seconds = 0 + + return seconds + + +def _retry_timeout(response: httpx.Response, retries: int) -> float: + """ + Determine the amount of time to wait before retrying a request. + This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff + with a jitter to determine the number of seconds to wait. + """ + + # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. + retry_after = _parse_retry_after(response.headers) + if retry_after is not None and retry_after <= MAX_RETRY_DELAY_SECONDS_FROM_HEADER: + return retry_after + + # Apply exponential backoff, capped at MAX_RETRY_DELAY_SECONDS. + retry_delay = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + + # Add a randomness / jitter to the retry delay to avoid overwhelming the server with retries. + timeout = retry_delay * (1 - 0.25 * random()) + return timeout if timeout >= 0 else 0 + + +def _should_retry(response: httpx.Response) -> bool: + retryable_400s = [429, 408, 409] + return response.status_code >= 500 or response.status_code in retryable_400s + + +def remove_omit_from_dict( + original: typing.Dict[str, typing.Optional[typing.Any]], + omit: typing.Optional[typing.Any], +) -> typing.Dict[str, typing.Any]: + if omit is None: + return original + new: typing.Dict[str, typing.Any] = {} + for key, value in original.items(): + if value is not omit: + new[key] = value + return new + + +def maybe_filter_request_body( + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Optional[typing.Any]: + if data is None: + return ( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else None + ) + elif not isinstance(data, typing.Mapping): + data_content = jsonable_encoder(data) + else: + data_content = { + **(jsonable_encoder(remove_omit_from_dict(data, omit))), # type: ignore + **( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else {} + ), + } + return data_content + + +# Abstracted out for testing purposes +def get_request_body( + *, + json: typing.Optional[typing.Any], + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Tuple[typing.Optional[typing.Any], typing.Optional[typing.Any]]: + json_body = None + data_body = None + if data is not None: + data_body = maybe_filter_request_body(data, request_options, omit) + else: + # If both data and json are None, we send json data in the event extra properties are specified + json_body = maybe_filter_request_body(json, request_options, omit) + + # If you have an empty JSON body, you should just send None + return (json_body if json_body != {} else None), data_body if data_body != {} else None + + +class HttpClient: + def __init__( + self, + *, + httpx_client: httpx.Client, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.httpx_client = httpx_client + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 2, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + response = self.httpx_client.request( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + + max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0 + if _should_retry(response=response): + if max_retries > retries: + time.sleep(_retry_timeout(response=response, retries=retries)) + return self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + ) + + return response + + @contextmanager + def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 2, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> typing.Iterator[httpx.Response]: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + with self.httpx_client.stream( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) as stream: + yield stream + + +class AsyncHttpClient: + def __init__( + self, + *, + httpx_client: httpx.AsyncClient, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.httpx_client = httpx_client + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + async def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 2, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + # Add the input to each of these and do None-safety checks + response = await self.httpx_client.request( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + + max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0 + if _should_retry(response=response): + if max_retries > retries: + await asyncio.sleep(_retry_timeout(response=response, retries=retries)) + return await self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + ) + return response + + @asynccontextmanager + async def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 2, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> typing.AsyncIterator[httpx.Response]: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + async with self.httpx_client.stream( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit=omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) as stream: + yield stream diff --git a/src/browser_use/core/http_response.py b/src/browser_use/core/http_response.py new file mode 100644 index 0000000..48a1798 --- /dev/null +++ b/src/browser_use/core/http_response.py @@ -0,0 +1,55 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Dict, Generic, TypeVar + +import httpx + +T = TypeVar("T") +"""Generic to represent the underlying type of the data wrapped by the HTTP response.""" + + +class BaseHttpResponse: + """Minimalist HTTP response wrapper that exposes response headers.""" + + _response: httpx.Response + + def __init__(self, response: httpx.Response): + self._response = response + + @property + def headers(self) -> Dict[str, str]: + return dict(self._response.headers) + + +class HttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + def close(self) -> None: + self._response.close() + + +class AsyncHttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + async def close(self) -> None: + await self._response.aclose() diff --git a/src/browser_use/core/jsonable_encoder.py b/src/browser_use/core/jsonable_encoder.py new file mode 100644 index 0000000..afee366 --- /dev/null +++ b/src/browser_use/core/jsonable_encoder.py @@ -0,0 +1,100 @@ +# This file was auto-generated by Fern from our API Definition. + +""" +jsonable_encoder converts a Python object to a JSON-friendly dict +(e.g. datetimes to strings, Pydantic models to dicts). + +Taken from FastAPI, and made a bit simpler +https://github.com/tiangolo/fastapi/blob/master/fastapi/encoders.py +""" + +import base64 +import dataclasses +import datetime as dt +from enum import Enum +from pathlib import PurePath +from types import GeneratorType +from typing import Any, Callable, Dict, List, Optional, Set, Union + +import pydantic +from .datetime_utils import serialize_datetime +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + encode_by_type, + to_jsonable_with_fallback, +) + +SetIntStr = Set[Union[int, str]] +DictIntStrAny = Dict[Union[int, str], Any] + + +def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None) -> Any: + custom_encoder = custom_encoder or {} + if custom_encoder: + if type(obj) in custom_encoder: + return custom_encoder[type(obj)](obj) + else: + for encoder_type, encoder_instance in custom_encoder.items(): + if isinstance(obj, encoder_type): + return encoder_instance(obj) + if isinstance(obj, pydantic.BaseModel): + if IS_PYDANTIC_V2: + encoder = getattr(obj.model_config, "json_encoders", {}) # type: ignore # Pydantic v2 + else: + encoder = getattr(obj.__config__, "json_encoders", {}) # type: ignore # Pydantic v1 + if custom_encoder: + encoder.update(custom_encoder) + obj_dict = obj.dict(by_alias=True) + if "__root__" in obj_dict: + obj_dict = obj_dict["__root__"] + if "root" in obj_dict: + obj_dict = obj_dict["root"] + return jsonable_encoder(obj_dict, custom_encoder=encoder) + if dataclasses.is_dataclass(obj): + obj_dict = dataclasses.asdict(obj) # type: ignore + return jsonable_encoder(obj_dict, custom_encoder=custom_encoder) + if isinstance(obj, bytes): + return base64.b64encode(obj).decode("utf-8") + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, PurePath): + return str(obj) + if isinstance(obj, (str, int, float, type(None))): + return obj + if isinstance(obj, dt.datetime): + return serialize_datetime(obj) + if isinstance(obj, dt.date): + return str(obj) + if isinstance(obj, dict): + encoded_dict = {} + allowed_keys = set(obj.keys()) + for key, value in obj.items(): + if key in allowed_keys: + encoded_key = jsonable_encoder(key, custom_encoder=custom_encoder) + encoded_value = jsonable_encoder(value, custom_encoder=custom_encoder) + encoded_dict[encoded_key] = encoded_value + return encoded_dict + if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): + encoded_list = [] + for item in obj: + encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder)) + return encoded_list + + def fallback_serializer(o: Any) -> Any: + attempt_encode = encode_by_type(o) + if attempt_encode is not None: + return attempt_encode + + try: + data = dict(o) + except Exception as e: + errors: List[Exception] = [] + errors.append(e) + try: + data = vars(o) + except Exception as e: + errors.append(e) + raise ValueError(errors) from e + return jsonable_encoder(data, custom_encoder=custom_encoder) + + return to_jsonable_with_fallback(obj, fallback_serializer) diff --git a/src/browser_use/core/pydantic_utilities.py b/src/browser_use/core/pydantic_utilities.py new file mode 100644 index 0000000..7db2950 --- /dev/null +++ b/src/browser_use/core/pydantic_utilities.py @@ -0,0 +1,255 @@ +# This file was auto-generated by Fern from our API Definition. + +# nopycln: file +import datetime as dt +from collections import defaultdict +from typing import Any, Callable, ClassVar, Dict, List, Mapping, Optional, Set, Tuple, Type, TypeVar, Union, cast + +import pydantic + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +if IS_PYDANTIC_V2: + from pydantic.v1.datetime_parse import parse_date as parse_date + from pydantic.v1.datetime_parse import parse_datetime as parse_datetime + from pydantic.v1.fields import ModelField as ModelField + from pydantic.v1.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore[attr-defined] + from pydantic.v1.typing import get_args as get_args + from pydantic.v1.typing import get_origin as get_origin + from pydantic.v1.typing import is_literal_type as is_literal_type + from pydantic.v1.typing import is_union as is_union +else: + from pydantic.datetime_parse import parse_date as parse_date # type: ignore[no-redef] + from pydantic.datetime_parse import parse_datetime as parse_datetime # type: ignore[no-redef] + from pydantic.fields import ModelField as ModelField # type: ignore[attr-defined, no-redef] + from pydantic.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore[no-redef] + from pydantic.typing import get_args as get_args # type: ignore[no-redef] + from pydantic.typing import get_origin as get_origin # type: ignore[no-redef] + from pydantic.typing import is_literal_type as is_literal_type # type: ignore[no-redef] + from pydantic.typing import is_union as is_union # type: ignore[no-redef] + +from .datetime_utils import serialize_datetime +from .serialization import convert_and_respect_annotation_metadata +from typing_extensions import TypeAlias + +T = TypeVar("T") +Model = TypeVar("Model", bound=pydantic.BaseModel) + + +def parse_obj_as(type_: Type[T], object_: Any) -> T: + dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + if IS_PYDANTIC_V2: + adapter = pydantic.TypeAdapter(type_) # type: ignore[attr-defined] + return adapter.validate_python(dealiased_object) + return pydantic.parse_obj_as(type_, dealiased_object) + + +def to_jsonable_with_fallback(obj: Any, fallback_serializer: Callable[[Any], Any]) -> Any: + if IS_PYDANTIC_V2: + from pydantic_core import to_jsonable_python + + return to_jsonable_python(obj, fallback=fallback_serializer) + return fallback_serializer(obj) + + +class UniversalBaseModel(pydantic.BaseModel): + if IS_PYDANTIC_V2: + model_config: ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( # type: ignore[typeddict-unknown-key] + # Allow fields beginning with `model_` to be used in the model + protected_namespaces=(), + ) + + @pydantic.model_serializer(mode="plain", when_used="json") # type: ignore[attr-defined] + def serialize_model(self) -> Any: # type: ignore[name-defined] + serialized = self.model_dump() + data = {k: serialize_datetime(v) if isinstance(v, dt.datetime) else v for k, v in serialized.items()} + return data + + else: + + class Config: + smart_union = True + json_encoders = {dt.datetime: serialize_datetime} + + @classmethod + def model_construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") + return cls.construct(_fields_set, **dealiased_object) + + @classmethod + def construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") + if IS_PYDANTIC_V2: + return super().model_construct(_fields_set, **dealiased_object) # type: ignore[misc] + return super().construct(_fields_set, **dealiased_object) + + def json(self, **kwargs: Any) -> str: + kwargs_with_defaults = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + if IS_PYDANTIC_V2: + return super().model_dump_json(**kwargs_with_defaults) # type: ignore[misc] + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: Any) -> Dict[str, Any]: + """ + Override the default dict method to `exclude_unset` by default. This function patches + `exclude_unset` to work include fields within non-None default values. + """ + # Note: the logic here is multiplexed given the levers exposed in Pydantic V1 vs V2 + # Pydantic V1's .dict can be extremely slow, so we do not want to call it twice. + # + # We'd ideally do the same for Pydantic V2, but it shells out to a library to serialize models + # that we have less control over, and this is less intrusive than custom serializers for now. + if IS_PYDANTIC_V2: + kwargs_with_defaults_exclude_unset = { + **kwargs, + "by_alias": True, + "exclude_unset": True, + "exclude_none": False, + } + kwargs_with_defaults_exclude_none = { + **kwargs, + "by_alias": True, + "exclude_none": True, + "exclude_unset": False, + } + dict_dump = deep_union_pydantic_dicts( + super().model_dump(**kwargs_with_defaults_exclude_unset), # type: ignore[misc] + super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore[misc] + ) + + else: + _fields_set = self.__fields_set__.copy() + + fields = _get_model_fields(self.__class__) + for name, field in fields.items(): + if name not in _fields_set: + default = _get_field_default(field) + + # If the default values are non-null act like they've been set + # This effectively allows exclude_unset to work like exclude_none where + # the latter passes through intentionally set none values. + if default is not None or ("exclude_unset" in kwargs and not kwargs["exclude_unset"]): + _fields_set.add(name) + + if default is not None: + self.__fields_set__.add(name) + + kwargs_with_defaults_exclude_unset_include_fields = { + "by_alias": True, + "exclude_unset": True, + "include": _fields_set, + **kwargs, + } + + dict_dump = super().dict(**kwargs_with_defaults_exclude_unset_include_fields) + + return convert_and_respect_annotation_metadata(object_=dict_dump, annotation=self.__class__, direction="write") + + +def _union_list_of_pydantic_dicts(source: List[Any], destination: List[Any]) -> List[Any]: + converted_list: List[Any] = [] + for i, item in enumerate(source): + destination_value = destination[i] + if isinstance(item, dict): + converted_list.append(deep_union_pydantic_dicts(item, destination_value)) + elif isinstance(item, list): + converted_list.append(_union_list_of_pydantic_dicts(item, destination_value)) + else: + converted_list.append(item) + return converted_list + + +def deep_union_pydantic_dicts(source: Dict[str, Any], destination: Dict[str, Any]) -> Dict[str, Any]: + for key, value in source.items(): + node = destination.setdefault(key, {}) + if isinstance(value, dict): + deep_union_pydantic_dicts(value, node) + # Note: we do not do this same processing for sets given we do not have sets of models + # and given the sets are unordered, the processing of the set and matching objects would + # be non-trivial. + elif isinstance(value, list): + destination[key] = _union_list_of_pydantic_dicts(value, node) + else: + destination[key] = value + + return destination + + +if IS_PYDANTIC_V2: + + class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore[misc, name-defined, type-arg] + pass + + UniversalRootModel: TypeAlias = V2RootModel # type: ignore[misc] +else: + UniversalRootModel: TypeAlias = UniversalBaseModel # type: ignore[misc, no-redef] + + +def encode_by_type(o: Any) -> Any: + encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict(tuple) + for type_, encoder in encoders_by_type.items(): + encoders_by_class_tuples[encoder] += (type_,) + + if type(o) in encoders_by_type: + return encoders_by_type[type(o)](o) + for encoder, classes_tuple in encoders_by_class_tuples.items(): + if isinstance(o, classes_tuple): + return encoder(o) + + +def update_forward_refs(model: Type["Model"], **localns: Any) -> None: + if IS_PYDANTIC_V2: + model.model_rebuild(raise_errors=False) # type: ignore[attr-defined] + else: + model.update_forward_refs(**localns) + + +# Mirrors Pydantic's internal typing +AnyCallable = Callable[..., Any] + + +def universal_root_validator( + pre: bool = False, +) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return cast(AnyCallable, pydantic.model_validator(mode="before" if pre else "after")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.root_validator(pre=pre)(func)) # type: ignore[call-overload] + + return decorator + + +def universal_field_validator(field_name: str, pre: bool = False) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return cast(AnyCallable, pydantic.field_validator(field_name, mode="before" if pre else "after")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.validator(field_name, pre=pre)(func)) + + return decorator + + +PydanticField = Union[ModelField, pydantic.fields.FieldInfo] + + +def _get_model_fields(model: Type["Model"]) -> Mapping[str, PydanticField]: + if IS_PYDANTIC_V2: + return cast(Mapping[str, PydanticField], model.model_fields) # type: ignore[attr-defined] + return cast(Mapping[str, PydanticField], model.__fields__) + + +def _get_field_default(field: PydanticField) -> Any: + try: + value = field.get_default() # type: ignore[union-attr] + except: + value = field.default + if IS_PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value diff --git a/src/browser_use/core/query_encoder.py b/src/browser_use/core/query_encoder.py new file mode 100644 index 0000000..3183001 --- /dev/null +++ b/src/browser_use/core/query_encoder.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, List, Optional, Tuple + +import pydantic + + +# Flattens dicts to be of the form {"key[subkey][subkey2]": value} where value is not a dict +def traverse_query_dict(dict_flat: Dict[str, Any], key_prefix: Optional[str] = None) -> List[Tuple[str, Any]]: + result = [] + for k, v in dict_flat.items(): + key = f"{key_prefix}[{k}]" if key_prefix is not None else k + if isinstance(v, dict): + result.extend(traverse_query_dict(v, key)) + elif isinstance(v, list): + for arr_v in v: + if isinstance(arr_v, dict): + result.extend(traverse_query_dict(arr_v, key)) + else: + result.append((key, arr_v)) + else: + result.append((key, v)) + return result + + +def single_query_encoder(query_key: str, query_value: Any) -> List[Tuple[str, Any]]: + if isinstance(query_value, pydantic.BaseModel) or isinstance(query_value, dict): + if isinstance(query_value, pydantic.BaseModel): + obj_dict = query_value.dict(by_alias=True) + else: + obj_dict = query_value + return traverse_query_dict(obj_dict, query_key) + elif isinstance(query_value, list): + encoded_values: List[Tuple[str, Any]] = [] + for value in query_value: + if isinstance(value, pydantic.BaseModel) or isinstance(value, dict): + if isinstance(value, pydantic.BaseModel): + obj_dict = value.dict(by_alias=True) + elif isinstance(value, dict): + obj_dict = value + + encoded_values.extend(single_query_encoder(query_key, obj_dict)) + else: + encoded_values.append((query_key, value)) + + return encoded_values + + return [(query_key, query_value)] + + +def encode_query(query: Optional[Dict[str, Any]]) -> Optional[List[Tuple[str, Any]]]: + if query is None: + return None + + encoded_query = [] + for k, v in query.items(): + encoded_query.extend(single_query_encoder(k, v)) + return encoded_query diff --git a/src/browser_use/core/remove_none_from_dict.py b/src/browser_use/core/remove_none_from_dict.py new file mode 100644 index 0000000..c229814 --- /dev/null +++ b/src/browser_use/core/remove_none_from_dict.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Mapping, Optional + + +def remove_none_from_dict(original: Mapping[str, Optional[Any]]) -> Dict[str, Any]: + new: Dict[str, Any] = {} + for key, value in original.items(): + if value is not None: + new[key] = value + return new diff --git a/src/browser_use/core/request_options.py b/src/browser_use/core/request_options.py new file mode 100644 index 0000000..1b38804 --- /dev/null +++ b/src/browser_use/core/request_options.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +try: + from typing import NotRequired # type: ignore +except ImportError: + from typing_extensions import NotRequired + + +class RequestOptions(typing.TypedDict, total=False): + """ + Additional options for request-specific configuration when calling APIs via the SDK. + This is used primarily as an optional final parameter for service functions. + + Attributes: + - timeout_in_seconds: int. The number of seconds to await an API call before timing out. + + - max_retries: int. The max number of retries to attempt if the API call fails. + + - additional_headers: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's header dict + + - additional_query_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's query parameters dict + + - additional_body_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's body parameters dict + + - chunk_size: int. The size, in bytes, to process each chunk of data being streamed back within the response. This equates to leveraging `chunk_size` within `requests` or `httpx`, and is only leveraged for file downloads. + """ + + timeout_in_seconds: NotRequired[int] + max_retries: NotRequired[int] + additional_headers: NotRequired[typing.Dict[str, typing.Any]] + additional_query_parameters: NotRequired[typing.Dict[str, typing.Any]] + additional_body_parameters: NotRequired[typing.Dict[str, typing.Any]] + chunk_size: NotRequired[int] diff --git a/src/browser_use/core/serialization.py b/src/browser_use/core/serialization.py new file mode 100644 index 0000000..c36e865 --- /dev/null +++ b/src/browser_use/core/serialization.py @@ -0,0 +1,276 @@ +# This file was auto-generated by Fern from our API Definition. + +import collections +import inspect +import typing + +import pydantic +import typing_extensions + + +class FieldMetadata: + """ + Metadata class used to annotate fields to provide additional information. + + Example: + class MyDict(TypedDict): + field: typing.Annotated[str, FieldMetadata(alias="field_name")] + + Will serialize: `{"field": "value"}` + To: `{"field_name": "value"}` + """ + + alias: str + + def __init__(self, *, alias: str) -> None: + self.alias = alias + + +def convert_and_respect_annotation_metadata( + *, + object_: typing.Any, + annotation: typing.Any, + inner_type: typing.Optional[typing.Any] = None, + direction: typing.Literal["read", "write"], +) -> typing.Any: + """ + Respect the metadata annotations on a field, such as aliasing. This function effectively + manipulates the dict-form of an object to respect the metadata annotations. This is primarily used for + TypedDicts, which cannot support aliasing out of the box, and can be extended for additional + utilities, such as defaults. + + Parameters + ---------- + object_ : typing.Any + + annotation : type + The type we're looking to apply typing annotations from + + inner_type : typing.Optional[type] + + Returns + ------- + typing.Any + """ + + if object_ is None: + return None + if inner_type is None: + inner_type = annotation + + clean_type = _remove_annotations(inner_type) + # Pydantic models + if ( + inspect.isclass(clean_type) + and issubclass(clean_type, pydantic.BaseModel) + and isinstance(object_, typing.Mapping) + ): + return _convert_mapping(object_, clean_type, direction) + # TypedDicts + if typing_extensions.is_typeddict(clean_type) and isinstance(object_, typing.Mapping): + return _convert_mapping(object_, clean_type, direction) + + if ( + typing_extensions.get_origin(clean_type) == typing.Dict + or typing_extensions.get_origin(clean_type) == dict + or clean_type == typing.Dict + ) and isinstance(object_, typing.Dict): + key_type = typing_extensions.get_args(clean_type)[0] + value_type = typing_extensions.get_args(clean_type)[1] + + return { + key: convert_and_respect_annotation_metadata( + object_=value, + annotation=annotation, + inner_type=value_type, + direction=direction, + ) + for key, value in object_.items() + } + + # If you're iterating on a string, do not bother to coerce it to a sequence. + if not isinstance(object_, str): + if ( + typing_extensions.get_origin(clean_type) == typing.Set + or typing_extensions.get_origin(clean_type) == set + or clean_type == typing.Set + ) and isinstance(object_, typing.Set): + inner_type = typing_extensions.get_args(clean_type)[0] + return { + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + } + elif ( + ( + typing_extensions.get_origin(clean_type) == typing.List + or typing_extensions.get_origin(clean_type) == list + or clean_type == typing.List + ) + and isinstance(object_, typing.List) + ) or ( + ( + typing_extensions.get_origin(clean_type) == typing.Sequence + or typing_extensions.get_origin(clean_type) == collections.abc.Sequence + or clean_type == typing.Sequence + ) + and isinstance(object_, typing.Sequence) + ): + inner_type = typing_extensions.get_args(clean_type)[0] + return [ + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + ] + + if typing_extensions.get_origin(clean_type) == typing.Union: + # We should be able to ~relatively~ safely try to convert keys against all + # member types in the union, the edge case here is if one member aliases a field + # of the same name to a different name from another member + # Or if another member aliases a field of the same name that another member does not. + for member in typing_extensions.get_args(clean_type): + object_ = convert_and_respect_annotation_metadata( + object_=object_, + annotation=annotation, + inner_type=member, + direction=direction, + ) + return object_ + + annotated_type = _get_annotation(annotation) + if annotated_type is None: + return object_ + + # If the object is not a TypedDict, a Union, or other container (list, set, sequence, etc.) + # Then we can safely call it on the recursive conversion. + return object_ + + +def _convert_mapping( + object_: typing.Mapping[str, object], + expected_type: typing.Any, + direction: typing.Literal["read", "write"], +) -> typing.Mapping[str, object]: + converted_object: typing.Dict[str, object] = {} + try: + annotations = typing_extensions.get_type_hints(expected_type, include_extras=True) + except NameError: + # The TypedDict contains a circular reference, so + # we use the __annotations__ attribute directly. + annotations = getattr(expected_type, "__annotations__", {}) + aliases_to_field_names = _get_alias_to_field_name(annotations) + for key, value in object_.items(): + if direction == "read" and key in aliases_to_field_names: + dealiased_key = aliases_to_field_names.get(key) + if dealiased_key is not None: + type_ = annotations.get(dealiased_key) + else: + type_ = annotations.get(key) + # Note you can't get the annotation by the field name if you're in read mode, so you must check the aliases map + # + # So this is effectively saying if we're in write mode, and we don't have a type, or if we're in read mode and we don't have an alias + # then we can just pass the value through as is + if type_ is None: + converted_object[key] = value + elif direction == "read" and key not in aliases_to_field_names: + converted_object[key] = convert_and_respect_annotation_metadata( + object_=value, annotation=type_, direction=direction + ) + else: + converted_object[_alias_key(key, type_, direction, aliases_to_field_names)] = ( + convert_and_respect_annotation_metadata(object_=value, annotation=type_, direction=direction) + ) + return converted_object + + +def _get_annotation(type_: typing.Any) -> typing.Optional[typing.Any]: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return None + + if maybe_annotated_type == typing_extensions.NotRequired: + type_ = typing_extensions.get_args(type_)[0] + maybe_annotated_type = typing_extensions.get_origin(type_) + + if maybe_annotated_type == typing_extensions.Annotated: + return type_ + + return None + + +def _remove_annotations(type_: typing.Any) -> typing.Any: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return type_ + + if maybe_annotated_type == typing_extensions.NotRequired: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + if maybe_annotated_type == typing_extensions.Annotated: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + return type_ + + +def get_alias_to_field_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_alias_to_field_name(annotations) + + +def get_field_to_alias_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_field_to_alias_name(annotations) + + +def _get_alias_to_field_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[maybe_alias] = field + return aliases + + +def _get_field_to_alias_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[field] = maybe_alias + return aliases + + +def _get_alias_from_type(type_: typing.Any) -> typing.Optional[str]: + maybe_annotated_type = _get_annotation(type_) + + if maybe_annotated_type is not None: + # The actual annotations are 1 onward, the first is the annotated type + annotations = typing_extensions.get_args(maybe_annotated_type)[1:] + + for annotation in annotations: + if isinstance(annotation, FieldMetadata) and annotation.alias is not None: + return annotation.alias + return None + + +def _alias_key( + key: str, + type_: typing.Any, + direction: typing.Literal["read", "write"], + aliases_to_field_names: typing.Dict[str, str], +) -> str: + if direction == "read": + return aliases_to_field_names.get(key, key) + return _get_alias_from_type(type_=type_) or key diff --git a/src/browser_use/core/unchecked_base_model.py b/src/browser_use/core/unchecked_base_model.py new file mode 100644 index 0000000..e04a6f8 --- /dev/null +++ b/src/browser_use/core/unchecked_base_model.py @@ -0,0 +1,341 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import inspect +import typing +import uuid + +import pydantic +import typing_extensions +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + ModelField, + UniversalBaseModel, + get_args, + get_origin, + is_literal_type, + is_union, + parse_date, + parse_datetime, + parse_obj_as, +) +from .serialization import get_field_to_alias_mapping +from pydantic_core import PydanticUndefined + + +class UnionMetadata: + discriminant: str + + def __init__(self, *, discriminant: str) -> None: + self.discriminant = discriminant + + +Model = typing.TypeVar("Model", bound=pydantic.BaseModel) + + +class UncheckedBaseModel(UniversalBaseModel): + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + extra = pydantic.Extra.allow + + @classmethod + def model_construct( + cls: typing.Type["Model"], + _fields_set: typing.Optional[typing.Set[str]] = None, + **values: typing.Any, + ) -> "Model": + # Fallback construct function to the specified override below. + return cls.construct(_fields_set=_fields_set, **values) + + # Allow construct to not validate model + # Implementation taken from: https://github.com/pydantic/pydantic/issues/1168#issuecomment-817742836 + @classmethod + def construct( + cls: typing.Type["Model"], + _fields_set: typing.Optional[typing.Set[str]] = None, + **values: typing.Any, + ) -> "Model": + m = cls.__new__(cls) + fields_values = {} + + if _fields_set is None: + _fields_set = set(values.keys()) + + fields = _get_model_fields(cls) + populate_by_name = _get_is_populate_by_name(cls) + field_aliases = get_field_to_alias_mapping(cls) + + for name, field in fields.items(): + # Key here is only used to pull data from the values dict + # you should always use the NAME of the field to for field_values, etc. + # because that's how the object is constructed from a pydantic perspective + key = field.alias + if (key is None or field.alias == name) and name in field_aliases: + key = field_aliases[name] + + if key is None or (key not in values and populate_by_name): # Added this to allow population by field name + key = name + + if key in values: + if IS_PYDANTIC_V2: + type_ = field.annotation # type: ignore # Pydantic v2 + else: + type_ = typing.cast(typing.Type, field.outer_type_) # type: ignore # Pydantic < v1.10.15 + + fields_values[name] = ( + construct_type(object_=values[key], type_=type_) if type_ is not None else values[key] + ) + _fields_set.add(name) + else: + default = _get_field_default(field) + fields_values[name] = default + + # If the default values are non-null act like they've been set + # This effectively allows exclude_unset to work like exclude_none where + # the latter passes through intentionally set none values. + if default != None and default != PydanticUndefined: + _fields_set.add(name) + + # Add extras back in + extras = {} + pydantic_alias_fields = [field.alias for field in fields.values()] + internal_alias_fields = list(field_aliases.values()) + for key, value in values.items(): + # If the key is not a field by name, nor an alias to a field, then it's extra + if (key not in pydantic_alias_fields and key not in internal_alias_fields) and key not in fields: + if IS_PYDANTIC_V2: + extras[key] = value + else: + _fields_set.add(key) + fields_values[key] = value + + object.__setattr__(m, "__dict__", fields_values) + + if IS_PYDANTIC_V2: + object.__setattr__(m, "__pydantic_private__", None) + object.__setattr__(m, "__pydantic_extra__", extras) + object.__setattr__(m, "__pydantic_fields_set__", _fields_set) + else: + object.__setattr__(m, "__fields_set__", _fields_set) + m._init_private_attributes() # type: ignore # Pydantic v1 + return m + + +def _validate_collection_items_compatible(collection: typing.Any, target_type: typing.Type[typing.Any]) -> bool: + """ + Validate that all items in a collection are compatible with the target type. + + Args: + collection: The collection to validate (list, set, or dict values) + target_type: The target type to validate against + + Returns: + True if all items are compatible, False otherwise + """ + if inspect.isclass(target_type) and issubclass(target_type, pydantic.BaseModel): + for item in collection: + try: + # Try to validate the item against the target type + if isinstance(item, dict): + parse_obj_as(target_type, item) + else: + # If it's not a dict, it might already be the right type + if not isinstance(item, target_type): + return False + except Exception: + return False + return True + + +def _convert_undiscriminated_union_type(union_type: typing.Type[typing.Any], object_: typing.Any) -> typing.Any: + inner_types = get_args(union_type) + if typing.Any in inner_types: + return object_ + + for inner_type in inner_types: + # Handle lists of objects that need parsing + if get_origin(inner_type) is list and isinstance(object_, list): + list_inner_type = get_args(inner_type)[0] + try: + if inspect.isclass(list_inner_type) and issubclass(list_inner_type, pydantic.BaseModel): + # Validate that all items in the list are compatible with the target type + if _validate_collection_items_compatible(object_, list_inner_type): + parsed_list = [parse_obj_as(object_=item, type_=list_inner_type) for item in object_] + return parsed_list + except Exception: + pass + + try: + if inspect.isclass(inner_type) and issubclass(inner_type, pydantic.BaseModel): + # Attempt a validated parse until one works + return parse_obj_as(inner_type, object_) + except Exception: + continue + + # If none of the types work, just return the first successful cast + for inner_type in inner_types: + try: + return construct_type(object_=object_, type_=inner_type) + except Exception: + continue + + +def _convert_union_type(type_: typing.Type[typing.Any], object_: typing.Any) -> typing.Any: + base_type = get_origin(type_) or type_ + union_type = type_ + if base_type == typing_extensions.Annotated: + union_type = get_args(type_)[0] + annotated_metadata = get_args(type_)[1:] + for metadata in annotated_metadata: + if isinstance(metadata, UnionMetadata): + try: + # Cast to the correct type, based on the discriminant + for inner_type in get_args(union_type): + try: + objects_discriminant = getattr(object_, metadata.discriminant) + except: + objects_discriminant = object_[metadata.discriminant] + if inner_type.__fields__[metadata.discriminant].default == objects_discriminant: + return construct_type(object_=object_, type_=inner_type) + except Exception: + # Allow to fall through to our regular union handling + pass + return _convert_undiscriminated_union_type(union_type, object_) + + +def construct_type(*, type_: typing.Type[typing.Any], object_: typing.Any) -> typing.Any: + """ + Here we are essentially creating the same `construct` method in spirit as the above, but for all types, not just + Pydantic models. + The idea is to essentially attempt to coerce object_ to type_ (recursively) + """ + # Short circuit when dealing with optionals, don't try to coerces None to a type + if object_ is None: + return None + + base_type = get_origin(type_) or type_ + is_annotated = base_type == typing_extensions.Annotated + maybe_annotation_members = get_args(type_) + is_annotated_union = is_annotated and is_union(get_origin(maybe_annotation_members[0])) + + if base_type == typing.Any: + return object_ + + if base_type == dict: + if not isinstance(object_, typing.Mapping): + return object_ + + key_type, items_type = get_args(type_) + d = { + construct_type(object_=key, type_=key_type): construct_type(object_=item, type_=items_type) + for key, item in object_.items() + } + return d + + if base_type == list: + if not isinstance(object_, list): + return object_ + + inner_type = get_args(type_)[0] + return [construct_type(object_=entry, type_=inner_type) for entry in object_] + + if base_type == set: + if not isinstance(object_, set) and not isinstance(object_, list): + return object_ + + inner_type = get_args(type_)[0] + return {construct_type(object_=entry, type_=inner_type) for entry in object_} + + if is_union(base_type) or is_annotated_union: + return _convert_union_type(type_, object_) + + # Cannot do an `issubclass` with a literal type, let's also just confirm we have a class before this call + if ( + object_ is not None + and not is_literal_type(type_) + and ( + (inspect.isclass(base_type) and issubclass(base_type, pydantic.BaseModel)) + or ( + is_annotated + and inspect.isclass(maybe_annotation_members[0]) + and issubclass(maybe_annotation_members[0], pydantic.BaseModel) + ) + ) + ): + if IS_PYDANTIC_V2: + return type_.model_construct(**object_) + else: + return type_.construct(**object_) + + if base_type == dt.datetime: + try: + return parse_datetime(object_) + except Exception: + return object_ + + if base_type == dt.date: + try: + return parse_date(object_) + except Exception: + return object_ + + if base_type == uuid.UUID: + try: + return uuid.UUID(object_) + except Exception: + return object_ + + if base_type == int: + try: + return int(object_) + except Exception: + return object_ + + if base_type == bool: + try: + if isinstance(object_, str): + stringified_object = object_.lower() + return stringified_object == "true" or stringified_object == "1" + + return bool(object_) + except Exception: + return object_ + + return object_ + + +def _get_is_populate_by_name(model: typing.Type["Model"]) -> bool: + if IS_PYDANTIC_V2: + return model.model_config.get("populate_by_name", False) # type: ignore # Pydantic v2 + return model.__config__.allow_population_by_field_name # type: ignore # Pydantic v1 + + +PydanticField = typing.Union[ModelField, pydantic.fields.FieldInfo] + + +# Pydantic V1 swapped the typing of __fields__'s values from ModelField to FieldInfo +# And so we try to handle both V1 cases, as well as V2 (FieldInfo from model.model_fields) +def _get_model_fields( + model: typing.Type["Model"], +) -> typing.Mapping[str, PydanticField]: + if IS_PYDANTIC_V2: + return model.model_fields # type: ignore # Pydantic v2 + else: + return model.__fields__ # type: ignore # Pydantic v1 + + +def _get_field_default(field: PydanticField) -> typing.Any: + try: + value = field.get_default() # type: ignore # Pydantic < v1.10.15 + except: + value = field.default + if IS_PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value diff --git a/src/browser_use/environment.py b/src/browser_use/environment.py new file mode 100644 index 0000000..3d6dc8a --- /dev/null +++ b/src/browser_use/environment.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import enum + + +class BrowserUseEnvironment(enum.Enum): + PRODUCTION = "https://api.browser-use.com/api/v2" diff --git a/src/browser_use/errors/__init__.py b/src/browser_use/errors/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/browser_use/errors/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/browser_use/errors/bad_request_error.py b/src/browser_use/errors/bad_request_error.py new file mode 100644 index 0000000..baf5be4 --- /dev/null +++ b/src/browser_use/errors/bad_request_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError + + +class BadRequestError(ApiError): + def __init__(self, body: typing.Optional[typing.Any], headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=400, headers=headers, body=body) diff --git a/src/browser_use/errors/internal_server_error.py b/src/browser_use/errors/internal_server_error.py new file mode 100644 index 0000000..14313ab --- /dev/null +++ b/src/browser_use/errors/internal_server_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError + + +class InternalServerError(ApiError): + def __init__(self, body: typing.Optional[typing.Any], headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=500, headers=headers, body=body) diff --git a/src/browser_use/errors/not_found_error.py b/src/browser_use/errors/not_found_error.py new file mode 100644 index 0000000..dcd60e3 --- /dev/null +++ b/src/browser_use/errors/not_found_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError + + +class NotFoundError(ApiError): + def __init__(self, body: typing.Optional[typing.Any], headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=404, headers=headers, body=body) diff --git a/src/browser_use/errors/payment_required_error.py b/src/browser_use/errors/payment_required_error.py new file mode 100644 index 0000000..414ec60 --- /dev/null +++ b/src/browser_use/errors/payment_required_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.insufficient_credits_error import InsufficientCreditsError + + +class PaymentRequiredError(ApiError): + def __init__(self, body: InsufficientCreditsError, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=402, headers=headers, body=body) diff --git a/src/browser_use/errors/unprocessable_entity_error.py b/src/browser_use/errors/unprocessable_entity_error.py new file mode 100644 index 0000000..93cb1ab --- /dev/null +++ b/src/browser_use/errors/unprocessable_entity_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError + + +class UnprocessableEntityError(ApiError): + def __init__(self, body: typing.Optional[typing.Any], headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=422, headers=headers, body=body) diff --git a/src/browser_use/files/__init__.py b/src/browser_use/files/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/browser_use/files/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/browser_use/files/client.py b/src/browser_use/files/client.py new file mode 100644 index 0000000..8824f2b --- /dev/null +++ b/src/browser_use/files/client.py @@ -0,0 +1,245 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.task_output_file_response import TaskOutputFileResponse +from ..types.upload_file_presigned_url_response import UploadFilePresignedUrlResponse +from .raw_client import AsyncRawFilesClient, RawFilesClient +from .types.upload_file_request_content_type import UploadFileRequestContentType + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class FilesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawFilesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawFilesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawFilesClient + """ + return self._raw_client + + def user_upload_file_presigned_url( + self, + session_id: str, + *, + file_name: str, + content_type: UploadFileRequestContentType, + size_bytes: int, + request_options: typing.Optional[RequestOptions] = None, + ) -> UploadFilePresignedUrlResponse: + """ + Generate a secure presigned URL for uploading files that AI agents can use during tasks. + + Parameters + ---------- + session_id : str + + file_name : str + The name of the file to upload + + content_type : UploadFileRequestContentType + The content type of the file to upload + + size_bytes : int + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + UploadFilePresignedUrlResponse + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.files.user_upload_file_presigned_url( + session_id="session_id", + file_name="fileName", + content_type="image/jpg", + size_bytes=1, + ) + """ + _response = self._raw_client.user_upload_file_presigned_url( + session_id, + file_name=file_name, + content_type=content_type, + size_bytes=size_bytes, + request_options=request_options, + ) + return _response.data + + def get_task_output_file_presigned_url( + self, task_id: str, file_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> TaskOutputFileResponse: + """ + Get secure download URL for an output file generated by the AI agent. + + Parameters + ---------- + task_id : str + + file_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskOutputFileResponse + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.files.get_task_output_file_presigned_url( + task_id="task_id", + file_id="file_id", + ) + """ + _response = self._raw_client.get_task_output_file_presigned_url( + task_id, file_id, request_options=request_options + ) + return _response.data + + +class AsyncFilesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawFilesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawFilesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawFilesClient + """ + return self._raw_client + + async def user_upload_file_presigned_url( + self, + session_id: str, + *, + file_name: str, + content_type: UploadFileRequestContentType, + size_bytes: int, + request_options: typing.Optional[RequestOptions] = None, + ) -> UploadFilePresignedUrlResponse: + """ + Generate a secure presigned URL for uploading files that AI agents can use during tasks. + + Parameters + ---------- + session_id : str + + file_name : str + The name of the file to upload + + content_type : UploadFileRequestContentType + The content type of the file to upload + + size_bytes : int + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + UploadFilePresignedUrlResponse + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.files.user_upload_file_presigned_url( + session_id="session_id", + file_name="fileName", + content_type="image/jpg", + size_bytes=1, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.user_upload_file_presigned_url( + session_id, + file_name=file_name, + content_type=content_type, + size_bytes=size_bytes, + request_options=request_options, + ) + return _response.data + + async def get_task_output_file_presigned_url( + self, task_id: str, file_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> TaskOutputFileResponse: + """ + Get secure download URL for an output file generated by the AI agent. + + Parameters + ---------- + task_id : str + + file_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskOutputFileResponse + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.files.get_task_output_file_presigned_url( + task_id="task_id", + file_id="file_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_task_output_file_presigned_url( + task_id, file_id, request_options=request_options + ) + return _response.data diff --git a/src/browser_use/files/raw_client.py b/src/browser_use/files/raw_client.py new file mode 100644 index 0000000..c269b87 --- /dev/null +++ b/src/browser_use/files/raw_client.py @@ -0,0 +1,387 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.bad_request_error import BadRequestError +from ..errors.internal_server_error import InternalServerError +from ..errors.not_found_error import NotFoundError +from ..errors.unprocessable_entity_error import UnprocessableEntityError +from ..types.task_output_file_response import TaskOutputFileResponse +from ..types.upload_file_presigned_url_response import UploadFilePresignedUrlResponse +from .types.upload_file_request_content_type import UploadFileRequestContentType + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawFilesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def user_upload_file_presigned_url( + self, + session_id: str, + *, + file_name: str, + content_type: UploadFileRequestContentType, + size_bytes: int, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[UploadFilePresignedUrlResponse]: + """ + Generate a secure presigned URL for uploading files that AI agents can use during tasks. + + Parameters + ---------- + session_id : str + + file_name : str + The name of the file to upload + + content_type : UploadFileRequestContentType + The content type of the file to upload + + size_bytes : int + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[UploadFilePresignedUrlResponse] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"files/sessions/{jsonable_encoder(session_id)}/presigned-url", + method="POST", + json={ + "fileName": file_name, + "contentType": content_type, + "sizeBytes": size_bytes, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + UploadFilePresignedUrlResponse, + construct_type( + type_=UploadFilePresignedUrlResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_task_output_file_presigned_url( + self, task_id: str, file_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[TaskOutputFileResponse]: + """ + Get secure download URL for an output file generated by the AI agent. + + Parameters + ---------- + task_id : str + + file_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TaskOutputFileResponse] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"files/tasks/{jsonable_encoder(task_id)}/output-files/{jsonable_encoder(file_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskOutputFileResponse, + construct_type( + type_=TaskOutputFileResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawFilesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def user_upload_file_presigned_url( + self, + session_id: str, + *, + file_name: str, + content_type: UploadFileRequestContentType, + size_bytes: int, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[UploadFilePresignedUrlResponse]: + """ + Generate a secure presigned URL for uploading files that AI agents can use during tasks. + + Parameters + ---------- + session_id : str + + file_name : str + The name of the file to upload + + content_type : UploadFileRequestContentType + The content type of the file to upload + + size_bytes : int + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[UploadFilePresignedUrlResponse] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"files/sessions/{jsonable_encoder(session_id)}/presigned-url", + method="POST", + json={ + "fileName": file_name, + "contentType": content_type, + "sizeBytes": size_bytes, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + UploadFilePresignedUrlResponse, + construct_type( + type_=UploadFilePresignedUrlResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_task_output_file_presigned_url( + self, task_id: str, file_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[TaskOutputFileResponse]: + """ + Get secure download URL for an output file generated by the AI agent. + + Parameters + ---------- + task_id : str + + file_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TaskOutputFileResponse] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"files/tasks/{jsonable_encoder(task_id)}/output-files/{jsonable_encoder(file_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskOutputFileResponse, + construct_type( + type_=TaskOutputFileResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/browser_use/files/types/__init__.py b/src/browser_use/files/types/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/browser_use/files/types/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/browser_use/files/types/upload_file_request_content_type.py b/src/browser_use/files/types/upload_file_request_content_type.py new file mode 100644 index 0000000..9930434 --- /dev/null +++ b/src/browser_use/files/types/upload_file_request_content_type.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +UploadFileRequestContentType = typing.Union[ + typing.Literal[ + "image/jpg", + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "image/svg+xml", + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "text/plain", + "text/csv", + "text/markdown", + ], + typing.Any, +] diff --git a/src/browser_use/profiles/__init__.py b/src/browser_use/profiles/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/browser_use/profiles/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/browser_use/profiles/client.py b/src/browser_use/profiles/client.py new file mode 100644 index 0000000..5d5c658 --- /dev/null +++ b/src/browser_use/profiles/client.py @@ -0,0 +1,335 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.profile_list_response import ProfileListResponse +from ..types.profile_view import ProfileView +from .raw_client import AsyncRawProfilesClient, RawProfilesClient + + +class ProfilesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawProfilesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawProfilesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawProfilesClient + """ + return self._raw_client + + def list_profiles( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ProfileListResponse: + """ + Get paginated list of profiles. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileListResponse + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.profiles.list_profiles() + """ + _response = self._raw_client.list_profiles( + page_size=page_size, page_number=page_number, request_options=request_options + ) + return _response.data + + def create_profile(self, *, request_options: typing.Optional[RequestOptions] = None) -> ProfileView: + """ + Profiles allow you to preserve the state of the browser between tasks. + + They are most commonly used to allow users to preserve the log-in state in the agent between tasks. + You'd normally create one profile per user and then use it for all their tasks. + + You can create a new profile by calling this endpoint. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.profiles.create_profile() + """ + _response = self._raw_client.create_profile(request_options=request_options) + return _response.data + + def get_profile(self, profile_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> ProfileView: + """ + Get profile details. + + Parameters + ---------- + profile_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.profiles.get_profile( + profile_id="profile_id", + ) + """ + _response = self._raw_client.get_profile(profile_id, request_options=request_options) + return _response.data + + def delete_browser_profile( + self, profile_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Permanently delete a browser profile and its configuration. + + Parameters + ---------- + profile_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.profiles.delete_browser_profile( + profile_id="profile_id", + ) + """ + _response = self._raw_client.delete_browser_profile(profile_id, request_options=request_options) + return _response.data + + +class AsyncProfilesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawProfilesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawProfilesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawProfilesClient + """ + return self._raw_client + + async def list_profiles( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ProfileListResponse: + """ + Get paginated list of profiles. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileListResponse + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.profiles.list_profiles() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_profiles( + page_size=page_size, page_number=page_number, request_options=request_options + ) + return _response.data + + async def create_profile(self, *, request_options: typing.Optional[RequestOptions] = None) -> ProfileView: + """ + Profiles allow you to preserve the state of the browser between tasks. + + They are most commonly used to allow users to preserve the log-in state in the agent between tasks. + You'd normally create one profile per user and then use it for all their tasks. + + You can create a new profile by calling this endpoint. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.profiles.create_profile() + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_profile(request_options=request_options) + return _response.data + + async def get_profile( + self, profile_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> ProfileView: + """ + Get profile details. + + Parameters + ---------- + profile_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.profiles.get_profile( + profile_id="profile_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_profile(profile_id, request_options=request_options) + return _response.data + + async def delete_browser_profile( + self, profile_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Permanently delete a browser profile and its configuration. + + Parameters + ---------- + profile_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.profiles.delete_browser_profile( + profile_id="profile_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_browser_profile(profile_id, request_options=request_options) + return _response.data diff --git a/src/browser_use/profiles/raw_client.py b/src/browser_use/profiles/raw_client.py new file mode 100644 index 0000000..c5694b5 --- /dev/null +++ b/src/browser_use/profiles/raw_client.py @@ -0,0 +1,471 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.not_found_error import NotFoundError +from ..errors.payment_required_error import PaymentRequiredError +from ..errors.unprocessable_entity_error import UnprocessableEntityError +from ..types.insufficient_credits_error import InsufficientCreditsError +from ..types.profile_list_response import ProfileListResponse +from ..types.profile_view import ProfileView + + +class RawProfilesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list_profiles( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ProfileListResponse]: + """ + Get paginated list of profiles. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ProfileListResponse] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + "profiles", + method="GET", + params={ + "pageSize": page_size, + "pageNumber": page_number, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileListResponse, + construct_type( + type_=ProfileListResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create_profile(self, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[ProfileView]: + """ + Profiles allow you to preserve the state of the browser between tasks. + + They are most commonly used to allow users to preserve the log-in state in the agent between tasks. + You'd normally create one profile per user and then use it for all their tasks. + + You can create a new profile by calling this endpoint. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ProfileView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + "profiles", + method="POST", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileView, + construct_type( + type_=ProfileView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + InsufficientCreditsError, + construct_type( + type_=InsufficientCreditsError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_profile( + self, profile_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ProfileView]: + """ + Get profile details. + + Parameters + ---------- + profile_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ProfileView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"profiles/{jsonable_encoder(profile_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileView, + construct_type( + type_=ProfileView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def delete_browser_profile( + self, profile_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[None]: + """ + Permanently delete a browser profile and its configuration. + + Parameters + ---------- + profile_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[None] + """ + _response = self._client_wrapper.httpx_client.request( + f"profiles/{jsonable_encoder(profile_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return HttpResponse(response=_response, data=None) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawProfilesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list_profiles( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ProfileListResponse]: + """ + Get paginated list of profiles. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ProfileListResponse] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + "profiles", + method="GET", + params={ + "pageSize": page_size, + "pageNumber": page_number, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileListResponse, + construct_type( + type_=ProfileListResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create_profile( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ProfileView]: + """ + Profiles allow you to preserve the state of the browser between tasks. + + They are most commonly used to allow users to preserve the log-in state in the agent between tasks. + You'd normally create one profile per user and then use it for all their tasks. + + You can create a new profile by calling this endpoint. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ProfileView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + "profiles", + method="POST", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileView, + construct_type( + type_=ProfileView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + InsufficientCreditsError, + construct_type( + type_=InsufficientCreditsError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_profile( + self, profile_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ProfileView]: + """ + Get profile details. + + Parameters + ---------- + profile_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ProfileView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"profiles/{jsonable_encoder(profile_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileView, + construct_type( + type_=ProfileView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def delete_browser_profile( + self, profile_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[None]: + """ + Permanently delete a browser profile and its configuration. + + Parameters + ---------- + profile_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[None] + """ + _response = await self._client_wrapper.httpx_client.request( + f"profiles/{jsonable_encoder(profile_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return AsyncHttpResponse(response=_response, data=None) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/browser_use_sdk/py.typed b/src/browser_use/py.typed similarity index 100% rename from src/browser_use_sdk/py.typed rename to src/browser_use/py.typed diff --git a/src/browser_use/sessions/__init__.py b/src/browser_use/sessions/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/browser_use/sessions/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/browser_use/sessions/client.py b/src/browser_use/sessions/client.py new file mode 100644 index 0000000..606cde5 --- /dev/null +++ b/src/browser_use/sessions/client.py @@ -0,0 +1,648 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.proxy_country_code import ProxyCountryCode +from ..types.session_item_view import SessionItemView +from ..types.session_list_response import SessionListResponse +from ..types.session_status import SessionStatus +from ..types.session_view import SessionView +from ..types.share_view import ShareView +from .raw_client import AsyncRawSessionsClient, RawSessionsClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class SessionsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawSessionsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawSessionsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawSessionsClient + """ + return self._raw_client + + def list_sessions( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + filter_by: typing.Optional[SessionStatus] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> SessionListResponse: + """ + Get paginated list of AI agent sessions with optional status filtering. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + filter_by : typing.Optional[SessionStatus] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionListResponse + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.sessions.list_sessions() + """ + _response = self._raw_client.list_sessions( + page_size=page_size, page_number=page_number, filter_by=filter_by, request_options=request_options + ) + return _response.data + + def create_session( + self, + *, + profile_id: typing.Optional[str] = OMIT, + proxy_country_code: typing.Optional[ProxyCountryCode] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> SessionItemView: + """ + Create a new session with a new task. + + Parameters + ---------- + profile_id : typing.Optional[str] + The ID of the profile to use for the session + + proxy_country_code : typing.Optional[ProxyCountryCode] + Country code for proxy location. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionItemView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.sessions.create_session() + """ + _response = self._raw_client.create_session( + profile_id=profile_id, proxy_country_code=proxy_country_code, request_options=request_options + ) + return _response.data + + def get_session(self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> SessionView: + """ + Get detailed session information including status, URLs, and task details. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.sessions.get_session( + session_id="session_id", + ) + """ + _response = self._raw_client.get_session(session_id, request_options=request_options) + return _response.data + + def delete_session(self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: + """ + Permanently delete a session and all associated data. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.sessions.delete_session( + session_id="session_id", + ) + """ + _response = self._raw_client.delete_session(session_id, request_options=request_options) + return _response.data + + def update_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> SessionView: + """ + Stop a session and all its running tasks. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.sessions.update_session( + session_id="session_id", + ) + """ + _response = self._raw_client.update_session(session_id, request_options=request_options) + return _response.data + + def get_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> ShareView: + """ + Get public share information including URL and usage statistics. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ShareView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.sessions.get_session_public_share( + session_id="session_id", + ) + """ + _response = self._raw_client.get_session_public_share(session_id, request_options=request_options) + return _response.data + + def create_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> ShareView: + """ + Create or return existing public share for a session. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ShareView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.sessions.create_session_public_share( + session_id="session_id", + ) + """ + _response = self._raw_client.create_session_public_share(session_id, request_options=request_options) + return _response.data + + def delete_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Remove public share for a session. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.sessions.delete_session_public_share( + session_id="session_id", + ) + """ + _response = self._raw_client.delete_session_public_share(session_id, request_options=request_options) + return _response.data + + +class AsyncSessionsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawSessionsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawSessionsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawSessionsClient + """ + return self._raw_client + + async def list_sessions( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + filter_by: typing.Optional[SessionStatus] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> SessionListResponse: + """ + Get paginated list of AI agent sessions with optional status filtering. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + filter_by : typing.Optional[SessionStatus] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionListResponse + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.sessions.list_sessions() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_sessions( + page_size=page_size, page_number=page_number, filter_by=filter_by, request_options=request_options + ) + return _response.data + + async def create_session( + self, + *, + profile_id: typing.Optional[str] = OMIT, + proxy_country_code: typing.Optional[ProxyCountryCode] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> SessionItemView: + """ + Create a new session with a new task. + + Parameters + ---------- + profile_id : typing.Optional[str] + The ID of the profile to use for the session + + proxy_country_code : typing.Optional[ProxyCountryCode] + Country code for proxy location. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionItemView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.sessions.create_session() + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_session( + profile_id=profile_id, proxy_country_code=proxy_country_code, request_options=request_options + ) + return _response.data + + async def get_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> SessionView: + """ + Get detailed session information including status, URLs, and task details. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.sessions.get_session( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_session(session_id, request_options=request_options) + return _response.data + + async def delete_session(self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: + """ + Permanently delete a session and all associated data. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.sessions.delete_session( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_session(session_id, request_options=request_options) + return _response.data + + async def update_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> SessionView: + """ + Stop a session and all its running tasks. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.sessions.update_session( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update_session(session_id, request_options=request_options) + return _response.data + + async def get_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> ShareView: + """ + Get public share information including URL and usage statistics. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ShareView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.sessions.get_session_public_share( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_session_public_share(session_id, request_options=request_options) + return _response.data + + async def create_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> ShareView: + """ + Create or return existing public share for a session. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ShareView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.sessions.create_session_public_share( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_session_public_share(session_id, request_options=request_options) + return _response.data + + async def delete_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Remove public share for a session. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.sessions.delete_session_public_share( + session_id="session_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_session_public_share(session_id, request_options=request_options) + return _response.data diff --git a/src/browser_use/sessions/raw_client.py b/src/browser_use/sessions/raw_client.py new file mode 100644 index 0000000..80a6cd8 --- /dev/null +++ b/src/browser_use/sessions/raw_client.py @@ -0,0 +1,990 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.not_found_error import NotFoundError +from ..errors.unprocessable_entity_error import UnprocessableEntityError +from ..types.proxy_country_code import ProxyCountryCode +from ..types.session_item_view import SessionItemView +from ..types.session_list_response import SessionListResponse +from ..types.session_status import SessionStatus +from ..types.session_view import SessionView +from ..types.share_view import ShareView + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawSessionsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list_sessions( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + filter_by: typing.Optional[SessionStatus] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[SessionListResponse]: + """ + Get paginated list of AI agent sessions with optional status filtering. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + filter_by : typing.Optional[SessionStatus] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[SessionListResponse] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + "sessions", + method="GET", + params={ + "pageSize": page_size, + "pageNumber": page_number, + "filterBy": filter_by, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionListResponse, + construct_type( + type_=SessionListResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create_session( + self, + *, + profile_id: typing.Optional[str] = OMIT, + proxy_country_code: typing.Optional[ProxyCountryCode] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[SessionItemView]: + """ + Create a new session with a new task. + + Parameters + ---------- + profile_id : typing.Optional[str] + The ID of the profile to use for the session + + proxy_country_code : typing.Optional[ProxyCountryCode] + Country code for proxy location. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[SessionItemView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + "sessions", + method="POST", + json={ + "profileId": profile_id, + "proxyCountryCode": proxy_country_code, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionItemView, + construct_type( + type_=SessionItemView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[SessionView]: + """ + Get detailed session information including status, URLs, and task details. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[SessionView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionView, + construct_type( + type_=SessionView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def delete_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[None]: + """ + Permanently delete a session and all associated data. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[None] + """ + _response = self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return HttpResponse(response=_response, data=None) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def update_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[SessionView]: + """ + Stop a session and all its running tasks. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[SessionView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}", + method="PATCH", + json={ + "action": "stop", + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionView, + construct_type( + type_=SessionView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ShareView]: + """ + Get public share information including URL and usage statistics. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ShareView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}/public-share", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ShareView, + construct_type( + type_=ShareView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ShareView]: + """ + Create or return existing public share for a session. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ShareView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}/public-share", + method="POST", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ShareView, + construct_type( + type_=ShareView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def delete_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[None]: + """ + Remove public share for a session. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[None] + """ + _response = self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}/public-share", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return HttpResponse(response=_response, data=None) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawSessionsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list_sessions( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + filter_by: typing.Optional[SessionStatus] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[SessionListResponse]: + """ + Get paginated list of AI agent sessions with optional status filtering. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + filter_by : typing.Optional[SessionStatus] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[SessionListResponse] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + "sessions", + method="GET", + params={ + "pageSize": page_size, + "pageNumber": page_number, + "filterBy": filter_by, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionListResponse, + construct_type( + type_=SessionListResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create_session( + self, + *, + profile_id: typing.Optional[str] = OMIT, + proxy_country_code: typing.Optional[ProxyCountryCode] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[SessionItemView]: + """ + Create a new session with a new task. + + Parameters + ---------- + profile_id : typing.Optional[str] + The ID of the profile to use for the session + + proxy_country_code : typing.Optional[ProxyCountryCode] + Country code for proxy location. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[SessionItemView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + "sessions", + method="POST", + json={ + "profileId": profile_id, + "proxyCountryCode": proxy_country_code, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionItemView, + construct_type( + type_=SessionItemView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[SessionView]: + """ + Get detailed session information including status, URLs, and task details. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[SessionView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionView, + construct_type( + type_=SessionView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def delete_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[None]: + """ + Permanently delete a session and all associated data. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[None] + """ + _response = await self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return AsyncHttpResponse(response=_response, data=None) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def update_session( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[SessionView]: + """ + Stop a session and all its running tasks. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[SessionView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}", + method="PATCH", + json={ + "action": "stop", + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionView, + construct_type( + type_=SessionView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ShareView]: + """ + Get public share information including URL and usage statistics. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ShareView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}/public-share", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ShareView, + construct_type( + type_=ShareView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ShareView]: + """ + Create or return existing public share for a session. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ShareView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}/public-share", + method="POST", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ShareView, + construct_type( + type_=ShareView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def delete_session_public_share( + self, session_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[None]: + """ + Remove public share for a session. + + Parameters + ---------- + session_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[None] + """ + _response = await self._client_wrapper.httpx_client.request( + f"sessions/{jsonable_encoder(session_id)}/public-share", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return AsyncHttpResponse(response=_response, data=None) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/browser_use/tasks/__init__.py b/src/browser_use/tasks/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/browser_use/tasks/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/browser_use/tasks/client.py b/src/browser_use/tasks/client.py new file mode 100644 index 0000000..5d2574e --- /dev/null +++ b/src/browser_use/tasks/client.py @@ -0,0 +1,610 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.supported_ll_ms import SupportedLlMs +from ..types.task_created_response import TaskCreatedResponse +from ..types.task_list_response import TaskListResponse +from ..types.task_log_file_response import TaskLogFileResponse +from ..types.task_status import TaskStatus +from ..types.task_update_action import TaskUpdateAction +from ..types.task_view import TaskView +from .raw_client import AsyncRawTasksClient, RawTasksClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class TasksClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawTasksClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawTasksClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawTasksClient + """ + return self._raw_client + + def list_tasks( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + session_id: typing.Optional[str] = None, + filter_by: typing.Optional[TaskStatus] = None, + after: typing.Optional[dt.datetime] = None, + before: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> TaskListResponse: + """ + Get paginated list of AI agent tasks with optional filtering by session and status. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + session_id : typing.Optional[str] + + filter_by : typing.Optional[TaskStatus] + + after : typing.Optional[dt.datetime] + + before : typing.Optional[dt.datetime] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskListResponse + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.tasks.list_tasks() + """ + _response = self._raw_client.list_tasks( + page_size=page_size, + page_number=page_number, + session_id=session_id, + filter_by=filter_by, + after=after, + before=before, + request_options=request_options, + ) + return _response.data + + def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + structured_output: typing.Optional[str] = OMIT, + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> TaskCreatedResponse: + """ + You can either: + 1. Start a new task (auto creates a new simple session) + 2. Start a new task in an existing session (you can create a custom session before starting the task and reuse it for follow-up tasks) + + Parameters + ---------- + task : str + The task prompt/instruction for the agent. + + llm : typing.Optional[SupportedLlMs] + The LLM model to use for the agent. + + start_url : typing.Optional[str] + The URL to start the task from. + + max_steps : typing.Optional[int] + Maximum number of steps the agent can take before stopping. + + structured_output : typing.Optional[str] + The stringified JSON schema for the structured output. + + session_id : typing.Optional[str] + The ID of the session where the task will run. + + metadata : typing.Optional[typing.Dict[str, typing.Optional[str]]] + The metadata for the task. + + secrets : typing.Optional[typing.Dict[str, typing.Optional[str]]] + The secrets for the task. + + allowed_domains : typing.Optional[typing.Sequence[str]] + The allowed domains for the task. + + highlight_elements : typing.Optional[bool] + Tells the agent to highlight interactive elements on the page. + + flash_mode : typing.Optional[bool] + Whether agent flash mode is enabled. + + thinking : typing.Optional[bool] + Whether agent thinking mode is enabled. + + vision : typing.Optional[bool] + Whether agent vision capabilities are enabled. + + system_prompt_extension : typing.Optional[str] + Optional extension to the agent system prompt. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskCreatedResponse + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.tasks.create_task( + task="task", + ) + """ + _response = self._raw_client.create_task( + task=task, + llm=llm, + start_url=start_url, + max_steps=max_steps, + structured_output=structured_output, + session_id=session_id, + metadata=metadata, + secrets=secrets, + allowed_domains=allowed_domains, + highlight_elements=highlight_elements, + flash_mode=flash_mode, + thinking=thinking, + vision=vision, + system_prompt_extension=system_prompt_extension, + request_options=request_options, + ) + return _response.data + + def get_task(self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> TaskView: + """ + Get detailed task information including status, progress, steps, and file outputs. + + Parameters + ---------- + task_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.tasks.get_task( + task_id="task_id", + ) + """ + _response = self._raw_client.get_task(task_id, request_options=request_options) + return _response.data + + def update_task( + self, task_id: str, *, action: TaskUpdateAction, request_options: typing.Optional[RequestOptions] = None + ) -> TaskView: + """ + Control task execution with stop, pause, resume, or stop task and session actions. + + Parameters + ---------- + task_id : str + + action : TaskUpdateAction + The action to perform on the task + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskView + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.tasks.update_task( + task_id="task_id", + action="stop", + ) + """ + _response = self._raw_client.update_task(task_id, action=action, request_options=request_options) + return _response.data + + def get_task_logs( + self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> TaskLogFileResponse: + """ + Get secure download URL for task execution logs with step-by-step details. + + Parameters + ---------- + task_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskLogFileResponse + Successful Response + + Examples + -------- + from browser_use import BrowserUse + + client = BrowserUse( + api_key="YOUR_API_KEY", + ) + client.tasks.get_task_logs( + task_id="task_id", + ) + """ + _response = self._raw_client.get_task_logs(task_id, request_options=request_options) + return _response.data + + +class AsyncTasksClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawTasksClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawTasksClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawTasksClient + """ + return self._raw_client + + async def list_tasks( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + session_id: typing.Optional[str] = None, + filter_by: typing.Optional[TaskStatus] = None, + after: typing.Optional[dt.datetime] = None, + before: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> TaskListResponse: + """ + Get paginated list of AI agent tasks with optional filtering by session and status. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + session_id : typing.Optional[str] + + filter_by : typing.Optional[TaskStatus] + + after : typing.Optional[dt.datetime] + + before : typing.Optional[dt.datetime] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskListResponse + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.tasks.list_tasks() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_tasks( + page_size=page_size, + page_number=page_number, + session_id=session_id, + filter_by=filter_by, + after=after, + before=before, + request_options=request_options, + ) + return _response.data + + async def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + structured_output: typing.Optional[str] = OMIT, + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> TaskCreatedResponse: + """ + You can either: + 1. Start a new task (auto creates a new simple session) + 2. Start a new task in an existing session (you can create a custom session before starting the task and reuse it for follow-up tasks) + + Parameters + ---------- + task : str + The task prompt/instruction for the agent. + + llm : typing.Optional[SupportedLlMs] + The LLM model to use for the agent. + + start_url : typing.Optional[str] + The URL to start the task from. + + max_steps : typing.Optional[int] + Maximum number of steps the agent can take before stopping. + + structured_output : typing.Optional[str] + The stringified JSON schema for the structured output. + + session_id : typing.Optional[str] + The ID of the session where the task will run. + + metadata : typing.Optional[typing.Dict[str, typing.Optional[str]]] + The metadata for the task. + + secrets : typing.Optional[typing.Dict[str, typing.Optional[str]]] + The secrets for the task. + + allowed_domains : typing.Optional[typing.Sequence[str]] + The allowed domains for the task. + + highlight_elements : typing.Optional[bool] + Tells the agent to highlight interactive elements on the page. + + flash_mode : typing.Optional[bool] + Whether agent flash mode is enabled. + + thinking : typing.Optional[bool] + Whether agent thinking mode is enabled. + + vision : typing.Optional[bool] + Whether agent vision capabilities are enabled. + + system_prompt_extension : typing.Optional[str] + Optional extension to the agent system prompt. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskCreatedResponse + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.tasks.create_task( + task="task", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_task( + task=task, + llm=llm, + start_url=start_url, + max_steps=max_steps, + structured_output=structured_output, + session_id=session_id, + metadata=metadata, + secrets=secrets, + allowed_domains=allowed_domains, + highlight_elements=highlight_elements, + flash_mode=flash_mode, + thinking=thinking, + vision=vision, + system_prompt_extension=system_prompt_extension, + request_options=request_options, + ) + return _response.data + + async def get_task(self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> TaskView: + """ + Get detailed task information including status, progress, steps, and file outputs. + + Parameters + ---------- + task_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.tasks.get_task( + task_id="task_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_task(task_id, request_options=request_options) + return _response.data + + async def update_task( + self, task_id: str, *, action: TaskUpdateAction, request_options: typing.Optional[RequestOptions] = None + ) -> TaskView: + """ + Control task execution with stop, pause, resume, or stop task and session actions. + + Parameters + ---------- + task_id : str + + action : TaskUpdateAction + The action to perform on the task + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskView + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.tasks.update_task( + task_id="task_id", + action="stop", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update_task(task_id, action=action, request_options=request_options) + return _response.data + + async def get_task_logs( + self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> TaskLogFileResponse: + """ + Get secure download URL for task execution logs with step-by-step details. + + Parameters + ---------- + task_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TaskLogFileResponse + Successful Response + + Examples + -------- + import asyncio + + from browser_use import AsyncBrowserUse + + client = AsyncBrowserUse( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.tasks.get_task_logs( + task_id="task_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_task_logs(task_id, request_options=request_options) + return _response.data diff --git a/src/browser_use/tasks/raw_client.py b/src/browser_use/tasks/raw_client.py new file mode 100644 index 0000000..4235d34 --- /dev/null +++ b/src/browser_use/tasks/raw_client.py @@ -0,0 +1,933 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.datetime_utils import serialize_datetime +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.bad_request_error import BadRequestError +from ..errors.internal_server_error import InternalServerError +from ..errors.not_found_error import NotFoundError +from ..errors.payment_required_error import PaymentRequiredError +from ..errors.unprocessable_entity_error import UnprocessableEntityError +from ..types.insufficient_credits_error import InsufficientCreditsError +from ..types.supported_ll_ms import SupportedLlMs +from ..types.task_created_response import TaskCreatedResponse +from ..types.task_list_response import TaskListResponse +from ..types.task_log_file_response import TaskLogFileResponse +from ..types.task_status import TaskStatus +from ..types.task_update_action import TaskUpdateAction +from ..types.task_view import TaskView + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawTasksClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list_tasks( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + session_id: typing.Optional[str] = None, + filter_by: typing.Optional[TaskStatus] = None, + after: typing.Optional[dt.datetime] = None, + before: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[TaskListResponse]: + """ + Get paginated list of AI agent tasks with optional filtering by session and status. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + session_id : typing.Optional[str] + + filter_by : typing.Optional[TaskStatus] + + after : typing.Optional[dt.datetime] + + before : typing.Optional[dt.datetime] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TaskListResponse] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + "tasks", + method="GET", + params={ + "pageSize": page_size, + "pageNumber": page_number, + "sessionId": session_id, + "filterBy": filter_by, + "after": serialize_datetime(after) if after is not None else None, + "before": serialize_datetime(before) if before is not None else None, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskListResponse, + construct_type( + type_=TaskListResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + structured_output: typing.Optional[str] = OMIT, + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[TaskCreatedResponse]: + """ + You can either: + 1. Start a new task (auto creates a new simple session) + 2. Start a new task in an existing session (you can create a custom session before starting the task and reuse it for follow-up tasks) + + Parameters + ---------- + task : str + The task prompt/instruction for the agent. + + llm : typing.Optional[SupportedLlMs] + The LLM model to use for the agent. + + start_url : typing.Optional[str] + The URL to start the task from. + + max_steps : typing.Optional[int] + Maximum number of steps the agent can take before stopping. + + structured_output : typing.Optional[str] + The stringified JSON schema for the structured output. + + session_id : typing.Optional[str] + The ID of the session where the task will run. + + metadata : typing.Optional[typing.Dict[str, typing.Optional[str]]] + The metadata for the task. + + secrets : typing.Optional[typing.Dict[str, typing.Optional[str]]] + The secrets for the task. + + allowed_domains : typing.Optional[typing.Sequence[str]] + The allowed domains for the task. + + highlight_elements : typing.Optional[bool] + Tells the agent to highlight interactive elements on the page. + + flash_mode : typing.Optional[bool] + Whether agent flash mode is enabled. + + thinking : typing.Optional[bool] + Whether agent thinking mode is enabled. + + vision : typing.Optional[bool] + Whether agent vision capabilities are enabled. + + system_prompt_extension : typing.Optional[str] + Optional extension to the agent system prompt. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TaskCreatedResponse] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + "tasks", + method="POST", + json={ + "task": task, + "llm": llm, + "startUrl": start_url, + "maxSteps": max_steps, + "structuredOutput": structured_output, + "sessionId": session_id, + "metadata": metadata, + "secrets": secrets, + "allowedDomains": allowed_domains, + "highlightElements": highlight_elements, + "flashMode": flash_mode, + "thinking": thinking, + "vision": vision, + "systemPromptExtension": system_prompt_extension, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskCreatedResponse, + construct_type( + type_=TaskCreatedResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + InsufficientCreditsError, + construct_type( + type_=InsufficientCreditsError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_task( + self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[TaskView]: + """ + Get detailed task information including status, progress, steps, and file outputs. + + Parameters + ---------- + task_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TaskView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"tasks/{jsonable_encoder(task_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskView, + construct_type( + type_=TaskView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def update_task( + self, task_id: str, *, action: TaskUpdateAction, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[TaskView]: + """ + Control task execution with stop, pause, resume, or stop task and session actions. + + Parameters + ---------- + task_id : str + + action : TaskUpdateAction + The action to perform on the task + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TaskView] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"tasks/{jsonable_encoder(task_id)}", + method="PATCH", + json={ + "action": action, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskView, + construct_type( + type_=TaskView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_task_logs( + self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[TaskLogFileResponse]: + """ + Get secure download URL for task execution logs with step-by-step details. + + Parameters + ---------- + task_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TaskLogFileResponse] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + f"tasks/{jsonable_encoder(task_id)}/logs", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskLogFileResponse, + construct_type( + type_=TaskLogFileResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawTasksClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list_tasks( + self, + *, + page_size: typing.Optional[int] = None, + page_number: typing.Optional[int] = None, + session_id: typing.Optional[str] = None, + filter_by: typing.Optional[TaskStatus] = None, + after: typing.Optional[dt.datetime] = None, + before: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[TaskListResponse]: + """ + Get paginated list of AI agent tasks with optional filtering by session and status. + + Parameters + ---------- + page_size : typing.Optional[int] + + page_number : typing.Optional[int] + + session_id : typing.Optional[str] + + filter_by : typing.Optional[TaskStatus] + + after : typing.Optional[dt.datetime] + + before : typing.Optional[dt.datetime] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TaskListResponse] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + "tasks", + method="GET", + params={ + "pageSize": page_size, + "pageNumber": page_number, + "sessionId": session_id, + "filterBy": filter_by, + "after": serialize_datetime(after) if after is not None else None, + "before": serialize_datetime(before) if before is not None else None, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskListResponse, + construct_type( + type_=TaskListResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + structured_output: typing.Optional[str] = OMIT, + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[TaskCreatedResponse]: + """ + You can either: + 1. Start a new task (auto creates a new simple session) + 2. Start a new task in an existing session (you can create a custom session before starting the task and reuse it for follow-up tasks) + + Parameters + ---------- + task : str + The task prompt/instruction for the agent. + + llm : typing.Optional[SupportedLlMs] + The LLM model to use for the agent. + + start_url : typing.Optional[str] + The URL to start the task from. + + max_steps : typing.Optional[int] + Maximum number of steps the agent can take before stopping. + + structured_output : typing.Optional[str] + The stringified JSON schema for the structured output. + + session_id : typing.Optional[str] + The ID of the session where the task will run. + + metadata : typing.Optional[typing.Dict[str, typing.Optional[str]]] + The metadata for the task. + + secrets : typing.Optional[typing.Dict[str, typing.Optional[str]]] + The secrets for the task. + + allowed_domains : typing.Optional[typing.Sequence[str]] + The allowed domains for the task. + + highlight_elements : typing.Optional[bool] + Tells the agent to highlight interactive elements on the page. + + flash_mode : typing.Optional[bool] + Whether agent flash mode is enabled. + + thinking : typing.Optional[bool] + Whether agent thinking mode is enabled. + + vision : typing.Optional[bool] + Whether agent vision capabilities are enabled. + + system_prompt_extension : typing.Optional[str] + Optional extension to the agent system prompt. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TaskCreatedResponse] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + "tasks", + method="POST", + json={ + "task": task, + "llm": llm, + "startUrl": start_url, + "maxSteps": max_steps, + "structuredOutput": structured_output, + "sessionId": session_id, + "metadata": metadata, + "secrets": secrets, + "allowedDomains": allowed_domains, + "highlightElements": highlight_elements, + "flashMode": flash_mode, + "thinking": thinking, + "vision": vision, + "systemPromptExtension": system_prompt_extension, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskCreatedResponse, + construct_type( + type_=TaskCreatedResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + InsufficientCreditsError, + construct_type( + type_=InsufficientCreditsError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_task( + self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[TaskView]: + """ + Get detailed task information including status, progress, steps, and file outputs. + + Parameters + ---------- + task_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TaskView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"tasks/{jsonable_encoder(task_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskView, + construct_type( + type_=TaskView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def update_task( + self, task_id: str, *, action: TaskUpdateAction, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[TaskView]: + """ + Control task execution with stop, pause, resume, or stop task and session actions. + + Parameters + ---------- + task_id : str + + action : TaskUpdateAction + The action to perform on the task + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TaskView] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"tasks/{jsonable_encoder(task_id)}", + method="PATCH", + json={ + "action": action, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskView, + construct_type( + type_=TaskView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_task_logs( + self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[TaskLogFileResponse]: + """ + Get secure download URL for task execution logs with step-by-step details. + + Parameters + ---------- + task_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TaskLogFileResponse] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + f"tasks/{jsonable_encoder(task_id)}/logs", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TaskLogFileResponse, + construct_type( + type_=TaskLogFileResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + construct_type( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/browser_use/types/__init__.py b/src/browser_use/types/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/browser_use/types/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/browser_use/types/account_not_found_error.py b/src/browser_use/types/account_not_found_error.py new file mode 100644 index 0000000..8b7cdae --- /dev/null +++ b/src/browser_use/types/account_not_found_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class AccountNotFoundError(UncheckedBaseModel): + """ + Error response when a account is not found + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/account_view.py b/src/browser_use/types/account_view.py new file mode 100644 index 0000000..0deeeb7 --- /dev/null +++ b/src/browser_use/types/account_view.py @@ -0,0 +1,54 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class AccountView(UncheckedBaseModel): + """ + View model for account information + """ + + monthly_credits_balance_usd: typing_extensions.Annotated[float, FieldMetadata(alias="monthlyCreditsBalanceUsd")] = ( + pydantic.Field() + ) + """ + The monthly credits balance in USD + """ + + additional_credits_balance_usd: typing_extensions.Annotated[ + float, FieldMetadata(alias="additionalCreditsBalanceUsd") + ] = pydantic.Field() + """ + The additional credits balance in USD + """ + + email: typing.Optional[str] = pydantic.Field(default=None) + """ + The email address of the user + """ + + name: typing.Optional[str] = pydantic.Field(default=None) + """ + The name of the user + """ + + signed_up_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="signedUpAt")] = pydantic.Field() + """ + The date and time the user signed up + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/bad_request_error_body.py b/src/browser_use/types/bad_request_error_body.py new file mode 100644 index 0000000..0826417 --- /dev/null +++ b/src/browser_use/types/bad_request_error_body.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .session_has_running_task_error import SessionHasRunningTaskError +from .session_stopped_error import SessionStoppedError + +BadRequestErrorBody = typing.Union[SessionStoppedError, SessionHasRunningTaskError] diff --git a/src/browser_use/types/credits_deduction_error.py b/src/browser_use/types/credits_deduction_error.py new file mode 100644 index 0000000..e57637c --- /dev/null +++ b/src/browser_use/types/credits_deduction_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class CreditsDeductionError(UncheckedBaseModel): + """ + Error response when credits deduction fails + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/download_url_generation_error.py b/src/browser_use/types/download_url_generation_error.py new file mode 100644 index 0000000..f97eb2f --- /dev/null +++ b/src/browser_use/types/download_url_generation_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class DownloadUrlGenerationError(UncheckedBaseModel): + """ + Error response when download URL generation fails + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/file_view.py b/src/browser_use/types/file_view.py new file mode 100644 index 0000000..b283213 --- /dev/null +++ b/src/browser_use/types/file_view.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class FileView(UncheckedBaseModel): + """ + View model for representing an output file generated by the agent + """ + + id: str = pydantic.Field() + """ + Unique identifier for the output file + """ + + file_name: typing_extensions.Annotated[str, FieldMetadata(alias="fileName")] = pydantic.Field() + """ + Name of the output file + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/http_validation_error.py b/src/browser_use/types/http_validation_error.py new file mode 100644 index 0000000..188935a --- /dev/null +++ b/src/browser_use/types/http_validation_error.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .validation_error import ValidationError + + +class HttpValidationError(UncheckedBaseModel): + detail: typing.Optional[typing.List[ValidationError]] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/insufficient_credits_error.py b/src/browser_use/types/insufficient_credits_error.py new file mode 100644 index 0000000..2a141c4 --- /dev/null +++ b/src/browser_use/types/insufficient_credits_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class InsufficientCreditsError(UncheckedBaseModel): + """ + Error response when user has insufficient credits + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/internal_server_error_body.py b/src/browser_use/types/internal_server_error_body.py new file mode 100644 index 0000000..32825e1 --- /dev/null +++ b/src/browser_use/types/internal_server_error_body.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class InternalServerErrorBody(UncheckedBaseModel): + """ + Error response for internal server errors + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/not_found_error_body.py b/src/browser_use/types/not_found_error_body.py new file mode 100644 index 0000000..a935598 --- /dev/null +++ b/src/browser_use/types/not_found_error_body.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .output_file_not_found_error import OutputFileNotFoundError +from .task_not_found_error import TaskNotFoundError + +NotFoundErrorBody = typing.Union[TaskNotFoundError, OutputFileNotFoundError] diff --git a/src/browser_use/types/output_file_not_found_error.py b/src/browser_use/types/output_file_not_found_error.py new file mode 100644 index 0000000..1f2c0dd --- /dev/null +++ b/src/browser_use/types/output_file_not_found_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class OutputFileNotFoundError(UncheckedBaseModel): + """ + Error response when an output file is not found + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/profile_list_response.py b/src/browser_use/types/profile_list_response.py new file mode 100644 index 0000000..dfb1a45 --- /dev/null +++ b/src/browser_use/types/profile_list_response.py @@ -0,0 +1,45 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .profile_view import ProfileView + + +class ProfileListResponse(UncheckedBaseModel): + """ + Response model for paginated profile list requests. + """ + + items: typing.List[ProfileView] = pydantic.Field() + """ + List of profile views for the current page + """ + + total_items: typing_extensions.Annotated[int, FieldMetadata(alias="totalItems")] = pydantic.Field() + """ + Total number of items in the list + """ + + page_number: typing_extensions.Annotated[int, FieldMetadata(alias="pageNumber")] = pydantic.Field() + """ + Page number + """ + + page_size: typing_extensions.Annotated[int, FieldMetadata(alias="pageSize")] = pydantic.Field() + """ + Number of items per page + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/profile_not_found_error.py b/src/browser_use/types/profile_not_found_error.py new file mode 100644 index 0000000..0c22e9c --- /dev/null +++ b/src/browser_use/types/profile_not_found_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ProfileNotFoundError(UncheckedBaseModel): + """ + Error response when a profile is not found + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/profile_view.py b/src/browser_use/types/profile_view.py new file mode 100644 index 0000000..745a4a4 --- /dev/null +++ b/src/browser_use/types/profile_view.py @@ -0,0 +1,49 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ProfileView(UncheckedBaseModel): + """ + View model for representing a profile. A profile lets you preserve the login state between sessions. + + We recommend that you create a separate profile for each user of your app. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the profile + """ + + last_used_at: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="lastUsedAt")] = ( + pydantic.Field(default=None) + ) + """ + Timestamp when the profile was last used + """ + + created_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="createdAt")] = pydantic.Field() + """ + Timestamp when the profile was created + """ + + updated_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="updatedAt")] = pydantic.Field() + """ + Timestamp when the profile was last updated + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/proxy_country_code.py b/src/browser_use/types/proxy_country_code.py new file mode 100644 index 0000000..e531daf --- /dev/null +++ b/src/browser_use/types/proxy_country_code.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ProxyCountryCode = typing.Union[typing.Literal["us", "uk", "fr", "it", "jp", "au", "de", "fi", "ca", "in"], typing.Any] diff --git a/src/browser_use/types/session_has_running_task_error.py b/src/browser_use/types/session_has_running_task_error.py new file mode 100644 index 0000000..502761f --- /dev/null +++ b/src/browser_use/types/session_has_running_task_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SessionHasRunningTaskError(UncheckedBaseModel): + """ + Error response when session already has a running task + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/session_item_view.py b/src/browser_use/types/session_item_view.py new file mode 100644 index 0000000..e14c15d --- /dev/null +++ b/src/browser_use/types/session_item_view.py @@ -0,0 +1,55 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .session_status import SessionStatus + + +class SessionItemView(UncheckedBaseModel): + """ + View model for representing a (browser) session. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the session + """ + + status: SessionStatus = pydantic.Field() + """ + Current status of the session (active/stopped) + """ + + live_url: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="liveUrl")] = pydantic.Field( + default=None + ) + """ + URL where the browser can be viewed live in real-time + """ + + started_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="startedAt")] = pydantic.Field() + """ + Timestamp when the session was created and started + """ + + finished_at: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="finishedAt")] = ( + pydantic.Field(default=None) + ) + """ + Timestamp when the session was stopped (None if still active) + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/session_list_response.py b/src/browser_use/types/session_list_response.py new file mode 100644 index 0000000..7299478 --- /dev/null +++ b/src/browser_use/types/session_list_response.py @@ -0,0 +1,45 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .session_item_view import SessionItemView + + +class SessionListResponse(UncheckedBaseModel): + """ + Response model for paginated session list requests. + """ + + items: typing.List[SessionItemView] = pydantic.Field() + """ + List of session views for the current page + """ + + total_items: typing_extensions.Annotated[int, FieldMetadata(alias="totalItems")] = pydantic.Field() + """ + Total number of items in the list + """ + + page_number: typing_extensions.Annotated[int, FieldMetadata(alias="pageNumber")] = pydantic.Field() + """ + Page number + """ + + page_size: typing_extensions.Annotated[int, FieldMetadata(alias="pageSize")] = pydantic.Field() + """ + Number of items per page + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/session_not_found_error.py b/src/browser_use/types/session_not_found_error.py new file mode 100644 index 0000000..213aabb --- /dev/null +++ b/src/browser_use/types/session_not_found_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SessionNotFoundError(UncheckedBaseModel): + """ + Error response when a session is not found + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/session_status.py b/src/browser_use/types/session_status.py new file mode 100644 index 0000000..8f037d7 --- /dev/null +++ b/src/browser_use/types/session_status.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +SessionStatus = typing.Union[typing.Literal["active", "stopped"], typing.Any] diff --git a/src/browser_use/types/session_stopped_error.py b/src/browser_use/types/session_stopped_error.py new file mode 100644 index 0000000..f753bf0 --- /dev/null +++ b/src/browser_use/types/session_stopped_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SessionStoppedError(UncheckedBaseModel): + """ + Error response when trying to use a stopped session + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/session_update_action.py b/src/browser_use/types/session_update_action.py new file mode 100644 index 0000000..7028c98 --- /dev/null +++ b/src/browser_use/types/session_update_action.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +SessionUpdateAction = typing.Literal["stop"] diff --git a/src/browser_use/types/session_view.py b/src/browser_use/types/session_view.py new file mode 100644 index 0000000..f2e67e3 --- /dev/null +++ b/src/browser_use/types/session_view.py @@ -0,0 +1,68 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .session_status import SessionStatus +from .task_item_view import TaskItemView + + +class SessionView(UncheckedBaseModel): + """ + View model for representing a (browser) session with its associated tasks. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the session + """ + + status: SessionStatus = pydantic.Field() + """ + Current status of the session (active/stopped) + """ + + live_url: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="liveUrl")] = pydantic.Field( + default=None + ) + """ + URL where the browser can be viewed live in real-time + """ + + started_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="startedAt")] = pydantic.Field() + """ + Timestamp when the session was created and started + """ + + finished_at: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="finishedAt")] = ( + pydantic.Field(default=None) + ) + """ + Timestamp when the session was stopped (None if still active) + """ + + tasks: typing.List[TaskItemView] = pydantic.Field() + """ + List of tasks associated with this session + """ + + public_share_url: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="publicShareUrl")] = ( + pydantic.Field(default=None) + ) + """ + Optional URL to access the public share of the session + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/share_not_found_error.py b/src/browser_use/types/share_not_found_error.py new file mode 100644 index 0000000..6f540c7 --- /dev/null +++ b/src/browser_use/types/share_not_found_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ShareNotFoundError(UncheckedBaseModel): + """ + Error response when a public share is not found + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/share_view.py b/src/browser_use/types/share_view.py new file mode 100644 index 0000000..c7aee1c --- /dev/null +++ b/src/browser_use/types/share_view.py @@ -0,0 +1,47 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ShareView(UncheckedBaseModel): + """ + View model for representing a public share of a session. + """ + + share_token: typing_extensions.Annotated[str, FieldMetadata(alias="shareToken")] = pydantic.Field() + """ + Token to access the public share + """ + + share_url: typing_extensions.Annotated[str, FieldMetadata(alias="shareUrl")] = pydantic.Field() + """ + URL to access the public share + """ + + view_count: typing_extensions.Annotated[int, FieldMetadata(alias="viewCount")] = pydantic.Field() + """ + Number of times the public share has been viewed + """ + + last_viewed_at: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="lastViewedAt")] = ( + pydantic.Field(default=None) + ) + """ + Timestamp of the last time the public share was viewed (None if never viewed) + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/supported_ll_ms.py b/src/browser_use/types/supported_ll_ms.py new file mode 100644 index 0000000..980f4e4 --- /dev/null +++ b/src/browser_use/types/supported_ll_ms.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +SupportedLlMs = typing.Union[ + typing.Literal[ + "gpt-4.1", + "gpt-4.1-mini", + "o4-mini", + "o3", + "gemini-2.5-flash", + "gemini-2.5-pro", + "claude-sonnet-4-20250514", + "gpt-4o", + "gpt-4o-mini", + "llama-4-maverick-17b-128e-instruct", + "claude-3-7-sonnet-20250219", + ], + typing.Any, +] diff --git a/src/browser_use/types/task_created_response.py b/src/browser_use/types/task_created_response.py new file mode 100644 index 0000000..4d15a67 --- /dev/null +++ b/src/browser_use/types/task_created_response.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TaskCreatedResponse(UncheckedBaseModel): + """ + Response model for creating a task + """ + + id: str = pydantic.Field() + """ + Unique identifier for the created task + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/task_item_view.py b/src/browser_use/types/task_item_view.py new file mode 100644 index 0000000..54168ca --- /dev/null +++ b/src/browser_use/types/task_item_view.py @@ -0,0 +1,88 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .task_status import TaskStatus + + +class TaskItemView(UncheckedBaseModel): + """ + View model for representing a task with its execution details + """ + + id: str = pydantic.Field() + """ + Unique identifier for the task + """ + + session_id: typing_extensions.Annotated[str, FieldMetadata(alias="sessionId")] = pydantic.Field() + """ + ID of the session this task belongs to + """ + + llm: str = pydantic.Field() + """ + The LLM model used for this task represented as a string + """ + + task: str = pydantic.Field() + """ + The task prompt/instruction given to the agent + """ + + status: TaskStatus + started_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="startedAt")] = pydantic.Field() + """ + Naive UTC timestamp when the task was started + """ + + finished_at: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="finishedAt")] = ( + pydantic.Field(default=None) + ) + """ + Naive UTC timestamp when the task completed (None if still running) + """ + + metadata: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) + """ + Optional additional metadata associated with the task set by the user + """ + + is_scheduled: typing_extensions.Annotated[bool, FieldMetadata(alias="isScheduled")] = pydantic.Field() + """ + Whether this task was created as a scheduled task + """ + + output: typing.Optional[str] = pydantic.Field(default=None) + """ + Final output/result of the task + """ + + browser_use_version: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="browserUseVersion")] = ( + pydantic.Field(default=None) + ) + """ + Version of browser-use used for this task (older tasks may not have this set) + """ + + is_success: typing_extensions.Annotated[typing.Optional[bool], FieldMetadata(alias="isSuccess")] = pydantic.Field( + default=None + ) + """ + Whether the task was successful (self-reported by the agent) + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/task_list_response.py b/src/browser_use/types/task_list_response.py new file mode 100644 index 0000000..59cc4f7 --- /dev/null +++ b/src/browser_use/types/task_list_response.py @@ -0,0 +1,45 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .task_item_view import TaskItemView + + +class TaskListResponse(UncheckedBaseModel): + """ + Response model for paginated task list requests. + """ + + items: typing.List[TaskItemView] = pydantic.Field() + """ + List of task views for the current page + """ + + total_items: typing_extensions.Annotated[int, FieldMetadata(alias="totalItems")] = pydantic.Field() + """ + Total number of items in the list + """ + + page_number: typing_extensions.Annotated[int, FieldMetadata(alias="pageNumber")] = pydantic.Field() + """ + Page number + """ + + page_size: typing_extensions.Annotated[int, FieldMetadata(alias="pageSize")] = pydantic.Field() + """ + Number of items per page + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/task_log_file_response.py b/src/browser_use/types/task_log_file_response.py new file mode 100644 index 0000000..d2bff05 --- /dev/null +++ b/src/browser_use/types/task_log_file_response.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TaskLogFileResponse(UncheckedBaseModel): + """ + Response model for log file requests + """ + + download_url: typing_extensions.Annotated[str, FieldMetadata(alias="downloadUrl")] = pydantic.Field() + """ + URL to download the log file + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/task_not_found_error.py b/src/browser_use/types/task_not_found_error.py new file mode 100644 index 0000000..41c660d --- /dev/null +++ b/src/browser_use/types/task_not_found_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TaskNotFoundError(UncheckedBaseModel): + """ + Error response when a task is not found + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/task_output_file_response.py b/src/browser_use/types/task_output_file_response.py new file mode 100644 index 0000000..ef5fca8 --- /dev/null +++ b/src/browser_use/types/task_output_file_response.py @@ -0,0 +1,39 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TaskOutputFileResponse(UncheckedBaseModel): + """ + Response model for output file requests. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the file + """ + + file_name: typing_extensions.Annotated[str, FieldMetadata(alias="fileName")] = pydantic.Field() + """ + Name of the file + """ + + download_url: typing_extensions.Annotated[str, FieldMetadata(alias="downloadUrl")] = pydantic.Field() + """ + URL to download the file + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/task_status.py b/src/browser_use/types/task_status.py new file mode 100644 index 0000000..7e63a77 --- /dev/null +++ b/src/browser_use/types/task_status.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +TaskStatus = typing.Union[typing.Literal["started", "paused", "finished", "stopped"], typing.Any] diff --git a/src/browser_use/types/task_step_view.py b/src/browser_use/types/task_step_view.py new file mode 100644 index 0000000..aa2e957 --- /dev/null +++ b/src/browser_use/types/task_step_view.py @@ -0,0 +1,63 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TaskStepView(UncheckedBaseModel): + """ + View model for representing a single step in a task's execution + """ + + number: int = pydantic.Field() + """ + Sequential step number within the task + """ + + memory: str = pydantic.Field() + """ + Agent's memory at this step + """ + + evaluation_previous_goal: typing_extensions.Annotated[str, FieldMetadata(alias="evaluationPreviousGoal")] = ( + pydantic.Field() + ) + """ + Agent's evaluation of the previous goal completion + """ + + next_goal: typing_extensions.Annotated[str, FieldMetadata(alias="nextGoal")] = pydantic.Field() + """ + The goal for the next step + """ + + url: str = pydantic.Field() + """ + Current URL the browser is on for this step + """ + + screenshot_url: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="screenshotUrl")] = ( + pydantic.Field(default=None) + ) + """ + Optional URL to the screenshot taken at this step + """ + + actions: typing.List[str] = pydantic.Field() + """ + List of stringified json actions performed by the agent in this step + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/task_update_action.py b/src/browser_use/types/task_update_action.py new file mode 100644 index 0000000..3cd41a7 --- /dev/null +++ b/src/browser_use/types/task_update_action.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +TaskUpdateAction = typing.Union[typing.Literal["stop", "pause", "resume", "stop_task_and_session"], typing.Any] diff --git a/src/browser_use/types/task_view.py b/src/browser_use/types/task_view.py new file mode 100644 index 0000000..6e0ad0b --- /dev/null +++ b/src/browser_use/types/task_view.py @@ -0,0 +1,92 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .file_view import FileView +from .task_status import TaskStatus +from .task_step_view import TaskStepView + + +class TaskView(UncheckedBaseModel): + """ + View model for representing a task with its execution details + """ + + id: str = pydantic.Field() + """ + Unique identifier for the task + """ + + session_id: typing_extensions.Annotated[str, FieldMetadata(alias="sessionId")] + llm: str = pydantic.Field() + """ + The LLM model used for this task represented as a string + """ + + task: str = pydantic.Field() + """ + The task prompt/instruction given to the agent + """ + + status: TaskStatus = pydantic.Field() + """ + Current status of the task execution + """ + + started_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="startedAt")] = pydantic.Field() + """ + Naive UTC timestamp when the task was started + """ + + finished_at: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="finishedAt")] = ( + pydantic.Field(default=None) + ) + """ + Naive UTC timestamp when the task completed (None if still running) + """ + + metadata: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) + """ + Optional additional metadata associated with the task set by the user + """ + + is_scheduled: typing_extensions.Annotated[bool, FieldMetadata(alias="isScheduled")] = pydantic.Field() + """ + Whether this task was created as a scheduled task + """ + + steps: typing.List[TaskStepView] + output: typing.Optional[str] = pydantic.Field(default=None) + """ + Final output/result of the task + """ + + output_files: typing_extensions.Annotated[typing.List[FileView], FieldMetadata(alias="outputFiles")] + browser_use_version: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="browserUseVersion")] = ( + pydantic.Field(default=None) + ) + """ + Version of browser-use used for this task (older tasks may not have this set) + """ + + is_success: typing_extensions.Annotated[typing.Optional[bool], FieldMetadata(alias="isSuccess")] = pydantic.Field( + default=None + ) + """ + Whether the task was successful (self-reported by the agent) + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/unsupported_content_type_error.py b/src/browser_use/types/unsupported_content_type_error.py new file mode 100644 index 0000000..5bd09c2 --- /dev/null +++ b/src/browser_use/types/unsupported_content_type_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class UnsupportedContentTypeError(UncheckedBaseModel): + """ + Error response for unsupported content types + """ + + detail: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/upload_file_presigned_url_response.py b/src/browser_use/types/upload_file_presigned_url_response.py new file mode 100644 index 0000000..8d01683 --- /dev/null +++ b/src/browser_use/types/upload_file_presigned_url_response.py @@ -0,0 +1,49 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class UploadFilePresignedUrlResponse(UncheckedBaseModel): + """ + Response model for a presigned upload URL. + """ + + url: str = pydantic.Field() + """ + The URL to upload the file to. + """ + + method: typing.Literal["POST"] = pydantic.Field(default="POST") + """ + The HTTP method to use for the upload. + """ + + fields: typing.Dict[str, str] = pydantic.Field() + """ + The form fields to include in the upload request. + """ + + file_name: typing_extensions.Annotated[str, FieldMetadata(alias="fileName")] = pydantic.Field() + """ + The name of the file to upload (should be referenced when user wants to use the file in a task). + """ + + expires_in: typing_extensions.Annotated[int, FieldMetadata(alias="expiresIn")] = pydantic.Field() + """ + The number of seconds until the presigned URL expires. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/validation_error.py b/src/browser_use/types/validation_error.py new file mode 100644 index 0000000..0438bc0 --- /dev/null +++ b/src/browser_use/types/validation_error.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .validation_error_loc_item import ValidationErrorLocItem + + +class ValidationError(UncheckedBaseModel): + loc: typing.List[ValidationErrorLocItem] + msg: str + type: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/browser_use/types/validation_error_loc_item.py b/src/browser_use/types/validation_error_loc_item.py new file mode 100644 index 0000000..9a0a83f --- /dev/null +++ b/src/browser_use/types/validation_error_loc_item.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ValidationErrorLocItem = typing.Union[str, int] diff --git a/src/browser_use/version.py b/src/browser_use/version.py new file mode 100644 index 0000000..a95c7ec --- /dev/null +++ b/src/browser_use/version.py @@ -0,0 +1,3 @@ +from importlib import metadata + +__version__ = metadata.version("browser-use") diff --git a/src/browser_use/wrapper/browser_use_client.py b/src/browser_use/wrapper/browser_use_client.py new file mode 100644 index 0000000..e69de29 diff --git a/src/browser_use/wrapper/parse.py b/src/browser_use/wrapper/parse.py new file mode 100644 index 0000000..f62cec4 --- /dev/null +++ b/src/browser_use/wrapper/parse.py @@ -0,0 +1,80 @@ +import hashlib +import json +from datetime import datetime +from typing import Any, Generic, TypeVar, Union + +from pydantic import BaseModel + +from browser_use.types.task_created_response import TaskCreatedResponse +from browser_use.types.task_view import TaskView + +T = TypeVar("T", bound=BaseModel) + + +class TaskViewWithOutput(TaskView, Generic[T]): + """ + TaskView with structured output. + """ + + parsed_output: Union[T, None] + + +class CustomJSONEncoder(json.JSONEncoder): + """Custom JSON encoder to handle datetime objects.""" + + # NOTE: Python doesn't have the override decorator in 3.8, that's why we ignore it. + def default(self, o: Any) -> Any: # type: ignore[override] + if isinstance(o, datetime): + return o.isoformat() + return super().default(o) + + +def hash_task_view(task_view: TaskView) -> str: + """Hashes the task view to detect changes.""" + return hashlib.sha256( + json.dumps(task_view.model_dump(), sort_keys=True, cls=CustomJSONEncoder).encode() + ).hexdigest() + + +def _watch( + self, + task_id: str, + interval: float = 1, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, +) -> Iterator[TaskView]: + """Converts a polling loop into a generator loop.""" + hash: str | None = None + + while True: + res = self.retrieve( + task_id=task_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + + res_hash = hash_task_view(res) + + if hash is None or res_hash != hash: + hash = res_hash + yield res + + if res.status == "finished": + break + + time.sleep(interval) + + +class WrapperTaskCreatedResponse(TaskCreatedResponse): + """TaskCreatedResponse with utility methods for easier interfacing with Browser Use Cloud.""" + + def __init__(self, id: str): + super().__init__() + self.id = id diff --git a/src/browser_use/wrapper/tasks/client.py b/src/browser_use/wrapper/tasks/client.py new file mode 100644 index 0000000..5e8388b --- /dev/null +++ b/src/browser_use/wrapper/tasks/client.py @@ -0,0 +1,8 @@ +from browser_use.tasks.client import SyncClientWrapper, TasksClient + + +class BrowserUseTasksClient(TasksClient): + """TasksClient with utility method overrides.""" + + def __init__(self, *, client_wrapper: SyncClientWrapper): + super().__init__(client_wrapper=client_wrapper) diff --git a/src/browser_use_sdk/lib/webhooks.py b/src/browser_use/wrapper/webhooks.py similarity index 100% rename from src/browser_use_sdk/lib/webhooks.py rename to src/browser_use/wrapper/webhooks.py diff --git a/src/browser_use_sdk/__init__.py b/src/browser_use_sdk/__init__.py deleted file mode 100644 index 235c0b0..0000000 --- a/src/browser_use_sdk/__init__.py +++ /dev/null @@ -1,100 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import typing as _t - -from . import types -from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes -from ._utils import file_from_path -from ._client import ( - Client, - Stream, - Timeout, - Transport, - BrowserUse, - AsyncClient, - AsyncStream, - RequestOptions, - AsyncBrowserUse, -) -from ._models import BaseModel -from ._version import __title__, __version__ -from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse -from ._constants import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_CONNECTION_LIMITS -from ._exceptions import ( - APIError, - ConflictError, - NotFoundError, - APIStatusError, - RateLimitError, - APITimeoutError, - BadRequestError, - BrowserUseError, - APIConnectionError, - AuthenticationError, - InternalServerError, - PermissionDeniedError, - UnprocessableEntityError, - APIResponseValidationError, -) -from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient -from ._utils._logs import setup_logging as _setup_logging - -__all__ = [ - "types", - "__version__", - "__title__", - "NoneType", - "Transport", - "ProxiesTypes", - "NotGiven", - "NOT_GIVEN", - "Omit", - "BrowserUseError", - "APIError", - "APIStatusError", - "APITimeoutError", - "APIConnectionError", - "APIResponseValidationError", - "BadRequestError", - "AuthenticationError", - "PermissionDeniedError", - "NotFoundError", - "ConflictError", - "UnprocessableEntityError", - "RateLimitError", - "InternalServerError", - "Timeout", - "RequestOptions", - "Client", - "AsyncClient", - "Stream", - "AsyncStream", - "BrowserUse", - "AsyncBrowserUse", - "file_from_path", - "BaseModel", - "DEFAULT_TIMEOUT", - "DEFAULT_MAX_RETRIES", - "DEFAULT_CONNECTION_LIMITS", - "DefaultHttpxClient", - "DefaultAsyncHttpxClient", - "DefaultAioHttpClient", -] - -if not _t.TYPE_CHECKING: - from ._utils._resources_proxy import resources as resources - -_setup_logging() - -# Update the __module__ attribute for exported symbols so that -# error messages point to this module instead of the module -# it was originally defined in, e.g. -# browser_use_sdk._exceptions.NotFoundError -> browser_use_sdk.NotFoundError -__locals = locals() -for __name in __all__: - if not __name.startswith("__"): - try: - __locals[__name].__module__ = "browser_use_sdk" - except (TypeError, AttributeError): - # Some of our exported symbols are builtins which we can't set attributes for. - pass diff --git a/src/browser_use_sdk/_base_client.py b/src/browser_use_sdk/_base_client.py deleted file mode 100644 index f182716..0000000 --- a/src/browser_use_sdk/_base_client.py +++ /dev/null @@ -1,1995 +0,0 @@ -from __future__ import annotations - -import sys -import json -import time -import uuid -import email -import asyncio -import inspect -import logging -import platform -import email.utils -from types import TracebackType -from random import random -from typing import ( - TYPE_CHECKING, - Any, - Dict, - Type, - Union, - Generic, - Mapping, - TypeVar, - Iterable, - Iterator, - Optional, - Generator, - AsyncIterator, - cast, - overload, -) -from typing_extensions import Literal, override, get_origin - -import anyio -import httpx -import distro -import pydantic -from httpx import URL -from pydantic import PrivateAttr - -from . import _exceptions -from ._qs import Querystring -from ._files import to_httpx_files, async_to_httpx_files -from ._types import ( - NOT_GIVEN, - Body, - Omit, - Query, - Headers, - Timeout, - NotGiven, - ResponseT, - AnyMapping, - PostParser, - RequestFiles, - HttpxSendArgs, - RequestOptions, - HttpxRequestFiles, - ModelBuilderProtocol, -) -from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping -from ._compat import PYDANTIC_V2, model_copy, model_dump -from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type -from ._response import ( - APIResponse, - BaseAPIResponse, - AsyncAPIResponse, - extract_response_type, -) -from ._constants import ( - DEFAULT_TIMEOUT, - MAX_RETRY_DELAY, - DEFAULT_MAX_RETRIES, - INITIAL_RETRY_DELAY, - RAW_RESPONSE_HEADER, - OVERRIDE_CAST_TO_HEADER, - DEFAULT_CONNECTION_LIMITS, -) -from ._streaming import Stream, SSEDecoder, AsyncStream, SSEBytesDecoder -from ._exceptions import ( - APIStatusError, - APITimeoutError, - APIConnectionError, - APIResponseValidationError, -) - -log: logging.Logger = logging.getLogger(__name__) - -# TODO: make base page type vars covariant -SyncPageT = TypeVar("SyncPageT", bound="BaseSyncPage[Any]") -AsyncPageT = TypeVar("AsyncPageT", bound="BaseAsyncPage[Any]") - - -_T = TypeVar("_T") -_T_co = TypeVar("_T_co", covariant=True) - -_StreamT = TypeVar("_StreamT", bound=Stream[Any]) -_AsyncStreamT = TypeVar("_AsyncStreamT", bound=AsyncStream[Any]) - -if TYPE_CHECKING: - from httpx._config import ( - DEFAULT_TIMEOUT_CONFIG, # pyright: ignore[reportPrivateImportUsage] - ) - - HTTPX_DEFAULT_TIMEOUT = DEFAULT_TIMEOUT_CONFIG -else: - try: - from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT - except ImportError: - # taken from https://github.com/encode/httpx/blob/3ba5fe0d7ac70222590e759c31442b1cab263791/httpx/_config.py#L366 - HTTPX_DEFAULT_TIMEOUT = Timeout(5.0) - - -class PageInfo: - """Stores the necessary information to build the request to retrieve the next page. - - Either `url` or `params` must be set. - """ - - url: URL | NotGiven - params: Query | NotGiven - json: Body | NotGiven - - @overload - def __init__( - self, - *, - url: URL, - ) -> None: ... - - @overload - def __init__( - self, - *, - params: Query, - ) -> None: ... - - @overload - def __init__( - self, - *, - json: Body, - ) -> None: ... - - def __init__( - self, - *, - url: URL | NotGiven = NOT_GIVEN, - json: Body | NotGiven = NOT_GIVEN, - params: Query | NotGiven = NOT_GIVEN, - ) -> None: - self.url = url - self.json = json - self.params = params - - @override - def __repr__(self) -> str: - if self.url: - return f"{self.__class__.__name__}(url={self.url})" - if self.json: - return f"{self.__class__.__name__}(json={self.json})" - return f"{self.__class__.__name__}(params={self.params})" - - -class BasePage(GenericModel, Generic[_T]): - """ - Defines the core interface for pagination. - - Type Args: - ModelT: The pydantic model that represents an item in the response. - - Methods: - has_next_page(): Check if there is another page available - next_page_info(): Get the necessary information to make a request for the next page - """ - - _options: FinalRequestOptions = PrivateAttr() - _model: Type[_T] = PrivateAttr() - - def has_next_page(self) -> bool: - items = self._get_page_items() - if not items: - return False - return self.next_page_info() is not None - - def next_page_info(self) -> Optional[PageInfo]: ... - - def _get_page_items(self) -> Iterable[_T]: # type: ignore[empty-body] - ... - - def _params_from_url(self, url: URL) -> httpx.QueryParams: - # TODO: do we have to preprocess params here? - return httpx.QueryParams(cast(Any, self._options.params)).merge(url.params) - - def _info_to_options(self, info: PageInfo) -> FinalRequestOptions: - options = model_copy(self._options) - options._strip_raw_response_header() - - if not isinstance(info.params, NotGiven): - options.params = {**options.params, **info.params} - return options - - if not isinstance(info.url, NotGiven): - params = self._params_from_url(info.url) - url = info.url.copy_with(params=params) - options.params = dict(url.params) - options.url = str(url) - return options - - if not isinstance(info.json, NotGiven): - if not is_mapping(info.json): - raise TypeError("Pagination is only supported with mappings") - - if not options.json_data: - options.json_data = {**info.json} - else: - if not is_mapping(options.json_data): - raise TypeError("Pagination is only supported with mappings") - - options.json_data = {**options.json_data, **info.json} - return options - - raise ValueError("Unexpected PageInfo state") - - -class BaseSyncPage(BasePage[_T], Generic[_T]): - _client: SyncAPIClient = pydantic.PrivateAttr() - - def _set_private_attributes( - self, - client: SyncAPIClient, - model: Type[_T], - options: FinalRequestOptions, - ) -> None: - if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: - self.__pydantic_private__ = {} - - self._model = model - self._client = client - self._options = options - - # Pydantic uses a custom `__iter__` method to support casting BaseModels - # to dictionaries. e.g. dict(model). - # As we want to support `for item in page`, this is inherently incompatible - # with the default pydantic behaviour. It is not possible to support both - # use cases at once. Fortunately, this is not a big deal as all other pydantic - # methods should continue to work as expected as there is an alternative method - # to cast a model to a dictionary, model.dict(), which is used internally - # by pydantic. - def __iter__(self) -> Iterator[_T]: # type: ignore - for page in self.iter_pages(): - for item in page._get_page_items(): - yield item - - def iter_pages(self: SyncPageT) -> Iterator[SyncPageT]: - page = self - while True: - yield page - if page.has_next_page(): - page = page.get_next_page() - else: - return - - def get_next_page(self: SyncPageT) -> SyncPageT: - info = self.next_page_info() - if not info: - raise RuntimeError( - "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." - ) - - options = self._info_to_options(info) - return self._client._request_api_list(self._model, page=self.__class__, options=options) - - -class AsyncPaginator(Generic[_T, AsyncPageT]): - def __init__( - self, - client: AsyncAPIClient, - options: FinalRequestOptions, - page_cls: Type[AsyncPageT], - model: Type[_T], - ) -> None: - self._model = model - self._client = client - self._options = options - self._page_cls = page_cls - - def __await__(self) -> Generator[Any, None, AsyncPageT]: - return self._get_page().__await__() - - async def _get_page(self) -> AsyncPageT: - def _parser(resp: AsyncPageT) -> AsyncPageT: - resp._set_private_attributes( - model=self._model, - options=self._options, - client=self._client, - ) - return resp - - self._options.post_parser = _parser - - return await self._client.request(self._page_cls, self._options) - - async def __aiter__(self) -> AsyncIterator[_T]: - # https://github.com/microsoft/pyright/issues/3464 - page = cast( - AsyncPageT, - await self, # type: ignore - ) - async for item in page: - yield item - - -class BaseAsyncPage(BasePage[_T], Generic[_T]): - _client: AsyncAPIClient = pydantic.PrivateAttr() - - def _set_private_attributes( - self, - model: Type[_T], - client: AsyncAPIClient, - options: FinalRequestOptions, - ) -> None: - if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: - self.__pydantic_private__ = {} - - self._model = model - self._client = client - self._options = options - - async def __aiter__(self) -> AsyncIterator[_T]: - async for page in self.iter_pages(): - for item in page._get_page_items(): - yield item - - async def iter_pages(self: AsyncPageT) -> AsyncIterator[AsyncPageT]: - page = self - while True: - yield page - if page.has_next_page(): - page = await page.get_next_page() - else: - return - - async def get_next_page(self: AsyncPageT) -> AsyncPageT: - info = self.next_page_info() - if not info: - raise RuntimeError( - "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." - ) - - options = self._info_to_options(info) - return await self._client._request_api_list(self._model, page=self.__class__, options=options) - - -_HttpxClientT = TypeVar("_HttpxClientT", bound=Union[httpx.Client, httpx.AsyncClient]) -_DefaultStreamT = TypeVar("_DefaultStreamT", bound=Union[Stream[Any], AsyncStream[Any]]) - - -class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]): - _client: _HttpxClientT - _version: str - _base_url: URL - max_retries: int - timeout: Union[float, Timeout, None] - _strict_response_validation: bool - _idempotency_header: str | None - _default_stream_cls: type[_DefaultStreamT] | None = None - - def __init__( - self, - *, - version: str, - base_url: str | URL, - _strict_response_validation: bool, - max_retries: int = DEFAULT_MAX_RETRIES, - timeout: float | Timeout | None = DEFAULT_TIMEOUT, - custom_headers: Mapping[str, str] | None = None, - custom_query: Mapping[str, object] | None = None, - ) -> None: - self._version = version - self._base_url = self._enforce_trailing_slash(URL(base_url)) - self.max_retries = max_retries - self.timeout = timeout - self._custom_headers = custom_headers or {} - self._custom_query = custom_query or {} - self._strict_response_validation = _strict_response_validation - self._idempotency_header = None - self._platform: Platform | None = None - - if max_retries is None: # pyright: ignore[reportUnnecessaryComparison] - raise TypeError( - "max_retries cannot be None. If you want to disable retries, pass `0`; if you want unlimited retries, pass `math.inf` or a very high number; if you want the default behavior, pass `browser_use_sdk.DEFAULT_MAX_RETRIES`" - ) - - def _enforce_trailing_slash(self, url: URL) -> URL: - if url.raw_path.endswith(b"/"): - return url - return url.copy_with(raw_path=url.raw_path + b"/") - - def _make_status_error_from_response( - self, - response: httpx.Response, - ) -> APIStatusError: - if response.is_closed and not response.is_stream_consumed: - # We can't read the response body as it has been closed - # before it was read. This can happen if an event hook - # raises a status error. - body = None - err_msg = f"Error code: {response.status_code}" - else: - err_text = response.text.strip() - body = err_text - - try: - body = json.loads(err_text) - err_msg = f"Error code: {response.status_code} - {body}" - except Exception: - err_msg = err_text or f"Error code: {response.status_code}" - - return self._make_status_error(err_msg, body=body, response=response) - - def _make_status_error( - self, - err_msg: str, - *, - body: object, - response: httpx.Response, - ) -> _exceptions.APIStatusError: - raise NotImplementedError() - - def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx.Headers: - custom_headers = options.headers or {} - headers_dict = _merge_mappings(self.default_headers, custom_headers) - self._validate_headers(headers_dict, custom_headers) - - # headers are case-insensitive while dictionaries are not. - headers = httpx.Headers(headers_dict) - - idempotency_header = self._idempotency_header - if idempotency_header and options.idempotency_key and idempotency_header not in headers: - headers[idempotency_header] = options.idempotency_key - - # Don't set these headers if they were already set or removed by the caller. We check - # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case. - lower_custom_headers = [header.lower() for header in custom_headers] - if "x-stainless-retry-count" not in lower_custom_headers: - headers["x-stainless-retry-count"] = str(retries_taken) - if "x-stainless-read-timeout" not in lower_custom_headers: - timeout = self.timeout if isinstance(options.timeout, NotGiven) else options.timeout - if isinstance(timeout, Timeout): - timeout = timeout.read - if timeout is not None: - headers["x-stainless-read-timeout"] = str(timeout) - - return headers - - def _prepare_url(self, url: str) -> URL: - """ - Merge a URL argument together with any 'base_url' on the client, - to create the URL used for the outgoing request. - """ - # Copied from httpx's `_merge_url` method. - merge_url = URL(url) - if merge_url.is_relative_url: - merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/") - return self.base_url.copy_with(raw_path=merge_raw_path) - - return merge_url - - def _make_sse_decoder(self) -> SSEDecoder | SSEBytesDecoder: - return SSEDecoder() - - def _build_request( - self, - options: FinalRequestOptions, - *, - retries_taken: int = 0, - ) -> httpx.Request: - if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - - kwargs: dict[str, Any] = {} - - json_data = options.json_data - if options.extra_json is not None: - if json_data is None: - json_data = cast(Body, options.extra_json) - elif is_mapping(json_data): - json_data = _merge_mappings(json_data, options.extra_json) - else: - raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`") - - headers = self._build_headers(options, retries_taken=retries_taken) - params = _merge_mappings(self.default_query, options.params) - content_type = headers.get("Content-Type") - files = options.files - - # If the given Content-Type header is multipart/form-data then it - # has to be removed so that httpx can generate the header with - # additional information for us as it has to be in this form - # for the server to be able to correctly parse the request: - # multipart/form-data; boundary=---abc-- - if content_type is not None and content_type.startswith("multipart/form-data"): - if "boundary" not in content_type: - # only remove the header if the boundary hasn't been explicitly set - # as the caller doesn't want httpx to come up with their own boundary - headers.pop("Content-Type") - - # As we are now sending multipart/form-data instead of application/json - # we need to tell httpx to use it, https://www.python-httpx.org/advanced/clients/#multipart-file-encoding - if json_data: - if not is_dict(json_data): - raise TypeError( - f"Expected query input to be a dictionary for multipart requests but got {type(json_data)} instead." - ) - kwargs["data"] = self._serialize_multipartform(json_data) - - # httpx determines whether or not to send a "multipart/form-data" - # request based on the truthiness of the "files" argument. - # This gets around that issue by generating a dict value that - # evaluates to true. - # - # https://github.com/encode/httpx/discussions/2399#discussioncomment-3814186 - if not files: - files = cast(HttpxRequestFiles, ForceMultipartDict()) - - prepared_url = self._prepare_url(options.url) - if "_" in prepared_url.host: - # work around https://github.com/encode/httpx/discussions/2880 - kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} - - is_body_allowed = options.method.lower() != "get" - - if is_body_allowed: - if isinstance(json_data, bytes): - kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None - kwargs["files"] = files - else: - headers.pop("Content-Type", None) - kwargs.pop("data", None) - - # TODO: report this error to httpx - return self._client.build_request( # pyright: ignore[reportUnknownMemberType] - headers=headers, - timeout=self.timeout if isinstance(options.timeout, NotGiven) else options.timeout, - method=options.method, - url=prepared_url, - # the `Query` type that we use is incompatible with qs' - # `Params` type as it needs to be typed as `Mapping[str, object]` - # so that passing a `TypedDict` doesn't cause an error. - # https://github.com/microsoft/pyright/issues/3526#event-6715453066 - params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, - **kwargs, - ) - - def _serialize_multipartform(self, data: Mapping[object, object]) -> dict[str, object]: - items = self.qs.stringify_items( - # TODO: type ignore is required as stringify_items is well typed but we can't be - # well typed without heavy validation. - data, # type: ignore - array_format="brackets", - ) - serialized: dict[str, object] = {} - for key, value in items: - existing = serialized.get(key) - - if not existing: - serialized[key] = value - continue - - # If a value has already been set for this key then that - # means we're sending data like `array[]=[1, 2, 3]` and we - # need to tell httpx that we want to send multiple values with - # the same key which is done by using a list or a tuple. - # - # Note: 2d arrays should never result in the same key at both - # levels so it's safe to assume that if the value is a list, - # it was because we changed it to be a list. - if is_list(existing): - existing.append(value) - else: - serialized[key] = [existing, value] - - return serialized - - def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalRequestOptions) -> type[ResponseT]: - if not is_given(options.headers): - return cast_to - - # make a copy of the headers so we don't mutate user-input - headers = dict(options.headers) - - # we internally support defining a temporary header to override the - # default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response` - # see _response.py for implementation details - override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, NOT_GIVEN) - if is_given(override_cast_to): - options.headers = headers - return cast(Type[ResponseT], override_cast_to) - - return cast_to - - def _should_stream_response_body(self, request: httpx.Request) -> bool: - return request.headers.get(RAW_RESPONSE_HEADER) == "stream" # type: ignore[no-any-return] - - def _process_response_data( - self, - *, - data: object, - cast_to: type[ResponseT], - response: httpx.Response, - ) -> ResponseT: - if data is None: - return cast(ResponseT, None) - - if cast_to is object: - return cast(ResponseT, data) - - try: - if inspect.isclass(cast_to) and issubclass(cast_to, ModelBuilderProtocol): - return cast(ResponseT, cast_to.build(response=response, data=data)) - - if self._strict_response_validation: - return cast(ResponseT, validate_type(type_=cast_to, value=data)) - - return cast(ResponseT, construct_type(type_=cast_to, value=data)) - except pydantic.ValidationError as err: - raise APIResponseValidationError(response=response, body=data) from err - - @property - def qs(self) -> Querystring: - return Querystring() - - @property - def custom_auth(self) -> httpx.Auth | None: - return None - - @property - def auth_headers(self) -> dict[str, str]: - return {} - - @property - def default_headers(self) -> dict[str, str | Omit]: - return { - "Accept": "application/json", - "Content-Type": "application/json", - "User-Agent": self.user_agent, - **self.platform_headers(), - **self.auth_headers, - **self._custom_headers, - } - - @property - def default_query(self) -> dict[str, object]: - return { - **self._custom_query, - } - - def _validate_headers( - self, - headers: Headers, # noqa: ARG002 - custom_headers: Headers, # noqa: ARG002 - ) -> None: - """Validate the given default headers and custom headers. - - Does nothing by default. - """ - return - - @property - def user_agent(self) -> str: - return f"{self.__class__.__name__}/Python {self._version}" - - @property - def base_url(self) -> URL: - return self._base_url - - @base_url.setter - def base_url(self, url: URL | str) -> None: - self._base_url = self._enforce_trailing_slash(url if isinstance(url, URL) else URL(url)) - - def platform_headers(self) -> Dict[str, str]: - # the actual implementation is in a separate `lru_cache` decorated - # function because adding `lru_cache` to methods will leak memory - # https://github.com/python/cpython/issues/88476 - return platform_headers(self._version, platform=self._platform) - - def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None: - """Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified. - - About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After - See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax - """ - if response_headers is None: - return None - - # First, try the non-standard `retry-after-ms` header for milliseconds, - # which is more precise than integer-seconds `retry-after` - try: - retry_ms_header = response_headers.get("retry-after-ms", None) - return float(retry_ms_header) / 1000 - except (TypeError, ValueError): - pass - - # Next, try parsing `retry-after` header as seconds (allowing nonstandard floats). - retry_header = response_headers.get("retry-after") - try: - # note: the spec indicates that this should only ever be an integer - # but if someone sends a float there's no reason for us to not respect it - return float(retry_header) - except (TypeError, ValueError): - pass - - # Last, try parsing `retry-after` as a date. - retry_date_tuple = email.utils.parsedate_tz(retry_header) - if retry_date_tuple is None: - return None - - retry_date = email.utils.mktime_tz(retry_date_tuple) - return float(retry_date - time.time()) - - def _calculate_retry_timeout( - self, - remaining_retries: int, - options: FinalRequestOptions, - response_headers: Optional[httpx.Headers] = None, - ) -> float: - max_retries = options.get_max_retries(self.max_retries) - - # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. - retry_after = self._parse_retry_after_header(response_headers) - if retry_after is not None and 0 < retry_after <= 60: - return retry_after - - # Also cap retry count to 1000 to avoid any potential overflows with `pow` - nb_retries = min(max_retries - remaining_retries, 1000) - - # Apply exponential backoff, but not more than the max. - sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY) - - # Apply some jitter, plus-or-minus half a second. - jitter = 1 - 0.25 * random() - timeout = sleep_seconds * jitter - return timeout if timeout >= 0 else 0 - - def _should_retry(self, response: httpx.Response) -> bool: - # Note: this is not a standard header - should_retry_header = response.headers.get("x-should-retry") - - # If the server explicitly says whether or not to retry, obey. - if should_retry_header == "true": - log.debug("Retrying as header `x-should-retry` is set to `true`") - return True - if should_retry_header == "false": - log.debug("Not retrying as header `x-should-retry` is set to `false`") - return False - - # Retry on request timeouts. - if response.status_code == 408: - log.debug("Retrying due to status code %i", response.status_code) - return True - - # Retry on lock timeouts. - if response.status_code == 409: - log.debug("Retrying due to status code %i", response.status_code) - return True - - # Retry on rate limits. - if response.status_code == 429: - log.debug("Retrying due to status code %i", response.status_code) - return True - - # Retry internal errors. - if response.status_code >= 500: - log.debug("Retrying due to status code %i", response.status_code) - return True - - log.debug("Not retrying") - return False - - def _idempotency_key(self) -> str: - return f"stainless-python-retry-{uuid.uuid4()}" - - -class _DefaultHttpxClient(httpx.Client): - def __init__(self, **kwargs: Any) -> None: - kwargs.setdefault("timeout", DEFAULT_TIMEOUT) - kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) - kwargs.setdefault("follow_redirects", True) - super().__init__(**kwargs) - - -if TYPE_CHECKING: - DefaultHttpxClient = httpx.Client - """An alias to `httpx.Client` that provides the same defaults that this SDK - uses internally. - - This is useful because overriding the `http_client` with your own instance of - `httpx.Client` will result in httpx's defaults being used, not ours. - """ -else: - DefaultHttpxClient = _DefaultHttpxClient - - -class SyncHttpxClientWrapper(DefaultHttpxClient): - def __del__(self) -> None: - if self.is_closed: - return - - try: - self.close() - except Exception: - pass - - -class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]): - _client: httpx.Client - _default_stream_cls: type[Stream[Any]] | None = None - - def __init__( - self, - *, - version: str, - base_url: str | URL, - max_retries: int = DEFAULT_MAX_RETRIES, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, - http_client: httpx.Client | None = None, - custom_headers: Mapping[str, str] | None = None, - custom_query: Mapping[str, object] | None = None, - _strict_response_validation: bool, - ) -> None: - if not is_given(timeout): - # if the user passed in a custom http client with a non-default - # timeout set then we use that timeout. - # - # note: there is an edge case here where the user passes in a client - # where they've explicitly set the timeout to match the default timeout - # as this check is structural, meaning that we'll think they didn't - # pass in a timeout and will ignore it - if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: - timeout = http_client.timeout - else: - timeout = DEFAULT_TIMEOUT - - if http_client is not None and not isinstance(http_client, httpx.Client): # pyright: ignore[reportUnnecessaryIsInstance] - raise TypeError( - f"Invalid `http_client` argument; Expected an instance of `httpx.Client` but got {type(http_client)}" - ) - - super().__init__( - version=version, - # cast to a valid type because mypy doesn't understand our type narrowing - timeout=cast(Timeout, timeout), - base_url=base_url, - max_retries=max_retries, - custom_query=custom_query, - custom_headers=custom_headers, - _strict_response_validation=_strict_response_validation, - ) - self._client = http_client or SyncHttpxClientWrapper( - base_url=base_url, - # cast to a valid type because mypy doesn't understand our type narrowing - timeout=cast(Timeout, timeout), - ) - - def is_closed(self) -> bool: - return self._client.is_closed - - def close(self) -> None: - """Close the underlying HTTPX client. - - The client will *not* be usable after this. - """ - # If an error is thrown while constructing a client, self._client - # may not be present - if hasattr(self, "_client"): - self._client.close() - - def __enter__(self: _T) -> _T: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - self.close() - - def _prepare_options( - self, - options: FinalRequestOptions, # noqa: ARG002 - ) -> FinalRequestOptions: - """Hook for mutating the given options""" - return options - - def _prepare_request( - self, - request: httpx.Request, # noqa: ARG002 - ) -> None: - """This method is used as a callback for mutating the `Request` object - after it has been constructed. - This is useful for cases where you want to add certain headers based off of - the request properties, e.g. `url`, `method` etc. - """ - return None - - @overload - def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: Literal[True], - stream_cls: Type[_StreamT], - ) -> _StreamT: ... - - @overload - def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: bool = False, - stream_cls: Type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: ... - - def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: bool = False, - stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: - cast_to = self._maybe_override_cast_to(cast_to, options) - - # create a copy of the options we were given so that if the - # options are mutated later & we then retry, the retries are - # given the original options - input_options = model_copy(options) - if input_options.idempotency_key is None and input_options.method.lower() != "get": - # ensure the idempotency key is reused between requests - input_options.idempotency_key = self._idempotency_key() - - response: httpx.Response | None = None - max_retries = input_options.get_max_retries(self.max_retries) - - retries_taken = 0 - for retries_taken in range(max_retries + 1): - options = model_copy(input_options) - options = self._prepare_options(options) - - remaining_retries = max_retries - retries_taken - request = self._build_request(options, retries_taken=retries_taken) - self._prepare_request(request) - - kwargs: HttpxSendArgs = {} - if self.custom_auth is not None: - kwargs["auth"] = self.custom_auth - - if options.follow_redirects is not None: - kwargs["follow_redirects"] = options.follow_redirects - - log.debug("Sending HTTP Request: %s %s", request.method, request.url) - - response = None - try: - response = self._client.send( - request, - stream=stream or self._should_stream_response_body(request=request), - **kwargs, - ) - except httpx.TimeoutException as err: - log.debug("Encountered httpx.TimeoutException", exc_info=True) - - if remaining_retries > 0: - self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=None, - ) - continue - - log.debug("Raising timeout error") - raise APITimeoutError(request=request) from err - except Exception as err: - log.debug("Encountered Exception", exc_info=True) - - if remaining_retries > 0: - self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=None, - ) - continue - - log.debug("Raising connection error") - raise APIConnectionError(request=request) from err - - log.debug( - 'HTTP Response: %s %s "%i %s" %s', - request.method, - request.url, - response.status_code, - response.reason_phrase, - response.headers, - ) - - try: - response.raise_for_status() - except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code - log.debug("Encountered httpx.HTTPStatusError", exc_info=True) - - if remaining_retries > 0 and self._should_retry(err.response): - err.response.close() - self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=response, - ) - continue - - # If the response is streamed then we need to explicitly read the response - # to completion before attempting to access the response text. - if not err.response.is_closed: - err.response.read() - - log.debug("Re-raising status error") - raise self._make_status_error_from_response(err.response) from None - - break - - assert response is not None, "could not resolve response (should never happen)" - return self._process_response( - cast_to=cast_to, - options=options, - response=response, - stream=stream, - stream_cls=stream_cls, - retries_taken=retries_taken, - ) - - def _sleep_for_retry( - self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None - ) -> None: - remaining_retries = max_retries - retries_taken - if remaining_retries == 1: - log.debug("1 retry left") - else: - log.debug("%i retries left", remaining_retries) - - timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) - log.info("Retrying request to %s in %f seconds", options.url, timeout) - - time.sleep(timeout) - - def _process_response( - self, - *, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - response: httpx.Response, - stream: bool, - stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, - retries_taken: int = 0, - ) -> ResponseT: - origin = get_origin(cast_to) or cast_to - - if ( - inspect.isclass(origin) - and issubclass(origin, BaseAPIResponse) - # we only want to actually return the custom BaseAPIResponse class if we're - # returning the raw response, or if we're not streaming SSE, as if we're streaming - # SSE then `cast_to` doesn't actively reflect the type we need to parse into - and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) - ): - if not issubclass(origin, APIResponse): - raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}") - - response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) - return cast( - ResponseT, - response_cls( - raw=response, - client=self, - cast_to=extract_response_type(response_cls), - stream=stream, - stream_cls=stream_cls, - options=options, - retries_taken=retries_taken, - ), - ) - - if cast_to == httpx.Response: - return cast(ResponseT, response) - - api_response = APIResponse( - raw=response, - client=self, - cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] - stream=stream, - stream_cls=stream_cls, - options=options, - retries_taken=retries_taken, - ) - if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): - return cast(ResponseT, api_response) - - return api_response.parse() - - def _request_api_list( - self, - model: Type[object], - page: Type[SyncPageT], - options: FinalRequestOptions, - ) -> SyncPageT: - def _parser(resp: SyncPageT) -> SyncPageT: - resp._set_private_attributes( - client=self, - model=model, - options=options, - ) - return resp - - options.post_parser = _parser - - return self.request(page, options, stream=False) - - @overload - def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: Literal[True], - stream_cls: type[_StreamT], - ) -> _StreamT: ... - - @overload - def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: bool, - stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: ... - - def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: bool = False, - stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: - opts = FinalRequestOptions.construct(method="get", url=path, **options) - # cast is required because mypy complains about returning Any even though - # it understands the type variables - return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) - - @overload - def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - options: RequestOptions = {}, - files: RequestFiles | None = None, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - options: RequestOptions = {}, - files: RequestFiles | None = None, - stream: Literal[True], - stream_cls: type[_StreamT], - ) -> _StreamT: ... - - @overload - def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - options: RequestOptions = {}, - files: RequestFiles | None = None, - stream: bool, - stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: ... - - def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - options: RequestOptions = {}, - files: RequestFiles | None = None, - stream: bool = False, - stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: - opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options - ) - return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) - - def patch( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) - return self.request(cast_to, opts) - - def put( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options - ) - return self.request(cast_to, opts) - - def delete( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) - return self.request(cast_to, opts) - - def get_api_list( - self, - path: str, - *, - model: Type[object], - page: Type[SyncPageT], - body: Body | None = None, - options: RequestOptions = {}, - method: str = "get", - ) -> SyncPageT: - opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) - return self._request_api_list(model, page, opts) - - -class _DefaultAsyncHttpxClient(httpx.AsyncClient): - def __init__(self, **kwargs: Any) -> None: - kwargs.setdefault("timeout", DEFAULT_TIMEOUT) - kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) - kwargs.setdefault("follow_redirects", True) - super().__init__(**kwargs) - - -try: - import httpx_aiohttp -except ImportError: - - class _DefaultAioHttpClient(httpx.AsyncClient): - def __init__(self, **_kwargs: Any) -> None: - raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra") -else: - - class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore - def __init__(self, **kwargs: Any) -> None: - kwargs.setdefault("timeout", DEFAULT_TIMEOUT) - kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) - kwargs.setdefault("follow_redirects", True) - - super().__init__(**kwargs) - - -if TYPE_CHECKING: - DefaultAsyncHttpxClient = httpx.AsyncClient - """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK - uses internally. - - This is useful because overriding the `http_client` with your own instance of - `httpx.AsyncClient` will result in httpx's defaults being used, not ours. - """ - - DefaultAioHttpClient = httpx.AsyncClient - """An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`.""" -else: - DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient - DefaultAioHttpClient = _DefaultAioHttpClient - - -class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): - def __del__(self) -> None: - if self.is_closed: - return - - try: - # TODO(someday): support non asyncio runtimes here - asyncio.get_running_loop().create_task(self.aclose()) - except Exception: - pass - - -class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]): - _client: httpx.AsyncClient - _default_stream_cls: type[AsyncStream[Any]] | None = None - - def __init__( - self, - *, - version: str, - base_url: str | URL, - _strict_response_validation: bool, - max_retries: int = DEFAULT_MAX_RETRIES, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, - http_client: httpx.AsyncClient | None = None, - custom_headers: Mapping[str, str] | None = None, - custom_query: Mapping[str, object] | None = None, - ) -> None: - if not is_given(timeout): - # if the user passed in a custom http client with a non-default - # timeout set then we use that timeout. - # - # note: there is an edge case here where the user passes in a client - # where they've explicitly set the timeout to match the default timeout - # as this check is structural, meaning that we'll think they didn't - # pass in a timeout and will ignore it - if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: - timeout = http_client.timeout - else: - timeout = DEFAULT_TIMEOUT - - if http_client is not None and not isinstance(http_client, httpx.AsyncClient): # pyright: ignore[reportUnnecessaryIsInstance] - raise TypeError( - f"Invalid `http_client` argument; Expected an instance of `httpx.AsyncClient` but got {type(http_client)}" - ) - - super().__init__( - version=version, - base_url=base_url, - # cast to a valid type because mypy doesn't understand our type narrowing - timeout=cast(Timeout, timeout), - max_retries=max_retries, - custom_query=custom_query, - custom_headers=custom_headers, - _strict_response_validation=_strict_response_validation, - ) - self._client = http_client or AsyncHttpxClientWrapper( - base_url=base_url, - # cast to a valid type because mypy doesn't understand our type narrowing - timeout=cast(Timeout, timeout), - ) - - def is_closed(self) -> bool: - return self._client.is_closed - - async def close(self) -> None: - """Close the underlying HTTPX client. - - The client will *not* be usable after this. - """ - await self._client.aclose() - - async def __aenter__(self: _T) -> _T: - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - await self.close() - - async def _prepare_options( - self, - options: FinalRequestOptions, # noqa: ARG002 - ) -> FinalRequestOptions: - """Hook for mutating the given options""" - return options - - async def _prepare_request( - self, - request: httpx.Request, # noqa: ARG002 - ) -> None: - """This method is used as a callback for mutating the `Request` object - after it has been constructed. - This is useful for cases where you want to add certain headers based off of - the request properties, e.g. `url`, `method` etc. - """ - return None - - @overload - async def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - async def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: Literal[True], - stream_cls: type[_AsyncStreamT], - ) -> _AsyncStreamT: ... - - @overload - async def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: bool, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: ... - - async def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: bool = False, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: - if self._platform is None: - # `get_platform` can make blocking IO calls so we - # execute it earlier while we are in an async context - self._platform = await asyncify(get_platform)() - - cast_to = self._maybe_override_cast_to(cast_to, options) - - # create a copy of the options we were given so that if the - # options are mutated later & we then retry, the retries are - # given the original options - input_options = model_copy(options) - if input_options.idempotency_key is None and input_options.method.lower() != "get": - # ensure the idempotency key is reused between requests - input_options.idempotency_key = self._idempotency_key() - - response: httpx.Response | None = None - max_retries = input_options.get_max_retries(self.max_retries) - - retries_taken = 0 - for retries_taken in range(max_retries + 1): - options = model_copy(input_options) - options = await self._prepare_options(options) - - remaining_retries = max_retries - retries_taken - request = self._build_request(options, retries_taken=retries_taken) - await self._prepare_request(request) - - kwargs: HttpxSendArgs = {} - if self.custom_auth is not None: - kwargs["auth"] = self.custom_auth - - if options.follow_redirects is not None: - kwargs["follow_redirects"] = options.follow_redirects - - log.debug("Sending HTTP Request: %s %s", request.method, request.url) - - response = None - try: - response = await self._client.send( - request, - stream=stream or self._should_stream_response_body(request=request), - **kwargs, - ) - except httpx.TimeoutException as err: - log.debug("Encountered httpx.TimeoutException", exc_info=True) - - if remaining_retries > 0: - await self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=None, - ) - continue - - log.debug("Raising timeout error") - raise APITimeoutError(request=request) from err - except Exception as err: - log.debug("Encountered Exception", exc_info=True) - - if remaining_retries > 0: - await self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=None, - ) - continue - - log.debug("Raising connection error") - raise APIConnectionError(request=request) from err - - log.debug( - 'HTTP Response: %s %s "%i %s" %s', - request.method, - request.url, - response.status_code, - response.reason_phrase, - response.headers, - ) - - try: - response.raise_for_status() - except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code - log.debug("Encountered httpx.HTTPStatusError", exc_info=True) - - if remaining_retries > 0 and self._should_retry(err.response): - await err.response.aclose() - await self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=response, - ) - continue - - # If the response is streamed then we need to explicitly read the response - # to completion before attempting to access the response text. - if not err.response.is_closed: - await err.response.aread() - - log.debug("Re-raising status error") - raise self._make_status_error_from_response(err.response) from None - - break - - assert response is not None, "could not resolve response (should never happen)" - return await self._process_response( - cast_to=cast_to, - options=options, - response=response, - stream=stream, - stream_cls=stream_cls, - retries_taken=retries_taken, - ) - - async def _sleep_for_retry( - self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None - ) -> None: - remaining_retries = max_retries - retries_taken - if remaining_retries == 1: - log.debug("1 retry left") - else: - log.debug("%i retries left", remaining_retries) - - timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) - log.info("Retrying request to %s in %f seconds", options.url, timeout) - - await anyio.sleep(timeout) - - async def _process_response( - self, - *, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - response: httpx.Response, - stream: bool, - stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, - retries_taken: int = 0, - ) -> ResponseT: - origin = get_origin(cast_to) or cast_to - - if ( - inspect.isclass(origin) - and issubclass(origin, BaseAPIResponse) - # we only want to actually return the custom BaseAPIResponse class if we're - # returning the raw response, or if we're not streaming SSE, as if we're streaming - # SSE then `cast_to` doesn't actively reflect the type we need to parse into - and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) - ): - if not issubclass(origin, AsyncAPIResponse): - raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}") - - response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) - return cast( - "ResponseT", - response_cls( - raw=response, - client=self, - cast_to=extract_response_type(response_cls), - stream=stream, - stream_cls=stream_cls, - options=options, - retries_taken=retries_taken, - ), - ) - - if cast_to == httpx.Response: - return cast(ResponseT, response) - - api_response = AsyncAPIResponse( - raw=response, - client=self, - cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] - stream=stream, - stream_cls=stream_cls, - options=options, - retries_taken=retries_taken, - ) - if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): - return cast(ResponseT, api_response) - - return await api_response.parse() - - def _request_api_list( - self, - model: Type[_T], - page: Type[AsyncPageT], - options: FinalRequestOptions, - ) -> AsyncPaginator[_T, AsyncPageT]: - return AsyncPaginator(client=self, options=options, page_cls=page, model=model) - - @overload - async def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - async def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: Literal[True], - stream_cls: type[_AsyncStreamT], - ) -> _AsyncStreamT: ... - - @overload - async def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: bool, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: ... - - async def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: bool = False, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: - opts = FinalRequestOptions.construct(method="get", url=path, **options) - return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) - - @overload - async def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - async def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - stream: Literal[True], - stream_cls: type[_AsyncStreamT], - ) -> _AsyncStreamT: ... - - @overload - async def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - stream: bool, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: ... - - async def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - stream: bool = False, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: - opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options - ) - return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) - - async def patch( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) - return await self.request(cast_to, opts) - - async def put( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options - ) - return await self.request(cast_to, opts) - - async def delete( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) - return await self.request(cast_to, opts) - - def get_api_list( - self, - path: str, - *, - model: Type[_T], - page: Type[AsyncPageT], - body: Body | None = None, - options: RequestOptions = {}, - method: str = "get", - ) -> AsyncPaginator[_T, AsyncPageT]: - opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) - return self._request_api_list(model, page, opts) - - -def make_request_options( - *, - query: Query | None = None, - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - idempotency_key: str | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - post_parser: PostParser | NotGiven = NOT_GIVEN, -) -> RequestOptions: - """Create a dict of type RequestOptions without keys of NotGiven values.""" - options: RequestOptions = {} - if extra_headers is not None: - options["headers"] = extra_headers - - if extra_body is not None: - options["extra_json"] = cast(AnyMapping, extra_body) - - if query is not None: - options["params"] = query - - if extra_query is not None: - options["params"] = {**options.get("params", {}), **extra_query} - - if not isinstance(timeout, NotGiven): - options["timeout"] = timeout - - if idempotency_key is not None: - options["idempotency_key"] = idempotency_key - - if is_given(post_parser): - # internal - options["post_parser"] = post_parser # type: ignore - - return options - - -class ForceMultipartDict(Dict[str, None]): - def __bool__(self) -> bool: - return True - - -class OtherPlatform: - def __init__(self, name: str) -> None: - self.name = name - - @override - def __str__(self) -> str: - return f"Other:{self.name}" - - -Platform = Union[ - OtherPlatform, - Literal[ - "MacOS", - "Linux", - "Windows", - "FreeBSD", - "OpenBSD", - "iOS", - "Android", - "Unknown", - ], -] - - -def get_platform() -> Platform: - try: - system = platform.system().lower() - platform_name = platform.platform().lower() - except Exception: - return "Unknown" - - if "iphone" in platform_name or "ipad" in platform_name: - # Tested using Python3IDE on an iPhone 11 and Pythonista on an iPad 7 - # system is Darwin and platform_name is a string like: - # - Darwin-21.6.0-iPhone12,1-64bit - # - Darwin-21.6.0-iPad7,11-64bit - return "iOS" - - if system == "darwin": - return "MacOS" - - if system == "windows": - return "Windows" - - if "android" in platform_name: - # Tested using Pydroid 3 - # system is Linux and platform_name is a string like 'Linux-5.10.81-android12-9-00001-geba40aecb3b7-ab8534902-aarch64-with-libc' - return "Android" - - if system == "linux": - # https://distro.readthedocs.io/en/latest/#distro.id - distro_id = distro.id() - if distro_id == "freebsd": - return "FreeBSD" - - if distro_id == "openbsd": - return "OpenBSD" - - return "Linux" - - if platform_name: - return OtherPlatform(platform_name) - - return "Unknown" - - -@lru_cache(maxsize=None) -def platform_headers(version: str, *, platform: Platform | None) -> Dict[str, str]: - return { - "X-Stainless-Lang": "python", - "X-Stainless-Package-Version": version, - "X-Stainless-OS": str(platform or get_platform()), - "X-Stainless-Arch": str(get_architecture()), - "X-Stainless-Runtime": get_python_runtime(), - "X-Stainless-Runtime-Version": get_python_version(), - } - - -class OtherArch: - def __init__(self, name: str) -> None: - self.name = name - - @override - def __str__(self) -> str: - return f"other:{self.name}" - - -Arch = Union[OtherArch, Literal["x32", "x64", "arm", "arm64", "unknown"]] - - -def get_python_runtime() -> str: - try: - return platform.python_implementation() - except Exception: - return "unknown" - - -def get_python_version() -> str: - try: - return platform.python_version() - except Exception: - return "unknown" - - -def get_architecture() -> Arch: - try: - machine = platform.machine().lower() - except Exception: - return "unknown" - - if machine in ("arm64", "aarch64"): - return "arm64" - - # TODO: untested - if machine == "arm": - return "arm" - - if machine == "x86_64": - return "x64" - - # TODO: untested - if sys.maxsize <= 2**32: - return "x32" - - if machine: - return OtherArch(machine) - - return "unknown" - - -def _merge_mappings( - obj1: Mapping[_T_co, Union[_T, Omit]], - obj2: Mapping[_T_co, Union[_T, Omit]], -) -> Dict[_T_co, _T]: - """Merge two mappings of the same type, removing any values that are instances of `Omit`. - - In cases with duplicate keys the second mapping takes precedence. - """ - merged = {**obj1, **obj2} - return {key: value for key, value in merged.items() if not isinstance(value, Omit)} diff --git a/src/browser_use_sdk/_client.py b/src/browser_use_sdk/_client.py deleted file mode 100644 index 13ad53e..0000000 --- a/src/browser_use_sdk/_client.py +++ /dev/null @@ -1,439 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, Union, Mapping -from typing_extensions import Self, override - -import httpx - -from . import _exceptions -from ._qs import Querystring -from ._types import ( - NOT_GIVEN, - Omit, - Timeout, - NotGiven, - Transport, - ProxiesTypes, - RequestOptions, -) -from ._utils import is_given, get_async_library -from ._version import __version__ -from .resources import tasks, agent_profiles, browser_profiles -from ._streaming import Stream as Stream, AsyncStream as AsyncStream -from ._exceptions import APIStatusError, BrowserUseError -from ._base_client import ( - DEFAULT_MAX_RETRIES, - SyncAPIClient, - AsyncAPIClient, -) -from .resources.users import users -from .resources.sessions import sessions - -__all__ = [ - "Timeout", - "Transport", - "ProxiesTypes", - "RequestOptions", - "BrowserUse", - "AsyncBrowserUse", - "Client", - "AsyncClient", -] - - -class BrowserUse(SyncAPIClient): - users: users.UsersResource - tasks: tasks.TasksResource - sessions: sessions.SessionsResource - browser_profiles: browser_profiles.BrowserProfilesResource - agent_profiles: agent_profiles.AgentProfilesResource - with_raw_response: BrowserUseWithRawResponse - with_streaming_response: BrowserUseWithStreamedResponse - - # client options - api_key: str - - def __init__( - self, - *, - api_key: str | None = None, - base_url: str | httpx.URL | None = None, - timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, - max_retries: int = DEFAULT_MAX_RETRIES, - default_headers: Mapping[str, str] | None = None, - default_query: Mapping[str, object] | None = None, - # Configure a custom httpx client. - # We provide a `DefaultHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. - # See the [httpx documentation](https://www.python-httpx.org/api/#client) for more details. - http_client: httpx.Client | None = None, - # Enable or disable schema validation for data returned by the API. - # When enabled an error APIResponseValidationError is raised - # if the API responds with invalid data for the expected schema. - # - # This parameter may be removed or changed in the future. - # If you rely on this feature, please open a GitHub issue - # outlining your use-case to help us decide if it should be - # part of our public interface in the future. - _strict_response_validation: bool = False, - ) -> None: - """Construct a new synchronous BrowserUse client instance. - - This automatically infers the `api_key` argument from the `BROWSER_USE_API_KEY` environment variable if it is not provided. - """ - if api_key is None: - api_key = os.environ.get("BROWSER_USE_API_KEY") - if api_key is None: - raise BrowserUseError( - "The api_key client option must be set either by passing api_key to the client or by setting the BROWSER_USE_API_KEY environment variable" - ) - self.api_key = api_key - - if base_url is None: - base_url = os.environ.get("BROWSER_USE_BASE_URL") - if base_url is None: - base_url = f"https://api.browser-use.com/api/v2" - - super().__init__( - version=__version__, - base_url=base_url, - max_retries=max_retries, - timeout=timeout, - http_client=http_client, - custom_headers=default_headers, - custom_query=default_query, - _strict_response_validation=_strict_response_validation, - ) - - self.users = users.UsersResource(self) - self.tasks = tasks.TasksResource(self) - self.sessions = sessions.SessionsResource(self) - self.browser_profiles = browser_profiles.BrowserProfilesResource(self) - self.agent_profiles = agent_profiles.AgentProfilesResource(self) - self.with_raw_response = BrowserUseWithRawResponse(self) - self.with_streaming_response = BrowserUseWithStreamedResponse(self) - - @property - @override - def qs(self) -> Querystring: - return Querystring(array_format="comma") - - @property - @override - def auth_headers(self) -> dict[str, str]: - api_key = self.api_key - return {"X-Browser-Use-API-Key": api_key} - - @property - @override - def default_headers(self) -> dict[str, str | Omit]: - return { - **super().default_headers, - "X-Stainless-Async": "false", - **self._custom_headers, - } - - def copy( - self, - *, - api_key: str | None = None, - base_url: str | httpx.URL | None = None, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, - http_client: httpx.Client | None = None, - max_retries: int | NotGiven = NOT_GIVEN, - default_headers: Mapping[str, str] | None = None, - set_default_headers: Mapping[str, str] | None = None, - default_query: Mapping[str, object] | None = None, - set_default_query: Mapping[str, object] | None = None, - _extra_kwargs: Mapping[str, Any] = {}, - ) -> Self: - """ - Create a new client instance re-using the same options given to the current client with optional overriding. - """ - if default_headers is not None and set_default_headers is not None: - raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") - - if default_query is not None and set_default_query is not None: - raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") - - headers = self._custom_headers - if default_headers is not None: - headers = {**headers, **default_headers} - elif set_default_headers is not None: - headers = set_default_headers - - params = self._custom_query - if default_query is not None: - params = {**params, **default_query} - elif set_default_query is not None: - params = set_default_query - - http_client = http_client or self._client - return self.__class__( - api_key=api_key or self.api_key, - base_url=base_url or self.base_url, - timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, - http_client=http_client, - max_retries=max_retries if is_given(max_retries) else self.max_retries, - default_headers=headers, - default_query=params, - **_extra_kwargs, - ) - - # Alias for `copy` for nicer inline usage, e.g. - # client.with_options(timeout=10).foo.create(...) - with_options = copy - - @override - def _make_status_error( - self, - err_msg: str, - *, - body: object, - response: httpx.Response, - ) -> APIStatusError: - if response.status_code == 400: - return _exceptions.BadRequestError(err_msg, response=response, body=body) - - if response.status_code == 401: - return _exceptions.AuthenticationError(err_msg, response=response, body=body) - - if response.status_code == 403: - return _exceptions.PermissionDeniedError(err_msg, response=response, body=body) - - if response.status_code == 404: - return _exceptions.NotFoundError(err_msg, response=response, body=body) - - if response.status_code == 409: - return _exceptions.ConflictError(err_msg, response=response, body=body) - - if response.status_code == 422: - return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body) - - if response.status_code == 429: - return _exceptions.RateLimitError(err_msg, response=response, body=body) - - if response.status_code >= 500: - return _exceptions.InternalServerError(err_msg, response=response, body=body) - return APIStatusError(err_msg, response=response, body=body) - - -class AsyncBrowserUse(AsyncAPIClient): - users: users.AsyncUsersResource - tasks: tasks.AsyncTasksResource - sessions: sessions.AsyncSessionsResource - browser_profiles: browser_profiles.AsyncBrowserProfilesResource - agent_profiles: agent_profiles.AsyncAgentProfilesResource - with_raw_response: AsyncBrowserUseWithRawResponse - with_streaming_response: AsyncBrowserUseWithStreamedResponse - - # client options - api_key: str - - def __init__( - self, - *, - api_key: str | None = None, - base_url: str | httpx.URL | None = None, - timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, - max_retries: int = DEFAULT_MAX_RETRIES, - default_headers: Mapping[str, str] | None = None, - default_query: Mapping[str, object] | None = None, - # Configure a custom httpx client. - # We provide a `DefaultAsyncHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. - # See the [httpx documentation](https://www.python-httpx.org/api/#asyncclient) for more details. - http_client: httpx.AsyncClient | None = None, - # Enable or disable schema validation for data returned by the API. - # When enabled an error APIResponseValidationError is raised - # if the API responds with invalid data for the expected schema. - # - # This parameter may be removed or changed in the future. - # If you rely on this feature, please open a GitHub issue - # outlining your use-case to help us decide if it should be - # part of our public interface in the future. - _strict_response_validation: bool = False, - ) -> None: - """Construct a new async AsyncBrowserUse client instance. - - This automatically infers the `api_key` argument from the `BROWSER_USE_API_KEY` environment variable if it is not provided. - """ - if api_key is None: - api_key = os.environ.get("BROWSER_USE_API_KEY") - if api_key is None: - raise BrowserUseError( - "The api_key client option must be set either by passing api_key to the client or by setting the BROWSER_USE_API_KEY environment variable" - ) - self.api_key = api_key - - if base_url is None: - base_url = os.environ.get("BROWSER_USE_BASE_URL") - if base_url is None: - base_url = f"https://api.browser-use.com/api/v2" - - super().__init__( - version=__version__, - base_url=base_url, - max_retries=max_retries, - timeout=timeout, - http_client=http_client, - custom_headers=default_headers, - custom_query=default_query, - _strict_response_validation=_strict_response_validation, - ) - - self.users = users.AsyncUsersResource(self) - self.tasks = tasks.AsyncTasksResource(self) - self.sessions = sessions.AsyncSessionsResource(self) - self.browser_profiles = browser_profiles.AsyncBrowserProfilesResource(self) - self.agent_profiles = agent_profiles.AsyncAgentProfilesResource(self) - self.with_raw_response = AsyncBrowserUseWithRawResponse(self) - self.with_streaming_response = AsyncBrowserUseWithStreamedResponse(self) - - @property - @override - def qs(self) -> Querystring: - return Querystring(array_format="comma") - - @property - @override - def auth_headers(self) -> dict[str, str]: - api_key = self.api_key - return {"X-Browser-Use-API-Key": api_key} - - @property - @override - def default_headers(self) -> dict[str, str | Omit]: - return { - **super().default_headers, - "X-Stainless-Async": f"async:{get_async_library()}", - **self._custom_headers, - } - - def copy( - self, - *, - api_key: str | None = None, - base_url: str | httpx.URL | None = None, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, - http_client: httpx.AsyncClient | None = None, - max_retries: int | NotGiven = NOT_GIVEN, - default_headers: Mapping[str, str] | None = None, - set_default_headers: Mapping[str, str] | None = None, - default_query: Mapping[str, object] | None = None, - set_default_query: Mapping[str, object] | None = None, - _extra_kwargs: Mapping[str, Any] = {}, - ) -> Self: - """ - Create a new client instance re-using the same options given to the current client with optional overriding. - """ - if default_headers is not None and set_default_headers is not None: - raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") - - if default_query is not None and set_default_query is not None: - raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") - - headers = self._custom_headers - if default_headers is not None: - headers = {**headers, **default_headers} - elif set_default_headers is not None: - headers = set_default_headers - - params = self._custom_query - if default_query is not None: - params = {**params, **default_query} - elif set_default_query is not None: - params = set_default_query - - http_client = http_client or self._client - return self.__class__( - api_key=api_key or self.api_key, - base_url=base_url or self.base_url, - timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, - http_client=http_client, - max_retries=max_retries if is_given(max_retries) else self.max_retries, - default_headers=headers, - default_query=params, - **_extra_kwargs, - ) - - # Alias for `copy` for nicer inline usage, e.g. - # client.with_options(timeout=10).foo.create(...) - with_options = copy - - @override - def _make_status_error( - self, - err_msg: str, - *, - body: object, - response: httpx.Response, - ) -> APIStatusError: - if response.status_code == 400: - return _exceptions.BadRequestError(err_msg, response=response, body=body) - - if response.status_code == 401: - return _exceptions.AuthenticationError(err_msg, response=response, body=body) - - if response.status_code == 403: - return _exceptions.PermissionDeniedError(err_msg, response=response, body=body) - - if response.status_code == 404: - return _exceptions.NotFoundError(err_msg, response=response, body=body) - - if response.status_code == 409: - return _exceptions.ConflictError(err_msg, response=response, body=body) - - if response.status_code == 422: - return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body) - - if response.status_code == 429: - return _exceptions.RateLimitError(err_msg, response=response, body=body) - - if response.status_code >= 500: - return _exceptions.InternalServerError(err_msg, response=response, body=body) - return APIStatusError(err_msg, response=response, body=body) - - -class BrowserUseWithRawResponse: - def __init__(self, client: BrowserUse) -> None: - self.users = users.UsersResourceWithRawResponse(client.users) - self.tasks = tasks.TasksResourceWithRawResponse(client.tasks) - self.sessions = sessions.SessionsResourceWithRawResponse(client.sessions) - self.browser_profiles = browser_profiles.BrowserProfilesResourceWithRawResponse(client.browser_profiles) - self.agent_profiles = agent_profiles.AgentProfilesResourceWithRawResponse(client.agent_profiles) - - -class AsyncBrowserUseWithRawResponse: - def __init__(self, client: AsyncBrowserUse) -> None: - self.users = users.AsyncUsersResourceWithRawResponse(client.users) - self.tasks = tasks.AsyncTasksResourceWithRawResponse(client.tasks) - self.sessions = sessions.AsyncSessionsResourceWithRawResponse(client.sessions) - self.browser_profiles = browser_profiles.AsyncBrowserProfilesResourceWithRawResponse(client.browser_profiles) - self.agent_profiles = agent_profiles.AsyncAgentProfilesResourceWithRawResponse(client.agent_profiles) - - -class BrowserUseWithStreamedResponse: - def __init__(self, client: BrowserUse) -> None: - self.users = users.UsersResourceWithStreamingResponse(client.users) - self.tasks = tasks.TasksResourceWithStreamingResponse(client.tasks) - self.sessions = sessions.SessionsResourceWithStreamingResponse(client.sessions) - self.browser_profiles = browser_profiles.BrowserProfilesResourceWithStreamingResponse(client.browser_profiles) - self.agent_profiles = agent_profiles.AgentProfilesResourceWithStreamingResponse(client.agent_profiles) - - -class AsyncBrowserUseWithStreamedResponse: - def __init__(self, client: AsyncBrowserUse) -> None: - self.users = users.AsyncUsersResourceWithStreamingResponse(client.users) - self.tasks = tasks.AsyncTasksResourceWithStreamingResponse(client.tasks) - self.sessions = sessions.AsyncSessionsResourceWithStreamingResponse(client.sessions) - self.browser_profiles = browser_profiles.AsyncBrowserProfilesResourceWithStreamingResponse( - client.browser_profiles - ) - self.agent_profiles = agent_profiles.AsyncAgentProfilesResourceWithStreamingResponse(client.agent_profiles) - - -Client = BrowserUse - -AsyncClient = AsyncBrowserUse diff --git a/src/browser_use_sdk/_compat.py b/src/browser_use_sdk/_compat.py deleted file mode 100644 index 92d9ee6..0000000 --- a/src/browser_use_sdk/_compat.py +++ /dev/null @@ -1,219 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload -from datetime import date, datetime -from typing_extensions import Self, Literal - -import pydantic -from pydantic.fields import FieldInfo - -from ._types import IncEx, StrBytesIntFloat - -_T = TypeVar("_T") -_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) - -# --------------- Pydantic v2 compatibility --------------- - -# Pyright incorrectly reports some of our functions as overriding a method when they don't -# pyright: reportIncompatibleMethodOverride=false - -PYDANTIC_V2 = pydantic.VERSION.startswith("2.") - -# v1 re-exports -if TYPE_CHECKING: - - def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001 - ... - - def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: # noqa: ARG001 - ... - - def get_args(t: type[Any]) -> tuple[Any, ...]: # noqa: ARG001 - ... - - def is_union(tp: type[Any] | None) -> bool: # noqa: ARG001 - ... - - def get_origin(t: type[Any]) -> type[Any] | None: # noqa: ARG001 - ... - - def is_literal_type(type_: type[Any]) -> bool: # noqa: ARG001 - ... - - def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001 - ... - -else: - if PYDANTIC_V2: - from pydantic.v1.typing import ( - get_args as get_args, - is_union as is_union, - get_origin as get_origin, - is_typeddict as is_typeddict, - is_literal_type as is_literal_type, - ) - from pydantic.v1.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime - else: - from pydantic.typing import ( - get_args as get_args, - is_union as is_union, - get_origin as get_origin, - is_typeddict as is_typeddict, - is_literal_type as is_literal_type, - ) - from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime - - -# refactored config -if TYPE_CHECKING: - from pydantic import ConfigDict as ConfigDict -else: - if PYDANTIC_V2: - from pydantic import ConfigDict - else: - # TODO: provide an error message here? - ConfigDict = None - - -# renamed methods / properties -def parse_obj(model: type[_ModelT], value: object) -> _ModelT: - if PYDANTIC_V2: - return model.model_validate(value) - else: - return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - - -def field_is_required(field: FieldInfo) -> bool: - if PYDANTIC_V2: - return field.is_required() - return field.required # type: ignore - - -def field_get_default(field: FieldInfo) -> Any: - value = field.get_default() - if PYDANTIC_V2: - from pydantic_core import PydanticUndefined - - if value == PydanticUndefined: - return None - return value - return value - - -def field_outer_type(field: FieldInfo) -> Any: - if PYDANTIC_V2: - return field.annotation - return field.outer_type_ # type: ignore - - -def get_model_config(model: type[pydantic.BaseModel]) -> Any: - if PYDANTIC_V2: - return model.model_config - return model.__config__ # type: ignore - - -def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: - if PYDANTIC_V2: - return model.model_fields - return model.__fields__ # type: ignore - - -def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT: - if PYDANTIC_V2: - return model.model_copy(deep=deep) - return model.copy(deep=deep) # type: ignore - - -def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: - if PYDANTIC_V2: - return model.model_dump_json(indent=indent) - return model.json(indent=indent) # type: ignore - - -def model_dump( - model: pydantic.BaseModel, - *, - exclude: IncEx | None = None, - exclude_unset: bool = False, - exclude_defaults: bool = False, - warnings: bool = True, - mode: Literal["json", "python"] = "python", -) -> dict[str, Any]: - if PYDANTIC_V2 or hasattr(model, "model_dump"): - return model.model_dump( - mode=mode, - exclude=exclude, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - # warnings are not supported in Pydantic v1 - warnings=warnings if PYDANTIC_V2 else True, - ) - return cast( - "dict[str, Any]", - model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - exclude=exclude, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - ), - ) - - -def model_parse(model: type[_ModelT], data: Any) -> _ModelT: - if PYDANTIC_V2: - return model.model_validate(data) - return model.parse_obj(data) # pyright: ignore[reportDeprecated] - - -# generic models -if TYPE_CHECKING: - - class GenericModel(pydantic.BaseModel): ... - -else: - if PYDANTIC_V2: - # there no longer needs to be a distinction in v2 but - # we still have to create our own subclass to avoid - # inconsistent MRO ordering errors - class GenericModel(pydantic.BaseModel): ... - - else: - import pydantic.generics - - class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... - - -# cached properties -if TYPE_CHECKING: - cached_property = property - - # we define a separate type (copied from typeshed) - # that represents that `cached_property` is `set`able - # at runtime, which differs from `@property`. - # - # this is a separate type as editors likely special case - # `@property` and we don't want to cause issues just to have - # more helpful internal types. - - class typed_cached_property(Generic[_T]): - func: Callable[[Any], _T] - attrname: str | None - - def __init__(self, func: Callable[[Any], _T]) -> None: ... - - @overload - def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... - - @overload - def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: ... - - def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self: - raise NotImplementedError() - - def __set_name__(self, owner: type[Any], name: str) -> None: ... - - # __set__ is not defined at runtime, but @cached_property is designed to be settable - def __set__(self, instance: object, value: _T) -> None: ... -else: - from functools import cached_property as cached_property - - typed_cached_property = cached_property diff --git a/src/browser_use_sdk/_constants.py b/src/browser_use_sdk/_constants.py deleted file mode 100644 index 6ddf2c7..0000000 --- a/src/browser_use_sdk/_constants.py +++ /dev/null @@ -1,14 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import httpx - -RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response" -OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" - -# default timeout is 1 minute -DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0) -DEFAULT_MAX_RETRIES = 2 -DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) - -INITIAL_RETRY_DELAY = 0.5 -MAX_RETRY_DELAY = 8.0 diff --git a/src/browser_use_sdk/_exceptions.py b/src/browser_use_sdk/_exceptions.py deleted file mode 100644 index 125f8b3..0000000 --- a/src/browser_use_sdk/_exceptions.py +++ /dev/null @@ -1,108 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal - -import httpx - -__all__ = [ - "BadRequestError", - "AuthenticationError", - "PermissionDeniedError", - "NotFoundError", - "ConflictError", - "UnprocessableEntityError", - "RateLimitError", - "InternalServerError", -] - - -class BrowserUseError(Exception): - pass - - -class APIError(BrowserUseError): - message: str - request: httpx.Request - - body: object | None - """The API response body. - - If the API responded with a valid JSON structure then this property will be the - decoded result. - - If it isn't a valid JSON structure then this will be the raw response. - - If there was no response associated with this error then it will be `None`. - """ - - def __init__(self, message: str, request: httpx.Request, *, body: object | None) -> None: # noqa: ARG002 - super().__init__(message) - self.request = request - self.message = message - self.body = body - - -class APIResponseValidationError(APIError): - response: httpx.Response - status_code: int - - def __init__(self, response: httpx.Response, body: object | None, *, message: str | None = None) -> None: - super().__init__(message or "Data returned by API invalid for expected schema.", response.request, body=body) - self.response = response - self.status_code = response.status_code - - -class APIStatusError(APIError): - """Raised when an API response has a status code of 4xx or 5xx.""" - - response: httpx.Response - status_code: int - - def __init__(self, message: str, *, response: httpx.Response, body: object | None) -> None: - super().__init__(message, response.request, body=body) - self.response = response - self.status_code = response.status_code - - -class APIConnectionError(APIError): - def __init__(self, *, message: str = "Connection error.", request: httpx.Request) -> None: - super().__init__(message, request, body=None) - - -class APITimeoutError(APIConnectionError): - def __init__(self, request: httpx.Request) -> None: - super().__init__(message="Request timed out.", request=request) - - -class BadRequestError(APIStatusError): - status_code: Literal[400] = 400 # pyright: ignore[reportIncompatibleVariableOverride] - - -class AuthenticationError(APIStatusError): - status_code: Literal[401] = 401 # pyright: ignore[reportIncompatibleVariableOverride] - - -class PermissionDeniedError(APIStatusError): - status_code: Literal[403] = 403 # pyright: ignore[reportIncompatibleVariableOverride] - - -class NotFoundError(APIStatusError): - status_code: Literal[404] = 404 # pyright: ignore[reportIncompatibleVariableOverride] - - -class ConflictError(APIStatusError): - status_code: Literal[409] = 409 # pyright: ignore[reportIncompatibleVariableOverride] - - -class UnprocessableEntityError(APIStatusError): - status_code: Literal[422] = 422 # pyright: ignore[reportIncompatibleVariableOverride] - - -class RateLimitError(APIStatusError): - status_code: Literal[429] = 429 # pyright: ignore[reportIncompatibleVariableOverride] - - -class InternalServerError(APIStatusError): - pass diff --git a/src/browser_use_sdk/_files.py b/src/browser_use_sdk/_files.py deleted file mode 100644 index cc14c14..0000000 --- a/src/browser_use_sdk/_files.py +++ /dev/null @@ -1,123 +0,0 @@ -from __future__ import annotations - -import io -import os -import pathlib -from typing import overload -from typing_extensions import TypeGuard - -import anyio - -from ._types import ( - FileTypes, - FileContent, - RequestFiles, - HttpxFileTypes, - Base64FileInput, - HttpxFileContent, - HttpxRequestFiles, -) -from ._utils import is_tuple_t, is_mapping_t, is_sequence_t - - -def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: - return isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) - - -def is_file_content(obj: object) -> TypeGuard[FileContent]: - return ( - isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) - ) - - -def assert_is_file_content(obj: object, *, key: str | None = None) -> None: - if not is_file_content(obj): - prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" - raise RuntimeError( - f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." - ) from None - - -@overload -def to_httpx_files(files: None) -> None: ... - - -@overload -def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... - - -def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: - if files is None: - return None - - if is_mapping_t(files): - files = {key: _transform_file(file) for key, file in files.items()} - elif is_sequence_t(files): - files = [(key, _transform_file(file)) for key, file in files] - else: - raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") - - return files - - -def _transform_file(file: FileTypes) -> HttpxFileTypes: - if is_file_content(file): - if isinstance(file, os.PathLike): - path = pathlib.Path(file) - return (path.name, path.read_bytes()) - - return file - - if is_tuple_t(file): - return (file[0], read_file_content(file[1]), *file[2:]) - - raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") - - -def read_file_content(file: FileContent) -> HttpxFileContent: - if isinstance(file, os.PathLike): - return pathlib.Path(file).read_bytes() - return file - - -@overload -async def async_to_httpx_files(files: None) -> None: ... - - -@overload -async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... - - -async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: - if files is None: - return None - - if is_mapping_t(files): - files = {key: await _async_transform_file(file) for key, file in files.items()} - elif is_sequence_t(files): - files = [(key, await _async_transform_file(file)) for key, file in files] - else: - raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") - - return files - - -async def _async_transform_file(file: FileTypes) -> HttpxFileTypes: - if is_file_content(file): - if isinstance(file, os.PathLike): - path = anyio.Path(file) - return (path.name, await path.read_bytes()) - - return file - - if is_tuple_t(file): - return (file[0], await async_read_file_content(file[1]), *file[2:]) - - raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") - - -async def async_read_file_content(file: FileContent) -> HttpxFileContent: - if isinstance(file, os.PathLike): - return await anyio.Path(file).read_bytes() - - return file diff --git a/src/browser_use_sdk/_models.py b/src/browser_use_sdk/_models.py deleted file mode 100644 index b8387ce..0000000 --- a/src/browser_use_sdk/_models.py +++ /dev/null @@ -1,829 +0,0 @@ -from __future__ import annotations - -import os -import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast -from datetime import date, datetime -from typing_extensions import ( - List, - Unpack, - Literal, - ClassVar, - Protocol, - Required, - ParamSpec, - TypedDict, - TypeGuard, - final, - override, - runtime_checkable, -) - -import pydantic -from pydantic.fields import FieldInfo - -from ._types import ( - Body, - IncEx, - Query, - ModelT, - Headers, - Timeout, - NotGiven, - AnyMapping, - HttpxRequestFiles, -) -from ._utils import ( - PropertyInfo, - is_list, - is_given, - json_safe, - lru_cache, - is_mapping, - parse_date, - coerce_boolean, - parse_datetime, - strip_not_given, - extract_type_arg, - is_annotated_type, - is_type_alias_type, - strip_annotated_type, -) -from ._compat import ( - PYDANTIC_V2, - ConfigDict, - GenericModel as BaseGenericModel, - get_args, - is_union, - parse_obj, - get_origin, - is_literal_type, - get_model_config, - get_model_fields, - field_get_default, -) -from ._constants import RAW_RESPONSE_HEADER - -if TYPE_CHECKING: - from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema - -__all__ = ["BaseModel", "GenericModel"] - -_T = TypeVar("_T") -_BaseModelT = TypeVar("_BaseModelT", bound="BaseModel") - -P = ParamSpec("P") - - -@runtime_checkable -class _ConfigProtocol(Protocol): - allow_population_by_field_name: bool - - -class BaseModel(pydantic.BaseModel): - if PYDANTIC_V2: - model_config: ClassVar[ConfigDict] = ConfigDict( - extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) - ) - else: - - @property - @override - def model_fields_set(self) -> set[str]: - # a forwards-compat shim for pydantic v2 - return self.__fields_set__ # type: ignore - - class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] - extra: Any = pydantic.Extra.allow # type: ignore - - def to_dict( - self, - *, - mode: Literal["json", "python"] = "python", - use_api_names: bool = True, - exclude_unset: bool = True, - exclude_defaults: bool = False, - exclude_none: bool = False, - warnings: bool = True, - ) -> dict[str, object]: - """Recursively generate a dictionary representation of the model, optionally specifying which fields to include or exclude. - - By default, fields that were not set by the API will not be included, - and keys will match the API response, *not* the property names from the model. - - For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, - the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). - - Args: - mode: - If mode is 'json', the dictionary will only contain JSON serializable types. e.g. `datetime` will be turned into a string, `"2024-3-22T18:11:19.117000Z"`. - If mode is 'python', the dictionary may contain any Python objects. e.g. `datetime(2024, 3, 22)` - - use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. - exclude_unset: Whether to exclude fields that have not been explicitly set. - exclude_defaults: Whether to exclude fields that are set to their default value from the output. - exclude_none: Whether to exclude fields that have a value of `None` from the output. - warnings: Whether to log warnings when invalid fields are encountered. This is only supported in Pydantic v2. - """ - return self.model_dump( - mode=mode, - by_alias=use_api_names, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - warnings=warnings, - ) - - def to_json( - self, - *, - indent: int | None = 2, - use_api_names: bool = True, - exclude_unset: bool = True, - exclude_defaults: bool = False, - exclude_none: bool = False, - warnings: bool = True, - ) -> str: - """Generates a JSON string representing this model as it would be received from or sent to the API (but with indentation). - - By default, fields that were not set by the API will not be included, - and keys will match the API response, *not* the property names from the model. - - For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, - the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). - - Args: - indent: Indentation to use in the JSON output. If `None` is passed, the output will be compact. Defaults to `2` - use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. - exclude_unset: Whether to exclude fields that have not been explicitly set. - exclude_defaults: Whether to exclude fields that have the default value. - exclude_none: Whether to exclude fields that have a value of `None`. - warnings: Whether to show any warnings that occurred during serialization. This is only supported in Pydantic v2. - """ - return self.model_dump_json( - indent=indent, - by_alias=use_api_names, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - warnings=warnings, - ) - - @override - def __str__(self) -> str: - # mypy complains about an invalid self arg - return f"{self.__repr_name__()}({self.__repr_str__(', ')})" # type: ignore[misc] - - # Override the 'construct' method in a way that supports recursive parsing without validation. - # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836. - @classmethod - @override - def construct( # pyright: ignore[reportIncompatibleMethodOverride] - __cls: Type[ModelT], - _fields_set: set[str] | None = None, - **values: object, - ) -> ModelT: - m = __cls.__new__(__cls) - fields_values: dict[str, object] = {} - - config = get_model_config(__cls) - populate_by_name = ( - config.allow_population_by_field_name - if isinstance(config, _ConfigProtocol) - else config.get("populate_by_name") - ) - - if _fields_set is None: - _fields_set = set() - - model_fields = get_model_fields(__cls) - for name, field in model_fields.items(): - key = field.alias - if key is None or (key not in values and populate_by_name): - key = name - - if key in values: - fields_values[name] = _construct_field(value=values[key], field=field, key=key) - _fields_set.add(name) - else: - fields_values[name] = field_get_default(field) - - extra_field_type = _get_extra_fields_type(__cls) - - _extra = {} - for key, value in values.items(): - if key not in model_fields: - parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value - - if PYDANTIC_V2: - _extra[key] = parsed - else: - _fields_set.add(key) - fields_values[key] = parsed - - object.__setattr__(m, "__dict__", fields_values) - - if PYDANTIC_V2: - # these properties are copied from Pydantic's `model_construct()` method - object.__setattr__(m, "__pydantic_private__", None) - object.__setattr__(m, "__pydantic_extra__", _extra) - object.__setattr__(m, "__pydantic_fields_set__", _fields_set) - else: - # init_private_attributes() does not exist in v2 - m._init_private_attributes() # type: ignore - - # copied from Pydantic v1's `construct()` method - object.__setattr__(m, "__fields_set__", _fields_set) - - return m - - if not TYPE_CHECKING: - # type checkers incorrectly complain about this assignment - # because the type signatures are technically different - # although not in practice - model_construct = construct - - if not PYDANTIC_V2: - # we define aliases for some of the new pydantic v2 methods so - # that we can just document these methods without having to specify - # a specific pydantic version as some users may not know which - # pydantic version they are currently using - - @override - def model_dump( - self, - *, - mode: Literal["json", "python"] | str = "python", - include: IncEx | None = None, - exclude: IncEx | None = None, - by_alias: bool = False, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - round_trip: bool = False, - warnings: bool | Literal["none", "warn", "error"] = True, - context: dict[str, Any] | None = None, - serialize_as_any: bool = False, - ) -> dict[str, Any]: - """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump - - Generate a dictionary representation of the model, optionally specifying which fields to include or exclude. - - Args: - mode: The mode in which `to_python` should run. - If mode is 'json', the dictionary will only contain JSON serializable types. - If mode is 'python', the dictionary may contain any Python objects. - include: A list of fields to include in the output. - exclude: A list of fields to exclude from the output. - by_alias: Whether to use the field's alias in the dictionary key if defined. - exclude_unset: Whether to exclude fields that are unset or None from the output. - exclude_defaults: Whether to exclude fields that are set to their default value from the output. - exclude_none: Whether to exclude fields that have a value of `None` from the output. - round_trip: Whether to enable serialization and deserialization round-trip support. - warnings: Whether to log warnings when invalid fields are encountered. - - Returns: - A dictionary representation of the model. - """ - if mode not in {"json", "python"}: - raise ValueError("mode must be either 'json' or 'python'") - if round_trip != False: - raise ValueError("round_trip is only supported in Pydantic v2") - if warnings != True: - raise ValueError("warnings is only supported in Pydantic v2") - if context is not None: - raise ValueError("context is only supported in Pydantic v2") - if serialize_as_any != False: - raise ValueError("serialize_as_any is only supported in Pydantic v2") - dumped = super().dict( # pyright: ignore[reportDeprecated] - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - - return cast(dict[str, Any], json_safe(dumped)) if mode == "json" else dumped - - @override - def model_dump_json( - self, - *, - indent: int | None = None, - include: IncEx | None = None, - exclude: IncEx | None = None, - by_alias: bool = False, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - round_trip: bool = False, - warnings: bool | Literal["none", "warn", "error"] = True, - context: dict[str, Any] | None = None, - serialize_as_any: bool = False, - ) -> str: - """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json - - Generates a JSON representation of the model using Pydantic's `to_json` method. - - Args: - indent: Indentation to use in the JSON output. If None is passed, the output will be compact. - include: Field(s) to include in the JSON output. Can take either a string or set of strings. - exclude: Field(s) to exclude from the JSON output. Can take either a string or set of strings. - by_alias: Whether to serialize using field aliases. - exclude_unset: Whether to exclude fields that have not been explicitly set. - exclude_defaults: Whether to exclude fields that have the default value. - exclude_none: Whether to exclude fields that have a value of `None`. - round_trip: Whether to use serialization/deserialization between JSON and class instance. - warnings: Whether to show any warnings that occurred during serialization. - - Returns: - A JSON string representation of the model. - """ - if round_trip != False: - raise ValueError("round_trip is only supported in Pydantic v2") - if warnings != True: - raise ValueError("warnings is only supported in Pydantic v2") - if context is not None: - raise ValueError("context is only supported in Pydantic v2") - if serialize_as_any != False: - raise ValueError("serialize_as_any is only supported in Pydantic v2") - return super().json( # type: ignore[reportDeprecated] - indent=indent, - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - - -def _construct_field(value: object, field: FieldInfo, key: str) -> object: - if value is None: - return field_get_default(field) - - if PYDANTIC_V2: - type_ = field.annotation - else: - type_ = cast(type, field.outer_type_) # type: ignore - - if type_ is None: - raise RuntimeError(f"Unexpected field type is None for {key}") - - return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) - - -def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None: - if not PYDANTIC_V2: - # TODO - return None - - schema = cls.__pydantic_core_schema__ - if schema["type"] == "model": - fields = schema["schema"] - if fields["type"] == "model-fields": - extras = fields.get("extras_schema") - if extras and "cls" in extras: - # mypy can't narrow the type - return extras["cls"] # type: ignore[no-any-return] - - return None - - -def is_basemodel(type_: type) -> bool: - """Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`""" - if is_union(type_): - for variant in get_args(type_): - if is_basemodel(variant): - return True - - return False - - return is_basemodel_type(type_) - - -def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericModel]]: - origin = get_origin(type_) or type_ - if not inspect.isclass(origin): - return False - return issubclass(origin, BaseModel) or issubclass(origin, GenericModel) - - -def build( - base_model_cls: Callable[P, _BaseModelT], - *args: P.args, - **kwargs: P.kwargs, -) -> _BaseModelT: - """Construct a BaseModel class without validation. - - This is useful for cases where you need to instantiate a `BaseModel` - from an API response as this provides type-safe params which isn't supported - by helpers like `construct_type()`. - - ```py - build(MyModel, my_field_a="foo", my_field_b=123) - ``` - """ - if args: - raise TypeError( - "Received positional arguments which are not supported; Keyword arguments must be used instead", - ) - - return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs)) - - -def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: - """Loose coercion to the expected type with construction of nested values. - - Note: the returned value from this function is not guaranteed to match the - given type. - """ - return cast(_T, construct_type(value=value, type_=type_)) - - -def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object: - """Loose coercion to the expected type with construction of nested values. - - If the given value does not match the expected type then it is returned as-is. - """ - - # store a reference to the original type we were given before we extract any inner - # types so that we can properly resolve forward references in `TypeAliasType` annotations - original_type = None - - # we allow `object` as the input type because otherwise, passing things like - # `Literal['value']` will be reported as a type error by type checkers - type_ = cast("type[object]", type_) - if is_type_alias_type(type_): - original_type = type_ # type: ignore[unreachable] - type_ = type_.__value__ # type: ignore[unreachable] - - # unwrap `Annotated[T, ...]` -> `T` - if metadata is not None and len(metadata) > 0: - meta: tuple[Any, ...] = tuple(metadata) - elif is_annotated_type(type_): - meta = get_args(type_)[1:] - type_ = extract_type_arg(type_, 0) - else: - meta = tuple() - - # we need to use the origin class for any types that are subscripted generics - # e.g. Dict[str, object] - origin = get_origin(type_) or type_ - args = get_args(type_) - - if is_union(origin): - try: - return validate_type(type_=cast("type[object]", original_type or type_), value=value) - except Exception: - pass - - # if the type is a discriminated union then we want to construct the right variant - # in the union, even if the data doesn't match exactly, otherwise we'd break code - # that relies on the constructed class types, e.g. - # - # class FooType: - # kind: Literal['foo'] - # value: str - # - # class BarType: - # kind: Literal['bar'] - # value: int - # - # without this block, if the data we get is something like `{'kind': 'bar', 'value': 'foo'}` then - # we'd end up constructing `FooType` when it should be `BarType`. - discriminator = _build_discriminated_union_meta(union=type_, meta_annotations=meta) - if discriminator and is_mapping(value): - variant_value = value.get(discriminator.field_alias_from or discriminator.field_name) - if variant_value and isinstance(variant_value, str): - variant_type = discriminator.mapping.get(variant_value) - if variant_type: - return construct_type(type_=variant_type, value=value) - - # if the data is not valid, use the first variant that doesn't fail while deserializing - for variant in args: - try: - return construct_type(value=value, type_=variant) - except Exception: - continue - - raise RuntimeError(f"Could not convert data into a valid instance of {type_}") - - if origin == dict: - if not is_mapping(value): - return value - - _, items_type = get_args(type_) # Dict[_, items_type] - return {key: construct_type(value=item, type_=items_type) for key, item in value.items()} - - if ( - not is_literal_type(type_) - and inspect.isclass(origin) - and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel)) - ): - if is_list(value): - return [cast(Any, type_).construct(**entry) if is_mapping(entry) else entry for entry in value] - - if is_mapping(value): - if issubclass(type_, BaseModel): - return type_.construct(**value) # type: ignore[arg-type] - - return cast(Any, type_).construct(**value) - - if origin == list: - if not is_list(value): - return value - - inner_type = args[0] # List[inner_type] - return [construct_type(value=entry, type_=inner_type) for entry in value] - - if origin == float: - if isinstance(value, int): - coerced = float(value) - if coerced != value: - return value - return coerced - - return value - - if type_ == datetime: - try: - return parse_datetime(value) # type: ignore - except Exception: - return value - - if type_ == date: - try: - return parse_date(value) # type: ignore - except Exception: - return value - - return value - - -@runtime_checkable -class CachedDiscriminatorType(Protocol): - __discriminator__: DiscriminatorDetails - - -class DiscriminatorDetails: - field_name: str - """The name of the discriminator field in the variant class, e.g. - - ```py - class Foo(BaseModel): - type: Literal['foo'] - ``` - - Will result in field_name='type' - """ - - field_alias_from: str | None - """The name of the discriminator field in the API response, e.g. - - ```py - class Foo(BaseModel): - type: Literal['foo'] = Field(alias='type_from_api') - ``` - - Will result in field_alias_from='type_from_api' - """ - - mapping: dict[str, type] - """Mapping of discriminator value to variant type, e.g. - - {'foo': FooVariant, 'bar': BarVariant} - """ - - def __init__( - self, - *, - mapping: dict[str, type], - discriminator_field: str, - discriminator_alias: str | None, - ) -> None: - self.mapping = mapping - self.field_name = discriminator_field - self.field_alias_from = discriminator_alias - - -def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None: - if isinstance(union, CachedDiscriminatorType): - return union.__discriminator__ - - discriminator_field_name: str | None = None - - for annotation in meta_annotations: - if isinstance(annotation, PropertyInfo) and annotation.discriminator is not None: - discriminator_field_name = annotation.discriminator - break - - if not discriminator_field_name: - return None - - mapping: dict[str, type] = {} - discriminator_alias: str | None = None - - for variant in get_args(union): - variant = strip_annotated_type(variant) - if is_basemodel_type(variant): - if PYDANTIC_V2: - field = _extract_field_schema_pv2(variant, discriminator_field_name) - if not field: - continue - - # Note: if one variant defines an alias then they all should - discriminator_alias = field.get("serialization_alias") - - field_schema = field["schema"] - - if field_schema["type"] == "literal": - for entry in cast("LiteralSchema", field_schema)["expected"]: - if isinstance(entry, str): - mapping[entry] = variant - else: - field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - if not field_info: - continue - - # Note: if one variant defines an alias then they all should - discriminator_alias = field_info.alias - - if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): - for entry in get_args(annotation): - if isinstance(entry, str): - mapping[entry] = variant - - if not mapping: - return None - - details = DiscriminatorDetails( - mapping=mapping, - discriminator_field=discriminator_field_name, - discriminator_alias=discriminator_alias, - ) - cast(CachedDiscriminatorType, union).__discriminator__ = details - return details - - -def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None: - schema = model.__pydantic_core_schema__ - if schema["type"] == "definitions": - schema = schema["schema"] - - if schema["type"] != "model": - return None - - schema = cast("ModelSchema", schema) - fields_schema = schema["schema"] - if fields_schema["type"] != "model-fields": - return None - - fields_schema = cast("ModelFieldsSchema", fields_schema) - field = fields_schema["fields"].get(field_name) - if not field: - return None - - return cast("ModelField", field) # pyright: ignore[reportUnnecessaryCast] - - -def validate_type(*, type_: type[_T], value: object) -> _T: - """Strict validation that the given value matches the expected type""" - if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel): - return cast(_T, parse_obj(type_, value)) - - return cast(_T, _validate_non_model_type(type_=type_, value=value)) - - -def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None: - """Add a pydantic config for the given type. - - Note: this is a no-op on Pydantic v1. - """ - setattr(typ, "__pydantic_config__", config) # noqa: B010 - - -# our use of subclassing here causes weirdness for type checkers, -# so we just pretend that we don't subclass -if TYPE_CHECKING: - GenericModel = BaseModel -else: - - class GenericModel(BaseGenericModel, BaseModel): - pass - - -if PYDANTIC_V2: - from pydantic import TypeAdapter as _TypeAdapter - - _CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter)) - - if TYPE_CHECKING: - from pydantic import TypeAdapter - else: - TypeAdapter = _CachedTypeAdapter - - def _validate_non_model_type(*, type_: type[_T], value: object) -> _T: - return TypeAdapter(type_).validate_python(value) - -elif not TYPE_CHECKING: # TODO: condition is weird - - class RootModel(GenericModel, Generic[_T]): - """Used as a placeholder to easily convert runtime types to a Pydantic format - to provide validation. - - For example: - ```py - validated = RootModel[int](__root__="5").__root__ - # validated: 5 - ``` - """ - - __root__: _T - - def _validate_non_model_type(*, type_: type[_T], value: object) -> _T: - model = _create_pydantic_model(type_).validate(value) - return cast(_T, model.__root__) - - def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]: - return RootModel[type_] # type: ignore - - -class FinalRequestOptionsInput(TypedDict, total=False): - method: Required[str] - url: Required[str] - params: Query - headers: Headers - max_retries: int - timeout: float | Timeout | None - files: HttpxRequestFiles | None - idempotency_key: str - json_data: Body - extra_json: AnyMapping - follow_redirects: bool - - -@final -class FinalRequestOptions(pydantic.BaseModel): - method: str - url: str - params: Query = {} - headers: Union[Headers, NotGiven] = NotGiven() - max_retries: Union[int, NotGiven] = NotGiven() - timeout: Union[float, Timeout, None, NotGiven] = NotGiven() - files: Union[HttpxRequestFiles, None] = None - idempotency_key: Union[str, None] = None - post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() - follow_redirects: Union[bool, None] = None - - # It should be noted that we cannot use `json` here as that would override - # a BaseModel method in an incompatible fashion. - json_data: Union[Body, None] = None - extra_json: Union[AnyMapping, None] = None - - if PYDANTIC_V2: - model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) - else: - - class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] - arbitrary_types_allowed: bool = True - - def get_max_retries(self, max_retries: int) -> int: - if isinstance(self.max_retries, NotGiven): - return max_retries - return self.max_retries - - def _strip_raw_response_header(self) -> None: - if not is_given(self.headers): - return - - if self.headers.get(RAW_RESPONSE_HEADER): - self.headers = {**self.headers} - self.headers.pop(RAW_RESPONSE_HEADER) - - # override the `construct` method so that we can run custom transformations. - # this is necessary as we don't want to do any actual runtime type checking - # (which means we can't use validators) but we do want to ensure that `NotGiven` - # values are not present - # - # type ignore required because we're adding explicit types to `**values` - @classmethod - def construct( # type: ignore - cls, - _fields_set: set[str] | None = None, - **values: Unpack[FinalRequestOptionsInput], - ) -> FinalRequestOptions: - kwargs: dict[str, Any] = { - # we unconditionally call `strip_not_given` on any value - # as it will just ignore any non-mapping types - key: strip_not_given(value) - for key, value in values.items() - } - if PYDANTIC_V2: - return super().model_construct(_fields_set, **kwargs) - return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] - - if not TYPE_CHECKING: - # type checkers incorrectly complain about this assignment - model_construct = construct diff --git a/src/browser_use_sdk/_qs.py b/src/browser_use_sdk/_qs.py deleted file mode 100644 index 274320c..0000000 --- a/src/browser_use_sdk/_qs.py +++ /dev/null @@ -1,150 +0,0 @@ -from __future__ import annotations - -from typing import Any, List, Tuple, Union, Mapping, TypeVar -from urllib.parse import parse_qs, urlencode -from typing_extensions import Literal, get_args - -from ._types import NOT_GIVEN, NotGiven, NotGivenOr -from ._utils import flatten - -_T = TypeVar("_T") - - -ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] -NestedFormat = Literal["dots", "brackets"] - -PrimitiveData = Union[str, int, float, bool, None] -# this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] -# https://github.com/microsoft/pyright/issues/3555 -Data = Union[PrimitiveData, List[Any], Tuple[Any], "Mapping[str, Any]"] -Params = Mapping[str, Data] - - -class Querystring: - array_format: ArrayFormat - nested_format: NestedFormat - - def __init__( - self, - *, - array_format: ArrayFormat = "repeat", - nested_format: NestedFormat = "brackets", - ) -> None: - self.array_format = array_format - self.nested_format = nested_format - - def parse(self, query: str) -> Mapping[str, object]: - # Note: custom format syntax is not supported yet - return parse_qs(query) - - def stringify( - self, - params: Params, - *, - array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, - nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, - ) -> str: - return urlencode( - self.stringify_items( - params, - array_format=array_format, - nested_format=nested_format, - ) - ) - - def stringify_items( - self, - params: Params, - *, - array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, - nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, - ) -> list[tuple[str, str]]: - opts = Options( - qs=self, - array_format=array_format, - nested_format=nested_format, - ) - return flatten([self._stringify_item(key, value, opts) for key, value in params.items()]) - - def _stringify_item( - self, - key: str, - value: Data, - opts: Options, - ) -> list[tuple[str, str]]: - if isinstance(value, Mapping): - items: list[tuple[str, str]] = [] - nested_format = opts.nested_format - for subkey, subvalue in value.items(): - items.extend( - self._stringify_item( - # TODO: error if unknown format - f"{key}.{subkey}" if nested_format == "dots" else f"{key}[{subkey}]", - subvalue, - opts, - ) - ) - return items - - if isinstance(value, (list, tuple)): - array_format = opts.array_format - if array_format == "comma": - return [ - ( - key, - ",".join(self._primitive_value_to_str(item) for item in value if item is not None), - ), - ] - elif array_format == "repeat": - items = [] - for item in value: - items.extend(self._stringify_item(key, item, opts)) - return items - elif array_format == "indices": - raise NotImplementedError("The array indices format is not supported yet") - elif array_format == "brackets": - items = [] - key = key + "[]" - for item in value: - items.extend(self._stringify_item(key, item, opts)) - return items - else: - raise NotImplementedError( - f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" - ) - - serialised = self._primitive_value_to_str(value) - if not serialised: - return [] - return [(key, serialised)] - - def _primitive_value_to_str(self, value: PrimitiveData) -> str: - # copied from httpx - if value is True: - return "true" - elif value is False: - return "false" - elif value is None: - return "" - return str(value) - - -_qs = Querystring() -parse = _qs.parse -stringify = _qs.stringify -stringify_items = _qs.stringify_items - - -class Options: - array_format: ArrayFormat - nested_format: NestedFormat - - def __init__( - self, - qs: Querystring = _qs, - *, - array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, - nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, - ) -> None: - self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format - self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format diff --git a/src/browser_use_sdk/_resource.py b/src/browser_use_sdk/_resource.py deleted file mode 100644 index 770c41a..0000000 --- a/src/browser_use_sdk/_resource.py +++ /dev/null @@ -1,43 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import time -from typing import TYPE_CHECKING - -import anyio - -if TYPE_CHECKING: - from ._client import BrowserUse, AsyncBrowserUse - - -class SyncAPIResource: - _client: BrowserUse - - def __init__(self, client: BrowserUse) -> None: - self._client = client - self._get = client.get - self._post = client.post - self._patch = client.patch - self._put = client.put - self._delete = client.delete - self._get_api_list = client.get_api_list - - def _sleep(self, seconds: float) -> None: - time.sleep(seconds) - - -class AsyncAPIResource: - _client: AsyncBrowserUse - - def __init__(self, client: AsyncBrowserUse) -> None: - self._client = client - self._get = client.get - self._post = client.post - self._patch = client.patch - self._put = client.put - self._delete = client.delete - self._get_api_list = client.get_api_list - - async def _sleep(self, seconds: float) -> None: - await anyio.sleep(seconds) diff --git a/src/browser_use_sdk/_response.py b/src/browser_use_sdk/_response.py deleted file mode 100644 index 28a84a0..0000000 --- a/src/browser_use_sdk/_response.py +++ /dev/null @@ -1,832 +0,0 @@ -from __future__ import annotations - -import os -import inspect -import logging -import datetime -import functools -from types import TracebackType -from typing import ( - TYPE_CHECKING, - Any, - Union, - Generic, - TypeVar, - Callable, - Iterator, - AsyncIterator, - cast, - overload, -) -from typing_extensions import Awaitable, ParamSpec, override, get_origin - -import anyio -import httpx -import pydantic - -from ._types import NoneType -from ._utils import is_given, extract_type_arg, is_annotated_type, is_type_alias_type, extract_type_var_from_base -from ._models import BaseModel, is_basemodel -from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER -from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type -from ._exceptions import BrowserUseError, APIResponseValidationError - -if TYPE_CHECKING: - from ._models import FinalRequestOptions - from ._base_client import BaseClient - - -P = ParamSpec("P") -R = TypeVar("R") -_T = TypeVar("_T") -_APIResponseT = TypeVar("_APIResponseT", bound="APIResponse[Any]") -_AsyncAPIResponseT = TypeVar("_AsyncAPIResponseT", bound="AsyncAPIResponse[Any]") - -log: logging.Logger = logging.getLogger(__name__) - - -class BaseAPIResponse(Generic[R]): - _cast_to: type[R] - _client: BaseClient[Any, Any] - _parsed_by_type: dict[type[Any], Any] - _is_sse_stream: bool - _stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None - _options: FinalRequestOptions - - http_response: httpx.Response - - retries_taken: int - """The number of retries made. If no retries happened this will be `0`""" - - def __init__( - self, - *, - raw: httpx.Response, - cast_to: type[R], - client: BaseClient[Any, Any], - stream: bool, - stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, - options: FinalRequestOptions, - retries_taken: int = 0, - ) -> None: - self._cast_to = cast_to - self._client = client - self._parsed_by_type = {} - self._is_sse_stream = stream - self._stream_cls = stream_cls - self._options = options - self.http_response = raw - self.retries_taken = retries_taken - - @property - def headers(self) -> httpx.Headers: - return self.http_response.headers - - @property - def http_request(self) -> httpx.Request: - """Returns the httpx Request instance associated with the current response.""" - return self.http_response.request - - @property - def status_code(self) -> int: - return self.http_response.status_code - - @property - def url(self) -> httpx.URL: - """Returns the URL for which the request was made.""" - return self.http_response.url - - @property - def method(self) -> str: - return self.http_request.method - - @property - def http_version(self) -> str: - return self.http_response.http_version - - @property - def elapsed(self) -> datetime.timedelta: - """The time taken for the complete request/response cycle to complete.""" - return self.http_response.elapsed - - @property - def is_closed(self) -> bool: - """Whether or not the response body has been closed. - - If this is False then there is response data that has not been read yet. - You must either fully consume the response body or call `.close()` - before discarding the response to prevent resource leaks. - """ - return self.http_response.is_closed - - @override - def __repr__(self) -> str: - return ( - f"<{self.__class__.__name__} [{self.status_code} {self.http_response.reason_phrase}] type={self._cast_to}>" - ) - - def _parse(self, *, to: type[_T] | None = None) -> R | _T: - cast_to = to if to is not None else self._cast_to - - # unwrap `TypeAlias('Name', T)` -> `T` - if is_type_alias_type(cast_to): - cast_to = cast_to.__value__ # type: ignore[unreachable] - - # unwrap `Annotated[T, ...]` -> `T` - if cast_to and is_annotated_type(cast_to): - cast_to = extract_type_arg(cast_to, 0) - - origin = get_origin(cast_to) or cast_to - - if self._is_sse_stream: - if to: - if not is_stream_class_type(to): - raise TypeError(f"Expected custom parse type to be a subclass of {Stream} or {AsyncStream}") - - return cast( - _T, - to( - cast_to=extract_stream_chunk_type( - to, - failure_message="Expected custom stream type to be passed with a type argument, e.g. Stream[ChunkType]", - ), - response=self.http_response, - client=cast(Any, self._client), - ), - ) - - if self._stream_cls: - return cast( - R, - self._stream_cls( - cast_to=extract_stream_chunk_type(self._stream_cls), - response=self.http_response, - client=cast(Any, self._client), - ), - ) - - stream_cls = cast("type[Stream[Any]] | type[AsyncStream[Any]] | None", self._client._default_stream_cls) - if stream_cls is None: - raise MissingStreamClassError() - - return cast( - R, - stream_cls( - cast_to=cast_to, - response=self.http_response, - client=cast(Any, self._client), - ), - ) - - if cast_to is NoneType: - return cast(R, None) - - response = self.http_response - if cast_to == str: - return cast(R, response.text) - - if cast_to == bytes: - return cast(R, response.content) - - if cast_to == int: - return cast(R, int(response.text)) - - if cast_to == float: - return cast(R, float(response.text)) - - if cast_to == bool: - return cast(R, response.text.lower() == "true") - - if origin == APIResponse: - raise RuntimeError("Unexpected state - cast_to is `APIResponse`") - - if inspect.isclass(origin) and issubclass(origin, httpx.Response): - # Because of the invariance of our ResponseT TypeVar, users can subclass httpx.Response - # and pass that class to our request functions. We cannot change the variance to be either - # covariant or contravariant as that makes our usage of ResponseT illegal. We could construct - # the response class ourselves but that is something that should be supported directly in httpx - # as it would be easy to incorrectly construct the Response object due to the multitude of arguments. - if cast_to != httpx.Response: - raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`") - return cast(R, response) - - if ( - inspect.isclass( - origin # pyright: ignore[reportUnknownArgumentType] - ) - and not issubclass(origin, BaseModel) - and issubclass(origin, pydantic.BaseModel) - ): - raise TypeError( - "Pydantic models must subclass our base model type, e.g. `from browser_use_sdk import BaseModel`" - ) - - if ( - cast_to is not object - and not origin is list - and not origin is dict - and not origin is Union - and not issubclass(origin, BaseModel) - ): - raise RuntimeError( - f"Unsupported type, expected {cast_to} to be a subclass of {BaseModel}, {dict}, {list}, {Union}, {NoneType}, {str} or {httpx.Response}." - ) - - # split is required to handle cases where additional information is included - # in the response, e.g. application/json; charset=utf-8 - content_type, *_ = response.headers.get("content-type", "*").split(";") - if not content_type.endswith("json"): - if is_basemodel(cast_to): - try: - data = response.json() - except Exception as exc: - log.debug("Could not read JSON from response data due to %s - %s", type(exc), exc) - else: - return self._client._process_response_data( - data=data, - cast_to=cast_to, # type: ignore - response=response, - ) - - if self._client._strict_response_validation: - raise APIResponseValidationError( - response=response, - message=f"Expected Content-Type response header to be `application/json` but received `{content_type}` instead.", - body=response.text, - ) - - # If the API responds with content that isn't JSON then we just return - # the (decoded) text without performing any parsing so that you can still - # handle the response however you need to. - return response.text # type: ignore - - data = response.json() - - return self._client._process_response_data( - data=data, - cast_to=cast_to, # type: ignore - response=response, - ) - - -class APIResponse(BaseAPIResponse[R]): - @overload - def parse(self, *, to: type[_T]) -> _T: ... - - @overload - def parse(self) -> R: ... - - def parse(self, *, to: type[_T] | None = None) -> R | _T: - """Returns the rich python representation of this response's data. - - For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`. - - You can customise the type that the response is parsed into through - the `to` argument, e.g. - - ```py - from browser_use_sdk import BaseModel - - - class MyModel(BaseModel): - foo: str - - - obj = response.parse(to=MyModel) - print(obj.foo) - ``` - - We support parsing: - - `BaseModel` - - `dict` - - `list` - - `Union` - - `str` - - `int` - - `float` - - `httpx.Response` - """ - cache_key = to if to is not None else self._cast_to - cached = self._parsed_by_type.get(cache_key) - if cached is not None: - return cached # type: ignore[no-any-return] - - if not self._is_sse_stream: - self.read() - - parsed = self._parse(to=to) - if is_given(self._options.post_parser): - parsed = self._options.post_parser(parsed) - - self._parsed_by_type[cache_key] = parsed - return parsed - - def read(self) -> bytes: - """Read and return the binary response content.""" - try: - return self.http_response.read() - except httpx.StreamConsumed as exc: - # The default error raised by httpx isn't very - # helpful in our case so we re-raise it with - # a different error message. - raise StreamAlreadyConsumed() from exc - - def text(self) -> str: - """Read and decode the response content into a string.""" - self.read() - return self.http_response.text - - def json(self) -> object: - """Read and decode the JSON response content.""" - self.read() - return self.http_response.json() - - def close(self) -> None: - """Close the response and release the connection. - - Automatically called if the response body is read to completion. - """ - self.http_response.close() - - def iter_bytes(self, chunk_size: int | None = None) -> Iterator[bytes]: - """ - A byte-iterator over the decoded response content. - - This automatically handles gzip, deflate and brotli encoded responses. - """ - for chunk in self.http_response.iter_bytes(chunk_size): - yield chunk - - def iter_text(self, chunk_size: int | None = None) -> Iterator[str]: - """A str-iterator over the decoded response content - that handles both gzip, deflate, etc but also detects the content's - string encoding. - """ - for chunk in self.http_response.iter_text(chunk_size): - yield chunk - - def iter_lines(self) -> Iterator[str]: - """Like `iter_text()` but will only yield chunks for each line""" - for chunk in self.http_response.iter_lines(): - yield chunk - - -class AsyncAPIResponse(BaseAPIResponse[R]): - @overload - async def parse(self, *, to: type[_T]) -> _T: ... - - @overload - async def parse(self) -> R: ... - - async def parse(self, *, to: type[_T] | None = None) -> R | _T: - """Returns the rich python representation of this response's data. - - For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`. - - You can customise the type that the response is parsed into through - the `to` argument, e.g. - - ```py - from browser_use_sdk import BaseModel - - - class MyModel(BaseModel): - foo: str - - - obj = response.parse(to=MyModel) - print(obj.foo) - ``` - - We support parsing: - - `BaseModel` - - `dict` - - `list` - - `Union` - - `str` - - `httpx.Response` - """ - cache_key = to if to is not None else self._cast_to - cached = self._parsed_by_type.get(cache_key) - if cached is not None: - return cached # type: ignore[no-any-return] - - if not self._is_sse_stream: - await self.read() - - parsed = self._parse(to=to) - if is_given(self._options.post_parser): - parsed = self._options.post_parser(parsed) - - self._parsed_by_type[cache_key] = parsed - return parsed - - async def read(self) -> bytes: - """Read and return the binary response content.""" - try: - return await self.http_response.aread() - except httpx.StreamConsumed as exc: - # the default error raised by httpx isn't very - # helpful in our case so we re-raise it with - # a different error message - raise StreamAlreadyConsumed() from exc - - async def text(self) -> str: - """Read and decode the response content into a string.""" - await self.read() - return self.http_response.text - - async def json(self) -> object: - """Read and decode the JSON response content.""" - await self.read() - return self.http_response.json() - - async def close(self) -> None: - """Close the response and release the connection. - - Automatically called if the response body is read to completion. - """ - await self.http_response.aclose() - - async def iter_bytes(self, chunk_size: int | None = None) -> AsyncIterator[bytes]: - """ - A byte-iterator over the decoded response content. - - This automatically handles gzip, deflate and brotli encoded responses. - """ - async for chunk in self.http_response.aiter_bytes(chunk_size): - yield chunk - - async def iter_text(self, chunk_size: int | None = None) -> AsyncIterator[str]: - """A str-iterator over the decoded response content - that handles both gzip, deflate, etc but also detects the content's - string encoding. - """ - async for chunk in self.http_response.aiter_text(chunk_size): - yield chunk - - async def iter_lines(self) -> AsyncIterator[str]: - """Like `iter_text()` but will only yield chunks for each line""" - async for chunk in self.http_response.aiter_lines(): - yield chunk - - -class BinaryAPIResponse(APIResponse[bytes]): - """Subclass of APIResponse providing helpers for dealing with binary data. - - Note: If you want to stream the response data instead of eagerly reading it - all at once then you should use `.with_streaming_response` when making - the API request, e.g. `.with_streaming_response.get_binary_response()` - """ - - def write_to_file( - self, - file: str | os.PathLike[str], - ) -> None: - """Write the output to the given file. - - Accepts a filename or any path-like object, e.g. pathlib.Path - - Note: if you want to stream the data to the file instead of writing - all at once then you should use `.with_streaming_response` when making - the API request, e.g. `.with_streaming_response.get_binary_response()` - """ - with open(file, mode="wb") as f: - for data in self.iter_bytes(): - f.write(data) - - -class AsyncBinaryAPIResponse(AsyncAPIResponse[bytes]): - """Subclass of APIResponse providing helpers for dealing with binary data. - - Note: If you want to stream the response data instead of eagerly reading it - all at once then you should use `.with_streaming_response` when making - the API request, e.g. `.with_streaming_response.get_binary_response()` - """ - - async def write_to_file( - self, - file: str | os.PathLike[str], - ) -> None: - """Write the output to the given file. - - Accepts a filename or any path-like object, e.g. pathlib.Path - - Note: if you want to stream the data to the file instead of writing - all at once then you should use `.with_streaming_response` when making - the API request, e.g. `.with_streaming_response.get_binary_response()` - """ - path = anyio.Path(file) - async with await path.open(mode="wb") as f: - async for data in self.iter_bytes(): - await f.write(data) - - -class StreamedBinaryAPIResponse(APIResponse[bytes]): - def stream_to_file( - self, - file: str | os.PathLike[str], - *, - chunk_size: int | None = None, - ) -> None: - """Streams the output to the given file. - - Accepts a filename or any path-like object, e.g. pathlib.Path - """ - with open(file, mode="wb") as f: - for data in self.iter_bytes(chunk_size): - f.write(data) - - -class AsyncStreamedBinaryAPIResponse(AsyncAPIResponse[bytes]): - async def stream_to_file( - self, - file: str | os.PathLike[str], - *, - chunk_size: int | None = None, - ) -> None: - """Streams the output to the given file. - - Accepts a filename or any path-like object, e.g. pathlib.Path - """ - path = anyio.Path(file) - async with await path.open(mode="wb") as f: - async for data in self.iter_bytes(chunk_size): - await f.write(data) - - -class MissingStreamClassError(TypeError): - def __init__(self) -> None: - super().__init__( - "The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `browser_use_sdk._streaming` for reference", - ) - - -class StreamAlreadyConsumed(BrowserUseError): - """ - Attempted to read or stream content, but the content has already - been streamed. - - This can happen if you use a method like `.iter_lines()` and then attempt - to read th entire response body afterwards, e.g. - - ```py - response = await client.post(...) - async for line in response.iter_lines(): - ... # do something with `line` - - content = await response.read() - # ^ error - ``` - - If you want this behaviour you'll need to either manually accumulate the response - content or call `await response.read()` before iterating over the stream. - """ - - def __init__(self) -> None: - message = ( - "Attempted to read or stream some content, but the content has " - "already been streamed. " - "This could be due to attempting to stream the response " - "content more than once." - "\n\n" - "You can fix this by manually accumulating the response content while streaming " - "or by calling `.read()` before starting to stream." - ) - super().__init__(message) - - -class ResponseContextManager(Generic[_APIResponseT]): - """Context manager for ensuring that a request is not made - until it is entered and that the response will always be closed - when the context manager exits - """ - - def __init__(self, request_func: Callable[[], _APIResponseT]) -> None: - self._request_func = request_func - self.__response: _APIResponseT | None = None - - def __enter__(self) -> _APIResponseT: - self.__response = self._request_func() - return self.__response - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - if self.__response is not None: - self.__response.close() - - -class AsyncResponseContextManager(Generic[_AsyncAPIResponseT]): - """Context manager for ensuring that a request is not made - until it is entered and that the response will always be closed - when the context manager exits - """ - - def __init__(self, api_request: Awaitable[_AsyncAPIResponseT]) -> None: - self._api_request = api_request - self.__response: _AsyncAPIResponseT | None = None - - async def __aenter__(self) -> _AsyncAPIResponseT: - self.__response = await self._api_request - return self.__response - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - if self.__response is not None: - await self.__response.close() - - -def to_streamed_response_wrapper(func: Callable[P, R]) -> Callable[P, ResponseContextManager[APIResponse[R]]]: - """Higher order function that takes one of our bound API methods and wraps it - to support streaming and returning the raw `APIResponse` object directly. - """ - - @functools.wraps(func) - def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[APIResponse[R]]: - extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} - extra_headers[RAW_RESPONSE_HEADER] = "stream" - - kwargs["extra_headers"] = extra_headers - - make_request = functools.partial(func, *args, **kwargs) - - return ResponseContextManager(cast(Callable[[], APIResponse[R]], make_request)) - - return wrapped - - -def async_to_streamed_response_wrapper( - func: Callable[P, Awaitable[R]], -) -> Callable[P, AsyncResponseContextManager[AsyncAPIResponse[R]]]: - """Higher order function that takes one of our bound API methods and wraps it - to support streaming and returning the raw `APIResponse` object directly. - """ - - @functools.wraps(func) - def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[AsyncAPIResponse[R]]: - extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} - extra_headers[RAW_RESPONSE_HEADER] = "stream" - - kwargs["extra_headers"] = extra_headers - - make_request = func(*args, **kwargs) - - return AsyncResponseContextManager(cast(Awaitable[AsyncAPIResponse[R]], make_request)) - - return wrapped - - -def to_custom_streamed_response_wrapper( - func: Callable[P, object], - response_cls: type[_APIResponseT], -) -> Callable[P, ResponseContextManager[_APIResponseT]]: - """Higher order function that takes one of our bound API methods and an `APIResponse` class - and wraps the method to support streaming and returning the given response class directly. - - Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` - """ - - @functools.wraps(func) - def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[_APIResponseT]: - extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} - extra_headers[RAW_RESPONSE_HEADER] = "stream" - extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls - - kwargs["extra_headers"] = extra_headers - - make_request = functools.partial(func, *args, **kwargs) - - return ResponseContextManager(cast(Callable[[], _APIResponseT], make_request)) - - return wrapped - - -def async_to_custom_streamed_response_wrapper( - func: Callable[P, Awaitable[object]], - response_cls: type[_AsyncAPIResponseT], -) -> Callable[P, AsyncResponseContextManager[_AsyncAPIResponseT]]: - """Higher order function that takes one of our bound API methods and an `APIResponse` class - and wraps the method to support streaming and returning the given response class directly. - - Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` - """ - - @functools.wraps(func) - def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[_AsyncAPIResponseT]: - extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} - extra_headers[RAW_RESPONSE_HEADER] = "stream" - extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls - - kwargs["extra_headers"] = extra_headers - - make_request = func(*args, **kwargs) - - return AsyncResponseContextManager(cast(Awaitable[_AsyncAPIResponseT], make_request)) - - return wrapped - - -def to_raw_response_wrapper(func: Callable[P, R]) -> Callable[P, APIResponse[R]]: - """Higher order function that takes one of our bound API methods and wraps it - to support returning the raw `APIResponse` object directly. - """ - - @functools.wraps(func) - def wrapped(*args: P.args, **kwargs: P.kwargs) -> APIResponse[R]: - extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} - extra_headers[RAW_RESPONSE_HEADER] = "raw" - - kwargs["extra_headers"] = extra_headers - - return cast(APIResponse[R], func(*args, **kwargs)) - - return wrapped - - -def async_to_raw_response_wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[AsyncAPIResponse[R]]]: - """Higher order function that takes one of our bound API methods and wraps it - to support returning the raw `APIResponse` object directly. - """ - - @functools.wraps(func) - async def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncAPIResponse[R]: - extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} - extra_headers[RAW_RESPONSE_HEADER] = "raw" - - kwargs["extra_headers"] = extra_headers - - return cast(AsyncAPIResponse[R], await func(*args, **kwargs)) - - return wrapped - - -def to_custom_raw_response_wrapper( - func: Callable[P, object], - response_cls: type[_APIResponseT], -) -> Callable[P, _APIResponseT]: - """Higher order function that takes one of our bound API methods and an `APIResponse` class - and wraps the method to support returning the given response class directly. - - Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` - """ - - @functools.wraps(func) - def wrapped(*args: P.args, **kwargs: P.kwargs) -> _APIResponseT: - extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} - extra_headers[RAW_RESPONSE_HEADER] = "raw" - extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls - - kwargs["extra_headers"] = extra_headers - - return cast(_APIResponseT, func(*args, **kwargs)) - - return wrapped - - -def async_to_custom_raw_response_wrapper( - func: Callable[P, Awaitable[object]], - response_cls: type[_AsyncAPIResponseT], -) -> Callable[P, Awaitable[_AsyncAPIResponseT]]: - """Higher order function that takes one of our bound API methods and an `APIResponse` class - and wraps the method to support returning the given response class directly. - - Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` - """ - - @functools.wraps(func) - def wrapped(*args: P.args, **kwargs: P.kwargs) -> Awaitable[_AsyncAPIResponseT]: - extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} - extra_headers[RAW_RESPONSE_HEADER] = "raw" - extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls - - kwargs["extra_headers"] = extra_headers - - return cast(Awaitable[_AsyncAPIResponseT], func(*args, **kwargs)) - - return wrapped - - -def extract_response_type(typ: type[BaseAPIResponse[Any]]) -> type: - """Given a type like `APIResponse[T]`, returns the generic type variable `T`. - - This also handles the case where a concrete subclass is given, e.g. - ```py - class MyResponse(APIResponse[bytes]): - ... - - extract_response_type(MyResponse) -> bytes - ``` - """ - return extract_type_var_from_base( - typ, - generic_bases=cast("tuple[type, ...]", (BaseAPIResponse, APIResponse, AsyncAPIResponse)), - index=0, - ) diff --git a/src/browser_use_sdk/_streaming.py b/src/browser_use_sdk/_streaming.py deleted file mode 100644 index f52ffd4..0000000 --- a/src/browser_use_sdk/_streaming.py +++ /dev/null @@ -1,333 +0,0 @@ -# Note: initially copied from https://github.com/florimondmanca/httpx-sse/blob/master/src/httpx_sse/_decoders.py -from __future__ import annotations - -import json -import inspect -from types import TracebackType -from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast -from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable - -import httpx - -from ._utils import extract_type_var_from_base - -if TYPE_CHECKING: - from ._client import BrowserUse, AsyncBrowserUse - - -_T = TypeVar("_T") - - -class Stream(Generic[_T]): - """Provides the core interface to iterate over a synchronous stream response.""" - - response: httpx.Response - - _decoder: SSEBytesDecoder - - def __init__( - self, - *, - cast_to: type[_T], - response: httpx.Response, - client: BrowserUse, - ) -> None: - self.response = response - self._cast_to = cast_to - self._client = client - self._decoder = client._make_sse_decoder() - self._iterator = self.__stream__() - - def __next__(self) -> _T: - return self._iterator.__next__() - - def __iter__(self) -> Iterator[_T]: - for item in self._iterator: - yield item - - def _iter_events(self) -> Iterator[ServerSentEvent]: - yield from self._decoder.iter_bytes(self.response.iter_bytes()) - - def __stream__(self) -> Iterator[_T]: - cast_to = cast(Any, self._cast_to) - response = self.response - process_data = self._client._process_response_data - iterator = self._iter_events() - - for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - # Ensure the entire stream is consumed - for _sse in iterator: - ... - - def __enter__(self) -> Self: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - self.close() - - def close(self) -> None: - """ - Close the response and release the connection. - - Automatically called if the response body is read to completion. - """ - self.response.close() - - -class AsyncStream(Generic[_T]): - """Provides the core interface to iterate over an asynchronous stream response.""" - - response: httpx.Response - - _decoder: SSEDecoder | SSEBytesDecoder - - def __init__( - self, - *, - cast_to: type[_T], - response: httpx.Response, - client: AsyncBrowserUse, - ) -> None: - self.response = response - self._cast_to = cast_to - self._client = client - self._decoder = client._make_sse_decoder() - self._iterator = self.__stream__() - - async def __anext__(self) -> _T: - return await self._iterator.__anext__() - - async def __aiter__(self) -> AsyncIterator[_T]: - async for item in self._iterator: - yield item - - async def _iter_events(self) -> AsyncIterator[ServerSentEvent]: - async for sse in self._decoder.aiter_bytes(self.response.aiter_bytes()): - yield sse - - async def __stream__(self) -> AsyncIterator[_T]: - cast_to = cast(Any, self._cast_to) - response = self.response - process_data = self._client._process_response_data - iterator = self._iter_events() - - async for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - # Ensure the entire stream is consumed - async for _sse in iterator: - ... - - async def __aenter__(self) -> Self: - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - await self.close() - - async def close(self) -> None: - """ - Close the response and release the connection. - - Automatically called if the response body is read to completion. - """ - await self.response.aclose() - - -class ServerSentEvent: - def __init__( - self, - *, - event: str | None = None, - data: str | None = None, - id: str | None = None, - retry: int | None = None, - ) -> None: - if data is None: - data = "" - - self._id = id - self._data = data - self._event = event or None - self._retry = retry - - @property - def event(self) -> str | None: - return self._event - - @property - def id(self) -> str | None: - return self._id - - @property - def retry(self) -> int | None: - return self._retry - - @property - def data(self) -> str: - return self._data - - def json(self) -> Any: - return json.loads(self.data) - - @override - def __repr__(self) -> str: - return f"ServerSentEvent(event={self.event}, data={self.data}, id={self.id}, retry={self.retry})" - - -class SSEDecoder: - _data: list[str] - _event: str | None - _retry: int | None - _last_event_id: str | None - - def __init__(self) -> None: - self._event = None - self._data = [] - self._last_event_id = None - self._retry = None - - def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: - """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" - for chunk in self._iter_chunks(iterator): - # Split before decoding so splitlines() only uses \r and \n - for raw_line in chunk.splitlines(): - line = raw_line.decode("utf-8") - sse = self.decode(line) - if sse: - yield sse - - def _iter_chunks(self, iterator: Iterator[bytes]) -> Iterator[bytes]: - """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" - data = b"" - for chunk in iterator: - for line in chunk.splitlines(keepends=True): - data += line - if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): - yield data - data = b"" - if data: - yield data - - async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: - """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" - async for chunk in self._aiter_chunks(iterator): - # Split before decoding so splitlines() only uses \r and \n - for raw_line in chunk.splitlines(): - line = raw_line.decode("utf-8") - sse = self.decode(line) - if sse: - yield sse - - async def _aiter_chunks(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[bytes]: - """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" - data = b"" - async for chunk in iterator: - for line in chunk.splitlines(keepends=True): - data += line - if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): - yield data - data = b"" - if data: - yield data - - def decode(self, line: str) -> ServerSentEvent | None: - # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 - - if not line: - if not self._event and not self._data and not self._last_event_id and self._retry is None: - return None - - sse = ServerSentEvent( - event=self._event, - data="\n".join(self._data), - id=self._last_event_id, - retry=self._retry, - ) - - # NOTE: as per the SSE spec, do not reset last_event_id. - self._event = None - self._data = [] - self._retry = None - - return sse - - if line.startswith(":"): - return None - - fieldname, _, value = line.partition(":") - - if value.startswith(" "): - value = value[1:] - - if fieldname == "event": - self._event = value - elif fieldname == "data": - self._data.append(value) - elif fieldname == "id": - if "\0" in value: - pass - else: - self._last_event_id = value - elif fieldname == "retry": - try: - self._retry = int(value) - except (TypeError, ValueError): - pass - else: - pass # Field is ignored. - - return None - - -@runtime_checkable -class SSEBytesDecoder(Protocol): - def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: - """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" - ... - - def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: - """Given an async iterator that yields raw binary data, iterate over it & yield every event encountered""" - ... - - -def is_stream_class_type(typ: type) -> TypeGuard[type[Stream[object]] | type[AsyncStream[object]]]: - """TypeGuard for determining whether or not the given type is a subclass of `Stream` / `AsyncStream`""" - origin = get_origin(typ) or typ - return inspect.isclass(origin) and issubclass(origin, (Stream, AsyncStream)) - - -def extract_stream_chunk_type( - stream_cls: type, - *, - failure_message: str | None = None, -) -> type: - """Given a type like `Stream[T]`, returns the generic type variable `T`. - - This also handles the case where a concrete subclass is given, e.g. - ```py - class MyStream(Stream[bytes]): - ... - - extract_stream_chunk_type(MyStream) -> bytes - ``` - """ - from ._base_client import Stream, AsyncStream - - return extract_type_var_from_base( - stream_cls, - index=0, - generic_bases=cast("tuple[type, ...]", (Stream, AsyncStream)), - failure_message=failure_message, - ) diff --git a/src/browser_use_sdk/_types.py b/src/browser_use_sdk/_types.py deleted file mode 100644 index 450c5bb..0000000 --- a/src/browser_use_sdk/_types.py +++ /dev/null @@ -1,219 +0,0 @@ -from __future__ import annotations - -from os import PathLike -from typing import ( - IO, - TYPE_CHECKING, - Any, - Dict, - List, - Type, - Tuple, - Union, - Mapping, - TypeVar, - Callable, - Optional, - Sequence, -) -from typing_extensions import Set, Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable - -import httpx -import pydantic -from httpx import URL, Proxy, Timeout, Response, BaseTransport, AsyncBaseTransport - -if TYPE_CHECKING: - from ._models import BaseModel - from ._response import APIResponse, AsyncAPIResponse - -Transport = BaseTransport -AsyncTransport = AsyncBaseTransport -Query = Mapping[str, object] -Body = object -AnyMapping = Mapping[str, object] -ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) -_T = TypeVar("_T") - - -# Approximates httpx internal ProxiesTypes and RequestFiles types -# while adding support for `PathLike` instances -ProxiesDict = Dict["str | URL", Union[None, str, URL, Proxy]] -ProxiesTypes = Union[str, Proxy, ProxiesDict] -if TYPE_CHECKING: - Base64FileInput = Union[IO[bytes], PathLike[str]] - FileContent = Union[IO[bytes], bytes, PathLike[str]] -else: - Base64FileInput = Union[IO[bytes], PathLike] - FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. -FileTypes = Union[ - # file (or bytes) - FileContent, - # (filename, file (or bytes)) - Tuple[Optional[str], FileContent], - # (filename, file (or bytes), content_type) - Tuple[Optional[str], FileContent, Optional[str]], - # (filename, file (or bytes), content_type, headers) - Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], -] -RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]] - -# duplicate of the above but without our custom file support -HttpxFileContent = Union[IO[bytes], bytes] -HttpxFileTypes = Union[ - # file (or bytes) - HttpxFileContent, - # (filename, file (or bytes)) - Tuple[Optional[str], HttpxFileContent], - # (filename, file (or bytes), content_type) - Tuple[Optional[str], HttpxFileContent, Optional[str]], - # (filename, file (or bytes), content_type, headers) - Tuple[Optional[str], HttpxFileContent, Optional[str], Mapping[str, str]], -] -HttpxRequestFiles = Union[Mapping[str, HttpxFileTypes], Sequence[Tuple[str, HttpxFileTypes]]] - -# Workaround to support (cast_to: Type[ResponseT]) -> ResponseT -# where ResponseT includes `None`. In order to support directly -# passing `None`, overloads would have to be defined for every -# method that uses `ResponseT` which would lead to an unacceptable -# amount of code duplication and make it unreadable. See _base_client.py -# for example usage. -# -# This unfortunately means that you will either have -# to import this type and pass it explicitly: -# -# from browser_use_sdk import NoneType -# client.get('/foo', cast_to=NoneType) -# -# or build it yourself: -# -# client.get('/foo', cast_to=type(None)) -if TYPE_CHECKING: - NoneType: Type[None] -else: - NoneType = type(None) - - -class RequestOptions(TypedDict, total=False): - headers: Headers - max_retries: int - timeout: float | Timeout | None - params: Query - extra_json: AnyMapping - idempotency_key: str - follow_redirects: bool - - -# Sentinel class used until PEP 0661 is accepted -class NotGiven: - """ - A sentinel singleton class used to distinguish omitted keyword arguments - from those passed in with the value None (which may have different behavior). - - For example: - - ```py - def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ... - - - get(timeout=1) # 1s timeout - get(timeout=None) # No timeout - get() # Default timeout behavior, which may not be statically known at the method definition. - ``` - """ - - def __bool__(self) -> Literal[False]: - return False - - @override - def __repr__(self) -> str: - return "NOT_GIVEN" - - -NotGivenOr = Union[_T, NotGiven] -NOT_GIVEN = NotGiven() - - -class Omit: - """In certain situations you need to be able to represent a case where a default value has - to be explicitly removed and `None` is not an appropriate substitute, for example: - - ```py - # as the default `Content-Type` header is `application/json` that will be sent - client.post("/upload/files", files={"file": b"my raw file content"}) - - # you can't explicitly override the header as it has to be dynamically generated - # to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983' - client.post(..., headers={"Content-Type": "multipart/form-data"}) - - # instead you can remove the default `application/json` header by passing Omit - client.post(..., headers={"Content-Type": Omit()}) - ``` - """ - - def __bool__(self) -> Literal[False]: - return False - - -@runtime_checkable -class ModelBuilderProtocol(Protocol): - @classmethod - def build( - cls: type[_T], - *, - response: Response, - data: object, - ) -> _T: ... - - -Headers = Mapping[str, Union[str, Omit]] - - -class HeadersLikeProtocol(Protocol): - def get(self, __key: str) -> str | None: ... - - -HeadersLike = Union[Headers, HeadersLikeProtocol] - -ResponseT = TypeVar( - "ResponseT", - bound=Union[ - object, - str, - None, - "BaseModel", - List[Any], - Dict[str, Any], - Response, - ModelBuilderProtocol, - "APIResponse[Any]", - "AsyncAPIResponse[Any]", - ], -) - -StrBytesIntFloat = Union[str, bytes, int, float] - -# Note: copied from Pydantic -# https://github.com/pydantic/pydantic/blob/6f31f8f68ef011f84357330186f603ff295312fd/pydantic/main.py#L79 -IncEx: TypeAlias = Union[Set[int], Set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]]] - -PostParser = Callable[[Any], Any] - - -@runtime_checkable -class InheritsGeneric(Protocol): - """Represents a type that has inherited from `Generic` - - The `__orig_bases__` property can be used to determine the resolved - type variable for a given base class. - """ - - __orig_bases__: tuple[_GenericAlias] - - -class _GenericAlias(Protocol): - __origin__: type[object] - - -class HttpxSendArgs(TypedDict, total=False): - auth: httpx.Auth - follow_redirects: bool diff --git a/src/browser_use_sdk/_utils/__init__.py b/src/browser_use_sdk/_utils/__init__.py deleted file mode 100644 index d4fda26..0000000 --- a/src/browser_use_sdk/_utils/__init__.py +++ /dev/null @@ -1,57 +0,0 @@ -from ._sync import asyncify as asyncify -from ._proxy import LazyProxy as LazyProxy -from ._utils import ( - flatten as flatten, - is_dict as is_dict, - is_list as is_list, - is_given as is_given, - is_tuple as is_tuple, - json_safe as json_safe, - lru_cache as lru_cache, - is_mapping as is_mapping, - is_tuple_t as is_tuple_t, - parse_date as parse_date, - is_iterable as is_iterable, - is_sequence as is_sequence, - coerce_float as coerce_float, - is_mapping_t as is_mapping_t, - removeprefix as removeprefix, - removesuffix as removesuffix, - extract_files as extract_files, - is_sequence_t as is_sequence_t, - required_args as required_args, - coerce_boolean as coerce_boolean, - coerce_integer as coerce_integer, - file_from_path as file_from_path, - parse_datetime as parse_datetime, - strip_not_given as strip_not_given, - deepcopy_minimal as deepcopy_minimal, - get_async_library as get_async_library, - maybe_coerce_float as maybe_coerce_float, - get_required_header as get_required_header, - maybe_coerce_boolean as maybe_coerce_boolean, - maybe_coerce_integer as maybe_coerce_integer, -) -from ._typing import ( - is_list_type as is_list_type, - is_union_type as is_union_type, - extract_type_arg as extract_type_arg, - is_iterable_type as is_iterable_type, - is_required_type as is_required_type, - is_annotated_type as is_annotated_type, - is_type_alias_type as is_type_alias_type, - strip_annotated_type as strip_annotated_type, - extract_type_var_from_base as extract_type_var_from_base, -) -from ._streams import consume_sync_iterator as consume_sync_iterator, consume_async_iterator as consume_async_iterator -from ._transform import ( - PropertyInfo as PropertyInfo, - transform as transform, - async_transform as async_transform, - maybe_transform as maybe_transform, - async_maybe_transform as async_maybe_transform, -) -from ._reflection import ( - function_has_argument as function_has_argument, - assert_signatures_in_sync as assert_signatures_in_sync, -) diff --git a/src/browser_use_sdk/_utils/_logs.py b/src/browser_use_sdk/_utils/_logs.py deleted file mode 100644 index 8398493..0000000 --- a/src/browser_use_sdk/_utils/_logs.py +++ /dev/null @@ -1,25 +0,0 @@ -import os -import logging - -logger: logging.Logger = logging.getLogger("browser_use_sdk") -httpx_logger: logging.Logger = logging.getLogger("httpx") - - -def _basic_config() -> None: - # e.g. [2023-10-05 14:12:26 - browser_use_sdk._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK" - logging.basicConfig( - format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - - -def setup_logging() -> None: - env = os.environ.get("BROWSER_USE_LOG") - if env == "debug": - _basic_config() - logger.setLevel(logging.DEBUG) - httpx_logger.setLevel(logging.DEBUG) - elif env == "info": - _basic_config() - logger.setLevel(logging.INFO) - httpx_logger.setLevel(logging.INFO) diff --git a/src/browser_use_sdk/_utils/_proxy.py b/src/browser_use_sdk/_utils/_proxy.py deleted file mode 100644 index 0f239a3..0000000 --- a/src/browser_use_sdk/_utils/_proxy.py +++ /dev/null @@ -1,65 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import Generic, TypeVar, Iterable, cast -from typing_extensions import override - -T = TypeVar("T") - - -class LazyProxy(Generic[T], ABC): - """Implements data methods to pretend that an instance is another instance. - - This includes forwarding attribute access and other methods. - """ - - # Note: we have to special case proxies that themselves return proxies - # to support using a proxy as a catch-all for any random access, e.g. `proxy.foo.bar.baz` - - def __getattr__(self, attr: str) -> object: - proxied = self.__get_proxied__() - if isinstance(proxied, LazyProxy): - return proxied # pyright: ignore - return getattr(proxied, attr) - - @override - def __repr__(self) -> str: - proxied = self.__get_proxied__() - if isinstance(proxied, LazyProxy): - return proxied.__class__.__name__ - return repr(self.__get_proxied__()) - - @override - def __str__(self) -> str: - proxied = self.__get_proxied__() - if isinstance(proxied, LazyProxy): - return proxied.__class__.__name__ - return str(proxied) - - @override - def __dir__(self) -> Iterable[str]: - proxied = self.__get_proxied__() - if isinstance(proxied, LazyProxy): - return [] - return proxied.__dir__() - - @property # type: ignore - @override - def __class__(self) -> type: # pyright: ignore - try: - proxied = self.__get_proxied__() - except Exception: - return type(self) - if issubclass(type(proxied), LazyProxy): - return type(proxied) - return proxied.__class__ - - def __get_proxied__(self) -> T: - return self.__load__() - - def __as_proxied__(self) -> T: - """Helper method that returns the current proxy, typed as the loaded object""" - return cast(T, self) - - @abstractmethod - def __load__(self) -> T: ... diff --git a/src/browser_use_sdk/_utils/_reflection.py b/src/browser_use_sdk/_utils/_reflection.py deleted file mode 100644 index 89aa712..0000000 --- a/src/browser_use_sdk/_utils/_reflection.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations - -import inspect -from typing import Any, Callable - - -def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool: - """Returns whether or not the given function has a specific parameter""" - sig = inspect.signature(func) - return arg_name in sig.parameters - - -def assert_signatures_in_sync( - source_func: Callable[..., Any], - check_func: Callable[..., Any], - *, - exclude_params: set[str] = set(), -) -> None: - """Ensure that the signature of the second function matches the first.""" - - check_sig = inspect.signature(check_func) - source_sig = inspect.signature(source_func) - - errors: list[str] = [] - - for name, source_param in source_sig.parameters.items(): - if name in exclude_params: - continue - - custom_param = check_sig.parameters.get(name) - if not custom_param: - errors.append(f"the `{name}` param is missing") - continue - - if custom_param.annotation != source_param.annotation: - errors.append( - f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(custom_param.annotation)}" - ) - continue - - if errors: - raise AssertionError(f"{len(errors)} errors encountered when comparing signatures:\n\n" + "\n\n".join(errors)) diff --git a/src/browser_use_sdk/_utils/_resources_proxy.py b/src/browser_use_sdk/_utils/_resources_proxy.py deleted file mode 100644 index 9451692..0000000 --- a/src/browser_use_sdk/_utils/_resources_proxy.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -from typing import Any -from typing_extensions import override - -from ._proxy import LazyProxy - - -class ResourcesProxy(LazyProxy[Any]): - """A proxy for the `browser_use_sdk.resources` module. - - This is used so that we can lazily import `browser_use_sdk.resources` only when - needed *and* so that users can just import `browser_use_sdk` and reference `browser_use_sdk.resources` - """ - - @override - def __load__(self) -> Any: - import importlib - - mod = importlib.import_module("browser_use_sdk.resources") - return mod - - -resources = ResourcesProxy().__as_proxied__() diff --git a/src/browser_use_sdk/_utils/_streams.py b/src/browser_use_sdk/_utils/_streams.py deleted file mode 100644 index f4a0208..0000000 --- a/src/browser_use_sdk/_utils/_streams.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Any -from typing_extensions import Iterator, AsyncIterator - - -def consume_sync_iterator(iterator: Iterator[Any]) -> None: - for _ in iterator: - ... - - -async def consume_async_iterator(iterator: AsyncIterator[Any]) -> None: - async for _ in iterator: - ... diff --git a/src/browser_use_sdk/_utils/_sync.py b/src/browser_use_sdk/_utils/_sync.py deleted file mode 100644 index ad7ec71..0000000 --- a/src/browser_use_sdk/_utils/_sync.py +++ /dev/null @@ -1,86 +0,0 @@ -from __future__ import annotations - -import sys -import asyncio -import functools -import contextvars -from typing import Any, TypeVar, Callable, Awaitable -from typing_extensions import ParamSpec - -import anyio -import sniffio -import anyio.to_thread - -T_Retval = TypeVar("T_Retval") -T_ParamSpec = ParamSpec("T_ParamSpec") - - -if sys.version_info >= (3, 9): - _asyncio_to_thread = asyncio.to_thread -else: - # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread - # for Python 3.8 support - async def _asyncio_to_thread( - func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs - ) -> Any: - """Asynchronously run function *func* in a separate thread. - - Any *args and **kwargs supplied for this function are directly passed - to *func*. Also, the current :class:`contextvars.Context` is propagated, - allowing context variables from the main thread to be accessed in the - separate thread. - - Returns a coroutine that can be awaited to get the eventual result of *func*. - """ - loop = asyncio.events.get_running_loop() - ctx = contextvars.copy_context() - func_call = functools.partial(ctx.run, func, *args, **kwargs) - return await loop.run_in_executor(None, func_call) - - -async def to_thread( - func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs -) -> T_Retval: - if sniffio.current_async_library() == "asyncio": - return await _asyncio_to_thread(func, *args, **kwargs) - - return await anyio.to_thread.run_sync( - functools.partial(func, *args, **kwargs), - ) - - -# inspired by `asyncer`, https://github.com/tiangolo/asyncer -def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: - """ - Take a blocking function and create an async one that receives the same - positional and keyword arguments. For python version 3.9 and above, it uses - asyncio.to_thread to run the function in a separate thread. For python version - 3.8, it uses locally defined copy of the asyncio.to_thread function which was - introduced in python 3.9. - - Usage: - - ```python - def blocking_func(arg1, arg2, kwarg1=None): - # blocking code - return result - - - result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1) - ``` - - ## Arguments - - `function`: a blocking regular callable (e.g. a function) - - ## Return - - An async function that takes the same positional and keyword arguments as the - original one, that when called runs the same original function in a thread worker - and returns the result. - """ - - async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval: - return await to_thread(function, *args, **kwargs) - - return wrapper diff --git a/src/browser_use_sdk/_utils/_transform.py b/src/browser_use_sdk/_utils/_transform.py deleted file mode 100644 index b0cc20a..0000000 --- a/src/browser_use_sdk/_utils/_transform.py +++ /dev/null @@ -1,447 +0,0 @@ -from __future__ import annotations - -import io -import base64 -import pathlib -from typing import Any, Mapping, TypeVar, cast -from datetime import date, datetime -from typing_extensions import Literal, get_args, override, get_type_hints as _get_type_hints - -import anyio -import pydantic - -from ._utils import ( - is_list, - is_given, - lru_cache, - is_mapping, - is_iterable, -) -from .._files import is_base64_file_input -from ._typing import ( - is_list_type, - is_union_type, - extract_type_arg, - is_iterable_type, - is_required_type, - is_annotated_type, - strip_annotated_type, -) -from .._compat import get_origin, model_dump, is_typeddict - -_T = TypeVar("_T") - - -# TODO: support for drilling globals() and locals() -# TODO: ensure works correctly with forward references in all cases - - -PropertyFormat = Literal["iso8601", "base64", "custom"] - - -class PropertyInfo: - """Metadata class to be used in Annotated types to provide information about a given type. - - For example: - - class MyParams(TypedDict): - account_holder_name: Annotated[str, PropertyInfo(alias='accountHolderName')] - - This means that {'account_holder_name': 'Robert'} will be transformed to {'accountHolderName': 'Robert'} before being sent to the API. - """ - - alias: str | None - format: PropertyFormat | None - format_template: str | None - discriminator: str | None - - def __init__( - self, - *, - alias: str | None = None, - format: PropertyFormat | None = None, - format_template: str | None = None, - discriminator: str | None = None, - ) -> None: - self.alias = alias - self.format = format - self.format_template = format_template - self.discriminator = discriminator - - @override - def __repr__(self) -> str: - return f"{self.__class__.__name__}(alias='{self.alias}', format={self.format}, format_template='{self.format_template}', discriminator='{self.discriminator}')" - - -def maybe_transform( - data: object, - expected_type: object, -) -> Any | None: - """Wrapper over `transform()` that allows `None` to be passed. - - See `transform()` for more details. - """ - if data is None: - return None - return transform(data, expected_type) - - -# Wrapper over _transform_recursive providing fake types -def transform( - data: _T, - expected_type: object, -) -> _T: - """Transform dictionaries based off of type information from the given type, for example: - - ```py - class Params(TypedDict, total=False): - card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]] - - - transformed = transform({"card_id": ""}, Params) - # {'cardID': ''} - ``` - - Any keys / data that does not have type information given will be included as is. - - It should be noted that the transformations that this function does are not represented in the type system. - """ - transformed = _transform_recursive(data, annotation=cast(type, expected_type)) - return cast(_T, transformed) - - -@lru_cache(maxsize=8096) -def _get_annotated_type(type_: type) -> type | None: - """If the given type is an `Annotated` type then it is returned, if not `None` is returned. - - This also unwraps the type when applicable, e.g. `Required[Annotated[T, ...]]` - """ - if is_required_type(type_): - # Unwrap `Required[Annotated[T, ...]]` to `Annotated[T, ...]` - type_ = get_args(type_)[0] - - if is_annotated_type(type_): - return type_ - - return None - - -def _maybe_transform_key(key: str, type_: type) -> str: - """Transform the given `data` based on the annotations provided in `type_`. - - Note: this function only looks at `Annotated` types that contain `PropertyInfo` metadata. - """ - annotated_type = _get_annotated_type(type_) - if annotated_type is None: - # no `Annotated` definition for this type, no transformation needed - return key - - # ignore the first argument as it is the actual type - annotations = get_args(annotated_type)[1:] - for annotation in annotations: - if isinstance(annotation, PropertyInfo) and annotation.alias is not None: - return annotation.alias - - return key - - -def _no_transform_needed(annotation: type) -> bool: - return annotation == float or annotation == int - - -def _transform_recursive( - data: object, - *, - annotation: type, - inner_type: type | None = None, -) -> object: - """Transform the given data against the expected type. - - Args: - annotation: The direct type annotation given to the particular piece of data. - This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc - - inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type - is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in - the list can be transformed using the metadata from the container type. - - Defaults to the same value as the `annotation` argument. - """ - if inner_type is None: - inner_type = annotation - - stripped_type = strip_annotated_type(inner_type) - origin = get_origin(stripped_type) or stripped_type - if is_typeddict(stripped_type) and is_mapping(data): - return _transform_typeddict(data, stripped_type) - - if origin == dict and is_mapping(data): - items_type = get_args(stripped_type)[1] - return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} - - if ( - # List[T] - (is_list_type(stripped_type) and is_list(data)) - # Iterable[T] - or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) - ): - # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually - # intended as an iterable, so we don't transform it. - if isinstance(data, dict): - return cast(object, data) - - inner_type = extract_type_arg(stripped_type, 0) - if _no_transform_needed(inner_type): - # for some types there is no need to transform anything, so we can get a small - # perf boost from skipping that work. - # - # but we still need to convert to a list to ensure the data is json-serializable - if is_list(data): - return data - return list(data) - - return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] - - if is_union_type(stripped_type): - # For union types we run the transformation against all subtypes to ensure that everything is transformed. - # - # TODO: there may be edge cases where the same normalized field name will transform to two different names - # in different subtypes. - for subtype in get_args(stripped_type): - data = _transform_recursive(data, annotation=annotation, inner_type=subtype) - return data - - if isinstance(data, pydantic.BaseModel): - return model_dump(data, exclude_unset=True, mode="json") - - annotated_type = _get_annotated_type(annotation) - if annotated_type is None: - return data - - # ignore the first argument as it is the actual type - annotations = get_args(annotated_type)[1:] - for annotation in annotations: - if isinstance(annotation, PropertyInfo) and annotation.format is not None: - return _format_data(data, annotation.format, annotation.format_template) - - return data - - -def _format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: - if isinstance(data, (date, datetime)): - if format_ == "iso8601": - return data.isoformat() - - if format_ == "custom" and format_template is not None: - return data.strftime(format_template) - - if format_ == "base64" and is_base64_file_input(data): - binary: str | bytes | None = None - - if isinstance(data, pathlib.Path): - binary = data.read_bytes() - elif isinstance(data, io.IOBase): - binary = data.read() - - if isinstance(binary, str): # type: ignore[unreachable] - binary = binary.encode() - - if not isinstance(binary, bytes): - raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") - - return base64.b64encode(binary).decode("ascii") - - return data - - -def _transform_typeddict( - data: Mapping[str, object], - expected_type: type, -) -> Mapping[str, object]: - result: dict[str, object] = {} - annotations = get_type_hints(expected_type, include_extras=True) - for key, value in data.items(): - if not is_given(value): - # we don't need to include `NotGiven` values here as they'll - # be stripped out before the request is sent anyway - continue - - type_ = annotations.get(key) - if type_ is None: - # we do not have a type annotation for this field, leave it as is - result[key] = value - else: - result[_maybe_transform_key(key, type_)] = _transform_recursive(value, annotation=type_) - return result - - -async def async_maybe_transform( - data: object, - expected_type: object, -) -> Any | None: - """Wrapper over `async_transform()` that allows `None` to be passed. - - See `async_transform()` for more details. - """ - if data is None: - return None - return await async_transform(data, expected_type) - - -async def async_transform( - data: _T, - expected_type: object, -) -> _T: - """Transform dictionaries based off of type information from the given type, for example: - - ```py - class Params(TypedDict, total=False): - card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]] - - - transformed = transform({"card_id": ""}, Params) - # {'cardID': ''} - ``` - - Any keys / data that does not have type information given will be included as is. - - It should be noted that the transformations that this function does are not represented in the type system. - """ - transformed = await _async_transform_recursive(data, annotation=cast(type, expected_type)) - return cast(_T, transformed) - - -async def _async_transform_recursive( - data: object, - *, - annotation: type, - inner_type: type | None = None, -) -> object: - """Transform the given data against the expected type. - - Args: - annotation: The direct type annotation given to the particular piece of data. - This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc - - inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type - is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in - the list can be transformed using the metadata from the container type. - - Defaults to the same value as the `annotation` argument. - """ - if inner_type is None: - inner_type = annotation - - stripped_type = strip_annotated_type(inner_type) - origin = get_origin(stripped_type) or stripped_type - if is_typeddict(stripped_type) and is_mapping(data): - return await _async_transform_typeddict(data, stripped_type) - - if origin == dict and is_mapping(data): - items_type = get_args(stripped_type)[1] - return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} - - if ( - # List[T] - (is_list_type(stripped_type) and is_list(data)) - # Iterable[T] - or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) - ): - # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually - # intended as an iterable, so we don't transform it. - if isinstance(data, dict): - return cast(object, data) - - inner_type = extract_type_arg(stripped_type, 0) - if _no_transform_needed(inner_type): - # for some types there is no need to transform anything, so we can get a small - # perf boost from skipping that work. - # - # but we still need to convert to a list to ensure the data is json-serializable - if is_list(data): - return data - return list(data) - - return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] - - if is_union_type(stripped_type): - # For union types we run the transformation against all subtypes to ensure that everything is transformed. - # - # TODO: there may be edge cases where the same normalized field name will transform to two different names - # in different subtypes. - for subtype in get_args(stripped_type): - data = await _async_transform_recursive(data, annotation=annotation, inner_type=subtype) - return data - - if isinstance(data, pydantic.BaseModel): - return model_dump(data, exclude_unset=True, mode="json") - - annotated_type = _get_annotated_type(annotation) - if annotated_type is None: - return data - - # ignore the first argument as it is the actual type - annotations = get_args(annotated_type)[1:] - for annotation in annotations: - if isinstance(annotation, PropertyInfo) and annotation.format is not None: - return await _async_format_data(data, annotation.format, annotation.format_template) - - return data - - -async def _async_format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: - if isinstance(data, (date, datetime)): - if format_ == "iso8601": - return data.isoformat() - - if format_ == "custom" and format_template is not None: - return data.strftime(format_template) - - if format_ == "base64" and is_base64_file_input(data): - binary: str | bytes | None = None - - if isinstance(data, pathlib.Path): - binary = await anyio.Path(data).read_bytes() - elif isinstance(data, io.IOBase): - binary = data.read() - - if isinstance(binary, str): # type: ignore[unreachable] - binary = binary.encode() - - if not isinstance(binary, bytes): - raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") - - return base64.b64encode(binary).decode("ascii") - - return data - - -async def _async_transform_typeddict( - data: Mapping[str, object], - expected_type: type, -) -> Mapping[str, object]: - result: dict[str, object] = {} - annotations = get_type_hints(expected_type, include_extras=True) - for key, value in data.items(): - if not is_given(value): - # we don't need to include `NotGiven` values here as they'll - # be stripped out before the request is sent anyway - continue - - type_ = annotations.get(key) - if type_ is None: - # we do not have a type annotation for this field, leave it as is - result[key] = value - else: - result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_) - return result - - -@lru_cache(maxsize=8096) -def get_type_hints( - obj: Any, - globalns: dict[str, Any] | None = None, - localns: Mapping[str, Any] | None = None, - include_extras: bool = False, -) -> dict[str, Any]: - return _get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras) diff --git a/src/browser_use_sdk/_utils/_typing.py b/src/browser_use_sdk/_utils/_typing.py deleted file mode 100644 index 1bac954..0000000 --- a/src/browser_use_sdk/_utils/_typing.py +++ /dev/null @@ -1,151 +0,0 @@ -from __future__ import annotations - -import sys -import typing -import typing_extensions -from typing import Any, TypeVar, Iterable, cast -from collections import abc as _c_abc -from typing_extensions import ( - TypeIs, - Required, - Annotated, - get_args, - get_origin, -) - -from ._utils import lru_cache -from .._types import InheritsGeneric -from .._compat import is_union as _is_union - - -def is_annotated_type(typ: type) -> bool: - return get_origin(typ) == Annotated - - -def is_list_type(typ: type) -> bool: - return (get_origin(typ) or typ) == list - - -def is_iterable_type(typ: type) -> bool: - """If the given type is `typing.Iterable[T]`""" - origin = get_origin(typ) or typ - return origin == Iterable or origin == _c_abc.Iterable - - -def is_union_type(typ: type) -> bool: - return _is_union(get_origin(typ)) - - -def is_required_type(typ: type) -> bool: - return get_origin(typ) == Required - - -def is_typevar(typ: type) -> bool: - # type ignore is required because type checkers - # think this expression will always return False - return type(typ) == TypeVar # type: ignore - - -_TYPE_ALIAS_TYPES: tuple[type[typing_extensions.TypeAliasType], ...] = (typing_extensions.TypeAliasType,) -if sys.version_info >= (3, 12): - _TYPE_ALIAS_TYPES = (*_TYPE_ALIAS_TYPES, typing.TypeAliasType) - - -def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]: - """Return whether the provided argument is an instance of `TypeAliasType`. - - ```python - type Int = int - is_type_alias_type(Int) - # > True - Str = TypeAliasType("Str", str) - is_type_alias_type(Str) - # > True - ``` - """ - return isinstance(tp, _TYPE_ALIAS_TYPES) - - -# Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] -@lru_cache(maxsize=8096) -def strip_annotated_type(typ: type) -> type: - if is_required_type(typ) or is_annotated_type(typ): - return strip_annotated_type(cast(type, get_args(typ)[0])) - - return typ - - -def extract_type_arg(typ: type, index: int) -> type: - args = get_args(typ) - try: - return cast(type, args[index]) - except IndexError as err: - raise RuntimeError(f"Expected type {typ} to have a type argument at index {index} but it did not") from err - - -def extract_type_var_from_base( - typ: type, - *, - generic_bases: tuple[type, ...], - index: int, - failure_message: str | None = None, -) -> type: - """Given a type like `Foo[T]`, returns the generic type variable `T`. - - This also handles the case where a concrete subclass is given, e.g. - ```py - class MyResponse(Foo[bytes]): - ... - - extract_type_var(MyResponse, bases=(Foo,), index=0) -> bytes - ``` - - And where a generic subclass is given: - ```py - _T = TypeVar('_T') - class MyResponse(Foo[_T]): - ... - - extract_type_var(MyResponse[bytes], bases=(Foo,), index=0) -> bytes - ``` - """ - cls = cast(object, get_origin(typ) or typ) - if cls in generic_bases: # pyright: ignore[reportUnnecessaryContains] - # we're given the class directly - return extract_type_arg(typ, index) - - # if a subclass is given - # --- - # this is needed as __orig_bases__ is not present in the typeshed stubs - # because it is intended to be for internal use only, however there does - # not seem to be a way to resolve generic TypeVars for inherited subclasses - # without using it. - if isinstance(cls, InheritsGeneric): - target_base_class: Any | None = None - for base in cls.__orig_bases__: - if base.__origin__ in generic_bases: - target_base_class = base - break - - if target_base_class is None: - raise RuntimeError( - "Could not find the generic base class;\n" - "This should never happen;\n" - f"Does {cls} inherit from one of {generic_bases} ?" - ) - - extracted = extract_type_arg(target_base_class, index) - if is_typevar(extracted): - # If the extracted type argument is itself a type variable - # then that means the subclass itself is generic, so we have - # to resolve the type argument from the class itself, not - # the base class. - # - # Note: if there is more than 1 type argument, the subclass could - # change the ordering of the type arguments, this is not currently - # supported. - return extract_type_arg(typ, index) - - return extracted - - raise RuntimeError(failure_message or f"Could not resolve inner type variable at index {index} for {typ}") diff --git a/src/browser_use_sdk/_utils/_utils.py b/src/browser_use_sdk/_utils/_utils.py deleted file mode 100644 index ea3cf3f..0000000 --- a/src/browser_use_sdk/_utils/_utils.py +++ /dev/null @@ -1,422 +0,0 @@ -from __future__ import annotations - -import os -import re -import inspect -import functools -from typing import ( - Any, - Tuple, - Mapping, - TypeVar, - Callable, - Iterable, - Sequence, - cast, - overload, -) -from pathlib import Path -from datetime import date, datetime -from typing_extensions import TypeGuard - -import sniffio - -from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike -from .._compat import parse_date as parse_date, parse_datetime as parse_datetime - -_T = TypeVar("_T") -_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) -_MappingT = TypeVar("_MappingT", bound=Mapping[str, object]) -_SequenceT = TypeVar("_SequenceT", bound=Sequence[object]) -CallableT = TypeVar("CallableT", bound=Callable[..., Any]) - - -def flatten(t: Iterable[Iterable[_T]]) -> list[_T]: - return [item for sublist in t for item in sublist] - - -def extract_files( - # TODO: this needs to take Dict but variance issues..... - # create protocol type ? - query: Mapping[str, object], - *, - paths: Sequence[Sequence[str]], -) -> list[tuple[str, FileTypes]]: - """Recursively extract files from the given dictionary based on specified paths. - - A path may look like this ['foo', 'files', '', 'data']. - - Note: this mutates the given dictionary. - """ - files: list[tuple[str, FileTypes]] = [] - for path in paths: - files.extend(_extract_items(query, path, index=0, flattened_key=None)) - return files - - -def _extract_items( - obj: object, - path: Sequence[str], - *, - index: int, - flattened_key: str | None, -) -> list[tuple[str, FileTypes]]: - try: - key = path[index] - except IndexError: - if isinstance(obj, NotGiven): - # no value was provided - we can safely ignore - return [] - - # cyclical import - from .._files import assert_is_file_content - - # We have exhausted the path, return the entry we found. - assert flattened_key is not None - - if is_list(obj): - files: list[tuple[str, FileTypes]] = [] - for entry in obj: - assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "") - files.append((flattened_key + "[]", cast(FileTypes, entry))) - return files - - assert_is_file_content(obj, key=flattened_key) - return [(flattened_key, cast(FileTypes, obj))] - - index += 1 - if is_dict(obj): - try: - # We are at the last entry in the path so we must remove the field - if (len(path)) == index: - item = obj.pop(key) - else: - item = obj[key] - except KeyError: - # Key was not present in the dictionary, this is not indicative of an error - # as the given path may not point to a required field. We also do not want - # to enforce required fields as the API may differ from the spec in some cases. - return [] - if flattened_key is None: - flattened_key = key - else: - flattened_key += f"[{key}]" - return _extract_items( - item, - path, - index=index, - flattened_key=flattened_key, - ) - elif is_list(obj): - if key != "": - return [] - - return flatten( - [ - _extract_items( - item, - path, - index=index, - flattened_key=flattened_key + "[]" if flattened_key is not None else "[]", - ) - for item in obj - ] - ) - - # Something unexpected was passed, just ignore it. - return [] - - -def is_given(obj: NotGivenOr[_T]) -> TypeGuard[_T]: - return not isinstance(obj, NotGiven) - - -# Type safe methods for narrowing types with TypeVars. -# The default narrowing for isinstance(obj, dict) is dict[unknown, unknown], -# however this cause Pyright to rightfully report errors. As we know we don't -# care about the contained types we can safely use `object` in it's place. -# -# There are two separate functions defined, `is_*` and `is_*_t` for different use cases. -# `is_*` is for when you're dealing with an unknown input -# `is_*_t` is for when you're narrowing a known union type to a specific subset - - -def is_tuple(obj: object) -> TypeGuard[tuple[object, ...]]: - return isinstance(obj, tuple) - - -def is_tuple_t(obj: _TupleT | object) -> TypeGuard[_TupleT]: - return isinstance(obj, tuple) - - -def is_sequence(obj: object) -> TypeGuard[Sequence[object]]: - return isinstance(obj, Sequence) - - -def is_sequence_t(obj: _SequenceT | object) -> TypeGuard[_SequenceT]: - return isinstance(obj, Sequence) - - -def is_mapping(obj: object) -> TypeGuard[Mapping[str, object]]: - return isinstance(obj, Mapping) - - -def is_mapping_t(obj: _MappingT | object) -> TypeGuard[_MappingT]: - return isinstance(obj, Mapping) - - -def is_dict(obj: object) -> TypeGuard[dict[object, object]]: - return isinstance(obj, dict) - - -def is_list(obj: object) -> TypeGuard[list[object]]: - return isinstance(obj, list) - - -def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: - return isinstance(obj, Iterable) - - -def deepcopy_minimal(item: _T) -> _T: - """Minimal reimplementation of copy.deepcopy() that will only copy certain object types: - - - mappings, e.g. `dict` - - list - - This is done for performance reasons. - """ - if is_mapping(item): - return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()}) - if is_list(item): - return cast(_T, [deepcopy_minimal(entry) for entry in item]) - return item - - -# copied from https://github.com/Rapptz/RoboDanny -def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: - size = len(seq) - if size == 0: - return "" - - if size == 1: - return seq[0] - - if size == 2: - return f"{seq[0]} {final} {seq[1]}" - - return delim.join(seq[:-1]) + f" {final} {seq[-1]}" - - -def quote(string: str) -> str: - """Add single quotation marks around the given string. Does *not* do any escaping.""" - return f"'{string}'" - - -def required_args(*variants: Sequence[str]) -> Callable[[CallableT], CallableT]: - """Decorator to enforce a given set of arguments or variants of arguments are passed to the decorated function. - - Useful for enforcing runtime validation of overloaded functions. - - Example usage: - ```py - @overload - def foo(*, a: str) -> str: ... - - - @overload - def foo(*, b: bool) -> str: ... - - - # This enforces the same constraints that a static type checker would - # i.e. that either a or b must be passed to the function - @required_args(["a"], ["b"]) - def foo(*, a: str | None = None, b: bool | None = None) -> str: ... - ``` - """ - - def inner(func: CallableT) -> CallableT: - params = inspect.signature(func).parameters - positional = [ - name - for name, param in params.items() - if param.kind - in { - param.POSITIONAL_ONLY, - param.POSITIONAL_OR_KEYWORD, - } - ] - - @functools.wraps(func) - def wrapper(*args: object, **kwargs: object) -> object: - given_params: set[str] = set() - for i, _ in enumerate(args): - try: - given_params.add(positional[i]) - except IndexError: - raise TypeError( - f"{func.__name__}() takes {len(positional)} argument(s) but {len(args)} were given" - ) from None - - for key in kwargs.keys(): - given_params.add(key) - - for variant in variants: - matches = all((param in given_params for param in variant)) - if matches: - break - else: # no break - if len(variants) > 1: - variations = human_join( - ["(" + human_join([quote(arg) for arg in variant], final="and") + ")" for variant in variants] - ) - msg = f"Missing required arguments; Expected either {variations} arguments to be given" - else: - assert len(variants) > 0 - - # TODO: this error message is not deterministic - missing = list(set(variants[0]) - given_params) - if len(missing) > 1: - msg = f"Missing required arguments: {human_join([quote(arg) for arg in missing])}" - else: - msg = f"Missing required argument: {quote(missing[0])}" - raise TypeError(msg) - return func(*args, **kwargs) - - return wrapper # type: ignore - - return inner - - -_K = TypeVar("_K") -_V = TypeVar("_V") - - -@overload -def strip_not_given(obj: None) -> None: ... - - -@overload -def strip_not_given(obj: Mapping[_K, _V | NotGiven]) -> dict[_K, _V]: ... - - -@overload -def strip_not_given(obj: object) -> object: ... - - -def strip_not_given(obj: object | None) -> object: - """Remove all top-level keys where their values are instances of `NotGiven`""" - if obj is None: - return None - - if not is_mapping(obj): - return obj - - return {key: value for key, value in obj.items() if not isinstance(value, NotGiven)} - - -def coerce_integer(val: str) -> int: - return int(val, base=10) - - -def coerce_float(val: str) -> float: - return float(val) - - -def coerce_boolean(val: str) -> bool: - return val == "true" or val == "1" or val == "on" - - -def maybe_coerce_integer(val: str | None) -> int | None: - if val is None: - return None - return coerce_integer(val) - - -def maybe_coerce_float(val: str | None) -> float | None: - if val is None: - return None - return coerce_float(val) - - -def maybe_coerce_boolean(val: str | None) -> bool | None: - if val is None: - return None - return coerce_boolean(val) - - -def removeprefix(string: str, prefix: str) -> str: - """Remove a prefix from a string. - - Backport of `str.removeprefix` for Python < 3.9 - """ - if string.startswith(prefix): - return string[len(prefix) :] - return string - - -def removesuffix(string: str, suffix: str) -> str: - """Remove a suffix from a string. - - Backport of `str.removesuffix` for Python < 3.9 - """ - if string.endswith(suffix): - return string[: -len(suffix)] - return string - - -def file_from_path(path: str) -> FileTypes: - contents = Path(path).read_bytes() - file_name = os.path.basename(path) - return (file_name, contents) - - -def get_required_header(headers: HeadersLike, header: str) -> str: - lower_header = header.lower() - if is_mapping_t(headers): - # mypy doesn't understand the type narrowing here - for k, v in headers.items(): # type: ignore - if k.lower() == lower_header and isinstance(v, str): - return v - - # to deal with the case where the header looks like Stainless-Event-Id - intercaps_header = re.sub(r"([^\w])(\w)", lambda pat: pat.group(1) + pat.group(2).upper(), header.capitalize()) - - for normalized_header in [header, lower_header, header.upper(), intercaps_header]: - value = headers.get(normalized_header) - if value: - return value - - raise ValueError(f"Could not find {header} header") - - -def get_async_library() -> str: - try: - return sniffio.current_async_library() - except Exception: - return "false" - - -def lru_cache(*, maxsize: int | None = 128) -> Callable[[CallableT], CallableT]: - """A version of functools.lru_cache that retains the type signature - for the wrapped function arguments. - """ - wrapper = functools.lru_cache( # noqa: TID251 - maxsize=maxsize, - ) - return cast(Any, wrapper) # type: ignore[no-any-return] - - -def json_safe(data: object) -> object: - """Translates a mapping / sequence recursively in the same fashion - as `pydantic` v2's `model_dump(mode="json")`. - """ - if is_mapping(data): - return {json_safe(key): json_safe(value) for key, value in data.items()} - - if is_iterable(data) and not isinstance(data, (str, bytes, bytearray)): - return [json_safe(item) for item in data] - - if isinstance(data, (datetime, date)): - return data.isoformat() - - return data diff --git a/src/browser_use_sdk/_version.py b/src/browser_use_sdk/_version.py deleted file mode 100644 index dbca030..0000000 --- a/src/browser_use_sdk/_version.py +++ /dev/null @@ -1,4 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -__title__ = "browser_use_sdk" -__version__ = "1.0.2" # x-release-please-version diff --git a/src/browser_use_sdk/lib/.keep b/src/browser_use_sdk/lib/.keep deleted file mode 100644 index 5e2c99f..0000000 --- a/src/browser_use_sdk/lib/.keep +++ /dev/null @@ -1,4 +0,0 @@ -File generated from our OpenAPI spec by Stainless. - -This directory can be used to store custom files to expand the SDK. -It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/src/browser_use_sdk/lib/parse.py b/src/browser_use_sdk/lib/parse.py deleted file mode 100644 index b11e44e..0000000 --- a/src/browser_use_sdk/lib/parse.py +++ /dev/null @@ -1,35 +0,0 @@ -import json -import hashlib -from typing import Any, Union, Generic, TypeVar -from datetime import datetime - -from pydantic import BaseModel - -from browser_use_sdk.types.task_view import TaskView - -T = TypeVar("T", bound=BaseModel) - - -class TaskViewWithOutput(TaskView, Generic[T]): - """ - TaskView with structured output. - """ - - parsed_output: Union[T, None] - - -class CustomJSONEncoder(json.JSONEncoder): - """Custom JSON encoder to handle datetime objects.""" - - # NOTE: Python doesn't have the override decorator in 3.8, that's why we ignore it. - def default(self, o: Any) -> Any: # type: ignore[override] - if isinstance(o, datetime): - return o.isoformat() - return super().default(o) - - -def hash_task_view(task_view: TaskView) -> str: - """Hashes the task view to detect changes.""" - return hashlib.sha256( - json.dumps(task_view.model_dump(), sort_keys=True, cls=CustomJSONEncoder).encode() - ).hexdigest() diff --git a/src/browser_use_sdk/resources/__init__.py b/src/browser_use_sdk/resources/__init__.py deleted file mode 100644 index c8d13bb..0000000 --- a/src/browser_use_sdk/resources/__init__.py +++ /dev/null @@ -1,75 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .tasks import ( - TasksResource, - AsyncTasksResource, - TasksResourceWithRawResponse, - AsyncTasksResourceWithRawResponse, - TasksResourceWithStreamingResponse, - AsyncTasksResourceWithStreamingResponse, -) -from .users import ( - UsersResource, - AsyncUsersResource, - UsersResourceWithRawResponse, - AsyncUsersResourceWithRawResponse, - UsersResourceWithStreamingResponse, - AsyncUsersResourceWithStreamingResponse, -) -from .sessions import ( - SessionsResource, - AsyncSessionsResource, - SessionsResourceWithRawResponse, - AsyncSessionsResourceWithRawResponse, - SessionsResourceWithStreamingResponse, - AsyncSessionsResourceWithStreamingResponse, -) -from .agent_profiles import ( - AgentProfilesResource, - AsyncAgentProfilesResource, - AgentProfilesResourceWithRawResponse, - AsyncAgentProfilesResourceWithRawResponse, - AgentProfilesResourceWithStreamingResponse, - AsyncAgentProfilesResourceWithStreamingResponse, -) -from .browser_profiles import ( - BrowserProfilesResource, - AsyncBrowserProfilesResource, - BrowserProfilesResourceWithRawResponse, - AsyncBrowserProfilesResourceWithRawResponse, - BrowserProfilesResourceWithStreamingResponse, - AsyncBrowserProfilesResourceWithStreamingResponse, -) - -__all__ = [ - "UsersResource", - "AsyncUsersResource", - "UsersResourceWithRawResponse", - "AsyncUsersResourceWithRawResponse", - "UsersResourceWithStreamingResponse", - "AsyncUsersResourceWithStreamingResponse", - "TasksResource", - "AsyncTasksResource", - "TasksResourceWithRawResponse", - "AsyncTasksResourceWithRawResponse", - "TasksResourceWithStreamingResponse", - "AsyncTasksResourceWithStreamingResponse", - "SessionsResource", - "AsyncSessionsResource", - "SessionsResourceWithRawResponse", - "AsyncSessionsResourceWithRawResponse", - "SessionsResourceWithStreamingResponse", - "AsyncSessionsResourceWithStreamingResponse", - "BrowserProfilesResource", - "AsyncBrowserProfilesResource", - "BrowserProfilesResourceWithRawResponse", - "AsyncBrowserProfilesResourceWithRawResponse", - "BrowserProfilesResourceWithStreamingResponse", - "AsyncBrowserProfilesResourceWithStreamingResponse", - "AgentProfilesResource", - "AsyncAgentProfilesResource", - "AgentProfilesResourceWithRawResponse", - "AsyncAgentProfilesResourceWithRawResponse", - "AgentProfilesResourceWithStreamingResponse", - "AsyncAgentProfilesResourceWithStreamingResponse", -] diff --git a/src/browser_use_sdk/resources/agent_profiles.py b/src/browser_use_sdk/resources/agent_profiles.py deleted file mode 100644 index b596dbc..0000000 --- a/src/browser_use_sdk/resources/agent_profiles.py +++ /dev/null @@ -1,748 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import List, Optional - -import httpx - -from ..types import agent_profile_list_params, agent_profile_create_params, agent_profile_update_params -from .._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven -from .._utils import maybe_transform, async_maybe_transform -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from .._base_client import make_request_options -from ..types.agent_profile_view import AgentProfileView -from ..types.agent_profile_list_response import AgentProfileListResponse - -__all__ = ["AgentProfilesResource", "AsyncAgentProfilesResource"] - - -class AgentProfilesResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> AgentProfilesResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return AgentProfilesResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AgentProfilesResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return AgentProfilesResourceWithStreamingResponse(self) - - def create( - self, - *, - name: str, - allowed_domains: List[str] | NotGiven = NOT_GIVEN, - custom_system_prompt_extension: str | NotGiven = NOT_GIVEN, - description: str | NotGiven = NOT_GIVEN, - flash_mode: bool | NotGiven = NOT_GIVEN, - highlight_elements: bool | NotGiven = NOT_GIVEN, - max_agent_steps: int | NotGiven = NOT_GIVEN, - thinking: bool | NotGiven = NOT_GIVEN, - vision: bool | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AgentProfileView: - """ - Create a new agent profile for the authenticated user. - - Agent profiles define how your AI agents behave during tasks. You can create - multiple profiles for different use cases (e.g., customer support, data - analysis, web scraping). Free users can create 1 profile; paid users can create - unlimited profiles. - - Key features you can configure: - - - System prompt: The core instructions that define the agent's personality and - behavior - - Allowed domains: Restrict which websites the agent can access - - Max steps: Limit how many actions the agent can take in a single task - - Vision: Enable/disable the agent's ability to see and analyze screenshots - - Thinking: Enable/disable the agent's reasoning process - - Args: - - - request: The agent profile configuration including name, description, and - behavior settings - - Returns: - - - The newly created agent profile with all its details - - Raises: - - - 402: If user needs a subscription to create additional profiles - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._post( - "/agent-profiles", - body=maybe_transform( - { - "name": name, - "allowed_domains": allowed_domains, - "custom_system_prompt_extension": custom_system_prompt_extension, - "description": description, - "flash_mode": flash_mode, - "highlight_elements": highlight_elements, - "max_agent_steps": max_agent_steps, - "thinking": thinking, - "vision": vision, - }, - agent_profile_create_params.AgentProfileCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentProfileView, - ) - - def retrieve( - self, - profile_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AgentProfileView: - """ - Get a specific agent profile by its ID. - - Retrieves the complete details of an agent profile, including all its - configuration settings like system prompts, allowed domains, and behavior flags. - - Args: - - - profile_id: The unique identifier of the agent profile - - Returns: - - - Complete agent profile information - - Raises: - - - 404: If the user agent profile doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - return self._get( - f"/agent-profiles/{profile_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentProfileView, - ) - - def update( - self, - profile_id: str, - *, - allowed_domains: Optional[List[str]] | NotGiven = NOT_GIVEN, - custom_system_prompt_extension: Optional[str] | NotGiven = NOT_GIVEN, - description: Optional[str] | NotGiven = NOT_GIVEN, - flash_mode: Optional[bool] | NotGiven = NOT_GIVEN, - highlight_elements: Optional[bool] | NotGiven = NOT_GIVEN, - max_agent_steps: Optional[int] | NotGiven = NOT_GIVEN, - name: Optional[str] | NotGiven = NOT_GIVEN, - thinking: Optional[bool] | NotGiven = NOT_GIVEN, - vision: Optional[bool] | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AgentProfileView: - """ - Update an existing agent profile. - - Modify any aspect of an agent profile, such as its name, description, system - prompt, or behavior settings. Only the fields you provide will be updated; other - fields remain unchanged. - - Args: - - - profile_id: The unique identifier of the agent profile to update - - request: The fields to update (only provided fields will be changed) - - Returns: - - - The updated agent profile with all its current details - - Raises: - - - 404: If the user agent profile doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - return self._patch( - f"/agent-profiles/{profile_id}", - body=maybe_transform( - { - "allowed_domains": allowed_domains, - "custom_system_prompt_extension": custom_system_prompt_extension, - "description": description, - "flash_mode": flash_mode, - "highlight_elements": highlight_elements, - "max_agent_steps": max_agent_steps, - "name": name, - "thinking": thinking, - "vision": vision, - }, - agent_profile_update_params.AgentProfileUpdateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentProfileView, - ) - - def list( - self, - *, - page_number: int | NotGiven = NOT_GIVEN, - page_size: int | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AgentProfileListResponse: - """ - Get a paginated list of all agent profiles for the authenticated user. - - Agent profiles define how your AI agents behave, including their personality, - capabilities, and limitations. Use this endpoint to see all your configured - agent profiles. - - Returns: - - - A paginated list of agent profiles - - Total count of profiles - - Page information for navigation - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._get( - "/agent-profiles", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "page_number": page_number, - "page_size": page_size, - }, - agent_profile_list_params.AgentProfileListParams, - ), - ), - cast_to=AgentProfileListResponse, - ) - - def delete( - self, - profile_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> None: - """ - Delete an agent profile. - - Permanently removes an agent profile and all its configuration. This action - cannot be undone. Any tasks that were using this profile will continue to work, - but you won't be able to create new tasks with the deleted profile. - - Args: - - - profile_id: The unique identifier of the agent profile to delete - - Returns: - - - 204 No Content on successful deletion (idempotent) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._delete( - f"/agent-profiles/{profile_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class AsyncAgentProfilesResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncAgentProfilesResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return AsyncAgentProfilesResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncAgentProfilesResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return AsyncAgentProfilesResourceWithStreamingResponse(self) - - async def create( - self, - *, - name: str, - allowed_domains: List[str] | NotGiven = NOT_GIVEN, - custom_system_prompt_extension: str | NotGiven = NOT_GIVEN, - description: str | NotGiven = NOT_GIVEN, - flash_mode: bool | NotGiven = NOT_GIVEN, - highlight_elements: bool | NotGiven = NOT_GIVEN, - max_agent_steps: int | NotGiven = NOT_GIVEN, - thinking: bool | NotGiven = NOT_GIVEN, - vision: bool | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AgentProfileView: - """ - Create a new agent profile for the authenticated user. - - Agent profiles define how your AI agents behave during tasks. You can create - multiple profiles for different use cases (e.g., customer support, data - analysis, web scraping). Free users can create 1 profile; paid users can create - unlimited profiles. - - Key features you can configure: - - - System prompt: The core instructions that define the agent's personality and - behavior - - Allowed domains: Restrict which websites the agent can access - - Max steps: Limit how many actions the agent can take in a single task - - Vision: Enable/disable the agent's ability to see and analyze screenshots - - Thinking: Enable/disable the agent's reasoning process - - Args: - - - request: The agent profile configuration including name, description, and - behavior settings - - Returns: - - - The newly created agent profile with all its details - - Raises: - - - 402: If user needs a subscription to create additional profiles - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return await self._post( - "/agent-profiles", - body=await async_maybe_transform( - { - "name": name, - "allowed_domains": allowed_domains, - "custom_system_prompt_extension": custom_system_prompt_extension, - "description": description, - "flash_mode": flash_mode, - "highlight_elements": highlight_elements, - "max_agent_steps": max_agent_steps, - "thinking": thinking, - "vision": vision, - }, - agent_profile_create_params.AgentProfileCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentProfileView, - ) - - async def retrieve( - self, - profile_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AgentProfileView: - """ - Get a specific agent profile by its ID. - - Retrieves the complete details of an agent profile, including all its - configuration settings like system prompts, allowed domains, and behavior flags. - - Args: - - - profile_id: The unique identifier of the agent profile - - Returns: - - - Complete agent profile information - - Raises: - - - 404: If the user agent profile doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - return await self._get( - f"/agent-profiles/{profile_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentProfileView, - ) - - async def update( - self, - profile_id: str, - *, - allowed_domains: Optional[List[str]] | NotGiven = NOT_GIVEN, - custom_system_prompt_extension: Optional[str] | NotGiven = NOT_GIVEN, - description: Optional[str] | NotGiven = NOT_GIVEN, - flash_mode: Optional[bool] | NotGiven = NOT_GIVEN, - highlight_elements: Optional[bool] | NotGiven = NOT_GIVEN, - max_agent_steps: Optional[int] | NotGiven = NOT_GIVEN, - name: Optional[str] | NotGiven = NOT_GIVEN, - thinking: Optional[bool] | NotGiven = NOT_GIVEN, - vision: Optional[bool] | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AgentProfileView: - """ - Update an existing agent profile. - - Modify any aspect of an agent profile, such as its name, description, system - prompt, or behavior settings. Only the fields you provide will be updated; other - fields remain unchanged. - - Args: - - - profile_id: The unique identifier of the agent profile to update - - request: The fields to update (only provided fields will be changed) - - Returns: - - - The updated agent profile with all its current details - - Raises: - - - 404: If the user agent profile doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - return await self._patch( - f"/agent-profiles/{profile_id}", - body=await async_maybe_transform( - { - "allowed_domains": allowed_domains, - "custom_system_prompt_extension": custom_system_prompt_extension, - "description": description, - "flash_mode": flash_mode, - "highlight_elements": highlight_elements, - "max_agent_steps": max_agent_steps, - "name": name, - "thinking": thinking, - "vision": vision, - }, - agent_profile_update_params.AgentProfileUpdateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AgentProfileView, - ) - - async def list( - self, - *, - page_number: int | NotGiven = NOT_GIVEN, - page_size: int | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AgentProfileListResponse: - """ - Get a paginated list of all agent profiles for the authenticated user. - - Agent profiles define how your AI agents behave, including their personality, - capabilities, and limitations. Use this endpoint to see all your configured - agent profiles. - - Returns: - - - A paginated list of agent profiles - - Total count of profiles - - Page information for navigation - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return await self._get( - "/agent-profiles", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform( - { - "page_number": page_number, - "page_size": page_size, - }, - agent_profile_list_params.AgentProfileListParams, - ), - ), - cast_to=AgentProfileListResponse, - ) - - async def delete( - self, - profile_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> None: - """ - Delete an agent profile. - - Permanently removes an agent profile and all its configuration. This action - cannot be undone. Any tasks that were using this profile will continue to work, - but you won't be able to create new tasks with the deleted profile. - - Args: - - - profile_id: The unique identifier of the agent profile to delete - - Returns: - - - 204 No Content on successful deletion (idempotent) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._delete( - f"/agent-profiles/{profile_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class AgentProfilesResourceWithRawResponse: - def __init__(self, agent_profiles: AgentProfilesResource) -> None: - self._agent_profiles = agent_profiles - - self.create = to_raw_response_wrapper( - agent_profiles.create, - ) - self.retrieve = to_raw_response_wrapper( - agent_profiles.retrieve, - ) - self.update = to_raw_response_wrapper( - agent_profiles.update, - ) - self.list = to_raw_response_wrapper( - agent_profiles.list, - ) - self.delete = to_raw_response_wrapper( - agent_profiles.delete, - ) - - -class AsyncAgentProfilesResourceWithRawResponse: - def __init__(self, agent_profiles: AsyncAgentProfilesResource) -> None: - self._agent_profiles = agent_profiles - - self.create = async_to_raw_response_wrapper( - agent_profiles.create, - ) - self.retrieve = async_to_raw_response_wrapper( - agent_profiles.retrieve, - ) - self.update = async_to_raw_response_wrapper( - agent_profiles.update, - ) - self.list = async_to_raw_response_wrapper( - agent_profiles.list, - ) - self.delete = async_to_raw_response_wrapper( - agent_profiles.delete, - ) - - -class AgentProfilesResourceWithStreamingResponse: - def __init__(self, agent_profiles: AgentProfilesResource) -> None: - self._agent_profiles = agent_profiles - - self.create = to_streamed_response_wrapper( - agent_profiles.create, - ) - self.retrieve = to_streamed_response_wrapper( - agent_profiles.retrieve, - ) - self.update = to_streamed_response_wrapper( - agent_profiles.update, - ) - self.list = to_streamed_response_wrapper( - agent_profiles.list, - ) - self.delete = to_streamed_response_wrapper( - agent_profiles.delete, - ) - - -class AsyncAgentProfilesResourceWithStreamingResponse: - def __init__(self, agent_profiles: AsyncAgentProfilesResource) -> None: - self._agent_profiles = agent_profiles - - self.create = async_to_streamed_response_wrapper( - agent_profiles.create, - ) - self.retrieve = async_to_streamed_response_wrapper( - agent_profiles.retrieve, - ) - self.update = async_to_streamed_response_wrapper( - agent_profiles.update, - ) - self.list = async_to_streamed_response_wrapper( - agent_profiles.list, - ) - self.delete = async_to_streamed_response_wrapper( - agent_profiles.delete, - ) diff --git a/src/browser_use_sdk/resources/browser_profiles.py b/src/browser_use_sdk/resources/browser_profiles.py deleted file mode 100644 index 3a8b417..0000000 --- a/src/browser_use_sdk/resources/browser_profiles.py +++ /dev/null @@ -1,764 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Optional - -import httpx - -from ..types import ( - ProxyCountryCode, - browser_profile_list_params, - browser_profile_create_params, - browser_profile_update_params, -) -from .._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven -from .._utils import maybe_transform, async_maybe_transform -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from .._base_client import make_request_options -from ..types.proxy_country_code import ProxyCountryCode -from ..types.browser_profile_view import BrowserProfileView -from ..types.browser_profile_list_response import BrowserProfileListResponse - -__all__ = ["BrowserProfilesResource", "AsyncBrowserProfilesResource"] - - -class BrowserProfilesResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> BrowserProfilesResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return BrowserProfilesResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> BrowserProfilesResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return BrowserProfilesResourceWithStreamingResponse(self) - - def create( - self, - *, - name: str, - ad_blocker: bool | NotGiven = NOT_GIVEN, - browser_viewport_height: int | NotGiven = NOT_GIVEN, - browser_viewport_width: int | NotGiven = NOT_GIVEN, - description: str | NotGiven = NOT_GIVEN, - is_mobile: bool | NotGiven = NOT_GIVEN, - persist: bool | NotGiven = NOT_GIVEN, - proxy: bool | NotGiven = NOT_GIVEN, - proxy_country_code: ProxyCountryCode | NotGiven = NOT_GIVEN, - store_cache: bool | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BrowserProfileView: - """ - Create a new browser profile for the authenticated user. - - Browser profiles define how your web browsers behave during AI agent tasks. You - can create multiple profiles for different use cases (e.g., mobile testing, - desktop browsing, proxy-enabled scraping). Free users can create up to 10 - profiles; paid users can create unlimited profiles. - - Key features you can configure: - - - Viewport dimensions: Set the browser window size for consistent rendering - - Mobile emulation: Enable mobile device simulation - - Proxy settings: Route traffic through specific locations or proxy servers - - Ad blocking: Enable/disable ad blocking for cleaner browsing - - Cache persistence: Choose whether to save browser data between sessions - - Args: - - - request: The browser profile configuration including name, description, and - browser settings - - Returns: - - - The newly created browser profile with all its details - - Raises: - - - 402: If user needs a subscription to create additional profiles - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._post( - "/browser-profiles", - body=maybe_transform( - { - "name": name, - "ad_blocker": ad_blocker, - "browser_viewport_height": browser_viewport_height, - "browser_viewport_width": browser_viewport_width, - "description": description, - "is_mobile": is_mobile, - "persist": persist, - "proxy": proxy, - "proxy_country_code": proxy_country_code, - "store_cache": store_cache, - }, - browser_profile_create_params.BrowserProfileCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BrowserProfileView, - ) - - def retrieve( - self, - profile_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BrowserProfileView: - """ - Get a specific browser profile by its ID. - - Retrieves the complete details of a browser profile, including all its - configuration settings like viewport dimensions, proxy settings, and behavior - flags. - - Args: - - - profile_id: The unique identifier of the browser profile - - Returns: - - - Complete browser profile information - - Raises: - - - 404: If the user browser profile doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - return self._get( - f"/browser-profiles/{profile_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BrowserProfileView, - ) - - def update( - self, - profile_id: str, - *, - ad_blocker: Optional[bool] | NotGiven = NOT_GIVEN, - browser_viewport_height: Optional[int] | NotGiven = NOT_GIVEN, - browser_viewport_width: Optional[int] | NotGiven = NOT_GIVEN, - description: Optional[str] | NotGiven = NOT_GIVEN, - is_mobile: Optional[bool] | NotGiven = NOT_GIVEN, - name: Optional[str] | NotGiven = NOT_GIVEN, - persist: Optional[bool] | NotGiven = NOT_GIVEN, - proxy: Optional[bool] | NotGiven = NOT_GIVEN, - proxy_country_code: Optional[ProxyCountryCode] | NotGiven = NOT_GIVEN, - store_cache: Optional[bool] | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BrowserProfileView: - """ - Update an existing browser profile. - - Modify any aspect of a browser profile, such as its name, description, viewport - settings, or proxy configuration. Only the fields you provide will be updated; - other fields remain unchanged. - - Args: - - - profile_id: The unique identifier of the browser profile to update - - request: The fields to update (only provided fields will be changed) - - Returns: - - - The updated browser profile with all its current details - - Raises: - - - 404: If the user browser profile doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - return self._patch( - f"/browser-profiles/{profile_id}", - body=maybe_transform( - { - "ad_blocker": ad_blocker, - "browser_viewport_height": browser_viewport_height, - "browser_viewport_width": browser_viewport_width, - "description": description, - "is_mobile": is_mobile, - "name": name, - "persist": persist, - "proxy": proxy, - "proxy_country_code": proxy_country_code, - "store_cache": store_cache, - }, - browser_profile_update_params.BrowserProfileUpdateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BrowserProfileView, - ) - - def list( - self, - *, - page_number: int | NotGiven = NOT_GIVEN, - page_size: int | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BrowserProfileListResponse: - """ - Get a paginated list of all browser profiles for the authenticated user. - - Browser profiles define how your web browsers behave during AI agent tasks, - including settings like viewport size, mobile emulation, proxy configuration, - and ad blocking. Use this endpoint to see all your configured browser profiles. - - Returns: - - - A paginated list of browser profiles - - Total count of profiles - - Page information for navigation - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._get( - "/browser-profiles", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "page_number": page_number, - "page_size": page_size, - }, - browser_profile_list_params.BrowserProfileListParams, - ), - ), - cast_to=BrowserProfileListResponse, - ) - - def delete( - self, - profile_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> None: - """ - Delete a browser profile. - - Permanently removes a browser profile and all its configuration. This action - cannot be undone. The profile will also be removed from the browser service. Any - active sessions using this profile will continue to work, but you won't be able - to create new sessions with the deleted profile. - - Args: - - - profile_id: The unique identifier of the browser profile to delete - - Returns: - - - 204 No Content on successful deletion (idempotent) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._delete( - f"/browser-profiles/{profile_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class AsyncBrowserProfilesResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncBrowserProfilesResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return AsyncBrowserProfilesResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncBrowserProfilesResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return AsyncBrowserProfilesResourceWithStreamingResponse(self) - - async def create( - self, - *, - name: str, - ad_blocker: bool | NotGiven = NOT_GIVEN, - browser_viewport_height: int | NotGiven = NOT_GIVEN, - browser_viewport_width: int | NotGiven = NOT_GIVEN, - description: str | NotGiven = NOT_GIVEN, - is_mobile: bool | NotGiven = NOT_GIVEN, - persist: bool | NotGiven = NOT_GIVEN, - proxy: bool | NotGiven = NOT_GIVEN, - proxy_country_code: ProxyCountryCode | NotGiven = NOT_GIVEN, - store_cache: bool | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BrowserProfileView: - """ - Create a new browser profile for the authenticated user. - - Browser profiles define how your web browsers behave during AI agent tasks. You - can create multiple profiles for different use cases (e.g., mobile testing, - desktop browsing, proxy-enabled scraping). Free users can create up to 10 - profiles; paid users can create unlimited profiles. - - Key features you can configure: - - - Viewport dimensions: Set the browser window size for consistent rendering - - Mobile emulation: Enable mobile device simulation - - Proxy settings: Route traffic through specific locations or proxy servers - - Ad blocking: Enable/disable ad blocking for cleaner browsing - - Cache persistence: Choose whether to save browser data between sessions - - Args: - - - request: The browser profile configuration including name, description, and - browser settings - - Returns: - - - The newly created browser profile with all its details - - Raises: - - - 402: If user needs a subscription to create additional profiles - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return await self._post( - "/browser-profiles", - body=await async_maybe_transform( - { - "name": name, - "ad_blocker": ad_blocker, - "browser_viewport_height": browser_viewport_height, - "browser_viewport_width": browser_viewport_width, - "description": description, - "is_mobile": is_mobile, - "persist": persist, - "proxy": proxy, - "proxy_country_code": proxy_country_code, - "store_cache": store_cache, - }, - browser_profile_create_params.BrowserProfileCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BrowserProfileView, - ) - - async def retrieve( - self, - profile_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BrowserProfileView: - """ - Get a specific browser profile by its ID. - - Retrieves the complete details of a browser profile, including all its - configuration settings like viewport dimensions, proxy settings, and behavior - flags. - - Args: - - - profile_id: The unique identifier of the browser profile - - Returns: - - - Complete browser profile information - - Raises: - - - 404: If the user browser profile doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - return await self._get( - f"/browser-profiles/{profile_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BrowserProfileView, - ) - - async def update( - self, - profile_id: str, - *, - ad_blocker: Optional[bool] | NotGiven = NOT_GIVEN, - browser_viewport_height: Optional[int] | NotGiven = NOT_GIVEN, - browser_viewport_width: Optional[int] | NotGiven = NOT_GIVEN, - description: Optional[str] | NotGiven = NOT_GIVEN, - is_mobile: Optional[bool] | NotGiven = NOT_GIVEN, - name: Optional[str] | NotGiven = NOT_GIVEN, - persist: Optional[bool] | NotGiven = NOT_GIVEN, - proxy: Optional[bool] | NotGiven = NOT_GIVEN, - proxy_country_code: Optional[ProxyCountryCode] | NotGiven = NOT_GIVEN, - store_cache: Optional[bool] | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BrowserProfileView: - """ - Update an existing browser profile. - - Modify any aspect of a browser profile, such as its name, description, viewport - settings, or proxy configuration. Only the fields you provide will be updated; - other fields remain unchanged. - - Args: - - - profile_id: The unique identifier of the browser profile to update - - request: The fields to update (only provided fields will be changed) - - Returns: - - - The updated browser profile with all its current details - - Raises: - - - 404: If the user browser profile doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - return await self._patch( - f"/browser-profiles/{profile_id}", - body=await async_maybe_transform( - { - "ad_blocker": ad_blocker, - "browser_viewport_height": browser_viewport_height, - "browser_viewport_width": browser_viewport_width, - "description": description, - "is_mobile": is_mobile, - "name": name, - "persist": persist, - "proxy": proxy, - "proxy_country_code": proxy_country_code, - "store_cache": store_cache, - }, - browser_profile_update_params.BrowserProfileUpdateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BrowserProfileView, - ) - - async def list( - self, - *, - page_number: int | NotGiven = NOT_GIVEN, - page_size: int | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BrowserProfileListResponse: - """ - Get a paginated list of all browser profiles for the authenticated user. - - Browser profiles define how your web browsers behave during AI agent tasks, - including settings like viewport size, mobile emulation, proxy configuration, - and ad blocking. Use this endpoint to see all your configured browser profiles. - - Returns: - - - A paginated list of browser profiles - - Total count of profiles - - Page information for navigation - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return await self._get( - "/browser-profiles", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform( - { - "page_number": page_number, - "page_size": page_size, - }, - browser_profile_list_params.BrowserProfileListParams, - ), - ), - cast_to=BrowserProfileListResponse, - ) - - async def delete( - self, - profile_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> None: - """ - Delete a browser profile. - - Permanently removes a browser profile and all its configuration. This action - cannot be undone. The profile will also be removed from the browser service. Any - active sessions using this profile will continue to work, but you won't be able - to create new sessions with the deleted profile. - - Args: - - - profile_id: The unique identifier of the browser profile to delete - - Returns: - - - 204 No Content on successful deletion (idempotent) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not profile_id: - raise ValueError(f"Expected a non-empty value for `profile_id` but received {profile_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._delete( - f"/browser-profiles/{profile_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class BrowserProfilesResourceWithRawResponse: - def __init__(self, browser_profiles: BrowserProfilesResource) -> None: - self._browser_profiles = browser_profiles - - self.create = to_raw_response_wrapper( - browser_profiles.create, - ) - self.retrieve = to_raw_response_wrapper( - browser_profiles.retrieve, - ) - self.update = to_raw_response_wrapper( - browser_profiles.update, - ) - self.list = to_raw_response_wrapper( - browser_profiles.list, - ) - self.delete = to_raw_response_wrapper( - browser_profiles.delete, - ) - - -class AsyncBrowserProfilesResourceWithRawResponse: - def __init__(self, browser_profiles: AsyncBrowserProfilesResource) -> None: - self._browser_profiles = browser_profiles - - self.create = async_to_raw_response_wrapper( - browser_profiles.create, - ) - self.retrieve = async_to_raw_response_wrapper( - browser_profiles.retrieve, - ) - self.update = async_to_raw_response_wrapper( - browser_profiles.update, - ) - self.list = async_to_raw_response_wrapper( - browser_profiles.list, - ) - self.delete = async_to_raw_response_wrapper( - browser_profiles.delete, - ) - - -class BrowserProfilesResourceWithStreamingResponse: - def __init__(self, browser_profiles: BrowserProfilesResource) -> None: - self._browser_profiles = browser_profiles - - self.create = to_streamed_response_wrapper( - browser_profiles.create, - ) - self.retrieve = to_streamed_response_wrapper( - browser_profiles.retrieve, - ) - self.update = to_streamed_response_wrapper( - browser_profiles.update, - ) - self.list = to_streamed_response_wrapper( - browser_profiles.list, - ) - self.delete = to_streamed_response_wrapper( - browser_profiles.delete, - ) - - -class AsyncBrowserProfilesResourceWithStreamingResponse: - def __init__(self, browser_profiles: AsyncBrowserProfilesResource) -> None: - self._browser_profiles = browser_profiles - - self.create = async_to_streamed_response_wrapper( - browser_profiles.create, - ) - self.retrieve = async_to_streamed_response_wrapper( - browser_profiles.retrieve, - ) - self.update = async_to_streamed_response_wrapper( - browser_profiles.update, - ) - self.list = async_to_streamed_response_wrapper( - browser_profiles.list, - ) - self.delete = async_to_streamed_response_wrapper( - browser_profiles.delete, - ) diff --git a/src/browser_use_sdk/resources/sessions/__init__.py b/src/browser_use_sdk/resources/sessions/__init__.py deleted file mode 100644 index fd0ceb3..0000000 --- a/src/browser_use_sdk/resources/sessions/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .sessions import ( - SessionsResource, - AsyncSessionsResource, - SessionsResourceWithRawResponse, - AsyncSessionsResourceWithRawResponse, - SessionsResourceWithStreamingResponse, - AsyncSessionsResourceWithStreamingResponse, -) -from .public_share import ( - PublicShareResource, - AsyncPublicShareResource, - PublicShareResourceWithRawResponse, - AsyncPublicShareResourceWithRawResponse, - PublicShareResourceWithStreamingResponse, - AsyncPublicShareResourceWithStreamingResponse, -) - -__all__ = [ - "PublicShareResource", - "AsyncPublicShareResource", - "PublicShareResourceWithRawResponse", - "AsyncPublicShareResourceWithRawResponse", - "PublicShareResourceWithStreamingResponse", - "AsyncPublicShareResourceWithStreamingResponse", - "SessionsResource", - "AsyncSessionsResource", - "SessionsResourceWithRawResponse", - "AsyncSessionsResourceWithRawResponse", - "SessionsResourceWithStreamingResponse", - "AsyncSessionsResourceWithStreamingResponse", -] diff --git a/src/browser_use_sdk/resources/sessions/public_share.py b/src/browser_use_sdk/resources/sessions/public_share.py deleted file mode 100644 index 8a235d9..0000000 --- a/src/browser_use_sdk/resources/sessions/public_share.py +++ /dev/null @@ -1,429 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import httpx - -from ..._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven -from ..._compat import cached_property -from ..._resource import SyncAPIResource, AsyncAPIResource -from ..._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from ..._base_client import make_request_options -from ...types.sessions.share_view import ShareView - -__all__ = ["PublicShareResource", "AsyncPublicShareResource"] - - -class PublicShareResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> PublicShareResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return PublicShareResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> PublicShareResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return PublicShareResourceWithStreamingResponse(self) - - def create( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ShareView: - """ - Create a public share for a session. - - Generates a public sharing link that allows anyone with the URL to view the - session and its tasks. If a public share already exists for the session, it will - return the existing share instead of creating a new one. - - Public shares are useful for: - - - Sharing results with clients or team members - - Demonstrating AI agent capabilities - - Collaborative review of automated tasks - - Args: - - - session_id: The unique identifier of the agent session to share - - Returns: - - - Public share information including the share URL and usage statistics - - Raises: - - - 404: If the user agent session doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return self._post( - f"/sessions/{session_id}/public-share", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ShareView, - ) - - def retrieve( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ShareView: - """ - Get information about the public share for a session. - - Retrieves details about the public sharing link for a session, including the - share token, public URL, view count, and last viewed timestamp. This is useful - for monitoring how your shared sessions are being accessed. - - Args: - - - session_id: The unique identifier of the agent session - - Returns: - - - Public share information including the share URL and usage statistics - - Raises: - - - 404: If the user agent session doesn't exist or doesn't have a public share - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return self._get( - f"/sessions/{session_id}/public-share", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ShareView, - ) - - def delete( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> None: - """ - Remove the public share for a session. - - Deletes the public sharing link for a session, making it no longer accessible to - anyone with the previous share URL. This is useful for removing access to - sensitive sessions or when you no longer want to share the results. - - Args: - - - session_id: The unique identifier of the agent session - - Returns: - - - 204 No Content on successful deletion (idempotent) - - Raises: - - - 404: If the user agent session doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._delete( - f"/sessions/{session_id}/public-share", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class AsyncPublicShareResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncPublicShareResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return AsyncPublicShareResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncPublicShareResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return AsyncPublicShareResourceWithStreamingResponse(self) - - async def create( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ShareView: - """ - Create a public share for a session. - - Generates a public sharing link that allows anyone with the URL to view the - session and its tasks. If a public share already exists for the session, it will - return the existing share instead of creating a new one. - - Public shares are useful for: - - - Sharing results with clients or team members - - Demonstrating AI agent capabilities - - Collaborative review of automated tasks - - Args: - - - session_id: The unique identifier of the agent session to share - - Returns: - - - Public share information including the share URL and usage statistics - - Raises: - - - 404: If the user agent session doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return await self._post( - f"/sessions/{session_id}/public-share", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ShareView, - ) - - async def retrieve( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ShareView: - """ - Get information about the public share for a session. - - Retrieves details about the public sharing link for a session, including the - share token, public URL, view count, and last viewed timestamp. This is useful - for monitoring how your shared sessions are being accessed. - - Args: - - - session_id: The unique identifier of the agent session - - Returns: - - - Public share information including the share URL and usage statistics - - Raises: - - - 404: If the user agent session doesn't exist or doesn't have a public share - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return await self._get( - f"/sessions/{session_id}/public-share", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ShareView, - ) - - async def delete( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> None: - """ - Remove the public share for a session. - - Deletes the public sharing link for a session, making it no longer accessible to - anyone with the previous share URL. This is useful for removing access to - sensitive sessions or when you no longer want to share the results. - - Args: - - - session_id: The unique identifier of the agent session - - Returns: - - - 204 No Content on successful deletion (idempotent) - - Raises: - - - 404: If the user agent session doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._delete( - f"/sessions/{session_id}/public-share", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class PublicShareResourceWithRawResponse: - def __init__(self, public_share: PublicShareResource) -> None: - self._public_share = public_share - - self.create = to_raw_response_wrapper( - public_share.create, - ) - self.retrieve = to_raw_response_wrapper( - public_share.retrieve, - ) - self.delete = to_raw_response_wrapper( - public_share.delete, - ) - - -class AsyncPublicShareResourceWithRawResponse: - def __init__(self, public_share: AsyncPublicShareResource) -> None: - self._public_share = public_share - - self.create = async_to_raw_response_wrapper( - public_share.create, - ) - self.retrieve = async_to_raw_response_wrapper( - public_share.retrieve, - ) - self.delete = async_to_raw_response_wrapper( - public_share.delete, - ) - - -class PublicShareResourceWithStreamingResponse: - def __init__(self, public_share: PublicShareResource) -> None: - self._public_share = public_share - - self.create = to_streamed_response_wrapper( - public_share.create, - ) - self.retrieve = to_streamed_response_wrapper( - public_share.retrieve, - ) - self.delete = to_streamed_response_wrapper( - public_share.delete, - ) - - -class AsyncPublicShareResourceWithStreamingResponse: - def __init__(self, public_share: AsyncPublicShareResource) -> None: - self._public_share = public_share - - self.create = async_to_streamed_response_wrapper( - public_share.create, - ) - self.retrieve = async_to_streamed_response_wrapper( - public_share.retrieve, - ) - self.delete = async_to_streamed_response_wrapper( - public_share.delete, - ) diff --git a/src/browser_use_sdk/resources/sessions/sessions.py b/src/browser_use_sdk/resources/sessions/sessions.py deleted file mode 100644 index 4fc67dd..0000000 --- a/src/browser_use_sdk/resources/sessions/sessions.py +++ /dev/null @@ -1,618 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Optional -from typing_extensions import Literal - -import httpx - -from ...types import SessionStatus, session_list_params, session_update_params -from ..._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven -from ..._utils import maybe_transform, async_maybe_transform -from ..._compat import cached_property -from ..._resource import SyncAPIResource, AsyncAPIResource -from ..._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from .public_share import ( - PublicShareResource, - AsyncPublicShareResource, - PublicShareResourceWithRawResponse, - AsyncPublicShareResourceWithRawResponse, - PublicShareResourceWithStreamingResponse, - AsyncPublicShareResourceWithStreamingResponse, -) -from ..._base_client import make_request_options -from ...types.session_view import SessionView -from ...types.session_status import SessionStatus -from ...types.session_list_response import SessionListResponse - -__all__ = ["SessionsResource", "AsyncSessionsResource"] - - -class SessionsResource(SyncAPIResource): - @cached_property - def public_share(self) -> PublicShareResource: - return PublicShareResource(self._client) - - @cached_property - def with_raw_response(self) -> SessionsResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return SessionsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> SessionsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return SessionsResourceWithStreamingResponse(self) - - def retrieve( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionView: - """ - Get detailed information about a specific AI agent session. - - Retrieves comprehensive information about a session, including its current - status, live browser URL (if active), recording URL (if completed), and optional - task details. This endpoint is useful for monitoring active sessions or - reviewing completed ones. - - Args: - - - session_id: The unique identifier of the agent session - - params: Optional parameters to control what data is included - - Returns: - - - Complete session information including status, URLs, and optional task details - - Raises: - - - 404: If the user agent session doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return self._get( - f"/sessions/{session_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionView, - ) - - def update( - self, - session_id: str, - *, - action: Literal["stop"], - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionView: - """ - Update a session's status or perform actions on it. - - Currently supports stopping a session, which will: - - 1. Stop any running tasks in the session - 2. End the browser session - 3. Generate a recording URL if available - 4. Update the session status to 'stopped' - - This is useful for manually stopping long-running sessions or when you want to - end a session before all tasks are complete. - - Args: - - - session_id: The unique identifier of the agent session to update - - request: The action to perform on the session - - Returns: - - - The updated session information including the new status and recording URL - - Raises: - - - 404: If the user agent session doesn't exist - - Args: - action: Available actions that can be performed on a session - - Attributes: STOP: Stop the session and all its associated tasks (cannot be - undone) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return self._patch( - f"/sessions/{session_id}", - body=maybe_transform({"action": action}, session_update_params.SessionUpdateParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionView, - ) - - def list( - self, - *, - filter_by: Optional[SessionStatus] | NotGiven = NOT_GIVEN, - page_number: int | NotGiven = NOT_GIVEN, - page_size: int | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionListResponse: - """ - Get a paginated list of all AI agent sessions for the authenticated user. - - AI agent sessions represent active or completed browsing sessions where your AI - agents perform tasks. Each session can contain multiple tasks and maintains - browser state throughout the session lifecycle. - - You can filter sessions by status and optionally include task details for each - session. - - Returns: - - - A paginated list of agent sessions - - Total count of sessions - - Page information for navigation - - Optional task details for each session (if requested) - - Args: - filter_by: Enumeration of possible (browser) session states - - Attributes: ACTIVE: Session is currently active and running (browser is running) - STOPPED: Session has been stopped and is no longer active (browser is stopped) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._get( - "/sessions", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "filter_by": filter_by, - "page_number": page_number, - "page_size": page_size, - }, - session_list_params.SessionListParams, - ), - ), - cast_to=SessionListResponse, - ) - - def delete( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> None: - """ - Delete a session and all its associated data. - - Permanently removes a session and all its tasks, browser data, and public - shares. This action cannot be undone. Use this endpoint to clean up old sessions - and free up storage space. - - Args: - - - session_id: The unique identifier of the agent session to delete - - Returns: - - - 204 No Content on successful deletion (idempotent) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._delete( - f"/sessions/{session_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class AsyncSessionsResource(AsyncAPIResource): - @cached_property - def public_share(self) -> AsyncPublicShareResource: - return AsyncPublicShareResource(self._client) - - @cached_property - def with_raw_response(self) -> AsyncSessionsResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return AsyncSessionsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncSessionsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return AsyncSessionsResourceWithStreamingResponse(self) - - async def retrieve( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionView: - """ - Get detailed information about a specific AI agent session. - - Retrieves comprehensive information about a session, including its current - status, live browser URL (if active), recording URL (if completed), and optional - task details. This endpoint is useful for monitoring active sessions or - reviewing completed ones. - - Args: - - - session_id: The unique identifier of the agent session - - params: Optional parameters to control what data is included - - Returns: - - - Complete session information including status, URLs, and optional task details - - Raises: - - - 404: If the user agent session doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return await self._get( - f"/sessions/{session_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionView, - ) - - async def update( - self, - session_id: str, - *, - action: Literal["stop"], - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionView: - """ - Update a session's status or perform actions on it. - - Currently supports stopping a session, which will: - - 1. Stop any running tasks in the session - 2. End the browser session - 3. Generate a recording URL if available - 4. Update the session status to 'stopped' - - This is useful for manually stopping long-running sessions or when you want to - end a session before all tasks are complete. - - Args: - - - session_id: The unique identifier of the agent session to update - - request: The action to perform on the session - - Returns: - - - The updated session information including the new status and recording URL - - Raises: - - - 404: If the user agent session doesn't exist - - Args: - action: Available actions that can be performed on a session - - Attributes: STOP: Stop the session and all its associated tasks (cannot be - undone) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - return await self._patch( - f"/sessions/{session_id}", - body=await async_maybe_transform({"action": action}, session_update_params.SessionUpdateParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SessionView, - ) - - async def list( - self, - *, - filter_by: Optional[SessionStatus] | NotGiven = NOT_GIVEN, - page_number: int | NotGiven = NOT_GIVEN, - page_size: int | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionListResponse: - """ - Get a paginated list of all AI agent sessions for the authenticated user. - - AI agent sessions represent active or completed browsing sessions where your AI - agents perform tasks. Each session can contain multiple tasks and maintains - browser state throughout the session lifecycle. - - You can filter sessions by status and optionally include task details for each - session. - - Returns: - - - A paginated list of agent sessions - - Total count of sessions - - Page information for navigation - - Optional task details for each session (if requested) - - Args: - filter_by: Enumeration of possible (browser) session states - - Attributes: ACTIVE: Session is currently active and running (browser is running) - STOPPED: Session has been stopped and is no longer active (browser is stopped) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return await self._get( - "/sessions", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform( - { - "filter_by": filter_by, - "page_number": page_number, - "page_size": page_size, - }, - session_list_params.SessionListParams, - ), - ), - cast_to=SessionListResponse, - ) - - async def delete( - self, - session_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> None: - """ - Delete a session and all its associated data. - - Permanently removes a session and all its tasks, browser data, and public - shares. This action cannot be undone. Use this endpoint to clean up old sessions - and free up storage space. - - Args: - - - session_id: The unique identifier of the agent session to delete - - Returns: - - - 204 No Content on successful deletion (idempotent) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not session_id: - raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._delete( - f"/sessions/{session_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - -class SessionsResourceWithRawResponse: - def __init__(self, sessions: SessionsResource) -> None: - self._sessions = sessions - - self.retrieve = to_raw_response_wrapper( - sessions.retrieve, - ) - self.update = to_raw_response_wrapper( - sessions.update, - ) - self.list = to_raw_response_wrapper( - sessions.list, - ) - self.delete = to_raw_response_wrapper( - sessions.delete, - ) - - @cached_property - def public_share(self) -> PublicShareResourceWithRawResponse: - return PublicShareResourceWithRawResponse(self._sessions.public_share) - - -class AsyncSessionsResourceWithRawResponse: - def __init__(self, sessions: AsyncSessionsResource) -> None: - self._sessions = sessions - - self.retrieve = async_to_raw_response_wrapper( - sessions.retrieve, - ) - self.update = async_to_raw_response_wrapper( - sessions.update, - ) - self.list = async_to_raw_response_wrapper( - sessions.list, - ) - self.delete = async_to_raw_response_wrapper( - sessions.delete, - ) - - @cached_property - def public_share(self) -> AsyncPublicShareResourceWithRawResponse: - return AsyncPublicShareResourceWithRawResponse(self._sessions.public_share) - - -class SessionsResourceWithStreamingResponse: - def __init__(self, sessions: SessionsResource) -> None: - self._sessions = sessions - - self.retrieve = to_streamed_response_wrapper( - sessions.retrieve, - ) - self.update = to_streamed_response_wrapper( - sessions.update, - ) - self.list = to_streamed_response_wrapper( - sessions.list, - ) - self.delete = to_streamed_response_wrapper( - sessions.delete, - ) - - @cached_property - def public_share(self) -> PublicShareResourceWithStreamingResponse: - return PublicShareResourceWithStreamingResponse(self._sessions.public_share) - - -class AsyncSessionsResourceWithStreamingResponse: - def __init__(self, sessions: AsyncSessionsResource) -> None: - self._sessions = sessions - - self.retrieve = async_to_streamed_response_wrapper( - sessions.retrieve, - ) - self.update = async_to_streamed_response_wrapper( - sessions.update, - ) - self.list = async_to_streamed_response_wrapper( - sessions.list, - ) - self.delete = async_to_streamed_response_wrapper( - sessions.delete, - ) - - @cached_property - def public_share(self) -> AsyncPublicShareResourceWithStreamingResponse: - return AsyncPublicShareResourceWithStreamingResponse(self._sessions.public_share) diff --git a/src/browser_use_sdk/resources/tasks.py b/src/browser_use_sdk/resources/tasks.py deleted file mode 100644 index 8e96d22..0000000 --- a/src/browser_use_sdk/resources/tasks.py +++ /dev/null @@ -1,1741 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import json -import time -import asyncio -from typing import Dict, List, Union, TypeVar, Iterator, Optional, AsyncIterator, overload -from datetime import datetime -from typing_extensions import Literal - -import httpx -from pydantic import BaseModel - -from ..types import TaskStatus, task_list_params, task_create_params, task_update_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import maybe_transform, async_maybe_transform -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from ..lib.parse import TaskViewWithOutput, hash_task_view -from .._base_client import make_request_options -from ..types.task_view import TaskView -from ..types.task_status import TaskStatus -from ..types.task_list_response import TaskListResponse -from ..types.task_create_response import TaskCreateResponse -from ..types.task_get_logs_response import TaskGetLogsResponse -from ..types.task_get_output_file_response import TaskGetOutputFileResponse -from ..types.task_get_user_uploaded_file_response import TaskGetUserUploadedFileResponse - -__all__ = ["TasksResource", "AsyncTasksResource"] - -T = TypeVar("T", bound=BaseModel) - - -class TasksResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> TasksResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return TasksResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> TasksResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return TasksResourceWithStreamingResponse(self) - - @overload - def run( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: Optional[str] | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskView: ... - - @overload - def run( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: type[T], - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskViewWithOutput[T]: ... - - def run( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: Optional[Union[type[BaseModel], str]] | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Union[TaskView, TaskViewWithOutput[BaseModel]]: - """ - Run a new task and return the task view. - """ - if structured_output_json is not None and isinstance(structured_output_json, type): - create_task_res = self.create( - task=task, - agent_settings=agent_settings, - browser_settings=browser_settings, - included_file_names=included_file_names, - metadata=metadata, - secrets=secrets, - structured_output_json=structured_output_json, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - - for structured_msg in self.stream(create_task_res.id, structured_output_json=structured_output_json): - if structured_msg.status == "finished": - return structured_msg - - raise ValueError("Task did not finish") - - else: - create_task_res = self.create( - task=task, - agent_settings=agent_settings, - browser_settings=browser_settings, - included_file_names=included_file_names, - metadata=metadata, - secrets=secrets, - structured_output_json=structured_output_json, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - - for msg in self.stream(create_task_res.id): - if msg.status == "finished": - return msg - - raise ValueError("Task did not finish") - - @overload - def create( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: Optional[str] | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskCreateResponse: ... - - @overload - def create( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: type[BaseModel], - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskCreateResponse: ... - - def create( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: Optional[Union[type[BaseModel], str]] | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskCreateResponse: - """ - Create and start a new Browser Use Agent task. - - This is the main endpoint for running AI agents. You can either: - - 1. Start a new session with a new task. - 2. Add a follow-up task to an existing session. - - When starting a new session: - - - A new browser session is created - - Credits are deducted from your account - - The agent begins executing your task immediately - - When adding to an existing session: - - - The agent continues in the same browser context - - No additional browser start up costs are charged (browser session is already - active) - - The agent can build on previous work - - Key features: - - - Agent profiles: Define agent behavior and capabilities - - Browser profiles: Control browser settings and environment (only used for new - sessions) - - File uploads: Include documents for the agent to work with - - Structured output: Define the format of the task result - - Task metadata: Add custom data for tracking and organization - - Args: - - - request: Complete task configuration including agent settings, browser - settings, and task description - - Returns: - - - The created task ID together with the task's session ID - - Raises: - - - 402: If user has insufficient credits for a new session - - 404: If referenced agent/browser profiles don't exist - - 400: If session is stopped or already has a running task - - Args: - agent_settings: Configuration settings for the agent - - Attributes: llm: The LLM model to use for the agent start_url: Optional URL to - start the agent on (will not be changed as a step) profile_id: Unique identifier - of the agent profile to use for the task - - browser_settings: Configuration settings for the browser session - - Attributes: session_id: Unique identifier of existing session to continue - profile_id: Unique identifier of browser profile to use (use if you want to - start a new session) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if ( - structured_output_json is not None - and not isinstance(structured_output_json, str) - and isinstance(structured_output_json, type) - ): - structured_output_json = json.dumps(structured_output_json.model_json_schema()) - - return self._post( - "/tasks", - body=maybe_transform( - { - "task": task, - "agent_settings": agent_settings, - "browser_settings": browser_settings, - "included_file_names": included_file_names, - "metadata": metadata, - "secrets": secrets, - "structured_output_json": structured_output_json, - }, - task_create_params.TaskCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskCreateResponse, - ) - - @overload - def retrieve( - self, - task_id: str, - structured_output_json: type[T], - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskViewWithOutput[T]: ... - - @overload - def retrieve( - self, - task_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskView: ... - - def retrieve( - self, - task_id: str, - structured_output_json: Optional[type[BaseModel]] | NotGiven = NOT_GIVEN, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Union[TaskView, TaskViewWithOutput[BaseModel]]: - """ - Get detailed information about a specific AI agent task. - - Retrieves comprehensive information about a task, including its current status, - progress, and detailed execution data. You can choose to get just the status - (for quick polling) or full details including steps and file information. - - Use this endpoint to: - - - Monitor task progress in real-time - - Review completed task results - - Debug failed tasks by examining steps - - Download output files and logs - - Args: - - - task_id: The unique identifier of the agent task - - Returns: - - - Complete task information - - Raises: - - - 404: If the user agent task doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - - if structured_output_json is not None and isinstance(structured_output_json, type): - res = self._get( - f"/tasks/{task_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskView, - ) - - if res.done_output is None: - return TaskViewWithOutput[BaseModel]( - **res.model_dump(), - parsed_output=None, - ) - - parsed_output = structured_output_json.model_validate_json(res.done_output) - - return TaskViewWithOutput[BaseModel]( - **res.model_dump(), - parsed_output=parsed_output, - ) - - return self._get( - f"/tasks/{task_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskView, - ) - - @overload - def stream( - self, - task_id: str, - structured_output_json: type[T], - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Iterator[TaskViewWithOutput[T]]: ... - - @overload - def stream( - self, - task_id: str, - structured_output_json: None | NotGiven = NOT_GIVEN, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Iterator[TaskView]: ... - - def stream( - self, - task_id: str, - structured_output_json: type[T] | None | NotGiven = NOT_GIVEN, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Iterator[TaskView | TaskViewWithOutput[T]]: - """ - Stream the task view as it is updated until the task is finished. - """ - - for res in self._watch( - task_id=task_id, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ): - if structured_output_json is not None and isinstance(structured_output_json, type): - if res.done_output is None: - yield TaskViewWithOutput[T]( - **res.model_dump(), - parsed_output=None, - ) - else: - schema: type[T] = structured_output_json - parsed_output: T = schema.model_validate_json(res.done_output) - - yield TaskViewWithOutput[T]( - **res.model_dump(), - parsed_output=parsed_output, - ) - - else: - yield res - - def _watch( - self, - task_id: str, - interval: float = 1, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Iterator[TaskView]: - """Converts a polling loop into a generator loop.""" - hash: str | None = None - - while True: - res = self.retrieve( - task_id=task_id, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - - res_hash = hash_task_view(res) - - if hash is None or res_hash != hash: - hash = res_hash - yield res - - if res.status == "finished": - break - - time.sleep(interval) - - def update( - self, - task_id: str, - *, - action: Literal["stop", "pause", "resume", "stop_task_and_session"], - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskView: - """ - Control the execution of an AI agent task. - - Allows you to pause, resume, or stop tasks, and optionally stop the entire - session. This is useful for: - - - Pausing long-running tasks to review progress - - Stopping tasks that are taking too long - - Ending sessions when you're done with all tasks - - Available actions: - - - STOP: Stop the current task - - PAUSE: Pause the task (can be resumed later) - - RESUME: Resume a paused task - - STOP_TASK_AND_SESSION: Stop the task and end the entire session - - Args: - - - task_id: The unique identifier of the agent task to control - - request: The action to perform on the task - - Returns: - - - The updated task information - - Raises: - - - 404: If the user agent task doesn't exist - - Args: - action: Available actions that can be performed on a task - - Attributes: STOP: Stop the current task execution PAUSE: Pause the current task - execution RESUME: Resume a paused task execution STOP_TASK_AND_SESSION: Stop - both the task and its parent session - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - return self._patch( - f"/tasks/{task_id}", - body=maybe_transform({"action": action}, task_update_params.TaskUpdateParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskView, - ) - - def list( - self, - *, - after: Union[str, datetime, None] | NotGiven = NOT_GIVEN, - before: Union[str, datetime, None] | NotGiven = NOT_GIVEN, - filter_by: Optional[TaskStatus] | NotGiven = NOT_GIVEN, - page_number: int | NotGiven = NOT_GIVEN, - page_size: int | NotGiven = NOT_GIVEN, - session_id: Optional[str] | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskListResponse: - """ - Get a paginated list of all Browser Use Agent tasks for the authenticated user. - - Browser Use Agent tasks are the individual jobs that your agents perform within - a session. Each task represents a specific instruction or goal that the agent - works on, such as filling out a form, extracting data, or navigating to specific - pages. - - Returns: - - - A paginated list of Browser Use Agent tasks - - Total count of Browser Use Agent tasks - - Page information for navigation - - Args: - filter_by: Enumeration of possible task execution states - - Attributes: STARTED: Task has been started and is currently running. PAUSED: - Task execution has been temporarily paused (can be resumed) FINISHED: Task has - finished and the agent has completed the task. STOPPED: Task execution has been - manually stopped (cannot be resumed). - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._get( - "/tasks", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "after": after, - "before": before, - "filter_by": filter_by, - "page_number": page_number, - "page_size": page_size, - "session_id": session_id, - }, - task_list_params.TaskListParams, - ), - ), - cast_to=TaskListResponse, - ) - - def get_logs( - self, - task_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskGetLogsResponse: - """ - Get a download URL for the execution logs of an AI agent task. - - Task logs contain detailed information about how the AI agent executed the task, - including: - - - Step-by-step reasoning and decisions - - Actions taken on web pages - - Error messages and debugging information - - Performance metrics and timing data - - This is useful for: - - - Understanding how the agent solved the task - - Debugging failed or unexpected results - - Optimizing agent behavior and prompts - - Auditing agent actions for compliance - - Args: - - - task_id: The unique identifier of the agent task - - Returns: - - - A presigned download URL for the task log file - - Raises: - - - 404: If the user agent task doesn't exist - - 500: If the download URL cannot be generated (should not happen) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - return self._get( - f"/tasks/{task_id}/logs", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskGetLogsResponse, - ) - - def get_output_file( - self, - file_id: str, - *, - task_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskGetOutputFileResponse: - """ - Get a download URL for a specific output file generated by an AI agent task. - - AI agents can generate various output files during task execution, such as: - - - Screenshots of web pages - - Extracted data in CSV/JSON format - - Generated reports or documents - - Downloaded files from websites - - This endpoint provides a secure, time-limited download URL for accessing these - files. The URL expires after a short time for security. - - Args: - - - task_id: The unique identifier of the agent task - - file_id: The unique identifier of the output file - - Returns: - - - A presigned download URL for the requested file - - Raises: - - - 404: If the user agent task or output file doesn't exist - - 500: If the download URL cannot be generated (should not happen) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - if not file_id: - raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - return self._get( - f"/tasks/{task_id}/output-files/{file_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskGetOutputFileResponse, - ) - - def get_user_uploaded_file( - self, - file_id: str, - *, - task_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskGetUserUploadedFileResponse: - """ - Get a download URL for a specific user uploaded file that was used in the task. - - A user can upload files to their account file bucket and reference the name of - the file in a task. These files are then made available for the agent to use - during the agent task run. - - This endpoint provides a secure, time-limited download URL for accessing these - files. The URL expires after a short time for security. - - Args: - - - task_id: The unique identifier of the agent task - - file_id: The unique identifier of the user uploaded file - - Returns: - - - A presigned download URL for the requested file - - Raises: - - - 404: If the user agent task or user uploaded file doesn't exist - - 500: If the download URL cannot be generated (should not happen) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - if not file_id: - raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - return self._get( - f"/tasks/{task_id}/user-uploaded-files/{file_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskGetUserUploadedFileResponse, - ) - - -class AsyncTasksResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncTasksResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return AsyncTasksResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncTasksResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return AsyncTasksResourceWithStreamingResponse(self) - - @overload - async def run( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: Optional[str] | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskView: ... - - @overload - async def run( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: type[T], - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskViewWithOutput[T]: ... - - async def run( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: Optional[Union[type[BaseModel], str]] | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Union[TaskView, TaskViewWithOutput[BaseModel]]: - """ - Run a new Browser Use Agent task. - """ - if structured_output_json is not None and isinstance(structured_output_json, type): - create_task_res = await self.create( - task=task, - agent_settings=agent_settings, - browser_settings=browser_settings, - included_file_names=included_file_names, - metadata=metadata, - secrets=secrets, - structured_output_json=structured_output_json, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - - async for structured_msg in self.stream(create_task_res.id, structured_output_json=structured_output_json): - if structured_msg.status == "finished": - return structured_msg - - raise ValueError("Task did not finish") - - else: - create_task_res = await self.create( - task=task, - agent_settings=agent_settings, - browser_settings=browser_settings, - included_file_names=included_file_names, - metadata=metadata, - secrets=secrets, - structured_output_json=structured_output_json, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - - async for msg in self.stream(create_task_res.id): - if msg.status == "finished": - return msg - - raise ValueError("Task did not finish") - - @overload - async def create( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: Optional[str] | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskCreateResponse: ... - - @overload - async def create( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: type[BaseModel], - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskCreateResponse: ... - - async def create( - self, - *, - task: str, - agent_settings: task_create_params.AgentSettings | NotGiven = NOT_GIVEN, - browser_settings: task_create_params.BrowserSettings | NotGiven = NOT_GIVEN, - included_file_names: Optional[List[str]] | NotGiven = NOT_GIVEN, - metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, - structured_output_json: Optional[Union[type[BaseModel], str]] | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskCreateResponse: - """ - Create and start a new Browser Use Agent task. - - This is the main endpoint for running AI agents. You can either: - - 1. Start a new session with a new task. - 2. Add a follow-up task to an existing session. - - When starting a new session: - - - A new browser session is created - - Credits are deducted from your account - - The agent begins executing your task immediately - - When adding to an existing session: - - - The agent continues in the same browser context - - No additional browser start up costs are charged (browser session is already - active) - - The agent can build on previous work - - Key features: - - - Agent profiles: Define agent behavior and capabilities - - Browser profiles: Control browser settings and environment (only used for new - sessions) - - File uploads: Include documents for the agent to work with - - Structured output: Define the format of the task result - - Task metadata: Add custom data for tracking and organization - - Args: - - - request: Complete task configuration including agent settings, browser - settings, and task description - - Returns: - - - The created task ID together with the task's session ID - - Raises: - - - 402: If user has insufficient credits for a new session - - 404: If referenced agent/browser profiles don't exist - - 400: If session is stopped or already has a running task - - Args: - agent_settings: Configuration settings for the agent - - Attributes: llm: The LLM model to use for the agent start_url: Optional URL to - start the agent on (will not be changed as a step) profile_id: Unique identifier - of the agent profile to use for the task - - browser_settings: Configuration settings for the browser session - - Attributes: session_id: Unique identifier of existing session to continue - profile_id: Unique identifier of browser profile to use (use if you want to - start a new session) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - - if ( - structured_output_json is not None - and not isinstance(structured_output_json, str) - and isinstance(structured_output_json, type) - ): - structured_output_json = json.dumps(structured_output_json.model_json_schema()) - - return await self._post( - "/tasks", - body=await async_maybe_transform( - { - "task": task, - "agent_settings": agent_settings, - "browser_settings": browser_settings, - "included_file_names": included_file_names, - "metadata": metadata, - "secrets": secrets, - "structured_output_json": structured_output_json, - }, - task_create_params.TaskCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskCreateResponse, - ) - - @overload - async def retrieve( - self, - task_id: str, - structured_output_json: type[T], - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskViewWithOutput[T]: ... - - @overload - async def retrieve( - self, - task_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskView: ... - - async def retrieve( - self, - task_id: str, - structured_output_json: Optional[type[BaseModel]] | NotGiven = NOT_GIVEN, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Union[TaskView, TaskViewWithOutput[BaseModel]]: - """ - Get detailed information about a specific AI agent task. - - Retrieves comprehensive information about a task, including its current status, - progress, and detailed execution data. You can choose to get just the status - (for quick polling) or full details including steps and file information. - - Use this endpoint to: - - - Monitor task progress in real-time - - Review completed task results - - Debug failed tasks by examining steps - - Download output files and logs - - Args: - - - task_id: The unique identifier of the agent task - - Returns: - - - Complete task information - - Raises: - - - 404: If the user agent task doesn't exist - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - - if structured_output_json is not None and isinstance(structured_output_json, type): - res = await self._get( - f"/tasks/{task_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskView, - ) - - if res.done_output is None: - return TaskViewWithOutput[BaseModel]( - **res.model_dump(), - parsed_output=None, - ) - - parsed_output = structured_output_json.model_validate_json(res.done_output) - - return TaskViewWithOutput[BaseModel]( - **res.model_dump(), - parsed_output=parsed_output, - ) - - return await self._get( - f"/tasks/{task_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskView, - ) - - @overload - def stream( - self, - task_id: str, - structured_output_json: type[T], - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncIterator[TaskViewWithOutput[T]]: ... - - @overload - def stream( - self, - task_id: str, - structured_output_json: None | NotGiven = NOT_GIVEN, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncIterator[TaskView]: ... - - async def stream( - self, - task_id: str, - structured_output_json: type[T] | None | NotGiven = NOT_GIVEN, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncIterator[TaskView | TaskViewWithOutput[T]]: - """ - Stream the task view as it is updated until the task is finished. - """ - - async for res in self._watch( - task_id=task_id, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ): - if structured_output_json is not None and isinstance(structured_output_json, type): - if res.done_output is None: - yield TaskViewWithOutput[T]( - **res.model_dump(), - parsed_output=None, - ) - else: - schema: type[T] = structured_output_json - # pydantic returns the model instance, but the type checker can’t infer it. - parsed_output: T = schema.model_validate_json(res.done_output) - yield TaskViewWithOutput[T]( - **res.model_dump(), - parsed_output=parsed_output, - ) - else: - yield res - - async def _watch( - self, - task_id: str, - interval: float = 1, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncIterator[TaskView]: - """Converts a polling loop into a generator loop.""" - prev_hash: str | None = None - - while True: - res = await self.retrieve( - task_id=task_id, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - - res_hash = hash_task_view(res) - if prev_hash is None or res_hash != prev_hash: - prev_hash = res_hash - yield res - - if res.status == "finished": - break - if res.status == "paused": - break - if res.status == "stopped": - break - if res.status == "started": - await asyncio.sleep(interval) - else: - raise ValueError( - f"Expected one of 'finished', 'paused', 'stopped', or 'started' but received {res.status!r}" - ) - - async def update( - self, - task_id: str, - *, - action: Literal["stop", "pause", "resume", "stop_task_and_session"], - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskView: - """ - Control the execution of an AI agent task. - - Allows you to pause, resume, or stop tasks, and optionally stop the entire - session. This is useful for: - - - Pausing long-running tasks to review progress - - Stopping tasks that are taking too long - - Ending sessions when you're done with all tasks - - Available actions: - - - STOP: Stop the current task - - PAUSE: Pause the task (can be resumed later) - - RESUME: Resume a paused task - - STOP_TASK_AND_SESSION: Stop the task and end the entire session - - Args: - - - task_id: The unique identifier of the agent task to control - - request: The action to perform on the task - - Returns: - - - The updated task information - - Raises: - - - 404: If the user agent task doesn't exist - - Args: - action: Available actions that can be performed on a task - - Attributes: STOP: Stop the current task execution PAUSE: Pause the current task - execution RESUME: Resume a paused task execution STOP_TASK_AND_SESSION: Stop - both the task and its parent session - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - return await self._patch( - f"/tasks/{task_id}", - body=await async_maybe_transform({"action": action}, task_update_params.TaskUpdateParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskView, - ) - - async def list( - self, - *, - after: Union[str, datetime, None] | NotGiven = NOT_GIVEN, - before: Union[str, datetime, None] | NotGiven = NOT_GIVEN, - filter_by: Optional[TaskStatus] | NotGiven = NOT_GIVEN, - page_number: int | NotGiven = NOT_GIVEN, - page_size: int | NotGiven = NOT_GIVEN, - session_id: Optional[str] | NotGiven = NOT_GIVEN, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskListResponse: - """ - Get a paginated list of all Browser Use Agent tasks for the authenticated user. - - Browser Use Agent tasks are the individual jobs that your agents perform within - a session. Each task represents a specific instruction or goal that the agent - works on, such as filling out a form, extracting data, or navigating to specific - pages. - - Returns: - - - A paginated list of Browser Use Agent tasks - - Total count of Browser Use Agent tasks - - Page information for navigation - - Args: - filter_by: Enumeration of possible task execution states - - Attributes: STARTED: Task has been started and is currently running. PAUSED: - Task execution has been temporarily paused (can be resumed) FINISHED: Task has - finished and the agent has completed the task. STOPPED: Task execution has been - manually stopped (cannot be resumed). - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return await self._get( - "/tasks", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform( - { - "after": after, - "before": before, - "filter_by": filter_by, - "page_number": page_number, - "page_size": page_size, - "session_id": session_id, - }, - task_list_params.TaskListParams, - ), - ), - cast_to=TaskListResponse, - ) - - async def get_logs( - self, - task_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskGetLogsResponse: - """ - Get a download URL for the execution logs of an AI agent task. - - Task logs contain detailed information about how the AI agent executed the task, - including: - - - Step-by-step reasoning and decisions - - Actions taken on web pages - - Error messages and debugging information - - Performance metrics and timing data - - This is useful for: - - - Understanding how the agent solved the task - - Debugging failed or unexpected results - - Optimizing agent behavior and prompts - - Auditing agent actions for compliance - - Args: - - - task_id: The unique identifier of the agent task - - Returns: - - - A presigned download URL for the task log file - - Raises: - - - 404: If the user agent task doesn't exist - - 500: If the download URL cannot be generated (should not happen) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - return await self._get( - f"/tasks/{task_id}/logs", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskGetLogsResponse, - ) - - async def get_output_file( - self, - file_id: str, - *, - task_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskGetOutputFileResponse: - """ - Get a download URL for a specific output file generated by an AI agent task. - - AI agents can generate various output files during task execution, such as: - - - Screenshots of web pages - - Extracted data in CSV/JSON format - - Generated reports or documents - - Downloaded files from websites - - This endpoint provides a secure, time-limited download URL for accessing these - files. The URL expires after a short time for security. - - Args: - - - task_id: The unique identifier of the agent task - - file_id: The unique identifier of the output file - - Returns: - - - A presigned download URL for the requested file - - Raises: - - - 404: If the user agent task or output file doesn't exist - - 500: If the download URL cannot be generated (should not happen) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - if not file_id: - raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - return await self._get( - f"/tasks/{task_id}/output-files/{file_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskGetOutputFileResponse, - ) - - async def get_user_uploaded_file( - self, - file_id: str, - *, - task_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TaskGetUserUploadedFileResponse: - """ - Get a download URL for a specific user uploaded file that was used in the task. - - A user can upload files to their account file bucket and reference the name of - the file in a task. These files are then made available for the agent to use - during the agent task run. - - This endpoint provides a secure, time-limited download URL for accessing these - files. The URL expires after a short time for security. - - Args: - - - task_id: The unique identifier of the agent task - - file_id: The unique identifier of the user uploaded file - - Returns: - - - A presigned download URL for the requested file - - Raises: - - - 404: If the user agent task or user uploaded file doesn't exist - - 500: If the download URL cannot be generated (should not happen) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not task_id: - raise ValueError(f"Expected a non-empty value for `task_id` but received {task_id!r}") - if not file_id: - raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - return await self._get( - f"/tasks/{task_id}/user-uploaded-files/{file_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=TaskGetUserUploadedFileResponse, - ) - - -class TasksResourceWithRawResponse: - def __init__(self, tasks: TasksResource) -> None: - self._tasks = tasks - - self.create = to_raw_response_wrapper( - tasks.create, - ) - self.retrieve = to_raw_response_wrapper( - tasks.retrieve, - ) - self.update = to_raw_response_wrapper( - tasks.update, - ) - self.list = to_raw_response_wrapper( - tasks.list, - ) - self.get_logs = to_raw_response_wrapper( - tasks.get_logs, - ) - self.get_output_file = to_raw_response_wrapper( - tasks.get_output_file, - ) - self.get_user_uploaded_file = to_raw_response_wrapper( - tasks.get_user_uploaded_file, - ) - - -class AsyncTasksResourceWithRawResponse: - def __init__(self, tasks: AsyncTasksResource) -> None: - self._tasks = tasks - - self.create = async_to_raw_response_wrapper( - tasks.create, - ) - self.retrieve = async_to_raw_response_wrapper( - tasks.retrieve, - ) - self.update = async_to_raw_response_wrapper( - tasks.update, - ) - self.list = async_to_raw_response_wrapper( - tasks.list, - ) - self.get_logs = async_to_raw_response_wrapper( - tasks.get_logs, - ) - self.get_output_file = async_to_raw_response_wrapper( - tasks.get_output_file, - ) - self.get_user_uploaded_file = async_to_raw_response_wrapper( - tasks.get_user_uploaded_file, - ) - - -class TasksResourceWithStreamingResponse: - def __init__(self, tasks: TasksResource) -> None: - self._tasks = tasks - - self.create = to_streamed_response_wrapper( - tasks.create, - ) - self.retrieve = to_streamed_response_wrapper( - tasks.retrieve, - ) - self.update = to_streamed_response_wrapper( - tasks.update, - ) - self.list = to_streamed_response_wrapper( - tasks.list, - ) - self.get_logs = to_streamed_response_wrapper( - tasks.get_logs, - ) - self.get_output_file = to_streamed_response_wrapper( - tasks.get_output_file, - ) - self.get_user_uploaded_file = to_streamed_response_wrapper( - tasks.get_user_uploaded_file, - ) - - -class AsyncTasksResourceWithStreamingResponse: - def __init__(self, tasks: AsyncTasksResource) -> None: - self._tasks = tasks - - self.create = async_to_streamed_response_wrapper( - tasks.create, - ) - self.retrieve = async_to_streamed_response_wrapper( - tasks.retrieve, - ) - self.update = async_to_streamed_response_wrapper( - tasks.update, - ) - self.list = async_to_streamed_response_wrapper( - tasks.list, - ) - self.get_logs = async_to_streamed_response_wrapper( - tasks.get_logs, - ) - self.get_output_file = async_to_streamed_response_wrapper( - tasks.get_output_file, - ) - self.get_user_uploaded_file = async_to_streamed_response_wrapper( - tasks.get_user_uploaded_file, - ) diff --git a/src/browser_use_sdk/resources/users/__init__.py b/src/browser_use_sdk/resources/users/__init__.py deleted file mode 100644 index 8b1ed20..0000000 --- a/src/browser_use_sdk/resources/users/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .me import ( - MeResource, - AsyncMeResource, - MeResourceWithRawResponse, - AsyncMeResourceWithRawResponse, - MeResourceWithStreamingResponse, - AsyncMeResourceWithStreamingResponse, -) -from .users import ( - UsersResource, - AsyncUsersResource, - UsersResourceWithRawResponse, - AsyncUsersResourceWithRawResponse, - UsersResourceWithStreamingResponse, - AsyncUsersResourceWithStreamingResponse, -) - -__all__ = [ - "MeResource", - "AsyncMeResource", - "MeResourceWithRawResponse", - "AsyncMeResourceWithRawResponse", - "MeResourceWithStreamingResponse", - "AsyncMeResourceWithStreamingResponse", - "UsersResource", - "AsyncUsersResource", - "UsersResourceWithRawResponse", - "AsyncUsersResourceWithRawResponse", - "UsersResourceWithStreamingResponse", - "AsyncUsersResourceWithStreamingResponse", -] diff --git a/src/browser_use_sdk/resources/users/me/__init__.py b/src/browser_use_sdk/resources/users/me/__init__.py deleted file mode 100644 index 4409f6d..0000000 --- a/src/browser_use_sdk/resources/users/me/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .me import ( - MeResource, - AsyncMeResource, - MeResourceWithRawResponse, - AsyncMeResourceWithRawResponse, - MeResourceWithStreamingResponse, - AsyncMeResourceWithStreamingResponse, -) -from .files import ( - FilesResource, - AsyncFilesResource, - FilesResourceWithRawResponse, - AsyncFilesResourceWithRawResponse, - FilesResourceWithStreamingResponse, - AsyncFilesResourceWithStreamingResponse, -) - -__all__ = [ - "FilesResource", - "AsyncFilesResource", - "FilesResourceWithRawResponse", - "AsyncFilesResourceWithRawResponse", - "FilesResourceWithStreamingResponse", - "AsyncFilesResourceWithStreamingResponse", - "MeResource", - "AsyncMeResource", - "MeResourceWithRawResponse", - "AsyncMeResourceWithRawResponse", - "MeResourceWithStreamingResponse", - "AsyncMeResourceWithStreamingResponse", -] diff --git a/src/browser_use_sdk/resources/users/me/files.py b/src/browser_use_sdk/resources/users/me/files.py deleted file mode 100644 index 1468254..0000000 --- a/src/browser_use_sdk/resources/users/me/files.py +++ /dev/null @@ -1,269 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal - -import httpx - -from ...._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ...._utils import maybe_transform, async_maybe_transform -from ...._compat import cached_property -from ...._resource import SyncAPIResource, AsyncAPIResource -from ...._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from ...._base_client import make_request_options -from ....types.users.me import file_create_presigned_url_params -from ....types.users.me.file_create_presigned_url_response import FileCreatePresignedURLResponse - -__all__ = ["FilesResource", "AsyncFilesResource"] - - -class FilesResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> FilesResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return FilesResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> FilesResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return FilesResourceWithStreamingResponse(self) - - def create_presigned_url( - self, - *, - content_type: Literal[ - "image/jpg", - "image/jpeg", - "image/png", - "image/gif", - "image/webp", - "image/svg+xml", - "application/pdf", - "application/msword", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/vnd.ms-excel", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "text/plain", - "text/csv", - "text/markdown", - ], - file_name: str, - size_bytes: int, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> FileCreatePresignedURLResponse: - """ - Get a presigned URL for uploading files that AI agents can use during tasks. - - This endpoint generates a secure, time-limited upload URL that allows you to - upload files directly to our storage system. These files can then be referenced - in AI agent tasks for the agent to work with. - - Supported use cases: - - - Uploading documents for data extraction tasks - - Providing reference materials for agents - - Sharing files that agents need to process - - Including images or PDFs for analysis - - The upload URL expires after 2 minutes for security. Files are automatically - organized by user ID and can be referenced in task creation using the returned - file name. - - Args: - - - request: File upload details including name, content type, and size - - Returns: - - - Presigned upload URL and form fields for direct file upload - - Raises: - - - 400: If the content type is unsupported - - 500: If the upload URL generation fails (should not happen) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._post( - "/users/me/files/presigned-url", - body=maybe_transform( - { - "content_type": content_type, - "file_name": file_name, - "size_bytes": size_bytes, - }, - file_create_presigned_url_params.FileCreatePresignedURLParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=FileCreatePresignedURLResponse, - ) - - -class AsyncFilesResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncFilesResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return AsyncFilesResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncFilesResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return AsyncFilesResourceWithStreamingResponse(self) - - async def create_presigned_url( - self, - *, - content_type: Literal[ - "image/jpg", - "image/jpeg", - "image/png", - "image/gif", - "image/webp", - "image/svg+xml", - "application/pdf", - "application/msword", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/vnd.ms-excel", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "text/plain", - "text/csv", - "text/markdown", - ], - file_name: str, - size_bytes: int, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> FileCreatePresignedURLResponse: - """ - Get a presigned URL for uploading files that AI agents can use during tasks. - - This endpoint generates a secure, time-limited upload URL that allows you to - upload files directly to our storage system. These files can then be referenced - in AI agent tasks for the agent to work with. - - Supported use cases: - - - Uploading documents for data extraction tasks - - Providing reference materials for agents - - Sharing files that agents need to process - - Including images or PDFs for analysis - - The upload URL expires after 2 minutes for security. Files are automatically - organized by user ID and can be referenced in task creation using the returned - file name. - - Args: - - - request: File upload details including name, content type, and size - - Returns: - - - Presigned upload URL and form fields for direct file upload - - Raises: - - - 400: If the content type is unsupported - - 500: If the upload URL generation fails (should not happen) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return await self._post( - "/users/me/files/presigned-url", - body=await async_maybe_transform( - { - "content_type": content_type, - "file_name": file_name, - "size_bytes": size_bytes, - }, - file_create_presigned_url_params.FileCreatePresignedURLParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=FileCreatePresignedURLResponse, - ) - - -class FilesResourceWithRawResponse: - def __init__(self, files: FilesResource) -> None: - self._files = files - - self.create_presigned_url = to_raw_response_wrapper( - files.create_presigned_url, - ) - - -class AsyncFilesResourceWithRawResponse: - def __init__(self, files: AsyncFilesResource) -> None: - self._files = files - - self.create_presigned_url = async_to_raw_response_wrapper( - files.create_presigned_url, - ) - - -class FilesResourceWithStreamingResponse: - def __init__(self, files: FilesResource) -> None: - self._files = files - - self.create_presigned_url = to_streamed_response_wrapper( - files.create_presigned_url, - ) - - -class AsyncFilesResourceWithStreamingResponse: - def __init__(self, files: AsyncFilesResource) -> None: - self._files = files - - self.create_presigned_url = async_to_streamed_response_wrapper( - files.create_presigned_url, - ) diff --git a/src/browser_use_sdk/resources/users/me/me.py b/src/browser_use_sdk/resources/users/me/me.py deleted file mode 100644 index b63585c..0000000 --- a/src/browser_use_sdk/resources/users/me/me.py +++ /dev/null @@ -1,207 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import httpx - -from .files import ( - FilesResource, - AsyncFilesResource, - FilesResourceWithRawResponse, - AsyncFilesResourceWithRawResponse, - FilesResourceWithStreamingResponse, - AsyncFilesResourceWithStreamingResponse, -) -from ...._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ...._compat import cached_property -from ...._resource import SyncAPIResource, AsyncAPIResource -from ...._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from ...._base_client import make_request_options -from ....types.users.me_retrieve_response import MeRetrieveResponse - -__all__ = ["MeResource", "AsyncMeResource"] - - -class MeResource(SyncAPIResource): - @cached_property - def files(self) -> FilesResource: - return FilesResource(self._client) - - @cached_property - def with_raw_response(self) -> MeResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return MeResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> MeResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return MeResourceWithStreamingResponse(self) - - def retrieve( - self, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> MeRetrieveResponse: - """ - Get information about the currently authenticated user. - - Retrieves your user profile information including: - - - Credit balances (monthly and additional credits in USD) - - Account details (email, name, signup date) - - This endpoint is useful for: - - - Checking your remaining credits before running tasks - - Displaying user information in your application - - Returns: - - - Complete user profile information including credits and account details - - Raises: - - - 404: If the user profile cannot be found - """ - return self._get( - "/users/me", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=MeRetrieveResponse, - ) - - -class AsyncMeResource(AsyncAPIResource): - @cached_property - def files(self) -> AsyncFilesResource: - return AsyncFilesResource(self._client) - - @cached_property - def with_raw_response(self) -> AsyncMeResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return AsyncMeResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncMeResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return AsyncMeResourceWithStreamingResponse(self) - - async def retrieve( - self, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> MeRetrieveResponse: - """ - Get information about the currently authenticated user. - - Retrieves your user profile information including: - - - Credit balances (monthly and additional credits in USD) - - Account details (email, name, signup date) - - This endpoint is useful for: - - - Checking your remaining credits before running tasks - - Displaying user information in your application - - Returns: - - - Complete user profile information including credits and account details - - Raises: - - - 404: If the user profile cannot be found - """ - return await self._get( - "/users/me", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=MeRetrieveResponse, - ) - - -class MeResourceWithRawResponse: - def __init__(self, me: MeResource) -> None: - self._me = me - - self.retrieve = to_raw_response_wrapper( - me.retrieve, - ) - - @cached_property - def files(self) -> FilesResourceWithRawResponse: - return FilesResourceWithRawResponse(self._me.files) - - -class AsyncMeResourceWithRawResponse: - def __init__(self, me: AsyncMeResource) -> None: - self._me = me - - self.retrieve = async_to_raw_response_wrapper( - me.retrieve, - ) - - @cached_property - def files(self) -> AsyncFilesResourceWithRawResponse: - return AsyncFilesResourceWithRawResponse(self._me.files) - - -class MeResourceWithStreamingResponse: - def __init__(self, me: MeResource) -> None: - self._me = me - - self.retrieve = to_streamed_response_wrapper( - me.retrieve, - ) - - @cached_property - def files(self) -> FilesResourceWithStreamingResponse: - return FilesResourceWithStreamingResponse(self._me.files) - - -class AsyncMeResourceWithStreamingResponse: - def __init__(self, me: AsyncMeResource) -> None: - self._me = me - - self.retrieve = async_to_streamed_response_wrapper( - me.retrieve, - ) - - @cached_property - def files(self) -> AsyncFilesResourceWithStreamingResponse: - return AsyncFilesResourceWithStreamingResponse(self._me.files) diff --git a/src/browser_use_sdk/resources/users/users.py b/src/browser_use_sdk/resources/users/users.py deleted file mode 100644 index 95dfc93..0000000 --- a/src/browser_use_sdk/resources/users/users.py +++ /dev/null @@ -1,102 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .me.me import ( - MeResource, - AsyncMeResource, - MeResourceWithRawResponse, - AsyncMeResourceWithRawResponse, - MeResourceWithStreamingResponse, - AsyncMeResourceWithStreamingResponse, -) -from ..._compat import cached_property -from ..._resource import SyncAPIResource, AsyncAPIResource - -__all__ = ["UsersResource", "AsyncUsersResource"] - - -class UsersResource(SyncAPIResource): - @cached_property - def me(self) -> MeResource: - return MeResource(self._client) - - @cached_property - def with_raw_response(self) -> UsersResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return UsersResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> UsersResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return UsersResourceWithStreamingResponse(self) - - -class AsyncUsersResource(AsyncAPIResource): - @cached_property - def me(self) -> AsyncMeResource: - return AsyncMeResource(self._client) - - @cached_property - def with_raw_response(self) -> AsyncUsersResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/browser-use/browser-use-python#accessing-raw-response-data-eg-headers - """ - return AsyncUsersResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncUsersResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/browser-use/browser-use-python#with_streaming_response - """ - return AsyncUsersResourceWithStreamingResponse(self) - - -class UsersResourceWithRawResponse: - def __init__(self, users: UsersResource) -> None: - self._users = users - - @cached_property - def me(self) -> MeResourceWithRawResponse: - return MeResourceWithRawResponse(self._users.me) - - -class AsyncUsersResourceWithRawResponse: - def __init__(self, users: AsyncUsersResource) -> None: - self._users = users - - @cached_property - def me(self) -> AsyncMeResourceWithRawResponse: - return AsyncMeResourceWithRawResponse(self._users.me) - - -class UsersResourceWithStreamingResponse: - def __init__(self, users: UsersResource) -> None: - self._users = users - - @cached_property - def me(self) -> MeResourceWithStreamingResponse: - return MeResourceWithStreamingResponse(self._users.me) - - -class AsyncUsersResourceWithStreamingResponse: - def __init__(self, users: AsyncUsersResource) -> None: - self._users = users - - @cached_property - def me(self) -> AsyncMeResourceWithStreamingResponse: - return AsyncMeResourceWithStreamingResponse(self._users.me) diff --git a/src/browser_use_sdk/types/__init__.py b/src/browser_use_sdk/types/__init__.py deleted file mode 100644 index 191c708..0000000 --- a/src/browser_use_sdk/types/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .file_view import FileView as FileView -from .task_view import TaskView as TaskView -from .task_status import TaskStatus as TaskStatus -from .session_view import SessionView as SessionView -from .session_status import SessionStatus as SessionStatus -from .task_item_view import TaskItemView as TaskItemView -from .task_step_view import TaskStepView as TaskStepView -from .task_list_params import TaskListParams as TaskListParams -from .agent_profile_view import AgentProfileView as AgentProfileView -from .proxy_country_code import ProxyCountryCode as ProxyCountryCode -from .task_create_params import TaskCreateParams as TaskCreateParams -from .task_list_response import TaskListResponse as TaskListResponse -from .task_update_params import TaskUpdateParams as TaskUpdateParams -from .session_list_params import SessionListParams as SessionListParams -from .browser_profile_view import BrowserProfileView as BrowserProfileView -from .task_create_response import TaskCreateResponse as TaskCreateResponse -from .session_list_response import SessionListResponse as SessionListResponse -from .session_update_params import SessionUpdateParams as SessionUpdateParams -from .task_get_logs_response import TaskGetLogsResponse as TaskGetLogsResponse -from .agent_profile_list_params import AgentProfileListParams as AgentProfileListParams -from .agent_profile_create_params import AgentProfileCreateParams as AgentProfileCreateParams -from .agent_profile_list_response import AgentProfileListResponse as AgentProfileListResponse -from .agent_profile_update_params import AgentProfileUpdateParams as AgentProfileUpdateParams -from .browser_profile_list_params import BrowserProfileListParams as BrowserProfileListParams -from .browser_profile_create_params import BrowserProfileCreateParams as BrowserProfileCreateParams -from .browser_profile_list_response import BrowserProfileListResponse as BrowserProfileListResponse -from .browser_profile_update_params import BrowserProfileUpdateParams as BrowserProfileUpdateParams -from .task_get_output_file_response import TaskGetOutputFileResponse as TaskGetOutputFileResponse -from .task_get_user_uploaded_file_response import TaskGetUserUploadedFileResponse as TaskGetUserUploadedFileResponse diff --git a/src/browser_use_sdk/types/agent_profile_create_params.py b/src/browser_use_sdk/types/agent_profile_create_params.py deleted file mode 100644 index ef3080b..0000000 --- a/src/browser_use_sdk/types/agent_profile_create_params.py +++ /dev/null @@ -1,30 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import List -from typing_extensions import Required, Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["AgentProfileCreateParams"] - - -class AgentProfileCreateParams(TypedDict, total=False): - name: Required[str] - - allowed_domains: Annotated[List[str], PropertyInfo(alias="allowedDomains")] - - custom_system_prompt_extension: Annotated[str, PropertyInfo(alias="customSystemPromptExtension")] - - description: str - - flash_mode: Annotated[bool, PropertyInfo(alias="flashMode")] - - highlight_elements: Annotated[bool, PropertyInfo(alias="highlightElements")] - - max_agent_steps: Annotated[int, PropertyInfo(alias="maxAgentSteps")] - - thinking: bool - - vision: bool diff --git a/src/browser_use_sdk/types/agent_profile_list_params.py b/src/browser_use_sdk/types/agent_profile_list_params.py deleted file mode 100644 index 072292c..0000000 --- a/src/browser_use_sdk/types/agent_profile_list_params.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["AgentProfileListParams"] - - -class AgentProfileListParams(TypedDict, total=False): - page_number: Annotated[int, PropertyInfo(alias="pageNumber")] - - page_size: Annotated[int, PropertyInfo(alias="pageSize")] diff --git a/src/browser_use_sdk/types/agent_profile_list_response.py b/src/browser_use_sdk/types/agent_profile_list_response.py deleted file mode 100644 index 3f77c1a..0000000 --- a/src/browser_use_sdk/types/agent_profile_list_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List - -from pydantic import Field as FieldInfo - -from .._models import BaseModel -from .agent_profile_view import AgentProfileView - -__all__ = ["AgentProfileListResponse"] - - -class AgentProfileListResponse(BaseModel): - items: List[AgentProfileView] - - page_number: int = FieldInfo(alias="pageNumber") - - page_size: int = FieldInfo(alias="pageSize") - - total_items: int = FieldInfo(alias="totalItems") diff --git a/src/browser_use_sdk/types/agent_profile_update_params.py b/src/browser_use_sdk/types/agent_profile_update_params.py deleted file mode 100644 index 8595815..0000000 --- a/src/browser_use_sdk/types/agent_profile_update_params.py +++ /dev/null @@ -1,30 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import List, Optional -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["AgentProfileUpdateParams"] - - -class AgentProfileUpdateParams(TypedDict, total=False): - allowed_domains: Annotated[Optional[List[str]], PropertyInfo(alias="allowedDomains")] - - custom_system_prompt_extension: Annotated[Optional[str], PropertyInfo(alias="customSystemPromptExtension")] - - description: Optional[str] - - flash_mode: Annotated[Optional[bool], PropertyInfo(alias="flashMode")] - - highlight_elements: Annotated[Optional[bool], PropertyInfo(alias="highlightElements")] - - max_agent_steps: Annotated[Optional[int], PropertyInfo(alias="maxAgentSteps")] - - name: Optional[str] - - thinking: Optional[bool] - - vision: Optional[bool] diff --git a/src/browser_use_sdk/types/agent_profile_view.py b/src/browser_use_sdk/types/agent_profile_view.py deleted file mode 100644 index 1c10f12..0000000 --- a/src/browser_use_sdk/types/agent_profile_view.py +++ /dev/null @@ -1,36 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List -from datetime import datetime - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["AgentProfileView"] - - -class AgentProfileView(BaseModel): - id: str - - allowed_domains: List[str] = FieldInfo(alias="allowedDomains") - - created_at: datetime = FieldInfo(alias="createdAt") - - custom_system_prompt_extension: str = FieldInfo(alias="customSystemPromptExtension") - - description: str - - flash_mode: bool = FieldInfo(alias="flashMode") - - highlight_elements: bool = FieldInfo(alias="highlightElements") - - max_agent_steps: int = FieldInfo(alias="maxAgentSteps") - - name: str - - thinking: bool - - updated_at: datetime = FieldInfo(alias="updatedAt") - - vision: bool diff --git a/src/browser_use_sdk/types/browser_profile_create_params.py b/src/browser_use_sdk/types/browser_profile_create_params.py deleted file mode 100644 index 57a2e97..0000000 --- a/src/browser_use_sdk/types/browser_profile_create_params.py +++ /dev/null @@ -1,32 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, Annotated, TypedDict - -from .._utils import PropertyInfo -from .proxy_country_code import ProxyCountryCode - -__all__ = ["BrowserProfileCreateParams"] - - -class BrowserProfileCreateParams(TypedDict, total=False): - name: Required[str] - - ad_blocker: Annotated[bool, PropertyInfo(alias="adBlocker")] - - browser_viewport_height: Annotated[int, PropertyInfo(alias="browserViewportHeight")] - - browser_viewport_width: Annotated[int, PropertyInfo(alias="browserViewportWidth")] - - description: str - - is_mobile: Annotated[bool, PropertyInfo(alias="isMobile")] - - persist: bool - - proxy: bool - - proxy_country_code: Annotated[ProxyCountryCode, PropertyInfo(alias="proxyCountryCode")] - - store_cache: Annotated[bool, PropertyInfo(alias="storeCache")] diff --git a/src/browser_use_sdk/types/browser_profile_list_params.py b/src/browser_use_sdk/types/browser_profile_list_params.py deleted file mode 100644 index bb03c96..0000000 --- a/src/browser_use_sdk/types/browser_profile_list_params.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["BrowserProfileListParams"] - - -class BrowserProfileListParams(TypedDict, total=False): - page_number: Annotated[int, PropertyInfo(alias="pageNumber")] - - page_size: Annotated[int, PropertyInfo(alias="pageSize")] diff --git a/src/browser_use_sdk/types/browser_profile_list_response.py b/src/browser_use_sdk/types/browser_profile_list_response.py deleted file mode 100644 index 02d22ae..0000000 --- a/src/browser_use_sdk/types/browser_profile_list_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List - -from pydantic import Field as FieldInfo - -from .._models import BaseModel -from .browser_profile_view import BrowserProfileView - -__all__ = ["BrowserProfileListResponse"] - - -class BrowserProfileListResponse(BaseModel): - items: List[BrowserProfileView] - - page_number: int = FieldInfo(alias="pageNumber") - - page_size: int = FieldInfo(alias="pageSize") - - total_items: int = FieldInfo(alias="totalItems") diff --git a/src/browser_use_sdk/types/browser_profile_update_params.py b/src/browser_use_sdk/types/browser_profile_update_params.py deleted file mode 100644 index 5305fa1..0000000 --- a/src/browser_use_sdk/types/browser_profile_update_params.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Optional -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo -from .proxy_country_code import ProxyCountryCode - -__all__ = ["BrowserProfileUpdateParams"] - - -class BrowserProfileUpdateParams(TypedDict, total=False): - ad_blocker: Annotated[Optional[bool], PropertyInfo(alias="adBlocker")] - - browser_viewport_height: Annotated[Optional[int], PropertyInfo(alias="browserViewportHeight")] - - browser_viewport_width: Annotated[Optional[int], PropertyInfo(alias="browserViewportWidth")] - - description: Optional[str] - - is_mobile: Annotated[Optional[bool], PropertyInfo(alias="isMobile")] - - name: Optional[str] - - persist: Optional[bool] - - proxy: Optional[bool] - - proxy_country_code: Annotated[Optional[ProxyCountryCode], PropertyInfo(alias="proxyCountryCode")] - - store_cache: Annotated[Optional[bool], PropertyInfo(alias="storeCache")] diff --git a/src/browser_use_sdk/types/browser_profile_view.py b/src/browser_use_sdk/types/browser_profile_view.py deleted file mode 100644 index b9384ed..0000000 --- a/src/browser_use_sdk/types/browser_profile_view.py +++ /dev/null @@ -1,38 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from datetime import datetime - -from pydantic import Field as FieldInfo - -from .._models import BaseModel -from .proxy_country_code import ProxyCountryCode - -__all__ = ["BrowserProfileView"] - - -class BrowserProfileView(BaseModel): - id: str - - ad_blocker: bool = FieldInfo(alias="adBlocker") - - browser_viewport_height: int = FieldInfo(alias="browserViewportHeight") - - browser_viewport_width: int = FieldInfo(alias="browserViewportWidth") - - created_at: datetime = FieldInfo(alias="createdAt") - - description: str - - is_mobile: bool = FieldInfo(alias="isMobile") - - name: str - - persist: bool - - proxy: bool - - proxy_country_code: ProxyCountryCode = FieldInfo(alias="proxyCountryCode") - - store_cache: bool = FieldInfo(alias="storeCache") - - updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/browser_use_sdk/types/file_view.py b/src/browser_use_sdk/types/file_view.py deleted file mode 100644 index 620c57c..0000000 --- a/src/browser_use_sdk/types/file_view.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["FileView"] - - -class FileView(BaseModel): - id: str - - file_name: str = FieldInfo(alias="fileName") diff --git a/src/browser_use_sdk/types/proxy_country_code.py b/src/browser_use_sdk/types/proxy_country_code.py deleted file mode 100644 index b60c0fa..0000000 --- a/src/browser_use_sdk/types/proxy_country_code.py +++ /dev/null @@ -1,7 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing_extensions import Literal, TypeAlias - -__all__ = ["ProxyCountryCode"] - -ProxyCountryCode: TypeAlias = Literal["us", "uk", "fr", "it", "jp", "au", "de", "fi", "ca", "in"] diff --git a/src/browser_use_sdk/types/session_list_params.py b/src/browser_use_sdk/types/session_list_params.py deleted file mode 100644 index 0bd7b72..0000000 --- a/src/browser_use_sdk/types/session_list_params.py +++ /dev/null @@ -1,24 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Optional -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo -from .session_status import SessionStatus - -__all__ = ["SessionListParams"] - - -class SessionListParams(TypedDict, total=False): - filter_by: Annotated[Optional[SessionStatus], PropertyInfo(alias="filterBy")] - """Enumeration of possible (browser) session states - - Attributes: ACTIVE: Session is currently active and running (browser is running) - STOPPED: Session has been stopped and is no longer active (browser is stopped) - """ - - page_number: Annotated[int, PropertyInfo(alias="pageNumber")] - - page_size: Annotated[int, PropertyInfo(alias="pageSize")] diff --git a/src/browser_use_sdk/types/session_list_response.py b/src/browser_use_sdk/types/session_list_response.py deleted file mode 100644 index 0958cfa..0000000 --- a/src/browser_use_sdk/types/session_list_response.py +++ /dev/null @@ -1,38 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional -from datetime import datetime - -from pydantic import Field as FieldInfo - -from .._models import BaseModel -from .session_status import SessionStatus - -__all__ = ["SessionListResponse", "Item"] - - -class Item(BaseModel): - id: str - - started_at: datetime = FieldInfo(alias="startedAt") - - status: SessionStatus - """Enumeration of possible (browser) session states - - Attributes: ACTIVE: Session is currently active and running (browser is running) - STOPPED: Session has been stopped and is no longer active (browser is stopped) - """ - - finished_at: Optional[datetime] = FieldInfo(alias="finishedAt", default=None) - - live_url: Optional[str] = FieldInfo(alias="liveUrl", default=None) - - -class SessionListResponse(BaseModel): - items: List[Item] - - page_number: int = FieldInfo(alias="pageNumber") - - page_size: int = FieldInfo(alias="pageSize") - - total_items: int = FieldInfo(alias="totalItems") diff --git a/src/browser_use_sdk/types/session_status.py b/src/browser_use_sdk/types/session_status.py deleted file mode 100644 index dc8ad95..0000000 --- a/src/browser_use_sdk/types/session_status.py +++ /dev/null @@ -1,7 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing_extensions import Literal, TypeAlias - -__all__ = ["SessionStatus"] - -SessionStatus: TypeAlias = Literal["active", "stopped"] diff --git a/src/browser_use_sdk/types/session_update_params.py b/src/browser_use_sdk/types/session_update_params.py deleted file mode 100644 index 92734a7..0000000 --- a/src/browser_use_sdk/types/session_update_params.py +++ /dev/null @@ -1,16 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Required, TypedDict - -__all__ = ["SessionUpdateParams"] - - -class SessionUpdateParams(TypedDict, total=False): - action: Required[Literal["stop"]] - """Available actions that can be performed on a session - - Attributes: STOP: Stop the session and all its associated tasks (cannot be - undone) - """ diff --git a/src/browser_use_sdk/types/session_view.py b/src/browser_use_sdk/types/session_view.py deleted file mode 100644 index 492e4aa..0000000 --- a/src/browser_use_sdk/types/session_view.py +++ /dev/null @@ -1,35 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional -from datetime import datetime - -from pydantic import Field as FieldInfo - -from .._models import BaseModel -from .session_status import SessionStatus -from .task_item_view import TaskItemView - -__all__ = ["SessionView"] - - -class SessionView(BaseModel): - id: str - - started_at: datetime = FieldInfo(alias="startedAt") - - status: SessionStatus - """Enumeration of possible (browser) session states - - Attributes: ACTIVE: Session is currently active and running (browser is running) - STOPPED: Session has been stopped and is no longer active (browser is stopped) - """ - - finished_at: Optional[datetime] = FieldInfo(alias="finishedAt", default=None) - - live_url: Optional[str] = FieldInfo(alias="liveUrl", default=None) - - public_share_url: Optional[str] = FieldInfo(alias="publicShareUrl", default=None) - - record_url: Optional[str] = FieldInfo(alias="recordUrl", default=None) - - tasks: Optional[List[TaskItemView]] = None diff --git a/src/browser_use_sdk/types/sessions/__init__.py b/src/browser_use_sdk/types/sessions/__init__.py deleted file mode 100644 index ecf1857..0000000 --- a/src/browser_use_sdk/types/sessions/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .share_view import ShareView as ShareView diff --git a/src/browser_use_sdk/types/sessions/share_view.py b/src/browser_use_sdk/types/sessions/share_view.py deleted file mode 100644 index 2360441..0000000 --- a/src/browser_use_sdk/types/sessions/share_view.py +++ /dev/null @@ -1,20 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from datetime import datetime - -from pydantic import Field as FieldInfo - -from ..._models import BaseModel - -__all__ = ["ShareView"] - - -class ShareView(BaseModel): - share_token: str = FieldInfo(alias="shareToken") - - share_url: str = FieldInfo(alias="shareUrl") - - view_count: int = FieldInfo(alias="viewCount") - - last_viewed_at: Optional[datetime] = FieldInfo(alias="lastViewedAt", default=None) diff --git a/src/browser_use_sdk/types/task_create_params.py b/src/browser_use_sdk/types/task_create_params.py deleted file mode 100644 index 0585011..0000000 --- a/src/browser_use_sdk/types/task_create_params.py +++ /dev/null @@ -1,64 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, List, Optional -from typing_extensions import Literal, Required, Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["TaskCreateParams", "AgentSettings", "BrowserSettings"] - - -class TaskCreateParams(TypedDict, total=False): - task: Required[str] - - agent_settings: Annotated[AgentSettings, PropertyInfo(alias="agentSettings")] - """Configuration settings for the agent - - Attributes: llm: The LLM model to use for the agent start_url: Optional URL to - start the agent on (will not be changed as a step) profile_id: Unique identifier - of the agent profile to use for the task - """ - - browser_settings: Annotated[BrowserSettings, PropertyInfo(alias="browserSettings")] - """Configuration settings for the browser session - - Attributes: session_id: Unique identifier of existing session to continue - profile_id: Unique identifier of browser profile to use (use if you want to - start a new session) - """ - - included_file_names: Annotated[Optional[List[str]], PropertyInfo(alias="includedFileNames")] - - metadata: Optional[Dict[str, str]] - - secrets: Optional[Dict[str, str]] - - structured_output_json: Annotated[Optional[str], PropertyInfo(alias="structuredOutputJson")] - - -class AgentSettings(TypedDict, total=False): - llm: Literal[ - "gpt-4.1", - "gpt-4.1-mini", - "o4-mini", - "o3", - "gemini-2.5-flash", - "gemini-2.5-pro", - "claude-sonnet-4-20250514", - "gpt-4o", - "gpt-4o-mini", - "llama-4-maverick-17b-128e-instruct", - "claude-3-7-sonnet-20250219", - ] - - profile_id: Annotated[Optional[str], PropertyInfo(alias="profileId")] - - start_url: Annotated[Optional[str], PropertyInfo(alias="startUrl")] - - -class BrowserSettings(TypedDict, total=False): - profile_id: Annotated[Optional[str], PropertyInfo(alias="profileId")] - - session_id: Annotated[Optional[str], PropertyInfo(alias="sessionId")] diff --git a/src/browser_use_sdk/types/task_create_response.py b/src/browser_use_sdk/types/task_create_response.py deleted file mode 100644 index b17ab00..0000000 --- a/src/browser_use_sdk/types/task_create_response.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["TaskCreateResponse"] - - -class TaskCreateResponse(BaseModel): - id: str - - session_id: str = FieldInfo(alias="sessionId") diff --git a/src/browser_use_sdk/types/task_get_logs_response.py b/src/browser_use_sdk/types/task_get_logs_response.py deleted file mode 100644 index 5bc035c..0000000 --- a/src/browser_use_sdk/types/task_get_logs_response.py +++ /dev/null @@ -1,11 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["TaskGetLogsResponse"] - - -class TaskGetLogsResponse(BaseModel): - download_url: str = FieldInfo(alias="downloadUrl") diff --git a/src/browser_use_sdk/types/task_get_output_file_response.py b/src/browser_use_sdk/types/task_get_output_file_response.py deleted file mode 100644 index 812ac90..0000000 --- a/src/browser_use_sdk/types/task_get_output_file_response.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["TaskGetOutputFileResponse"] - - -class TaskGetOutputFileResponse(BaseModel): - id: str - - download_url: str = FieldInfo(alias="downloadUrl") - - file_name: str = FieldInfo(alias="fileName") diff --git a/src/browser_use_sdk/types/task_get_user_uploaded_file_response.py b/src/browser_use_sdk/types/task_get_user_uploaded_file_response.py deleted file mode 100644 index f802207..0000000 --- a/src/browser_use_sdk/types/task_get_user_uploaded_file_response.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["TaskGetUserUploadedFileResponse"] - - -class TaskGetUserUploadedFileResponse(BaseModel): - id: str - - download_url: str = FieldInfo(alias="downloadUrl") - - file_name: str = FieldInfo(alias="fileName") diff --git a/src/browser_use_sdk/types/task_item_view.py b/src/browser_use_sdk/types/task_item_view.py deleted file mode 100644 index 695e846..0000000 --- a/src/browser_use_sdk/types/task_item_view.py +++ /dev/null @@ -1,44 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, Optional -from datetime import datetime - -from pydantic import Field as FieldInfo - -from .._models import BaseModel -from .task_status import TaskStatus - -__all__ = ["TaskItemView"] - - -class TaskItemView(BaseModel): - id: str - - is_scheduled: bool = FieldInfo(alias="isScheduled") - - llm: str - - session_id: str = FieldInfo(alias="sessionId") - - started_at: datetime = FieldInfo(alias="startedAt") - - status: TaskStatus - """Enumeration of possible task execution states - - Attributes: STARTED: Task has been started and is currently running. PAUSED: - Task execution has been temporarily paused (can be resumed) FINISHED: Task has - finished and the agent has completed the task. STOPPED: Task execution has been - manually stopped (cannot be resumed). - """ - - task: str - - browser_use_version: Optional[str] = FieldInfo(alias="browserUseVersion", default=None) - - done_output: Optional[str] = FieldInfo(alias="doneOutput", default=None) - - finished_at: Optional[datetime] = FieldInfo(alias="finishedAt", default=None) - - is_success: Optional[bool] = FieldInfo(alias="isSuccess", default=None) - - metadata: Optional[Dict[str, object]] = None diff --git a/src/browser_use_sdk/types/task_list_params.py b/src/browser_use_sdk/types/task_list_params.py deleted file mode 100644 index 0d83aaa..0000000 --- a/src/browser_use_sdk/types/task_list_params.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Union, Optional -from datetime import datetime -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo -from .task_status import TaskStatus - -__all__ = ["TaskListParams"] - - -class TaskListParams(TypedDict, total=False): - after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")] - - before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")] - - filter_by: Annotated[Optional[TaskStatus], PropertyInfo(alias="filterBy")] - """Enumeration of possible task execution states - - Attributes: STARTED: Task has been started and is currently running. PAUSED: - Task execution has been temporarily paused (can be resumed) FINISHED: Task has - finished and the agent has completed the task. STOPPED: Task execution has been - manually stopped (cannot be resumed). - """ - - page_number: Annotated[int, PropertyInfo(alias="pageNumber")] - - page_size: Annotated[int, PropertyInfo(alias="pageSize")] - - session_id: Annotated[Optional[str], PropertyInfo(alias="sessionId")] diff --git a/src/browser_use_sdk/types/task_list_response.py b/src/browser_use_sdk/types/task_list_response.py deleted file mode 100644 index de14f6e..0000000 --- a/src/browser_use_sdk/types/task_list_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List - -from pydantic import Field as FieldInfo - -from .._models import BaseModel -from .task_item_view import TaskItemView - -__all__ = ["TaskListResponse"] - - -class TaskListResponse(BaseModel): - items: List[TaskItemView] - - page_number: int = FieldInfo(alias="pageNumber") - - page_size: int = FieldInfo(alias="pageSize") - - total_items: int = FieldInfo(alias="totalItems") diff --git a/src/browser_use_sdk/types/task_status.py b/src/browser_use_sdk/types/task_status.py deleted file mode 100644 index 0eabe70..0000000 --- a/src/browser_use_sdk/types/task_status.py +++ /dev/null @@ -1,7 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing_extensions import Literal, TypeAlias - -__all__ = ["TaskStatus"] - -TaskStatus: TypeAlias = Literal["started", "paused", "finished", "stopped"] diff --git a/src/browser_use_sdk/types/task_step_view.py b/src/browser_use_sdk/types/task_step_view.py deleted file mode 100644 index b32e08c..0000000 --- a/src/browser_use_sdk/types/task_step_view.py +++ /dev/null @@ -1,25 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["TaskStepView"] - - -class TaskStepView(BaseModel): - actions: List[str] - - evaluation_previous_goal: str = FieldInfo(alias="evaluationPreviousGoal") - - memory: str - - next_goal: str = FieldInfo(alias="nextGoal") - - number: int - - url: str - - screenshot_url: Optional[str] = FieldInfo(alias="screenshotUrl", default=None) diff --git a/src/browser_use_sdk/types/task_update_params.py b/src/browser_use_sdk/types/task_update_params.py deleted file mode 100644 index 3ab9614..0000000 --- a/src/browser_use_sdk/types/task_update_params.py +++ /dev/null @@ -1,17 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Required, TypedDict - -__all__ = ["TaskUpdateParams"] - - -class TaskUpdateParams(TypedDict, total=False): - action: Required[Literal["stop", "pause", "resume", "stop_task_and_session"]] - """Available actions that can be performed on a task - - Attributes: STOP: Stop the current task execution PAUSE: Pause the current task - execution RESUME: Resume a paused task execution STOP_TASK_AND_SESSION: Stop - both the task and its parent session - """ diff --git a/src/browser_use_sdk/types/task_view.py b/src/browser_use_sdk/types/task_view.py deleted file mode 100644 index 6eef227..0000000 --- a/src/browser_use_sdk/types/task_view.py +++ /dev/null @@ -1,79 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, List, Optional -from datetime import datetime -from typing_extensions import Literal - -from pydantic import Field as FieldInfo - -from .._models import BaseModel -from .file_view import FileView -from .task_status import TaskStatus -from .task_step_view import TaskStepView - -__all__ = ["TaskView", "Session"] - - -class Session(BaseModel): - id: str - - started_at: datetime = FieldInfo(alias="startedAt") - - status: Literal["active", "stopped"] - """Enumeration of possible (browser) session states - - Attributes: ACTIVE: Session is currently active and running (browser is running) - STOPPED: Session has been stopped and is no longer active (browser is stopped) - """ - - finished_at: Optional[datetime] = FieldInfo(alias="finishedAt", default=None) - - live_url: Optional[str] = FieldInfo(alias="liveUrl", default=None) - - -class TaskView(BaseModel): - id: str - - is_scheduled: bool = FieldInfo(alias="isScheduled") - - llm: str - - output_files: List[FileView] = FieldInfo(alias="outputFiles") - - session: Session - """View model for representing a session that a task belongs to - - Attributes: id: Unique identifier for the session status: Current status of the - session (active/stopped) live_url: URL where the browser can be viewed live in - real-time. started_at: Timestamp when the session was created and started. - finished_at: Timestamp when the session was stopped (None if still active). - """ - - session_id: str = FieldInfo(alias="sessionId") - - started_at: datetime = FieldInfo(alias="startedAt") - - status: TaskStatus - """Enumeration of possible task execution states - - Attributes: STARTED: Task has been started and is currently running. PAUSED: - Task execution has been temporarily paused (can be resumed) FINISHED: Task has - finished and the agent has completed the task. STOPPED: Task execution has been - manually stopped (cannot be resumed). - """ - - steps: List[TaskStepView] - - task: str - - user_uploaded_files: List[FileView] = FieldInfo(alias="userUploadedFiles") - - browser_use_version: Optional[str] = FieldInfo(alias="browserUseVersion", default=None) - - done_output: Optional[str] = FieldInfo(alias="doneOutput", default=None) - - finished_at: Optional[datetime] = FieldInfo(alias="finishedAt", default=None) - - is_success: Optional[bool] = FieldInfo(alias="isSuccess", default=None) - - metadata: Optional[Dict[str, object]] = None diff --git a/src/browser_use_sdk/types/users/__init__.py b/src/browser_use_sdk/types/users/__init__.py deleted file mode 100644 index de5007b..0000000 --- a/src/browser_use_sdk/types/users/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .me_retrieve_response import MeRetrieveResponse as MeRetrieveResponse diff --git a/src/browser_use_sdk/types/users/me/__init__.py b/src/browser_use_sdk/types/users/me/__init__.py deleted file mode 100644 index 27f2334..0000000 --- a/src/browser_use_sdk/types/users/me/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .file_create_presigned_url_params import FileCreatePresignedURLParams as FileCreatePresignedURLParams -from .file_create_presigned_url_response import FileCreatePresignedURLResponse as FileCreatePresignedURLResponse diff --git a/src/browser_use_sdk/types/users/me/file_create_presigned_url_params.py b/src/browser_use_sdk/types/users/me/file_create_presigned_url_params.py deleted file mode 100644 index 368b0c9..0000000 --- a/src/browser_use_sdk/types/users/me/file_create_presigned_url_params.py +++ /dev/null @@ -1,37 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Required, Annotated, TypedDict - -from ...._utils import PropertyInfo - -__all__ = ["FileCreatePresignedURLParams"] - - -class FileCreatePresignedURLParams(TypedDict, total=False): - content_type: Required[ - Annotated[ - Literal[ - "image/jpg", - "image/jpeg", - "image/png", - "image/gif", - "image/webp", - "image/svg+xml", - "application/pdf", - "application/msword", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/vnd.ms-excel", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "text/plain", - "text/csv", - "text/markdown", - ], - PropertyInfo(alias="contentType"), - ] - ] - - file_name: Required[Annotated[str, PropertyInfo(alias="fileName")]] - - size_bytes: Required[Annotated[int, PropertyInfo(alias="sizeBytes")]] diff --git a/src/browser_use_sdk/types/users/me/file_create_presigned_url_response.py b/src/browser_use_sdk/types/users/me/file_create_presigned_url_response.py deleted file mode 100644 index f3dc32f..0000000 --- a/src/browser_use_sdk/types/users/me/file_create_presigned_url_response.py +++ /dev/null @@ -1,22 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict -from typing_extensions import Literal - -from pydantic import Field as FieldInfo - -from ...._models import BaseModel - -__all__ = ["FileCreatePresignedURLResponse"] - - -class FileCreatePresignedURLResponse(BaseModel): - expires_in: int = FieldInfo(alias="expiresIn") - - fields: Dict[str, str] - - file_name: str = FieldInfo(alias="fileName") - - method: Literal["POST"] - - url: str diff --git a/src/browser_use_sdk/types/users/me_retrieve_response.py b/src/browser_use_sdk/types/users/me_retrieve_response.py deleted file mode 100644 index 75d83b0..0000000 --- a/src/browser_use_sdk/types/users/me_retrieve_response.py +++ /dev/null @@ -1,22 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from datetime import datetime - -from pydantic import Field as FieldInfo - -from ..._models import BaseModel - -__all__ = ["MeRetrieveResponse"] - - -class MeRetrieveResponse(BaseModel): - additional_credits_balance_usd: float = FieldInfo(alias="additionalCreditsBalanceUsd") - - monthly_credits_balance_usd: float = FieldInfo(alias="monthlyCreditsBalanceUsd") - - signed_up_at: datetime = FieldInfo(alias="signedUpAt") - - email: Optional[str] = None - - name: Optional[str] = None diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index fd8019a..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/__init__.py b/tests/api_resources/__init__.py deleted file mode 100644 index fd8019a..0000000 --- a/tests/api_resources/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/sessions/__init__.py b/tests/api_resources/sessions/__init__.py deleted file mode 100644 index fd8019a..0000000 --- a/tests/api_resources/sessions/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/sessions/test_public_share.py b/tests/api_resources/sessions/test_public_share.py deleted file mode 100644 index 1b877cb..0000000 --- a/tests/api_resources/sessions/test_public_share.py +++ /dev/null @@ -1,276 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from tests.utils import assert_matches_type -from browser_use_sdk import BrowserUse, AsyncBrowserUse -from browser_use_sdk.types.sessions import ShareView - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestPublicShare: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create(self, client: BrowserUse) -> None: - public_share = client.sessions.public_share.create( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(ShareView, public_share, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_create(self, client: BrowserUse) -> None: - response = client.sessions.public_share.with_raw_response.create( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - public_share = response.parse() - assert_matches_type(ShareView, public_share, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_create(self, client: BrowserUse) -> None: - with client.sessions.public_share.with_streaming_response.create( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - public_share = response.parse() - assert_matches_type(ShareView, public_share, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_create(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.public_share.with_raw_response.create( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_retrieve(self, client: BrowserUse) -> None: - public_share = client.sessions.public_share.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(ShareView, public_share, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: BrowserUse) -> None: - response = client.sessions.public_share.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - public_share = response.parse() - assert_matches_type(ShareView, public_share, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: BrowserUse) -> None: - with client.sessions.public_share.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - public_share = response.parse() - assert_matches_type(ShareView, public_share, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_retrieve(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.public_share.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_delete(self, client: BrowserUse) -> None: - public_share = client.sessions.public_share.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert public_share is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_delete(self, client: BrowserUse) -> None: - response = client.sessions.public_share.with_raw_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - public_share = response.parse() - assert public_share is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_delete(self, client: BrowserUse) -> None: - with client.sessions.public_share.with_streaming_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - public_share = response.parse() - assert public_share is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_delete(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.public_share.with_raw_response.delete( - "", - ) - - -class TestAsyncPublicShare: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create(self, async_client: AsyncBrowserUse) -> None: - public_share = await async_client.sessions.public_share.create( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(ShareView, public_share, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_create(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.sessions.public_share.with_raw_response.create( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - public_share = await response.parse() - assert_matches_type(ShareView, public_share, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_create(self, async_client: AsyncBrowserUse) -> None: - async with async_client.sessions.public_share.with_streaming_response.create( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - public_share = await response.parse() - assert_matches_type(ShareView, public_share, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_create(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.public_share.with_raw_response.create( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_retrieve(self, async_client: AsyncBrowserUse) -> None: - public_share = await async_client.sessions.public_share.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(ShareView, public_share, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.sessions.public_share.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - public_share = await response.parse() - assert_matches_type(ShareView, public_share, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - async with async_client.sessions.public_share.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - public_share = await response.parse() - assert_matches_type(ShareView, public_share, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_retrieve(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.public_share.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_delete(self, async_client: AsyncBrowserUse) -> None: - public_share = await async_client.sessions.public_share.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert public_share is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_delete(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.sessions.public_share.with_raw_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - public_share = await response.parse() - assert public_share is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_delete(self, async_client: AsyncBrowserUse) -> None: - async with async_client.sessions.public_share.with_streaming_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - public_share = await response.parse() - assert public_share is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_delete(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.public_share.with_raw_response.delete( - "", - ) diff --git a/tests/api_resources/test_agent_profiles.py b/tests/api_resources/test_agent_profiles.py deleted file mode 100644 index 77ee21c..0000000 --- a/tests/api_resources/test_agent_profiles.py +++ /dev/null @@ -1,487 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from tests.utils import assert_matches_type -from browser_use_sdk import BrowserUse, AsyncBrowserUse -from browser_use_sdk.types import ( - AgentProfileView, - AgentProfileListResponse, -) - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestAgentProfiles: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create(self, client: BrowserUse) -> None: - agent_profile = client.agent_profiles.create( - name="x", - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create_with_all_params(self, client: BrowserUse) -> None: - agent_profile = client.agent_profiles.create( - name="x", - allowed_domains=["string"], - custom_system_prompt_extension="x", - description="x", - flash_mode=True, - highlight_elements=True, - max_agent_steps=1, - thinking=True, - vision=True, - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_create(self, client: BrowserUse) -> None: - response = client.agent_profiles.with_raw_response.create( - name="x", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_create(self, client: BrowserUse) -> None: - with client.agent_profiles.with_streaming_response.create( - name="x", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_retrieve(self, client: BrowserUse) -> None: - agent_profile = client.agent_profiles.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: BrowserUse) -> None: - response = client.agent_profiles.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: BrowserUse) -> None: - with client.agent_profiles.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_retrieve(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - client.agent_profiles.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_update(self, client: BrowserUse) -> None: - agent_profile = client.agent_profiles.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_update_with_all_params(self, client: BrowserUse) -> None: - agent_profile = client.agent_profiles.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - allowed_domains=["string"], - custom_system_prompt_extension="x", - description="x", - flash_mode=True, - highlight_elements=True, - max_agent_steps=1, - name="x", - thinking=True, - vision=True, - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_update(self, client: BrowserUse) -> None: - response = client.agent_profiles.with_raw_response.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_update(self, client: BrowserUse) -> None: - with client.agent_profiles.with_streaming_response.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_update(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - client.agent_profiles.with_raw_response.update( - profile_id="", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list(self, client: BrowserUse) -> None: - agent_profile = client.agent_profiles.list() - assert_matches_type(AgentProfileListResponse, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list_with_all_params(self, client: BrowserUse) -> None: - agent_profile = client.agent_profiles.list( - page_number=1, - page_size=1, - ) - assert_matches_type(AgentProfileListResponse, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_list(self, client: BrowserUse) -> None: - response = client.agent_profiles.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = response.parse() - assert_matches_type(AgentProfileListResponse, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_list(self, client: BrowserUse) -> None: - with client.agent_profiles.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = response.parse() - assert_matches_type(AgentProfileListResponse, agent_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_delete(self, client: BrowserUse) -> None: - agent_profile = client.agent_profiles.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert agent_profile is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_delete(self, client: BrowserUse) -> None: - response = client.agent_profiles.with_raw_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = response.parse() - assert agent_profile is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_delete(self, client: BrowserUse) -> None: - with client.agent_profiles.with_streaming_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = response.parse() - assert agent_profile is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_delete(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - client.agent_profiles.with_raw_response.delete( - "", - ) - - -class TestAsyncAgentProfiles: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create(self, async_client: AsyncBrowserUse) -> None: - agent_profile = await async_client.agent_profiles.create( - name="x", - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncBrowserUse) -> None: - agent_profile = await async_client.agent_profiles.create( - name="x", - allowed_domains=["string"], - custom_system_prompt_extension="x", - description="x", - flash_mode=True, - highlight_elements=True, - max_agent_steps=1, - thinking=True, - vision=True, - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_create(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.agent_profiles.with_raw_response.create( - name="x", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = await response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_create(self, async_client: AsyncBrowserUse) -> None: - async with async_client.agent_profiles.with_streaming_response.create( - name="x", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = await response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_retrieve(self, async_client: AsyncBrowserUse) -> None: - agent_profile = await async_client.agent_profiles.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.agent_profiles.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = await response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - async with async_client.agent_profiles.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = await response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_retrieve(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - await async_client.agent_profiles.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_update(self, async_client: AsyncBrowserUse) -> None: - agent_profile = await async_client.agent_profiles.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_update_with_all_params(self, async_client: AsyncBrowserUse) -> None: - agent_profile = await async_client.agent_profiles.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - allowed_domains=["string"], - custom_system_prompt_extension="x", - description="x", - flash_mode=True, - highlight_elements=True, - max_agent_steps=1, - name="x", - thinking=True, - vision=True, - ) - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_update(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.agent_profiles.with_raw_response.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = await response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_update(self, async_client: AsyncBrowserUse) -> None: - async with async_client.agent_profiles.with_streaming_response.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = await response.parse() - assert_matches_type(AgentProfileView, agent_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_update(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - await async_client.agent_profiles.with_raw_response.update( - profile_id="", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list(self, async_client: AsyncBrowserUse) -> None: - agent_profile = await async_client.agent_profiles.list() - assert_matches_type(AgentProfileListResponse, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list_with_all_params(self, async_client: AsyncBrowserUse) -> None: - agent_profile = await async_client.agent_profiles.list( - page_number=1, - page_size=1, - ) - assert_matches_type(AgentProfileListResponse, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_list(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.agent_profiles.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = await response.parse() - assert_matches_type(AgentProfileListResponse, agent_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_list(self, async_client: AsyncBrowserUse) -> None: - async with async_client.agent_profiles.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = await response.parse() - assert_matches_type(AgentProfileListResponse, agent_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_delete(self, async_client: AsyncBrowserUse) -> None: - agent_profile = await async_client.agent_profiles.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert agent_profile is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_delete(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.agent_profiles.with_raw_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - agent_profile = await response.parse() - assert agent_profile is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_delete(self, async_client: AsyncBrowserUse) -> None: - async with async_client.agent_profiles.with_streaming_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - agent_profile = await response.parse() - assert agent_profile is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_delete(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - await async_client.agent_profiles.with_raw_response.delete( - "", - ) diff --git a/tests/api_resources/test_browser_profiles.py b/tests/api_resources/test_browser_profiles.py deleted file mode 100644 index d10b16a..0000000 --- a/tests/api_resources/test_browser_profiles.py +++ /dev/null @@ -1,491 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from tests.utils import assert_matches_type -from browser_use_sdk import BrowserUse, AsyncBrowserUse -from browser_use_sdk.types import ( - BrowserProfileView, - BrowserProfileListResponse, -) - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestBrowserProfiles: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create(self, client: BrowserUse) -> None: - browser_profile = client.browser_profiles.create( - name="x", - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create_with_all_params(self, client: BrowserUse) -> None: - browser_profile = client.browser_profiles.create( - name="x", - ad_blocker=True, - browser_viewport_height=100, - browser_viewport_width=100, - description="x", - is_mobile=True, - persist=True, - proxy=True, - proxy_country_code="us", - store_cache=True, - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_create(self, client: BrowserUse) -> None: - response = client.browser_profiles.with_raw_response.create( - name="x", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_create(self, client: BrowserUse) -> None: - with client.browser_profiles.with_streaming_response.create( - name="x", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_retrieve(self, client: BrowserUse) -> None: - browser_profile = client.browser_profiles.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: BrowserUse) -> None: - response = client.browser_profiles.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: BrowserUse) -> None: - with client.browser_profiles.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_retrieve(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - client.browser_profiles.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_update(self, client: BrowserUse) -> None: - browser_profile = client.browser_profiles.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_update_with_all_params(self, client: BrowserUse) -> None: - browser_profile = client.browser_profiles.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ad_blocker=True, - browser_viewport_height=100, - browser_viewport_width=100, - description="x", - is_mobile=True, - name="x", - persist=True, - proxy=True, - proxy_country_code="us", - store_cache=True, - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_update(self, client: BrowserUse) -> None: - response = client.browser_profiles.with_raw_response.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_update(self, client: BrowserUse) -> None: - with client.browser_profiles.with_streaming_response.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_update(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - client.browser_profiles.with_raw_response.update( - profile_id="", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list(self, client: BrowserUse) -> None: - browser_profile = client.browser_profiles.list() - assert_matches_type(BrowserProfileListResponse, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list_with_all_params(self, client: BrowserUse) -> None: - browser_profile = client.browser_profiles.list( - page_number=1, - page_size=1, - ) - assert_matches_type(BrowserProfileListResponse, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_list(self, client: BrowserUse) -> None: - response = client.browser_profiles.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = response.parse() - assert_matches_type(BrowserProfileListResponse, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_list(self, client: BrowserUse) -> None: - with client.browser_profiles.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = response.parse() - assert_matches_type(BrowserProfileListResponse, browser_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_delete(self, client: BrowserUse) -> None: - browser_profile = client.browser_profiles.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert browser_profile is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_delete(self, client: BrowserUse) -> None: - response = client.browser_profiles.with_raw_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = response.parse() - assert browser_profile is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_delete(self, client: BrowserUse) -> None: - with client.browser_profiles.with_streaming_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = response.parse() - assert browser_profile is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_delete(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - client.browser_profiles.with_raw_response.delete( - "", - ) - - -class TestAsyncBrowserProfiles: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create(self, async_client: AsyncBrowserUse) -> None: - browser_profile = await async_client.browser_profiles.create( - name="x", - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncBrowserUse) -> None: - browser_profile = await async_client.browser_profiles.create( - name="x", - ad_blocker=True, - browser_viewport_height=100, - browser_viewport_width=100, - description="x", - is_mobile=True, - persist=True, - proxy=True, - proxy_country_code="us", - store_cache=True, - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_create(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.browser_profiles.with_raw_response.create( - name="x", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = await response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_create(self, async_client: AsyncBrowserUse) -> None: - async with async_client.browser_profiles.with_streaming_response.create( - name="x", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = await response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_retrieve(self, async_client: AsyncBrowserUse) -> None: - browser_profile = await async_client.browser_profiles.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.browser_profiles.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = await response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - async with async_client.browser_profiles.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = await response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_retrieve(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - await async_client.browser_profiles.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_update(self, async_client: AsyncBrowserUse) -> None: - browser_profile = await async_client.browser_profiles.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_update_with_all_params(self, async_client: AsyncBrowserUse) -> None: - browser_profile = await async_client.browser_profiles.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ad_blocker=True, - browser_viewport_height=100, - browser_viewport_width=100, - description="x", - is_mobile=True, - name="x", - persist=True, - proxy=True, - proxy_country_code="us", - store_cache=True, - ) - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_update(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.browser_profiles.with_raw_response.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = await response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_update(self, async_client: AsyncBrowserUse) -> None: - async with async_client.browser_profiles.with_streaming_response.update( - profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = await response.parse() - assert_matches_type(BrowserProfileView, browser_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_update(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - await async_client.browser_profiles.with_raw_response.update( - profile_id="", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list(self, async_client: AsyncBrowserUse) -> None: - browser_profile = await async_client.browser_profiles.list() - assert_matches_type(BrowserProfileListResponse, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list_with_all_params(self, async_client: AsyncBrowserUse) -> None: - browser_profile = await async_client.browser_profiles.list( - page_number=1, - page_size=1, - ) - assert_matches_type(BrowserProfileListResponse, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_list(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.browser_profiles.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = await response.parse() - assert_matches_type(BrowserProfileListResponse, browser_profile, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_list(self, async_client: AsyncBrowserUse) -> None: - async with async_client.browser_profiles.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = await response.parse() - assert_matches_type(BrowserProfileListResponse, browser_profile, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_delete(self, async_client: AsyncBrowserUse) -> None: - browser_profile = await async_client.browser_profiles.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert browser_profile is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_delete(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.browser_profiles.with_raw_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - browser_profile = await response.parse() - assert browser_profile is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_delete(self, async_client: AsyncBrowserUse) -> None: - async with async_client.browser_profiles.with_streaming_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - browser_profile = await response.parse() - assert browser_profile is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_delete(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `profile_id` but received ''"): - await async_client.browser_profiles.with_raw_response.delete( - "", - ) diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py deleted file mode 100644 index 0751000..0000000 --- a/tests/api_resources/test_sessions.py +++ /dev/null @@ -1,363 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from tests.utils import assert_matches_type -from browser_use_sdk import BrowserUse, AsyncBrowserUse -from browser_use_sdk.types import ( - SessionView, - SessionListResponse, -) - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestSessions: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_retrieve(self, client: BrowserUse) -> None: - session = client.sessions.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(SessionView, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: BrowserUse) -> None: - response = client.sessions.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = response.parse() - assert_matches_type(SessionView, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: BrowserUse) -> None: - with client.sessions.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = response.parse() - assert_matches_type(SessionView, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_retrieve(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_update(self, client: BrowserUse) -> None: - session = client.sessions.update( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) - assert_matches_type(SessionView, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_update(self, client: BrowserUse) -> None: - response = client.sessions.with_raw_response.update( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = response.parse() - assert_matches_type(SessionView, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_update(self, client: BrowserUse) -> None: - with client.sessions.with_streaming_response.update( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = response.parse() - assert_matches_type(SessionView, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_update(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.with_raw_response.update( - session_id="", - action="stop", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list(self, client: BrowserUse) -> None: - session = client.sessions.list() - assert_matches_type(SessionListResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list_with_all_params(self, client: BrowserUse) -> None: - session = client.sessions.list( - filter_by="active", - page_number=1, - page_size=1, - ) - assert_matches_type(SessionListResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_list(self, client: BrowserUse) -> None: - response = client.sessions.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = response.parse() - assert_matches_type(SessionListResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_list(self, client: BrowserUse) -> None: - with client.sessions.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = response.parse() - assert_matches_type(SessionListResponse, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_delete(self, client: BrowserUse) -> None: - session = client.sessions.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert session is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_delete(self, client: BrowserUse) -> None: - response = client.sessions.with_raw_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = response.parse() - assert session is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_delete(self, client: BrowserUse) -> None: - with client.sessions.with_streaming_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = response.parse() - assert session is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_delete(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - client.sessions.with_raw_response.delete( - "", - ) - - -class TestAsyncSessions: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_retrieve(self, async_client: AsyncBrowserUse) -> None: - session = await async_client.sessions.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(SessionView, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.sessions.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = await response.parse() - assert_matches_type(SessionView, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - async with async_client.sessions.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = await response.parse() - assert_matches_type(SessionView, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_retrieve(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_update(self, async_client: AsyncBrowserUse) -> None: - session = await async_client.sessions.update( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) - assert_matches_type(SessionView, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_update(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.sessions.with_raw_response.update( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = await response.parse() - assert_matches_type(SessionView, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_update(self, async_client: AsyncBrowserUse) -> None: - async with async_client.sessions.with_streaming_response.update( - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = await response.parse() - assert_matches_type(SessionView, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_update(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.with_raw_response.update( - session_id="", - action="stop", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list(self, async_client: AsyncBrowserUse) -> None: - session = await async_client.sessions.list() - assert_matches_type(SessionListResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list_with_all_params(self, async_client: AsyncBrowserUse) -> None: - session = await async_client.sessions.list( - filter_by="active", - page_number=1, - page_size=1, - ) - assert_matches_type(SessionListResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_list(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.sessions.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = await response.parse() - assert_matches_type(SessionListResponse, session, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_list(self, async_client: AsyncBrowserUse) -> None: - async with async_client.sessions.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = await response.parse() - assert_matches_type(SessionListResponse, session, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_delete(self, async_client: AsyncBrowserUse) -> None: - session = await async_client.sessions.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert session is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_delete(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.sessions.with_raw_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - session = await response.parse() - assert session is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_delete(self, async_client: AsyncBrowserUse) -> None: - async with async_client.sessions.with_streaming_response.delete( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - session = await response.parse() - assert session is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_delete(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): - await async_client.sessions.with_raw_response.delete( - "", - ) diff --git a/tests/api_resources/test_tasks.py b/tests/api_resources/test_tasks.py deleted file mode 100644 index d296a63..0000000 --- a/tests/api_resources/test_tasks.py +++ /dev/null @@ -1,692 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from tests.utils import assert_matches_type -from browser_use_sdk import BrowserUse, AsyncBrowserUse -from browser_use_sdk.types import ( - TaskView, - TaskListResponse, - TaskCreateResponse, - TaskGetLogsResponse, - TaskGetOutputFileResponse, - TaskGetUserUploadedFileResponse, -) -from browser_use_sdk._utils import parse_datetime - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestTasks: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create(self, client: BrowserUse) -> None: - task = client.tasks.create( - task="x", - ) - assert_matches_type(TaskCreateResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create_with_all_params(self, client: BrowserUse) -> None: - task = client.tasks.create( - task="x", - agent_settings={ - "llm": "gpt-4.1", - "profile_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "start_url": "startUrl", - }, - browser_settings={ - "profile_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "session_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - }, - included_file_names=["string"], - metadata={"foo": "string"}, - secrets={"foo": "string"}, - structured_output_json="structuredOutputJson", - ) - assert_matches_type(TaskCreateResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_create(self, client: BrowserUse) -> None: - response = client.tasks.with_raw_response.create( - task="x", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = response.parse() - assert_matches_type(TaskCreateResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_create(self, client: BrowserUse) -> None: - with client.tasks.with_streaming_response.create( - task="x", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = response.parse() - assert_matches_type(TaskCreateResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_retrieve(self, client: BrowserUse) -> None: - task = client.tasks.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskView, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: BrowserUse) -> None: - response = client.tasks.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = response.parse() - assert_matches_type(TaskView, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: BrowserUse) -> None: - with client.tasks.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = response.parse() - assert_matches_type(TaskView, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_retrieve(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - client.tasks.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_update(self, client: BrowserUse) -> None: - task = client.tasks.update( - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) - assert_matches_type(TaskView, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_update(self, client: BrowserUse) -> None: - response = client.tasks.with_raw_response.update( - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = response.parse() - assert_matches_type(TaskView, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_update(self, client: BrowserUse) -> None: - with client.tasks.with_streaming_response.update( - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = response.parse() - assert_matches_type(TaskView, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_update(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - client.tasks.with_raw_response.update( - task_id="", - action="stop", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list(self, client: BrowserUse) -> None: - task = client.tasks.list() - assert_matches_type(TaskListResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list_with_all_params(self, client: BrowserUse) -> None: - task = client.tasks.list( - after=parse_datetime("2019-12-27T18:11:19.117Z"), - before=parse_datetime("2019-12-27T18:11:19.117Z"), - filter_by="started", - page_number=1, - page_size=1, - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskListResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_list(self, client: BrowserUse) -> None: - response = client.tasks.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = response.parse() - assert_matches_type(TaskListResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_list(self, client: BrowserUse) -> None: - with client.tasks.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = response.parse() - assert_matches_type(TaskListResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_get_logs(self, client: BrowserUse) -> None: - task = client.tasks.get_logs( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskGetLogsResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_get_logs(self, client: BrowserUse) -> None: - response = client.tasks.with_raw_response.get_logs( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = response.parse() - assert_matches_type(TaskGetLogsResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_get_logs(self, client: BrowserUse) -> None: - with client.tasks.with_streaming_response.get_logs( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = response.parse() - assert_matches_type(TaskGetLogsResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_get_logs(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - client.tasks.with_raw_response.get_logs( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_get_output_file(self, client: BrowserUse) -> None: - task = client.tasks.get_output_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskGetOutputFileResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_get_output_file(self, client: BrowserUse) -> None: - response = client.tasks.with_raw_response.get_output_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = response.parse() - assert_matches_type(TaskGetOutputFileResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_get_output_file(self, client: BrowserUse) -> None: - with client.tasks.with_streaming_response.get_output_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = response.parse() - assert_matches_type(TaskGetOutputFileResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_get_output_file(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - client.tasks.with_raw_response.get_output_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="", - ) - - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - client.tasks.with_raw_response.get_output_file( - file_id="", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_get_user_uploaded_file(self, client: BrowserUse) -> None: - task = client.tasks.get_user_uploaded_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskGetUserUploadedFileResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_get_user_uploaded_file(self, client: BrowserUse) -> None: - response = client.tasks.with_raw_response.get_user_uploaded_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = response.parse() - assert_matches_type(TaskGetUserUploadedFileResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_get_user_uploaded_file(self, client: BrowserUse) -> None: - with client.tasks.with_streaming_response.get_user_uploaded_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = response.parse() - assert_matches_type(TaskGetUserUploadedFileResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_get_user_uploaded_file(self, client: BrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - client.tasks.with_raw_response.get_user_uploaded_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="", - ) - - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - client.tasks.with_raw_response.get_user_uploaded_file( - file_id="", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - -class TestAsyncTasks: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.create( - task="x", - ) - assert_matches_type(TaskCreateResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.create( - task="x", - agent_settings={ - "llm": "gpt-4.1", - "profile_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "start_url": "startUrl", - }, - browser_settings={ - "profile_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "session_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - }, - included_file_names=["string"], - metadata={"foo": "string"}, - secrets={"foo": "string"}, - structured_output_json="structuredOutputJson", - ) - assert_matches_type(TaskCreateResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_create(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.tasks.with_raw_response.create( - task="x", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = await response.parse() - assert_matches_type(TaskCreateResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_create(self, async_client: AsyncBrowserUse) -> None: - async with async_client.tasks.with_streaming_response.create( - task="x", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = await response.parse() - assert_matches_type(TaskCreateResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_retrieve(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskView, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.tasks.with_raw_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = await response.parse() - assert_matches_type(TaskView, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - async with async_client.tasks.with_streaming_response.retrieve( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = await response.parse() - assert_matches_type(TaskView, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_retrieve(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - await async_client.tasks.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_update(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.update( - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) - assert_matches_type(TaskView, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_update(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.tasks.with_raw_response.update( - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = await response.parse() - assert_matches_type(TaskView, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_update(self, async_client: AsyncBrowserUse) -> None: - async with async_client.tasks.with_streaming_response.update( - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - action="stop", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = await response.parse() - assert_matches_type(TaskView, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_update(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - await async_client.tasks.with_raw_response.update( - task_id="", - action="stop", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.list() - assert_matches_type(TaskListResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list_with_all_params(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.list( - after=parse_datetime("2019-12-27T18:11:19.117Z"), - before=parse_datetime("2019-12-27T18:11:19.117Z"), - filter_by="started", - page_number=1, - page_size=1, - session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskListResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_list(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.tasks.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = await response.parse() - assert_matches_type(TaskListResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_list(self, async_client: AsyncBrowserUse) -> None: - async with async_client.tasks.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = await response.parse() - assert_matches_type(TaskListResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_get_logs(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.get_logs( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskGetLogsResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_get_logs(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.tasks.with_raw_response.get_logs( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = await response.parse() - assert_matches_type(TaskGetLogsResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_get_logs(self, async_client: AsyncBrowserUse) -> None: - async with async_client.tasks.with_streaming_response.get_logs( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = await response.parse() - assert_matches_type(TaskGetLogsResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_get_logs(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - await async_client.tasks.with_raw_response.get_logs( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_get_output_file(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.get_output_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskGetOutputFileResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_get_output_file(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.tasks.with_raw_response.get_output_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = await response.parse() - assert_matches_type(TaskGetOutputFileResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_get_output_file(self, async_client: AsyncBrowserUse) -> None: - async with async_client.tasks.with_streaming_response.get_output_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = await response.parse() - assert_matches_type(TaskGetOutputFileResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_get_output_file(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - await async_client.tasks.with_raw_response.get_output_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="", - ) - - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - await async_client.tasks.with_raw_response.get_output_file( - file_id="", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_get_user_uploaded_file(self, async_client: AsyncBrowserUse) -> None: - task = await async_client.tasks.get_user_uploaded_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(TaskGetUserUploadedFileResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_get_user_uploaded_file(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.tasks.with_raw_response.get_user_uploaded_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - task = await response.parse() - assert_matches_type(TaskGetUserUploadedFileResponse, task, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_get_user_uploaded_file(self, async_client: AsyncBrowserUse) -> None: - async with async_client.tasks.with_streaming_response.get_user_uploaded_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - task = await response.parse() - assert_matches_type(TaskGetUserUploadedFileResponse, task, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_get_user_uploaded_file(self, async_client: AsyncBrowserUse) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `task_id` but received ''"): - await async_client.tasks.with_raw_response.get_user_uploaded_file( - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - task_id="", - ) - - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - await async_client.tasks.with_raw_response.get_user_uploaded_file( - file_id="", - task_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) diff --git a/tests/api_resources/users/__init__.py b/tests/api_resources/users/__init__.py deleted file mode 100644 index fd8019a..0000000 --- a/tests/api_resources/users/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/users/me/__init__.py b/tests/api_resources/users/me/__init__.py deleted file mode 100644 index fd8019a..0000000 --- a/tests/api_resources/users/me/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/users/me/test_files.py b/tests/api_resources/users/me/test_files.py deleted file mode 100644 index 974f05a..0000000 --- a/tests/api_resources/users/me/test_files.py +++ /dev/null @@ -1,104 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from tests.utils import assert_matches_type -from browser_use_sdk import BrowserUse, AsyncBrowserUse -from browser_use_sdk.types.users.me import FileCreatePresignedURLResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestFiles: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create_presigned_url(self, client: BrowserUse) -> None: - file = client.users.me.files.create_presigned_url( - content_type="image/jpg", - file_name="x", - size_bytes=1, - ) - assert_matches_type(FileCreatePresignedURLResponse, file, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_create_presigned_url(self, client: BrowserUse) -> None: - response = client.users.me.files.with_raw_response.create_presigned_url( - content_type="image/jpg", - file_name="x", - size_bytes=1, - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - file = response.parse() - assert_matches_type(FileCreatePresignedURLResponse, file, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_create_presigned_url(self, client: BrowserUse) -> None: - with client.users.me.files.with_streaming_response.create_presigned_url( - content_type="image/jpg", - file_name="x", - size_bytes=1, - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - file = response.parse() - assert_matches_type(FileCreatePresignedURLResponse, file, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncFiles: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create_presigned_url(self, async_client: AsyncBrowserUse) -> None: - file = await async_client.users.me.files.create_presigned_url( - content_type="image/jpg", - file_name="x", - size_bytes=1, - ) - assert_matches_type(FileCreatePresignedURLResponse, file, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_create_presigned_url(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.users.me.files.with_raw_response.create_presigned_url( - content_type="image/jpg", - file_name="x", - size_bytes=1, - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - file = await response.parse() - assert_matches_type(FileCreatePresignedURLResponse, file, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_create_presigned_url(self, async_client: AsyncBrowserUse) -> None: - async with async_client.users.me.files.with_streaming_response.create_presigned_url( - content_type="image/jpg", - file_name="x", - size_bytes=1, - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - file = await response.parse() - assert_matches_type(FileCreatePresignedURLResponse, file, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/users/test_me.py b/tests/api_resources/users/test_me.py deleted file mode 100644 index 20e7981..0000000 --- a/tests/api_resources/users/test_me.py +++ /dev/null @@ -1,80 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from tests.utils import assert_matches_type -from browser_use_sdk import BrowserUse, AsyncBrowserUse -from browser_use_sdk.types.users import MeRetrieveResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestMe: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_retrieve(self, client: BrowserUse) -> None: - me = client.users.me.retrieve() - assert_matches_type(MeRetrieveResponse, me, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: BrowserUse) -> None: - response = client.users.me.with_raw_response.retrieve() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - me = response.parse() - assert_matches_type(MeRetrieveResponse, me, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: BrowserUse) -> None: - with client.users.me.with_streaming_response.retrieve() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - me = response.parse() - assert_matches_type(MeRetrieveResponse, me, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncMe: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_retrieve(self, async_client: AsyncBrowserUse) -> None: - me = await async_client.users.me.retrieve() - assert_matches_type(MeRetrieveResponse, me, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - response = await async_client.users.me.with_raw_response.retrieve() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - me = await response.parse() - assert_matches_type(MeRetrieveResponse, me, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncBrowserUse) -> None: - async with async_client.users.me.with_streaming_response.retrieve() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - me = await response.parse() - assert_matches_type(MeRetrieveResponse, me, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index ef9cc12..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,84 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -import logging -from typing import TYPE_CHECKING, Iterator, AsyncIterator - -import httpx -import pytest -from pytest_asyncio import is_async_test - -from browser_use_sdk import BrowserUse, AsyncBrowserUse, DefaultAioHttpClient -from browser_use_sdk._utils import is_dict - -if TYPE_CHECKING: - from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] - -pytest.register_assert_rewrite("tests.utils") - -logging.getLogger("browser_use_sdk").setLevel(logging.DEBUG) - - -# automatically add `pytest.mark.asyncio()` to all of our async tests -# so we don't have to add that boilerplate everywhere -def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: - pytest_asyncio_tests = (item for item in items if is_async_test(item)) - session_scope_marker = pytest.mark.asyncio(loop_scope="session") - for async_test in pytest_asyncio_tests: - async_test.add_marker(session_scope_marker, append=False) - - # We skip tests that use both the aiohttp client and respx_mock as respx_mock - # doesn't support custom transports. - for item in items: - if "async_client" not in item.fixturenames or "respx_mock" not in item.fixturenames: - continue - - if not hasattr(item, "callspec"): - continue - - async_client_param = item.callspec.params.get("async_client") - if is_dict(async_client_param) and async_client_param.get("http_client") == "aiohttp": - item.add_marker(pytest.mark.skip(reason="aiohttp client is not compatible with respx_mock")) - - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - -api_key = "My API Key" - - -@pytest.fixture(scope="session") -def client(request: FixtureRequest) -> Iterator[BrowserUse]: - strict = getattr(request, "param", True) - if not isinstance(strict, bool): - raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - - with BrowserUse(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: - yield client - - -@pytest.fixture(scope="session") -async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncBrowserUse]: - param = getattr(request, "param", True) - - # defaults - strict = True - http_client: None | httpx.AsyncClient = None - - if isinstance(param, bool): - strict = param - elif is_dict(param): - strict = param.get("strict", True) - assert isinstance(strict, bool) - - http_client_type = param.get("http_client", "httpx") - if http_client_type == "aiohttp": - http_client = DefaultAioHttpClient() - else: - raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict") - - async with AsyncBrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client - ) as client: - yield client diff --git a/tests/custom/test_client.py b/tests/custom/test_client.py new file mode 100644 index 0000000..ab04ce6 --- /dev/null +++ b/tests/custom/test_client.py @@ -0,0 +1,7 @@ +import pytest + + +# Get started with writing tests with pytest at https://docs.pytest.org +@pytest.mark.skip(reason="Unimplemented") +def test_client() -> None: + assert True diff --git a/tests/sample_file.txt b/tests/sample_file.txt deleted file mode 100644 index af5626b..0000000 --- a/tests/sample_file.txt +++ /dev/null @@ -1 +0,0 @@ -Hello, world! diff --git a/tests/test_client.py b/tests/test_client.py deleted file mode 100644 index a3b498a..0000000 --- a/tests/test_client.py +++ /dev/null @@ -1,1736 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import gc -import os -import sys -import json -import time -import asyncio -import inspect -import subprocess -import tracemalloc -from typing import Any, Union, cast -from textwrap import dedent -from unittest import mock -from typing_extensions import Literal - -import httpx -import pytest -from respx import MockRouter -from pydantic import ValidationError - -from browser_use_sdk import BrowserUse, AsyncBrowserUse, APIResponseValidationError -from browser_use_sdk._types import Omit -from browser_use_sdk._models import BaseModel, FinalRequestOptions -from browser_use_sdk._exceptions import APIStatusError, APITimeoutError, BrowserUseError, APIResponseValidationError -from browser_use_sdk._base_client import ( - DEFAULT_TIMEOUT, - HTTPX_DEFAULT_TIMEOUT, - BaseClient, - DefaultHttpxClient, - DefaultAsyncHttpxClient, - make_request_options, -) - -from .utils import update_env - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") -api_key = "My API Key" - - -def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - url = httpx.URL(request.url) - return dict(url.params) - - -def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: - return 0.1 - - -def _get_open_connections(client: BrowserUse | AsyncBrowserUse) -> int: - transport = client._client._transport - assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) - - pool = transport._pool - return len(pool._requests) - - -class TestBrowserUse: - client = BrowserUse(base_url=base_url, api_key=api_key, _strict_response_validation=True) - - @pytest.mark.respx(base_url=base_url) - def test_raw_response(self, respx_mock: MockRouter) -> None: - respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - - response = self.client.post("/foo", cast_to=httpx.Response) - assert response.status_code == 200 - assert isinstance(response, httpx.Response) - assert response.json() == {"foo": "bar"} - - @pytest.mark.respx(base_url=base_url) - def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: - respx_mock.post("/foo").mock( - return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') - ) - - response = self.client.post("/foo", cast_to=httpx.Response) - assert response.status_code == 200 - assert isinstance(response, httpx.Response) - assert response.json() == {"foo": "bar"} - - def test_copy(self) -> None: - copied = self.client.copy() - assert id(copied) != id(self.client) - - copied = self.client.copy(api_key="another My API Key") - assert copied.api_key == "another My API Key" - assert self.client.api_key == "My API Key" - - def test_copy_default_options(self) -> None: - # options that have a default are overridden correctly - copied = self.client.copy(max_retries=7) - assert copied.max_retries == 7 - assert self.client.max_retries == 2 - - copied2 = copied.copy(max_retries=6) - assert copied2.max_retries == 6 - assert copied.max_retries == 7 - - # timeout - assert isinstance(self.client.timeout, httpx.Timeout) - copied = self.client.copy(timeout=None) - assert copied.timeout is None - assert isinstance(self.client.timeout, httpx.Timeout) - - def test_copy_default_headers(self) -> None: - client = BrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} - ) - assert client.default_headers["X-Foo"] == "bar" - - # does not override the already given value when not specified - copied = client.copy() - assert copied.default_headers["X-Foo"] == "bar" - - # merges already given headers - copied = client.copy(default_headers={"X-Bar": "stainless"}) - assert copied.default_headers["X-Foo"] == "bar" - assert copied.default_headers["X-Bar"] == "stainless" - - # uses new values for any already given headers - copied = client.copy(default_headers={"X-Foo": "stainless"}) - assert copied.default_headers["X-Foo"] == "stainless" - - # set_default_headers - - # completely overrides already set values - copied = client.copy(set_default_headers={}) - assert copied.default_headers.get("X-Foo") is None - - copied = client.copy(set_default_headers={"X-Bar": "Robert"}) - assert copied.default_headers["X-Bar"] == "Robert" - - with pytest.raises( - ValueError, - match="`default_headers` and `set_default_headers` arguments are mutually exclusive", - ): - client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) - - def test_copy_default_query(self) -> None: - client = BrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} - ) - assert _get_params(client)["foo"] == "bar" - - # does not override the already given value when not specified - copied = client.copy() - assert _get_params(copied)["foo"] == "bar" - - # merges already given params - copied = client.copy(default_query={"bar": "stainless"}) - params = _get_params(copied) - assert params["foo"] == "bar" - assert params["bar"] == "stainless" - - # uses new values for any already given headers - copied = client.copy(default_query={"foo": "stainless"}) - assert _get_params(copied)["foo"] == "stainless" - - # set_default_query - - # completely overrides already set values - copied = client.copy(set_default_query={}) - assert _get_params(copied) == {} - - copied = client.copy(set_default_query={"bar": "Robert"}) - assert _get_params(copied)["bar"] == "Robert" - - with pytest.raises( - ValueError, - # TODO: update - match="`default_query` and `set_default_query` arguments are mutually exclusive", - ): - client.copy(set_default_query={}, default_query={"foo": "Bar"}) - - def test_copy_signature(self) -> None: - # ensure the same parameters that can be passed to the client are defined in the `.copy()` method - init_signature = inspect.signature( - # mypy doesn't like that we access the `__init__` property. - self.client.__init__, # type: ignore[misc] - ) - copy_signature = inspect.signature(self.client.copy) - exclude_params = {"transport", "proxies", "_strict_response_validation"} - - for name in init_signature.parameters.keys(): - if name in exclude_params: - continue - - copy_param = copy_signature.parameters.get(name) - assert copy_param is not None, f"copy() signature is missing the {name} param" - - @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") - def test_copy_build_request(self) -> None: - options = FinalRequestOptions(method="get", url="/foo") - - def build_request(options: FinalRequestOptions) -> None: - client = self.client.copy() - client._build_request(options) - - # ensure that the machinery is warmed up before tracing starts. - build_request(options) - gc.collect() - - tracemalloc.start(1000) - - snapshot_before = tracemalloc.take_snapshot() - - ITERATIONS = 10 - for _ in range(ITERATIONS): - build_request(options) - - gc.collect() - snapshot_after = tracemalloc.take_snapshot() - - tracemalloc.stop() - - def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None: - if diff.count == 0: - # Avoid false positives by considering only leaks (i.e. allocations that persist). - return - - if diff.count % ITERATIONS != 0: - # Avoid false positives by considering only leaks that appear per iteration. - return - - for frame in diff.traceback: - if any( - frame.filename.endswith(fragment) - for fragment in [ - # to_raw_response_wrapper leaks through the @functools.wraps() decorator. - # - # removing the decorator fixes the leak for reasons we don't understand. - "browser_use_sdk/_legacy_response.py", - "browser_use_sdk/_response.py", - # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. - "browser_use_sdk/_compat.py", - # Standard library leaks we don't care about. - "/logging/__init__.py", - ] - ): - return - - leaks.append(diff) - - leaks: list[tracemalloc.StatisticDiff] = [] - for diff in snapshot_after.compare_to(snapshot_before, "traceback"): - add_leak(leaks, diff) - if leaks: - for leak in leaks: - print("MEMORY LEAK:", leak) - for frame in leak.traceback: - print(frame) - raise AssertionError() - - def test_request_timeout(self) -> None: - request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) - timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore - assert timeout == DEFAULT_TIMEOUT - - request = self.client._build_request( - FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) - ) - timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore - assert timeout == httpx.Timeout(100.0) - - def test_client_timeout_option(self) -> None: - client = BrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) - ) - - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore - assert timeout == httpx.Timeout(0) - - def test_http_client_timeout_option(self) -> None: - # custom timeout given to the httpx client should be used - with httpx.Client(timeout=None) as http_client: - client = BrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client - ) - - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore - assert timeout == httpx.Timeout(None) - - # no timeout given to the httpx client should not use the httpx default - with httpx.Client() as http_client: - client = BrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client - ) - - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore - assert timeout == DEFAULT_TIMEOUT - - # explicitly passing the default timeout currently results in it being ignored - with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: - client = BrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client - ) - - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore - assert timeout == DEFAULT_TIMEOUT # our default - - async def test_invalid_http_client(self) -> None: - with pytest.raises(TypeError, match="Invalid `http_client` arg"): - async with httpx.AsyncClient() as http_client: - BrowserUse( - base_url=base_url, - api_key=api_key, - _strict_response_validation=True, - http_client=cast(Any, http_client), - ) - - def test_default_headers_option(self) -> None: - client = BrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} - ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - assert request.headers.get("x-foo") == "bar" - assert request.headers.get("x-stainless-lang") == "python" - - client2 = BrowserUse( - base_url=base_url, - api_key=api_key, - _strict_response_validation=True, - default_headers={ - "X-Foo": "stainless", - "X-Stainless-Lang": "my-overriding-header", - }, - ) - request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) - assert request.headers.get("x-foo") == "stainless" - assert request.headers.get("x-stainless-lang") == "my-overriding-header" - - def test_validate_headers(self) -> None: - client = BrowserUse(base_url=base_url, api_key=api_key, _strict_response_validation=True) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - assert request.headers.get("X-Browser-Use-API-Key") == api_key - - with pytest.raises(BrowserUseError): - with update_env(**{"BROWSER_USE_API_KEY": Omit()}): - client2 = BrowserUse(base_url=base_url, api_key=None, _strict_response_validation=True) - _ = client2 - - def test_default_query_option(self) -> None: - client = BrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} - ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - url = httpx.URL(request.url) - assert dict(url.params) == {"query_param": "bar"} - - request = client._build_request( - FinalRequestOptions( - method="get", - url="/foo", - params={"foo": "baz", "query_param": "overridden"}, - ) - ) - url = httpx.URL(request.url) - assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - - def test_request_extra_json(self) -> None: - request = self.client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - json_data={"foo": "bar"}, - extra_json={"baz": False}, - ), - ) - data = json.loads(request.content.decode("utf-8")) - assert data == {"foo": "bar", "baz": False} - - request = self.client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - extra_json={"baz": False}, - ), - ) - data = json.loads(request.content.decode("utf-8")) - assert data == {"baz": False} - - # `extra_json` takes priority over `json_data` when keys clash - request = self.client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - json_data={"foo": "bar", "baz": True}, - extra_json={"baz": None}, - ), - ) - data = json.loads(request.content.decode("utf-8")) - assert data == {"foo": "bar", "baz": None} - - def test_request_extra_headers(self) -> None: - request = self.client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options(extra_headers={"X-Foo": "Foo"}), - ), - ) - assert request.headers.get("X-Foo") == "Foo" - - # `extra_headers` takes priority over `default_headers` when keys clash - request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options( - extra_headers={"X-Bar": "false"}, - ), - ), - ) - assert request.headers.get("X-Bar") == "false" - - def test_request_extra_query(self) -> None: - request = self.client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options( - extra_query={"my_query_param": "Foo"}, - ), - ), - ) - params = dict(request.url.params) - assert params == {"my_query_param": "Foo"} - - # if both `query` and `extra_query` are given, they are merged - request = self.client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options( - query={"bar": "1"}, - extra_query={"foo": "2"}, - ), - ), - ) - params = dict(request.url.params) - assert params == {"bar": "1", "foo": "2"} - - # `extra_query` takes priority over `query` when keys clash - request = self.client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options( - query={"foo": "1"}, - extra_query={"foo": "2"}, - ), - ), - ) - params = dict(request.url.params) - assert params == {"foo": "2"} - - def test_multipart_repeating_array(self, client: BrowserUse) -> None: - request = client._build_request( - FinalRequestOptions.construct( - method="post", - url="/foo", - headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, - json_data={"array": ["foo", "bar"]}, - files=[("foo.txt", b"hello world")], - ) - ) - - assert request.read().split(b"\r\n") == [ - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="array[]"', - b"", - b"foo", - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="array[]"', - b"", - b"bar", - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', - b"Content-Type: application/octet-stream", - b"", - b"hello world", - b"--6b7ba517decee4a450543ea6ae821c82--", - b"", - ] - - @pytest.mark.respx(base_url=base_url) - def test_basic_union_response(self, respx_mock: MockRouter) -> None: - class Model1(BaseModel): - name: str - - class Model2(BaseModel): - foo: str - - respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) - assert isinstance(response, Model2) - assert response.foo == "bar" - - @pytest.mark.respx(base_url=base_url) - def test_union_response_different_types(self, respx_mock: MockRouter) -> None: - """Union of objects with the same field name using a different type""" - - class Model1(BaseModel): - foo: int - - class Model2(BaseModel): - foo: str - - respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) - assert isinstance(response, Model2) - assert response.foo == "bar" - - respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) - - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) - assert isinstance(response, Model1) - assert response.foo == 1 - - @pytest.mark.respx(base_url=base_url) - def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: - """ - Response that sets Content-Type to something other than application/json but returns json data - """ - - class Model(BaseModel): - foo: int - - respx_mock.get("/foo").mock( - return_value=httpx.Response( - 200, - content=json.dumps({"foo": 2}), - headers={"Content-Type": "application/text"}, - ) - ) - - response = self.client.get("/foo", cast_to=Model) - assert isinstance(response, Model) - assert response.foo == 2 - - def test_base_url_setter(self) -> None: - client = BrowserUse(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True) - assert client.base_url == "https://example.com/from_init/" - - client.base_url = "https://example.com/from_setter" # type: ignore[assignment] - - assert client.base_url == "https://example.com/from_setter/" - - def test_base_url_env(self) -> None: - with update_env(BROWSER_USE_BASE_URL="http://localhost:5000/from/env"): - client = BrowserUse(api_key=api_key, _strict_response_validation=True) - assert client.base_url == "http://localhost:5000/from/env/" - - @pytest.mark.parametrize( - "client", - [ - BrowserUse( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - BrowserUse( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - _strict_response_validation=True, - http_client=httpx.Client(), - ), - ], - ids=["standard", "custom http client"], - ) - def test_base_url_trailing_slash(self, client: BrowserUse) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - json_data={"foo": "bar"}, - ), - ) - assert request.url == "http://localhost:5000/custom/path/foo" - - @pytest.mark.parametrize( - "client", - [ - BrowserUse( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - BrowserUse( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - _strict_response_validation=True, - http_client=httpx.Client(), - ), - ], - ids=["standard", "custom http client"], - ) - def test_base_url_no_trailing_slash(self, client: BrowserUse) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - json_data={"foo": "bar"}, - ), - ) - assert request.url == "http://localhost:5000/custom/path/foo" - - @pytest.mark.parametrize( - "client", - [ - BrowserUse( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - BrowserUse( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - _strict_response_validation=True, - http_client=httpx.Client(), - ), - ], - ids=["standard", "custom http client"], - ) - def test_absolute_request_url(self, client: BrowserUse) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="https://myapi.com/foo", - json_data={"foo": "bar"}, - ), - ) - assert request.url == "https://myapi.com/foo" - - def test_copied_client_does_not_close_http(self) -> None: - client = BrowserUse(base_url=base_url, api_key=api_key, _strict_response_validation=True) - assert not client.is_closed() - - copied = client.copy() - assert copied is not client - - del copied - - assert not client.is_closed() - - def test_client_context_manager(self) -> None: - client = BrowserUse(base_url=base_url, api_key=api_key, _strict_response_validation=True) - with client as c2: - assert c2 is client - assert not c2.is_closed() - assert not client.is_closed() - assert client.is_closed() - - @pytest.mark.respx(base_url=base_url) - def test_client_response_validation_error(self, respx_mock: MockRouter) -> None: - class Model(BaseModel): - foo: str - - respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) - - with pytest.raises(APIResponseValidationError) as exc: - self.client.get("/foo", cast_to=Model) - - assert isinstance(exc.value.__cause__, ValidationError) - - def test_client_max_retries_validation(self) -> None: - with pytest.raises(TypeError, match=r"max_retries cannot be None"): - BrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) - ) - - @pytest.mark.respx(base_url=base_url) - def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: - class Model(BaseModel): - name: str - - respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - - strict_client = BrowserUse(base_url=base_url, api_key=api_key, _strict_response_validation=True) - - with pytest.raises(APIResponseValidationError): - strict_client.get("/foo", cast_to=Model) - - client = BrowserUse(base_url=base_url, api_key=api_key, _strict_response_validation=False) - - response = client.get("/foo", cast_to=Model) - assert isinstance(response, str) # type: ignore[unreachable] - - @pytest.mark.parametrize( - "remaining_retries,retry_after,timeout", - [ - [3, "20", 20], - [3, "0", 0.5], - [3, "-10", 0.5], - [3, "60", 60], - [3, "61", 0.5], - [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], - [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], - [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], - [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], - [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], - [3, "99999999999999999999999999999999999", 0.5], - [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], - [3, "", 0.5], - [2, "", 0.5 * 2.0], - [1, "", 0.5 * 4.0], - [-1100, "", 8], # test large number potentially overflowing - ], - ) - @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) - def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = BrowserUse(base_url=base_url, api_key=api_key, _strict_response_validation=True) - - headers = httpx.Headers({"retry-after": retry_after}) - options = FinalRequestOptions(method="get", url="/foo", max_retries=3) - calculated = client._calculate_retry_timeout(remaining_retries, options, headers) - assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] - - @mock.patch("browser_use_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: BrowserUse) -> None: - respx_mock.post("/tasks").mock(side_effect=httpx.TimeoutException("Test timeout error")) - - with pytest.raises(APITimeoutError): - client.tasks.with_streaming_response.create(task="x").__enter__() - - assert _get_open_connections(self.client) == 0 - - @mock.patch("browser_use_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: BrowserUse) -> None: - respx_mock.post("/tasks").mock(return_value=httpx.Response(500)) - - with pytest.raises(APIStatusError): - client.tasks.with_streaming_response.create(task="x").__enter__() - assert _get_open_connections(self.client) == 0 - - @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) - @mock.patch("browser_use_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - @pytest.mark.parametrize("failure_mode", ["status", "exception"]) - def test_retries_taken( - self, - client: BrowserUse, - failures_before_success: int, - failure_mode: Literal["status", "exception"], - respx_mock: MockRouter, - ) -> None: - client = client.with_options(max_retries=4) - - nb_retries = 0 - - def retry_handler(_request: httpx.Request) -> httpx.Response: - nonlocal nb_retries - if nb_retries < failures_before_success: - nb_retries += 1 - if failure_mode == "exception": - raise RuntimeError("oops") - return httpx.Response(500) - return httpx.Response(200) - - respx_mock.post("/tasks").mock(side_effect=retry_handler) - - response = client.tasks.with_raw_response.create(task="x") - - assert response.retries_taken == failures_before_success - assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success - - @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) - @mock.patch("browser_use_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - def test_omit_retry_count_header( - self, client: BrowserUse, failures_before_success: int, respx_mock: MockRouter - ) -> None: - client = client.with_options(max_retries=4) - - nb_retries = 0 - - def retry_handler(_request: httpx.Request) -> httpx.Response: - nonlocal nb_retries - if nb_retries < failures_before_success: - nb_retries += 1 - return httpx.Response(500) - return httpx.Response(200) - - respx_mock.post("/tasks").mock(side_effect=retry_handler) - - response = client.tasks.with_raw_response.create(task="x", extra_headers={"x-stainless-retry-count": Omit()}) - - assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 - - @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) - @mock.patch("browser_use_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - def test_overwrite_retry_count_header( - self, client: BrowserUse, failures_before_success: int, respx_mock: MockRouter - ) -> None: - client = client.with_options(max_retries=4) - - nb_retries = 0 - - def retry_handler(_request: httpx.Request) -> httpx.Response: - nonlocal nb_retries - if nb_retries < failures_before_success: - nb_retries += 1 - return httpx.Response(500) - return httpx.Response(200) - - respx_mock.post("/tasks").mock(side_effect=retry_handler) - - response = client.tasks.with_raw_response.create(task="x", extra_headers={"x-stainless-retry-count": "42"}) - - assert response.http_request.headers.get("x-stainless-retry-count") == "42" - - def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: - # Test that the proxy environment variables are set correctly - monkeypatch.setenv("HTTPS_PROXY", "https://example.org") - - client = DefaultHttpxClient() - - mounts = tuple(client._mounts.items()) - assert len(mounts) == 1 - assert mounts[0][0].pattern == "https://" - - @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") - def test_default_client_creation(self) -> None: - # Ensure that the client can be initialized without any exceptions - DefaultHttpxClient( - verify=True, - cert=None, - trust_env=True, - http1=True, - http2=False, - limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), - ) - - @pytest.mark.respx(base_url=base_url) - def test_follow_redirects(self, respx_mock: MockRouter) -> None: - # Test that the default follow_redirects=True allows following redirects - respx_mock.post("/redirect").mock( - return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) - ) - respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) - - response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) - assert response.status_code == 200 - assert response.json() == {"status": "ok"} - - @pytest.mark.respx(base_url=base_url) - def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: - # Test that follow_redirects=False prevents following redirects - respx_mock.post("/redirect").mock( - return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) - ) - - with pytest.raises(APIStatusError) as exc_info: - self.client.post( - "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response - ) - - assert exc_info.value.response.status_code == 302 - assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" - - -class TestAsyncBrowserUse: - client = AsyncBrowserUse(base_url=base_url, api_key=api_key, _strict_response_validation=True) - - @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_raw_response(self, respx_mock: MockRouter) -> None: - respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - - response = await self.client.post("/foo", cast_to=httpx.Response) - assert response.status_code == 200 - assert isinstance(response, httpx.Response) - assert response.json() == {"foo": "bar"} - - @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: - respx_mock.post("/foo").mock( - return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') - ) - - response = await self.client.post("/foo", cast_to=httpx.Response) - assert response.status_code == 200 - assert isinstance(response, httpx.Response) - assert response.json() == {"foo": "bar"} - - def test_copy(self) -> None: - copied = self.client.copy() - assert id(copied) != id(self.client) - - copied = self.client.copy(api_key="another My API Key") - assert copied.api_key == "another My API Key" - assert self.client.api_key == "My API Key" - - def test_copy_default_options(self) -> None: - # options that have a default are overridden correctly - copied = self.client.copy(max_retries=7) - assert copied.max_retries == 7 - assert self.client.max_retries == 2 - - copied2 = copied.copy(max_retries=6) - assert copied2.max_retries == 6 - assert copied.max_retries == 7 - - # timeout - assert isinstance(self.client.timeout, httpx.Timeout) - copied = self.client.copy(timeout=None) - assert copied.timeout is None - assert isinstance(self.client.timeout, httpx.Timeout) - - def test_copy_default_headers(self) -> None: - client = AsyncBrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} - ) - assert client.default_headers["X-Foo"] == "bar" - - # does not override the already given value when not specified - copied = client.copy() - assert copied.default_headers["X-Foo"] == "bar" - - # merges already given headers - copied = client.copy(default_headers={"X-Bar": "stainless"}) - assert copied.default_headers["X-Foo"] == "bar" - assert copied.default_headers["X-Bar"] == "stainless" - - # uses new values for any already given headers - copied = client.copy(default_headers={"X-Foo": "stainless"}) - assert copied.default_headers["X-Foo"] == "stainless" - - # set_default_headers - - # completely overrides already set values - copied = client.copy(set_default_headers={}) - assert copied.default_headers.get("X-Foo") is None - - copied = client.copy(set_default_headers={"X-Bar": "Robert"}) - assert copied.default_headers["X-Bar"] == "Robert" - - with pytest.raises( - ValueError, - match="`default_headers` and `set_default_headers` arguments are mutually exclusive", - ): - client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) - - def test_copy_default_query(self) -> None: - client = AsyncBrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} - ) - assert _get_params(client)["foo"] == "bar" - - # does not override the already given value when not specified - copied = client.copy() - assert _get_params(copied)["foo"] == "bar" - - # merges already given params - copied = client.copy(default_query={"bar": "stainless"}) - params = _get_params(copied) - assert params["foo"] == "bar" - assert params["bar"] == "stainless" - - # uses new values for any already given headers - copied = client.copy(default_query={"foo": "stainless"}) - assert _get_params(copied)["foo"] == "stainless" - - # set_default_query - - # completely overrides already set values - copied = client.copy(set_default_query={}) - assert _get_params(copied) == {} - - copied = client.copy(set_default_query={"bar": "Robert"}) - assert _get_params(copied)["bar"] == "Robert" - - with pytest.raises( - ValueError, - # TODO: update - match="`default_query` and `set_default_query` arguments are mutually exclusive", - ): - client.copy(set_default_query={}, default_query={"foo": "Bar"}) - - def test_copy_signature(self) -> None: - # ensure the same parameters that can be passed to the client are defined in the `.copy()` method - init_signature = inspect.signature( - # mypy doesn't like that we access the `__init__` property. - self.client.__init__, # type: ignore[misc] - ) - copy_signature = inspect.signature(self.client.copy) - exclude_params = {"transport", "proxies", "_strict_response_validation"} - - for name in init_signature.parameters.keys(): - if name in exclude_params: - continue - - copy_param = copy_signature.parameters.get(name) - assert copy_param is not None, f"copy() signature is missing the {name} param" - - @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") - def test_copy_build_request(self) -> None: - options = FinalRequestOptions(method="get", url="/foo") - - def build_request(options: FinalRequestOptions) -> None: - client = self.client.copy() - client._build_request(options) - - # ensure that the machinery is warmed up before tracing starts. - build_request(options) - gc.collect() - - tracemalloc.start(1000) - - snapshot_before = tracemalloc.take_snapshot() - - ITERATIONS = 10 - for _ in range(ITERATIONS): - build_request(options) - - gc.collect() - snapshot_after = tracemalloc.take_snapshot() - - tracemalloc.stop() - - def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None: - if diff.count == 0: - # Avoid false positives by considering only leaks (i.e. allocations that persist). - return - - if diff.count % ITERATIONS != 0: - # Avoid false positives by considering only leaks that appear per iteration. - return - - for frame in diff.traceback: - if any( - frame.filename.endswith(fragment) - for fragment in [ - # to_raw_response_wrapper leaks through the @functools.wraps() decorator. - # - # removing the decorator fixes the leak for reasons we don't understand. - "browser_use_sdk/_legacy_response.py", - "browser_use_sdk/_response.py", - # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. - "browser_use_sdk/_compat.py", - # Standard library leaks we don't care about. - "/logging/__init__.py", - ] - ): - return - - leaks.append(diff) - - leaks: list[tracemalloc.StatisticDiff] = [] - for diff in snapshot_after.compare_to(snapshot_before, "traceback"): - add_leak(leaks, diff) - if leaks: - for leak in leaks: - print("MEMORY LEAK:", leak) - for frame in leak.traceback: - print(frame) - raise AssertionError() - - async def test_request_timeout(self) -> None: - request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) - timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore - assert timeout == DEFAULT_TIMEOUT - - request = self.client._build_request( - FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) - ) - timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore - assert timeout == httpx.Timeout(100.0) - - async def test_client_timeout_option(self) -> None: - client = AsyncBrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) - ) - - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore - assert timeout == httpx.Timeout(0) - - async def test_http_client_timeout_option(self) -> None: - # custom timeout given to the httpx client should be used - async with httpx.AsyncClient(timeout=None) as http_client: - client = AsyncBrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client - ) - - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore - assert timeout == httpx.Timeout(None) - - # no timeout given to the httpx client should not use the httpx default - async with httpx.AsyncClient() as http_client: - client = AsyncBrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client - ) - - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore - assert timeout == DEFAULT_TIMEOUT - - # explicitly passing the default timeout currently results in it being ignored - async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: - client = AsyncBrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client - ) - - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore - assert timeout == DEFAULT_TIMEOUT # our default - - def test_invalid_http_client(self) -> None: - with pytest.raises(TypeError, match="Invalid `http_client` arg"): - with httpx.Client() as http_client: - AsyncBrowserUse( - base_url=base_url, - api_key=api_key, - _strict_response_validation=True, - http_client=cast(Any, http_client), - ) - - def test_default_headers_option(self) -> None: - client = AsyncBrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} - ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - assert request.headers.get("x-foo") == "bar" - assert request.headers.get("x-stainless-lang") == "python" - - client2 = AsyncBrowserUse( - base_url=base_url, - api_key=api_key, - _strict_response_validation=True, - default_headers={ - "X-Foo": "stainless", - "X-Stainless-Lang": "my-overriding-header", - }, - ) - request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) - assert request.headers.get("x-foo") == "stainless" - assert request.headers.get("x-stainless-lang") == "my-overriding-header" - - def test_validate_headers(self) -> None: - client = AsyncBrowserUse(base_url=base_url, api_key=api_key, _strict_response_validation=True) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - assert request.headers.get("X-Browser-Use-API-Key") == api_key - - with pytest.raises(BrowserUseError): - with update_env(**{"BROWSER_USE_API_KEY": Omit()}): - client2 = AsyncBrowserUse(base_url=base_url, api_key=None, _strict_response_validation=True) - _ = client2 - - def test_default_query_option(self) -> None: - client = AsyncBrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} - ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - url = httpx.URL(request.url) - assert dict(url.params) == {"query_param": "bar"} - - request = client._build_request( - FinalRequestOptions( - method="get", - url="/foo", - params={"foo": "baz", "query_param": "overridden"}, - ) - ) - url = httpx.URL(request.url) - assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - - def test_request_extra_json(self) -> None: - request = self.client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - json_data={"foo": "bar"}, - extra_json={"baz": False}, - ), - ) - data = json.loads(request.content.decode("utf-8")) - assert data == {"foo": "bar", "baz": False} - - request = self.client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - extra_json={"baz": False}, - ), - ) - data = json.loads(request.content.decode("utf-8")) - assert data == {"baz": False} - - # `extra_json` takes priority over `json_data` when keys clash - request = self.client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - json_data={"foo": "bar", "baz": True}, - extra_json={"baz": None}, - ), - ) - data = json.loads(request.content.decode("utf-8")) - assert data == {"foo": "bar", "baz": None} - - def test_request_extra_headers(self) -> None: - request = self.client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options(extra_headers={"X-Foo": "Foo"}), - ), - ) - assert request.headers.get("X-Foo") == "Foo" - - # `extra_headers` takes priority over `default_headers` when keys clash - request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options( - extra_headers={"X-Bar": "false"}, - ), - ), - ) - assert request.headers.get("X-Bar") == "false" - - def test_request_extra_query(self) -> None: - request = self.client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options( - extra_query={"my_query_param": "Foo"}, - ), - ), - ) - params = dict(request.url.params) - assert params == {"my_query_param": "Foo"} - - # if both `query` and `extra_query` are given, they are merged - request = self.client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options( - query={"bar": "1"}, - extra_query={"foo": "2"}, - ), - ), - ) - params = dict(request.url.params) - assert params == {"bar": "1", "foo": "2"} - - # `extra_query` takes priority over `query` when keys clash - request = self.client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options( - query={"foo": "1"}, - extra_query={"foo": "2"}, - ), - ), - ) - params = dict(request.url.params) - assert params == {"foo": "2"} - - def test_multipart_repeating_array(self, async_client: AsyncBrowserUse) -> None: - request = async_client._build_request( - FinalRequestOptions.construct( - method="post", - url="/foo", - headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, - json_data={"array": ["foo", "bar"]}, - files=[("foo.txt", b"hello world")], - ) - ) - - assert request.read().split(b"\r\n") == [ - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="array[]"', - b"", - b"foo", - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="array[]"', - b"", - b"bar", - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', - b"Content-Type: application/octet-stream", - b"", - b"hello world", - b"--6b7ba517decee4a450543ea6ae821c82--", - b"", - ] - - @pytest.mark.respx(base_url=base_url) - async def test_basic_union_response(self, respx_mock: MockRouter) -> None: - class Model1(BaseModel): - name: str - - class Model2(BaseModel): - foo: str - - respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) - assert isinstance(response, Model2) - assert response.foo == "bar" - - @pytest.mark.respx(base_url=base_url) - async def test_union_response_different_types(self, respx_mock: MockRouter) -> None: - """Union of objects with the same field name using a different type""" - - class Model1(BaseModel): - foo: int - - class Model2(BaseModel): - foo: str - - respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) - assert isinstance(response, Model2) - assert response.foo == "bar" - - respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) - - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) - assert isinstance(response, Model1) - assert response.foo == 1 - - @pytest.mark.respx(base_url=base_url) - async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: - """ - Response that sets Content-Type to something other than application/json but returns json data - """ - - class Model(BaseModel): - foo: int - - respx_mock.get("/foo").mock( - return_value=httpx.Response( - 200, - content=json.dumps({"foo": 2}), - headers={"Content-Type": "application/text"}, - ) - ) - - response = await self.client.get("/foo", cast_to=Model) - assert isinstance(response, Model) - assert response.foo == 2 - - def test_base_url_setter(self) -> None: - client = AsyncBrowserUse( - base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True - ) - assert client.base_url == "https://example.com/from_init/" - - client.base_url = "https://example.com/from_setter" # type: ignore[assignment] - - assert client.base_url == "https://example.com/from_setter/" - - def test_base_url_env(self) -> None: - with update_env(BROWSER_USE_BASE_URL="http://localhost:5000/from/env"): - client = AsyncBrowserUse(api_key=api_key, _strict_response_validation=True) - assert client.base_url == "http://localhost:5000/from/env/" - - @pytest.mark.parametrize( - "client", - [ - AsyncBrowserUse( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - AsyncBrowserUse( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - _strict_response_validation=True, - http_client=httpx.AsyncClient(), - ), - ], - ids=["standard", "custom http client"], - ) - def test_base_url_trailing_slash(self, client: AsyncBrowserUse) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - json_data={"foo": "bar"}, - ), - ) - assert request.url == "http://localhost:5000/custom/path/foo" - - @pytest.mark.parametrize( - "client", - [ - AsyncBrowserUse( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - AsyncBrowserUse( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - _strict_response_validation=True, - http_client=httpx.AsyncClient(), - ), - ], - ids=["standard", "custom http client"], - ) - def test_base_url_no_trailing_slash(self, client: AsyncBrowserUse) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - json_data={"foo": "bar"}, - ), - ) - assert request.url == "http://localhost:5000/custom/path/foo" - - @pytest.mark.parametrize( - "client", - [ - AsyncBrowserUse( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - AsyncBrowserUse( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - _strict_response_validation=True, - http_client=httpx.AsyncClient(), - ), - ], - ids=["standard", "custom http client"], - ) - def test_absolute_request_url(self, client: AsyncBrowserUse) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="https://myapi.com/foo", - json_data={"foo": "bar"}, - ), - ) - assert request.url == "https://myapi.com/foo" - - async def test_copied_client_does_not_close_http(self) -> None: - client = AsyncBrowserUse(base_url=base_url, api_key=api_key, _strict_response_validation=True) - assert not client.is_closed() - - copied = client.copy() - assert copied is not client - - del copied - - await asyncio.sleep(0.2) - assert not client.is_closed() - - async def test_client_context_manager(self) -> None: - client = AsyncBrowserUse(base_url=base_url, api_key=api_key, _strict_response_validation=True) - async with client as c2: - assert c2 is client - assert not c2.is_closed() - assert not client.is_closed() - assert client.is_closed() - - @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None: - class Model(BaseModel): - foo: str - - respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) - - with pytest.raises(APIResponseValidationError) as exc: - await self.client.get("/foo", cast_to=Model) - - assert isinstance(exc.value.__cause__, ValidationError) - - async def test_client_max_retries_validation(self) -> None: - with pytest.raises(TypeError, match=r"max_retries cannot be None"): - AsyncBrowserUse( - base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) - ) - - @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: - class Model(BaseModel): - name: str - - respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - - strict_client = AsyncBrowserUse(base_url=base_url, api_key=api_key, _strict_response_validation=True) - - with pytest.raises(APIResponseValidationError): - await strict_client.get("/foo", cast_to=Model) - - client = AsyncBrowserUse(base_url=base_url, api_key=api_key, _strict_response_validation=False) - - response = await client.get("/foo", cast_to=Model) - assert isinstance(response, str) # type: ignore[unreachable] - - @pytest.mark.parametrize( - "remaining_retries,retry_after,timeout", - [ - [3, "20", 20], - [3, "0", 0.5], - [3, "-10", 0.5], - [3, "60", 60], - [3, "61", 0.5], - [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], - [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], - [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], - [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], - [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], - [3, "99999999999999999999999999999999999", 0.5], - [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], - [3, "", 0.5], - [2, "", 0.5 * 2.0], - [1, "", 0.5 * 4.0], - [-1100, "", 8], # test large number potentially overflowing - ], - ) - @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) - @pytest.mark.asyncio - async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = AsyncBrowserUse(base_url=base_url, api_key=api_key, _strict_response_validation=True) - - headers = httpx.Headers({"retry-after": retry_after}) - options = FinalRequestOptions(method="get", url="/foo", max_retries=3) - calculated = client._calculate_retry_timeout(remaining_retries, options, headers) - assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] - - @mock.patch("browser_use_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - async def test_retrying_timeout_errors_doesnt_leak( - self, respx_mock: MockRouter, async_client: AsyncBrowserUse - ) -> None: - respx_mock.post("/tasks").mock(side_effect=httpx.TimeoutException("Test timeout error")) - - with pytest.raises(APITimeoutError): - await async_client.tasks.with_streaming_response.create(task="x").__aenter__() - - assert _get_open_connections(self.client) == 0 - - @mock.patch("browser_use_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - async def test_retrying_status_errors_doesnt_leak( - self, respx_mock: MockRouter, async_client: AsyncBrowserUse - ) -> None: - respx_mock.post("/tasks").mock(return_value=httpx.Response(500)) - - with pytest.raises(APIStatusError): - await async_client.tasks.with_streaming_response.create(task="x").__aenter__() - assert _get_open_connections(self.client) == 0 - - @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) - @mock.patch("browser_use_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - @pytest.mark.parametrize("failure_mode", ["status", "exception"]) - async def test_retries_taken( - self, - async_client: AsyncBrowserUse, - failures_before_success: int, - failure_mode: Literal["status", "exception"], - respx_mock: MockRouter, - ) -> None: - client = async_client.with_options(max_retries=4) - - nb_retries = 0 - - def retry_handler(_request: httpx.Request) -> httpx.Response: - nonlocal nb_retries - if nb_retries < failures_before_success: - nb_retries += 1 - if failure_mode == "exception": - raise RuntimeError("oops") - return httpx.Response(500) - return httpx.Response(200) - - respx_mock.post("/tasks").mock(side_effect=retry_handler) - - response = await client.tasks.with_raw_response.create(task="x") - - assert response.retries_taken == failures_before_success - assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success - - @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) - @mock.patch("browser_use_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_omit_retry_count_header( - self, async_client: AsyncBrowserUse, failures_before_success: int, respx_mock: MockRouter - ) -> None: - client = async_client.with_options(max_retries=4) - - nb_retries = 0 - - def retry_handler(_request: httpx.Request) -> httpx.Response: - nonlocal nb_retries - if nb_retries < failures_before_success: - nb_retries += 1 - return httpx.Response(500) - return httpx.Response(200) - - respx_mock.post("/tasks").mock(side_effect=retry_handler) - - response = await client.tasks.with_raw_response.create( - task="x", extra_headers={"x-stainless-retry-count": Omit()} - ) - - assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 - - @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) - @mock.patch("browser_use_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_overwrite_retry_count_header( - self, async_client: AsyncBrowserUse, failures_before_success: int, respx_mock: MockRouter - ) -> None: - client = async_client.with_options(max_retries=4) - - nb_retries = 0 - - def retry_handler(_request: httpx.Request) -> httpx.Response: - nonlocal nb_retries - if nb_retries < failures_before_success: - nb_retries += 1 - return httpx.Response(500) - return httpx.Response(200) - - respx_mock.post("/tasks").mock(side_effect=retry_handler) - - response = await client.tasks.with_raw_response.create( - task="x", extra_headers={"x-stainless-retry-count": "42"} - ) - - assert response.http_request.headers.get("x-stainless-retry-count") == "42" - - def test_get_platform(self) -> None: - # A previous implementation of asyncify could leave threads unterminated when - # used with nest_asyncio. - # - # Since nest_asyncio.apply() is global and cannot be un-applied, this - # test is run in a separate process to avoid affecting other tests. - test_code = dedent(""" - import asyncio - import nest_asyncio - import threading - - from browser_use_sdk._utils import asyncify - from browser_use_sdk._base_client import get_platform - - async def test_main() -> None: - result = await asyncify(get_platform)() - print(result) - for thread in threading.enumerate(): - print(thread.name) - - nest_asyncio.apply() - asyncio.run(test_main()) - """) - with subprocess.Popen( - [sys.executable, "-c", test_code], - text=True, - ) as process: - timeout = 10 # seconds - - start_time = time.monotonic() - while True: - return_code = process.poll() - if return_code is not None: - if return_code != 0: - raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code") - - # success - break - - if time.monotonic() - start_time > timeout: - process.kill() - raise AssertionError("calling get_platform using asyncify resulted in a hung process") - - time.sleep(0.1) - - async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: - # Test that the proxy environment variables are set correctly - monkeypatch.setenv("HTTPS_PROXY", "https://example.org") - - client = DefaultAsyncHttpxClient() - - mounts = tuple(client._mounts.items()) - assert len(mounts) == 1 - assert mounts[0][0].pattern == "https://" - - @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") - async def test_default_client_creation(self) -> None: - # Ensure that the client can be initialized without any exceptions - DefaultAsyncHttpxClient( - verify=True, - cert=None, - trust_env=True, - http1=True, - http2=False, - limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), - ) - - @pytest.mark.respx(base_url=base_url) - async def test_follow_redirects(self, respx_mock: MockRouter) -> None: - # Test that the default follow_redirects=True allows following redirects - respx_mock.post("/redirect").mock( - return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) - ) - respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) - - response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) - assert response.status_code == 200 - assert response.json() == {"status": "ok"} - - @pytest.mark.respx(base_url=base_url) - async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: - # Test that follow_redirects=False prevents following redirects - respx_mock.post("/redirect").mock( - return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) - ) - - with pytest.raises(APIStatusError) as exc_info: - await self.client.post( - "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response - ) - - assert exc_info.value.response.status_code == 302 - assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py deleted file mode 100644 index 852adc3..0000000 --- a/tests/test_deepcopy.py +++ /dev/null @@ -1,58 +0,0 @@ -from browser_use_sdk._utils import deepcopy_minimal - - -def assert_different_identities(obj1: object, obj2: object) -> None: - assert obj1 == obj2 - assert id(obj1) != id(obj2) - - -def test_simple_dict() -> None: - obj1 = {"foo": "bar"} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_dict() -> None: - obj1 = {"foo": {"bar": True}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - - -def test_complex_nested_dict() -> None: - obj1 = {"foo": {"bar": [{"hello": "world"}]}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"]) - assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0]) - - -def test_simple_list() -> None: - obj1 = ["a", "b", "c"] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_list() -> None: - obj1 = ["a", [1, 2, 3]] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1[1], obj2[1]) - - -class MyObject: ... - - -def test_ignores_other_types() -> None: - # custom classes - my_obj = MyObject() - obj1 = {"foo": my_obj} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert obj1["foo"] is my_obj - - # tuples - obj3 = ("a", "b") - obj4 = deepcopy_minimal(obj3) - assert obj3 is obj4 diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py deleted file mode 100644 index 151f6c6..0000000 --- a/tests/test_extract_files.py +++ /dev/null @@ -1,64 +0,0 @@ -from __future__ import annotations - -from typing import Sequence - -import pytest - -from browser_use_sdk._types import FileTypes -from browser_use_sdk._utils import extract_files - - -def test_removes_files_from_input() -> None: - query = {"foo": "bar"} - assert extract_files(query, paths=[]) == [] - assert query == {"foo": "bar"} - - query2 = {"foo": b"Bar", "hello": "world"} - assert extract_files(query2, paths=[["foo"]]) == [("foo", b"Bar")] - assert query2 == {"hello": "world"} - - query3 = {"foo": {"foo": {"bar": b"Bar"}}, "hello": "world"} - assert extract_files(query3, paths=[["foo", "foo", "bar"]]) == [("foo[foo][bar]", b"Bar")] - assert query3 == {"foo": {"foo": {}}, "hello": "world"} - - query4 = {"foo": {"bar": b"Bar", "baz": "foo"}, "hello": "world"} - assert extract_files(query4, paths=[["foo", "bar"]]) == [("foo[bar]", b"Bar")] - assert query4 == {"hello": "world", "foo": {"baz": "foo"}} - - -def test_multiple_files() -> None: - query = {"documents": [{"file": b"My first file"}, {"file": b"My second file"}]} - assert extract_files(query, paths=[["documents", "", "file"]]) == [ - ("documents[][file]", b"My first file"), - ("documents[][file]", b"My second file"), - ] - assert query == {"documents": [{}, {}]} - - -@pytest.mark.parametrize( - "query,paths,expected", - [ - [ - {"foo": {"bar": "baz"}}, - [["foo", "", "bar"]], - [], - ], - [ - {"foo": ["bar", "baz"]}, - [["foo", "bar"]], - [], - ], - [ - {"foo": {"bar": "baz"}}, - [["foo", "foo"]], - [], - ], - ], - ids=["dict expecting array", "array expecting dict", "unknown keys"], -) -def test_ignores_incorrect_paths( - query: dict[str, object], - paths: Sequence[Sequence[str]], - expected: list[tuple[str, FileTypes]], -) -> None: - assert extract_files(query, paths=paths) == expected diff --git a/tests/test_files.py b/tests/test_files.py deleted file mode 100644 index f14804f..0000000 --- a/tests/test_files.py +++ /dev/null @@ -1,51 +0,0 @@ -from pathlib import Path - -import anyio -import pytest -from dirty_equals import IsDict, IsList, IsBytes, IsTuple - -from browser_use_sdk._files import to_httpx_files, async_to_httpx_files - -readme_path = Path(__file__).parent.parent.joinpath("README.md") - - -def test_pathlib_includes_file_name() -> None: - result = to_httpx_files({"file": readme_path}) - print(result) - assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) - - -def test_tuple_input() -> None: - result = to_httpx_files([("file", readme_path)]) - print(result) - assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) - - -@pytest.mark.asyncio -async def test_async_pathlib_includes_file_name() -> None: - result = await async_to_httpx_files({"file": readme_path}) - print(result) - assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) - - -@pytest.mark.asyncio -async def test_async_supports_anyio_path() -> None: - result = await async_to_httpx_files({"file": anyio.Path(readme_path)}) - print(result) - assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) - - -@pytest.mark.asyncio -async def test_async_tuple_input() -> None: - result = await async_to_httpx_files([("file", readme_path)]) - print(result) - assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) - - -def test_string_not_allowed() -> None: - with pytest.raises(TypeError, match="Expected file types input to be a FileContent type or to be a tuple"): - to_httpx_files( - { - "file": "foo", # type: ignore - } - ) diff --git a/tests/test_models.py b/tests/test_models.py deleted file mode 100644 index ee8ad87..0000000 --- a/tests/test_models.py +++ /dev/null @@ -1,963 +0,0 @@ -import json -from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast -from datetime import datetime, timezone -from typing_extensions import Literal, Annotated, TypeAliasType - -import pytest -import pydantic -from pydantic import Field - -from browser_use_sdk._utils import PropertyInfo -from browser_use_sdk._compat import PYDANTIC_V2, parse_obj, model_dump, model_json -from browser_use_sdk._models import BaseModel, construct_type - - -class BasicModel(BaseModel): - foo: str - - -@pytest.mark.parametrize("value", ["hello", 1], ids=["correct type", "mismatched"]) -def test_basic(value: object) -> None: - m = BasicModel.construct(foo=value) - assert m.foo == value - - -def test_directly_nested_model() -> None: - class NestedModel(BaseModel): - nested: BasicModel - - m = NestedModel.construct(nested={"foo": "Foo!"}) - assert m.nested.foo == "Foo!" - - # mismatched types - m = NestedModel.construct(nested="hello!") - assert cast(Any, m.nested) == "hello!" - - -def test_optional_nested_model() -> None: - class NestedModel(BaseModel): - nested: Optional[BasicModel] - - m1 = NestedModel.construct(nested=None) - assert m1.nested is None - - m2 = NestedModel.construct(nested={"foo": "bar"}) - assert m2.nested is not None - assert m2.nested.foo == "bar" - - # mismatched types - m3 = NestedModel.construct(nested={"foo"}) - assert isinstance(cast(Any, m3.nested), set) - assert cast(Any, m3.nested) == {"foo"} - - -def test_list_nested_model() -> None: - class NestedModel(BaseModel): - nested: List[BasicModel] - - m = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}]) - assert m.nested is not None - assert isinstance(m.nested, list) - assert len(m.nested) == 2 - assert m.nested[0].foo == "bar" - assert m.nested[1].foo == "2" - - # mismatched types - m = NestedModel.construct(nested=True) - assert cast(Any, m.nested) is True - - m = NestedModel.construct(nested=[False]) - assert cast(Any, m.nested) == [False] - - -def test_optional_list_nested_model() -> None: - class NestedModel(BaseModel): - nested: Optional[List[BasicModel]] - - m1 = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}]) - assert m1.nested is not None - assert isinstance(m1.nested, list) - assert len(m1.nested) == 2 - assert m1.nested[0].foo == "bar" - assert m1.nested[1].foo == "2" - - m2 = NestedModel.construct(nested=None) - assert m2.nested is None - - # mismatched types - m3 = NestedModel.construct(nested={1}) - assert cast(Any, m3.nested) == {1} - - m4 = NestedModel.construct(nested=[False]) - assert cast(Any, m4.nested) == [False] - - -def test_list_optional_items_nested_model() -> None: - class NestedModel(BaseModel): - nested: List[Optional[BasicModel]] - - m = NestedModel.construct(nested=[None, {"foo": "bar"}]) - assert m.nested is not None - assert isinstance(m.nested, list) - assert len(m.nested) == 2 - assert m.nested[0] is None - assert m.nested[1] is not None - assert m.nested[1].foo == "bar" - - # mismatched types - m3 = NestedModel.construct(nested="foo") - assert cast(Any, m3.nested) == "foo" - - m4 = NestedModel.construct(nested=[False]) - assert cast(Any, m4.nested) == [False] - - -def test_list_mismatched_type() -> None: - class NestedModel(BaseModel): - nested: List[str] - - m = NestedModel.construct(nested=False) - assert cast(Any, m.nested) is False - - -def test_raw_dictionary() -> None: - class NestedModel(BaseModel): - nested: Dict[str, str] - - m = NestedModel.construct(nested={"hello": "world"}) - assert m.nested == {"hello": "world"} - - # mismatched types - m = NestedModel.construct(nested=False) - assert cast(Any, m.nested) is False - - -def test_nested_dictionary_model() -> None: - class NestedModel(BaseModel): - nested: Dict[str, BasicModel] - - m = NestedModel.construct(nested={"hello": {"foo": "bar"}}) - assert isinstance(m.nested, dict) - assert m.nested["hello"].foo == "bar" - - # mismatched types - m = NestedModel.construct(nested={"hello": False}) - assert cast(Any, m.nested["hello"]) is False - - -def test_unknown_fields() -> None: - m1 = BasicModel.construct(foo="foo", unknown=1) - assert m1.foo == "foo" - assert cast(Any, m1).unknown == 1 - - m2 = BasicModel.construct(foo="foo", unknown={"foo_bar": True}) - assert m2.foo == "foo" - assert cast(Any, m2).unknown == {"foo_bar": True} - - assert model_dump(m2) == {"foo": "foo", "unknown": {"foo_bar": True}} - - -def test_strict_validation_unknown_fields() -> None: - class Model(BaseModel): - foo: str - - model = parse_obj(Model, dict(foo="hello!", user="Robert")) - assert model.foo == "hello!" - assert cast(Any, model).user == "Robert" - - assert model_dump(model) == {"foo": "hello!", "user": "Robert"} - - -def test_aliases() -> None: - class Model(BaseModel): - my_field: int = Field(alias="myField") - - m = Model.construct(myField=1) - assert m.my_field == 1 - - # mismatched types - m = Model.construct(myField={"hello": False}) - assert cast(Any, m.my_field) == {"hello": False} - - -def test_repr() -> None: - model = BasicModel(foo="bar") - assert str(model) == "BasicModel(foo='bar')" - assert repr(model) == "BasicModel(foo='bar')" - - -def test_repr_nested_model() -> None: - class Child(BaseModel): - name: str - age: int - - class Parent(BaseModel): - name: str - child: Child - - model = Parent(name="Robert", child=Child(name="Foo", age=5)) - assert str(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))" - assert repr(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))" - - -def test_optional_list() -> None: - class Submodel(BaseModel): - name: str - - class Model(BaseModel): - items: Optional[List[Submodel]] - - m = Model.construct(items=None) - assert m.items is None - - m = Model.construct(items=[]) - assert m.items == [] - - m = Model.construct(items=[{"name": "Robert"}]) - assert m.items is not None - assert len(m.items) == 1 - assert m.items[0].name == "Robert" - - -def test_nested_union_of_models() -> None: - class Submodel1(BaseModel): - bar: bool - - class Submodel2(BaseModel): - thing: str - - class Model(BaseModel): - foo: Union[Submodel1, Submodel2] - - m = Model.construct(foo={"thing": "hello"}) - assert isinstance(m.foo, Submodel2) - assert m.foo.thing == "hello" - - -def test_nested_union_of_mixed_types() -> None: - class Submodel1(BaseModel): - bar: bool - - class Model(BaseModel): - foo: Union[Submodel1, Literal[True], Literal["CARD_HOLDER"]] - - m = Model.construct(foo=True) - assert m.foo is True - - m = Model.construct(foo="CARD_HOLDER") - assert m.foo == "CARD_HOLDER" - - m = Model.construct(foo={"bar": False}) - assert isinstance(m.foo, Submodel1) - assert m.foo.bar is False - - -def test_nested_union_multiple_variants() -> None: - class Submodel1(BaseModel): - bar: bool - - class Submodel2(BaseModel): - thing: str - - class Submodel3(BaseModel): - foo: int - - class Model(BaseModel): - foo: Union[Submodel1, Submodel2, None, Submodel3] - - m = Model.construct(foo={"thing": "hello"}) - assert isinstance(m.foo, Submodel2) - assert m.foo.thing == "hello" - - m = Model.construct(foo=None) - assert m.foo is None - - m = Model.construct() - assert m.foo is None - - m = Model.construct(foo={"foo": "1"}) - assert isinstance(m.foo, Submodel3) - assert m.foo.foo == 1 - - -def test_nested_union_invalid_data() -> None: - class Submodel1(BaseModel): - level: int - - class Submodel2(BaseModel): - name: str - - class Model(BaseModel): - foo: Union[Submodel1, Submodel2] - - m = Model.construct(foo=True) - assert cast(bool, m.foo) is True - - m = Model.construct(foo={"name": 3}) - if PYDANTIC_V2: - assert isinstance(m.foo, Submodel1) - assert m.foo.name == 3 # type: ignore - else: - assert isinstance(m.foo, Submodel2) - assert m.foo.name == "3" - - -def test_list_of_unions() -> None: - class Submodel1(BaseModel): - level: int - - class Submodel2(BaseModel): - name: str - - class Model(BaseModel): - items: List[Union[Submodel1, Submodel2]] - - m = Model.construct(items=[{"level": 1}, {"name": "Robert"}]) - assert len(m.items) == 2 - assert isinstance(m.items[0], Submodel1) - assert m.items[0].level == 1 - assert isinstance(m.items[1], Submodel2) - assert m.items[1].name == "Robert" - - m = Model.construct(items=[{"level": -1}, 156]) - assert len(m.items) == 2 - assert isinstance(m.items[0], Submodel1) - assert m.items[0].level == -1 - assert cast(Any, m.items[1]) == 156 - - -def test_union_of_lists() -> None: - class SubModel1(BaseModel): - level: int - - class SubModel2(BaseModel): - name: str - - class Model(BaseModel): - items: Union[List[SubModel1], List[SubModel2]] - - # with one valid entry - m = Model.construct(items=[{"name": "Robert"}]) - assert len(m.items) == 1 - assert isinstance(m.items[0], SubModel2) - assert m.items[0].name == "Robert" - - # with two entries pointing to different types - m = Model.construct(items=[{"level": 1}, {"name": "Robert"}]) - assert len(m.items) == 2 - assert isinstance(m.items[0], SubModel1) - assert m.items[0].level == 1 - assert isinstance(m.items[1], SubModel1) - assert cast(Any, m.items[1]).name == "Robert" - - # with two entries pointing to *completely* different types - m = Model.construct(items=[{"level": -1}, 156]) - assert len(m.items) == 2 - assert isinstance(m.items[0], SubModel1) - assert m.items[0].level == -1 - assert cast(Any, m.items[1]) == 156 - - -def test_dict_of_union() -> None: - class SubModel1(BaseModel): - name: str - - class SubModel2(BaseModel): - foo: str - - class Model(BaseModel): - data: Dict[str, Union[SubModel1, SubModel2]] - - m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}}) - assert len(list(m.data.keys())) == 2 - assert isinstance(m.data["hello"], SubModel1) - assert m.data["hello"].name == "there" - assert isinstance(m.data["foo"], SubModel2) - assert m.data["foo"].foo == "bar" - - # TODO: test mismatched type - - -def test_double_nested_union() -> None: - class SubModel1(BaseModel): - name: str - - class SubModel2(BaseModel): - bar: str - - class Model(BaseModel): - data: Dict[str, List[Union[SubModel1, SubModel2]]] - - m = Model.construct(data={"foo": [{"bar": "baz"}, {"name": "Robert"}]}) - assert len(m.data["foo"]) == 2 - - entry1 = m.data["foo"][0] - assert isinstance(entry1, SubModel2) - assert entry1.bar == "baz" - - entry2 = m.data["foo"][1] - assert isinstance(entry2, SubModel1) - assert entry2.name == "Robert" - - # TODO: test mismatched type - - -def test_union_of_dict() -> None: - class SubModel1(BaseModel): - name: str - - class SubModel2(BaseModel): - foo: str - - class Model(BaseModel): - data: Union[Dict[str, SubModel1], Dict[str, SubModel2]] - - m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}}) - assert len(list(m.data.keys())) == 2 - assert isinstance(m.data["hello"], SubModel1) - assert m.data["hello"].name == "there" - assert isinstance(m.data["foo"], SubModel1) - assert cast(Any, m.data["foo"]).foo == "bar" - - -def test_iso8601_datetime() -> None: - class Model(BaseModel): - created_at: datetime - - expected = datetime(2019, 12, 27, 18, 11, 19, 117000, tzinfo=timezone.utc) - - if PYDANTIC_V2: - expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' - else: - expected_json = '{"created_at": "2019-12-27T18:11:19.117000+00:00"}' - - model = Model.construct(created_at="2019-12-27T18:11:19.117Z") - assert model.created_at == expected - assert model_json(model) == expected_json - - model = parse_obj(Model, dict(created_at="2019-12-27T18:11:19.117Z")) - assert model.created_at == expected - assert model_json(model) == expected_json - - -def test_does_not_coerce_int() -> None: - class Model(BaseModel): - bar: int - - assert Model.construct(bar=1).bar == 1 - assert Model.construct(bar=10.9).bar == 10.9 - assert Model.construct(bar="19").bar == "19" # type: ignore[comparison-overlap] - assert Model.construct(bar=False).bar is False - - -def test_int_to_float_safe_conversion() -> None: - class Model(BaseModel): - float_field: float - - m = Model.construct(float_field=10) - assert m.float_field == 10.0 - assert isinstance(m.float_field, float) - - m = Model.construct(float_field=10.12) - assert m.float_field == 10.12 - assert isinstance(m.float_field, float) - - # number too big - m = Model.construct(float_field=2**53 + 1) - assert m.float_field == 2**53 + 1 - assert isinstance(m.float_field, int) - - -def test_deprecated_alias() -> None: - class Model(BaseModel): - resource_id: str = Field(alias="model_id") - - @property - def model_id(self) -> str: - return self.resource_id - - m = Model.construct(model_id="id") - assert m.model_id == "id" - assert m.resource_id == "id" - assert m.resource_id is m.model_id - - m = parse_obj(Model, {"model_id": "id"}) - assert m.model_id == "id" - assert m.resource_id == "id" - assert m.resource_id is m.model_id - - -def test_omitted_fields() -> None: - class Model(BaseModel): - resource_id: Optional[str] = None - - m = Model.construct() - assert m.resource_id is None - assert "resource_id" not in m.model_fields_set - - m = Model.construct(resource_id=None) - assert m.resource_id is None - assert "resource_id" in m.model_fields_set - - m = Model.construct(resource_id="foo") - assert m.resource_id == "foo" - assert "resource_id" in m.model_fields_set - - -def test_to_dict() -> None: - class Model(BaseModel): - foo: Optional[str] = Field(alias="FOO", default=None) - - m = Model(FOO="hello") - assert m.to_dict() == {"FOO": "hello"} - assert m.to_dict(use_api_names=False) == {"foo": "hello"} - - m2 = Model() - assert m2.to_dict() == {} - assert m2.to_dict(exclude_unset=False) == {"FOO": None} - assert m2.to_dict(exclude_unset=False, exclude_none=True) == {} - assert m2.to_dict(exclude_unset=False, exclude_defaults=True) == {} - - m3 = Model(FOO=None) - assert m3.to_dict() == {"FOO": None} - assert m3.to_dict(exclude_none=True) == {} - assert m3.to_dict(exclude_defaults=True) == {} - - class Model2(BaseModel): - created_at: datetime - - time_str = "2024-03-21T11:39:01.275859" - m4 = Model2.construct(created_at=time_str) - assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} - assert m4.to_dict(mode="json") == {"created_at": time_str} - - if not PYDANTIC_V2: - with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): - m.to_dict(warnings=False) - - -def test_forwards_compat_model_dump_method() -> None: - class Model(BaseModel): - foo: Optional[str] = Field(alias="FOO", default=None) - - m = Model(FOO="hello") - assert m.model_dump() == {"foo": "hello"} - assert m.model_dump(include={"bar"}) == {} - assert m.model_dump(exclude={"foo"}) == {} - assert m.model_dump(by_alias=True) == {"FOO": "hello"} - - m2 = Model() - assert m2.model_dump() == {"foo": None} - assert m2.model_dump(exclude_unset=True) == {} - assert m2.model_dump(exclude_none=True) == {} - assert m2.model_dump(exclude_defaults=True) == {} - - m3 = Model(FOO=None) - assert m3.model_dump() == {"foo": None} - assert m3.model_dump(exclude_none=True) == {} - - if not PYDANTIC_V2: - with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): - m.model_dump(round_trip=True) - - with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): - m.model_dump(warnings=False) - - -def test_compat_method_no_error_for_warnings() -> None: - class Model(BaseModel): - foo: Optional[str] - - m = Model(foo="hello") - assert isinstance(model_dump(m, warnings=False), dict) - - -def test_to_json() -> None: - class Model(BaseModel): - foo: Optional[str] = Field(alias="FOO", default=None) - - m = Model(FOO="hello") - assert json.loads(m.to_json()) == {"FOO": "hello"} - assert json.loads(m.to_json(use_api_names=False)) == {"foo": "hello"} - - if PYDANTIC_V2: - assert m.to_json(indent=None) == '{"FOO":"hello"}' - else: - assert m.to_json(indent=None) == '{"FOO": "hello"}' - - m2 = Model() - assert json.loads(m2.to_json()) == {} - assert json.loads(m2.to_json(exclude_unset=False)) == {"FOO": None} - assert json.loads(m2.to_json(exclude_unset=False, exclude_none=True)) == {} - assert json.loads(m2.to_json(exclude_unset=False, exclude_defaults=True)) == {} - - m3 = Model(FOO=None) - assert json.loads(m3.to_json()) == {"FOO": None} - assert json.loads(m3.to_json(exclude_none=True)) == {} - - if not PYDANTIC_V2: - with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): - m.to_json(warnings=False) - - -def test_forwards_compat_model_dump_json_method() -> None: - class Model(BaseModel): - foo: Optional[str] = Field(alias="FOO", default=None) - - m = Model(FOO="hello") - assert json.loads(m.model_dump_json()) == {"foo": "hello"} - assert json.loads(m.model_dump_json(include={"bar"})) == {} - assert json.loads(m.model_dump_json(include={"foo"})) == {"foo": "hello"} - assert json.loads(m.model_dump_json(by_alias=True)) == {"FOO": "hello"} - - assert m.model_dump_json(indent=2) == '{\n "foo": "hello"\n}' - - m2 = Model() - assert json.loads(m2.model_dump_json()) == {"foo": None} - assert json.loads(m2.model_dump_json(exclude_unset=True)) == {} - assert json.loads(m2.model_dump_json(exclude_none=True)) == {} - assert json.loads(m2.model_dump_json(exclude_defaults=True)) == {} - - m3 = Model(FOO=None) - assert json.loads(m3.model_dump_json()) == {"foo": None} - assert json.loads(m3.model_dump_json(exclude_none=True)) == {} - - if not PYDANTIC_V2: - with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): - m.model_dump_json(round_trip=True) - - with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): - m.model_dump_json(warnings=False) - - -def test_type_compat() -> None: - # our model type can be assigned to Pydantic's model type - - def takes_pydantic(model: pydantic.BaseModel) -> None: # noqa: ARG001 - ... - - class OurModel(BaseModel): - foo: Optional[str] = None - - takes_pydantic(OurModel()) - - -def test_annotated_types() -> None: - class Model(BaseModel): - value: str - - m = construct_type( - value={"value": "foo"}, - type_=cast(Any, Annotated[Model, "random metadata"]), - ) - assert isinstance(m, Model) - assert m.value == "foo" - - -def test_discriminated_unions_invalid_data() -> None: - class A(BaseModel): - type: Literal["a"] - - data: str - - class B(BaseModel): - type: Literal["b"] - - data: int - - m = construct_type( - value={"type": "b", "data": "foo"}, - type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), - ) - assert isinstance(m, B) - assert m.type == "b" - assert m.data == "foo" # type: ignore[comparison-overlap] - - m = construct_type( - value={"type": "a", "data": 100}, - type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), - ) - assert isinstance(m, A) - assert m.type == "a" - if PYDANTIC_V2: - assert m.data == 100 # type: ignore[comparison-overlap] - else: - # pydantic v1 automatically converts inputs to strings - # if the expected type is a str - assert m.data == "100" - - -def test_discriminated_unions_unknown_variant() -> None: - class A(BaseModel): - type: Literal["a"] - - data: str - - class B(BaseModel): - type: Literal["b"] - - data: int - - m = construct_type( - value={"type": "c", "data": None, "new_thing": "bar"}, - type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), - ) - - # just chooses the first variant - assert isinstance(m, A) - assert m.type == "c" # type: ignore[comparison-overlap] - assert m.data == None # type: ignore[unreachable] - assert m.new_thing == "bar" - - -def test_discriminated_unions_invalid_data_nested_unions() -> None: - class A(BaseModel): - type: Literal["a"] - - data: str - - class B(BaseModel): - type: Literal["b"] - - data: int - - class C(BaseModel): - type: Literal["c"] - - data: bool - - m = construct_type( - value={"type": "b", "data": "foo"}, - type_=cast(Any, Annotated[Union[Union[A, B], C], PropertyInfo(discriminator="type")]), - ) - assert isinstance(m, B) - assert m.type == "b" - assert m.data == "foo" # type: ignore[comparison-overlap] - - m = construct_type( - value={"type": "c", "data": "foo"}, - type_=cast(Any, Annotated[Union[Union[A, B], C], PropertyInfo(discriminator="type")]), - ) - assert isinstance(m, C) - assert m.type == "c" - assert m.data == "foo" # type: ignore[comparison-overlap] - - -def test_discriminated_unions_with_aliases_invalid_data() -> None: - class A(BaseModel): - foo_type: Literal["a"] = Field(alias="type") - - data: str - - class B(BaseModel): - foo_type: Literal["b"] = Field(alias="type") - - data: int - - m = construct_type( - value={"type": "b", "data": "foo"}, - type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="foo_type")]), - ) - assert isinstance(m, B) - assert m.foo_type == "b" - assert m.data == "foo" # type: ignore[comparison-overlap] - - m = construct_type( - value={"type": "a", "data": 100}, - type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="foo_type")]), - ) - assert isinstance(m, A) - assert m.foo_type == "a" - if PYDANTIC_V2: - assert m.data == 100 # type: ignore[comparison-overlap] - else: - # pydantic v1 automatically converts inputs to strings - # if the expected type is a str - assert m.data == "100" - - -def test_discriminated_unions_overlapping_discriminators_invalid_data() -> None: - class A(BaseModel): - type: Literal["a"] - - data: bool - - class B(BaseModel): - type: Literal["a"] - - data: int - - m = construct_type( - value={"type": "a", "data": "foo"}, - type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), - ) - assert isinstance(m, B) - assert m.type == "a" - assert m.data == "foo" # type: ignore[comparison-overlap] - - -def test_discriminated_unions_invalid_data_uses_cache() -> None: - class A(BaseModel): - type: Literal["a"] - - data: str - - class B(BaseModel): - type: Literal["b"] - - data: int - - UnionType = cast(Any, Union[A, B]) - - assert not hasattr(UnionType, "__discriminator__") - - m = construct_type( - value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) - ) - assert isinstance(m, B) - assert m.type == "b" - assert m.data == "foo" # type: ignore[comparison-overlap] - - discriminator = UnionType.__discriminator__ - assert discriminator is not None - - m = construct_type( - value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) - ) - assert isinstance(m, B) - assert m.type == "b" - assert m.data == "foo" # type: ignore[comparison-overlap] - - # if the discriminator details object stays the same between invocations then - # we hit the cache - assert UnionType.__discriminator__ is discriminator - - -@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") -def test_type_alias_type() -> None: - Alias = TypeAliasType("Alias", str) # pyright: ignore - - class Model(BaseModel): - alias: Alias - union: Union[int, Alias] - - m = construct_type(value={"alias": "foo", "union": "bar"}, type_=Model) - assert isinstance(m, Model) - assert isinstance(m.alias, str) - assert m.alias == "foo" - assert isinstance(m.union, str) - assert m.union == "bar" - - -@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") -def test_field_named_cls() -> None: - class Model(BaseModel): - cls: str - - m = construct_type(value={"cls": "foo"}, type_=Model) - assert isinstance(m, Model) - assert isinstance(m.cls, str) - - -def test_discriminated_union_case() -> None: - class A(BaseModel): - type: Literal["a"] - - data: bool - - class B(BaseModel): - type: Literal["b"] - - data: List[Union[A, object]] - - class ModelA(BaseModel): - type: Literal["modelA"] - - data: int - - class ModelB(BaseModel): - type: Literal["modelB"] - - required: str - - data: Union[A, B] - - # when constructing ModelA | ModelB, value data doesn't match ModelB exactly - missing `required` - m = construct_type( - value={"type": "modelB", "data": {"type": "a", "data": True}}, - type_=cast(Any, Annotated[Union[ModelA, ModelB], PropertyInfo(discriminator="type")]), - ) - - assert isinstance(m, ModelB) - - -def test_nested_discriminated_union() -> None: - class InnerType1(BaseModel): - type: Literal["type_1"] - - class InnerModel(BaseModel): - inner_value: str - - class InnerType2(BaseModel): - type: Literal["type_2"] - some_inner_model: InnerModel - - class Type1(BaseModel): - base_type: Literal["base_type_1"] - value: Annotated[ - Union[ - InnerType1, - InnerType2, - ], - PropertyInfo(discriminator="type"), - ] - - class Type2(BaseModel): - base_type: Literal["base_type_2"] - - T = Annotated[ - Union[ - Type1, - Type2, - ], - PropertyInfo(discriminator="base_type"), - ] - - model = construct_type( - type_=T, - value={ - "base_type": "base_type_1", - "value": { - "type": "type_2", - }, - }, - ) - assert isinstance(model, Type1) - assert isinstance(model.value, InnerType2) - - -@pytest.mark.skipif(not PYDANTIC_V2, reason="this is only supported in pydantic v2 for now") -def test_extra_properties() -> None: - class Item(BaseModel): - prop: int - - class Model(BaseModel): - __pydantic_extra__: Dict[str, Item] = Field(init=False) # pyright: ignore[reportIncompatibleVariableOverride] - - other: str - - if TYPE_CHECKING: - - def __getattr__(self, attr: str) -> Item: ... - - model = construct_type( - type_=Model, - value={ - "a": {"prop": 1}, - "other": "foo", - }, - ) - assert isinstance(model, Model) - assert model.a.prop == 1 - assert isinstance(model.a, Item) - assert model.other == "foo" diff --git a/tests/test_qs.py b/tests/test_qs.py deleted file mode 100644 index b47f757..0000000 --- a/tests/test_qs.py +++ /dev/null @@ -1,78 +0,0 @@ -from typing import Any, cast -from functools import partial -from urllib.parse import unquote - -import pytest - -from browser_use_sdk._qs import Querystring, stringify - - -def test_empty() -> None: - assert stringify({}) == "" - assert stringify({"a": {}}) == "" - assert stringify({"a": {"b": {"c": {}}}}) == "" - - -def test_basic() -> None: - assert stringify({"a": 1}) == "a=1" - assert stringify({"a": "b"}) == "a=b" - assert stringify({"a": True}) == "a=true" - assert stringify({"a": False}) == "a=false" - assert stringify({"a": 1.23456}) == "a=1.23456" - assert stringify({"a": None}) == "" - - -@pytest.mark.parametrize("method", ["class", "function"]) -def test_nested_dotted(method: str) -> None: - if method == "class": - serialise = Querystring(nested_format="dots").stringify - else: - serialise = partial(stringify, nested_format="dots") - - assert unquote(serialise({"a": {"b": "c"}})) == "a.b=c" - assert unquote(serialise({"a": {"b": "c", "d": "e", "f": "g"}})) == "a.b=c&a.d=e&a.f=g" - assert unquote(serialise({"a": {"b": {"c": {"d": "e"}}}})) == "a.b.c.d=e" - assert unquote(serialise({"a": {"b": True}})) == "a.b=true" - - -def test_nested_brackets() -> None: - assert unquote(stringify({"a": {"b": "c"}})) == "a[b]=c" - assert unquote(stringify({"a": {"b": "c", "d": "e", "f": "g"}})) == "a[b]=c&a[d]=e&a[f]=g" - assert unquote(stringify({"a": {"b": {"c": {"d": "e"}}}})) == "a[b][c][d]=e" - assert unquote(stringify({"a": {"b": True}})) == "a[b]=true" - - -@pytest.mark.parametrize("method", ["class", "function"]) -def test_array_comma(method: str) -> None: - if method == "class": - serialise = Querystring(array_format="comma").stringify - else: - serialise = partial(stringify, array_format="comma") - - assert unquote(serialise({"in": ["foo", "bar"]})) == "in=foo,bar" - assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b]=true,false" - assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b]=true,false,true" - - -def test_array_repeat() -> None: - assert unquote(stringify({"in": ["foo", "bar"]})) == "in=foo&in=bar" - assert unquote(stringify({"a": {"b": [True, False]}})) == "a[b]=true&a[b]=false" - assert unquote(stringify({"a": {"b": [True, False, None, True]}})) == "a[b]=true&a[b]=false&a[b]=true" - assert unquote(stringify({"in": ["foo", {"b": {"c": ["d", "e"]}}]})) == "in=foo&in[b][c]=d&in[b][c]=e" - - -@pytest.mark.parametrize("method", ["class", "function"]) -def test_array_brackets(method: str) -> None: - if method == "class": - serialise = Querystring(array_format="brackets").stringify - else: - serialise = partial(stringify, array_format="brackets") - - assert unquote(serialise({"in": ["foo", "bar"]})) == "in[]=foo&in[]=bar" - assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b][]=true&a[b][]=false" - assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b][]=true&a[b][]=false&a[b][]=true" - - -def test_unknown_array_format() -> None: - with pytest.raises(NotImplementedError, match="Unknown array_format value: foo, choose from comma, repeat"): - stringify({"a": ["foo", "bar"]}, array_format=cast(Any, "foo")) diff --git a/tests/test_required_args.py b/tests/test_required_args.py deleted file mode 100644 index eb5821a..0000000 --- a/tests/test_required_args.py +++ /dev/null @@ -1,111 +0,0 @@ -from __future__ import annotations - -import pytest - -from browser_use_sdk._utils import required_args - - -def test_too_many_positional_params() -> None: - @required_args(["a"]) - def foo(a: str | None = None) -> str | None: - return a - - with pytest.raises(TypeError, match=r"foo\(\) takes 1 argument\(s\) but 2 were given"): - foo("a", "b") # type: ignore - - -def test_positional_param() -> None: - @required_args(["a"]) - def foo(a: str | None = None) -> str | None: - return a - - assert foo("a") == "a" - assert foo(None) is None - assert foo(a="b") == "b" - - with pytest.raises(TypeError, match="Missing required argument: 'a'"): - foo() - - -def test_keyword_only_param() -> None: - @required_args(["a"]) - def foo(*, a: str | None = None) -> str | None: - return a - - assert foo(a="a") == "a" - assert foo(a=None) is None - assert foo(a="b") == "b" - - with pytest.raises(TypeError, match="Missing required argument: 'a'"): - foo() - - -def test_multiple_params() -> None: - @required_args(["a", "b", "c"]) - def foo(a: str = "", *, b: str = "", c: str = "") -> str | None: - return f"{a} {b} {c}" - - assert foo(a="a", b="b", c="c") == "a b c" - - error_message = r"Missing required arguments.*" - - with pytest.raises(TypeError, match=error_message): - foo() - - with pytest.raises(TypeError, match=error_message): - foo(a="a") - - with pytest.raises(TypeError, match=error_message): - foo(b="b") - - with pytest.raises(TypeError, match=error_message): - foo(c="c") - - with pytest.raises(TypeError, match=r"Missing required argument: 'a'"): - foo(b="a", c="c") - - with pytest.raises(TypeError, match=r"Missing required argument: 'b'"): - foo("a", c="c") - - -def test_multiple_variants() -> None: - @required_args(["a"], ["b"]) - def foo(*, a: str | None = None, b: str | None = None) -> str | None: - return a if a is not None else b - - assert foo(a="foo") == "foo" - assert foo(b="bar") == "bar" - assert foo(a=None) is None - assert foo(b=None) is None - - # TODO: this error message could probably be improved - with pytest.raises( - TypeError, - match=r"Missing required arguments; Expected either \('a'\) or \('b'\) arguments to be given", - ): - foo() - - -def test_multiple_params_multiple_variants() -> None: - @required_args(["a", "b"], ["c"]) - def foo(*, a: str | None = None, b: str | None = None, c: str | None = None) -> str | None: - if a is not None: - return a - if b is not None: - return b - return c - - error_message = r"Missing required arguments; Expected either \('a' and 'b'\) or \('c'\) arguments to be given" - - with pytest.raises(TypeError, match=error_message): - foo(a="foo") - - with pytest.raises(TypeError, match=error_message): - foo(b="bar") - - with pytest.raises(TypeError, match=error_message): - foo() - - assert foo(a=None, b="bar") == "bar" - assert foo(c=None) is None - assert foo(c="foo") == "foo" diff --git a/tests/test_response.py b/tests/test_response.py deleted file mode 100644 index c64b9ed..0000000 --- a/tests/test_response.py +++ /dev/null @@ -1,277 +0,0 @@ -import json -from typing import Any, List, Union, cast -from typing_extensions import Annotated - -import httpx -import pytest -import pydantic - -from browser_use_sdk import BaseModel, BrowserUse, AsyncBrowserUse -from browser_use_sdk._response import ( - APIResponse, - BaseAPIResponse, - AsyncAPIResponse, - BinaryAPIResponse, - AsyncBinaryAPIResponse, - extract_response_type, -) -from browser_use_sdk._streaming import Stream -from browser_use_sdk._base_client import FinalRequestOptions - - -class ConcreteBaseAPIResponse(APIResponse[bytes]): ... - - -class ConcreteAPIResponse(APIResponse[List[str]]): ... - - -class ConcreteAsyncAPIResponse(APIResponse[httpx.Response]): ... - - -def test_extract_response_type_direct_classes() -> None: - assert extract_response_type(BaseAPIResponse[str]) == str - assert extract_response_type(APIResponse[str]) == str - assert extract_response_type(AsyncAPIResponse[str]) == str - - -def test_extract_response_type_direct_class_missing_type_arg() -> None: - with pytest.raises( - RuntimeError, - match="Expected type to have a type argument at index 0 but it did not", - ): - extract_response_type(AsyncAPIResponse) - - -def test_extract_response_type_concrete_subclasses() -> None: - assert extract_response_type(ConcreteBaseAPIResponse) == bytes - assert extract_response_type(ConcreteAPIResponse) == List[str] - assert extract_response_type(ConcreteAsyncAPIResponse) == httpx.Response - - -def test_extract_response_type_binary_response() -> None: - assert extract_response_type(BinaryAPIResponse) == bytes - assert extract_response_type(AsyncBinaryAPIResponse) == bytes - - -class PydanticModel(pydantic.BaseModel): ... - - -def test_response_parse_mismatched_basemodel(client: BrowserUse) -> None: - response = APIResponse( - raw=httpx.Response(200, content=b"foo"), - client=client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - with pytest.raises( - TypeError, - match="Pydantic models must subclass our base model type, e.g. `from browser_use_sdk import BaseModel`", - ): - response.parse(to=PydanticModel) - - -@pytest.mark.asyncio -async def test_async_response_parse_mismatched_basemodel(async_client: AsyncBrowserUse) -> None: - response = AsyncAPIResponse( - raw=httpx.Response(200, content=b"foo"), - client=async_client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - with pytest.raises( - TypeError, - match="Pydantic models must subclass our base model type, e.g. `from browser_use_sdk import BaseModel`", - ): - await response.parse(to=PydanticModel) - - -def test_response_parse_custom_stream(client: BrowserUse) -> None: - response = APIResponse( - raw=httpx.Response(200, content=b"foo"), - client=client, - stream=True, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - stream = response.parse(to=Stream[int]) - assert stream._cast_to == int - - -@pytest.mark.asyncio -async def test_async_response_parse_custom_stream(async_client: AsyncBrowserUse) -> None: - response = AsyncAPIResponse( - raw=httpx.Response(200, content=b"foo"), - client=async_client, - stream=True, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - stream = await response.parse(to=Stream[int]) - assert stream._cast_to == int - - -class CustomModel(BaseModel): - foo: str - bar: int - - -def test_response_parse_custom_model(client: BrowserUse) -> None: - response = APIResponse( - raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), - client=client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - obj = response.parse(to=CustomModel) - assert obj.foo == "hello!" - assert obj.bar == 2 - - -@pytest.mark.asyncio -async def test_async_response_parse_custom_model(async_client: AsyncBrowserUse) -> None: - response = AsyncAPIResponse( - raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), - client=async_client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - obj = await response.parse(to=CustomModel) - assert obj.foo == "hello!" - assert obj.bar == 2 - - -def test_response_parse_annotated_type(client: BrowserUse) -> None: - response = APIResponse( - raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), - client=client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - obj = response.parse( - to=cast("type[CustomModel]", Annotated[CustomModel, "random metadata"]), - ) - assert obj.foo == "hello!" - assert obj.bar == 2 - - -async def test_async_response_parse_annotated_type(async_client: AsyncBrowserUse) -> None: - response = AsyncAPIResponse( - raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), - client=async_client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - obj = await response.parse( - to=cast("type[CustomModel]", Annotated[CustomModel, "random metadata"]), - ) - assert obj.foo == "hello!" - assert obj.bar == 2 - - -@pytest.mark.parametrize( - "content, expected", - [ - ("false", False), - ("true", True), - ("False", False), - ("True", True), - ("TrUe", True), - ("FalSe", False), - ], -) -def test_response_parse_bool(client: BrowserUse, content: str, expected: bool) -> None: - response = APIResponse( - raw=httpx.Response(200, content=content), - client=client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - result = response.parse(to=bool) - assert result is expected - - -@pytest.mark.parametrize( - "content, expected", - [ - ("false", False), - ("true", True), - ("False", False), - ("True", True), - ("TrUe", True), - ("FalSe", False), - ], -) -async def test_async_response_parse_bool(client: AsyncBrowserUse, content: str, expected: bool) -> None: - response = AsyncAPIResponse( - raw=httpx.Response(200, content=content), - client=client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - result = await response.parse(to=bool) - assert result is expected - - -class OtherModel(BaseModel): - a: str - - -@pytest.mark.parametrize("client", [False], indirect=True) # loose validation -def test_response_parse_expect_model_union_non_json_content(client: BrowserUse) -> None: - response = APIResponse( - raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), - client=client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - obj = response.parse(to=cast(Any, Union[CustomModel, OtherModel])) - assert isinstance(obj, str) - assert obj == "foo" - - -@pytest.mark.asyncio -@pytest.mark.parametrize("async_client", [False], indirect=True) # loose validation -async def test_async_response_parse_expect_model_union_non_json_content(async_client: AsyncBrowserUse) -> None: - response = AsyncAPIResponse( - raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), - client=async_client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - obj = await response.parse(to=cast(Any, Union[CustomModel, OtherModel])) - assert isinstance(obj, str) - assert obj == "foo" diff --git a/tests/test_streaming.py b/tests/test_streaming.py deleted file mode 100644 index 84f2452..0000000 --- a/tests/test_streaming.py +++ /dev/null @@ -1,250 +0,0 @@ -from __future__ import annotations - -from typing import Iterator, AsyncIterator - -import httpx -import pytest - -from browser_use_sdk import BrowserUse, AsyncBrowserUse -from browser_use_sdk._streaming import Stream, AsyncStream, ServerSentEvent - - -@pytest.mark.asyncio -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_basic(sync: bool, client: BrowserUse, async_client: AsyncBrowserUse) -> None: - def body() -> Iterator[bytes]: - yield b"event: completion\n" - yield b'data: {"foo":true}\n' - yield b"\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event == "completion" - assert sse.json() == {"foo": True} - - await assert_empty_iter(iterator) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_data_missing_event(sync: bool, client: BrowserUse, async_client: AsyncBrowserUse) -> None: - def body() -> Iterator[bytes]: - yield b'data: {"foo":true}\n' - yield b"\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event is None - assert sse.json() == {"foo": True} - - await assert_empty_iter(iterator) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_event_missing_data(sync: bool, client: BrowserUse, async_client: AsyncBrowserUse) -> None: - def body() -> Iterator[bytes]: - yield b"event: ping\n" - yield b"\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event == "ping" - assert sse.data == "" - - await assert_empty_iter(iterator) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_events(sync: bool, client: BrowserUse, async_client: AsyncBrowserUse) -> None: - def body() -> Iterator[bytes]: - yield b"event: ping\n" - yield b"\n" - yield b"event: completion\n" - yield b"\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event == "ping" - assert sse.data == "" - - sse = await iter_next(iterator) - assert sse.event == "completion" - assert sse.data == "" - - await assert_empty_iter(iterator) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_events_with_data(sync: bool, client: BrowserUse, async_client: AsyncBrowserUse) -> None: - def body() -> Iterator[bytes]: - yield b"event: ping\n" - yield b'data: {"foo":true}\n' - yield b"\n" - yield b"event: completion\n" - yield b'data: {"bar":false}\n' - yield b"\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event == "ping" - assert sse.json() == {"foo": True} - - sse = await iter_next(iterator) - assert sse.event == "completion" - assert sse.json() == {"bar": False} - - await assert_empty_iter(iterator) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_data_lines_with_empty_line( - sync: bool, client: BrowserUse, async_client: AsyncBrowserUse -) -> None: - def body() -> Iterator[bytes]: - yield b"event: ping\n" - yield b"data: {\n" - yield b'data: "foo":\n' - yield b"data: \n" - yield b"data:\n" - yield b"data: true}\n" - yield b"\n\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event == "ping" - assert sse.json() == {"foo": True} - assert sse.data == '{\n"foo":\n\n\ntrue}' - - await assert_empty_iter(iterator) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_data_json_escaped_double_new_line(sync: bool, client: BrowserUse, async_client: AsyncBrowserUse) -> None: - def body() -> Iterator[bytes]: - yield b"event: ping\n" - yield b'data: {"foo": "my long\\n\\ncontent"}' - yield b"\n\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event == "ping" - assert sse.json() == {"foo": "my long\n\ncontent"} - - await assert_empty_iter(iterator) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_data_lines(sync: bool, client: BrowserUse, async_client: AsyncBrowserUse) -> None: - def body() -> Iterator[bytes]: - yield b"event: ping\n" - yield b"data: {\n" - yield b'data: "foo":\n' - yield b"data: true}\n" - yield b"\n\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event == "ping" - assert sse.json() == {"foo": True} - - await assert_empty_iter(iterator) - - -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_special_new_line_character( - sync: bool, - client: BrowserUse, - async_client: AsyncBrowserUse, -) -> None: - def body() -> Iterator[bytes]: - yield b'data: {"content":" culpa"}\n' - yield b"\n" - yield b'data: {"content":" \xe2\x80\xa8"}\n' - yield b"\n" - yield b'data: {"content":"foo"}\n' - yield b"\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event is None - assert sse.json() == {"content": " culpa"} - - sse = await iter_next(iterator) - assert sse.event is None - assert sse.json() == {"content": " 
"} - - sse = await iter_next(iterator) - assert sse.event is None - assert sse.json() == {"content": "foo"} - - await assert_empty_iter(iterator) - - -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multi_byte_character_multiple_chunks( - sync: bool, - client: BrowserUse, - async_client: AsyncBrowserUse, -) -> None: - def body() -> Iterator[bytes]: - yield b'data: {"content":"' - # bytes taken from the string 'известни' and arbitrarily split - # so that some multi-byte characters span multiple chunks - yield b"\xd0" - yield b"\xb8\xd0\xb7\xd0" - yield b"\xb2\xd0\xb5\xd1\x81\xd1\x82\xd0\xbd\xd0\xb8" - yield b'"}\n' - yield b"\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event is None - assert sse.json() == {"content": "известни"} - - -async def to_aiter(iter: Iterator[bytes]) -> AsyncIterator[bytes]: - for chunk in iter: - yield chunk - - -async def iter_next(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> ServerSentEvent: - if isinstance(iter, AsyncIterator): - return await iter.__anext__() - - return next(iter) - - -async def assert_empty_iter(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> None: - with pytest.raises((StopAsyncIteration, RuntimeError)): - await iter_next(iter) - - -def make_event_iterator( - content: Iterator[bytes], - *, - sync: bool, - client: BrowserUse, - async_client: AsyncBrowserUse, -) -> Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]: - if sync: - return Stream(cast_to=object, client=client, response=httpx.Response(200, content=content))._iter_events() - - return AsyncStream( - cast_to=object, client=async_client, response=httpx.Response(200, content=to_aiter(content)) - )._iter_events() diff --git a/tests/test_transform.py b/tests/test_transform.py deleted file mode 100644 index b336597..0000000 --- a/tests/test_transform.py +++ /dev/null @@ -1,453 +0,0 @@ -from __future__ import annotations - -import io -import pathlib -from typing import Any, Dict, List, Union, TypeVar, Iterable, Optional, cast -from datetime import date, datetime -from typing_extensions import Required, Annotated, TypedDict - -import pytest - -from browser_use_sdk._types import NOT_GIVEN, Base64FileInput -from browser_use_sdk._utils import ( - PropertyInfo, - transform as _transform, - parse_datetime, - async_transform as _async_transform, -) -from browser_use_sdk._compat import PYDANTIC_V2 -from browser_use_sdk._models import BaseModel - -_T = TypeVar("_T") - -SAMPLE_FILE_PATH = pathlib.Path(__file__).parent.joinpath("sample_file.txt") - - -async def transform( - data: _T, - expected_type: object, - use_async: bool, -) -> _T: - if use_async: - return await _async_transform(data, expected_type=expected_type) - - return _transform(data, expected_type=expected_type) - - -parametrize = pytest.mark.parametrize("use_async", [False, True], ids=["sync", "async"]) - - -class Foo1(TypedDict): - foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] - - -@parametrize -@pytest.mark.asyncio -async def test_top_level_alias(use_async: bool) -> None: - assert await transform({"foo_bar": "hello"}, expected_type=Foo1, use_async=use_async) == {"fooBar": "hello"} - - -class Foo2(TypedDict): - bar: Bar2 - - -class Bar2(TypedDict): - this_thing: Annotated[int, PropertyInfo(alias="this__thing")] - baz: Annotated[Baz2, PropertyInfo(alias="Baz")] - - -class Baz2(TypedDict): - my_baz: Annotated[str, PropertyInfo(alias="myBaz")] - - -@parametrize -@pytest.mark.asyncio -async def test_recursive_typeddict(use_async: bool) -> None: - assert await transform({"bar": {"this_thing": 1}}, Foo2, use_async) == {"bar": {"this__thing": 1}} - assert await transform({"bar": {"baz": {"my_baz": "foo"}}}, Foo2, use_async) == {"bar": {"Baz": {"myBaz": "foo"}}} - - -class Foo3(TypedDict): - things: List[Bar3] - - -class Bar3(TypedDict): - my_field: Annotated[str, PropertyInfo(alias="myField")] - - -@parametrize -@pytest.mark.asyncio -async def test_list_of_typeddict(use_async: bool) -> None: - result = await transform({"things": [{"my_field": "foo"}, {"my_field": "foo2"}]}, Foo3, use_async) - assert result == {"things": [{"myField": "foo"}, {"myField": "foo2"}]} - - -class Foo4(TypedDict): - foo: Union[Bar4, Baz4] - - -class Bar4(TypedDict): - foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] - - -class Baz4(TypedDict): - foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] - - -@parametrize -@pytest.mark.asyncio -async def test_union_of_typeddict(use_async: bool) -> None: - assert await transform({"foo": {"foo_bar": "bar"}}, Foo4, use_async) == {"foo": {"fooBar": "bar"}} - assert await transform({"foo": {"foo_baz": "baz"}}, Foo4, use_async) == {"foo": {"fooBaz": "baz"}} - assert await transform({"foo": {"foo_baz": "baz", "foo_bar": "bar"}}, Foo4, use_async) == { - "foo": {"fooBaz": "baz", "fooBar": "bar"} - } - - -class Foo5(TypedDict): - foo: Annotated[Union[Bar4, List[Baz4]], PropertyInfo(alias="FOO")] - - -class Bar5(TypedDict): - foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] - - -class Baz5(TypedDict): - foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] - - -@parametrize -@pytest.mark.asyncio -async def test_union_of_list(use_async: bool) -> None: - assert await transform({"foo": {"foo_bar": "bar"}}, Foo5, use_async) == {"FOO": {"fooBar": "bar"}} - assert await transform( - { - "foo": [ - {"foo_baz": "baz"}, - {"foo_baz": "baz"}, - ] - }, - Foo5, - use_async, - ) == {"FOO": [{"fooBaz": "baz"}, {"fooBaz": "baz"}]} - - -class Foo6(TypedDict): - bar: Annotated[str, PropertyInfo(alias="Bar")] - - -@parametrize -@pytest.mark.asyncio -async def test_includes_unknown_keys(use_async: bool) -> None: - assert await transform({"bar": "bar", "baz_": {"FOO": 1}}, Foo6, use_async) == { - "Bar": "bar", - "baz_": {"FOO": 1}, - } - - -class Foo7(TypedDict): - bar: Annotated[List[Bar7], PropertyInfo(alias="bAr")] - foo: Bar7 - - -class Bar7(TypedDict): - foo: str - - -@parametrize -@pytest.mark.asyncio -async def test_ignores_invalid_input(use_async: bool) -> None: - assert await transform({"bar": ""}, Foo7, use_async) == {"bAr": ""} - assert await transform({"foo": ""}, Foo7, use_async) == {"foo": ""} - - -class DatetimeDict(TypedDict, total=False): - foo: Annotated[datetime, PropertyInfo(format="iso8601")] - - bar: Annotated[Optional[datetime], PropertyInfo(format="iso8601")] - - required: Required[Annotated[Optional[datetime], PropertyInfo(format="iso8601")]] - - list_: Required[Annotated[Optional[List[datetime]], PropertyInfo(format="iso8601")]] - - union: Annotated[Union[int, datetime], PropertyInfo(format="iso8601")] - - -class DateDict(TypedDict, total=False): - foo: Annotated[date, PropertyInfo(format="iso8601")] - - -class DatetimeModel(BaseModel): - foo: datetime - - -class DateModel(BaseModel): - foo: Optional[date] - - -@parametrize -@pytest.mark.asyncio -async def test_iso8601_format(use_async: bool) -> None: - dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") - tz = "Z" if PYDANTIC_V2 else "+00:00" - assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] - assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692" + tz} # type: ignore[comparison-overlap] - - dt = dt.replace(tzinfo=None) - assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap] - assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap] - - assert await transform({"foo": None}, DateDict, use_async) == {"foo": None} # type: ignore[comparison-overlap] - assert await transform(DateModel(foo=None), Any, use_async) == {"foo": None} # type: ignore - assert await transform({"foo": date.fromisoformat("2023-02-23")}, DateDict, use_async) == {"foo": "2023-02-23"} # type: ignore[comparison-overlap] - assert await transform(DateModel(foo=date.fromisoformat("2023-02-23")), DateDict, use_async) == { - "foo": "2023-02-23" - } # type: ignore[comparison-overlap] - - -@parametrize -@pytest.mark.asyncio -async def test_optional_iso8601_format(use_async: bool) -> None: - dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") - assert await transform({"bar": dt}, DatetimeDict, use_async) == {"bar": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] - - assert await transform({"bar": None}, DatetimeDict, use_async) == {"bar": None} - - -@parametrize -@pytest.mark.asyncio -async def test_required_iso8601_format(use_async: bool) -> None: - dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") - assert await transform({"required": dt}, DatetimeDict, use_async) == { - "required": "2023-02-23T14:16:36.337692+00:00" - } # type: ignore[comparison-overlap] - - assert await transform({"required": None}, DatetimeDict, use_async) == {"required": None} - - -@parametrize -@pytest.mark.asyncio -async def test_union_datetime(use_async: bool) -> None: - dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") - assert await transform({"union": dt}, DatetimeDict, use_async) == { # type: ignore[comparison-overlap] - "union": "2023-02-23T14:16:36.337692+00:00" - } - - assert await transform({"union": "foo"}, DatetimeDict, use_async) == {"union": "foo"} - - -@parametrize -@pytest.mark.asyncio -async def test_nested_list_iso6801_format(use_async: bool) -> None: - dt1 = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") - dt2 = parse_datetime("2022-01-15T06:34:23Z") - assert await transform({"list_": [dt1, dt2]}, DatetimeDict, use_async) == { # type: ignore[comparison-overlap] - "list_": ["2023-02-23T14:16:36.337692+00:00", "2022-01-15T06:34:23+00:00"] - } - - -@parametrize -@pytest.mark.asyncio -async def test_datetime_custom_format(use_async: bool) -> None: - dt = parse_datetime("2022-01-15T06:34:23Z") - - result = await transform(dt, Annotated[datetime, PropertyInfo(format="custom", format_template="%H")], use_async) - assert result == "06" # type: ignore[comparison-overlap] - - -class DateDictWithRequiredAlias(TypedDict, total=False): - required_prop: Required[Annotated[date, PropertyInfo(format="iso8601", alias="prop")]] - - -@parametrize -@pytest.mark.asyncio -async def test_datetime_with_alias(use_async: bool) -> None: - assert await transform({"required_prop": None}, DateDictWithRequiredAlias, use_async) == {"prop": None} # type: ignore[comparison-overlap] - assert await transform( - {"required_prop": date.fromisoformat("2023-02-23")}, DateDictWithRequiredAlias, use_async - ) == {"prop": "2023-02-23"} # type: ignore[comparison-overlap] - - -class MyModel(BaseModel): - foo: str - - -@parametrize -@pytest.mark.asyncio -async def test_pydantic_model_to_dictionary(use_async: bool) -> None: - assert cast(Any, await transform(MyModel(foo="hi!"), Any, use_async)) == {"foo": "hi!"} - assert cast(Any, await transform(MyModel.construct(foo="hi!"), Any, use_async)) == {"foo": "hi!"} - - -@parametrize -@pytest.mark.asyncio -async def test_pydantic_empty_model(use_async: bool) -> None: - assert cast(Any, await transform(MyModel.construct(), Any, use_async)) == {} - - -@parametrize -@pytest.mark.asyncio -async def test_pydantic_unknown_field(use_async: bool) -> None: - assert cast(Any, await transform(MyModel.construct(my_untyped_field=True), Any, use_async)) == { - "my_untyped_field": True - } - - -@parametrize -@pytest.mark.asyncio -async def test_pydantic_mismatched_types(use_async: bool) -> None: - model = MyModel.construct(foo=True) - if PYDANTIC_V2: - with pytest.warns(UserWarning): - params = await transform(model, Any, use_async) - else: - params = await transform(model, Any, use_async) - assert cast(Any, params) == {"foo": True} - - -@parametrize -@pytest.mark.asyncio -async def test_pydantic_mismatched_object_type(use_async: bool) -> None: - model = MyModel.construct(foo=MyModel.construct(hello="world")) - if PYDANTIC_V2: - with pytest.warns(UserWarning): - params = await transform(model, Any, use_async) - else: - params = await transform(model, Any, use_async) - assert cast(Any, params) == {"foo": {"hello": "world"}} - - -class ModelNestedObjects(BaseModel): - nested: MyModel - - -@parametrize -@pytest.mark.asyncio -async def test_pydantic_nested_objects(use_async: bool) -> None: - model = ModelNestedObjects.construct(nested={"foo": "stainless"}) - assert isinstance(model.nested, MyModel) - assert cast(Any, await transform(model, Any, use_async)) == {"nested": {"foo": "stainless"}} - - -class ModelWithDefaultField(BaseModel): - foo: str - with_none_default: Union[str, None] = None - with_str_default: str = "foo" - - -@parametrize -@pytest.mark.asyncio -async def test_pydantic_default_field(use_async: bool) -> None: - # should be excluded when defaults are used - model = ModelWithDefaultField.construct() - assert model.with_none_default is None - assert model.with_str_default == "foo" - assert cast(Any, await transform(model, Any, use_async)) == {} - - # should be included when the default value is explicitly given - model = ModelWithDefaultField.construct(with_none_default=None, with_str_default="foo") - assert model.with_none_default is None - assert model.with_str_default == "foo" - assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": None, "with_str_default": "foo"} - - # should be included when a non-default value is explicitly given - model = ModelWithDefaultField.construct(with_none_default="bar", with_str_default="baz") - assert model.with_none_default == "bar" - assert model.with_str_default == "baz" - assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": "bar", "with_str_default": "baz"} - - -class TypedDictIterableUnion(TypedDict): - foo: Annotated[Union[Bar8, Iterable[Baz8]], PropertyInfo(alias="FOO")] - - -class Bar8(TypedDict): - foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] - - -class Baz8(TypedDict): - foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] - - -@parametrize -@pytest.mark.asyncio -async def test_iterable_of_dictionaries(use_async: bool) -> None: - assert await transform({"foo": [{"foo_baz": "bar"}]}, TypedDictIterableUnion, use_async) == { - "FOO": [{"fooBaz": "bar"}] - } - assert cast(Any, await transform({"foo": ({"foo_baz": "bar"},)}, TypedDictIterableUnion, use_async)) == { - "FOO": [{"fooBaz": "bar"}] - } - - def my_iter() -> Iterable[Baz8]: - yield {"foo_baz": "hello"} - yield {"foo_baz": "world"} - - assert await transform({"foo": my_iter()}, TypedDictIterableUnion, use_async) == { - "FOO": [{"fooBaz": "hello"}, {"fooBaz": "world"}] - } - - -@parametrize -@pytest.mark.asyncio -async def test_dictionary_items(use_async: bool) -> None: - class DictItems(TypedDict): - foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] - - assert await transform({"foo": {"foo_baz": "bar"}}, Dict[str, DictItems], use_async) == {"foo": {"fooBaz": "bar"}} - - -class TypedDictIterableUnionStr(TypedDict): - foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")] - - -@parametrize -@pytest.mark.asyncio -async def test_iterable_union_str(use_async: bool) -> None: - assert await transform({"foo": "bar"}, TypedDictIterableUnionStr, use_async) == {"FOO": "bar"} - assert cast(Any, await transform(iter([{"foo_baz": "bar"}]), Union[str, Iterable[Baz8]], use_async)) == [ - {"fooBaz": "bar"} - ] - - -class TypedDictBase64Input(TypedDict): - foo: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")] - - -@parametrize -@pytest.mark.asyncio -async def test_base64_file_input(use_async: bool) -> None: - # strings are left as-is - assert await transform({"foo": "bar"}, TypedDictBase64Input, use_async) == {"foo": "bar"} - - # pathlib.Path is automatically converted to base64 - assert await transform({"foo": SAMPLE_FILE_PATH}, TypedDictBase64Input, use_async) == { - "foo": "SGVsbG8sIHdvcmxkIQo=" - } # type: ignore[comparison-overlap] - - # io instances are automatically converted to base64 - assert await transform({"foo": io.StringIO("Hello, world!")}, TypedDictBase64Input, use_async) == { - "foo": "SGVsbG8sIHdvcmxkIQ==" - } # type: ignore[comparison-overlap] - assert await transform({"foo": io.BytesIO(b"Hello, world!")}, TypedDictBase64Input, use_async) == { - "foo": "SGVsbG8sIHdvcmxkIQ==" - } # type: ignore[comparison-overlap] - - -@parametrize -@pytest.mark.asyncio -async def test_transform_skipping(use_async: bool) -> None: - # lists of ints are left as-is - data = [1, 2, 3] - assert await transform(data, List[int], use_async) is data - - # iterables of ints are converted to a list - data = iter([1, 2, 3]) - assert await transform(data, Iterable[int], use_async) == [1, 2, 3] - - -@parametrize -@pytest.mark.asyncio -async def test_strips_notgiven(use_async: bool) -> None: - assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} - assert await transform({"foo_bar": NOT_GIVEN}, Foo1, use_async) == {} diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py deleted file mode 100644 index a76d3a9..0000000 --- a/tests/test_utils/test_proxy.py +++ /dev/null @@ -1,34 +0,0 @@ -import operator -from typing import Any -from typing_extensions import override - -from browser_use_sdk._utils import LazyProxy - - -class RecursiveLazyProxy(LazyProxy[Any]): - @override - def __load__(self) -> Any: - return self - - def __call__(self, *_args: Any, **_kwds: Any) -> Any: - raise RuntimeError("This should never be called!") - - -def test_recursive_proxy() -> None: - proxy = RecursiveLazyProxy() - assert repr(proxy) == "RecursiveLazyProxy" - assert str(proxy) == "RecursiveLazyProxy" - assert dir(proxy) == [] - assert type(proxy).__name__ == "RecursiveLazyProxy" - assert type(operator.attrgetter("name.foo.bar.baz")(proxy)).__name__ == "RecursiveLazyProxy" - - -def test_isinstance_does_not_error() -> None: - class AlwaysErrorProxy(LazyProxy[Any]): - @override - def __load__(self) -> Any: - raise RuntimeError("Mocking missing dependency") - - proxy = AlwaysErrorProxy() - assert not isinstance(proxy, dict) - assert isinstance(proxy, LazyProxy) diff --git a/tests/test_utils/test_typing.py b/tests/test_utils/test_typing.py deleted file mode 100644 index e12cde1..0000000 --- a/tests/test_utils/test_typing.py +++ /dev/null @@ -1,73 +0,0 @@ -from __future__ import annotations - -from typing import Generic, TypeVar, cast - -from browser_use_sdk._utils import extract_type_var_from_base - -_T = TypeVar("_T") -_T2 = TypeVar("_T2") -_T3 = TypeVar("_T3") - - -class BaseGeneric(Generic[_T]): ... - - -class SubclassGeneric(BaseGeneric[_T]): ... - - -class BaseGenericMultipleTypeArgs(Generic[_T, _T2, _T3]): ... - - -class SubclassGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T, _T2, _T3]): ... - - -class SubclassDifferentOrderGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T2, _T, _T3]): ... - - -def test_extract_type_var() -> None: - assert ( - extract_type_var_from_base( - BaseGeneric[int], - index=0, - generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), - ) - == int - ) - - -def test_extract_type_var_generic_subclass() -> None: - assert ( - extract_type_var_from_base( - SubclassGeneric[int], - index=0, - generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), - ) - == int - ) - - -def test_extract_type_var_multiple() -> None: - typ = BaseGenericMultipleTypeArgs[int, str, None] - - generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) - assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int - assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str - assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) - - -def test_extract_type_var_generic_subclass_multiple() -> None: - typ = SubclassGenericMultipleTypeArgs[int, str, None] - - generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) - assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int - assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str - assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) - - -def test_extract_type_var_generic_subclass_different_ordering_multiple() -> None: - typ = SubclassDifferentOrderGenericMultipleTypeArgs[int, str, None] - - generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) - assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int - assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str - assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) diff --git a/tests/test_webhooks.py b/tests/test_webhooks.py deleted file mode 100644 index 84f09d7..0000000 --- a/tests/test_webhooks.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Tests for webhook functionality.""" - -from __future__ import annotations - -from typing import Any, Dict -from datetime import datetime, timezone - -import pytest - -from browser_use_sdk._compat import PYDANTIC_V2 -from browser_use_sdk.lib.webhooks import ( - WebhookTest, - WebhookTestPayload, - create_webhook_signature, - verify_webhook_event_signature, -) - -# Signature Creation --------------------------------------------------------- - - -@pytest.mark.skipif(not PYDANTIC_V2, reason="Webhook tests only run in Pydantic V2") -def test_create_webhook_signature() -> None: - """Test webhook signature creation.""" - secret = "test-secret-key" - timestamp = "2023-01-01T00:00:00Z" - payload = {"test": "ok"} - - signature = create_webhook_signature(payload, timestamp, secret) - - assert isinstance(signature, str) - assert len(signature) == 64 - - signature2 = create_webhook_signature(payload, timestamp, secret) - assert signature == signature2 - - different_payload = {"test": "different"} - different_signature = create_webhook_signature(different_payload, timestamp, secret) - - assert signature != different_signature - - -# Webhook Verification -------------------------------------------------------- - - -@pytest.mark.skipif(not PYDANTIC_V2, reason="Webhook tests only run in Pydantic V2") -def test_verify_webhook_event_signature_valid() -> None: - """Test webhook signature verification with valid signature.""" - secret = "test-secret-key" - timestamp = "2023-01-01T00:00:00Z" - - payload = WebhookTestPayload(test="ok") - webhook = WebhookTest(type="test", timestamp=datetime.now(timezone.utc), payload=payload) - signature = create_webhook_signature(webhook.payload.model_dump(), timestamp, secret) - - # Verify signature - verified_webhook = verify_webhook_event_signature( - body=webhook.model_dump(), - secret=secret, - timestamp=timestamp, - expected_signature=signature, - ) - - assert verified_webhook is not None - assert isinstance(verified_webhook, WebhookTest) - assert verified_webhook.payload.test == "ok" - - -@pytest.mark.skipif(not PYDANTIC_V2, reason="Webhook tests only run in Pydantic V2") -def test_verify_webhook_event_signature_invalid_signature() -> None: - """Test webhook signature verification with invalid signature.""" - secret = "test-secret-key" - timestamp = "2023-01-01T00:00:00Z" - - # Create test webhook - payload = WebhookTestPayload(test="ok") - webhook = WebhookTest(type="test", timestamp=datetime.now(timezone.utc), payload=payload) - - verified_webhook = verify_webhook_event_signature( - body=webhook.model_dump(), - secret=secret, - timestamp=timestamp, - expected_signature="random_invalid_signature", - ) - - assert verified_webhook is None - - -@pytest.mark.skipif(not PYDANTIC_V2, reason="Webhook tests only run in Pydantic V2") -def test_verify_webhook_event_signature_wrong_secret() -> None: - """Test webhook signature verification with wrong secret.""" - - timestamp = "2023-01-01T00:00:00Z" - - # Create test webhook - payload = WebhookTestPayload(test="ok") - webhook = WebhookTest(type="test", timestamp=datetime.now(timezone.utc), payload=payload) - - # Create signature with correct secret - signature = create_webhook_signature( - payload=payload.model_dump(), - timestamp=timestamp, - secret="test-secret-key", - ) - - # Verify with wrong secret - verified_webhook = verify_webhook_event_signature( - body=webhook.model_dump(), - secret="wrong-secret-key", - timestamp=timestamp, - expected_signature=signature, - ) - - assert verified_webhook is None - - -@pytest.mark.skipif(not PYDANTIC_V2, reason="Webhook tests only run in Pydantic V2") -def test_verify_webhook_event_signature_string_body() -> None: - """Test webhook signature verification with string body.""" - secret = "test-secret-key" - timestamp = "2023-01-01T00:00:00Z" - - # Create test webhook - payload = WebhookTestPayload(test="ok") - webhook = WebhookTest(type="test", timestamp=datetime.now(timezone.utc), payload=payload) - signature = create_webhook_signature(webhook.payload.model_dump(), timestamp, secret) - - verified_webhook = verify_webhook_event_signature( - body=webhook.model_dump_json(), - secret=secret, - timestamp=timestamp, - expected_signature=signature, - ) - - assert verified_webhook is not None - assert isinstance(verified_webhook, WebhookTest) - - -@pytest.mark.skipif(not PYDANTIC_V2, reason="Webhook tests only run in Pydantic V2") -def test_verify_webhook_event_signature_invalid_body() -> None: - """Test webhook signature verification with invalid body.""" - secret = "test-secret-key" - timestamp = "2023-01-01T00:00:00Z" - - # Invalid webhook data - invalid_body: Dict[str, Any] = {"type": "invalid_type", "timestamp": "invalid", "payload": {}} - - verified_webhook = verify_webhook_event_signature( - body=invalid_body, secret=secret, expected_signature="some_signature", timestamp=timestamp - ) - - assert verified_webhook is None diff --git a/tests/utils.py b/tests/utils.py deleted file mode 100644 index 858cc96..0000000 --- a/tests/utils.py +++ /dev/null @@ -1,159 +0,0 @@ -from __future__ import annotations - -import os -import inspect -import traceback -import contextlib -from typing import Any, TypeVar, Iterator, cast -from datetime import date, datetime -from typing_extensions import Literal, get_args, get_origin, assert_type - -from browser_use_sdk._types import Omit, NoneType -from browser_use_sdk._utils import ( - is_dict, - is_list, - is_list_type, - is_union_type, - extract_type_arg, - is_annotated_type, - is_type_alias_type, -) -from browser_use_sdk._compat import PYDANTIC_V2, field_outer_type, get_model_fields -from browser_use_sdk._models import BaseModel - -BaseModelT = TypeVar("BaseModelT", bound=BaseModel) - - -def assert_matches_model(model: type[BaseModelT], value: BaseModelT, *, path: list[str]) -> bool: - for name, field in get_model_fields(model).items(): - field_value = getattr(value, name) - if PYDANTIC_V2: - allow_none = False - else: - # in v1 nullability was structured differently - # https://docs.pydantic.dev/2.0/migration/#required-optional-and-nullable-fields - allow_none = getattr(field, "allow_none", False) - - assert_matches_type( - field_outer_type(field), - field_value, - path=[*path, name], - allow_none=allow_none, - ) - - return True - - -# Note: the `path` argument is only used to improve error messages when `--showlocals` is used -def assert_matches_type( - type_: Any, - value: object, - *, - path: list[str], - allow_none: bool = False, -) -> None: - if is_type_alias_type(type_): - type_ = type_.__value__ - - # unwrap `Annotated[T, ...]` -> `T` - if is_annotated_type(type_): - type_ = extract_type_arg(type_, 0) - - if allow_none and value is None: - return - - if type_ is None or type_ is NoneType: - assert value is None - return - - origin = get_origin(type_) or type_ - - if is_list_type(type_): - return _assert_list_type(type_, value) - - if origin == str: - assert isinstance(value, str) - elif origin == int: - assert isinstance(value, int) - elif origin == bool: - assert isinstance(value, bool) - elif origin == float: - assert isinstance(value, float) - elif origin == bytes: - assert isinstance(value, bytes) - elif origin == datetime: - assert isinstance(value, datetime) - elif origin == date: - assert isinstance(value, date) - elif origin == object: - # nothing to do here, the expected type is unknown - pass - elif origin == Literal: - assert value in get_args(type_) - elif origin == dict: - assert is_dict(value) - - args = get_args(type_) - key_type = args[0] - items_type = args[1] - - for key, item in value.items(): - assert_matches_type(key_type, key, path=[*path, ""]) - assert_matches_type(items_type, item, path=[*path, ""]) - elif is_union_type(type_): - variants = get_args(type_) - - try: - none_index = variants.index(type(None)) - except ValueError: - pass - else: - # special case Optional[T] for better error messages - if len(variants) == 2: - if value is None: - # valid - return - - return assert_matches_type(type_=variants[not none_index], value=value, path=path) - - for i, variant in enumerate(variants): - try: - assert_matches_type(variant, value, path=[*path, f"variant {i}"]) - return - except AssertionError: - traceback.print_exc() - continue - - raise AssertionError("Did not match any variants") - elif issubclass(origin, BaseModel): - assert isinstance(value, type_) - assert assert_matches_model(type_, cast(Any, value), path=path) - elif inspect.isclass(origin) and origin.__name__ == "HttpxBinaryResponseContent": - assert value.__class__.__name__ == "HttpxBinaryResponseContent" - else: - assert None, f"Unhandled field type: {type_}" - - -def _assert_list_type(type_: type[object], value: object) -> None: - assert is_list(value) - - inner_type = get_args(type_)[0] - for entry in value: - assert_type(inner_type, entry) # type: ignore - - -@contextlib.contextmanager -def update_env(**new_env: str | Omit) -> Iterator[None]: - old = os.environ.copy() - - try: - for name, value in new_env.items(): - if isinstance(value, Omit): - os.environ.pop(name, None) - else: - os.environ[name] = value - - yield None - finally: - os.environ.clear() - os.environ.update(old) diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000..f3ea265 --- /dev/null +++ b/tests/utils/__init__.py @@ -0,0 +1,2 @@ +# This file was auto-generated by Fern from our API Definition. + diff --git a/tests/utils/assets/models/__init__.py b/tests/utils/assets/models/__init__.py new file mode 100644 index 0000000..2cf0126 --- /dev/null +++ b/tests/utils/assets/models/__init__.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +from .circle import CircleParams +from .object_with_defaults import ObjectWithDefaultsParams +from .object_with_optional_field import ObjectWithOptionalFieldParams +from .shape import Shape_CircleParams, Shape_SquareParams, ShapeParams +from .square import SquareParams +from .undiscriminated_shape import UndiscriminatedShapeParams + +__all__ = [ + "CircleParams", + "ObjectWithDefaultsParams", + "ObjectWithOptionalFieldParams", + "ShapeParams", + "Shape_CircleParams", + "Shape_SquareParams", + "SquareParams", + "UndiscriminatedShapeParams", +] diff --git a/tests/utils/assets/models/circle.py b/tests/utils/assets/models/circle.py new file mode 100644 index 0000000..e2a7d91 --- /dev/null +++ b/tests/utils/assets/models/circle.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + +from browser_use.core.serialization import FieldMetadata + + +class CircleParams(typing_extensions.TypedDict): + radius_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="radiusMeasurement")] diff --git a/tests/utils/assets/models/color.py b/tests/utils/assets/models/color.py new file mode 100644 index 0000000..2aa2c4c --- /dev/null +++ b/tests/utils/assets/models/color.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing + +Color = typing.Union[typing.Literal["red", "blue"], typing.Any] diff --git a/tests/utils/assets/models/object_with_defaults.py b/tests/utils/assets/models/object_with_defaults.py new file mode 100644 index 0000000..a977b1d --- /dev/null +++ b/tests/utils/assets/models/object_with_defaults.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class ObjectWithDefaultsParams(typing_extensions.TypedDict): + """ + Defines properties with default values and validation rules. + """ + + decimal: typing_extensions.NotRequired[float] + string: typing_extensions.NotRequired[str] + required_string: str diff --git a/tests/utils/assets/models/object_with_optional_field.py b/tests/utils/assets/models/object_with_optional_field.py new file mode 100644 index 0000000..eac76e0 --- /dev/null +++ b/tests/utils/assets/models/object_with_optional_field.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +import uuid + +import typing_extensions +from .color import Color +from .shape import ShapeParams +from .undiscriminated_shape import UndiscriminatedShapeParams + +from browser_use.core.serialization import FieldMetadata + + +class ObjectWithOptionalFieldParams(typing_extensions.TypedDict): + literal: typing.Literal["lit_one"] + string: typing_extensions.NotRequired[str] + integer: typing_extensions.NotRequired[int] + long_: typing_extensions.NotRequired[typing_extensions.Annotated[int, FieldMetadata(alias="long")]] + double: typing_extensions.NotRequired[float] + bool_: typing_extensions.NotRequired[typing_extensions.Annotated[bool, FieldMetadata(alias="bool")]] + datetime: typing_extensions.NotRequired[dt.datetime] + date: typing_extensions.NotRequired[dt.date] + uuid_: typing_extensions.NotRequired[typing_extensions.Annotated[uuid.UUID, FieldMetadata(alias="uuid")]] + base_64: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="base64")]] + list_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Sequence[str], FieldMetadata(alias="list")]] + set_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Set[str], FieldMetadata(alias="set")]] + map_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Dict[int, str], FieldMetadata(alias="map")]] + enum: typing_extensions.NotRequired[Color] + union: typing_extensions.NotRequired[ShapeParams] + second_union: typing_extensions.NotRequired[ShapeParams] + undiscriminated_union: typing_extensions.NotRequired[UndiscriminatedShapeParams] + any: typing.Optional[typing.Any] diff --git a/tests/utils/assets/models/shape.py b/tests/utils/assets/models/shape.py new file mode 100644 index 0000000..1c7b3cb --- /dev/null +++ b/tests/utils/assets/models/shape.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import typing_extensions + +from browser_use.core.serialization import FieldMetadata + + +class Base(typing_extensions.TypedDict): + id: str + + +class Shape_CircleParams(Base): + shape_type: typing_extensions.Annotated[typing.Literal["circle"], FieldMetadata(alias="shapeType")] + radius_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="radiusMeasurement")] + + +class Shape_SquareParams(Base): + shape_type: typing_extensions.Annotated[typing.Literal["square"], FieldMetadata(alias="shapeType")] + length_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="lengthMeasurement")] + + +ShapeParams = typing.Union[Shape_CircleParams, Shape_SquareParams] diff --git a/tests/utils/assets/models/square.py b/tests/utils/assets/models/square.py new file mode 100644 index 0000000..3021cd4 --- /dev/null +++ b/tests/utils/assets/models/square.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + +from browser_use.core.serialization import FieldMetadata + + +class SquareParams(typing_extensions.TypedDict): + length_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="lengthMeasurement")] diff --git a/tests/utils/assets/models/undiscriminated_shape.py b/tests/utils/assets/models/undiscriminated_shape.py new file mode 100644 index 0000000..99f12b3 --- /dev/null +++ b/tests/utils/assets/models/undiscriminated_shape.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .circle import CircleParams +from .square import SquareParams + +UndiscriminatedShapeParams = typing.Union[CircleParams, SquareParams] diff --git a/tests/utils/test_http_client.py b/tests/utils/test_http_client.py new file mode 100644 index 0000000..d9cdaaa --- /dev/null +++ b/tests/utils/test_http_client.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from browser_use.core.http_client import get_request_body +from browser_use.core.request_options import RequestOptions + + +def get_request_options() -> RequestOptions: + return {"additional_body_parameters": {"see you": "later"}} + + +def test_get_json_request_body() -> None: + json_body, data_body = get_request_body(json={"hello": "world"}, data=None, request_options=None, omit=None) + assert json_body == {"hello": "world"} + assert data_body is None + + json_body_extras, data_body_extras = get_request_body( + json={"goodbye": "world"}, data=None, request_options=get_request_options(), omit=None + ) + + assert json_body_extras == {"goodbye": "world", "see you": "later"} + assert data_body_extras is None + + +def test_get_files_request_body() -> None: + json_body, data_body = get_request_body(json=None, data={"hello": "world"}, request_options=None, omit=None) + assert data_body == {"hello": "world"} + assert json_body is None + + json_body_extras, data_body_extras = get_request_body( + json=None, data={"goodbye": "world"}, request_options=get_request_options(), omit=None + ) + + assert data_body_extras == {"goodbye": "world", "see you": "later"} + assert json_body_extras is None + + +def test_get_none_request_body() -> None: + json_body, data_body = get_request_body(json=None, data=None, request_options=None, omit=None) + assert data_body is None + assert json_body is None + + json_body_extras, data_body_extras = get_request_body( + json=None, data=None, request_options=get_request_options(), omit=None + ) + + assert json_body_extras == {"see you": "later"} + assert data_body_extras is None + + +def test_get_empty_json_request_body() -> None: + unrelated_request_options: RequestOptions = {"max_retries": 3} + json_body, data_body = get_request_body(json=None, data=None, request_options=unrelated_request_options, omit=None) + assert json_body is None + assert data_body is None + + json_body_extras, data_body_extras = get_request_body( + json={}, data=None, request_options=unrelated_request_options, omit=None + ) + + assert json_body_extras is None + assert data_body_extras is None diff --git a/tests/utils/test_query_encoding.py b/tests/utils/test_query_encoding.py new file mode 100644 index 0000000..aebf381 --- /dev/null +++ b/tests/utils/test_query_encoding.py @@ -0,0 +1,37 @@ +# This file was auto-generated by Fern from our API Definition. + + +from browser_use.core.query_encoder import encode_query + + +def test_query_encoding_deep_objects() -> None: + assert encode_query({"hello world": "hello world"}) == [("hello world", "hello world")] + assert encode_query({"hello_world": {"hello": "world"}}) == [("hello_world[hello]", "world")] + assert encode_query({"hello_world": {"hello": {"world": "today"}, "test": "this"}, "hi": "there"}) == [ + ("hello_world[hello][world]", "today"), + ("hello_world[test]", "this"), + ("hi", "there"), + ] + + +def test_query_encoding_deep_object_arrays() -> None: + assert encode_query({"objects": [{"key": "hello", "value": "world"}, {"key": "foo", "value": "bar"}]}) == [ + ("objects[key]", "hello"), + ("objects[value]", "world"), + ("objects[key]", "foo"), + ("objects[value]", "bar"), + ] + assert encode_query( + {"users": [{"name": "string", "tags": ["string"]}, {"name": "string2", "tags": ["string2", "string3"]}]} + ) == [ + ("users[name]", "string"), + ("users[tags]", "string"), + ("users[name]", "string2"), + ("users[tags]", "string2"), + ("users[tags]", "string3"), + ] + + +def test_encode_query_with_none() -> None: + encoded = encode_query(None) + assert encoded is None diff --git a/tests/utils/test_serialization.py b/tests/utils/test_serialization.py new file mode 100644 index 0000000..1c91638 --- /dev/null +++ b/tests/utils/test_serialization.py @@ -0,0 +1,72 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, List + +from .assets.models import ObjectWithOptionalFieldParams, ShapeParams + +from browser_use.core.serialization import convert_and_respect_annotation_metadata + +UNION_TEST: ShapeParams = {"radius_measurement": 1.0, "shape_type": "circle", "id": "1"} +UNION_TEST_CONVERTED = {"shapeType": "circle", "radiusMeasurement": 1.0, "id": "1"} + + +def test_convert_and_respect_annotation_metadata() -> None: + data: ObjectWithOptionalFieldParams = { + "string": "string", + "long_": 12345, + "bool_": True, + "literal": "lit_one", + "any": "any", + } + converted = convert_and_respect_annotation_metadata( + object_=data, annotation=ObjectWithOptionalFieldParams, direction="write" + ) + assert converted == {"string": "string", "long": 12345, "bool": True, "literal": "lit_one", "any": "any"} + + +def test_convert_and_respect_annotation_metadata_in_list() -> None: + data: List[ObjectWithOptionalFieldParams] = [ + {"string": "string", "long_": 12345, "bool_": True, "literal": "lit_one", "any": "any"}, + {"string": "another string", "long_": 67890, "list_": [], "literal": "lit_one", "any": "any"}, + ] + converted = convert_and_respect_annotation_metadata( + object_=data, annotation=List[ObjectWithOptionalFieldParams], direction="write" + ) + + assert converted == [ + {"string": "string", "long": 12345, "bool": True, "literal": "lit_one", "any": "any"}, + {"string": "another string", "long": 67890, "list": [], "literal": "lit_one", "any": "any"}, + ] + + +def test_convert_and_respect_annotation_metadata_in_nested_object() -> None: + data: ObjectWithOptionalFieldParams = { + "string": "string", + "long_": 12345, + "union": UNION_TEST, + "literal": "lit_one", + "any": "any", + } + converted = convert_and_respect_annotation_metadata( + object_=data, annotation=ObjectWithOptionalFieldParams, direction="write" + ) + + assert converted == { + "string": "string", + "long": 12345, + "union": UNION_TEST_CONVERTED, + "literal": "lit_one", + "any": "any", + } + + +def test_convert_and_respect_annotation_metadata_in_union() -> None: + converted = convert_and_respect_annotation_metadata(object_=UNION_TEST, annotation=ShapeParams, direction="write") + + assert converted == UNION_TEST_CONVERTED + + +def test_convert_and_respect_annotation_metadata_with_empty_object() -> None: + data: Any = {} + converted = convert_and_respect_annotation_metadata(object_=data, annotation=ShapeParams, direction="write") + assert converted == data From 8fb83e2a437cf278e8a0dd5c68a06c5a95d582e9 Mon Sep 17 00:00:00 2001 From: fern-api <115122769+fern-api[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 08:14:53 +0000 Subject: [PATCH 02/13] SDK regeneration --- README.md | 20 ++-- assets/cloud-banner-python.png | Bin 34707 -> 0 bytes examples/async_create.py | 57 ---------- examples/async_retrieve.py | 97 ----------------- examples/async_run.py | 65 ------------ examples/async_stream.py | 82 -------------- examples/create.py | 48 --------- examples/retrieve.py | 89 ---------------- examples/run.py | 58 ---------- examples/stream.py | 76 ------------- examples/webhooks.py | 65 ------------ reference.md | 80 +++++++------- src/browser_use/__init__.py | 13 ++- src/browser_use/accounts/client.py | 8 +- src/browser_use/base_client.py | 165 +++++++++++++++++++++++++++++ src/browser_use/environment.py | 2 +- src/browser_use/files/client.py | 16 +-- src/browser_use/profiles/client.py | 32 +++--- src/browser_use/sessions/client.py | 64 +++++------ src/browser_use/tasks/client.py | 40 +++---- 20 files changed, 307 insertions(+), 770 deletions(-) delete mode 100644 assets/cloud-banner-python.png delete mode 100755 examples/async_create.py delete mode 100755 examples/async_retrieve.py delete mode 100755 examples/async_run.py delete mode 100755 examples/async_stream.py delete mode 100755 examples/create.py delete mode 100755 examples/retrieve.py delete mode 100755 examples/run.py delete mode 100755 examples/stream.py delete mode 100755 examples/webhooks.py create mode 100644 src/browser_use/base_client.py diff --git a/README.md b/README.md index 0f4433d..7bcad6e 100644 --- a/README.md +++ b/README.md @@ -205,9 +205,9 @@ The SDK provides access to raw response data, including headers, through the `.w The `.with_raw_response` property returns a "raw" client that can be used to access the `.headers` and `.data` attributes. ```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( ..., ) response = client.tasks.with_raw_response.create_task(...) @@ -241,9 +241,9 @@ The SDK defaults to a 60 second timeout. You can configure this with a timeout o ```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( ..., timeout=20.0, ) @@ -262,9 +262,9 @@ and transports. ```python import httpx -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( ..., httpx_client=httpx.Client( proxies="http://my.test.proxy.example.com", @@ -483,9 +483,9 @@ A full reference for this library is available [here](https://github.com/browser Instantiate and use the client with the following: ```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.tasks.create_task( @@ -500,9 +500,9 @@ The SDK also exports an `async` client so that you can make non-blocking calls t ```python import asyncio -from browser_use import AsyncBrowserUse +from browser_use import AsyncBrowserUseClient -client = AsyncBrowserUse( +client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) diff --git a/assets/cloud-banner-python.png b/assets/cloud-banner-python.png deleted file mode 100644 index 77e2aeef0fd9b6b11d91fa253abbf4f5ad4cfd98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34707 zcmeFY`8(9#`v)#5DN<6hM5QbxLQMAPg=7oaw`7Yk_NB2+rSd9UvXyNrA=$UFn~;4O z>kP&ivJ7Ud!whE3e4l!KzW>4Zm+y7G=ek_Z%$PIh+~;xb`*A<++q*|bx?Cp(PO`AD zaOpkx$CQQTcsL6S>$MZdfmiM;23MS93k=?o^8pu?;Q5y%|gWW2j6q4 zV(I)mEnSp1(c$Os>9BrC-aP){=m(*ex_?#_x9FdOqq0yBU7dy#iWZXHm^1HH!`rW1}uHYk=g&dbZ-KV z1MTv+Inw5nL|`sq3_1$Q9K@iu_MGvysMv#|Kfw+ae#>N*ZPmj5h)-ik*c$3rWJWMeEP}ZjiI#%n2R794-nMUD zX-weDLC@96&&fXGYHPD_d2bO&M5Mp!LT!2#S5jE zM>>47Nba!U^)Cn&BTvVIv+lN?V-K+2y@EdU|B`-=eBL?&H7jS)fq6C>gibo+p@ z?uflx#|->~Y25we!yVbNfnlJ14rI8qSdi2(OOR2v4;O<5iku^fiVx)KBWW{u#vCj> zoYcPA>&W016k%dTvAZ{sTg-{w-DqxxG+&2I_^g(EHyS1%gWoU4Pf%K!NJc4AW4F>v zcaPcTf2GfPKMc11Bw&A=a2%G_N$rG%gI88Fwhy8gK6POh>-LA!B58?pe?TxYeMWYH zFmP@+IU;*buJiYutwut*x59Ec6Igb{hEGQX?#G;B0+j$`EVp2|krMN=J|RVbNp&Ln z(d&l}>>e`wfLMblj*kDB{wU*@`{OJrJQcYc{v6HIuv zzehIj}=h58EgShp8A5KZX@+YJw`IIh31DH7kSR{>z z#21zNTxcM0M7}-6%vhdps!L6)t*jD$@*Ku|Ln|Z}v_Z^2PNYUXgj>xSZSoqQ($B!iV-eGcN_J`9dy?_-?F9~R9=3&7lx}0;&?S8CWjtj*bKVlR zvuiNdo4P6w--{OH`$Ud9U2eDA+uXxUu=~w7^z|d5lHLc~lws`M*VK2I*{Z|Sn!pKc z>Pm`RJ{tQ&O;IsE(y6uyDNb!Dl$$te?;YQ&b$kk`(BFs)zxP3d{4~$-900f%tFeVi zP1L4!!Pk-num!hOQ*r%ERcvwvt|h?3c!B_&^YW-CnDU&_C}IxIM!rQsK0^e8SKE)1dS8{Zb~G9R@#0h2ZP| zvtM&pfSunWpUUZ(^kWDDSjlM**(LQsW6w~dp&#kXP=aiC5wz> zMwwUwcr>%^TK&RPc_GR_twqD_iRThGAEu!;ntP5(iNuiB>ip(3)T`9mc)rc=Nf94$ z-Na_bJeBU++$qI-<;e;$AnJ?E;Zf7(kcl;q8ji!Q;~;ifQs+Xa_tu%)GsxcUjM*92 z=eV!^9uLW5O~dBAZJ;-4CFxt04{?Q4wIXeC$M+`v$*?->u)>PC+{Fg-x@8r_TT70# zp2$h>eUwB7kqZ|}3)8T3A8;OUmPyD-B{hte>8p`%I_`0EvBF>a{~=tSuwx;v$I>NY zy2K^>n2z)HSNRuG&*f((a|H5;#e(rS`3k%UsDmjt&wX*&mM_7S<4uoVxj9GucyO2xSGA)T;y17pN1{-ANY-SE-Ic4p0Gyz zw^8uptRl9%xs|zWH`_G3HZb`EY5U;41}M=X*GQ$&JG|a>tEm^CQYr}|x$~fW? z#8t++>Ob2$g)HNr`L1ZLezm%?7^jwRbdbcZPz`N;J*#ZzAtr%SG6ug0ui>K zkLPgVp?C#X(%aZUfiNZw@8gC~?T5*)EpCr0n)M2u&T~7Mo=ZR1<18|bujUN#J!p3% zP45Rfalx}645^3*dk}lO;np!Z^}>j}4yBTo7m1Gf8WQ(lAoGFqB# zFTI#3d>*+e=x&R8NXIbuLv-F6KviA67jE1D?bQ9(FTX$+EubPS)9s;$Eq>t!W5{a74@1OI0^jtmdq^OrS@xJun}0 zZ^rsJ2c_O-Nvw&Puo+^OxsPPjPpkqb`0-mfYqn`7>Yd7sjlCp|2@UiUB2kR z6NWZVq?UvD^)2RK$*IcA`J1ft)PAQP6|q`v%iHud@eWC(UjGuY?b`;bwvsOPEc3#&H}`sF15@)B1JNG|UhOnar5%U8*rf zW$~uYmm8WTkgobd*TeD)$wO){jT63~fwM+j;O*50k4hlqzTNacA}Koa5M<1VsJl1+ zG_h1fy?ewkF$RxbSCHaMkcn8X$tW3J;;~76?Kj~L^-dkaotxv?*;3^WF-}q8^)G7N z>~O0wR<6`zn ztHehB5oa@Ig$ql{pOGJiJXf48n>|+75f|3Wks70=RQDF6qbL68^QF%*vp;^hr*J0x zu)W@ULfk*x1(vM6jJUzqT!DPcC#Szb?hs6fJgB18u<6b%1|kl98;KNlnA4Pzla;N# zeT9YJUAXR|xA9S6dSg=gkP(S2uSr%Tpq?LG?Z1r9n1cJM!fR`iK@$~jC|{g3T<~=C zTV&)^Iw`4U;V8FSb}>(2#KKLcQ;DlWkGfDzN7w{GVrRzNgcLnd7Ez7T z@b%T^V2d{FLAP-U`d`&U_*j^+{yW<3J+*6`Al3B9`=d;+8)vlK-jr_$$`|r8{D6e5 zpla3_tu(Y989*;wP-N1nKKOres=SSy_(pryRBL*xj4F-}FfvH9l73Y#J?W-q}Z$*ufRuLT0JvU7Rj$m~iIeR^}h}N7OySIL01a zWB^x(D54l+Q7_byo3b$9tzA?kX{OF|;vWuw0HNYfPtx0l9cp`UYH<4eB1h&Z)XKU` zYO(1A)m+*Ws!ncPh<|w%<)hW};Jt(N;=gcOJX3VsZQWznKw5NJ zI0!|OK`8F*k>U{fLo@ukdq~#UziN5DZcB=PxLP*K2SPHkGCoB!n@7h@GX$#Hv^}jn zgu8T=U3B*P+!?c7a5MDDp5RmuBU@&}Xj>2Jo$1l38`V`6x7y6UyeCBgq(`9*r_feC z14Um)QIZdWmxqzCXI>5nSE9OcmNHY!k=k>Ux%qWHWx?PoE!?zN77q&Q<`D=gUo;h2 z-tee!>#J*lE8X)y69=~L)r6Ps0Vxcj_#}2u*tRX1&V+3zMDDaOVVmuE;XV0^yoW{| zCZf29JsC+(k1s!F{b2eVrI*sVZ;RaVIqngH_L5wUS2Eec&(&+ctqHP{zFUc)_s?}D z!frpvB7cI1t-%Dmz^(Nm(gy6h4zB|?dpad@(5D5ccga)pN;b(J6lgZ8@x`^V0cV3R zNvl$q>;SUVt^wOP=m}7{aq;FmShE|Lis5VaolPu7ZQk{6g8N~!U}ghNPe;i~b5J7Y zkWT)(+ZI&unycWAD_4doUw`2DS>-3J-=uDqDIF~1HB7;(L7st4@{VS$u@}#$CJPpk zn@pqotFi!63RkmZH_BQk!G!0b>+o6gwL z7}L&y;Dc^>IJk^E;2agbuGWZ(hQkjzT~uef%R#2*LJi(=$=D?{u;u=ct-6}mhveCcGp=U}HKvb_; zYi0VI1!g>PwlfTxLiBwjwcSyP{fY5zvJRgjn;~2qLY*nP)<3nz$??@9%-v2R*PRRw z52K3A%So?t&67rJ?C$XdbFeF7RYBBVD!aTIZ9DwXHJ4R>w@^^k^12{NsyUb_NXlJP zG6cXc(91QX0FK*h=D44SOO+;8+p7;)uT;08jQOw?*c1P+0JK(qD1`wI-r;|`4`7D> zX=H!F@IMjo&UYLr6JM&mZ)g9dxx{ITspfu2nP8~dzCmeDV_)w3tA0? z$nQ>YDxSWiJX-XTl*6V1yc;^WS|v4MEN&#^6ECE6gZ@|Q`0-%o{`zZ+S{Qy3V35EaN8R-J6h=q}|RlJDCH0_BfnCkM6 zHN(ppbWE*nc$M696BPUW?>axd0_OFnt~D)g%!)P%&!%od+)>%!?69@RBOPOfmKl5w zwSzRjV91Gxzw5{o-EeXxy)F~SsalrU3A3Fi^K7e1IpcWth3=Li|9iXX#`|lNEoy9Q9!AM{QPXLoIa4Prg9?Gd%AJ#b`!Q{{fVwkUmDykmJReQ6XLU#<+Rp!FzbCy-aLoK`ej5eIi5PRGFEtsE9|t;IG%(@A}4_B zqT^fXbB5Q8>(p>iQUdV&G^3BQAnIhOl?_F&bnhZg@eP1vnxEG#$FSK>SVD_jVcLwa zLK3_JS#f&k8*4GWF*dm2`KH$26MS)6B=vI6)M*j*VUL-l!9QJzqWwcTy6jFUIb}^? zuM?T7(9qn5PZjXf!m73938y(3eN3N*Ql9c(uLKa@FnW-*1KSeH zrTs({u>H=Mp^hx^0E?p-^g)f(&i5Hk#1r{iFZl&w}4+mpQje&Pi~(K(z^ zw(8jE$IiWdVu%H0^N|jYjVJ z{n?#C*?cvo9O%o~Lx9T>_}BDhw*1Q-t=}tiQUlASR7grNMtH#P#!&7XHI7i8P*)dh zdkkObSqd*lL`8q~RIT6tS*SlY(Ami|E&yBO6QK6VHEzR(a-xO}5MQ#q5AWT2=ch~R z;om1C>6ID{?nQ!scL)Y{c*Qe5K;N>r;v}uc2VWpy+V`|JM;vFUa%HO4f5G3ZcZZ1i z`^PVE4`b$r60N?Yy*ENyn6#yj!bUByu z^0(A*cXCNKeF9zk!9DpXI|YO(u@@u}wSOvzp_KpD4u5&OeizK@y<3BDG8R%DSEqk! z$2CxoYyQ64mL2QU;5(~z=)-IPL;tMEGej^qW^?*;_0#f0PxlCDLrF1#^IeH~$L;@b zuIn3L1E^~_i09qV?Prw|*GY8&?tYiF^Nr!cfw$GDQw44g9HDZNYX$ifP-lGBr<*L@ z0@^6mfU?PY{gK`6e4=Vi$U0gHeP|+RX)it`F7;2IzXSKmuQFu+F*CBn1hVGy%|RT zD@JGbS(epcje2}h++7DWY(CHM#Uz{hw$^*3{lhc^4#oQ5?WM#PTr%W-(K&G=FJ2T< zew*{vF+1;g$Wu$7fb=VD&-20M89<6Dq`_KQ_04ma z@BIdnn5B4y@FlyrL~n)G7k(GcQ(V}v{}*8_ik&6rX0{G4n6RPLSLEz^7#{jFmGZd( zH*sG4tNyziiJBq6JsT7O`%FDUH*eXI$N8Avm6ZN8L6$FqtRkUbbuQlS`PQ1vCRf@V zS_oaMGc$xNUkqJH5sccjv_3HLN&aZGp1S0|&F57>SjMw<+KY zMS!)w%lTV3NJ>6Mst=AdD%F5#A&LyI3nKSpz;^HHzt3U)Do~R@!FR$>6i%wTecTZHhtK51e zzWuI9`?0>Iy&cJmBT{bVRwv^st1%0Ul{wK^1e|UxHGWh;iw7vUTCu5yjS4b9c=%AR?vp#_5L+b(9KwqtY zUQUyi95)(R7Ft)h{{UdiDb#fcT7zOxX+Y$N;9-Q5ZAQXrV{VZ2wXC>OD2ZDgTf`jx z>~>)0F0f+V5R~h|=)IW``BYTMjtLy#i>%HEAmy}2Uy5l|yr-wtKjTSd*XPDk0e8#4 znJVkg0jmd`+(L_b@$4F|fvYO<%+_c3(>9%eo}apaOW@YZ-*JSNTQlh}Z?pzAIwMQh z%!-y1(?5xkR(!nOq>^F(*j7Du=;(MIn|h$PxlWLMeP98(jPU#2xvWx*d@&njS#Bk? z&z#~>JeYqxXEwOzkWsh~WI}-Zy>N3Dg%_iLzPii*TD{c4gXP8EiSVU2HPfCUoeaQD zgWmtb9T(xSD6y*>ElMwf4DXCro1cvE3``#KS>D$Bb&7?h=-=23eGJN7QGHrt?|64L z5d|`=f5h_QGshupS;H^5!7bNzH2?xRl6#6zn)6l4suXy1ba^EIxbtE?=Y5|2?}O^4 ziYUJ;q^}QtVHX4}0DzDxy!-YE&67ni->nndoy}WdL$YX2`jICVtN>P=8z&ky^-eZ- z#$Q^Ode*&L#wH0cC(oayj&7IzmIhlE1@X0GBrFD3TbCqmtN`*E2obl4`_uR<+9}`2 zFvI}}r>By=;jsF%`>GM)bPRpQp^~!T2xfUP0qou`?kXLicj?yG@7)dGeULJcX!3A8 z>2j_NPqS-A17`$JS^v4|KC+O{*uva57?1jZ;5}f%OQF=c{{7N$kEi*lNkLF-@X;6i zO@%Mx$CfreE8Oc!*RX%9#&LlsbM(1ajD;4Q1eexv$j!Kc4?{mG(BVrx4X+1mY`1d} z6_6RQ1pp!Hg^UOULzpx|rz!_7=>TE8C_8Zf4wBsMw#uG^&nSsljp^U-+z&{)&LWlZ zD|%pTss6#&yCNeOw&i>D#^oJ2B8KCV6yiO5n>8Zoy9UJLW8?Aa8sddZGc8eBA%+W3 zWDwCJ03hl{tX+Y-eg3O}zrP@nr4HXSt)w&{RkudKKGUA)kp`K;E+n=D>{j11Lt zM*K~cX7cFJ(D+yJo(bU#Q_*PsM|@!@qIk)x{; z&sfGKfqA8m=wk2yanKMZ4+S&FVZ z;|`ZcX*rm~tmuuC?LmG=e%%2$;_t>cyRX&ih%gn7Fo3+uSrz6WvVxRCR6%;@^61fEdp;&SUasoZ zEfMD+@$9bSVNZMw-`MyYxJ8r*MJq{1Cv=;XUTRnv7n+s<_?=%#>&M0&^g$e^KcmFN zmUBJLKkf$2t#C_Niq@jw$p@u&kQ?0R>R`kn=ck7eZs&N%?$7~%V!TJXh_{_HtZZQy z%IuKc0rK%3D$Ip^5$mxv4%@ZNNC+c3*<0Xr+znnMTM%OmHIC4qvjf_Vx=Sse{k8tL7Q zm&SsaQb%}0l=Pc-)=_!9_kqjSuqsgxZKg_LeOD*PG(S0Yz_NY=YHpkuJX@XPj$K|tJT*C zD{+Dtc5sR-eso+$iM@w$*LUV#%5@r!?%S;lJPl} zP1S+n2befwTa62QG7X z03*EN_BN2X1i=budgb?Z*6L#4ig=^Qg9H!=bd*@$5Cp=sh9X{vU74#Hcn1x z%j;IjsKt9r4k}pKxz4S6f?{{y|CaS@>k=Q^1%dj1pWMJO8-r^SF~f~{1EQVt3%zN_ zL8qiVrJOB0-5_;t^8$`4^vN4E(YYXQVb!X#gTxuiAscMWgRb7tw62>rY~Z$B2rA$0 zhI>)NjJ29PluOfc`-wjDyAcOFYZsu7^Tfq6Ri8rPM`?Jl|H0b6z*samm`}K2H$*qM zr3kiSP|47DPgl47zfc@cpTaXLfv%ZCa?qg{>Ilc}yEl*C5h3E){;kIE&AXLVkLjEZ zJ&>1aCI0IURQ8eBXo~+9!Ir(<3cagGw3<+ zyIZaEoAI_tR7UCPl4I4BUSLL_wWQY?gJ#O%R-fo{&A6rNsdB|tK~xEPwOjBlPMGo! z>u2i*ue?N11Q1$%nm6NYvB&R7x)}$6o*>M}D+3mc$cUGy3NOof32m(Qh4+XX+BbZdN5YN!A5X?LwP3=~J6nH^;?I0$%2-fnF`BG!Q5CR4^bMkrMx}^+Gz_uUJX_ zLUEyUW$NoI69{d4`3jAPyA|(g&KAA3)8%9-+T9zNAN^nENDq%`#4wuN#!4spJhW!j z=05ja>Xkg9k@|G=$8G1F4~^56zr@?utJmWqQI(9L==+V;$VQOdMur~iYmw@Px5dn7 znwVcTQQ6hDmO2`ENb@?VluO-o-m!Zozp#^m(0uUG`%~O6mov=}wo9k5>>7<4all~A`TPnw`yt9_5xFySwo+CZ$#!jNP+;0 z+GC=0ynuFT>}jHnuzSXt$`}l?ByvEi98omCksjdB1aIF|SF$QJvN_*F!^9 za6#kuB?xxPI%&|R9Hzx=P-R#3Wu$IVtHMtM2dl+(g@yF2)03|2+{$bHlxlW<9BPe0xGzV6Hsyx zz8M$W$}C4{N$9w|m#+bRKpfQ2^Ti#Ahkf}PYJ1Z7Ko8Z!gj8kkSaVEJ*zlMr3Hq1R zX<9+}UGMY@w(_2;JI0mM9hnj)q$+wtRSd*p8YAG+KWifhl4}GaX+9%5AWJ}O_%w7 zg>H2pG~M{ISV#2ayxnjgy6TXN>Ho%dsrRHaxju_XyRvn zO}$(sRcm9W)8LBOzmtO#>t|W_oDH?OAIFNDOIY`?(0pIhE3KS&JL%QfjUJ_Ji2l%0 z>zh*n$Ab8m<<=+fnxOw#uBa^PsJ`Qj%MSBq1Q{Vl5T_*Lg+8zCD7WvLOMN?iGpB#c zoe!iLJG@}?2I87w`lR{!W_ZAUU`XezY6YYX6Q6k#f@XVFg03#Q<>XZ&xIEzaPe@=S z8VLIqVoUs~#Cy~3^J;zHN+zVbJ5Qg!U@M|@@)j{lRUWr5*NadrX%Np=pC&2$u*;0F zohF7+yi17N8sRR3Tswoi#dI2*7;G;K2qfzUO>3wxIe8Oy=fFe9a}Jv6Q+=AKP$+3a zr0uK61UrYn+io_;W1fBbsylP0rQW(Xbud>Il*CDC*X52^7wbv4z(+uCU+|l{*sdEH z9OskxZ8TE(%lP zita#4CLc#C;?#>RyThBR70-8}UPNcY8_ya4F7u{!hes@k> zAQ*wLKVDuWT{at#D}`w0tcw)84N>_9PBi`JVOJ(`KnevY-S<62!0Jpv>i7~Cf61YieuiZzu}EJ*|InkUBYQ$S%Qh+4Oy#?PbH!Q7^Idz1KY*6qmC$K-`iJPnCH?MI3u$uB4M{J}w7^ZzS#(vqEY3U}|Q|^njWncYjeBpR0GlF)W!aE@X zz2$gpK|z!@oUYbaI3+P;S@$-i{2<~~*!yO0YRL}z^+3Ey$Xfoca^8qbSv)Gif(8`{ zjUgkcy zx-*fMz60AI+iQy}#>>+#({|)#PB$}uL;5HYU=EOs*qKY^KF4Wu0Wd2V%)rkeR>jCE z7)ca~?+bmsbidZ`X|ts!S97D^C0#1W<8w?!TA!N0oG;WG-`G%eeGATm#gbCw`KcVNc@9xu)l$LZg>>D*> zAGWhh5>Zg|_iWxNvuH=yE>loRy<9&kzVy~hTT0s#*yEF-!gBqxiNuGm?pZmpid^un zJ+VzEW7*m5WBq%7e3p9F6V8)7wwB&i`^IC<0+oK=Rjkg>Tg6uEqrfFjIAeRr&AKPu zviF9no9sfCZ*U*HIVQAdaAF+5>J=yAx z$zYZ1Y`xWMK&_|0VtMUrTvj0c+NYAu`jIq+Foc;3TaF+Yf64xl1H8&NT*o`kws%3$ zGCd{b&t5I-zBh3Vx;&xF@GcAPW9=ANa4*tOQ$HhIgd5h@)ouMAJC*U|%=3eu82MuJ zJ86U9#KF(ki9D=77G!KbqcS>QFizIjC!#sZ z^2+UJZpR6Lbs^>rqWOhJ-vJMiM2UF+*1p)a+8s3)g~_GUGiiGgMC3w%eHQ~xsTIl( z{Ii;Da^KYu?rMV$_1JbCXp{CCz8Dg>;$Ph2p(}1&k@RZc=E_Cf#aNJ_rNOk@6^dW` zgr-xlc4vJtZa>6X1EWW6eJ>C@jkOs!94uWu%+1?NW)EVH@P8Zt#QXn zEAO08Mi8IxZ?V?fZUy+;aawOPE(%4Rd$lMX&+%iyo=ZwoIqa=&KwOdv$&(K!ZCCq* z)9Tnnv^A_?aq=Nlbnj108e)~+BRJ2Sq?({&_Wlnb*Z(l3XXeF}fqNxXgNOK)q}~)1 z!BPW!fpS&Ui-kkUhag47&x>l4CQZLUXWx*`)Jm=I=NW2>J6W$94N9}tvc0cvxP)z9 z6IN4hE-ayRm9i-TwHRSugPK!&TLMYW-z=z464z4_p!YjY@vZQgRyz8}FV=NI)0qSK z1-pJ|I{2T+#luuJ>!|`&m!ri$pkHo&*Sycwq1tD-3!0XJ@t0n#4jATgR|?ruPK5l} z3zc16$IN{Pm&V{-{rfWm<8C0$TwtT>`R86m%n8;^vDCgt@IDu*=&vmRG8d2vtBzGe zkSnlukD`dM?Q%7%26w)=0C2Oyw=(3fGWW}1&4@+)ejBY%XVDJG^0O@TLeH%0IrWxr zZvpgiqY}@kb4KNZ6e-%TMiYVpkFMEL72EJ%59BY-AzmLskg+PR8!7GNpHDktGdSC3 zYH*GcXi{eIXk#j%u>8k{(Fc&{h-zA8mwm{{6u;L&ZkG6w7C=A-Y5mSIZ_PGeT6^cp zD-1qfu023yy9}eaQ`0RSn-rF|Zq6PoravPbhZ`qSRq%D7U3r)zc8@#~#0?boe|U%< ztC+MuX&?KfFe3cAFC>f+cS?OL0(_p!?4m&p0bGjv9NPD+gk4E2z-uwbEp4Sy(}FdP z&(RBMVJ+L}xOFigGB4_KLN%`<+5IGHkCs`pSC4T-n(c)5`_&Z zMO1)nX`E|o%eB4mfi|FwY;lm;w~is(VZlg|o0uPT>2jRH&qkC4Fl?~FtTuKKj`LWc zYFVx!R-fy&L>kNnO(`q&R#ksF_>dnm6l$iiJK)BL##RIm4Y$_C&ACs>-m$82HNC79 z(^7wL4zsE^UQK%8Ke#M^#U(-tJRUf%1QJrrCEvg)>TiBMNYZU++I)Dl@WD)5Pks2F z>6i*>>$ubTH45HiOAN7i@x8#D-%?uO+wWHUqh_?i^Xirh;V1P6m~kdeDJx4??U%$~G#SAj52?c6_% zNs%~_V~Woimfr<$q1wk~LpX}PgLuDtbKRGJ>izDLPf~V*=atKk-M$OnlthMO8s`-9 z?B)NRSTJ0bM~#A=Au7VrW2rC1MU}UN^5?z=pH)3pH+2b{Mpk>%A*vg1ps?nuQeB$G ze;%FpW9t zgyl?V=w0fR#u$Bd^n5c6MVZWn$NJ}J>wKtA!L(Tqbf$w#T;Zcj12RZ6pbA^s6BPD& zPJH4j?wq3_esj>}_r#syti(iHQDttl!$HVg11R;fbGgo>wiD{tU_f>Ad@JK#eYkop z(wa7%gFMHr^V~HI8ylqmXsdV=gWf?!R8NlAq7i}t)RmMLiS|isgQ98=WNyk4PNA>& zU~y649IZJXF+pO-`?I@cA3agKGN*Ee<9PM`+5dc&-{~k?h=Z)%af5mW+vILox>bZo zzG+NvpOL;^^x(*Mw=!$8(UeX5=d0RCJnZ8?KTNF98B#{+ zV>?wnk{z(OCAVxGXl^fr2tW^w_koZr-c`g%@B>;PDfy}ED%hd;vJ}BlYsB12Jg117 zJk*R{eUdET3<9ad65}ro1j%4h5lHtmFue*hXoF-&Ga{~T9rzp7;KaS76RK|z?kgkF9f2HfMc5l}y3YXZApWMcKkIJ)F!7E_eCpi~(yt>zt;x)Z z$J1<>-3RPi7$;+XHN=X~q}i7TmUgRF+++d0XI`8bew9!ffQG|Q)^R?!=StNiBVSRdYDO|u9gj8^3c@%_0^ z>x^y()bE$(KxfZFrmD4jxRFyb_Rsex79$ml%n=O^3DZ8?oq_BM%7bDPnKsWjPW_=~ zN}a0Iv3Xt(&+!u47b*Hw2N3=;5S}HcWY=hzS_{OguO2(*ZnZGX@}+ltia_T=)vRsY zT6U)Xvc<9~>m=fcUVp~(#l#P{G4{w)6fANq=E=wMA6}cLz(NbMeDY%lZ1PuS2DqwwSN4%r65SSfllG)wg>f<)#Ko zAtlp-4N$&MuGFp!I`3A6o3Y5+0n(6;pf?PsQW`bk!O~( zgkMO1w3n5?s&E67z51%y=2BaXjbJ_`4fQkd>YC0n=W`N1(tnGuAG>)Q@Re-0e545q z%UO_5kGAm;BDL{Wivzi3tlrOODxjPawFisN80|)^AmD&gG6=^_gttCZ7D%JQ= zyHRW2Yf(@jn~>LjmiJH56w32VxBQaIU_cu@cE zu|Bi19;*fcgkbv?sd828UjdwawN8oQaPT$5li`r~ud4YGjz_=h=#Jk3r;JH_m#zp? z!n{FfFS^DV^z^foC5|C$cI|hhoS)?<9Dgvk?`qi z)8p;?vU6$Z)Tssek|ktzkaIBcZjHD_QJKgb>knuQZ4cy(K4w!eLA))7Efti*ZD}Rt-bELHRqDc?OEE+k2@>@$0w=G4Ig4^onC#L7#5v)Y}e^$@}$Z z=kdoHc~+kZ?~sY(h&fK&r9aa{QD1+9k^iCLHA9Ixh16zmQ$JYtoV%$z=3$(4#(a*m z@?eFAXUIVD(;95F*wjUeXz7)r)DVTTtDYg$iwg)<$$xV1ITrp(l7Qu0zj7 z0tuVo*9}}8lWErf@+f<{diSK5{RLq#+sR&u_`G`oWVZ%TjjL+^_kmUtQ;{Zt;~-xc zZxAL}xM<9)ex@onA4VZT z=O?QHD%D|la-u`qF{i?KQXK}Js%svO(=4Zf`8rGO&Jv0J^e^$0)VgpUJti69a03B0h1?*Y}$0Mf=gr=!82r zC23C{r)8fOv4~CdE_$7gQ`31~^j4K!VKJ5uz2#)Z8Xqg96u$e6JfB2k*Pbcq{82Tu zu$v3C^?@L{k1C3;>c)BZt+IaReK|ShhjY6GpITKboa-dm>U(6v&qKo@T5TTdizR-T zFo#nXdv1BPu(ESAfJS6S1tm?4!EUA}zouxxb;8Ioj&RE7F@-Z6Zl#*)7P zRUSUwG@^&LR3eR=d0=_Y#GYcaC~xq1<%fF^74Xw-sr>yV_5Rm$<}+>Yhi2P`$85YB z2ai=K`{(Zg9PKABA#m@uQHcmpL>A`FgacJ*(>1BA@2gigu*(5{Vq2AFzkQt3*OGZ( zP0}+1&-RCUElh%p2?onH89Q}ZAyT$jmHPZ!6}g`#DLa=-j{$wA{DE82{;F2DASwm* z&DjRS6t{Hf(QfGWS8~#r8SyR9;TX?dV$!SXNEoB2_6I?&LDSU{hD+prwzlJr~+q_be629PyNC(L)W#@7rwJ3h--Bj?|F(*h&E6MBnuAc1k zl&q><T=wFP?<|g!_g2d)DQc2kq!wR~O|_LQRIV@V zwkm(PKGz)P-iorqb!zl{-$_L`+j|S(JK1nC(|f^A3fv z`cn-ZS{0;lB^{o9sfyaWdKjQ&lu!vt{2sS@;X^h$nD(_uQ?3+RqDBs!N`4jjsufD< zRmCYoN(w8d`qhm8RWNek9>`PzU0i;?*f$YJKps46E5RX|79(gm)R@5R&bRjk-V7&U zh@&{7r)!0-?!Pr(a05IS2uaTnM^yIx%I^pOq2dJk+|O)>uW@G_YIy?~8Ee?nZL1nh z3@RfA>hszAo1ZAW!&^k9v-ZWEEUBBy^LudeC16ZD6!c21?-OiOEK@kztv^168zL8?jR@(MZ; zKpR2G9IP`$BRS}O1!W{0&zycD=+5D^$YJ_+{Wjd46ITR2BjuTCJ8#!@0-4?tk)1}$ z37@ACtOm%L&YquVyCf7^Ct?-*pSQUF@GI8SeJY~6S*|i4bQN@Cj3glgKE0Sz)RK~5 zmtz6ho;hq2l5JNd#i;^F3Ha7bBtPX<;% zySd`xnjA)G;+C{SE)j(eJVtA6)lr)jog%k>23*F( zDVC~8R=GneJgzl9Zk>ydP$qeaJGa_gH_x;8H=%LeASCCX@9zBH!#6Bt+5e-Gf+C3#)! z<87Q_J*?|MmmpYo+T6kYpV6(hGJ%(H?jvHske)dpL2Cwkv}bB_;#Lm}SIq=TCCkH^3j~QU6bS-~P|!AOEeRDETBgCY@0z zY2>(4&Zm&``CR7M#+-%@(kG{ch-OKm*pTKhXKK!t)8;JAdCqfYcE8u>e%yb<{kZS% z^;@;BUGHn}!|U)o#KvpWP?BBSBoP?7I>66`y?y|DwSD^<`VryV`eYtR-q#{CZgzN? z3sAPYiq}G3AueNM1x8EO$yIJu`^-RJDZAN*CbV++VcJs@w_S`XYCt@pr7PKQeXhT5 zI$wJQWPFrB*>_5-U_XAZ{tlVSE5DMUrpx&q6xzN%t{iU zatXU_A?XJtveeRoh`+J}Z|RRDQgc9<5Y%{Sa_#(`xo@wcT~*3LlRkP&;3j26lml}U zox9M~^?i)jqcU(Vd$LtT7S5>Q!c}+#APUqldAgK|;$S-A`J9vHH??-}ExkXh6l()wB%+yT_EESJ{N6vcOt&JVEtuWp{Ym?=H zWYosde@sK*hwJp%(E>1MVtMS~@c_X(j$bF9a34)M{{Sn$YnkY-l%;6+xD;2ftJEK5 zjcI&D2>tdu2wSmrY7rUFn?gb>0s2p_da9+D7m+aW;>o-n6zuVr{a0KNGx;U7r+Rk^ z+hNQ9KH}p@Q{()~fhfk2z7}XFFic;4UHBX|e6FV0tm=~=pLS9v#({ZQ^<(V5Qr$0>T`EIL9 zAHu4(C`W(nwujW$2T{x%-QT(EStU3>G){S_Di3@8ZP?#0V}4jVmEfN@zpdhrqpB+g zbk8T^N49x(0qMl!bGoM4qxSO-a^)JXUUjOG-5gIbGN_l zNH(u&X~h@k{>OKlukHQ0Q*gKNpJx5fMuo!BUUsm!;OxoX_g zlKWEIm~C)Dp+k7w3aM#u@_G^c`%Ig7fU&zpY}^x53zIA&tv-L>y>x%)%OZEdF4ujU zLXXKzN1#0E1M;Mo<(A`R21bxN60KlVFvi5HcS&rD^c~qrTXO83TU{pFX27$r%20Am zkB7HFJkoyUM4uHkGgb^G=A&;2G?>*{5h97P@SQ$cu8JlDn;iGHA}{^(79xut4o@^< z&C@kG{RgZ5)fH>G8QB&SplkG~w^3Ho`jur+Lz%7LSqy1FkpzV*2q=c_2EzAtBR^yF zO%jak=Z;HkPY(H*d>YV35f9V==m!z?slvafDhw{yADz-X?5Q66!Hv1cz!JJ0`mV5FX$`c^U8X+{ zdkg=5@OGYWY|Gk>npUw@!5rcuZ@ES?>}LMAwP#tNRd1x~JgiFogG7rCfkPYenQz&z zV6396tTv~BsQh+KrC!hC6ewd}r%Sr1wA}ppx$+76l1)p%M2EaatCeQ)9BJh+^36P- z1SYa5v_dy_e}{pfvq@Hj&Hh4HofG+3fC+9}UGplqlZ8{F$H3DA9$1$kGm6}%WdZa*Jlqkw zuQq0i*jb;WWrDcYyNrh|wnAe99(~%pydQNF2tkB1Jqxn3^6OJAQd>0kg`xCvi`Vhj z0X5n=_pAP18AAtO4yO{-@ zM1W}jMbxFaW-!tgt*ZI9;>$n9@o`cC@#yc~cVf>!j;+m(6>|2Ol$gVpACA3})?TdI zBbnth**{`L@=AXlnm`?zO!)KYRqo6U{U?ybwOH9Q(mcNKO$|x4`Dlk!cguWN>1hE-o=@K?iPIi+PoLZlphD*{87wN_x zRpXId_v#tkxNI|ZNcF!_#1>h1$NYYzq$s_wvGQop;>G@WaJ<7Nlb@QC%Thm)ElN$# zXse+h3ulJ4VXD0XMW$C@V;a#)^U<;N0I!Qj9-0gZRTGB^kdW6mXdn#(19{mlw=XDc z-QX5-MZj1~{|;mrI@N*BQ{49I5vW>$pECLKh3Gfv_QN^Fqta)ZW`%O)wRt)d^Q096 z5pDf6N*a9eGE6MThZuPsR?q>0yQ;mGX-uNpCOf};eZnbOi6dLyHDRUJJ9S86c$dua zt9Jpoyoc~CBu$NSGwVkQ9At;Kwhx-ovzFbt^KdzRUo+}L55R~l6mrtq^V5Wo*9~WN zrmbR+SIK3Xrx$rQ-04@Qdd zFN263D!aw}vK{l-Ql7aHkx-YRp7XdxiDMK_)Mb&Q5bwvnBA=T4tf|}JEG#ZS^=HRe zc;~g!dUIh9D4CWSQ!&|ko89D3((i@NeT4B=t<*7ue{ObCqVeWTTiL`If>810R>E3H zKq;7Ef}(wu%R1d+FrRA0l|@STFns|ntA1d60U!i!W0k5(Lg;Uu%&BxR92e`je?#()T13Q0kcA= z7)XyY0jlsBrB1GeDul1Hl%JjJ-)hVz-smtjGuH^mqrCKL-#B}m(vZ8X{G}7GqMM2l z=6-QEeGJyUga_OahoG9mm-m%3NjGUO-eU2t4{C=eAw^Bgsq@cA0=N5`aFO{xi!%%` z648CIQVsTdG*Oj@UMJJmxaUR7tGfq~i*1PaQl|Uc{a~BZ3r~{IvuL5b)RP2HbI-4) z%91OWeM+CJN|f{-3wbp(AwR?Ai*>>;pHQ-o%eKne2^iRVU*zJop}&VxyhOca6h%p`RU1u-RPd`I^RY_wW6pTcttm zd61{xLK4TJYdw4Hl#Z31J~Fg4Om)WrKK~KLHQcn^Be&GNya&!V^N(8LD=z5k@VAt2 zHjRS;rzS!q|Ev|07u(DCISfHQdA+poeqk}bcyf6>QTS&MzMVM|%yoqsPRfX4hHvcx zT&G}Y+^~f6Fc}A}Rv>!y5t;TolYrp1)$cJs=-kn2cpCO$YjdoV`s?wKs|Y4uYCTOM z-fvjtfav1h2Erj2glQi&WWv&9#PP&(u0gI$Lvu$%y<)q28G|K0Hmw8F71_eD(q?$b zq+UhEpJ;VHZT^XVcf;=sLNDo4Bp=>~e8mw+vUQ$c`G9AooWIt)+6$wwPW5faAg-^=gzGR(r&!p7^tDbmQ|iW~_6lJ@?w$>JFGXv}S3x+V zAnxS>nD^Pi}$6=el${&&a-Vtvi`6*W1Zh1W0b3Y&`oxbi~t?w5!OTrC2PVvY1559XG zb4Dqk(&}t^A}BVOE8&SjN&Af@<^m)rT|&buK|>I=h!Wann^mB$(HH%tTZR@%e}*fk z?w6FdFPl0Kr+G5JN=ZVSKY3wg;-q%6TAwZ!TEV>h*OUgl%@tKW05-d)D~zJg?-Wak zyoCxbf#(wm5PBtFTOO@51VTRCR;_iUSR4|EmP6;Sj&7ff0w<=!K}x#G`@w>EyQoXU zcdx&Oc(2EVi4xvYr!=qbg)C@%D3_bG5qJ+JFR~T`;Q21r^!8kDPcM>m30|;4OGjV> zoTj_|`x51cNYL4H)~G?j^vs948->M*0U;9t1pjPHS6KM!&t*(#{bFFD2cTWy$OBips|9syU;oZI zswA=6>^yyAu9@z?{0HF^Y9f$l^nKpxqDX7P^&63i&sFdkifdfnjPV)s*n}4n7bgaO znVwT1!LZBeB&hwh-#*2kJMukZGOfqgOSrUQ`Nh0~3iy|lQK6br&r-g7vHMXavNRk* z!j}J9bzd05ChQuGaMe*h&z}*m&cvlVq3md|s>yMSbbMl|Tgu#dLa0Yf{NO@pe&@fb zQ>&ZHRP^C)yy6RbwX0a%uB(QqX!iuYJw+_Q9w5ROf|w(*|1_sw9VrU)-3_d*)2cI_ zU(rUAQXyXtHbJ1pe*5}SmSYuj~`!ULj;Q?#XD!QQKypJ)@tscUlA6S$PFqv4LdgsoQ6RZtv|sp~o^hOsB^Z=LDiYcDFHL*4tiIox5fw#SB5 zqD|=A_T%rha?Ej51v`4M@AR+Cch#C<&99NL&RB#!`IcyF$n%mZ zYT^6znWQg%v1ujg5(UMUt)ODIekZU*>74y~GpO)SRtHR35PZi%dkYKm*6e3;Bvqkb z%h31TRU$W~L~g@;;udx-nbX9e^)Pv*>-B>)_1I&aV2uE}EE&4P-u~5Mr==j4F|;i! zCNfp{5dmh4kk9O-)Z7FpIR#*g%a?;ZgXp~zP0?15#ccR#na6Z99Y+j6Gjw{uJLoRv zspp~P4`KgfI1YIz7g&!2e4m3)3O6q+?6WDIG@ z^7HWanNKiLxZEvrYbJ2WfS(rGe#85kd~DCQNH*_ecR~W=4c%90;=TS8#hveNeOVRm zMTD|Feue54tJmpw!dApYMc>N?={G4FZ#BX0&J46ONwC|h80XmwqWz&#O*M;OpOq40 z+3*EML2bqFqqC0LXL-arWlZ2Npba5PjXbB{!Zz|jyL6@0tuHZ7Z&*cneBNnSJd5as z1=)R5ftT4A+L(%S7%8wdWXknj-vpf)`>zNjI*A(M$!<+v2%2;`kbhZ(wO0zM(p#>r zNaT*|yPMxwIUf%UV{=OMjq3cor!IE!9(*-BB~=RS^xy}@(N_+Qhq@@4OiS;$z{iKH zqpIbyE*2LU-)xr~Ck-6UClL%Rs^x4sBwWbGoIswTx`}SFuC0L7DKlMH=N|~LxsJi% znAQ_z&6@2IzINp$J%O>;o>;aw-VBXx*&uDDrf12K8dcgeR2C~UCWKBYTx(tXvJSa2 zH6fYlfD^Pu#3eI$Ch%^XLZ)FkO<_$%L%p=2d|2E!-Oyd%?ilgMIY(*$k8WfudU05= z0@n;O=k(;|1(_Ooz~FyzNxQJ@#M_vo0U)TVIialDeN%KUY-sfs%m=XE7@a?t?U%mD z5<(mz|5D9mtQ&k@OeV<71s!-_(M))wL|(DDW|v>{D^wW5uPrJD82>lF8|6i$%ST3A zP@e8JEx)FR%g=nhU2NJ>ii>{L@EH50-|vQw-8G#PUrVf_pK{X%*X;n_DUo_(p9XQz zU*(Ut5`AbhAwj!5q$;{T=+!6uQDS!sL<>p!BQ?8Sb7e)yo{n4$EqE`BKkx$iw9oC! zWVyzKzdTcVQG#&4zCpc}mH^|;%4ZH0wRfPgurK3Gu_M712OiFoYlIn#tnA?w;hmqJ zQJB|-LLtcKxSs;gqc}{7&tJqdnBnDHgP^H5@mzJZsyiRGRbB){K{hNZ*v--syb(Bq zC{o5}?6sI-c89C*G&F- zq(u4vhwXR?O-S@8?Meg0=mX>%`qPLmFy<4q;O7q)LgN}Fk6Q;$qV#M(LJumW& zHrk?rBIwho4)+II1VqHLx%{$~sUM(JZ+3)-hqQ5>GAtlJc;Xj-iCn$)xZ%zCt(~?M zVtCkv7~N^v>5JAM@IF;T%79?L`|$Tx$(rW|pd+y5>Hofatta?Giu_f;sxH4Pg*26x zU2P^AEUk9w4#!F4o?5>*C@n2;38{dzthM)-$X|Spk2d~f)zg=pPwoEMRHpnw`jx9K zmYO)KTk)@p{9qW zS=VBip=uaF?A*kEyds?6pV*P1-T0%R@?nzjYQ$fKiA+OFJw@0lNh@M~k4!Ux3y7rxhk3=Mm`aw_g^ahp$FWs-T{&|Xem!Cmt$)4PYq)68E18FpPdWRw2uzU zzFs_?5ULs1v&c#NdQyyFDV$p|AB+CSn^#cSGVl7(6ypu7E%|D6O=o0tfYr2#9kl{v zd+4rCWBO(6!Z=|lk)@)uWpYf5|bQ}8i_s@3~ z4*_wGAH}4A;sAYf!Vz~q8O|b7WW#y13ZC+0+Ic$hm##Z$wgp~%azEKdlON2@r%!eb zI;WPsZ4u)su%1fVKB>E5P`wDTIY&OK+M9zHvo!3m;l9k-I6@2IY9c{PZvS(9fpV#! z`0k+Laf0??+e?F?HwT{|)bnAOSAX})G0aEN2PWyO0a~T&Rg^T#aawn_+~x*Ha5bLM z=jq73N~#c=eu5)FTZBI#hzJR4?+rJpJKFQ=g|hYYfai>k_l=frEbE=sS<&!D>YQjg zKQ^e_c_nh!wLrE6%^ROmp88lYF@WxdmM_|}v$ad-Ngdyz5aOgC59je7H@V-=b=03(WwvGi0fZ#zXQP-&pv!vn1-TrM99qrD8 zJ@AfD3|Y5vFUSsRFMoU~OL7Cd8YXce3`q};`~Ld%xHEg{d}yG@IVyUcUZ(AlwTyPfu4hm%Gt zy7L(mRqrUCLI_dpW)YXNP85*+F&dw}(Pr@=$B@&H!)-;Q!Yp%*rLu6s8^s;slpIaU z#~IhQL>0O#X4h7~LQ0C2Z4%|Zr66+>9&y?B>y%h%r_9tRYcAZ{-YmWs_wW&M>&9$Wu&u>@EEV|9vjSR5_ysZ`F5K znAMkAwXgU{+faUe|2N#;i)@ZR`tOs2@Ngv>Jqe#GSC5H7=~}G*YRpSvx4lDE>?V3Q z5=qQK<{~9h<(J7{m6OwklqVV^`Qz=2=WVKjy?l|V!HTQ)q03#wMyHZ;TA`SSa$~Ci z|{CG=I2CVK%RY!{OWwSM~$8>!o@og#j z&1l&qGj4=p*GD4AT~#X!^Y*mENBcL5Fb@ML2qP@;%&*F>5NZky}ZjXPqcUz)`pVW)W(L^cxO23VSW$dlfnL&S-hQ|bJk)cAIjV< zpN!=1;3h_v(c0uFm*7 zT7W18rK7kAkG1-t?~y<=UbiGDnR{WRRtC@vFs6UmYZ`C!AHINHS$-DMKETgh$if&p z?lm{)j~>(b1#~8+{HnHQ9b!`^D+;_+J4$1$?FBD=7(C)g-kWw}X(a*HSK80ur|4K} z2;*eqc4zisJ=gh0=-zVh0g;w#+m;odvCq#(r`6dg`tp_I^hN~mRWSKM^zWQ`;k?Ra zgC%@!r%n)jw@r3_^b?Tg3AkT)c%XId;?FdohId?XX;R8*BKXZOXXf4k&Kuv~Qhn)x zl`5OEpnnA!{8MZVA~5yE%1PHg97;r1@$XHIzE!+%cEPl0s zp0?D}mQZ*5$f%fM}4d(IyFhQ77$n*e1A>O1M4v1Uc)y zlW2|vDLASUHkFued)@k&MwUcc@jci~n~ph|8z>8WKa!zF>6YE5`&@Ocra+pjxi?p<{E`Ir+?8+k=FN*avO=b=KQ1^VbUug`bJ@u zNB|l+_uDm2FpB&D7l4ToX0rhLHHB=Df%%iwlEI$?)jl6QD~K=AD&JWI_z~{<&n3<` zzb&?^;anZpjqVr|Uhxh0p9~6xWKeFrVcB|lSngz9i`)B=({YoHRgIFohgHv${pcwO zPDtuFpNhs==l*Xc?aEpG8qI`*4P%C9O!lOokO7?S?TyQsWH=#r?FG0^p9;BJW&5yt z))1S8I_OURGxNKt4=2R)KoZuuh!`37Llgzh2YBEXi?4hW?QVW>_h~-j8W_uMuK$vg zy5jg<#n3txSRUh?4MNC#4n!z{$cBuib- z;C`_)%<|k>@CZsh^7dyK8=#xdR(pkq)j3pq^PhB=r2MKSk)Zn1cZsFnl#LMXeO)Ma zf##vr=geRkFMQpqt8$x<6^`FyREYYJ52>={ivw(j37oedXb+~8WEdtad(5o&m-;B& zx%(6YM1r`;n?SwqLBUfDjZz>FFl8k9F}A<58|}P+e@&MUWX-N zrlO$1q-t9e=6wAMP=#p!XFPmYl-cNJ>T=MwK7+Y8$_z1S{mO0w2EN)9zaSrPyT|?v zT583duKU|yXDj>)@Tm7^W}60NAr(p=^Yh@pYxmsV@syng-KK0}o-5|G!ef`h7hNor zx@*(kE&s3S(!f##-0(rsiIsdtoqIZ7Ub6;i%WR7#FYa^51}M-ne!E+v@1gcSjJJ;I z!)F>(29{4+V%+*+$70_&Ty!4g**gu8UO>ri`LixiC_0T0p2#{Hge$Nq9|HHiEetwF z1No?lasa0VK1~;w_C=UpUUD2G%&rK&b-0kJq5AgJUcOT)$!um%A#myU*@G>rd7b+} zS9*Su7%TkDI|E07bPayx^`nqq*NvjT4)D7EwUuwc&Vf&?N8EM0$==qy;K z1&EDSGY%D=>lc;NOhpN(?TdGMt`-da`tN!1`$cK%Y`?&_6keQ3ok`>wXzRu%I{aaY zyY9~lB0LZ51L9uKS??d=29FsOK}z=m@AB&-V@o4{ECK_%k7fZmrUu=tvBJ2NAo z2%q$XGqz)2%zRRs1t?Okb=J4W$j)n(9#n4M*^tw(M~lVXq_KAKk$Dv{Hpayz$C-uE zn$f?qz&M{o@5C5^bmW6UAa{7!`A`0$>^uJuaEsJ|jfI;q=x$yoM)w0fqy4@+gCY8e z%^?Rs0C-xqZ=twUc+@@(vQ+xoD-S}c^ZoSAvvd75;o*M;TWhlps^-U3N2zv}42m6u za-UoW_XLhOhQAwh`Z<7T03O9zN17BU1qi##b6|ym9ScyhlM{ad(Myx|_Vm&;)z;Kf z#OLob>;E2vf-a8-D*~IlO#bh~sUyx@`KdffE;p?(m&#Z&OwX3S>ZceGe^hPHV(V0! z!xsX?!OPf@;8laV(Hr1hz8iBn@unOofc*mVl z>0&(#+jW&E+*M2UNBIxa`d4ig5kT8+sOL=)(!9V( zyk#w@3Sa84%xC|}0)qw^ktJ|Tqxc-F$5dlcNsPpQ+BW4__m9{DB_A};?hwHrc1bhM zRk9y!M@rm5M2RsHkS%22nZ}kb$md<&nFbYp;hm{_tR?@~5FjD|2z6S`cr;^LlcbY* zRxZ|BWEZ*2Y?cSnC(rI$25gYd6VR-$U<5D~*&zj>JNBDo zdNi;kk%0g${km$M(g&2CSs<(S4CTK%ed63-_2}E02UX{^I3jplF7}my1N&@W)93_g zo>+V7(@U0b$ol-C{1N0zIhxPPrN#y0`2t$X9{!eLsw%JDFJ`@AI{NfF>lq?c!Me@6 z^y^67LDnDHQ2)pCvb<5~@87Tm>;L{cW&VG#Z(Q^M^V}4qU54qv%Wt=dR0XU7KOOpaA2Ee&IpvQ^WmmP#R@%ML`ij7=*IjO z#Cw!~Yd4qflw{PnJz1R=T6KUeLWt#cTG3a)+=lv8uV#;iEfqB_WbnQ*5jT*jbbAu` zhL;2c9OcEU<2X?Q^V6MK+odkaIB$kb%6J1x(3M8pxV3}q#ix;51J8wAh3$hTh#wN&-gRK0&|6V2Nc|-{ z?q{~PfI4%alCNTKKjb25c&X?O6G|hsP*P~a`Hi)bFA64d44H|Cfpr^DPi2d+ovT_z z8Pa8;-KeI-KTM!Rb7q&xds{b0pEGx+uq<%7z9~EeXzoVskHQ{5Er3FXaZ(#b;l;N@ zQTGG&_igliF?qIsTO2f2as3s@-G^VtB7V0dx5kH7BfiEAtdGs3*85Ki?@a!B#>5R0E29CRrfGOl0Zg-|)zF{zAtUfY|lia`2FWQd-YWdL3oiT@VB>TN^2^(%` zN`sEpd>t_g(>rG>u#fZ15mGH7{mTaAQX74MBh(apHk#bXv~$N%Y~^WcU^l*l55>7yhtR$4h)`EL9*yt$Bubu5@snw>|2Ti_VGQE|Mfunq)$_q9HL@y@k&@Krm{&`w56A>n1$iSf-{Q z>g0XN4{?%G|KoJ)lfyUT-_o|K*2y1SHeo4_x3nZK48MrS8ikt^EM7GB#&RBCsp`%= zNnLNa)$o&fkT>zuh3TLC@&{56f_#w@=9MM%;_f#iTbq6;M`;Ts0A-N!`it^ZL!$#% z1IbUV?SiIULziFXR6rNrTyd9YSJETt68M3lSU*sqT^T@6K?%> zcW#B=Dg&uJtUHq?ff3%@211@Hn-zYUy*W@uM_7|Z(Ib7OIH_~mn^;TNUYwJrK$~w0K09?0jx*nTpzHYc~UlMLqH@bwUlCiHBfVDn4g04qJEE!jgm zcs!E$CE3^gR#Iw7t7PNg(gx;Y<2G0&XB5ee{yQ!zY2R%7`MU!SN$ISc65k|4zOJOM zB+R$bGYryMJlp6Jy&|KU#~P(OD=X7D=>QVaauXDb2mLFBUnsX>s(;jZ4BOsc&f*JW~fiUYb>E7 zPAlr}%%lX#>*;Ilb3X$}uGpfmOFtx_SI-#=LQ4G^$z$_U&V^GRhIv z3csP;ahO{5&EXWwL&+iyv;Czp9r(?bUJlb=$Te&v-bz&T)4&$X=r^iZl%c1_Cu{c+=}~eIC-J6j;nfnGQN0EV53~y zI9CO=J$7FVwyt+_^3(mQTu9{hSn4!kfOUAW&Y)EDhn?fwc>=?mq z`v6+k5#A%RE|KpOaKtA4YR@|tV@LGXr3fv@8pKwTpSQKrxwnsQxro>GfUNWkFqsjK z-bERa_XK+d%{#aj z+_Kd@?_hqvm}RuMUGU7T4Q;5+BT&yi7V=?bH)X2)^wc0@=+SVw>~)K=dcd&FA ziOs2Hv;sWfpYxxVFfzV?5gfL38m%NhSXMjMkr*2PC=N5$=?y4o9(E}I67M&DJhq6Y?g;|Lcj(PiJk&wSpz(0j&+gocR41@XBK6KDUel7tFctyHOg88@zjT z)Qg&cG$SjC*M>q*AT#bAR~+w26mmn&)5qG~?n6A%Pkv)%(d?PTT<3G6KE`F&TdGlu zG4HOzIJ|NI-D4@q%fMVe*8dAIb5*H+g^k}xHXq>_<3X^*y8%_a$zk;K;1`6lSMu$A zVj$j;iaVqrFKzc;dEI-6DrFZk|Lga1w(}JFpGTn%$=7pFleV_8!-~|nwYk>Q&IFL9 zPd`oY2EHoo%IdyveQ4VkTv2JJH%`kP+m*}Q_@(_aDGn1F2J9(c zd#66)iVx5gD4y$#;;3LrQ76uZtwo2F}48X*UFV)K=@pL z;Y^FF#2v$u(tv+J68du>K75zD27+5#>KcXleXE{dZt(>a_ z!yUvewbB7pBFm||X7f+uA=RZ4gwr%pfV5{Rl_Jg9VTIMKtg)~a7{~7-=o@<@(?;rx zsDeWF5Aj4>3b3W!fUvR69oJx$XTh~PZX?5>SQzJXF|jlQ_V6!McJCz&%;l zFZg~CH-IbR|7yRr-O>5}eSd9}KQIdhPfT;|W1D>bH+yHRea|Xef-ChTIBHlIyq;BE z2AiV+2t-&{8^`}1qr%vKqrNWozu&|E+$7?E-pT*WlmB None: - res = await client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gemini-2.5-flash"}, - ) - - print(f"Regular Task ID: {res.id}") - - -# Structured Output -async def create_structured_task() -> None: - class HackerNewsPost(BaseModel): - title: str - url: str - - class SearchResult(BaseModel): - posts: List[HackerNewsPost] - - res = await client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gpt-4.1"}, - structured_output_json=SearchResult, - ) - - print(f"Structured Task ID: {res.id}") - - -# Main - - -async def main() -> None: - await asyncio.gather( - # - create_regular_task(), - create_structured_task(), - ) - - -asyncio.run(main()) diff --git a/examples/async_retrieve.py b/examples/async_retrieve.py deleted file mode 100755 index 1868e7e..0000000 --- a/examples/async_retrieve.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env -S rye run python - -import asyncio -from typing import List - -from pydantic import BaseModel - -from browser_use_sdk import AsyncBrowserUse - -# gets API Key from environment variable BROWSER_USE_API_KEY -client = AsyncBrowserUse() - - -# Regular Task -async def retrieve_regular_task() -> None: - """ - Retrieves a regular task and waits for it to finish. - """ - - print("Retrieving regular task...") - - regular_task = await client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gemini-2.5-flash"}, - ) - - print(f"Regular Task ID: {regular_task.id}") - - while True: - regular_status = await client.tasks.retrieve(regular_task.id) - print(f"Regular Task Status: {regular_status.status}") - if regular_status.status == "finished": - print(f"Regular Task Output: {regular_status.done_output}") - break - - await asyncio.sleep(1) - - print("Done") - - -async def retrieve_structured_task() -> None: - """ - Retrieves a structured task and waits for it to finish. - """ - - print("Retrieving structured task...") - - # Structured Output - class HackerNewsPost(BaseModel): - title: str - url: str - - class SearchResult(BaseModel): - posts: List[HackerNewsPost] - - structured_task = await client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gpt-4.1"}, - structured_output_json=SearchResult, - ) - - print(f"Structured Task ID: {structured_task.id}") - - while True: - structured_status = await client.tasks.retrieve(task_id=structured_task.id, structured_output_json=SearchResult) - print(f"Structured Task Status: {structured_status.status}") - - if structured_status.status == "finished": - if structured_status.parsed_output is None: - print("Structured Task No output") - else: - for post in structured_status.parsed_output.posts: - print(f" - {post.title} - {post.url}") - - break - - await asyncio.sleep(1) - - print("Done") - - -# Main - - -async def main() -> None: - await asyncio.gather( - # - retrieve_regular_task(), - retrieve_structured_task(), - ) - - -asyncio.run(main()) diff --git a/examples/async_run.py b/examples/async_run.py deleted file mode 100755 index ecb1d11..0000000 --- a/examples/async_run.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env -S rye run python - -import asyncio -from typing import List - -from pydantic import BaseModel - -from browser_use_sdk import AsyncBrowserUse - -# gets API Key from environment variable BROWSER_USE_API_KEY -client = AsyncBrowserUse() - - -# Regular Task -async def run_regular_task() -> None: - regular_result = await client.tasks.run( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gemini-2.5-flash"}, - ) - - print(f"Regular Task ID: {regular_result.id}") - - print(f"Regular Task Output: {regular_result.done_output}") - - print("Done") - - -# Structured Output -async def run_structured_task() -> None: - class HackerNewsPost(BaseModel): - title: str - url: str - - class SearchResult(BaseModel): - posts: List[HackerNewsPost] - - structured_result = await client.tasks.run( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gpt-4.1"}, - structured_output_json=SearchResult, - ) - - print(f"Structured Task ID: {structured_result.id}") - - if structured_result.parsed_output is not None: - print("Structured Task Output:") - for post in structured_result.parsed_output.posts: - print(f" - {post.title} - {post.url}") - - print("Structured Task Done") - - -async def main() -> None: - await asyncio.gather( - # - run_regular_task(), - run_structured_task(), - ) - - -asyncio.run(main()) diff --git a/examples/async_stream.py b/examples/async_stream.py deleted file mode 100755 index 772b781..0000000 --- a/examples/async_stream.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env -S rye run python - -import asyncio -from typing import List - -from pydantic import BaseModel - -from browser_use_sdk import AsyncBrowserUse -from browser_use_sdk.types.task_create_params import AgentSettings - -# gets API Key from environment variable BROWSER_USE_API_KEY -client = AsyncBrowserUse() - - -# Regular Task -async def stream_regular_task() -> None: - regular_task = await client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings=AgentSettings(llm="gemini-2.5-flash"), - ) - - print(f"Regular Task ID: {regular_task.id}") - - async for res in client.tasks.stream(regular_task.id): - print(f"Regular Task Status: {res.status}") - - if len(res.steps) > 0: - last_step = res.steps[-1] - print(f"Regular Task Step: {last_step.url} ({last_step.next_goal})") - for action in last_step.actions: - print(f" - Regular Task Action: {action}") - - print("Regular Task Done") - - -# Structured Output -async def stream_structured_task() -> None: - class HackerNewsPost(BaseModel): - title: str - url: str - - class SearchResult(BaseModel): - posts: List[HackerNewsPost] - - structured_task = await client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gpt-4.1"}, - structured_output_json=SearchResult, - ) - - print(f"Structured Task ID: {structured_task.id}") - - async for res in client.tasks.stream(structured_task.id, structured_output_json=SearchResult): - print(f"Structured Task Status: {res.status}") - - if res.status == "finished": - if res.parsed_output is None: - print("Structured Task No output") - else: - for post in res.parsed_output.posts: - print(f" - Structured Task Post: {post.title} - {post.url}") - break - - print("Structured Task Done") - - -# Main - - -async def main() -> None: - await asyncio.gather( - # - stream_regular_task(), - stream_structured_task(), - ) - - -asyncio.run(main()) diff --git a/examples/create.py b/examples/create.py deleted file mode 100755 index b0af2db..0000000 --- a/examples/create.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env -S rye run python - -from typing import List - -from pydantic import BaseModel - -from browser_use_sdk import BrowserUse - -# gets API Key from environment variable BROWSER_USE_API_KEY -client = BrowserUse() - - -# Regular Task -def create_regular_task() -> None: - res = client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gemini-2.5-flash"}, - ) - - print(res.id) - - -create_regular_task() - - -# Structured Output -def create_structured_task() -> None: - class HackerNewsPost(BaseModel): - title: str - url: str - - class SearchResult(BaseModel): - posts: List[HackerNewsPost] - - res = client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gpt-4.1"}, - structured_output_json=SearchResult, - ) - - print(res.id) - - -create_structured_task() diff --git a/examples/retrieve.py b/examples/retrieve.py deleted file mode 100755 index e2c4e47..0000000 --- a/examples/retrieve.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env -S rye run python - -import time -from typing import List - -from pydantic import BaseModel - -from browser_use_sdk import BrowserUse - -# gets API Key from environment variable BROWSER_USE_API_KEY -client = BrowserUse() - - -# Regular Task -def retrieve_regular_task() -> None: - """ - Retrieves a regular task and waits for it to finish. - """ - - print("Retrieving regular task...") - - regular_task = client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gemini-2.5-flash"}, - ) - - print(f"Task ID: {regular_task.id}") - - while True: - regular_status = client.tasks.retrieve(regular_task.id) - print(regular_status.status) - if regular_status.status == "finished": - print(regular_status.done_output) - break - - time.sleep(1) - - print("Done") - - -retrieve_regular_task() - - -def retrieve_structured_task() -> None: - """ - Retrieves a structured task and waits for it to finish. - """ - - print("Retrieving structured task...") - - # Structured Output - class HackerNewsPost(BaseModel): - title: str - url: str - - class SearchResult(BaseModel): - posts: List[HackerNewsPost] - - structured_task = client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gpt-4.1"}, - structured_output_json=SearchResult, - ) - - print(f"Task ID: {structured_task.id}") - - while True: - structured_status = client.tasks.retrieve(task_id=structured_task.id, structured_output_json=SearchResult) - print(structured_status.status) - - if structured_status.status == "finished": - if structured_status.parsed_output is None: - print("No output") - else: - for post in structured_status.parsed_output.posts: - print(f" - {post.title} - {post.url}") - - break - - time.sleep(1) - - print("Done") - - -retrieve_structured_task() diff --git a/examples/run.py b/examples/run.py deleted file mode 100755 index 14b0c00..0000000 --- a/examples/run.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env -S rye run python - -from typing import List - -from pydantic import BaseModel - -from browser_use_sdk import BrowserUse - -# gets API Key from environment variable BROWSER_USE_API_KEY -client = BrowserUse() - - -# Regular Task -def run_regular_task() -> None: - regular_result = client.tasks.run( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gemini-2.5-flash"}, - ) - - print(f"Task ID: {regular_result.id}") - - print(regular_result.done_output) - - print("Done") - - -run_regular_task() - - -# Structured Output -def run_structured_task() -> None: - class HackerNewsPost(BaseModel): - title: str - url: str - - class SearchResult(BaseModel): - posts: List[HackerNewsPost] - - structured_result = client.tasks.run( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gpt-4.1"}, - structured_output_json=SearchResult, - ) - - print(f"Task ID: {structured_result.id}") - - if structured_result.parsed_output is not None: - for post in structured_result.parsed_output.posts: - print(f" - {post.title} - {post.url}") - - print("Done") - - -run_structured_task() diff --git a/examples/stream.py b/examples/stream.py deleted file mode 100755 index e017260..0000000 --- a/examples/stream.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env -S rye run python - -from typing import List - -from pydantic import BaseModel - -from browser_use_sdk import BrowserUse -from browser_use_sdk.types.task_create_params import AgentSettings - -# gets API Key from environment variable BROWSER_USE_API_KEY -client = BrowserUse() - - -# Regular Task -def stream_regular_task() -> None: - regular_task = client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings=AgentSettings(llm="gemini-2.5-flash"), - ) - - print(f"Task ID: {regular_task.id}") - - for res in client.tasks.stream(regular_task.id): - print(res.status) - - if len(res.steps) > 0: - last_step = res.steps[-1] - print(f"{last_step.url} ({last_step.next_goal})") - for action in last_step.actions: - print(f" - {action}") - - if res.status == "finished": - print(res.done_output) - - print("Regular: DONE") - - -stream_regular_task() - - -# Structured Output -def stream_structured_task() -> None: - class HackerNewsPost(BaseModel): - title: str - url: str - - class SearchResult(BaseModel): - posts: List[HackerNewsPost] - - structured_task = client.tasks.create( - task=""" - Find top 10 Hacker News articles and return the title and url. - """, - agent_settings={"llm": "gpt-4.1"}, - structured_output_json=SearchResult, - ) - - print(f"Task ID: {structured_task.id}") - - for res in client.tasks.stream(structured_task.id, structured_output_json=SearchResult): - print(res.status) - - if res.status == "finished": - if res.parsed_output is None: - print("No output") - else: - for post in res.parsed_output.posts: - print(f" - {post.title} - {post.url}") - break - - print("Done") - - -stream_structured_task() diff --git a/examples/webhooks.py b/examples/webhooks.py deleted file mode 100755 index 65cb93b..0000000 --- a/examples/webhooks.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env -S rye run python - -from typing import Any, Dict, Tuple -from datetime import datetime - -from browser_use_sdk.lib.webhooks import ( - Webhook, - WebhookAgentTaskStatusUpdate, - WebhookAgentTaskStatusUpdatePayload, - create_webhook_signature, - verify_webhook_event_signature, -) - -SECRET = "your-webhook-secret-key" - - -def mock_webhook_event() -> Tuple[Dict[str, Any], str, str]: - """Mock a webhook event.""" - - timestamp = datetime.fromisoformat("2023-01-01T00:00:00").isoformat() - - payload = WebhookAgentTaskStatusUpdatePayload( - session_id="sess_123", - task_id="task_123", - status="started", - metadata={"progress": 25}, - ) - - signature = create_webhook_signature( - payload=payload.model_dump(), - timestamp=timestamp, - secret=SECRET, - ) - - evt: Webhook = WebhookAgentTaskStatusUpdate( - type="agent.task.status_update", - timestamp=datetime.fromisoformat("2023-01-01T00:00:00"), - payload=payload, - ) - - return evt.model_dump(), signature, timestamp - - -def main() -> None: - """Demonstrate webhook functionality.""" - - # NOTE: You'd get the evt and signature from the webhook request body and headers! - evt, signature, timestamp = mock_webhook_event() - - verified_webhook = verify_webhook_event_signature( - body=evt, - expected_signature=signature, - timestamp=timestamp, - secret=SECRET, - ) - - if verified_webhook is None: - print("✗ Webhook signature verification failed") - else: - print("✓ Webhook signature verified successfully") - print(f" Event type: {verified_webhook.type}") - - -if __name__ == "__main__": - main() diff --git a/reference.md b/reference.md index 33df870..4172264 100644 --- a/reference.md +++ b/reference.md @@ -27,9 +27,9 @@ Get authenticated account information including credit balances and account deta
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.accounts.get_account_me() @@ -88,9 +88,9 @@ Get paginated list of AI agent tasks with optional filtering by session and stat
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.tasks.list_tasks() @@ -198,9 +198,9 @@ You can either:
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.tasks.create_task( @@ -372,9 +372,9 @@ Get detailed task information including status, progress, steps, and file output
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.tasks.get_task( @@ -442,9 +442,9 @@ Control task execution with stop, pause, resume, or stop task and session action
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.tasks.update_task( @@ -521,9 +521,9 @@ Get secure download URL for task execution logs with step-by-step details.
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.tasks.get_task_logs( @@ -592,9 +592,9 @@ Get paginated list of AI agent sessions with optional status filtering.
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.sessions.list_sessions() @@ -676,9 +676,9 @@ Create a new session with a new task.
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.sessions.create_session() @@ -752,9 +752,9 @@ Get detailed session information including status, URLs, and task details.
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.sessions.get_session( @@ -822,9 +822,9 @@ Permanently delete a session and all associated data.
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.sessions.delete_session( @@ -892,9 +892,9 @@ Stop a session and all its running tasks.
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.sessions.update_session( @@ -962,9 +962,9 @@ Get public share information including URL and usage statistics.
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.sessions.get_session_public_share( @@ -1032,9 +1032,9 @@ Create or return existing public share for a session.
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.sessions.create_session_public_share( @@ -1102,9 +1102,9 @@ Remove public share for a session.
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.sessions.delete_session_public_share( @@ -1173,9 +1173,9 @@ Generate a secure presigned URL for uploading files that AI agents can use durin
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.files.user_upload_file_presigned_url( @@ -1270,9 +1270,9 @@ Get secure download URL for an output file generated by the AI agent.
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.files.get_task_output_file_presigned_url( @@ -1350,9 +1350,9 @@ Get paginated list of profiles.
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.profiles.list_profiles() @@ -1431,9 +1431,9 @@ You can create a new profile by calling this endpoint.
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.profiles.create_profile() @@ -1491,9 +1491,9 @@ Get profile details.
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.profiles.get_profile( @@ -1561,9 +1561,9 @@ Permanently delete a browser profile and its configuration.
```python -from browser_use import BrowserUse +from browser_use import BrowserUseClient -client = BrowserUse( +client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.profiles.delete_browser_profile( diff --git a/src/browser_use/__init__.py b/src/browser_use/__init__.py index b415d1a..3250bc9 100644 --- a/src/browser_use/__init__.py +++ b/src/browser_use/__init__.py @@ -3,7 +3,16 @@ # isort: skip_file from . import accounts, files, profiles, sessions, tasks -from .client import AsyncBrowserUse, BrowserUse +from .client import AsyncBrowserUseClient, BrowserUseClient from .version import __version__ -__all__ = ["AsyncBrowserUse", "BrowserUse", "__version__", "accounts", "files", "profiles", "sessions", "tasks"] +__all__ = [ + "AsyncBrowserUseClient", + "BrowserUseClient", + "__version__", + "accounts", + "files", + "profiles", + "sessions", + "tasks", +] diff --git a/src/browser_use/accounts/client.py b/src/browser_use/accounts/client.py index ba05a0e..9bf30ed 100644 --- a/src/browser_use/accounts/client.py +++ b/src/browser_use/accounts/client.py @@ -39,9 +39,9 @@ def get_account_me(self, *, request_options: typing.Optional[RequestOptions] = N Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.accounts.get_account_me() @@ -83,9 +83,9 @@ async def get_account_me(self, *, request_options: typing.Optional[RequestOption -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) diff --git a/src/browser_use/base_client.py b/src/browser_use/base_client.py new file mode 100644 index 0000000..4bcbfa6 --- /dev/null +++ b/src/browser_use/base_client.py @@ -0,0 +1,165 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from .accounts.client import AccountsClient, AsyncAccountsClient +from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .environment import BrowserUseClientEnvironment +from .files.client import AsyncFilesClient, FilesClient +from .profiles.client import AsyncProfilesClient, ProfilesClient +from .sessions.client import AsyncSessionsClient, SessionsClient +from .tasks.client import AsyncTasksClient, TasksClient + + +class BaseClient: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : typing.Optional[str] + The base url to use for requests from the client. + + environment : BrowserUseClientEnvironment + The environment to use for requests from the client. from .environment import BrowserUseClientEnvironment + + + + Defaults to BrowserUseClientEnvironment.PRODUCTION + + + + api_key : str + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.Client] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from browser_use import BrowserUseClient + + client = BrowserUseClient( + api_key="YOUR_API_KEY", + ) + """ + + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: BrowserUseClientEnvironment = BrowserUseClientEnvironment.PRODUCTION, + api_key: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.Client] = None, + ): + _defaulted_timeout = ( + timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read + ) + self._client_wrapper = SyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + api_key=api_key, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.Client(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + self.accounts = AccountsClient(client_wrapper=self._client_wrapper) + self.tasks = TasksClient(client_wrapper=self._client_wrapper) + self.sessions = SessionsClient(client_wrapper=self._client_wrapper) + self.files = FilesClient(client_wrapper=self._client_wrapper) + self.profiles = ProfilesClient(client_wrapper=self._client_wrapper) + + +class AsyncBaseClient: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : typing.Optional[str] + The base url to use for requests from the client. + + environment : BrowserUseClientEnvironment + The environment to use for requests from the client. from .environment import BrowserUseClientEnvironment + + + + Defaults to BrowserUseClientEnvironment.PRODUCTION + + + + api_key : str + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.AsyncClient] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from browser_use import AsyncBrowserUseClient + + client = AsyncBrowserUseClient( + api_key="YOUR_API_KEY", + ) + """ + + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: BrowserUseClientEnvironment = BrowserUseClientEnvironment.PRODUCTION, + api_key: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.AsyncClient] = None, + ): + _defaulted_timeout = ( + timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read + ) + self._client_wrapper = AsyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + api_key=api_key, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.AsyncClient(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + self.accounts = AsyncAccountsClient(client_wrapper=self._client_wrapper) + self.tasks = AsyncTasksClient(client_wrapper=self._client_wrapper) + self.sessions = AsyncSessionsClient(client_wrapper=self._client_wrapper) + self.files = AsyncFilesClient(client_wrapper=self._client_wrapper) + self.profiles = AsyncProfilesClient(client_wrapper=self._client_wrapper) + + +def _get_base_url(*, base_url: typing.Optional[str] = None, environment: BrowserUseClientEnvironment) -> str: + if base_url is not None: + return base_url + elif environment is not None: + return environment.value + else: + raise Exception("Please pass in either base_url or environment to construct the client") diff --git a/src/browser_use/environment.py b/src/browser_use/environment.py index 3d6dc8a..4c7cc89 100644 --- a/src/browser_use/environment.py +++ b/src/browser_use/environment.py @@ -3,5 +3,5 @@ import enum -class BrowserUseEnvironment(enum.Enum): +class BrowserUseClientEnvironment(enum.Enum): PRODUCTION = "https://api.browser-use.com/api/v2" diff --git a/src/browser_use/files/client.py b/src/browser_use/files/client.py index 8824f2b..65ab7af 100644 --- a/src/browser_use/files/client.py +++ b/src/browser_use/files/client.py @@ -62,9 +62,9 @@ def user_upload_file_presigned_url( Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.files.user_upload_file_presigned_url( @@ -105,9 +105,9 @@ def get_task_output_file_presigned_url( Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.files.get_task_output_file_presigned_url( @@ -172,9 +172,9 @@ async def user_upload_file_presigned_url( -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) @@ -223,9 +223,9 @@ async def get_task_output_file_presigned_url( -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) diff --git a/src/browser_use/profiles/client.py b/src/browser_use/profiles/client.py index 5d5c658..c2cda96 100644 --- a/src/browser_use/profiles/client.py +++ b/src/browser_use/profiles/client.py @@ -50,9 +50,9 @@ def list_profiles( Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.profiles.list_profiles() @@ -83,9 +83,9 @@ def create_profile(self, *, request_options: typing.Optional[RequestOptions] = N Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.profiles.create_profile() @@ -111,9 +111,9 @@ def get_profile(self, profile_id: str, *, request_options: typing.Optional[Reque Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.profiles.get_profile( @@ -142,9 +142,9 @@ def delete_browser_profile( Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.profiles.delete_browser_profile( @@ -198,9 +198,9 @@ async def list_profiles( -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) @@ -239,9 +239,9 @@ async def create_profile(self, *, request_options: typing.Optional[RequestOption -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) @@ -277,9 +277,9 @@ async def get_profile( -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) @@ -316,9 +316,9 @@ async def delete_browser_profile( -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) diff --git a/src/browser_use/sessions/client.py b/src/browser_use/sessions/client.py index 606cde5..84aa1e2 100644 --- a/src/browser_use/sessions/client.py +++ b/src/browser_use/sessions/client.py @@ -60,9 +60,9 @@ def list_sessions( Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.sessions.list_sessions() @@ -100,9 +100,9 @@ def create_session( Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.sessions.create_session() @@ -130,9 +130,9 @@ def get_session(self, session_id: str, *, request_options: typing.Optional[Reque Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.sessions.get_session( @@ -159,9 +159,9 @@ def delete_session(self, session_id: str, *, request_options: typing.Optional[Re Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.sessions.delete_session( @@ -191,9 +191,9 @@ def update_session( Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.sessions.update_session( @@ -223,9 +223,9 @@ def get_session_public_share( Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.sessions.get_session_public_share( @@ -255,9 +255,9 @@ def create_session_public_share( Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.sessions.create_session_public_share( @@ -286,9 +286,9 @@ def delete_session_public_share( Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.sessions.delete_session_public_share( @@ -345,9 +345,9 @@ async def list_sessions( -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) @@ -393,9 +393,9 @@ async def create_session( -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) @@ -433,9 +433,9 @@ async def get_session( -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) @@ -470,9 +470,9 @@ async def delete_session(self, session_id: str, *, request_options: typing.Optio -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) @@ -510,9 +510,9 @@ async def update_session( -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) @@ -550,9 +550,9 @@ async def get_session_public_share( -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) @@ -590,9 +590,9 @@ async def create_session_public_share( -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) @@ -629,9 +629,9 @@ async def delete_session_public_share( -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) diff --git a/src/browser_use/tasks/client.py b/src/browser_use/tasks/client.py index 5d2574e..0ff2499 100644 --- a/src/browser_use/tasks/client.py +++ b/src/browser_use/tasks/client.py @@ -71,9 +71,9 @@ def list_tasks( Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.tasks.list_tasks() @@ -167,9 +167,9 @@ def create_task( Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.tasks.create_task( @@ -213,9 +213,9 @@ def get_task(self, task_id: str, *, request_options: typing.Optional[RequestOpti Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.tasks.get_task( @@ -248,9 +248,9 @@ def update_task( Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.tasks.update_task( @@ -281,9 +281,9 @@ def get_task_logs( Examples -------- - from browser_use import BrowserUse + from browser_use import BrowserUseClient - client = BrowserUse( + client = BrowserUseClient( api_key="YOUR_API_KEY", ) client.tasks.get_task_logs( @@ -349,9 +349,9 @@ async def list_tasks( -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) @@ -453,9 +453,9 @@ async def create_task( -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) @@ -507,9 +507,9 @@ async def get_task(self, task_id: str, *, request_options: typing.Optional[Reque -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) @@ -550,9 +550,9 @@ async def update_task( -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) @@ -591,9 +591,9 @@ async def get_task_logs( -------- import asyncio - from browser_use import AsyncBrowserUse + from browser_use import AsyncBrowserUseClient - client = AsyncBrowserUse( + client = AsyncBrowserUseClient( api_key="YOUR_API_KEY", ) From 2bcffcdc75486db9f14803c071a58601970de70e Mon Sep 17 00:00:00 2001 From: fern-api <115122769+fern-api[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 08:16:58 +0000 Subject: [PATCH 03/13] SDK regeneration --- README.md | 20 ++++---- reference.md | 80 +++++++++++++++--------------- src/browser_use/__init__.py | 13 +---- src/browser_use/accounts/client.py | 8 +-- src/browser_use/base_client.py | 28 +++++------ src/browser_use/environment.py | 2 +- src/browser_use/files/client.py | 16 +++--- src/browser_use/profiles/client.py | 32 ++++++------ src/browser_use/sessions/client.py | 64 ++++++++++++------------ src/browser_use/tasks/client.py | 40 +++++++-------- 10 files changed, 147 insertions(+), 156 deletions(-) diff --git a/README.md b/README.md index 7bcad6e..0f4433d 100644 --- a/README.md +++ b/README.md @@ -205,9 +205,9 @@ The SDK provides access to raw response data, including headers, through the `.w The `.with_raw_response` property returns a "raw" client that can be used to access the `.headers` and `.data` attributes. ```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( ..., ) response = client.tasks.with_raw_response.create_task(...) @@ -241,9 +241,9 @@ The SDK defaults to a 60 second timeout. You can configure this with a timeout o ```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( ..., timeout=20.0, ) @@ -262,9 +262,9 @@ and transports. ```python import httpx -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( ..., httpx_client=httpx.Client( proxies="http://my.test.proxy.example.com", @@ -483,9 +483,9 @@ A full reference for this library is available [here](https://github.com/browser Instantiate and use the client with the following: ```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.tasks.create_task( @@ -500,9 +500,9 @@ The SDK also exports an `async` client so that you can make non-blocking calls t ```python import asyncio -from browser_use import AsyncBrowserUseClient +from browser_use import AsyncBrowserUse -client = AsyncBrowserUseClient( +client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) diff --git a/reference.md b/reference.md index 4172264..33df870 100644 --- a/reference.md +++ b/reference.md @@ -27,9 +27,9 @@ Get authenticated account information including credit balances and account deta
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.accounts.get_account_me() @@ -88,9 +88,9 @@ Get paginated list of AI agent tasks with optional filtering by session and stat
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.tasks.list_tasks() @@ -198,9 +198,9 @@ You can either:
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.tasks.create_task( @@ -372,9 +372,9 @@ Get detailed task information including status, progress, steps, and file output
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.tasks.get_task( @@ -442,9 +442,9 @@ Control task execution with stop, pause, resume, or stop task and session action
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.tasks.update_task( @@ -521,9 +521,9 @@ Get secure download URL for task execution logs with step-by-step details.
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.tasks.get_task_logs( @@ -592,9 +592,9 @@ Get paginated list of AI agent sessions with optional status filtering.
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.sessions.list_sessions() @@ -676,9 +676,9 @@ Create a new session with a new task.
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.sessions.create_session() @@ -752,9 +752,9 @@ Get detailed session information including status, URLs, and task details.
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.sessions.get_session( @@ -822,9 +822,9 @@ Permanently delete a session and all associated data.
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.sessions.delete_session( @@ -892,9 +892,9 @@ Stop a session and all its running tasks.
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.sessions.update_session( @@ -962,9 +962,9 @@ Get public share information including URL and usage statistics.
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.sessions.get_session_public_share( @@ -1032,9 +1032,9 @@ Create or return existing public share for a session.
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.sessions.create_session_public_share( @@ -1102,9 +1102,9 @@ Remove public share for a session.
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.sessions.delete_session_public_share( @@ -1173,9 +1173,9 @@ Generate a secure presigned URL for uploading files that AI agents can use durin
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.files.user_upload_file_presigned_url( @@ -1270,9 +1270,9 @@ Get secure download URL for an output file generated by the AI agent.
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.files.get_task_output_file_presigned_url( @@ -1350,9 +1350,9 @@ Get paginated list of profiles.
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.profiles.list_profiles() @@ -1431,9 +1431,9 @@ You can create a new profile by calling this endpoint.
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.profiles.create_profile() @@ -1491,9 +1491,9 @@ Get profile details.
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.profiles.get_profile( @@ -1561,9 +1561,9 @@ Permanently delete a browser profile and its configuration.
```python -from browser_use import BrowserUseClient +from browser_use import BrowserUse -client = BrowserUseClient( +client = BrowserUse( api_key="YOUR_API_KEY", ) client.profiles.delete_browser_profile( diff --git a/src/browser_use/__init__.py b/src/browser_use/__init__.py index 3250bc9..b415d1a 100644 --- a/src/browser_use/__init__.py +++ b/src/browser_use/__init__.py @@ -3,16 +3,7 @@ # isort: skip_file from . import accounts, files, profiles, sessions, tasks -from .client import AsyncBrowserUseClient, BrowserUseClient +from .client import AsyncBrowserUse, BrowserUse from .version import __version__ -__all__ = [ - "AsyncBrowserUseClient", - "BrowserUseClient", - "__version__", - "accounts", - "files", - "profiles", - "sessions", - "tasks", -] +__all__ = ["AsyncBrowserUse", "BrowserUse", "__version__", "accounts", "files", "profiles", "sessions", "tasks"] diff --git a/src/browser_use/accounts/client.py b/src/browser_use/accounts/client.py index 9bf30ed..ba05a0e 100644 --- a/src/browser_use/accounts/client.py +++ b/src/browser_use/accounts/client.py @@ -39,9 +39,9 @@ def get_account_me(self, *, request_options: typing.Optional[RequestOptions] = N Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.accounts.get_account_me() @@ -83,9 +83,9 @@ async def get_account_me(self, *, request_options: typing.Optional[RequestOption -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) diff --git a/src/browser_use/base_client.py b/src/browser_use/base_client.py index 4bcbfa6..c0d4296 100644 --- a/src/browser_use/base_client.py +++ b/src/browser_use/base_client.py @@ -5,7 +5,7 @@ import httpx from .accounts.client import AccountsClient, AsyncAccountsClient from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from .environment import BrowserUseClientEnvironment +from .environment import BrowserUseEnvironment from .files.client import AsyncFilesClient, FilesClient from .profiles.client import AsyncProfilesClient, ProfilesClient from .sessions.client import AsyncSessionsClient, SessionsClient @@ -21,12 +21,12 @@ class BaseClient: base_url : typing.Optional[str] The base url to use for requests from the client. - environment : BrowserUseClientEnvironment - The environment to use for requests from the client. from .environment import BrowserUseClientEnvironment + environment : BrowserUseEnvironment + The environment to use for requests from the client. from .environment import BrowserUseEnvironment - Defaults to BrowserUseClientEnvironment.PRODUCTION + Defaults to BrowserUseEnvironment.PRODUCTION @@ -45,9 +45,9 @@ class BaseClient: Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) """ @@ -56,7 +56,7 @@ def __init__( self, *, base_url: typing.Optional[str] = None, - environment: BrowserUseClientEnvironment = BrowserUseClientEnvironment.PRODUCTION, + environment: BrowserUseEnvironment = BrowserUseEnvironment.PRODUCTION, api_key: str, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, @@ -93,12 +93,12 @@ class AsyncBaseClient: base_url : typing.Optional[str] The base url to use for requests from the client. - environment : BrowserUseClientEnvironment - The environment to use for requests from the client. from .environment import BrowserUseClientEnvironment + environment : BrowserUseEnvironment + The environment to use for requests from the client. from .environment import BrowserUseEnvironment - Defaults to BrowserUseClientEnvironment.PRODUCTION + Defaults to BrowserUseEnvironment.PRODUCTION @@ -117,9 +117,9 @@ class AsyncBaseClient: Examples -------- - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) """ @@ -128,7 +128,7 @@ def __init__( self, *, base_url: typing.Optional[str] = None, - environment: BrowserUseClientEnvironment = BrowserUseClientEnvironment.PRODUCTION, + environment: BrowserUseEnvironment = BrowserUseEnvironment.PRODUCTION, api_key: str, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, @@ -156,7 +156,7 @@ def __init__( self.profiles = AsyncProfilesClient(client_wrapper=self._client_wrapper) -def _get_base_url(*, base_url: typing.Optional[str] = None, environment: BrowserUseClientEnvironment) -> str: +def _get_base_url(*, base_url: typing.Optional[str] = None, environment: BrowserUseEnvironment) -> str: if base_url is not None: return base_url elif environment is not None: diff --git a/src/browser_use/environment.py b/src/browser_use/environment.py index 4c7cc89..3d6dc8a 100644 --- a/src/browser_use/environment.py +++ b/src/browser_use/environment.py @@ -3,5 +3,5 @@ import enum -class BrowserUseClientEnvironment(enum.Enum): +class BrowserUseEnvironment(enum.Enum): PRODUCTION = "https://api.browser-use.com/api/v2" diff --git a/src/browser_use/files/client.py b/src/browser_use/files/client.py index 65ab7af..8824f2b 100644 --- a/src/browser_use/files/client.py +++ b/src/browser_use/files/client.py @@ -62,9 +62,9 @@ def user_upload_file_presigned_url( Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.files.user_upload_file_presigned_url( @@ -105,9 +105,9 @@ def get_task_output_file_presigned_url( Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.files.get_task_output_file_presigned_url( @@ -172,9 +172,9 @@ async def user_upload_file_presigned_url( -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) @@ -223,9 +223,9 @@ async def get_task_output_file_presigned_url( -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) diff --git a/src/browser_use/profiles/client.py b/src/browser_use/profiles/client.py index c2cda96..5d5c658 100644 --- a/src/browser_use/profiles/client.py +++ b/src/browser_use/profiles/client.py @@ -50,9 +50,9 @@ def list_profiles( Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.profiles.list_profiles() @@ -83,9 +83,9 @@ def create_profile(self, *, request_options: typing.Optional[RequestOptions] = N Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.profiles.create_profile() @@ -111,9 +111,9 @@ def get_profile(self, profile_id: str, *, request_options: typing.Optional[Reque Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.profiles.get_profile( @@ -142,9 +142,9 @@ def delete_browser_profile( Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.profiles.delete_browser_profile( @@ -198,9 +198,9 @@ async def list_profiles( -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) @@ -239,9 +239,9 @@ async def create_profile(self, *, request_options: typing.Optional[RequestOption -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) @@ -277,9 +277,9 @@ async def get_profile( -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) @@ -316,9 +316,9 @@ async def delete_browser_profile( -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) diff --git a/src/browser_use/sessions/client.py b/src/browser_use/sessions/client.py index 84aa1e2..606cde5 100644 --- a/src/browser_use/sessions/client.py +++ b/src/browser_use/sessions/client.py @@ -60,9 +60,9 @@ def list_sessions( Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.sessions.list_sessions() @@ -100,9 +100,9 @@ def create_session( Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.sessions.create_session() @@ -130,9 +130,9 @@ def get_session(self, session_id: str, *, request_options: typing.Optional[Reque Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.sessions.get_session( @@ -159,9 +159,9 @@ def delete_session(self, session_id: str, *, request_options: typing.Optional[Re Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.sessions.delete_session( @@ -191,9 +191,9 @@ def update_session( Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.sessions.update_session( @@ -223,9 +223,9 @@ def get_session_public_share( Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.sessions.get_session_public_share( @@ -255,9 +255,9 @@ def create_session_public_share( Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.sessions.create_session_public_share( @@ -286,9 +286,9 @@ def delete_session_public_share( Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.sessions.delete_session_public_share( @@ -345,9 +345,9 @@ async def list_sessions( -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) @@ -393,9 +393,9 @@ async def create_session( -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) @@ -433,9 +433,9 @@ async def get_session( -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) @@ -470,9 +470,9 @@ async def delete_session(self, session_id: str, *, request_options: typing.Optio -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) @@ -510,9 +510,9 @@ async def update_session( -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) @@ -550,9 +550,9 @@ async def get_session_public_share( -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) @@ -590,9 +590,9 @@ async def create_session_public_share( -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) @@ -629,9 +629,9 @@ async def delete_session_public_share( -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) diff --git a/src/browser_use/tasks/client.py b/src/browser_use/tasks/client.py index 0ff2499..5d2574e 100644 --- a/src/browser_use/tasks/client.py +++ b/src/browser_use/tasks/client.py @@ -71,9 +71,9 @@ def list_tasks( Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.tasks.list_tasks() @@ -167,9 +167,9 @@ def create_task( Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.tasks.create_task( @@ -213,9 +213,9 @@ def get_task(self, task_id: str, *, request_options: typing.Optional[RequestOpti Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.tasks.get_task( @@ -248,9 +248,9 @@ def update_task( Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.tasks.update_task( @@ -281,9 +281,9 @@ def get_task_logs( Examples -------- - from browser_use import BrowserUseClient + from browser_use import BrowserUse - client = BrowserUseClient( + client = BrowserUse( api_key="YOUR_API_KEY", ) client.tasks.get_task_logs( @@ -349,9 +349,9 @@ async def list_tasks( -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) @@ -453,9 +453,9 @@ async def create_task( -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) @@ -507,9 +507,9 @@ async def get_task(self, task_id: str, *, request_options: typing.Optional[Reque -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) @@ -550,9 +550,9 @@ async def update_task( -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) @@ -591,9 +591,9 @@ async def get_task_logs( -------- import asyncio - from browser_use import AsyncBrowserUseClient + from browser_use import AsyncBrowserUse - client = AsyncBrowserUseClient( + client = AsyncBrowserUse( api_key="YOUR_API_KEY", ) From 01010d803f5173ef01f75e7742a4d0a4f500f2a7 Mon Sep 17 00:00:00 2001 From: Matic Zavadlal Date: Tue, 2 Sep 2025 11:04:43 +0200 Subject: [PATCH 04/13] stash --- .fernignore | 4 +- examples/async_retrieve.py | 97 +++++++ examples/async_run.py | 69 +++++ examples/async_stream.py | 81 ++++++ examples/retrieve.py | 89 ++++++ examples/run.py | 60 ++++ examples/stream.py | 75 +++++ examples/webhooks.py | 65 +++++ src/browser_use/client.py | 6 +- src/browser_use/{wrapper => lib}/webhooks.py | 4 +- src/browser_use/wrapper/parse.py | 36 ++- src/browser_use/wrapper/tasks/client.py | 279 ++++++++++++++++++- 12 files changed, 857 insertions(+), 8 deletions(-) create mode 100755 examples/async_retrieve.py create mode 100755 examples/async_run.py create mode 100755 examples/async_stream.py create mode 100755 examples/retrieve.py create mode 100755 examples/run.py create mode 100755 examples/stream.py create mode 100755 examples/webhooks.py rename src/browser_use/{wrapper => lib}/webhooks.py (98%) diff --git a/.fernignore b/.fernignore index a5e6117..d7d43bf 100644 --- a/.fernignore +++ b/.fernignore @@ -3,4 +3,6 @@ .vscode/ src/browser_use/client.py -src/browser_use/wrapper/ \ No newline at end of file +src/browser_use/lib/ +src/browser_use/wrapper/ +examples/ \ No newline at end of file diff --git a/examples/async_retrieve.py b/examples/async_retrieve.py new file mode 100755 index 0000000..7346ccd --- /dev/null +++ b/examples/async_retrieve.py @@ -0,0 +1,97 @@ +#!/usr/bin/env -S rye run python + +import asyncio +from typing import List + +from pydantic import BaseModel + +from browser_use import AsyncBrowserUse + +# gets API Key from environment variable BROWSER_USE_API_KEY +client = AsyncBrowserUse() + + +# Regular Task +async def retrieve_regular_task() -> None: + """ + Retrieves a regular task and waits for it to finish. + """ + + print("Retrieving regular task...") + + regular_task = await client.tasks.create_task( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + llm="gemini-2.5-flash", + ) + + print(f"Regular Task ID: {regular_task.id}") + + while True: + regular_status = await client.tasks.get_task(regular_task.id) + print(f"Regular Task Status: {regular_status.status}") + if regular_status.status == "finished": + print(f"Regular Task Output: {regular_status.output}") + break + + await asyncio.sleep(1) + + print("Done") + + +async def retrieve_structured_task() -> None: + """ + Retrieves a structured task and waits for it to finish. + """ + + print("Retrieving structured task...") + + # Structured Output + class HackerNewsPost(BaseModel): + title: str + url: str + + class SearchResult(BaseModel): + posts: List[HackerNewsPost] + + structured_task = await client.tasks.create_task( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + llm="gpt-4.1", + schema=SearchResult, + ) + + print(f"Structured Task ID: {structured_task.id}") + + while True: + structured_status = await client.tasks.retrieve(task_id=structured_task.id, schema=SearchResult) + print(f"Structured Task Status: {structured_status.status}") + + if structured_status.status == "finished": + if structured_status.parsed_output is None: + print("Structured Task No output") + else: + for post in structured_status.parsed_output.posts: + print(f" - {post.title} - {post.url}") + + break + + await asyncio.sleep(1) + + print("Done") + + +# Main + + +async def main() -> None: + await asyncio.gather( + # + retrieve_regular_task(), + retrieve_structured_task(), + ) + + +asyncio.run(main()) diff --git a/examples/async_run.py b/examples/async_run.py new file mode 100755 index 0000000..3f6e07b --- /dev/null +++ b/examples/async_run.py @@ -0,0 +1,69 @@ +#!/usr/bin/env -S rye run python + +import asyncio +from typing import List + +from pydantic import BaseModel + +from browser_use import AsyncBrowserUse + +# gets API Key from environment variable BROWSER_USE_API_KEY +client = AsyncBrowserUse() + + +# Regular Task +async def run_regular_task() -> None: + task = await client.tasks.create_task( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + llm="gemini-2.5-flash", + ) + + print(f"Regular Task ID: {task.id}") + + result = await task.complete() + + print(f"Regular Task Output: {result.output}") + + print("Done") + + +# Structured Output +async def run_structured_task() -> None: + class HackerNewsPost(BaseModel): + title: str + url: str + + class SearchResult(BaseModel): + posts: List[HackerNewsPost] + + task = await client.tasks.create_task( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + llm="gpt-4.1", + structured_output_json=SearchResult, + ) + + print(f"Structured Task ID: {task.id}") + + result = await task.complete() + + if result.parsed is not None: + print("Structured Task Output:") + for post in result.parsed.posts: + print(f" - {post.title} - {post.url}") + + print("Structured Task Done") + + +async def main() -> None: + await asyncio.gather( + # + run_regular_task(), + run_structured_task(), + ) + + +asyncio.run(main()) diff --git a/examples/async_stream.py b/examples/async_stream.py new file mode 100755 index 0000000..fa75555 --- /dev/null +++ b/examples/async_stream.py @@ -0,0 +1,81 @@ +#!/usr/bin/env -S rye run python + +import asyncio +from typing import List + +from pydantic import BaseModel + +from browser_use import AsyncBrowserUse + +# gets API Key from environment variable BROWSER_USE_API_KEY +client = AsyncBrowserUse() + + +# Regular Task +async def stream_regular_task() -> None: + regular_task = await client.tasks.create( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + llm="gemini-2.5-flash", + ) + + print(f"Regular Task ID: {regular_task.id}") + + async for res in client.tasks.stream(regular_task.id): + print(f"Regular Task Status: {res.status}") + + if len(res.steps) > 0: + last_step = res.steps[-1] + print(f"Regular Task Step: {last_step.url} ({last_step.next_goal})") + for action in last_step.actions: + print(f" - Regular Task Action: {action}") + + print("Regular Task Done") + + +# Structured Output +async def stream_structured_task() -> None: + class HackerNewsPost(BaseModel): + title: str + url: str + + class SearchResult(BaseModel): + posts: List[HackerNewsPost] + + structured_task = await client.tasks.create( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + llm="gpt-4.1", + structured_output_json=SearchResult, + ) + + print(f"Structured Task ID: {structured_task.id}") + + async for res in client.tasks.stream(structured_task.id, structured_output_json=SearchResult): + print(f"Structured Task Status: {res.status}") + + if res.status == "finished": + if res.parsed_output is None: + print("Structured Task No output") + else: + for post in res.parsed_output.posts: + print(f" - Structured Task Post: {post.title} - {post.url}") + break + + print("Structured Task Done") + + +# Main + + +async def main() -> None: + await asyncio.gather( + # + stream_regular_task(), + stream_structured_task(), + ) + + +asyncio.run(main()) diff --git a/examples/retrieve.py b/examples/retrieve.py new file mode 100755 index 0000000..a942613 --- /dev/null +++ b/examples/retrieve.py @@ -0,0 +1,89 @@ +#!/usr/bin/env -S rye run python + +import time +from typing import List + +from pydantic import BaseModel + +from browser_use import BrowserUse + +# gets API Key from environment variable BROWSER_USE_API_KEY +client = BrowserUse() + + +# Regular Task +def retrieve_regular_task() -> None: + """ + Retrieves a regular task and waits for it to finish. + """ + + print("Retrieving regular task...") + + regular_task = client.tasks.create_task( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + llm="gemini-2.5-flash", + ) + + print(f"Task ID: {regular_task.id}") + + while True: + regular_status = client.tasks.get_task(regular_task.id) + print(regular_status.status) + if regular_status.status == "finished": + print(regular_status.output) + break + + time.sleep(1) + + print("Done") + + +retrieve_regular_task() + + +def retrieve_structured_task() -> None: + """ + Retrieves a structured task and waits for it to finish. + """ + + print("Retrieving structured task...") + + # Structured Output + class HackerNewsPost(BaseModel): + title: str + url: str + + class SearchResult(BaseModel): + posts: List[HackerNewsPost] + + structured_task = client.tasks.create_task( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + llm="gpt-4.1", + schema=SearchResult, + ) + + print(f"Task ID: {structured_task.id}") + + while True: + structured_status = client.tasks.get_task(task_id=structured_task.id, schema=SearchResult) + print(structured_status.status) + + if structured_status.status == "finished": + if structured_status.parsed is None: + print("No output") + else: + for post in structured_status.parsed.posts: + print(f" - {post.title} - {post.url}") + + break + + time.sleep(1) + + print("Done") + + +retrieve_structured_task() diff --git a/examples/run.py b/examples/run.py new file mode 100755 index 0000000..c18cec4 --- /dev/null +++ b/examples/run.py @@ -0,0 +1,60 @@ +#!/usr/bin/env -S rye run python + +from typing import List + +from pydantic import BaseModel + +from browser_use import BrowserUse + +# gets API Key from environment variable BROWSER_USE_API_KEY +client = BrowserUse() + + +# Regular Task +def run_regular_task() -> None: + task = client.tasks.create_task( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + llm="gemini-2.5-flash", + ) + + print(f"Task ID: {task.id}") + + result = task.complete() + + print(result.output) + + print("Done") + + +run_regular_task() + + +# Structured Output +def run_structured_task() -> None: + class HackerNewsPost(BaseModel): + title: str + url: str + + class SearchResult(BaseModel): + posts: List[HackerNewsPost] + + structured_result = client.tasks.create_task( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + llm="gpt-4.1", + structured_output_json=SearchResult, + ) + + print(f"Task ID: {structured_result.id}") + + if structured_result.parsed_output is not None: + for post in structured_result.parsed_output.posts: + print(f" - {post.title} - {post.url}") + + print("Done") + + +run_structured_task() diff --git a/examples/stream.py b/examples/stream.py new file mode 100755 index 0000000..a8c5b48 --- /dev/null +++ b/examples/stream.py @@ -0,0 +1,75 @@ +#!/usr/bin/env -S rye run python + +from typing import List + +from pydantic import BaseModel + +from browser_use import BrowserUse + +# gets API Key from environment variable BROWSER_USE_API_KEY +client = BrowserUse() + + +# Regular Task +def stream_regular_task() -> None: + task = client.tasks.create_task( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + llm="gemini-2.5-flash", + ) + + print(f"Task ID: {task.id}") + + for res in task.stream(): + print(res.status) + + if len(res.steps) > 0: + last_step = res.steps[-1] + print(f"{last_step.url} ({last_step.next_goal})") + for action in last_step.actions: + print(f" - {action}") + + if res.status == "finished": + print(res.done_output) + + print("Regular: DONE") + + +stream_regular_task() + + +# Structured Output +def stream_structured_task() -> None: + class HackerNewsPost(BaseModel): + title: str + url: str + + class SearchResult(BaseModel): + posts: List[HackerNewsPost] + + task = client.tasks.create_task( + task=""" + Find top 10 Hacker News articles and return the title and url. + """, + llm="gpt-4.1", + structured_output_json=SearchResult, + ) + + print(f"Task ID: {task.id}") + + for res in task.stream(): + print(res.status) + + if res.status == "finished": + if res.parsed_output is None: + print("No output") + else: + for post in res.parsed_output.posts: + print(f" - {post.title} - {post.url}") + break + + print("Done") + + +stream_structured_task() diff --git a/examples/webhooks.py b/examples/webhooks.py new file mode 100755 index 0000000..2afccdb --- /dev/null +++ b/examples/webhooks.py @@ -0,0 +1,65 @@ +#!/usr/bin/env -S rye run python + +from datetime import datetime +from typing import Any, Dict, Tuple + +from browser_use.lib.webhooks import ( + Webhook, + WebhookAgentTaskStatusUpdate, + WebhookAgentTaskStatusUpdatePayload, + create_webhook_signature, + verify_webhook_event_signature, +) + +SECRET = "your-webhook-secret-key" + + +def mock_webhook_event() -> Tuple[Dict[str, Any], str, str]: + """Mock a webhook event.""" + + timestamp = datetime.fromisoformat("2023-01-01T00:00:00").isoformat() + + payload = WebhookAgentTaskStatusUpdatePayload( + session_id="sess_123", + task_id="task_123", + status="started", + metadata={"progress": 25}, + ) + + signature = create_webhook_signature( + payload=payload.model_dump(), + timestamp=timestamp, + secret=SECRET, + ) + + evt: Webhook = WebhookAgentTaskStatusUpdate( + type="agent.task.status_update", + timestamp=datetime.fromisoformat("2023-01-01T00:00:00"), + payload=payload, + ) + + return evt.model_dump(), signature, timestamp + + +def main() -> None: + """Demonstrate webhook functionality.""" + + # NOTE: You'd get the evt and signature from the webhook request body and headers! + evt, signature, timestamp = mock_webhook_event() + + verified_webhook = verify_webhook_event_signature( + body=evt, + expected_signature=signature, + timestamp=timestamp, + secret=SECRET, + ) + + if verified_webhook is None: + print("✗ Webhook signature verification failed") + else: + print("✓ Webhook signature verified successfully") + print(f" Event type: {verified_webhook.type}") + + +if __name__ == "__main__": + main() diff --git a/src/browser_use/client.py b/src/browser_use/client.py index 675e144..e508239 100644 --- a/src/browser_use/client.py +++ b/src/browser_use/client.py @@ -9,7 +9,7 @@ from .files.client import AsyncFilesClient, FilesClient from .profiles.client import AsyncProfilesClient, ProfilesClient from .sessions.client import AsyncSessionsClient, SessionsClient -from .tasks.client import AsyncTasksClient, TasksClient +from .wrapper.tasks.client import AsyncBrowserUseTasksClient, BrowserUseTasksClient class BrowserUse: @@ -78,7 +78,7 @@ def __init__( timeout=_defaulted_timeout, ) self.accounts = AccountsClient(client_wrapper=self._client_wrapper) - self.tasks = TasksClient(client_wrapper=self._client_wrapper) + self.tasks = BrowserUseTasksClient(client_wrapper=self._client_wrapper) self.sessions = SessionsClient(client_wrapper=self._client_wrapper) self.files = FilesClient(client_wrapper=self._client_wrapper) self.profiles = ProfilesClient(client_wrapper=self._client_wrapper) @@ -150,7 +150,7 @@ def __init__( timeout=_defaulted_timeout, ) self.accounts = AsyncAccountsClient(client_wrapper=self._client_wrapper) - self.tasks = AsyncTasksClient(client_wrapper=self._client_wrapper) + self.tasks = AsyncBrowserUseTasksClient(client_wrapper=self._client_wrapper) self.sessions = AsyncSessionsClient(client_wrapper=self._client_wrapper) self.files = AsyncFilesClient(client_wrapper=self._client_wrapper) self.profiles = AsyncProfilesClient(client_wrapper=self._client_wrapper) diff --git a/src/browser_use/wrapper/webhooks.py b/src/browser_use/lib/webhooks.py similarity index 98% rename from src/browser_use/wrapper/webhooks.py rename to src/browser_use/lib/webhooks.py index 1a8f1d2..bb9e110 100644 --- a/src/browser_use/wrapper/webhooks.py +++ b/src/browser_use/lib/webhooks.py @@ -1,8 +1,8 @@ +import hashlib import hmac import json -import hashlib -from typing import Any, Dict, Union, Literal, Optional from datetime import datetime +from typing import Any, Dict, Literal, Optional, Union from pydantic import BaseModel diff --git a/src/browser_use/wrapper/parse.py b/src/browser_use/wrapper/parse.py index f62cec4..930fd39 100644 --- a/src/browser_use/wrapper/parse.py +++ b/src/browser_use/wrapper/parse.py @@ -72,9 +72,43 @@ def _watch( time.sleep(interval) -class WrapperTaskCreatedResponse(TaskCreatedResponse): +# Sync ----------------------------------------------------------------------- + + +class WrappedTaskCreatedResponse(TaskCreatedResponse): """TaskCreatedResponse with utility methods for easier interfacing with Browser Use Cloud.""" def __init__(self, id: str): super().__init__() self.id = id + + +class WrappedStructuredTaskCreatedResponse(TaskCreatedResponse): + """TaskCreatedResponse with structured output.""" + + def __init__(self, id: str, schema: type[T], client: BrowserUse): + super().__init__() + + self.id = id + self._schema = schema + + +# Async ---------------------------------------------------------------------- + + +class AsyncWrappedTaskCreatedResponse(TaskCreatedResponse): + """TaskCreatedResponse with utility methods for easier interfacing with Browser Use Cloud.""" + + def __init__(self, id: str): + super().__init__() + self.id = id + + +class AsyncWrappedStructuredTaskCreatedResponse(TaskCreatedResponse): + """TaskCreatedResponse with structured output.""" + + def __init__(self, id: str, schema: type[T], client: BrowserUse): + super().__init__() + + self.id = id + self._schema = schema diff --git a/src/browser_use/wrapper/tasks/client.py b/src/browser_use/wrapper/tasks/client.py index 5e8388b..ef44007 100644 --- a/src/browser_use/wrapper/tasks/client.py +++ b/src/browser_use/wrapper/tasks/client.py @@ -1,4 +1,20 @@ -from browser_use.tasks.client import SyncClientWrapper, TasksClient +import json +import typing + +from pydantic import BaseModel + +from browser_use.core.request_options import RequestOptions +from browser_use.tasks.client import OMIT, AsyncClientWrapper, AsyncTasksClient, SyncClientWrapper, TasksClient +from browser_use.types.supported_ll_ms import SupportedLlMs +from browser_use.types.task_view import TaskView +from browser_use.wrapper.parse import ( + AsyncWrappedStructuredTaskCreatedResponse, + AsyncWrappedTaskCreatedResponse, + T, + TaskViewWithOutput, + WrappedStructuredTaskCreatedResponse, + WrappedTaskCreatedResponse, +) class BrowserUseTasksClient(TasksClient): @@ -6,3 +22,264 @@ class BrowserUseTasksClient(TasksClient): def __init__(self, *, client_wrapper: SyncClientWrapper): super().__init__(client_wrapper=client_wrapper) + + @typing.overload + def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + schema: type[T], + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> WrappedStructuredTaskCreatedResponse[T]: ... + + @typing.overload + def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + structured_output: typing.Optional[str] = OMIT, + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> WrappedStructuredTaskCreatedResponse[T]: ... + + def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + structured_output: typing.Optional[str] = OMIT, + schema: typing.Optional[type[BaseModel]] = OMIT, + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Union[WrappedStructuredTaskCreatedResponse[T], WrappedTaskCreatedResponse]: + if schema is not None: + structured_output = json.dumps(schema.model_json_schema()) + + res = super().create_task( + task=task, + llm=llm, + start_url=start_url, + max_steps=max_steps, + structured_output=structured_output, + session_id=session_id, + metadata=metadata, + secrets=secrets, + allowed_domains=allowed_domains, + highlight_elements=highlight_elements, + flash_mode=flash_mode, + thinking=thinking, + vision=vision, + system_prompt_extension=system_prompt_extension, + request_options=request_options, + ) + return WrappedStructuredTaskCreatedResponse[T](id=res.id, schema=schema, client=self) + + else: + res = super().create_task( + task=task, + llm=llm, + start_url=start_url, + max_steps=max_steps, + structured_output=structured_output, + session_id=session_id, + metadata=metadata, + secrets=secrets, + allowed_domains=allowed_domains, + highlight_elements=highlight_elements, + flash_mode=flash_mode, + thinking=thinking, + vision=vision, + system_prompt_extension=system_prompt_extension, + request_options=request_options, + ) + return WrappedTaskCreatedResponse(id=res.id, client=self) + + @typing.overload + def get_task( + self, task_id: str, schema: type[T], *, request_options: typing.Optional[RequestOptions] = None + ) -> TaskViewWithOutput[T]: ... + + @typing.overload + def get_task(self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> TaskView: ... + + def get_task( + self, + task_id: str, + schema: typing.Optional[typing.Union[type[BaseModel], str]] = OMIT, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Union[TaskViewWithOutput[T], TaskView]: + res = super().get_task(task_id, request_options=request_options) + + if schema is not None: + parsed_output = schema.model_validate_json(res.output) + return TaskViewWithOutput(**res.model_dump(), parsed_output=parsed_output) + else: + return res + + +class AsyncBrowserUseTasksClient(AsyncTasksClient): + """AsyncTaskClient with utility method overrides.""" + + def __init__(self, *, client_wrapper: AsyncClientWrapper): + super().__init__(client_wrapper=client_wrapper) + + @typing.overload + async def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + schema: type[T], + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncWrappedStructuredTaskCreatedResponse[T]: ... + + @typing.overload + async def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + structured_output: typing.Optional[str] = OMIT, + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncWrappedTaskCreatedResponse: ... + + async def create_task( + self, + *, + task: str, + llm: typing.Optional[SupportedLlMs] = OMIT, + start_url: typing.Optional[str] = OMIT, + max_steps: typing.Optional[int] = OMIT, + structured_output: typing.Optional[str] = OMIT, + schema: typing.Optional[type[BaseModel]] = OMIT, + session_id: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, + allowed_domains: typing.Optional[typing.Sequence[str]] = OMIT, + highlight_elements: typing.Optional[bool] = OMIT, + flash_mode: typing.Optional[bool] = OMIT, + thinking: typing.Optional[bool] = OMIT, + vision: typing.Optional[bool] = OMIT, + system_prompt_extension: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Union[AsyncWrappedStructuredTaskCreatedResponse[T], AsyncWrappedTaskCreatedResponse]: + if schema is not None: + structured_output = json.dumps(schema.model_json_schema()) + + res = super().create_task( + task=task, + llm=llm, + start_url=start_url, + max_steps=max_steps, + structured_output=structured_output, + session_id=session_id, + metadata=metadata, + secrets=secrets, + allowed_domains=allowed_domains, + highlight_elements=highlight_elements, + flash_mode=flash_mode, + thinking=thinking, + vision=vision, + system_prompt_extension=system_prompt_extension, + request_options=request_options, + ) + return AsyncWrappedStructuredTaskCreatedResponse[T](id=res.id, schema=schema, client=self) + + else: + res = super().create_task( + task=task, + llm=llm, + start_url=start_url, + max_steps=max_steps, + structured_output=structured_output, + session_id=session_id, + metadata=metadata, + secrets=secrets, + allowed_domains=allowed_domains, + highlight_elements=highlight_elements, + flash_mode=flash_mode, + thinking=thinking, + vision=vision, + system_prompt_extension=system_prompt_extension, + request_options=request_options, + ) + return AsyncWrappedTaskCreatedResponse(id=res.id, client=self) + + @typing.overload + async def get_task( + self, task_id: str, schema: type[T], *, request_options: typing.Optional[RequestOptions] = None + ) -> TaskViewWithOutput[T]: ... + + @typing.overload + async def get_task(self, task_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> TaskView: ... + + async def get_task( + self, + task_id: str, + schema: typing.Optional[typing.Union[type[BaseModel], str]] = OMIT, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Union[TaskViewWithOutput[T], TaskView]: + res = await super().get_task(task_id, request_options=request_options) + + if schema is not None: + parsed_output = schema.model_validate_json(res.output) + return TaskViewWithOutput(**res.model_dump(), parsed_output=parsed_output) + else: + return res From edb7c4cb6268b78eaeb70704dc470b5df0122f03 Mon Sep 17 00:00:00 2001 From: Matic Zavadlal Date: Tue, 2 Sep 2025 11:53:13 +0200 Subject: [PATCH 05/13] placeholders --- examples/async_retrieve.py | 12 +- examples/async_run.py | 6 +- examples/async_stream.py | 36 ++-- examples/retrieve.py | 16 +- examples/run.py | 12 +- examples/stream.py | 31 ++-- src/browser_use/wrapper/browser_use_client.py | 0 src/browser_use/wrapper/parse.py | 157 ++++++++++++++---- src/browser_use/wrapper/tasks/client.py | 2 +- 9 files changed, 170 insertions(+), 102 deletions(-) delete mode 100644 src/browser_use/wrapper/browser_use_client.py diff --git a/examples/async_retrieve.py b/examples/async_retrieve.py index 7346ccd..ff46823 100755 --- a/examples/async_retrieve.py +++ b/examples/async_retrieve.py @@ -19,17 +19,17 @@ async def retrieve_regular_task() -> None: print("Retrieving regular task...") - regular_task = await client.tasks.create_task( + task = await client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, llm="gemini-2.5-flash", ) - print(f"Regular Task ID: {regular_task.id}") + print(f"Regular Task ID: {task.id}") while True: - regular_status = await client.tasks.get_task(regular_task.id) + regular_status = await client.tasks.get_task(task.id) print(f"Regular Task Status: {regular_status.status}") if regular_status.status == "finished": print(f"Regular Task Output: {regular_status.output}") @@ -55,7 +55,7 @@ class HackerNewsPost(BaseModel): class SearchResult(BaseModel): posts: List[HackerNewsPost] - structured_task = await client.tasks.create_task( + task = await client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, @@ -63,10 +63,10 @@ class SearchResult(BaseModel): schema=SearchResult, ) - print(f"Structured Task ID: {structured_task.id}") + print(f"Structured Task ID: {task.id}") while True: - structured_status = await client.tasks.retrieve(task_id=structured_task.id, schema=SearchResult) + structured_status = await client.tasks.get_task(task_id=task.id, schema=SearchResult) print(f"Structured Task Status: {structured_status.status}") if structured_status.status == "finished": diff --git a/examples/async_run.py b/examples/async_run.py index 3f6e07b..da1ef1e 100755 --- a/examples/async_run.py +++ b/examples/async_run.py @@ -43,16 +43,16 @@ class SearchResult(BaseModel): Find top 10 Hacker News articles and return the title and url. """, llm="gpt-4.1", - structured_output_json=SearchResult, + schema=SearchResult, ) print(f"Structured Task ID: {task.id}") result = await task.complete() - if result.parsed is not None: + if result.parsed_output is not None: print("Structured Task Output:") - for post in result.parsed.posts: + for post in result.parsed_output.posts: print(f" - {post.title} - {post.url}") print("Structured Task Done") diff --git a/examples/async_stream.py b/examples/async_stream.py index fa75555..f084851 100755 --- a/examples/async_stream.py +++ b/examples/async_stream.py @@ -13,23 +13,17 @@ # Regular Task async def stream_regular_task() -> None: - regular_task = await client.tasks.create( + task = await client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, llm="gemini-2.5-flash", ) - print(f"Regular Task ID: {regular_task.id}") + print(f"Regular Task ID: {task.id}") - async for res in client.tasks.stream(regular_task.id): - print(f"Regular Task Status: {res.status}") - - if len(res.steps) > 0: - last_step = res.steps[-1] - print(f"Regular Task Step: {last_step.url} ({last_step.next_goal})") - for action in last_step.actions: - print(f" - Regular Task Action: {action}") + async for step in task.stream(): + print(f"Regular Task Status: {step.number}") print("Regular Task Done") @@ -43,26 +37,24 @@ class HackerNewsPost(BaseModel): class SearchResult(BaseModel): posts: List[HackerNewsPost] - structured_task = await client.tasks.create( + task = await client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, llm="gpt-4.1", - structured_output_json=SearchResult, + schema=SearchResult, ) - print(f"Structured Task ID: {structured_task.id}") + print(f"Structured Task ID: {task.id}") + + async for step in task.stream(): + print(f"Structured Task Step {step.number}: {step.url} ({step.next_goal})") - async for res in client.tasks.stream(structured_task.id, structured_output_json=SearchResult): - print(f"Structured Task Status: {res.status}") + result = await task.complete() - if res.status == "finished": - if res.parsed_output is None: - print("Structured Task No output") - else: - for post in res.parsed_output.posts: - print(f" - Structured Task Post: {post.title} - {post.url}") - break + if result.parsed_output is not None: + for post in result.parsed_output.posts: + print(f" - {post.title} - {post.url}") print("Structured Task Done") diff --git a/examples/retrieve.py b/examples/retrieve.py index a942613..47e3732 100755 --- a/examples/retrieve.py +++ b/examples/retrieve.py @@ -19,17 +19,17 @@ def retrieve_regular_task() -> None: print("Retrieving regular task...") - regular_task = client.tasks.create_task( + task = client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, llm="gemini-2.5-flash", ) - print(f"Task ID: {regular_task.id}") + print(f"Task ID: {task.id}") while True: - regular_status = client.tasks.get_task(regular_task.id) + regular_status = client.tasks.get_task(task.id) print(regular_status.status) if regular_status.status == "finished": print(regular_status.output) @@ -58,7 +58,7 @@ class HackerNewsPost(BaseModel): class SearchResult(BaseModel): posts: List[HackerNewsPost] - structured_task = client.tasks.create_task( + task = client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, @@ -66,17 +66,17 @@ class SearchResult(BaseModel): schema=SearchResult, ) - print(f"Task ID: {structured_task.id}") + print(f"Task ID: {task.id}") while True: - structured_status = client.tasks.get_task(task_id=structured_task.id, schema=SearchResult) + structured_status = client.tasks.get_task(task_id=task.id, schema=SearchResult) print(structured_status.status) if structured_status.status == "finished": - if structured_status.parsed is None: + if structured_status.parsed_output is None: print("No output") else: - for post in structured_status.parsed.posts: + for post in structured_status.parsed_output.posts: print(f" - {post.title} - {post.url}") break diff --git a/examples/run.py b/examples/run.py index c18cec4..a15f0f3 100755 --- a/examples/run.py +++ b/examples/run.py @@ -40,18 +40,20 @@ class HackerNewsPost(BaseModel): class SearchResult(BaseModel): posts: List[HackerNewsPost] - structured_result = client.tasks.create_task( + task = client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, llm="gpt-4.1", - structured_output_json=SearchResult, + schema=SearchResult, ) - print(f"Task ID: {structured_result.id}") + print(f"Task ID: {task.id}") + + result = task.complete() - if structured_result.parsed_output is not None: - for post in structured_result.parsed_output.posts: + if result.parsed_output is not None: + for post in result.parsed_output.posts: print(f" - {post.title} - {post.url}") print("Done") diff --git a/examples/stream.py b/examples/stream.py index a8c5b48..c20cee4 100755 --- a/examples/stream.py +++ b/examples/stream.py @@ -21,17 +21,8 @@ def stream_regular_task() -> None: print(f"Task ID: {task.id}") - for res in task.stream(): - print(res.status) - - if len(res.steps) > 0: - last_step = res.steps[-1] - print(f"{last_step.url} ({last_step.next_goal})") - for action in last_step.actions: - print(f" - {action}") - - if res.status == "finished": - print(res.done_output) + for step in task.stream(): + print(f"Step {step.number}: {step.url} ({step.next_goal})") print("Regular: DONE") @@ -53,21 +44,19 @@ class SearchResult(BaseModel): Find top 10 Hacker News articles and return the title and url. """, llm="gpt-4.1", - structured_output_json=SearchResult, + schema=SearchResult, ) print(f"Task ID: {task.id}") - for res in task.stream(): - print(res.status) + for step in task.stream(): + print(f"Step {step.number}: {step.url} ({step.next_goal})") + + result = task.complete() - if res.status == "finished": - if res.parsed_output is None: - print("No output") - else: - for post in res.parsed_output.posts: - print(f" - {post.title} - {post.url}") - break + if result.parsed_output is not None: + for post in result.parsed_output.posts: + print(f" - {post.title} - {post.url}") print("Done") diff --git a/src/browser_use/wrapper/browser_use_client.py b/src/browser_use/wrapper/browser_use_client.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/browser_use/wrapper/parse.py b/src/browser_use/wrapper/parse.py index 930fd39..8c079c4 100644 --- a/src/browser_use/wrapper/parse.py +++ b/src/browser_use/wrapper/parse.py @@ -1,11 +1,15 @@ import hashlib import json +import typing from datetime import datetime -from typing import Any, Generic, TypeVar, Union +from typing import Any, AsyncIterator, Generic, Iterator, TypeVar, Union from pydantic import BaseModel +from browser_use.core.request_options import RequestOptions +from browser_use.tasks.client import AsyncTasksClient, TasksClient from browser_use.types.task_created_response import TaskCreatedResponse +from browser_use.types.task_step_view import TaskStepView from browser_use.types.task_view import TaskView T = TypeVar("T", bound=BaseModel) @@ -36,40 +40,37 @@ def hash_task_view(task_view: TaskView) -> str: ).hexdigest() -def _watch( - self, - task_id: str, - interval: float = 1, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, -) -> Iterator[TaskView]: - """Converts a polling loop into a generator loop.""" - hash: str | None = None +# def _watch( +# self, +# task_id: str, +# interval: float = 1, request_options:typing.Optional[RequestOptions] = None, +# *, +# # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. +# # The extra values given here take precedence over values defined on the client or passed to this method. +# request_options: typing.Optional[RequestOptions] = None, +# ) -> Iterator[TaskView]: +# """Converts a polling loop into a generator loop.""" +# hash: str | None = None - while True: - res = self.retrieve( - task_id=task_id, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) +# while True: +# res = self.retrieve( +# task_id=task_id, +# extra_headers=extra_headers, +# extra_query=extra_query, +# extra_body=extra_body, +# timeout=timeout, +# ) - res_hash = hash_task_view(res) +# res_hash = hash_task_view(res) - if hash is None or res_hash != hash: - hash = res_hash - yield res +# if hash is None or res_hash != hash: +# hash = res_hash +# yield res - if res.status == "finished": - break +# if res.status == "finished": +# break - time.sleep(interval) +# time.sleep(interval) # Sync ----------------------------------------------------------------------- @@ -78,20 +79,63 @@ def _watch( class WrappedTaskCreatedResponse(TaskCreatedResponse): """TaskCreatedResponse with utility methods for easier interfacing with Browser Use Cloud.""" - def __init__(self, id: str): + def __init__(self, id: str, client: TasksClient): super().__init__() self.id = id + def complete( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> TaskViewWithOutput[T]: + """Waits for the task to finish and return the result.""" + pass -class WrappedStructuredTaskCreatedResponse(TaskCreatedResponse): + def stream( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> Iterator[TaskStepView]: + """Streams the steps of the task and closes when the task is finished.""" + for i in range(10): + yield TaskStepView(number=i, status="finished") + + def watch( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> Iterator[TaskViewWithOutput[T]]: + """Yields the latest task state on every change.""" + for i in range(10): + yield TaskViewWithOutput[T](status="finished") + + +# Structured + + +class WrappedStructuredTaskCreatedResponse(TaskCreatedResponse, Generic[T]): """TaskCreatedResponse with structured output.""" - def __init__(self, id: str, schema: type[T], client: BrowserUse): + def __init__(self, id: str, schema: type[T], client: TasksClient): super().__init__() self.id = id self._schema = schema + def complete( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> TaskViewWithOutput[T]: + """Waits for the task to finish and return the result.""" + pass + + def stream( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> Iterator[TaskStepView]: + """Streams the steps of the task and closes when the task is finished.""" + for i in range(10): + yield TaskStepView(number=i, status="finished") + + def watch( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> Iterator[TaskViewWithOutput[T]]: + """Yields the latest task state on every change.""" + for i in range(10): + yield TaskViewWithOutput[T](status="finished") + # Async ---------------------------------------------------------------------- @@ -99,16 +143,57 @@ def __init__(self, id: str, schema: type[T], client: BrowserUse): class AsyncWrappedTaskCreatedResponse(TaskCreatedResponse): """TaskCreatedResponse with utility methods for easier interfacing with Browser Use Cloud.""" - def __init__(self, id: str): + def __init__(self, id: str, client: AsyncTasksClient): super().__init__() self.id = id + async def complete(self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None) -> TaskView: + """Waits for the task to finish and return the result.""" + pass + + async def stream( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncIterator[TaskStepView]: + """Streams the steps of the task and closes when the task is finished.""" + for i in range(10): + yield TaskStepView(number=i, status="finished") + + async def watch( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncIterator[TaskView]: + """Yields the latest task state on every change.""" + for i in range(10): + yield TaskView(status="finished") + -class AsyncWrappedStructuredTaskCreatedResponse(TaskCreatedResponse): +# Structured + + +class AsyncWrappedStructuredTaskCreatedResponse(TaskCreatedResponse, Generic[T]): """TaskCreatedResponse with structured output.""" - def __init__(self, id: str, schema: type[T], client: BrowserUse): + def __init__(self, id: str, schema: type[T], client: AsyncTasksClient): super().__init__() self.id = id self._schema = schema + + async def complete( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> TaskViewWithOutput[T]: + """Waits for the task to finish and return the result.""" + pass + + async def stream( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncIterator[TaskStepView]: + """Streams the steps of the task and closes when the task is finished.""" + for i in range(10): + yield TaskStepView(number=i, status="finished") + + async def watch( + self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncIterator[TaskViewWithOutput[T]]: + """Yields the latest task state on every change.""" + for i in range(10): + yield TaskViewWithOutput[T](status="finished") diff --git a/src/browser_use/wrapper/tasks/client.py b/src/browser_use/wrapper/tasks/client.py index ef44007..b957649 100644 --- a/src/browser_use/wrapper/tasks/client.py +++ b/src/browser_use/wrapper/tasks/client.py @@ -63,7 +63,7 @@ def create_task( vision: typing.Optional[bool] = OMIT, system_prompt_extension: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None, - ) -> WrappedStructuredTaskCreatedResponse[T]: ... + ) -> WrappedTaskCreatedResponse: ... def create_task( self, From 9185adffb3c8929a944704fc74beab01e3f9a0f1 Mon Sep 17 00:00:00 2001 From: Matic Zavadlal Date: Tue, 2 Sep 2025 12:02:12 +0200 Subject: [PATCH 06/13] fixes --- examples/async_retrieve.py | 4 ++-- examples/async_run.py | 3 ++- examples/async_stream.py | 4 ++-- examples/retrieve.py | 4 ++-- examples/run.py | 4 ++-- examples/stream.py | 4 ++-- src/browser_use/wrapper/parse.py | 16 ++++++++-------- src/browser_use/wrapper/tasks/client.py | 6 ++++-- 8 files changed, 24 insertions(+), 21 deletions(-) diff --git a/examples/async_retrieve.py b/examples/async_retrieve.py index ff46823..5d2e2b3 100755 --- a/examples/async_retrieve.py +++ b/examples/async_retrieve.py @@ -1,14 +1,14 @@ #!/usr/bin/env -S rye run python import asyncio +import os from typing import List from pydantic import BaseModel from browser_use import AsyncBrowserUse -# gets API Key from environment variable BROWSER_USE_API_KEY -client = AsyncBrowserUse() +client = AsyncBrowserUse(api_key=os.getenv("BROWSER_USE_API_KEY")) # Regular Task diff --git a/examples/async_run.py b/examples/async_run.py index da1ef1e..c43a969 100755 --- a/examples/async_run.py +++ b/examples/async_run.py @@ -1,6 +1,7 @@ #!/usr/bin/env -S rye run python import asyncio +import os from typing import List from pydantic import BaseModel @@ -8,7 +9,7 @@ from browser_use import AsyncBrowserUse # gets API Key from environment variable BROWSER_USE_API_KEY -client = AsyncBrowserUse() +client = AsyncBrowserUse(api_key=os.getenv("BROWSER_USE_API_KEY")) # Regular Task diff --git a/examples/async_stream.py b/examples/async_stream.py index f084851..c604f6d 100755 --- a/examples/async_stream.py +++ b/examples/async_stream.py @@ -1,14 +1,14 @@ #!/usr/bin/env -S rye run python import asyncio +import os from typing import List from pydantic import BaseModel from browser_use import AsyncBrowserUse -# gets API Key from environment variable BROWSER_USE_API_KEY -client = AsyncBrowserUse() +client = AsyncBrowserUse(api_key=os.getenv("BROWSER_USE_API_KEY")) # Regular Task diff --git a/examples/retrieve.py b/examples/retrieve.py index 47e3732..9882a05 100755 --- a/examples/retrieve.py +++ b/examples/retrieve.py @@ -1,5 +1,6 @@ #!/usr/bin/env -S rye run python +import os import time from typing import List @@ -7,8 +8,7 @@ from browser_use import BrowserUse -# gets API Key from environment variable BROWSER_USE_API_KEY -client = BrowserUse() +client = BrowserUse(api_key=os.getenv("BROWSER_USE_API_KEY")) # Regular Task diff --git a/examples/run.py b/examples/run.py index a15f0f3..a6655e8 100755 --- a/examples/run.py +++ b/examples/run.py @@ -1,13 +1,13 @@ #!/usr/bin/env -S rye run python +import os from typing import List from pydantic import BaseModel from browser_use import BrowserUse -# gets API Key from environment variable BROWSER_USE_API_KEY -client = BrowserUse() +client = BrowserUse(api_key=os.getenv("BROWSER_USE_API_KEY")) # Regular Task diff --git a/examples/stream.py b/examples/stream.py index c20cee4..4fa37ba 100755 --- a/examples/stream.py +++ b/examples/stream.py @@ -1,13 +1,13 @@ #!/usr/bin/env -S rye run python +import os from typing import List from pydantic import BaseModel from browser_use import BrowserUse -# gets API Key from environment variable BROWSER_USE_API_KEY -client = BrowserUse() +client = BrowserUse(api_key=os.getenv("BROWSER_USE_API_KEY")) # Regular Task diff --git a/src/browser_use/wrapper/parse.py b/src/browser_use/wrapper/parse.py index 8c079c4..edbeb0b 100644 --- a/src/browser_use/wrapper/parse.py +++ b/src/browser_use/wrapper/parse.py @@ -80,8 +80,8 @@ class WrappedTaskCreatedResponse(TaskCreatedResponse): """TaskCreatedResponse with utility methods for easier interfacing with Browser Use Cloud.""" def __init__(self, id: str, client: TasksClient): - super().__init__() - self.id = id + super().__init__(id=id) + self._client = client def complete( self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None @@ -111,9 +111,9 @@ class WrappedStructuredTaskCreatedResponse(TaskCreatedResponse, Generic[T]): """TaskCreatedResponse with structured output.""" def __init__(self, id: str, schema: type[T], client: TasksClient): - super().__init__() + super().__init__(id=id) - self.id = id + self._client = client self._schema = schema def complete( @@ -144,8 +144,8 @@ class AsyncWrappedTaskCreatedResponse(TaskCreatedResponse): """TaskCreatedResponse with utility methods for easier interfacing with Browser Use Cloud.""" def __init__(self, id: str, client: AsyncTasksClient): - super().__init__() - self.id = id + super().__init__(id=id) + self._client = client async def complete(self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None) -> TaskView: """Waits for the task to finish and return the result.""" @@ -173,9 +173,9 @@ class AsyncWrappedStructuredTaskCreatedResponse(TaskCreatedResponse, Generic[T]) """TaskCreatedResponse with structured output.""" def __init__(self, id: str, schema: type[T], client: AsyncTasksClient): - super().__init__() + super().__init__(id=id) - self.id = id + self._client = client self._schema = schema async def complete( diff --git a/src/browser_use/wrapper/tasks/client.py b/src/browser_use/wrapper/tasks/client.py index b957649..b0e8717 100644 --- a/src/browser_use/wrapper/tasks/client.py +++ b/src/browser_use/wrapper/tasks/client.py @@ -105,6 +105,7 @@ def create_task( system_prompt_extension=system_prompt_extension, request_options=request_options, ) + return WrappedStructuredTaskCreatedResponse[T](id=res.id, schema=schema, client=self) else: @@ -125,6 +126,7 @@ def create_task( system_prompt_extension=system_prompt_extension, request_options=request_options, ) + return WrappedTaskCreatedResponse(id=res.id, client=self) @typing.overload @@ -222,7 +224,7 @@ async def create_task( if schema is not None: structured_output = json.dumps(schema.model_json_schema()) - res = super().create_task( + res = await super().create_task( task=task, llm=llm, start_url=start_url, @@ -242,7 +244,7 @@ async def create_task( return AsyncWrappedStructuredTaskCreatedResponse[T](id=res.id, schema=schema, client=self) else: - res = super().create_task( + res = await super().create_task( task=task, llm=llm, start_url=start_url, From d9ca12a509258235d8da1d492875b4e950de1f0b Mon Sep 17 00:00:00 2001 From: Matic Zavadlal Date: Tue, 2 Sep 2025 12:07:59 +0200 Subject: [PATCH 07/13] fixes --- src/browser_use/wrapper/parse.py | 6 +++--- src/browser_use/wrapper/tasks/client.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/browser_use/wrapper/parse.py b/src/browser_use/wrapper/parse.py index edbeb0b..a66addc 100644 --- a/src/browser_use/wrapper/parse.py +++ b/src/browser_use/wrapper/parse.py @@ -2,7 +2,7 @@ import json import typing from datetime import datetime -from typing import Any, AsyncIterator, Generic, Iterator, TypeVar, Union +from typing import Any, AsyncIterator, Generic, Iterator, Type, TypeVar, Union from pydantic import BaseModel @@ -110,7 +110,7 @@ def watch( class WrappedStructuredTaskCreatedResponse(TaskCreatedResponse, Generic[T]): """TaskCreatedResponse with structured output.""" - def __init__(self, id: str, schema: type[T], client: TasksClient): + def __init__(self, id: str, schema: Type[T], client: TasksClient): super().__init__(id=id) self._client = client @@ -172,7 +172,7 @@ async def watch( class AsyncWrappedStructuredTaskCreatedResponse(TaskCreatedResponse, Generic[T]): """TaskCreatedResponse with structured output.""" - def __init__(self, id: str, schema: type[T], client: AsyncTasksClient): + def __init__(self, id: str, schema: Type[T], client: AsyncTasksClient): super().__init__(id=id) self._client = client diff --git a/src/browser_use/wrapper/tasks/client.py b/src/browser_use/wrapper/tasks/client.py index b0e8717..f893160 100644 --- a/src/browser_use/wrapper/tasks/client.py +++ b/src/browser_use/wrapper/tasks/client.py @@ -31,7 +31,7 @@ def create_task( llm: typing.Optional[SupportedLlMs] = OMIT, start_url: typing.Optional[str] = OMIT, max_steps: typing.Optional[int] = OMIT, - schema: type[T], + schema: typing.Type[T], session_id: typing.Optional[str] = OMIT, metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, @@ -73,7 +73,7 @@ def create_task( start_url: typing.Optional[str] = OMIT, max_steps: typing.Optional[int] = OMIT, structured_output: typing.Optional[str] = OMIT, - schema: typing.Optional[type[BaseModel]] = OMIT, + schema: typing.Optional[typing.Type[BaseModel]] = OMIT, session_id: typing.Optional[str] = OMIT, metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, @@ -131,7 +131,7 @@ def create_task( @typing.overload def get_task( - self, task_id: str, schema: type[T], *, request_options: typing.Optional[RequestOptions] = None + self, task_id: str, schema: typing.Type[T], *, request_options: typing.Optional[RequestOptions] = None ) -> TaskViewWithOutput[T]: ... @typing.overload @@ -140,7 +140,7 @@ def get_task(self, task_id: str, *, request_options: typing.Optional[RequestOpti def get_task( self, task_id: str, - schema: typing.Optional[typing.Union[type[BaseModel], str]] = OMIT, + schema: typing.Optional[typing.Union[typing.Type[BaseModel], str]] = OMIT, *, request_options: typing.Optional[RequestOptions] = None, ) -> typing.Union[TaskViewWithOutput[T], TaskView]: @@ -167,7 +167,7 @@ async def create_task( llm: typing.Optional[SupportedLlMs] = OMIT, start_url: typing.Optional[str] = OMIT, max_steps: typing.Optional[int] = OMIT, - schema: type[T], + schema: typing.Type[T], session_id: typing.Optional[str] = OMIT, metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, @@ -209,7 +209,7 @@ async def create_task( start_url: typing.Optional[str] = OMIT, max_steps: typing.Optional[int] = OMIT, structured_output: typing.Optional[str] = OMIT, - schema: typing.Optional[type[BaseModel]] = OMIT, + schema: typing.Optional[typing.Type[BaseModel]] = OMIT, session_id: typing.Optional[str] = OMIT, metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, @@ -265,7 +265,7 @@ async def create_task( @typing.overload async def get_task( - self, task_id: str, schema: type[T], *, request_options: typing.Optional[RequestOptions] = None + self, task_id: str, schema: typing.Type[T], *, request_options: typing.Optional[RequestOptions] = None ) -> TaskViewWithOutput[T]: ... @typing.overload @@ -274,7 +274,7 @@ async def get_task(self, task_id: str, *, request_options: typing.Optional[Reque async def get_task( self, task_id: str, - schema: typing.Optional[typing.Union[type[BaseModel], str]] = OMIT, + schema: typing.Optional[typing.Union[typing.Type[BaseModel], str]] = OMIT, *, request_options: typing.Optional[RequestOptions] = None, ) -> typing.Union[TaskViewWithOutput[T], TaskView]: From 84d8bd23e4577115ead64944703a77cb590717f3 Mon Sep 17 00:00:00 2001 From: Matic Zavadlal Date: Tue, 2 Sep 2025 12:12:48 +0200 Subject: [PATCH 08/13] Update client.py --- src/browser_use/wrapper/tasks/client.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/browser_use/wrapper/tasks/client.py b/src/browser_use/wrapper/tasks/client.py index f893160..5abb12e 100644 --- a/src/browser_use/wrapper/tasks/client.py +++ b/src/browser_use/wrapper/tasks/client.py @@ -73,7 +73,7 @@ def create_task( start_url: typing.Optional[str] = OMIT, max_steps: typing.Optional[int] = OMIT, structured_output: typing.Optional[str] = OMIT, - schema: typing.Optional[typing.Type[BaseModel]] = OMIT, + schema: typing.Optional[typing.Type[T]] = OMIT, session_id: typing.Optional[str] = OMIT, metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, @@ -140,15 +140,17 @@ def get_task(self, task_id: str, *, request_options: typing.Optional[RequestOpti def get_task( self, task_id: str, - schema: typing.Optional[typing.Union[typing.Type[BaseModel], str]] = OMIT, + schema: typing.Optional[typing.Union[typing.Type[T], str]] = OMIT, *, request_options: typing.Optional[RequestOptions] = None, ) -> typing.Union[TaskViewWithOutput[T], TaskView]: res = super().get_task(task_id, request_options=request_options) if schema is not None: - parsed_output = schema.model_validate_json(res.output) - return TaskViewWithOutput(**res.model_dump(), parsed_output=parsed_output) + if res.output is None: + return TaskViewWithOutput[T](**res.model_dump(), parsed_output=None) + + return TaskViewWithOutput[T](**res.model_dump(), parsed_output=schema.model_validate_json(res.output)) else: return res @@ -281,7 +283,9 @@ async def get_task( res = await super().get_task(task_id, request_options=request_options) if schema is not None: - parsed_output = schema.model_validate_json(res.output) - return TaskViewWithOutput(**res.model_dump(), parsed_output=parsed_output) + if res.output is None: + return TaskViewWithOutput[T](**res.model_dump(), parsed_output=None) + + return TaskViewWithOutput[T](**res.model_dump(), parsed_output=schema.model_validate_json(res.output)) else: return res From bef677e721b436f049e0d5855d5955398d9632a8 Mon Sep 17 00:00:00 2001 From: Matic Zavadlal Date: Tue, 2 Sep 2025 12:17:08 +0200 Subject: [PATCH 09/13] Update client.py --- src/browser_use/wrapper/tasks/client.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/browser_use/wrapper/tasks/client.py b/src/browser_use/wrapper/tasks/client.py index 5abb12e..6286c0c 100644 --- a/src/browser_use/wrapper/tasks/client.py +++ b/src/browser_use/wrapper/tasks/client.py @@ -1,8 +1,6 @@ import json import typing -from pydantic import BaseModel - from browser_use.core.request_options import RequestOptions from browser_use.tasks.client import OMIT, AsyncClientWrapper, AsyncTasksClient, SyncClientWrapper, TasksClient from browser_use.types.supported_ll_ms import SupportedLlMs @@ -140,7 +138,7 @@ def get_task(self, task_id: str, *, request_options: typing.Optional[RequestOpti def get_task( self, task_id: str, - schema: typing.Optional[typing.Union[typing.Type[T], str]] = OMIT, + schema: typing.Optional[typing.Type[T]] = OMIT, *, request_options: typing.Optional[RequestOptions] = None, ) -> typing.Union[TaskViewWithOutput[T], TaskView]: @@ -211,7 +209,7 @@ async def create_task( start_url: typing.Optional[str] = OMIT, max_steps: typing.Optional[int] = OMIT, structured_output: typing.Optional[str] = OMIT, - schema: typing.Optional[typing.Type[BaseModel]] = OMIT, + schema: typing.Optional[typing.Type[T]] = OMIT, session_id: typing.Optional[str] = OMIT, metadata: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, secrets: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, @@ -276,7 +274,7 @@ async def get_task(self, task_id: str, *, request_options: typing.Optional[Reque async def get_task( self, task_id: str, - schema: typing.Optional[typing.Union[typing.Type[BaseModel], str]] = OMIT, + schema: typing.Optional[typing.Type[T]] = OMIT, *, request_options: typing.Optional[RequestOptions] = None, ) -> typing.Union[TaskViewWithOutput[T], TaskView]: From 2dea4ac9f0e4f78c346f2cca852370165ef845bb Mon Sep 17 00:00:00 2001 From: Matic Zavadlal Date: Tue, 2 Sep 2025 12:32:46 +0200 Subject: [PATCH 10/13] add implementation --- src/browser_use/wrapper/parse.py | 142 ++++++++++++++++-------- src/browser_use/wrapper/tasks/client.py | 11 +- 2 files changed, 97 insertions(+), 56 deletions(-) diff --git a/src/browser_use/wrapper/parse.py b/src/browser_use/wrapper/parse.py index a66addc..d2fc022 100644 --- a/src/browser_use/wrapper/parse.py +++ b/src/browser_use/wrapper/parse.py @@ -1,5 +1,7 @@ +import asyncio import hashlib import json +import time import typing from datetime import datetime from typing import Any, AsyncIterator, Generic, Iterator, Type, TypeVar, Union @@ -33,47 +35,52 @@ def default(self, o: Any) -> Any: # type: ignore[override] return super().default(o) -def hash_task_view(task_view: TaskView) -> str: +def _hash_task_view(task_view: TaskView) -> str: """Hashes the task view to detect changes.""" return hashlib.sha256( json.dumps(task_view.model_dump(), sort_keys=True, cls=CustomJSONEncoder).encode() ).hexdigest() -# def _watch( -# self, -# task_id: str, -# interval: float = 1, request_options:typing.Optional[RequestOptions] = None, -# *, -# # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. -# # The extra values given here take precedence over values defined on the client or passed to this method. -# request_options: typing.Optional[RequestOptions] = None, -# ) -> Iterator[TaskView]: -# """Converts a polling loop into a generator loop.""" -# hash: str | None = None +def _parse_task_view_with_output(task_view: TaskView, schema: Type[T]) -> TaskViewWithOutput[T]: + """Parses the task view with output.""" + if task_view.output is None: + return TaskViewWithOutput[T](**task_view.model_dump(), parsed_output=None) -# while True: -# res = self.retrieve( -# task_id=task_id, -# extra_headers=extra_headers, -# extra_query=extra_query, -# extra_body=extra_body, -# timeout=timeout, -# ) + return TaskViewWithOutput[T](**task_view.model_dump(), parsed_output=schema.model_validate_json(task_view.output)) -# res_hash = hash_task_view(res) -# if hash is None or res_hash != hash: -# hash = res_hash -# yield res +# Sync ----------------------------------------------------------------------- -# if res.status == "finished": -# break -# time.sleep(interval) +def _watch( + client: TasksClient, task_id: str, interval: float = 1, request_options: typing.Optional[RequestOptions] = None +) -> Iterator[TaskViewWithOutput[T]]: + """Yields the latest task state on every change.""" + hash: str | None = None + while True: + res = client.get_task(task_id, request_options=request_options) + res_hash = _hash_task_view(res) + if hash is None or res_hash != hash: + hash = res_hash + yield res + + if res.status == "finished" or res.status == "stopped" or res.status == "paused": + break + + time.sleep(interval) -# Sync ----------------------------------------------------------------------- + +def _stream( + client: TasksClient, task_id: str, interval: float = 1, request_options: typing.Optional[RequestOptions] = None +) -> Iterator[TaskStepView]: + """Streams the steps of the task and closes when the task is finished.""" + total_steps = 0 + for state in _watch(client, task_id, interval, request_options): + for i in range(total_steps, len(state.steps)): + total_steps = i + 1 + yield state.steps[i] class WrappedTaskCreatedResponse(TaskCreatedResponse): @@ -87,21 +94,23 @@ def complete( self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None ) -> TaskViewWithOutput[T]: """Waits for the task to finish and return the result.""" - pass + for state in _watch(self._client, self.id, interval, request_options): + if state.status == "finished" or state.status == "stopped" or state.status == "paused": + return state + + raise Exception("Iterator ended without finding a finished state!") def stream( self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None ) -> Iterator[TaskStepView]: """Streams the steps of the task and closes when the task is finished.""" - for i in range(10): - yield TaskStepView(number=i, status="finished") + return _stream(self._client, self.id, interval, request_options) def watch( self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None ) -> Iterator[TaskViewWithOutput[T]]: """Yields the latest task state on every change.""" - for i in range(10): - yield TaskViewWithOutput[T](status="finished") + return _watch(self._client, self.id, interval, request_options) # Structured @@ -120,26 +129,58 @@ def complete( self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None ) -> TaskViewWithOutput[T]: """Waits for the task to finish and return the result.""" - pass + for state in _watch(self._client, self.id, interval, request_options): + if state.status == "finished" or state.status == "stopped" or state.status == "paused": + return _parse_task_view_with_output(state, self._schema) + + raise Exception("Iterator ended without finding a finished state!") def stream( self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None ) -> Iterator[TaskStepView]: """Streams the steps of the task and closes when the task is finished.""" - for i in range(10): - yield TaskStepView(number=i, status="finished") + return _stream(self._client, self.id, interval, request_options) def watch( self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None ) -> Iterator[TaskViewWithOutput[T]]: """Yields the latest task state on every change.""" - for i in range(10): - yield TaskViewWithOutput[T](status="finished") + for state in _watch(self._client, self.id, interval, request_options): + yield _parse_task_view_with_output(state, self._schema) # Async ---------------------------------------------------------------------- +async def _async_watch( + client: AsyncTasksClient, task_id: str, interval: float = 1, request_options: typing.Optional[RequestOptions] = None +) -> AsyncIterator[TaskViewWithOutput[T]]: + """Yields the latest task state on every change.""" + hash: str | None = None + while True: + res = await client.get_task(task_id, request_options=request_options) + res_hash = _hash_task_view(res) + if hash is None or res_hash != hash: + hash = res_hash + yield res + + if res.status == "finished" or res.status == "stopped" or res.status == "paused": + break + + await asyncio.sleep(interval) + + +async def _async_stream( + client: AsyncTasksClient, task_id: str, interval: float = 1, request_options: typing.Optional[RequestOptions] = None +) -> AsyncIterator[TaskStepView]: + """Streams the steps of the task and closes when the task is finished.""" + total_steps = 0 + for state in _async_watch(client, task_id, interval, request_options): + for i in range(total_steps, len(state.steps)): + total_steps = i + 1 + yield state.steps[i] + + class AsyncWrappedTaskCreatedResponse(TaskCreatedResponse): """TaskCreatedResponse with utility methods for easier interfacing with Browser Use Cloud.""" @@ -149,21 +190,23 @@ def __init__(self, id: str, client: AsyncTasksClient): async def complete(self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None) -> TaskView: """Waits for the task to finish and return the result.""" - pass + for state in _async_watch(self._client, self.id, interval, request_options): + if state.status == "finished" or state.status == "stopped" or state.status == "paused": + return state + + raise Exception("Iterator ended without finding a finished state!") async def stream( self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None ) -> AsyncIterator[TaskStepView]: """Streams the steps of the task and closes when the task is finished.""" - for i in range(10): - yield TaskStepView(number=i, status="finished") + return _async_stream(self._client, self.id, interval, request_options) async def watch( self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None ) -> AsyncIterator[TaskView]: """Yields the latest task state on every change.""" - for i in range(10): - yield TaskView(status="finished") + return _async_watch(self._client, self.id, interval, request_options) # Structured @@ -182,18 +225,21 @@ async def complete( self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None ) -> TaskViewWithOutput[T]: """Waits for the task to finish and return the result.""" - pass + for state in _async_watch(self._client, self.id, interval, request_options): + if state.status == "finished" or state.status == "stopped" or state.status == "paused": + return _parse_task_view_with_output(state, self._schema) + + raise Exception("Iterator ended without finding a finished state!") async def stream( self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None ) -> AsyncIterator[TaskStepView]: """Streams the steps of the task and closes when the task is finished.""" - for i in range(10): - yield TaskStepView(number=i, status="finished") + return _async_stream(self._client, self.id, interval, request_options) async def watch( self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None ) -> AsyncIterator[TaskViewWithOutput[T]]: """Yields the latest task state on every change.""" - for i in range(10): - yield TaskViewWithOutput[T](status="finished") + for state in _async_watch(self._client, self.id, interval, request_options): + yield _parse_task_view_with_output(state, self._schema) diff --git a/src/browser_use/wrapper/tasks/client.py b/src/browser_use/wrapper/tasks/client.py index 6286c0c..8b583ac 100644 --- a/src/browser_use/wrapper/tasks/client.py +++ b/src/browser_use/wrapper/tasks/client.py @@ -12,6 +12,7 @@ TaskViewWithOutput, WrappedStructuredTaskCreatedResponse, WrappedTaskCreatedResponse, + _parse_task_view_with_output, ) @@ -145,10 +146,7 @@ def get_task( res = super().get_task(task_id, request_options=request_options) if schema is not None: - if res.output is None: - return TaskViewWithOutput[T](**res.model_dump(), parsed_output=None) - - return TaskViewWithOutput[T](**res.model_dump(), parsed_output=schema.model_validate_json(res.output)) + return _parse_task_view_with_output(res, schema) else: return res @@ -281,9 +279,6 @@ async def get_task( res = await super().get_task(task_id, request_options=request_options) if schema is not None: - if res.output is None: - return TaskViewWithOutput[T](**res.model_dump(), parsed_output=None) - - return TaskViewWithOutput[T](**res.model_dump(), parsed_output=schema.model_validate_json(res.output)) + return _parse_task_view_with_output(res, schema) else: return res From 13408350e8fda16276eeee292f963b96131e7c9b Mon Sep 17 00:00:00 2001 From: Matic Zavadlal Date: Tue, 2 Sep 2025 12:46:28 +0200 Subject: [PATCH 11/13] fixes --- examples/async_retrieve.py | 6 ++--- examples/async_run.py | 7 +++--- examples/async_stream.py | 6 ++--- examples/retrieve.py | 6 ++--- examples/run.py | 6 ++--- examples/stream.py | 6 ++--- examples/utils.py | 7 ++++++ examples/webhooks.py | 2 +- poetry.lock | 38 +++++++++++++++++++++++++++----- src/browser_use/wrapper/parse.py | 28 ++++++++++------------- 10 files changed, 71 insertions(+), 41 deletions(-) create mode 100644 examples/utils.py diff --git a/examples/async_retrieve.py b/examples/async_retrieve.py index 5d2e2b3..d999494 100755 --- a/examples/async_retrieve.py +++ b/examples/async_retrieve.py @@ -1,14 +1,14 @@ -#!/usr/bin/env -S rye run python +#!/usr/bin/env -S poetry run python import asyncio -import os from typing import List from pydantic import BaseModel +from utils import API_KEY from browser_use import AsyncBrowserUse -client = AsyncBrowserUse(api_key=os.getenv("BROWSER_USE_API_KEY")) +client = AsyncBrowserUse(api_key=API_KEY) # Regular Task diff --git a/examples/async_run.py b/examples/async_run.py index c43a969..ad60894 100755 --- a/examples/async_run.py +++ b/examples/async_run.py @@ -1,15 +1,14 @@ -#!/usr/bin/env -S rye run python +#!/usr/bin/env -S poetry run python import asyncio -import os from typing import List from pydantic import BaseModel +from utils import API_KEY from browser_use import AsyncBrowserUse -# gets API Key from environment variable BROWSER_USE_API_KEY -client = AsyncBrowserUse(api_key=os.getenv("BROWSER_USE_API_KEY")) +client = AsyncBrowserUse(api_key=API_KEY) # Regular Task diff --git a/examples/async_stream.py b/examples/async_stream.py index c604f6d..c1974f6 100755 --- a/examples/async_stream.py +++ b/examples/async_stream.py @@ -1,14 +1,14 @@ -#!/usr/bin/env -S rye run python +#!/usr/bin/env -S poetry run python import asyncio -import os from typing import List from pydantic import BaseModel +from utils import API_KEY from browser_use import AsyncBrowserUse -client = AsyncBrowserUse(api_key=os.getenv("BROWSER_USE_API_KEY")) +client = AsyncBrowserUse(api_key=API_KEY) # Regular Task diff --git a/examples/retrieve.py b/examples/retrieve.py index 9882a05..ea2c961 100755 --- a/examples/retrieve.py +++ b/examples/retrieve.py @@ -1,14 +1,14 @@ -#!/usr/bin/env -S rye run python +#!/usr/bin/env -S poetry run python -import os import time from typing import List from pydantic import BaseModel +from utils import API_KEY from browser_use import BrowserUse -client = BrowserUse(api_key=os.getenv("BROWSER_USE_API_KEY")) +client = BrowserUse(api_key=API_KEY) # Regular Task diff --git a/examples/run.py b/examples/run.py index a6655e8..5dca07d 100755 --- a/examples/run.py +++ b/examples/run.py @@ -1,13 +1,13 @@ -#!/usr/bin/env -S rye run python +#!/usr/bin/env -S poetry run python -import os from typing import List from pydantic import BaseModel +from utils import API_KEY from browser_use import BrowserUse -client = BrowserUse(api_key=os.getenv("BROWSER_USE_API_KEY")) +client = BrowserUse(api_key=API_KEY) # Regular Task diff --git a/examples/stream.py b/examples/stream.py index 4fa37ba..b7ce714 100755 --- a/examples/stream.py +++ b/examples/stream.py @@ -1,13 +1,13 @@ -#!/usr/bin/env -S rye run python +#!/usr/bin/env -S poetry run python -import os from typing import List from pydantic import BaseModel +from utils import API_KEY from browser_use import BrowserUse -client = BrowserUse(api_key=os.getenv("BROWSER_USE_API_KEY")) +client = BrowserUse(api_key=API_KEY) # Regular Task diff --git a/examples/utils.py b/examples/utils.py new file mode 100644 index 0000000..53756c9 --- /dev/null +++ b/examples/utils.py @@ -0,0 +1,7 @@ +import os + +_API_KEY = os.getenv("BROWSER_USE_API_KEY") +if not _API_KEY: + raise RuntimeError("BROWSER_USE_API_KEY environment variable is not set") + +API_KEY = _API_KEY diff --git a/examples/webhooks.py b/examples/webhooks.py index 2afccdb..a26a29c 100755 --- a/examples/webhooks.py +++ b/examples/webhooks.py @@ -1,4 +1,4 @@ -#!/usr/bin/env -S rye run python +#!/usr/bin/env -S poetry run python from datetime import datetime from typing import Any, Dict, Tuple diff --git a/poetry.lock b/poetry.lock index c2ddd86..d07b671 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. [[package]] name = "annotated-types" @@ -6,6 +6,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -20,6 +21,7 @@ version = "4.5.2" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, @@ -33,7 +35,7 @@ typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21.0b1) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""] trio = ["trio (>=0.26.1)"] [[package]] @@ -42,6 +44,7 @@ version = "2025.8.3" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, @@ -53,6 +56,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -64,6 +69,8 @@ version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, @@ -81,6 +88,7 @@ version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -92,6 +100,7 @@ version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -113,6 +122,7 @@ version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -125,7 +135,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -137,6 +147,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -151,6 +162,7 @@ version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -162,6 +174,7 @@ version = "1.13.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, @@ -215,6 +228,7 @@ version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -226,6 +240,7 @@ version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, @@ -237,6 +252,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -252,6 +268,7 @@ version = "2.10.6" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, @@ -264,7 +281,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -272,6 +289,7 @@ version = "2.27.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, @@ -384,6 +402,7 @@ version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, @@ -406,6 +425,7 @@ version = "0.23.8" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, @@ -424,6 +444,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -438,6 +459,7 @@ version = "0.11.5" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b"}, {file = "ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077"}, @@ -465,6 +487,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -476,6 +499,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -487,6 +511,8 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -528,6 +554,7 @@ version = "2.9.0.20241206" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, @@ -539,12 +566,13 @@ version = "4.13.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, ] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.8" content-hash = "8551b871abee465e23fb0966d51f2c155fd257b55bdcb0c02d095de19f92f358" diff --git a/src/browser_use/wrapper/parse.py b/src/browser_use/wrapper/parse.py index d2fc022..07303be 100644 --- a/src/browser_use/wrapper/parse.py +++ b/src/browser_use/wrapper/parse.py @@ -55,9 +55,9 @@ def _parse_task_view_with_output(task_view: TaskView, schema: Type[T]) -> TaskVi def _watch( client: TasksClient, task_id: str, interval: float = 1, request_options: typing.Optional[RequestOptions] = None -) -> Iterator[TaskViewWithOutput[T]]: +) -> Iterator[TaskView]: """Yields the latest task state on every change.""" - hash: str | None = None + hash: typing.Union[str, None] = None while True: res = client.get_task(task_id, request_options=request_options) res_hash = _hash_task_view(res) @@ -90,9 +90,7 @@ def __init__(self, id: str, client: TasksClient): super().__init__(id=id) self._client = client - def complete( - self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None - ) -> TaskViewWithOutput[T]: + def complete(self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None) -> TaskView: """Waits for the task to finish and return the result.""" for state in _watch(self._client, self.id, interval, request_options): if state.status == "finished" or state.status == "stopped" or state.status == "paused": @@ -106,9 +104,7 @@ def stream( """Streams the steps of the task and closes when the task is finished.""" return _stream(self._client, self.id, interval, request_options) - def watch( - self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None - ) -> Iterator[TaskViewWithOutput[T]]: + def watch(self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None) -> Iterator[TaskView]: """Yields the latest task state on every change.""" return _watch(self._client, self.id, interval, request_options) @@ -154,9 +150,9 @@ def watch( async def _async_watch( client: AsyncTasksClient, task_id: str, interval: float = 1, request_options: typing.Optional[RequestOptions] = None -) -> AsyncIterator[TaskViewWithOutput[T]]: +) -> AsyncIterator[TaskView]: """Yields the latest task state on every change.""" - hash: str | None = None + hash: typing.Union[str, None] = None while True: res = await client.get_task(task_id, request_options=request_options) res_hash = _hash_task_view(res) @@ -175,7 +171,7 @@ async def _async_stream( ) -> AsyncIterator[TaskStepView]: """Streams the steps of the task and closes when the task is finished.""" total_steps = 0 - for state in _async_watch(client, task_id, interval, request_options): + async for state in _async_watch(client, task_id, interval, request_options): for i in range(total_steps, len(state.steps)): total_steps = i + 1 yield state.steps[i] @@ -190,13 +186,13 @@ def __init__(self, id: str, client: AsyncTasksClient): async def complete(self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None) -> TaskView: """Waits for the task to finish and return the result.""" - for state in _async_watch(self._client, self.id, interval, request_options): + async for state in _async_watch(self._client, self.id, interval, request_options): if state.status == "finished" or state.status == "stopped" or state.status == "paused": return state raise Exception("Iterator ended without finding a finished state!") - async def stream( + def stream( self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None ) -> AsyncIterator[TaskStepView]: """Streams the steps of the task and closes when the task is finished.""" @@ -225,13 +221,13 @@ async def complete( self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None ) -> TaskViewWithOutput[T]: """Waits for the task to finish and return the result.""" - for state in _async_watch(self._client, self.id, interval, request_options): + async for state in _async_watch(self._client, self.id, interval, request_options): if state.status == "finished" or state.status == "stopped" or state.status == "paused": return _parse_task_view_with_output(state, self._schema) raise Exception("Iterator ended without finding a finished state!") - async def stream( + def stream( self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None ) -> AsyncIterator[TaskStepView]: """Streams the steps of the task and closes when the task is finished.""" @@ -241,5 +237,5 @@ async def watch( self, interval: float = 1, request_options: typing.Optional[RequestOptions] = None ) -> AsyncIterator[TaskViewWithOutput[T]]: """Yields the latest task state on every change.""" - for state in _async_watch(self._client, self.id, interval, request_options): + async for state in _async_watch(self._client, self.id, interval, request_options): yield _parse_task_view_with_output(state, self._schema) From 48150a132031c58b5b13e7c2c3e6acdcb4d7e4f4 Mon Sep 17 00:00:00 2001 From: Matic Zavadlal Date: Tue, 2 Sep 2025 13:00:41 +0200 Subject: [PATCH 12/13] fixes --- examples/{utils.py => api.py} | 0 examples/async_retrieve.py | 2 +- examples/async_run.py | 2 +- examples/async_stream.py | 2 +- examples/retrieve.py | 2 +- examples/run.py | 2 +- examples/stream.py | 2 +- src/browser_use/wrapper/tasks/client.py | 8 ++++---- 8 files changed, 10 insertions(+), 10 deletions(-) rename examples/{utils.py => api.py} (100%) diff --git a/examples/utils.py b/examples/api.py similarity index 100% rename from examples/utils.py rename to examples/api.py diff --git a/examples/async_retrieve.py b/examples/async_retrieve.py index d999494..82f19b7 100755 --- a/examples/async_retrieve.py +++ b/examples/async_retrieve.py @@ -3,8 +3,8 @@ import asyncio from typing import List +from api import API_KEY from pydantic import BaseModel -from utils import API_KEY from browser_use import AsyncBrowserUse diff --git a/examples/async_run.py b/examples/async_run.py index ad60894..e0b6c9a 100755 --- a/examples/async_run.py +++ b/examples/async_run.py @@ -3,8 +3,8 @@ import asyncio from typing import List +from api import API_KEY from pydantic import BaseModel -from utils import API_KEY from browser_use import AsyncBrowserUse diff --git a/examples/async_stream.py b/examples/async_stream.py index c1974f6..fadf5d0 100755 --- a/examples/async_stream.py +++ b/examples/async_stream.py @@ -3,8 +3,8 @@ import asyncio from typing import List +from api import API_KEY from pydantic import BaseModel -from utils import API_KEY from browser_use import AsyncBrowserUse diff --git a/examples/retrieve.py b/examples/retrieve.py index ea2c961..121adbe 100755 --- a/examples/retrieve.py +++ b/examples/retrieve.py @@ -3,8 +3,8 @@ import time from typing import List +from api import API_KEY from pydantic import BaseModel -from utils import API_KEY from browser_use import BrowserUse diff --git a/examples/run.py b/examples/run.py index 5dca07d..8f4fc50 100755 --- a/examples/run.py +++ b/examples/run.py @@ -2,8 +2,8 @@ from typing import List +from api import API_KEY from pydantic import BaseModel -from utils import API_KEY from browser_use import BrowserUse diff --git a/examples/stream.py b/examples/stream.py index b7ce714..e90ad66 100755 --- a/examples/stream.py +++ b/examples/stream.py @@ -2,8 +2,8 @@ from typing import List +from api import API_KEY from pydantic import BaseModel -from utils import API_KEY from browser_use import BrowserUse diff --git a/src/browser_use/wrapper/tasks/client.py b/src/browser_use/wrapper/tasks/client.py index 8b583ac..835f0b8 100644 --- a/src/browser_use/wrapper/tasks/client.py +++ b/src/browser_use/wrapper/tasks/client.py @@ -84,7 +84,7 @@ def create_task( system_prompt_extension: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> typing.Union[WrappedStructuredTaskCreatedResponse[T], WrappedTaskCreatedResponse]: - if schema is not None: + if schema is not None and schema is not OMIT: structured_output = json.dumps(schema.model_json_schema()) res = super().create_task( @@ -145,7 +145,7 @@ def get_task( ) -> typing.Union[TaskViewWithOutput[T], TaskView]: res = super().get_task(task_id, request_options=request_options) - if schema is not None: + if schema is not None and schema is not OMIT: return _parse_task_view_with_output(res, schema) else: return res @@ -219,7 +219,7 @@ async def create_task( system_prompt_extension: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> typing.Union[AsyncWrappedStructuredTaskCreatedResponse[T], AsyncWrappedTaskCreatedResponse]: - if schema is not None: + if schema is not None and schema is not OMIT: structured_output = json.dumps(schema.model_json_schema()) res = await super().create_task( @@ -278,7 +278,7 @@ async def get_task( ) -> typing.Union[TaskViewWithOutput[T], TaskView]: res = await super().get_task(task_id, request_options=request_options) - if schema is not None: + if schema is not None and schema is not OMIT: return _parse_task_view_with_output(res, schema) else: return res From 39bf0c30e60ac94daecbff6ffb66277ab3679ccc Mon Sep 17 00:00:00 2001 From: Matic Zavadlal Date: Tue, 2 Sep 2025 13:49:26 +0200 Subject: [PATCH 13/13] fixes --- README.md | 49 ++++++++++++++++++++-------------------- examples/async_stream.py | 7 ++++++ 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 0f4433d..c23c96c 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,14 @@ from browser_use_sdk import BrowserUse client = BrowserUse(api_key="bu_...") -result = client.tasks.run( +task = client.tasks.create_task( task="Search for the top 10 Hacker News posts and return the title and url." + llm="gpt-4.1", ) -result.done_output +result = task.complete() + +result.output ``` > The full API of this library can be found in [api.md](api.md). @@ -38,16 +41,18 @@ class SearchResult(BaseModel): posts: List[HackerNewsPost] async def main() -> None: - result = await client.tasks.run( + task = await client.tasks.create_task( task=""" Find top 10 Hacker News articles and return the title and url. """, - structured_output_json=SearchResult, + schema=SearchResult, ) - if structured_result.parsed_output is not None: + result = await task.complete() + + if result.parsed_output is not None: print("Top HackerNews Posts:") - for post in structured_result.parsed_output.posts: + for post in result.parsed_output.posts: print(f" - {post.title} - {post.url}") asyncio.run(main()) @@ -73,25 +78,18 @@ async def main() -> None: task=""" Find top 10 Hacker News articles and return the title and url. """, - structured_output_json=SearchResult, + schema=SearchResult, ) - async for update in client.tasks.stream(task.id, structured_output_json=SearchResult): - if len(update.steps) > 0: - last_step = update.steps[-1] - print(f"{update.status}: {last_step.url} ({last_step.next_goal})") - else: - print(f"{update.status}") + async for step in task.stream(): + print(f"Step {step.number}: {step.url} ({step.next_goal})") - if update.status == "finished": - if update.parsed_output is None: - print("No output...") - else: - print("Top HackerNews Posts:") - for post in update.parsed_output.posts: - print(f" - {post.title} - {post.url}") + result = await task.complete() - break + if result.parsed_output is not None: + print("Top HackerNews Posts:") + for post in result.parsed_output.posts: + print(f" - {post.title} - {post.url}") asyncio.run(main()) ``` @@ -105,7 +103,7 @@ Browser Use SDK lets you easily verify the signature and structure of the payloa ```py import uvicorn import os -from browser_use_sdk.lib.webhooks import Webhook, verify_webhook_event_signature +from browser_use_sdk import Webhook, verify_webhook_event_signature from fastapi import FastAPI, Request, HTTPException @@ -153,10 +151,11 @@ client = AsyncBrowserUse( async def main() -> None: - task = await client.tasks.run( + task = await client.tasks.create_task( task="Search for the top 10 Hacker News posts and return the title and url.", ) - print(task.done_output) + + print(task.id) asyncio.run(main()) @@ -468,6 +467,7 @@ a proof of concept, but know that we will not be able to merge it as-is. We sugg an issue first to discuss with us! On the other hand, contributions to the README are always very welcome! + ## Installation ```sh @@ -530,4 +530,3 @@ except ApiError as e: print(e.status_code) print(e.body) ``` - diff --git a/examples/async_stream.py b/examples/async_stream.py index fadf5d0..244e792 100755 --- a/examples/async_stream.py +++ b/examples/async_stream.py @@ -25,6 +25,11 @@ async def stream_regular_task() -> None: async for step in task.stream(): print(f"Regular Task Status: {step.number}") + result = await task.complete() + + if result.output is not None: + print(f"Regular Task Output: {result.output}") + print("Regular Task Done") @@ -53,6 +58,8 @@ class SearchResult(BaseModel): result = await task.complete() if result.parsed_output is not None: + print("Structured Task Output:") + for post in result.parsed_output.posts: print(f" - {post.title} - {post.url}")