smooth-py 0.2.1.post0.dev20250911__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of smooth-py might be problematic. Click here for more details.

@@ -1,19 +1,20 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: smooth-py
3
- Version: 0.2.1.post0.dev20250911
3
+ Version: 0.2.3
4
4
  Summary:
5
5
  Author: Luca Pinchetti
6
6
  Author-email: luca@circlemind.co
7
- Requires-Python: >=3.10
7
+ Requires-Python: >=3.10,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
13
  Provides-Extra: mcp
14
- Requires-Dist: fastmcp (>=2.0.0,<3.0.0) ; extra == "mcp"
14
+ Requires-Dist: fastmcp (>=2.12.0,<3.0.0) ; extra == "mcp"
15
15
  Requires-Dist: httpx (>=0.28.1,<0.29.0)
16
16
  Requires-Dist: pydantic (>=2.11.7,<3.0.0)
17
+ Requires-Dist: requests (>=2.32.5,<3.0.0)
17
18
  Description-Content-Type: text/markdown
18
19
 
19
20
  # Smooth Python SDK
@@ -1,20 +1,21 @@
1
1
  [project]
2
2
  name = "smooth-py"
3
- version = "0.2.1.post0.dev20250911"
3
+ version = "0.2.3"
4
4
  description = ""
5
5
  authors = [
6
6
  {name = "Luca Pinchetti",email = "luca@circlemind.co"}
7
7
  ]
8
8
  readme = "README.md"
9
- requires-python = ">=3.10"
9
+ requires-python = ">=3.10,<4.0"
10
10
  dependencies = [
11
11
  "pydantic (>=2.11.7,<3.0.0)",
12
- "httpx (>=0.28.1,<0.29.0)"
12
+ "httpx (>=0.28.1,<0.29.0)",
13
+ "requests (>=2.32.5,<3.0.0)"
13
14
  ]
14
15
 
15
16
  [project.optional-dependencies]
16
17
  mcp = [
17
- "fastmcp (>=2.0.0,<3.0.0)"
18
+ "fastmcp (>=2.12.0,<3.0.0)"
18
19
  ]
19
20
 
20
21
  [tool.poetry]
@@ -1,6 +1,7 @@
1
1
  """Smooth python SDK."""
2
2
 
3
3
  import asyncio
4
+ import io
4
5
  import logging
5
6
  import os
6
7
  import time
@@ -53,9 +54,14 @@ class TaskRequest(BaseModel):
53
54
  )
54
55
  url: str | None = Field(
55
56
  default=None,
56
- description="(Optional) The starting URL for the task. If not provided, the agent will infer it from the task.",
57
+ description="The starting URL for the task. If not provided, the agent will infer it from the task.",
58
+ )
59
+ metadata: dict[str, str | int | float | bool] | None = Field(
60
+ default=None, description="A dictionary containing variables or parameters that will be passed to the agent."
61
+ )
62
+ files: list[str] | None = Field(
63
+ default=None, description="A dictionary of file ids to pass to the agent."
57
64
  )
58
- metadata: dict[str, str | int | float | bool] | None = Field(default=None, description="Optional metadata for the task.")
59
65
  agent: Literal["smooth"] = Field(default="smooth", description="The agent to use for the task.")
60
66
  max_steps: int = Field(default=32, ge=2, le=128, description="Maximum number of steps the agent can take (min 2, max 128).")
61
67
  device: Literal["desktop", "mobile"] = Field(default="mobile", description="Device type for the task. Default is mobile.")
@@ -98,6 +104,11 @@ class BrowserSessionsResponse(BaseModel):
98
104
  session_ids: list[str] = Field(description="The IDs of the browser sessions.")
99
105
 
100
106
 
107
+ class UploadFileResponse(BaseModel):
108
+ """Response model for listing browser sessions."""
109
+ id: str = Field(description="The ID assigned to the uploaded file.")
110
+
111
+
101
112
  # --- Exception Handling ---
102
113
 
103
114
 
@@ -140,7 +151,6 @@ class BaseClient:
140
151
  self.base_url = f"{base_url.rstrip('/')}/{api_version}"
141
152
  self.headers = {
142
153
  "apikey": self.api_key,
143
- "Content-Type": "application/json",
144
154
  "User-Agent": "smooth-python-sdk/0.2.0",
145
155
  }
146
156
 
@@ -279,6 +289,19 @@ class SmoothClient(BaseClient):
279
289
  logger.error(f"Request failed: {e}")
280
290
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
281
291
 
292
+ def _upload_file(self, file: io.IOBase, name: str) -> UploadFileResponse:
293
+ """Uploads a file and returns the assigned file id."""
294
+ try:
295
+ files = {
296
+ "file": (name, file)
297
+ }
298
+ response = self._session.post(f"{self.base_url}/upload_file", files=files)
299
+ data = self._handle_response(response)
300
+ return UploadFileResponse(**data["r"])
301
+ except requests.exceptions.RequestException as e:
302
+ logger.error(f"Request failed: {e}")
303
+ raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
304
+
282
305
  def _get_task(self, task_id: str) -> TaskResponse:
283
306
  """Retrieves the status and result of a task."""
284
307
  if not task_id:
@@ -298,6 +321,7 @@ class SmoothClient(BaseClient):
298
321
  response_model: dict[str, Any] | Type[BaseModel] | None = None,
299
322
  url: str | None = None,
300
323
  metadata: dict[str, str | int | float | bool] | None = None,
324
+ files: list[str] | None = None,
301
325
  agent: Literal["smooth"] = "smooth",
302
326
  max_steps: int = 32,
303
327
  device: Literal["desktop", "mobile"] = "mobile",
@@ -317,7 +341,8 @@ class SmoothClient(BaseClient):
317
341
  task: The task to run.
318
342
  response_model: If provided, the schema describing the desired output structure.
319
343
  url: The starting URL for the task. If not provided, the agent will infer it from the task.
320
- metadata: Optional metadata for the task.
344
+ metadata: A dictionary containing variables or parameters that will be passed to the agent.
345
+ files: A dictionary of file names to their ids. These files will be passed to the agent.
321
346
  agent: The agent to use for the task.
322
347
  max_steps: Maximum number of steps the agent can take (max 64).
323
348
  device: Device type for the task. Default is mobile.
@@ -339,6 +364,7 @@ class SmoothClient(BaseClient):
339
364
  response_model=response_model.model_json_schema() if issubclass(response_model, BaseModel) else response_model,
340
365
  url=url,
341
366
  metadata=metadata,
367
+ files=files,
342
368
  agent=agent,
343
369
  max_steps=max_steps,
344
370
  device=device,
@@ -399,10 +425,26 @@ class SmoothClient(BaseClient):
399
425
  try:
400
426
  response = self._session.delete(f"{self.base_url}/browser/session/{session_id}")
401
427
  self._handle_response(response)
402
- except httpx.RequestError as e:
428
+ except requests.exceptions.RequestException as e:
403
429
  logger.error(f"Request failed: {e}")
404
430
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
405
431
 
432
+ def upload_file(self, file: io.IOBase, name: str) -> UploadFileResponse:
433
+ """Upload a file and return the file ID.
434
+
435
+ Args:
436
+ file: File object to be uploaded.
437
+ name: The name to assign to the uploaded file.
438
+
439
+ Returns:
440
+ The file ID assigned to the uploaded file.
441
+
442
+ Raises:
443
+ ValueError: If the file doesn't exist or can't be read.
444
+ ApiError: If the API request fails.
445
+ """
446
+ return self._upload_file(file=file, name=name)
447
+
406
448
 
407
449
  # --- Asynchronous Client ---
408
450
 
@@ -497,6 +539,19 @@ class SmoothAsyncClient(BaseClient):
497
539
  logger.error(f"Request failed: {e}")
498
540
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
499
541
 
542
+ async def _upload_file(self, file: io.IOBase, name: str) -> UploadFileResponse:
543
+ """Uploads a file and returns the assigned file id."""
544
+ try:
545
+ files = {
546
+ "file": (name, file)
547
+ }
548
+ response = await self._client.post(f"{self.base_url}/upload_file", files=files)
549
+ data = self._handle_response(response)
550
+ return UploadFileResponse(**data["r"])
551
+ except httpx.RequestError as e:
552
+ logger.error(f"Request failed: {e}")
553
+ raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
554
+
500
555
  async def _get_task(self, task_id: str) -> TaskResponse:
501
556
  """Retrieves the status and result of a task asynchronously."""
502
557
  if not task_id:
@@ -516,6 +571,7 @@ class SmoothAsyncClient(BaseClient):
516
571
  response_model: dict[str, Any] | Type[BaseModel] | None = None,
517
572
  url: str | None = None,
518
573
  metadata: dict[str, str | int | float | bool] | None = None,
574
+ files: list[str] | None = None,
519
575
  agent: Literal["smooth"] = "smooth",
520
576
  max_steps: int = 32,
521
577
  device: Literal["desktop", "mobile"] = "mobile",
@@ -535,7 +591,8 @@ class SmoothAsyncClient(BaseClient):
535
591
  task: The task to run.
536
592
  response_model: If provided, the schema describing the desired output structure.
537
593
  url: The starting URL for the task. If not provided, the agent will infer it from the task.
538
- metadata: Optional metadata for the task.
594
+ metadata: A dictionary containing variables or parameters that will be passed to the agent.
595
+ files: A dictionary of file names to their url or base64-encoded content to be used by the agent.
539
596
  agent: The agent to use for the task.
540
597
  max_steps: Maximum number of steps the agent can take (max 64).
541
598
  device: Device type for the task. Default is mobile.
@@ -559,6 +616,7 @@ class SmoothAsyncClient(BaseClient):
559
616
  response_model=response_model.model_json_schema() if issubclass(response_model, BaseModel) else response_model,
560
617
  url=url,
561
618
  metadata=metadata,
619
+ files=files,
562
620
  agent=agent,
563
621
  max_steps=max_steps,
564
622
  device=device,
@@ -623,6 +681,22 @@ class SmoothAsyncClient(BaseClient):
623
681
  logger.error(f"Request failed: {e}")
624
682
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
625
683
 
684
+ async def upload_file(self, file: io.IOBase, name: str) -> UploadFileResponse:
685
+ """Upload a file and return the file ID.
686
+
687
+ Args:
688
+ file: File object to be uploaded.
689
+ name: The name to assign to the uploaded file.
690
+
691
+ Returns:
692
+ The file ID assigned to the uploaded file.
693
+
694
+ Raises:
695
+ ValueError: If the file doesn't exist or can't be read.
696
+ ApiError: If the API request fails.
697
+ """
698
+ return await self._upload_file(file=file, name=name)
699
+
626
700
  async def close(self):
627
701
  """Closes the async client session."""
628
702
  await self._client.aclose()
@@ -640,6 +714,7 @@ __all__ = [
640
714
  "BrowserSessionRequest",
641
715
  "BrowserSessionResponse",
642
716
  "BrowserSessionsResponse",
717
+ "UploadFileResponse",
643
718
  "ApiError",
644
719
  "TimeoutError",
645
720
  ]