From 98e689de822272e7f9e957436d2f8c6edab30a2b Mon Sep 17 00:00:00 2001 From: fern-api <115122769+fern-api[bot]@users.noreply.github.com> Date: Sun, 6 Apr 2025 00:23:22 +0000 Subject: [PATCH 1/3] SDK regeneration --- poetry.lock | 6 +- pyproject.toml | 2 +- reference.md | 168 +++++++++++++++ src/scrapybara/__init__.py | 2 + src/scrapybara/core/client_wrapper.py | 2 +- src/scrapybara/instance/client.py | 272 ++++++++++++++++++++++++ src/scrapybara/types/upload_response.py | 25 +++ 7 files changed, 472 insertions(+), 5 deletions(-) create mode 100644 src/scrapybara/types/upload_response.py diff --git a/poetry.lock b/poetry.lock index b603c94..d1637e3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -525,13 +525,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.13.0" +version = "4.13.1" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5"}, - {file = "typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b"}, + {file = "typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69"}, + {file = "typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff"}, ] [metadata] diff --git a/pyproject.toml b/pyproject.toml index c9b2011..ca8aae6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "scrapybara" [tool.poetry] name = "scrapybara" -version = "2.4.7" +version = "2.4.8" description = "" readme = "README.md" authors = [] diff --git a/reference.md b/reference.md index 3e91211..8061cb1 100644 --- a/reference.md +++ b/reference.md @@ -805,6 +805,174 @@ client.instance.file( + + + + +
client.instance.download(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Download a file from the instance. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from scrapybara import Scrapybara + +client = Scrapybara( + api_key="YOUR_API_KEY", +) +client.instance.download( + instance_id="instance_id", + path="path", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**instance_id:** `str` + +
+
+ +
+
+ +**path:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.instance.upload(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Upload a file to the instance. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from scrapybara import Scrapybara + +client = Scrapybara( + api_key="YOUR_API_KEY", +) +client.instance.upload( + instance_id="instance_id", + path="path", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**instance_id:** `str` + +
+
+ +
+
+ +**file:** `from __future__ import annotations + +core.File` — See core.File for more documentation + +
+
+ +
+
+ +**path:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ +
diff --git a/src/scrapybara/__init__.py b/src/scrapybara/__init__.py index 4cbd33f..0005fcd 100644 --- a/src/scrapybara/__init__.py +++ b/src/scrapybara/__init__.py @@ -38,6 +38,7 @@ StopInstanceResponse, TakeScreenshotAction, TypeTextAction, + UploadResponse, ValidationError, ValidationErrorLocItem, WaitAction, @@ -114,6 +115,7 @@ "TakeScreenshotAction", "TypeTextAction", "UnprocessableEntityError", + "UploadResponse", "ValidationError", "ValidationErrorLocItem", "WaitAction", diff --git a/src/scrapybara/core/client_wrapper.py b/src/scrapybara/core/client_wrapper.py index 0f89336..ada1d0c 100644 --- a/src/scrapybara/core/client_wrapper.py +++ b/src/scrapybara/core/client_wrapper.py @@ -16,7 +16,7 @@ def get_headers(self) -> typing.Dict[str, str]: headers: typing.Dict[str, str] = { "X-Fern-Language": "Python", "X-Fern-SDK-Name": "scrapybara", - "X-Fern-SDK-Version": "2.4.7", + "X-Fern-SDK-Version": "2.4.8", } headers["x-api-key"] = self.api_key return headers diff --git a/src/scrapybara/instance/client.py b/src/scrapybara/instance/client.py index 2ea47ac..3877b27 100644 --- a/src/scrapybara/instance/client.py +++ b/src/scrapybara/instance/client.py @@ -18,6 +18,8 @@ from .types.command import Command from ..types.edit_response import EditResponse from ..types.file_response import FileResponse +from .. import core +from ..types.upload_response import UploadResponse from ..types.stop_instance_response import StopInstanceResponse from ..types.get_instance_response import GetInstanceResponse from ..core.client_wrapper import AsyncClientWrapper @@ -520,6 +522,132 @@ def file( raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) + def download(self, instance_id: str, *, path: str, request_options: typing.Optional[RequestOptions] = None) -> None: + """ + Download a file from the instance. + + Parameters + ---------- + instance_id : str + + path : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from scrapybara import Scrapybara + + client = Scrapybara( + api_key="YOUR_API_KEY", + ) + client.instance.download( + instance_id="instance_id", + path="path", + ) + """ + _response = self._client_wrapper.httpx_client.request( + f"v1/instance/{jsonable_encoder(instance_id)}/download", + method="GET", + params={ + "path": path, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return + if _response.status_code == 422: + raise UnprocessableEntityError( + typing.cast( + HttpValidationError, + parse_obj_as( + type_=HttpValidationError, # type: ignore + object_=_response.json(), + ), + ) + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def upload( + self, instance_id: str, *, file: core.File, path: str, request_options: typing.Optional[RequestOptions] = None + ) -> UploadResponse: + """ + Upload a file to the instance. + + Parameters + ---------- + instance_id : str + + file : core.File + See core.File for more documentation + + path : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + UploadResponse + Successful Response + + Examples + -------- + from scrapybara import Scrapybara + + client = Scrapybara( + api_key="YOUR_API_KEY", + ) + client.instance.upload( + instance_id="instance_id", + path="path", + ) + """ + _response = self._client_wrapper.httpx_client.request( + f"v1/instance/{jsonable_encoder(instance_id)}/upload", + method="POST", + data={ + "path": path, + }, + files={ + "file": file, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return typing.cast( + UploadResponse, + parse_obj_as( + type_=UploadResponse, # type: ignore + object_=_response.json(), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + typing.cast( + HttpValidationError, + parse_obj_as( + type_=HttpValidationError, # type: ignore + object_=_response.json(), + ), + ) + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + def stop( self, instance_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> StopInstanceResponse: @@ -1240,6 +1368,150 @@ async def main() -> None: raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) + async def download( + self, instance_id: str, *, path: str, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Download a file from the instance. + + Parameters + ---------- + instance_id : str + + path : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import asyncio + + from scrapybara import AsyncScrapybara + + client = AsyncScrapybara( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.instance.download( + instance_id="instance_id", + path="path", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + f"v1/instance/{jsonable_encoder(instance_id)}/download", + method="GET", + params={ + "path": path, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return + if _response.status_code == 422: + raise UnprocessableEntityError( + typing.cast( + HttpValidationError, + parse_obj_as( + type_=HttpValidationError, # type: ignore + object_=_response.json(), + ), + ) + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def upload( + self, instance_id: str, *, file: core.File, path: str, request_options: typing.Optional[RequestOptions] = None + ) -> UploadResponse: + """ + Upload a file to the instance. + + Parameters + ---------- + instance_id : str + + file : core.File + See core.File for more documentation + + path : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + UploadResponse + Successful Response + + Examples + -------- + import asyncio + + from scrapybara import AsyncScrapybara + + client = AsyncScrapybara( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.instance.upload( + instance_id="instance_id", + path="path", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + f"v1/instance/{jsonable_encoder(instance_id)}/upload", + method="POST", + data={ + "path": path, + }, + files={ + "file": file, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return typing.cast( + UploadResponse, + parse_obj_as( + type_=UploadResponse, # type: ignore + object_=_response.json(), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + typing.cast( + HttpValidationError, + parse_obj_as( + type_=HttpValidationError, # type: ignore + object_=_response.json(), + ), + ) + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + async def stop( self, instance_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> StopInstanceResponse: diff --git a/src/scrapybara/types/upload_response.py b/src/scrapybara/types/upload_response.py new file mode 100644 index 0000000..c7b0678 --- /dev/null +++ b/src/scrapybara/types/upload_response.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +from ..core.pydantic_utilities import UniversalBaseModel +import typing +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +import pydantic + + +class UploadResponse(UniversalBaseModel): + """ + Response model for file uploads. + """ + + filename: str + path: str + media_type: 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 From bfe36d57fada2d9997d85e2798679daeaa64a4eb Mon Sep 17 00:00:00 2001 From: Cooper Miller Date: Sun, 6 Apr 2025 12:07:27 -0700 Subject: [PATCH 2/3] add type and methods to UbuntuInstance + custom tests --- src/scrapybara/client.py | 54 ++++++++++++++++++++++++++++++++ src/scrapybara/types/__init__.py | 1 + tests/custom/test_client.py | 42 +++++++++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/src/scrapybara/client.py b/src/scrapybara/client.py index 88b7566..8939911 100644 --- a/src/scrapybara/client.py +++ b/src/scrapybara/client.py @@ -26,6 +26,7 @@ from scrapybara.environment import ScrapybaraEnvironment from .core.request_options import RequestOptions from .core.api_error import ApiError +from .core import File from .types import ( Action, AuthStateResponse, @@ -49,6 +50,7 @@ StopBrowserResponse, StopInstanceResponse, ModifyBrowserAuthResponse, + UploadResponse, ) from .types.act import ( @@ -963,6 +965,32 @@ def file( line_numbers=line_numbers, request_options=request_options ) + + def upload( + self, + *, + file: File, + path: str, + request_options: Optional[RequestOptions] = None, + ) -> UploadResponse: + return self._client.instance.upload( + self.id, + file=file, + path=path, + request_options=request_options, + ) + + def download( + self, + *, + path: str, + request_options: Optional[RequestOptions] = None, + ) -> None: + return self._client.instance.download( + self.id, + path=path, + request_options=request_options, + ) class BrowserInstance(BaseInstance): def __init__( @@ -1471,6 +1499,32 @@ async def file( line_numbers=line_numbers, request_options=request_options ) + + async def upload( + self, + *, + file: File, + path: str, + request_options: Optional[RequestOptions] = None, + ) -> UploadResponse: + return await self._client.instance.upload( + self.id, + file=file, + path=path, + request_options=request_options, + ) + + async def download( + self, + *, + path: str, + request_options: Optional[RequestOptions] = None, + ) -> None: + return await self._client.instance.download( + self.id, + path=path, + request_options=request_options, + ) class AsyncBrowserInstance(AsyncBaseInstance): def __init__( diff --git a/src/scrapybara/types/__init__.py b/src/scrapybara/types/__init__.py index 40c71b8..8961e8b 100644 --- a/src/scrapybara/types/__init__.py +++ b/src/scrapybara/types/__init__.py @@ -16,6 +16,7 @@ from .env_response import EnvResponse from .execute_cell_request import ExecuteCellRequest from .file_response import FileResponse +from .upload_response import UploadResponse from .get_cursor_position_action import GetCursorPositionAction from .get_instance_response import GetInstanceResponse from .get_instance_response_instance_type import GetInstanceResponseInstanceType diff --git a/tests/custom/test_client.py b/tests/custom/test_client.py index a949167..baa408b 100644 --- a/tests/custom/test_client.py +++ b/tests/custom/test_client.py @@ -2,6 +2,8 @@ from scrapybara import Scrapybara import os import pytest +import tempfile +import uuid from scrapybara.anthropic import ( Anthropic, @@ -241,11 +243,51 @@ def test_browser_thinking() -> None: browser_instance.stop() +def test_upload_download() -> None: + _check_api_key() + client = Scrapybara() + + # Start Ubuntu instance + ubuntu_instance = client.start_ubuntu() + assert ubuntu_instance.id is not None + + try: + # Create a temporary file with test content + test_content = f"Test content {uuid.uuid4()}" + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as temp_file: + temp_file.write(test_content) + temp_path = temp_file.name + + # Upload the file to the instance + remote_path = f"/tmp/test_file_{uuid.uuid4()}" + with open(temp_path, 'rb') as f: + upload_response = ubuntu_instance.upload(file=f, path=remote_path) + assert upload_response is not None + + # Verify file exists on remote and content matches + file_check = ubuntu_instance.bash(command=f"cat {remote_path}") + assert file_check is not None + assert test_content in str(file_check) + + # Call the download method to at least test the API call + # Note: In a real application you would need to handle the response + # and save the content to a local file + ubuntu_instance.download(path=remote_path) + + # Clean up local files + os.unlink(temp_path) + + finally: + # Always stop the instance + ubuntu_instance.stop() + + if __name__ == "__main__": test_ubuntu() test_browser() test_ubuntu_openai() test_browser_openai() + test_upload_download() # test_ubuntu_thinking() # test_browser_thinking() # test_windows() From 75042ae776a63097be56e1ee1b581c9b2f0e1836 Mon Sep 17 00:00:00 2001 From: Cooper Miller Date: Sun, 6 Apr 2025 14:14:16 -0700 Subject: [PATCH 3/3] fix path in test --- tests/custom/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/custom/test_client.py b/tests/custom/test_client.py index baa408b..8726226 100644 --- a/tests/custom/test_client.py +++ b/tests/custom/test_client.py @@ -259,7 +259,7 @@ def test_upload_download() -> None: temp_path = temp_file.name # Upload the file to the instance - remote_path = f"/tmp/test_file_{uuid.uuid4()}" + remote_path = f"test_file_{uuid.uuid4()}" with open(temp_path, 'rb') as f: upload_response = ubuntu_instance.upload(file=f, path=remote_path) assert upload_response is not None