tinyfish 0.2.3__tar.gz → 0.2.5__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.
Files changed (47) hide show
  1. tinyfish-0.2.5/.pre-commit-config.yaml +23 -0
  2. tinyfish-0.2.5/AGENTS.md +1 -0
  3. tinyfish-0.2.5/CLAUDE.md +60 -0
  4. {tinyfish-0.2.3 → tinyfish-0.2.5}/PKG-INFO +1 -1
  5. {tinyfish-0.2.3 → tinyfish-0.2.5}/RELEASE.md +5 -5
  6. tinyfish-0.2.5/docs/internal/api-integration-header.md +58 -0
  7. {tinyfish-0.2.3 → tinyfish-0.2.5}/pyproject.toml +5 -1
  8. {tinyfish-0.2.3 → tinyfish-0.2.5}/src/tinyfish/__init__.py +23 -2
  9. {tinyfish-0.2.3 → tinyfish-0.2.5}/src/tinyfish/_utils/client/_base.py +12 -1
  10. {tinyfish-0.2.3 → tinyfish-0.2.5}/src/tinyfish/_utils/client/async_.py +2 -0
  11. {tinyfish-0.2.3 → tinyfish-0.2.5}/src/tinyfish/_utils/client/sync.py +2 -0
  12. {tinyfish-0.2.3 → tinyfish-0.2.5}/src/tinyfish/agent/__init__.py +50 -12
  13. {tinyfish-0.2.3 → tinyfish-0.2.5}/src/tinyfish/agent/types.py +22 -9
  14. tinyfish-0.2.5/src/tinyfish/browser/__init__.py +86 -0
  15. tinyfish-0.2.5/src/tinyfish/browser/types.py +24 -0
  16. {tinyfish-0.2.3 → tinyfish-0.2.5}/src/tinyfish/client.py +15 -0
  17. tinyfish-0.2.5/src/tinyfish/fetch/__init__.py +99 -0
  18. tinyfish-0.2.5/src/tinyfish/fetch/types.py +40 -0
  19. tinyfish-0.2.5/src/tinyfish/search/__init__.py +79 -0
  20. tinyfish-0.2.5/src/tinyfish/search/types.py +21 -0
  21. {tinyfish-0.2.3 → tinyfish-0.2.5}/tests/test_agent.py +3 -3
  22. tinyfish-0.2.5/tests/test_browser.py +141 -0
  23. {tinyfish-0.2.3 → tinyfish-0.2.5}/tests/test_client.py +43 -0
  24. tinyfish-0.2.5/tests/test_fetch.py +267 -0
  25. tinyfish-0.2.5/tests/test_search.py +229 -0
  26. {tinyfish-0.2.3 → tinyfish-0.2.5}/uv.lock +1 -1
  27. {tinyfish-0.2.3 → tinyfish-0.2.5}/.gitignore +0 -0
  28. {tinyfish-0.2.3 → tinyfish-0.2.5}/README.md +0 -0
  29. {tinyfish-0.2.3 → tinyfish-0.2.5}/docs/exceptions-and-errors-guide.md +0 -0
  30. {tinyfish-0.2.3 → tinyfish-0.2.5}/docs/internal/exceptions-and-errors-guide.md +0 -0
  31. {tinyfish-0.2.3 → tinyfish-0.2.5}/docs/internal/publishing-private-guide.md +0 -0
  32. {tinyfish-0.2.3 → tinyfish-0.2.5}/docs/pagination-guide.md +0 -0
  33. {tinyfish-0.2.3 → tinyfish-0.2.5}/docs/proxy-and-browser-profiles.md +0 -0
  34. {tinyfish-0.2.3 → tinyfish-0.2.5}/docs/streaming-guide.md +0 -0
  35. {tinyfish-0.2.3 → tinyfish-0.2.5}/src/tinyfish/_utils/__init__.py +0 -0
  36. {tinyfish-0.2.3 → tinyfish-0.2.5}/src/tinyfish/_utils/client/__init__.py +0 -0
  37. {tinyfish-0.2.3 → tinyfish-0.2.5}/src/tinyfish/_utils/exceptions.py +0 -0
  38. {tinyfish-0.2.3 → tinyfish-0.2.5}/src/tinyfish/_utils/resource.py +0 -0
  39. {tinyfish-0.2.3 → tinyfish-0.2.5}/src/tinyfish/_utils/sse_parser.py +0 -0
  40. {tinyfish-0.2.3 → tinyfish-0.2.5}/src/tinyfish/py.typed +0 -0
  41. {tinyfish-0.2.3 → tinyfish-0.2.5}/src/tinyfish/runs/__init__.py +0 -0
  42. {tinyfish-0.2.3 → tinyfish-0.2.5}/src/tinyfish/runs/types.py +0 -0
  43. {tinyfish-0.2.3 → tinyfish-0.2.5}/tests/__init__.py +0 -0
  44. {tinyfish-0.2.3 → tinyfish-0.2.5}/tests/conftest.py +0 -0
  45. {tinyfish-0.2.3 → tinyfish-0.2.5}/tests/test_errors.py +0 -0
  46. {tinyfish-0.2.3 → tinyfish-0.2.5}/tests/test_runs.py +0 -0
  47. {tinyfish-0.2.3 → tinyfish-0.2.5}/tests/testing_guide.md +0 -0
@@ -0,0 +1,23 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-yaml
8
+ - id: check-added-large-files
9
+ - id: check-toml
10
+ - id: debug-statements
11
+
12
+ - repo: https://github.com/astral-sh/ruff-pre-commit
13
+ rev: v0.15.5
14
+ hooks:
15
+ - id: ruff-check
16
+ - id: ruff-format
17
+
18
+ - repo: https://github.com/astral-sh/uv-pre-commit
19
+ rev: 0.10.9
20
+ hooks:
21
+ - id: uv-lock
22
+ name: uv-lock
23
+ files: ^sdk/sdk-python/(uv\.lock|pyproject\.toml)$
@@ -0,0 +1 @@
1
+ CLAUDE.md
@@ -0,0 +1,60 @@
1
+ # TinyFish Python SDK (`tinyfish`)
2
+
3
+ Official Python SDK for TinyFish. Provides sync (`TinyFish`) and async (`AsyncTinyFish`) clients for running
4
+ automations, streaming events, and managing runs.
5
+
6
+ For full method reference and examples: see `README.md`.
7
+
8
+ ---
9
+
10
+ ## Package Manager
11
+
12
+ `uv` — not pip, not poetry.
13
+
14
+ ## Key Entry Points
15
+
16
+ ```
17
+ src/tinyfish/__init__.py → exports TinyFish, AsyncTinyFish, and all public types
18
+ src/tinyfish/client.py → TinyFish client class (sync)
19
+ src/tinyfish/agent/ → agent.run(), agent.queue(), agent.stream()
20
+ src/tinyfish/runs/ → runs.get(), runs.list()
21
+ src/tinyfish/_utils/ → HTTP client base (sync + async), retry, error handling
22
+ ```
23
+
24
+ ## Quick Usage
25
+
26
+ ```python
27
+ from tinyfish import TinyFish
28
+
29
+ client = TinyFish(api_key="your-api-key") # or set TINYFISH_API_KEY env var
30
+ response = client.agent.run(goal="...", url="https://...")
31
+ print(response.result)
32
+ ```
33
+
34
+ ## Tests
35
+
36
+ ```bash
37
+ cd sdk/sdk-python && uv run pytest
38
+ ```
39
+
40
+ ## Pre-commit Hooks
41
+
42
+ ```bash
43
+ cd sdk/sdk-python && pre-commit install
44
+ ```
45
+
46
+ Hooks: `ruff format`, `ruff check`, `uv-lock` sync. Run before first commit and after any dep change.
47
+
48
+ ## Lint / Format
49
+
50
+ ```bash
51
+ cd sdk/sdk-python && uv run ruff check . && uv run ruff format .
52
+ ```
53
+
54
+ ## Package name
55
+
56
+ `tinyfish` (PyPI). The npm org uses a hyphen: `@tiny-fish/` (not `@tinyfish/`).
57
+
58
+ ## Further Reading
59
+
60
+ See `README.md` for full usage examples, authentication patterns, and SDK changelog.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinyfish
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: Official Python SDK for the TinyFish API
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: httpx>=0.27.0
@@ -33,13 +33,13 @@ Commit and merge to main.
33
33
  ### Step 2: Create a GitHub Release
34
34
 
35
35
  1. Go to the `ux-labs` repo on GitHub → **Releases** → **Draft a new release**
36
- 2. Set the tag to match the version exactly, prefixed with `v`:
37
- - Version `0.2.0` → tag `v0.2.0`
36
+ 2. Set the tag to match the version exactly, prefixed with `sdk-py/v`:
37
+ - Version `0.2.0` → tag `sdk-py/v0.2.0`
38
38
  3. Set the target to `main`
39
39
  4. Write release notes summarizing what changed
40
40
  5. Click **Publish release**
41
41
 
42
- The workflow validates that the tag matches the version in `pyproject.toml` — if they don't match, the build fails before anything is published.
42
+ The CD workflow (`CD_sdk_python.yml`) only triggers on tags prefixed with `sdk-py/v`. It validates that the version portion of the tag matches `pyproject.toml` — if they don't match, the build fails before anything is published.
43
43
 
44
44
  ### Step 3: Monitor the workflow
45
45
 
@@ -56,13 +56,13 @@ After the workflow completes, verify the release:
56
56
 
57
57
  ```bash
58
58
  pip install tinyfish==0.2.0
59
- python -c "from tinyfish import Tinyfish; print('ok')"
59
+ python -c "from tinyfish import TinyFish; print('ok')"
60
60
  ```
61
61
 
62
62
  ## Troubleshooting
63
63
 
64
64
  **Tag does not match pyproject.toml version**
65
- The build job validates that the git tag (e.g., `v0.2.0`) matches the version in `pyproject.toml` (e.g., `0.2.0`). Fix by updating `pyproject.toml` to match the tag before publishing, then delete and recreate the release.
65
+ The build job validates that the git tag (e.g., `sdk-py/v0.2.0`) matches the version in `pyproject.toml` (e.g., `0.2.0`). Fix by updating `pyproject.toml` to match the tag before publishing, then delete and recreate the release.
66
66
 
67
67
  **403 / authentication error**
68
68
  Verify that `PYPI_API_TOKEN` has been added to the `ux-labs` repo secrets in `github-control`. The token must exist as a GitHub Actions secret before the workflow can publish.
@@ -0,0 +1,58 @@
1
+ # API Integration Tagging (`TF_API_INTEGRATION`)
2
+
3
+ Internal-only mechanism for identifying which integration platform is making API requests through our SDK.
4
+
5
+ ## How it works
6
+
7
+ The SDK reads the `TF_API_INTEGRATION` environment variable when building request bodies. When set, the SDK injects `api_integration` into every request body sent to the automation endpoints.
8
+
9
+ ```text
10
+ TF_API_INTEGRATION=n8n --> { "goal": "...", "url": "...", "api_integration": "n8n" }
11
+ ```
12
+
13
+ This reuses the existing `api_integration` body field that the server already parses, stores in the DB, and emits to PostHog — no backend changes needed.
14
+
15
+ This is intentionally **not** exposed as a constructor parameter or public API. It does not appear in editor autocomplete, type definitions, or documentation. Only we control which integrations set this value.
16
+
17
+ ## Environment variable
18
+
19
+ | Variable | Example values | Required |
20
+ | --------------------- | ---------------------- | -------- |
21
+ | `TF_API_INTEGRATION` | `n8n`, `dify`, `zapier` | No |
22
+
23
+ When unset or empty, `api_integration` is omitted from the body. Existing behavior is unchanged.
24
+
25
+ ## Where it's read
26
+
27
+ `src/tinyfish/_utils/client/_base.py` — `_inject_integration()`, called from `_request()` in both sync and async clients:
28
+
29
+ ```python
30
+ integration = os.environ.get("TF_API_INTEGRATION", "").strip()
31
+ if integration:
32
+ json["api_integration"] = integration
33
+ ```
34
+
35
+ This runs on every API request automatically — no per-resource setup needed.
36
+
37
+ ## Server-side flow
38
+
39
+ 1. Server validates `api_integration` via Zod schema (`frontend/app/v1/schemas.ts`)
40
+ 2. `buildRunOptions()` extracts it from the request body
41
+ 3. Stored in `runs.api_integration` DB column
42
+ 4. Emitted on `run_completed` PostHog event as `api_integration` property
43
+
44
+ ## Setting up a new integration
45
+
46
+ To tag requests from a new integration partner:
47
+
48
+ 1. Set `TF_API_INTEGRATION=<name>` in the environment where the SDK runs (e.g., the n8n node, Dify plugin, Zapier action)
49
+ 2. No SDK code changes needed — the env var is picked up automatically
50
+ 3. The value will appear in PostHog under `run_completed.api_integration`
51
+
52
+ ## Tests
53
+
54
+ `tests/test_client.py`:
55
+
56
+ - `test_integration_in_body_via_env` — `api_integration` is in the request body when env var is set
57
+ - `test_no_integration_in_body_by_default` — `api_integration` is absent when env var is not set
58
+ - `test_async_integration_in_body_via_env` — async client variant
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tinyfish"
3
- version = "0.2.3"
3
+ version = "0.2.5"
4
4
  description = "Official Python SDK for the TinyFish API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -33,6 +33,10 @@ asyncio_default_fixture_loop_scope = "session"
33
33
  [tool.ruff]
34
34
  src = ["src"]
35
35
  line-length = 120
36
+ [tool.ruff.format]
37
+ quote-style = "double"
38
+ docstring-code-format = true
39
+ docstring-code-line-length = 20
36
40
 
37
41
  [tool.ruff.lint]
38
42
  select = ["E", "F", "I", "UP", "PYI", "TID"]
@@ -26,8 +26,6 @@ from ._utils.exceptions import (
26
26
 
27
27
  # Agent resource
28
28
  from .agent import AgentStream, AsyncAgentStream
29
-
30
- # Agent types
31
29
  from .agent.types import (
32
30
  AgentRunAsyncResponse,
33
31
  AgentRunResponse,
@@ -42,7 +40,16 @@ from .agent.types import (
42
40
  StartedEvent,
43
41
  StreamingUrlEvent,
44
42
  )
43
+
44
+ # Browser types
45
+ from .browser.types import BrowserSession, BrowserSessionCreateParams
45
46
  from .client import AsyncTinyFish, TinyFish
47
+ from .fetch.types import (
48
+ FetchError,
49
+ FetchFormat,
50
+ FetchResponse,
51
+ FetchResult,
52
+ )
46
53
 
47
54
  # Runs types
48
55
  from .runs.types import (
@@ -55,6 +62,9 @@ from .runs.types import (
55
62
  SortDirection,
56
63
  )
57
64
 
65
+ # Search types
66
+ from .search.types import SearchQueryResponse, SearchResult
67
+
58
68
  __version__ = version("tinyfish")
59
69
 
60
70
  __all__ = [
@@ -80,6 +90,9 @@ __all__ = [
80
90
  # Agent resource
81
91
  "AgentStream",
82
92
  "AsyncAgentStream",
93
+ # Browser types
94
+ "BrowserSession",
95
+ "BrowserSessionCreateParams",
83
96
  # Agent types
84
97
  "EventType",
85
98
  "BrowserProfile",
@@ -93,6 +106,14 @@ __all__ = [
93
106
  "HeartbeatEvent",
94
107
  "CompleteEvent",
95
108
  "AgentRunWithStreamingResponse",
109
+ # Fetch types
110
+ "FetchFormat",
111
+ "FetchResult",
112
+ "FetchError",
113
+ "FetchResponse",
114
+ # Search types
115
+ "SearchResult",
116
+ "SearchQueryResponse",
96
117
  # Runs types
97
118
  "ErrorCategory",
98
119
  "SortDirection",
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  from importlib.metadata import version as _pkg_version
7
- from typing import TypeVar
7
+ from typing import Any, TypeVar
8
8
 
9
9
  import httpx
10
10
  from pydantic import BaseModel
@@ -79,8 +79,19 @@ class _BaseClient:
79
79
  "Accept": "application/json",
80
80
  "X-API-Key": self._api_key,
81
81
  "User-Agent": f"tinyfish-python/{_pkg_version('tinyfish')}",
82
+ "X-TF-Request-Origin": "tinyfish-python",
82
83
  }
83
84
 
85
+ @staticmethod
86
+ def _inject_integration(json: dict[str, Any] | None) -> dict[str, Any] | None:
87
+ """Inject api_integration from TF_API_INTEGRATION env var into JSON body."""
88
+ if json is None:
89
+ return None
90
+ integration = os.environ.get("TF_API_INTEGRATION", "").strip()
91
+ if integration:
92
+ json["api_integration"] = integration
93
+ return json
94
+
84
95
  def _make_status_error(self, response: httpx.Response) -> APIStatusError:
85
96
  """
86
97
  Translate an error HTTP response into the appropriate SDK exception.
@@ -72,6 +72,7 @@ class BaseAsyncAPIClient(_BaseClient):
72
72
  APIConnectionError: Network/connection error
73
73
  APIStatusError: HTTP error status (4xx, 5xx)
74
74
  """
75
+ json = self._inject_integration(json)
75
76
  max_attempts = self._max_retries + 1
76
77
 
77
78
  # Retryable: 408, 429, 5xx, and network errors (TimeoutException ⊂ RequestError).
@@ -142,6 +143,7 @@ class BaseAsyncAPIClient(_BaseClient):
142
143
  matching the retry behaviour of ``_request()``. Once a 200 response
143
144
  is received and streaming begins, no further retries are attempted.
144
145
  """
146
+ json = self._inject_integration(json)
145
147
  max_attempts = self._max_retries + 1
146
148
 
147
149
  @retry(
@@ -72,6 +72,7 @@ class BaseSyncAPIClient(_BaseClient):
72
72
  APIConnectionError: Network/connection error
73
73
  APIStatusError: HTTP error status (4xx, 5xx)
74
74
  """
75
+ json = self._inject_integration(json)
75
76
  max_attempts = self._max_retries + 1
76
77
 
77
78
  # Retryable: 408, 429, 5xx, and network errors (TimeoutException ⊂ RequestError).
@@ -142,6 +143,7 @@ class BaseSyncAPIClient(_BaseClient):
142
143
  matching the retry behaviour of ``_request()``. Once a 200 response
143
144
  is received and streaming begins, no further retries are attempted.
144
145
  """
146
+ json = self._inject_integration(json)
145
147
  max_attempts = self._max_retries + 1
146
148
 
147
149
  @retry(
@@ -41,8 +41,15 @@ class AgentStream:
41
41
 
42
42
  Use as::
43
43
 
44
- with client.agent.stream(goal=..., url=...) as stream:
45
- for event in stream:
44
+ with (
45
+ client.agent.stream(
46
+ goal=...,
47
+ url=...,
48
+ ) as stream
49
+ ):
50
+ for (
51
+ event
52
+ ) in stream:
46
53
  ...
47
54
  """
48
55
 
@@ -64,8 +71,15 @@ class AsyncAgentStream:
64
71
 
65
72
  Use as::
66
73
 
67
- async with client.agent.stream(goal=..., url=...) as stream:
68
- async for event in stream:
74
+ async with (
75
+ client.agent.stream(
76
+ goal=...,
77
+ url=...,
78
+ ) as stream
79
+ ):
80
+ async for (
81
+ event
82
+ ) in stream:
69
83
  ...
70
84
  """
71
85
 
@@ -166,10 +180,22 @@ class AgentResource(BaseSyncAPIResource):
166
180
  Use the on_* callbacks for a reactive style, or iterate over
167
181
  the stream for a sequential style::
168
182
 
169
- with client.agent.stream(goal=..., url=...) as stream:
170
- for event in stream:
171
- if isinstance(event, ProgressEvent):
172
- print(event.purpose)
183
+ with (
184
+ client.agent.stream(
185
+ goal=...,
186
+ url=...,
187
+ ) as stream
188
+ ):
189
+ for (
190
+ event
191
+ ) in stream:
192
+ if isinstance(
193
+ event,
194
+ ProgressEvent,
195
+ ):
196
+ print(
197
+ event.purpose
198
+ )
173
199
 
174
200
  Args:
175
201
  goal: Natural language description of what to do on the page.
@@ -309,10 +335,22 @@ class AsyncAgentResource(BaseAsyncAPIResource):
309
335
  Use the on_* callbacks for a reactive style, or iterate over
310
336
  the stream for a sequential style::
311
337
 
312
- async with client.agent.stream(goal=..., url=...) as stream:
313
- async for event in stream:
314
- if isinstance(event, ProgressEvent):
315
- print(event.purpose)
338
+ async with (
339
+ client.agent.stream(
340
+ goal=...,
341
+ url=...,
342
+ ) as stream
343
+ ):
344
+ async for (
345
+ event
346
+ ) in stream:
347
+ if isinstance(
348
+ event,
349
+ ProgressEvent,
350
+ ):
351
+ print(
352
+ event.purpose
353
+ )
316
354
 
317
355
  Args:
318
356
  goal: Natural language description of what to do on the page.
@@ -67,12 +67,19 @@ class AgentRunResponse(BaseModel):
67
67
  ```python
68
68
  response = client.agent.run(
69
69
  goal="Find the price of iPhone 15",
70
- url="https://www.apple.com"
70
+ url="https://www.apple.com",
71
71
  )
72
- if response.status == "COMPLETED":
73
- print(response.result)
72
+ if (
73
+ response.status
74
+ == "COMPLETED"
75
+ ):
76
+ print(
77
+ response.result
78
+ )
74
79
  else:
75
- print(f"Failed: {response.error.message}")
80
+ print(
81
+ f"Failed: {response.error.message}"
82
+ )
76
83
  ```
77
84
  """
78
85
 
@@ -103,13 +110,19 @@ class AgentRunAsyncResponse(BaseModel):
103
110
  ```python
104
111
  response = client.agent.queue(
105
112
  goal="Extract product details",
106
- url="https://example.com"
113
+ url="https://example.com",
114
+ )
115
+ print(
116
+ f"Run started: {response.run_id}"
107
117
  )
108
- print(f"Run started: {response.run_id}")
109
118
 
110
119
  # Check status later
111
- run = client.runs.get(response.run_id)
112
- print(f"Status: {run.status}")
120
+ run = client.runs.get(
121
+ response.run_id
122
+ )
123
+ print(
124
+ f"Status: {run.status}"
125
+ )
113
126
  ```
114
127
  """
115
128
 
@@ -173,7 +186,7 @@ class CompleteEvent(BaseModel):
173
186
  status: RunStatus = Field(..., description="Final status of the automation")
174
187
  timestamp: datetime = Field(..., description="Timestamp of the event")
175
188
  result_json: dict[str, object] | None = Field(
176
- None, alias="resultJson", description="Structured JSON result extracted from the automation"
189
+ None, alias="result", description="Structured JSON result extracted from the automation"
177
190
  )
178
191
  error: RunError | None = Field(None, description="Error details if the run failed. None if succeeded.")
179
192
 
@@ -0,0 +1,86 @@
1
+ """Browser resource for creating remote browser sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from tinyfish._utils.client import BaseAsyncAPIClient, BaseSyncAPIClient
6
+ from tinyfish._utils.resource import BaseAsyncAPIResource, BaseSyncAPIResource
7
+
8
+ from .types import BrowserSession, BrowserSessionCreateParams
9
+
10
+
11
+ class BrowserSessionsResource(BaseSyncAPIResource):
12
+ """Create remote browser sessions (sync)."""
13
+
14
+ def create(
15
+ self,
16
+ *,
17
+ url: str | None = None,
18
+ timeout_seconds: int | None = None,
19
+ ) -> BrowserSession:
20
+ """Create a new remote browser session.
21
+
22
+ Args:
23
+ url: Target URL for the browser session. If omitted, browser starts at about:blank.
24
+ timeout_seconds: Inactivity timeout in seconds. If omitted or exceeds plan max,
25
+ the plan max is used.
26
+
27
+ Returns:
28
+ BrowserSession with session_id, cdp_url, and base_url.
29
+
30
+ Raises:
31
+ BadRequestError: Invalid parameters.
32
+ AuthenticationError: Invalid API key.
33
+ """
34
+ params = BrowserSessionCreateParams(url=url, timeout_seconds=timeout_seconds)
35
+ body = params.model_dump(exclude_none=True)
36
+ return self._post("/v1/browser", json=body, cast_to=BrowserSession)
37
+
38
+
39
+ class AsyncBrowserSessionsResource(BaseAsyncAPIResource):
40
+ """Create remote browser sessions (async)."""
41
+
42
+ async def create(
43
+ self,
44
+ *,
45
+ url: str | None = None,
46
+ timeout_seconds: int | None = None,
47
+ ) -> BrowserSession:
48
+ """Create a new remote browser session.
49
+
50
+ Async version of `BrowserSessionsResource.create()`.
51
+
52
+ Args:
53
+ url: Target URL for the browser session. If omitted, browser starts at about:blank.
54
+ timeout_seconds: Inactivity timeout in seconds. If omitted or exceeds plan max,
55
+ the plan max is used.
56
+
57
+ Returns:
58
+ BrowserSession with session_id, cdp_url, and base_url.
59
+
60
+ Raises:
61
+ BadRequestError: Invalid parameters.
62
+ AuthenticationError: Invalid API key.
63
+ """
64
+ params = BrowserSessionCreateParams(url=url, timeout_seconds=timeout_seconds)
65
+ body = params.model_dump(exclude_none=True)
66
+ return await self._post("/v1/browser", json=body, cast_to=BrowserSession)
67
+
68
+
69
+ class BrowserResource(BaseSyncAPIResource):
70
+ """Browser API namespace (sync)."""
71
+
72
+ sessions: BrowserSessionsResource
73
+
74
+ def __init__(self, client: BaseSyncAPIClient) -> None:
75
+ super().__init__(client)
76
+ self.sessions = BrowserSessionsResource(client)
77
+
78
+
79
+ class AsyncBrowserResource(BaseAsyncAPIResource):
80
+ """Browser API namespace (async)."""
81
+
82
+ sessions: AsyncBrowserSessionsResource
83
+
84
+ def __init__(self, client: BaseAsyncAPIClient) -> None:
85
+ super().__init__(client)
86
+ self.sessions = AsyncBrowserSessionsResource(client)
@@ -0,0 +1,24 @@
1
+ """Browser API request/response types."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class BrowserSessionCreateParams(BaseModel):
7
+ """Parameters for `browser.sessions.create()`."""
8
+
9
+ url: str | None = Field(
10
+ None,
11
+ description="Target URL for the browser session. If omitted, browser starts at about:blank.",
12
+ )
13
+ timeout_seconds: int | None = Field(
14
+ None,
15
+ description="Inactivity timeout in seconds. If omitted or exceeds plan max, the plan max is used.",
16
+ )
17
+
18
+
19
+ class BrowserSession(BaseModel):
20
+ """Response from creating a browser session."""
21
+
22
+ session_id: str = Field(..., description="Unique session identifier")
23
+ cdp_url: str = Field(..., description="CDP WebSocket URL for direct browser connection")
24
+ base_url: str = Field(..., description="HTTPS base URL for the browser session")
@@ -4,7 +4,10 @@ from tinyfish._utils.client import BaseAsyncAPIClient, BaseSyncAPIClient
4
4
  from tinyfish._utils.client._base import _DEFAULT_TIMEOUT
5
5
 
6
6
  from .agent import AgentResource, AsyncAgentResource
7
+ from .browser import AsyncBrowserResource, BrowserResource
8
+ from .fetch import AsyncFetchResource, FetchResource
7
9
  from .runs import AsyncRunsResource, RunsResource
10
+ from .search import AsyncSearchResource, SearchResource
8
11
 
9
12
  __all__ = ["TinyFish", "AsyncTinyFish"]
10
13
 
@@ -21,7 +24,10 @@ class TinyFish(BaseSyncAPIClient):
21
24
  # __init__ assignments below — which means no autocomplete for users who just
22
25
  # did `pip install tinyfish`.
23
26
  agent: AgentResource
27
+ browser: BrowserResource
28
+ fetch: FetchResource
24
29
  runs: RunsResource
30
+ search: SearchResource
25
31
 
26
32
  def __init__(
27
33
  self,
@@ -42,7 +48,10 @@ class TinyFish(BaseSyncAPIClient):
42
48
  )
43
49
 
44
50
  self.agent = AgentResource(self)
51
+ self.browser = BrowserResource(self)
52
+ self.fetch = FetchResource(self)
45
53
  self.runs = RunsResource(self)
54
+ self.search = SearchResource(self)
46
55
 
47
56
 
48
57
  class AsyncTinyFish(BaseAsyncAPIClient):
@@ -52,7 +61,10 @@ class AsyncTinyFish(BaseAsyncAPIClient):
52
61
  # checkers recognise these as AsyncAgentResource / AsyncRunsResource (not
53
62
  # the sync variants) when resolving types from an installed package.
54
63
  agent: AsyncAgentResource
64
+ browser: AsyncBrowserResource
65
+ fetch: AsyncFetchResource
55
66
  runs: AsyncRunsResource
67
+ search: AsyncSearchResource
56
68
 
57
69
  def __init__(
58
70
  self,
@@ -73,4 +85,7 @@ class AsyncTinyFish(BaseAsyncAPIClient):
73
85
  )
74
86
 
75
87
  self.agent = AsyncAgentResource(self)
88
+ self.browser = AsyncBrowserResource(self)
89
+ self.fetch = AsyncFetchResource(self)
76
90
  self.runs = AsyncRunsResource(self)
91
+ self.search = AsyncSearchResource(self)