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