diff --git a/pyproject.toml b/pyproject.toml index 84eda73..a8f3cce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "scrapybara" [tool.poetry] name = "scrapybara" -version = "2.4.3" +version = "2.4.4" description = "" readme = "README.md" authors = [] diff --git a/reference.md b/reference.md index 5cf384f..051386b 100644 --- a/reference.md +++ b/reference.md @@ -466,6 +466,22 @@ client.instance.bash(
+**get_background_processes:** `typing.Optional[bool]` + +
+
+ +
+
+ +**kill_pid:** `typing.Optional[int]` + +
+
+ +
+
+ **request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
@@ -588,6 +604,207 @@ client.instance.edit(
+ + + + +
client.instance.filesystem(...) +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from scrapybara import Scrapybara + +client = Scrapybara( + api_key="YOUR_API_KEY", +) +client.instance.filesystem( + instance_id="instance_id", + command="command", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**instance_id:** `str` + +
+
+ +
+
+ +**command:** `str` + +
+
+ +
+
+ +**path:** `typing.Optional[str]` + +
+
+ +
+
+ +**content:** `typing.Optional[str]` + +
+
+ +
+
+ +**mode:** `typing.Optional[str]` + +
+
+ +
+
+ +**encoding:** `typing.Optional[str]` + +
+
+ +
+
+ +**view_range:** `typing.Optional[typing.Sequence[int]]` + +
+
+ +
+
+ +**recursive:** `typing.Optional[bool]` + +
+
+ +
+
+ +**src:** `typing.Optional[str]` + +
+
+ +
+
+ +**dst:** `typing.Optional[str]` + +
+
+ +
+
+ +**old_str:** `typing.Optional[str]` + +
+
+ +
+
+ +**new_str:** `typing.Optional[str]` + +
+
+ +
+
+ +**line:** `typing.Optional[int]` + +
+
+ +
+
+ +**text:** `typing.Optional[str]` + +
+
+ +
+
+ +**lines:** `typing.Optional[typing.Sequence[int]]` + +
+
+ +
+
+ +**all_occurrences:** `typing.Optional[bool]` + +
+
+ +
+
+ +**pattern:** `typing.Optional[str]` + +
+
+ +
+
+ +**case_sensitive:** `typing.Optional[bool]` + +
+
+ +
+
+ +**line_numbers:** `typing.Optional[bool]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ +
diff --git a/src/scrapybara/__init__.py b/src/scrapybara/__init__.py index 3787602..02e6e49 100644 --- a/src/scrapybara/__init__.py +++ b/src/scrapybara/__init__.py @@ -19,6 +19,7 @@ ExecuteCellRequest, FileDownloadResponse, FileReadResponse, + FilesystemResponse, GetCursorPositionAction, GetInstanceResponse, GetInstanceResponseInstanceType, @@ -83,6 +84,7 @@ "ExecuteCellRequest", "FileDownloadResponse", "FileReadResponse", + "FilesystemResponse", "GetCursorPositionAction", "GetInstanceResponse", "GetInstanceResponseInstanceType", diff --git a/src/scrapybara/client.py b/src/scrapybara/client.py index 74271ad..f65c338 100644 --- a/src/scrapybara/client.py +++ b/src/scrapybara/client.py @@ -751,6 +751,7 @@ def computer( action: Literal["move_mouse"], coordinates: List[int], hold_keys: Optional[List[str]] = None, + screenshot: Optional[bool] = True, request_options: Optional[RequestOptions] = None, ) -> ComputerResponse: ... @@ -764,6 +765,7 @@ def computer( coordinates: Optional[List[int]] = None, num_clicks: Optional[int] = 1, hold_keys: Optional[List[str]] = None, + screenshot: Optional[bool] = True, request_options: Optional[RequestOptions] = None, ) -> ComputerResponse: ... @@ -774,6 +776,7 @@ def computer( action: Literal["drag_mouse"], path: List[List[int]], hold_keys: Optional[List[str]] = None, + screenshot: Optional[bool] = True, request_options: Optional[RequestOptions] = None, ) -> ComputerResponse: ... @@ -786,6 +789,7 @@ def computer( delta_x: Optional[float] = 0, delta_y: Optional[float] = 0, hold_keys: Optional[List[str]] = None, + screenshot: Optional[bool] = True, request_options: Optional[RequestOptions] = None, ) -> ComputerResponse: ... @@ -796,6 +800,7 @@ def computer( action: Literal["press_key"], keys: List[str], duration: Optional[float] = None, + screenshot: Optional[bool] = True, request_options: Optional[RequestOptions] = None, ) -> ComputerResponse: ... @@ -806,6 +811,7 @@ def computer( action: Literal["type_text"], text: str, hold_keys: Optional[List[str]] = None, + screenshot: Optional[bool] = True, request_options: Optional[RequestOptions] = None, ) -> ComputerResponse: ... @@ -815,6 +821,7 @@ def computer( *, action: Literal["wait"], duration: float, + screenshot: Optional[bool] = True, request_options: Optional[RequestOptions] = None, ) -> ComputerResponse: ... @@ -860,6 +867,7 @@ def computer( keys: Optional[List[str]] = None, text: Optional[str] = None, duration: Optional[float] = None, + screenshot: Optional[bool] = True, request_options: Optional[RequestOptions] = None, ) -> ComputerResponse: """Control computer actions like mouse movements, clicks, and keyboard input. @@ -910,7 +918,7 @@ def computer( # If it wasn't an object or the object wasn't recognized, use the legacy string-based approach if request is None: if action == "move_mouse": - request = Request_MoveMouse(coordinates=coordinates, hold_keys=hold_keys) + request = Request_MoveMouse(coordinates=coordinates, hold_keys=hold_keys, screenshot=screenshot) elif action == "click_mouse": request = Request_ClickMouse( button=button, @@ -918,22 +926,24 @@ def computer( coordinates=coordinates, num_clicks=num_clicks, hold_keys=hold_keys, + screenshot=screenshot, ) elif action == "drag_mouse": - request = Request_DragMouse(path=path, hold_keys=hold_keys) + request = Request_DragMouse(path=path, hold_keys=hold_keys, screenshot=screenshot) elif action == "scroll": request = Request_Scroll( coordinates=coordinates, delta_x=delta_x, delta_y=delta_y, hold_keys=hold_keys, + screenshot=screenshot, ) elif action == "press_key": - request = Request_PressKey(keys=keys, duration=duration) + request = Request_PressKey(keys=keys, duration=duration, screenshot=screenshot) elif action == "type_text": - request = Request_TypeText(text=text, hold_keys=hold_keys) + request = Request_TypeText(text=text, hold_keys=hold_keys, screenshot=screenshot) elif action == "wait": - request = Request_Wait(duration=duration) + request = Request_Wait(duration=duration, screenshot=screenshot) elif action == "take_screenshot": request = Request_TakeScreenshot() elif action == "get_cursor_position": @@ -988,10 +998,17 @@ def bash( *, command: Optional[str] = OMIT, restart: Optional[bool] = OMIT, + get_background_processes: Optional[bool] = OMIT, + kill_pid: Optional[int] = OMIT, request_options: Optional[RequestOptions] = None, ) -> Optional[Any]: return self._client.instance.bash( - self.id, command=command, restart=restart, request_options=request_options + self.id, + command=command, + restart=restart, + get_background_processes=get_background_processes, + kill_pid=kill_pid, + request_options=request_options ) def edit( @@ -1017,7 +1034,52 @@ def edit( insert_line=insert_line, request_options=request_options, ) - + + async def filesystem( + self, + *, + command: str, + path: Optional[str] = OMIT, + content: Optional[str] = OMIT, + mode: Optional[str] = OMIT, + encoding: Optional[str] = OMIT, + view_range: Optional[Sequence[int]] = OMIT, + recursive: Optional[bool] = OMIT, + src: Optional[str] = OMIT, + dst: Optional[str] = OMIT, + old_str: Optional[str] = OMIT, + new_str: Optional[str] = OMIT, + line: Optional[int] = OMIT, + text: Optional[str] = OMIT, + lines: Optional[Sequence[int]] = OMIT, + all_occurrences: Optional[bool] = OMIT, + pattern: Optional[str] = OMIT, + case_sensitive: Optional[bool] = OMIT, + line_numbers: Optional[bool] = OMIT, + request_options: Optional[RequestOptions] = None, + ) -> Optional[Any]: + return self._client.instance.filesystem( + self.id, + command=command, + path=path, + content=content, + mode=mode, + encoding=encoding, + view_range=view_range, + recursive=recursive, + src=src, + dst=dst, + old_str=old_str, + new_str=new_str, + line=line, + text=text, + lines=lines, + all_occurrences=all_occurrences, + pattern=pattern, + case_sensitive=case_sensitive, + line_numbers=line_numbers, + request_options=request_options + ) class BrowserInstance(BaseInstance): def __init__( @@ -1194,6 +1256,7 @@ async def computer( action: Literal["move_mouse"], coordinates: List[int], hold_keys: Optional[List[str]] = None, + screenshot: Optional[bool] = True, request_options: Optional[RequestOptions] = None, ) -> ComputerResponse: ... @@ -1207,6 +1270,7 @@ async def computer( coordinates: Optional[List[int]] = None, num_clicks: Optional[int] = 1, hold_keys: Optional[List[str]] = None, + screenshot: Optional[bool] = True, request_options: Optional[RequestOptions] = None, ) -> ComputerResponse: ... @@ -1217,6 +1281,7 @@ async def computer( action: Literal["drag_mouse"], path: List[List[int]], hold_keys: Optional[List[str]] = None, + screenshot: Optional[bool] = True, request_options: Optional[RequestOptions] = None, ) -> ComputerResponse: ... @@ -1229,6 +1294,7 @@ async def computer( delta_x: Optional[float] = 0, delta_y: Optional[float] = 0, hold_keys: Optional[List[str]] = None, + screenshot: Optional[bool] = True, request_options: Optional[RequestOptions] = None, ) -> ComputerResponse: ... @@ -1239,6 +1305,7 @@ async def computer( action: Literal["press_key"], keys: List[str], duration: Optional[float] = None, + screenshot: Optional[bool] = True, request_options: Optional[RequestOptions] = None, ) -> ComputerResponse: ... @@ -1249,6 +1316,7 @@ async def computer( action: Literal["type_text"], text: str, hold_keys: Optional[List[str]] = None, + screenshot: Optional[bool] = True, request_options: Optional[RequestOptions] = None, ) -> ComputerResponse: ... @@ -1258,6 +1326,7 @@ async def computer( *, action: Literal["wait"], duration: float, + screenshot: Optional[bool] = True, request_options: Optional[RequestOptions] = None, ) -> ComputerResponse: ... @@ -1303,6 +1372,7 @@ async def computer( keys: Optional[List[str]] = None, text: Optional[str] = None, duration: Optional[float] = None, + screenshot: Optional[bool] = True, request_options: Optional[RequestOptions] = None, ) -> ComputerResponse: """Control computer actions like mouse movements, clicks, and keyboard input. @@ -1353,7 +1423,7 @@ async def computer( # If it wasn't an object or the object wasn't recognized, use the legacy string-based approach if request is None: if action == "move_mouse": - request = Request_MoveMouse(coordinates=coordinates, hold_keys=hold_keys) + request = Request_MoveMouse(coordinates=coordinates, hold_keys=hold_keys, screenshot=screenshot) elif action == "click_mouse": request = Request_ClickMouse( button=button, @@ -1361,22 +1431,24 @@ async def computer( coordinates=coordinates, num_clicks=num_clicks, hold_keys=hold_keys, + screenshot=screenshot, ) elif action == "drag_mouse": - request = Request_DragMouse(path=path, hold_keys=hold_keys) + request = Request_DragMouse(path=path, hold_keys=hold_keys, screenshot=screenshot) elif action == "scroll": request = Request_Scroll( coordinates=coordinates, delta_x=delta_x, delta_y=delta_y, hold_keys=hold_keys, + screenshot=screenshot, ) elif action == "press_key": - request = Request_PressKey(keys=keys, duration=duration) + request = Request_PressKey(keys=keys, duration=duration, screenshot=screenshot) elif action == "type_text": - request = Request_TypeText(text=text, hold_keys=hold_keys) + request = Request_TypeText(text=text, hold_keys=hold_keys, screenshot=screenshot) elif action == "wait": - request = Request_Wait(duration=duration) + request = Request_Wait(duration=duration, screenshot=screenshot) elif action == "take_screenshot": request = Request_TakeScreenshot() elif action == "get_cursor_position": @@ -1435,10 +1507,17 @@ async def bash( *, command: Optional[str] = OMIT, restart: Optional[bool] = OMIT, + get_background_processes: Optional[bool] = OMIT, + kill_pid: Optional[int] = OMIT, request_options: Optional[RequestOptions] = None, ) -> Optional[Any]: return await self._client.instance.bash( - self.id, command=command, restart=restart, request_options=request_options + self.id, + command=command, + restart=restart, + get_background_processes=get_background_processes, + kill_pid=kill_pid, + request_options=request_options ) async def edit( @@ -1464,7 +1543,52 @@ async def edit( insert_line=insert_line, request_options=request_options, ) - + + async def filesystem( + self, + *, + command: str, + path: Optional[str] = OMIT, + content: Optional[str] = OMIT, + mode: Optional[str] = OMIT, + encoding: Optional[str] = OMIT, + view_range: Optional[Sequence[int]] = OMIT, + recursive: Optional[bool] = OMIT, + src: Optional[str] = OMIT, + dst: Optional[str] = OMIT, + old_str: Optional[str] = OMIT, + new_str: Optional[str] = OMIT, + line: Optional[int] = OMIT, + text: Optional[str] = OMIT, + lines: Optional[Sequence[int]] = OMIT, + all_occurrences: Optional[bool] = OMIT, + pattern: Optional[str] = OMIT, + case_sensitive: Optional[bool] = OMIT, + line_numbers: Optional[bool] = OMIT, + request_options: Optional[RequestOptions] = None, + ) -> Optional[Any]: + return await self._client.instance.filesystem( + self.id, + command=command, + path=path, + content=content, + mode=mode, + encoding=encoding, + view_range=view_range, + recursive=recursive, + src=src, + dst=dst, + old_str=old_str, + new_str=new_str, + line=line, + text=text, + lines=lines, + all_occurrences=all_occurrences, + pattern=pattern, + case_sensitive=case_sensitive, + line_numbers=line_numbers, + request_options=request_options + ) class AsyncBrowserInstance(AsyncBaseInstance): def __init__( @@ -2477,7 +2601,8 @@ def _create_request_from_action(action): if isinstance(action, MoveMouseAction): return Request_MoveMouse( coordinates=action.coordinates, - hold_keys=action.hold_keys + hold_keys=action.hold_keys, + screenshot=action.screenshot ) elif isinstance(action, ClickMouseAction): return Request_ClickMouse( @@ -2486,11 +2611,13 @@ def _create_request_from_action(action): coordinates=action.coordinates, num_clicks=action.num_clicks, hold_keys=action.hold_keys, + screenshot=action.screenshot ) elif isinstance(action, DragMouseAction): return Request_DragMouse( path=action.path, - hold_keys=action.hold_keys + hold_keys=action.hold_keys, + screenshot=action.screenshot ) elif isinstance(action, ScrollAction): return Request_Scroll( @@ -2498,20 +2625,24 @@ def _create_request_from_action(action): delta_x=action.delta_x, delta_y=action.delta_y, hold_keys=action.hold_keys, + screenshot=action.screenshot ) elif isinstance(action, PressKeyAction): return Request_PressKey( keys=action.keys, - duration=action.duration + duration=action.duration, + screenshot=action.screenshot ) elif isinstance(action, TypeTextAction): return Request_TypeText( text=action.text, - hold_keys=action.hold_keys + hold_keys=action.hold_keys, + screenshot=action.screenshot ) elif isinstance(action, WaitAction): return Request_Wait( - duration=action.duration + duration=action.duration, + screenshot=action.screenshot ) elif isinstance(action, TakeScreenshotAction): return Request_TakeScreenshot() diff --git a/src/scrapybara/core/client_wrapper.py b/src/scrapybara/core/client_wrapper.py index f03289d..0ead1a3 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.3", + "X-Fern-SDK-Version": "2.4.4", } headers["x-api-key"] = self.api_key return headers diff --git a/src/scrapybara/instance/client.py b/src/scrapybara/instance/client.py index 78e8799..3ef7a22 100644 --- a/src/scrapybara/instance/client.py +++ b/src/scrapybara/instance/client.py @@ -17,6 +17,7 @@ from ..types.bash_response import BashResponse from .types.command import Command from ..types.edit_response import EditResponse +from ..types.filesystem_response import FilesystemResponse from ..types.stop_instance_response import StopInstanceResponse from ..types.get_instance_response import GetInstanceResponse from ..core.client_wrapper import AsyncClientWrapper @@ -211,6 +212,8 @@ def bash( *, command: typing.Optional[str] = OMIT, restart: typing.Optional[bool] = OMIT, + get_background_processes: typing.Optional[bool] = OMIT, + kill_pid: typing.Optional[int] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> BashResponse: """ @@ -222,6 +225,10 @@ def bash( restart : typing.Optional[bool] + get_background_processes : typing.Optional[bool] + + kill_pid : typing.Optional[int] + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -247,6 +254,8 @@ def bash( json={ "command": command, "restart": restart, + "get_background_processes": get_background_processes, + "kill_pid": kill_pid, }, headers={ "content-type": "application/json", @@ -373,6 +382,144 @@ def edit( raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) + def filesystem( + self, + instance_id: str, + *, + command: str, + path: typing.Optional[str] = OMIT, + content: typing.Optional[str] = OMIT, + mode: typing.Optional[str] = OMIT, + encoding: typing.Optional[str] = OMIT, + view_range: typing.Optional[typing.Sequence[int]] = OMIT, + recursive: typing.Optional[bool] = OMIT, + src: typing.Optional[str] = OMIT, + dst: typing.Optional[str] = OMIT, + old_str: typing.Optional[str] = OMIT, + new_str: typing.Optional[str] = OMIT, + line: typing.Optional[int] = OMIT, + text: typing.Optional[str] = OMIT, + lines: typing.Optional[typing.Sequence[int]] = OMIT, + all_occurrences: typing.Optional[bool] = OMIT, + pattern: typing.Optional[str] = OMIT, + case_sensitive: typing.Optional[bool] = OMIT, + line_numbers: typing.Optional[bool] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> FilesystemResponse: + """ + Parameters + ---------- + instance_id : str + + command : str + + path : typing.Optional[str] + + content : typing.Optional[str] + + mode : typing.Optional[str] + + encoding : typing.Optional[str] + + view_range : typing.Optional[typing.Sequence[int]] + + recursive : typing.Optional[bool] + + src : typing.Optional[str] + + dst : typing.Optional[str] + + old_str : typing.Optional[str] + + new_str : typing.Optional[str] + + line : typing.Optional[int] + + text : typing.Optional[str] + + lines : typing.Optional[typing.Sequence[int]] + + all_occurrences : typing.Optional[bool] + + pattern : typing.Optional[str] + + case_sensitive : typing.Optional[bool] + + line_numbers : typing.Optional[bool] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + FilesystemResponse + Successful Response + + Examples + -------- + from scrapybara import Scrapybara + + client = Scrapybara( + api_key="YOUR_API_KEY", + ) + client.instance.filesystem( + instance_id="instance_id", + command="command", + ) + """ + _response = self._client_wrapper.httpx_client.request( + f"v1/instance/{jsonable_encoder(instance_id)}/filesystem", + method="POST", + json={ + "command": command, + "path": path, + "content": content, + "mode": mode, + "encoding": encoding, + "view_range": view_range, + "recursive": recursive, + "src": src, + "dst": dst, + "old_str": old_str, + "new_str": new_str, + "line": line, + "text": text, + "lines": lines, + "all_occurrences": all_occurrences, + "pattern": pattern, + "case_sensitive": case_sensitive, + "line_numbers": line_numbers, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return typing.cast( + FilesystemResponse, + parse_obj_as( + type_=FilesystemResponse, # 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: @@ -761,6 +908,8 @@ async def bash( *, command: typing.Optional[str] = OMIT, restart: typing.Optional[bool] = OMIT, + get_background_processes: typing.Optional[bool] = OMIT, + kill_pid: typing.Optional[int] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> BashResponse: """ @@ -772,6 +921,10 @@ async def bash( restart : typing.Optional[bool] + get_background_processes : typing.Optional[bool] + + kill_pid : typing.Optional[int] + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -805,6 +958,8 @@ async def main() -> None: json={ "command": command, "restart": restart, + "get_background_processes": get_background_processes, + "kill_pid": kill_pid, }, headers={ "content-type": "application/json", @@ -939,6 +1094,152 @@ 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 filesystem( + self, + instance_id: str, + *, + command: str, + path: typing.Optional[str] = OMIT, + content: typing.Optional[str] = OMIT, + mode: typing.Optional[str] = OMIT, + encoding: typing.Optional[str] = OMIT, + view_range: typing.Optional[typing.Sequence[int]] = OMIT, + recursive: typing.Optional[bool] = OMIT, + src: typing.Optional[str] = OMIT, + dst: typing.Optional[str] = OMIT, + old_str: typing.Optional[str] = OMIT, + new_str: typing.Optional[str] = OMIT, + line: typing.Optional[int] = OMIT, + text: typing.Optional[str] = OMIT, + lines: typing.Optional[typing.Sequence[int]] = OMIT, + all_occurrences: typing.Optional[bool] = OMIT, + pattern: typing.Optional[str] = OMIT, + case_sensitive: typing.Optional[bool] = OMIT, + line_numbers: typing.Optional[bool] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> FilesystemResponse: + """ + Parameters + ---------- + instance_id : str + + command : str + + path : typing.Optional[str] + + content : typing.Optional[str] + + mode : typing.Optional[str] + + encoding : typing.Optional[str] + + view_range : typing.Optional[typing.Sequence[int]] + + recursive : typing.Optional[bool] + + src : typing.Optional[str] + + dst : typing.Optional[str] + + old_str : typing.Optional[str] + + new_str : typing.Optional[str] + + line : typing.Optional[int] + + text : typing.Optional[str] + + lines : typing.Optional[typing.Sequence[int]] + + all_occurrences : typing.Optional[bool] + + pattern : typing.Optional[str] + + case_sensitive : typing.Optional[bool] + + line_numbers : typing.Optional[bool] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + FilesystemResponse + Successful Response + + Examples + -------- + import asyncio + + from scrapybara import AsyncScrapybara + + client = AsyncScrapybara( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.instance.filesystem( + instance_id="instance_id", + command="command", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + f"v1/instance/{jsonable_encoder(instance_id)}/filesystem", + method="POST", + json={ + "command": command, + "path": path, + "content": content, + "mode": mode, + "encoding": encoding, + "view_range": view_range, + "recursive": recursive, + "src": src, + "dst": dst, + "old_str": old_str, + "new_str": new_str, + "line": line, + "text": text, + "lines": lines, + "all_occurrences": all_occurrences, + "pattern": pattern, + "case_sensitive": case_sensitive, + "line_numbers": line_numbers, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return typing.cast( + FilesystemResponse, + parse_obj_as( + type_=FilesystemResponse, # 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/instance/types/request.py b/src/scrapybara/instance/types/request.py index 141e199..f2fef5b 100644 --- a/src/scrapybara/instance/types/request.py +++ b/src/scrapybara/instance/types/request.py @@ -13,6 +13,7 @@ class Request_MoveMouse(UniversalBaseModel): action: typing.Literal["move_mouse"] = "move_mouse" coordinates: typing.List[int] hold_keys: typing.Optional[typing.List[str]] = None + screenshot: typing.Optional[bool] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 @@ -31,6 +32,7 @@ class Request_ClickMouse(UniversalBaseModel): coordinates: typing.Optional[typing.List[int]] = None num_clicks: typing.Optional[int] = None hold_keys: typing.Optional[typing.List[str]] = None + screenshot: typing.Optional[bool] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 @@ -46,6 +48,7 @@ class Request_DragMouse(UniversalBaseModel): action: typing.Literal["drag_mouse"] = "drag_mouse" path: typing.List[typing.List[int]] hold_keys: typing.Optional[typing.List[str]] = None + screenshot: typing.Optional[bool] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 @@ -63,6 +66,7 @@ class Request_Scroll(UniversalBaseModel): delta_x: typing.Optional[float] = None delta_y: typing.Optional[float] = None hold_keys: typing.Optional[typing.List[str]] = None + screenshot: typing.Optional[bool] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 @@ -78,6 +82,7 @@ class Request_PressKey(UniversalBaseModel): action: typing.Literal["press_key"] = "press_key" keys: typing.List[str] duration: typing.Optional[float] = None + screenshot: typing.Optional[bool] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 @@ -93,6 +98,7 @@ class Request_TypeText(UniversalBaseModel): action: typing.Literal["type_text"] = "type_text" text: str hold_keys: typing.Optional[typing.List[str]] = None + screenshot: typing.Optional[bool] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 @@ -107,6 +113,7 @@ class Config: class Request_Wait(UniversalBaseModel): action: typing.Literal["wait"] = "wait" duration: float + screenshot: typing.Optional[bool] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/scrapybara/tools/__init__.py b/src/scrapybara/tools/__init__.py index 5d8d605..c83f8c9 100644 --- a/src/scrapybara/tools/__init__.py +++ b/src/scrapybara/tools/__init__.py @@ -4,7 +4,7 @@ from ..types import Action, Button, ClickMouseActionClickType, Tool from ..client import BaseInstance, UbuntuInstance from ..instance.types import Command - +from typing import Literal class ComputerToolParameters(BaseModel): """Parameters for computer interaction commands.""" @@ -164,8 +164,10 @@ def __call__(self, **kwargs: Any) -> Any: class BashToolParameters(BaseModel): """Parameters for bash command execution.""" - command: str = Field(description="The bash command to execute") + command: Optional[str] = Field(description="The bash command to execute") restart: Optional[bool] = Field(False, description="Whether to restart the shell") + get_background_processes: Optional[bool] = Field(None, description="Retrieve information (pid, status, command) about background processes") + kill_pid: Optional[int] = Field(None, description="Process ID to kill") class BashTool(Tool): @@ -185,4 +187,144 @@ def __init__(self, instance: UbuntuInstance) -> None: def __call__(self, **kwargs: Any) -> Any: params = BashToolParameters.model_validate(kwargs) - return self._instance.bash(command=params.command, restart=params.restart) + return self._instance.bash( + command=params.command, + restart=params.restart, + get_background_processes=params.get_background_processes, + kill_pid=params.kill_pid + ) + + +class FilesystemToolParameters(BaseModel): + """Parameters for filesystem operations.""" + + command: Literal[ + "read", "write", "append", "delete", "exists", "list", "mkdir", "rmdir", "move", "copy", + "view", "create", "replace", "insert", "delete_lines", "undo", "grep" + ] = Field( + description="The filesystem command to execute. Determines which other parameters are required or optional. Supported commands: read, write, append, delete, exists, list, mkdir, rmdir, move, copy, view, create, replace, insert, delete_lines, undo, grep." + ) + + path: Optional[str] = Field( + None, + description="Path to the file or directory. Required for commands: read, write, append, delete, exists, list, mkdir, rmdir, view, create, replace, insert, delete_lines, undo, grep." + ) + + content: Optional[str] = Field( + None, + description="Content to write, append, or create in a file. Required for commands: write, append, create." + ) + + mode: Optional[str] = Field( + None, + description="Mode for file operations ('text' or 'binary'). Optional for commands: read, write, append, create. Defaults to 'text' if not specified." + ) + + encoding: Optional[str] = Field( + None, + description="Encoding for text operations (e.g., 'utf-8'). Optional for commands: read, write, append, create. Defaults to 'utf-8' if not specified." + ) + + view_range: Optional[List[int]] = Field( + None, + description="Range of lines to view as [start, end]. Optional for command: view. Not applicable if viewing a directory." + ) + + recursive: Optional[bool] = Field( + None, + description="Whether to perform the operation recursively. Optional for command: delete (defaults to False); required to be True for grep when searching directories." + ) + + src: Optional[str] = Field( + None, + description="Source path for move and copy operations. Required for commands: move, copy." + ) + + dst: Optional[str] = Field( + None, + description="Destination path for move and copy operations. Required for commands: move, copy." + ) + + old_str: Optional[str] = Field( + None, + description="String to be replaced in the file. Required for command: replace." + ) + + new_str: Optional[str] = Field( + None, + description="Replacement string. Required for command: replace." + ) + + line: Optional[int] = Field( + None, + description="Line number where text should be inserted (1-based index). Required for command: insert." + ) + + text: Optional[str] = Field( + None, + description="Text to insert at the specified line. Required for command: insert." + ) + + lines: Optional[List[int]] = Field( + None, + description="List of line numbers to delete (1-based indices). Required for command: delete_lines." + ) + + all_occurrences: Optional[bool] = Field( + None, + description="Whether to replace all occurrences of old_str. Optional for command: replace. Defaults to False, replacing only the first occurrence." + ) + + pattern: Optional[str] = Field( + None, + description="Regex pattern to search for in files. Required for command: grep." + ) + + case_sensitive: Optional[bool] = Field( + None, + description="Whether the grep search is case-sensitive. Optional for command: grep. Defaults to True." + ) + + line_numbers: Optional[bool] = Field( + None, + description="Whether to include line numbers in grep output. Optional for command: grep. Defaults to True." + ) + + +class FilesystemTool(Tool): + """A filesystem manipulation tool that allows the agent to perform various filesystem operations. + + Available for Ubuntu instances.""" + + _instance: UbuntuInstance + + def __init__(self, instance: UbuntuInstance) -> None: + super().__init__( + name="filesystem", + description="Perform filesystem operations like reading, writing, and manipulating files", + parameters=FilesystemToolParameters, + ) + self._instance = instance + + def __call__(self, **kwargs: Any) -> Any: + params = FilesystemToolParameters.model_validate(kwargs) + return self._instance.filesystem( + command=params.command, + path=params.path, + content=params.content, + mode=params.mode, + encoding=params.encoding, + view_range=params.view_range, + recursive=params.recursive, + src=params.src, + dst=params.dst, + old_str=params.old_str, + new_str=params.new_str, + line=params.line, + text=params.text, + lines=params.lines, + all_occurrences=params.all_occurrences, + pattern=params.pattern, + case_sensitive=params.case_sensitive, + line_numbers=params.line_numbers, + ) diff --git a/src/scrapybara/types/__init__.py b/src/scrapybara/types/__init__.py index 93d8c21..8221f74 100644 --- a/src/scrapybara/types/__init__.py +++ b/src/scrapybara/types/__init__.py @@ -17,6 +17,7 @@ from .execute_cell_request import ExecuteCellRequest from .file_download_response import FileDownloadResponse from .file_read_response import FileReadResponse +from .filesystem_response import FilesystemResponse from .get_cursor_position_action import GetCursorPositionAction from .get_instance_response import GetInstanceResponse from .get_instance_response_instance_type import GetInstanceResponseInstanceType @@ -93,6 +94,7 @@ "ExecuteCellRequest", "FileDownloadResponse", "FileReadResponse", + "FilesystemResponse", "GetCursorPositionAction", "GetInstanceResponse", "GetInstanceResponseInstanceType", diff --git a/src/scrapybara/types/click_mouse_action.py b/src/scrapybara/types/click_mouse_action.py index fdb5b8a..df7299b 100644 --- a/src/scrapybara/types/click_mouse_action.py +++ b/src/scrapybara/types/click_mouse_action.py @@ -14,6 +14,7 @@ class ClickMouseAction(UniversalBaseModel): coordinates: typing.Optional[typing.List[int]] = None num_clicks: typing.Optional[int] = None hold_keys: typing.Optional[typing.List[str]] = None + screenshot: typing.Optional[bool] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/scrapybara/types/drag_mouse_action.py b/src/scrapybara/types/drag_mouse_action.py index 354caec..9daf7dd 100644 --- a/src/scrapybara/types/drag_mouse_action.py +++ b/src/scrapybara/types/drag_mouse_action.py @@ -9,6 +9,7 @@ class DragMouseAction(UniversalBaseModel): path: typing.List[typing.List[int]] hold_keys: typing.Optional[typing.List[str]] = None + screenshot: typing.Optional[bool] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/scrapybara/types/filesystem_response.py b/src/scrapybara/types/filesystem_response.py new file mode 100644 index 0000000..a9d9f8b --- /dev/null +++ b/src/scrapybara/types/filesystem_response.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +from ..core.pydantic_utilities import UniversalBaseModel +import typing +import typing_extensions +from ..core.serialization import FieldMetadata +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +import pydantic + + +class FilesystemResponse(UniversalBaseModel): + """ + Response model for filesystem actions. + """ + + output: typing.Optional[str] = None + error: typing.Optional[str] = None + base_64_image: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="base64_image")] = None + system: 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/scrapybara/types/get_instance_response.py b/src/scrapybara/types/get_instance_response.py index a0b60c4..d7850c4 100644 --- a/src/scrapybara/types/get_instance_response.py +++ b/src/scrapybara/types/get_instance_response.py @@ -4,8 +4,8 @@ import datetime as dt from .get_instance_response_instance_type import GetInstanceResponseInstanceType from .status import Status -from ..core.pydantic_utilities import IS_PYDANTIC_V2 import typing +from ..core.pydantic_utilities import IS_PYDANTIC_V2 import pydantic @@ -14,6 +14,7 @@ class GetInstanceResponse(UniversalBaseModel): launch_time: dt.datetime instance_type: GetInstanceResponseInstanceType status: Status + resolution: typing.Optional[typing.List[int]] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/scrapybara/types/move_mouse_action.py b/src/scrapybara/types/move_mouse_action.py index 66e0335..d814c82 100644 --- a/src/scrapybara/types/move_mouse_action.py +++ b/src/scrapybara/types/move_mouse_action.py @@ -9,6 +9,7 @@ class MoveMouseAction(UniversalBaseModel): coordinates: typing.List[int] hold_keys: typing.Optional[typing.List[str]] = None + screenshot: typing.Optional[bool] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/scrapybara/types/press_key_action.py b/src/scrapybara/types/press_key_action.py index 432eea0..2d36fe4 100644 --- a/src/scrapybara/types/press_key_action.py +++ b/src/scrapybara/types/press_key_action.py @@ -9,6 +9,7 @@ class PressKeyAction(UniversalBaseModel): keys: typing.List[str] duration: typing.Optional[float] = None + screenshot: typing.Optional[bool] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/scrapybara/types/scroll_action.py b/src/scrapybara/types/scroll_action.py index 9bee45f..5b20666 100644 --- a/src/scrapybara/types/scroll_action.py +++ b/src/scrapybara/types/scroll_action.py @@ -11,6 +11,7 @@ class ScrollAction(UniversalBaseModel): delta_x: typing.Optional[float] = None delta_y: typing.Optional[float] = None hold_keys: typing.Optional[typing.List[str]] = None + screenshot: typing.Optional[bool] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/scrapybara/types/status.py b/src/scrapybara/types/status.py index 3fb3c9c..0c98034 100644 --- a/src/scrapybara/types/status.py +++ b/src/scrapybara/types/status.py @@ -2,4 +2,4 @@ import typing -Status = typing.Union[typing.Literal["deploying", "running", "paused", "terminated", "error"], typing.Any] +Status = typing.Union[typing.Literal["deploying", "running", "paused", "terminated", "error", "warm_pool"], typing.Any] diff --git a/src/scrapybara/types/type_text_action.py b/src/scrapybara/types/type_text_action.py index 6bf9e52..17f4758 100644 --- a/src/scrapybara/types/type_text_action.py +++ b/src/scrapybara/types/type_text_action.py @@ -9,6 +9,7 @@ class TypeTextAction(UniversalBaseModel): text: str hold_keys: typing.Optional[typing.List[str]] = None + screenshot: typing.Optional[bool] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/scrapybara/types/wait_action.py b/src/scrapybara/types/wait_action.py index 23e1131..e18b548 100644 --- a/src/scrapybara/types/wait_action.py +++ b/src/scrapybara/types/wait_action.py @@ -1,13 +1,14 @@ # This file was auto-generated by Fern from our API Definition. from ..core.pydantic_utilities import UniversalBaseModel -from ..core.pydantic_utilities import IS_PYDANTIC_V2 import typing +from ..core.pydantic_utilities import IS_PYDANTIC_V2 import pydantic class WaitAction(UniversalBaseModel): duration: float + screenshot: typing.Optional[bool] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2