tinyfish 0.2.2__tar.gz → 0.2.4__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.
- tinyfish-0.2.4/.pre-commit-config.yaml +23 -0
- tinyfish-0.2.4/CLAUDE.md +60 -0
- tinyfish-0.2.4/PKG-INFO +344 -0
- tinyfish-0.2.4/docs/internal/api-integration-header.md +58 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/pyproject.toml +6 -1
- {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/_utils/client/_base.py +12 -1
- {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/_utils/client/async_.py +2 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/_utils/client/sync.py +2 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/agent/__init__.py +50 -12
- {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/agent/types.py +21 -8
- {tinyfish-0.2.2 → tinyfish-0.2.4}/tests/test_client.py +43 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/uv.lock +1 -1
- tinyfish-0.2.2/PKG-INFO +0 -8
- {tinyfish-0.2.2 → tinyfish-0.2.4}/.gitignore +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/README.md +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/RELEASE.md +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/docs/exceptions-and-errors-guide.md +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/docs/internal/exceptions-and-errors-guide.md +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/docs/internal/publishing-private-guide.md +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/docs/pagination-guide.md +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/docs/proxy-and-browser-profiles.md +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/docs/streaming-guide.md +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/__init__.py +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/_utils/__init__.py +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/_utils/client/__init__.py +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/_utils/exceptions.py +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/_utils/resource.py +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/_utils/sse_parser.py +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/client.py +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/py.typed +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/runs/__init__.py +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/runs/types.py +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/tests/__init__.py +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/tests/conftest.py +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/tests/test_agent.py +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/tests/test_errors.py +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/tests/test_runs.py +0 -0
- {tinyfish-0.2.2 → tinyfish-0.2.4}/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)$
|
tinyfish-0.2.4/CLAUDE.md
ADDED
|
@@ -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.
|
tinyfish-0.2.4/PKG-INFO
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tinyfish
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: Official Python SDK for the TinyFish API
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: httpx>=0.27.0
|
|
7
|
+
Requires-Dist: pydantic>=2.0.0
|
|
8
|
+
Requires-Dist: tenacity>=8.0.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# TinyFish Python SDK
|
|
12
|
+
|
|
13
|
+
The official Python SDK for [TinyFish](https://agent.tinyfish.ai)
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install tinyfish
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires Python 3.11+.
|
|
22
|
+
|
|
23
|
+
## Get your API key
|
|
24
|
+
|
|
25
|
+
Sign up and grab your key at [agent.tinyfish.ai/api-keys](https://agent.tinyfish.ai/api-keys).
|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from tinyfish import TinyFish
|
|
31
|
+
|
|
32
|
+
client = TinyFish(api_key="your-api-key")
|
|
33
|
+
|
|
34
|
+
response = client.agent.run(
|
|
35
|
+
goal="What is the current Bitcoin price?",
|
|
36
|
+
url="https://www.coinbase.com/price/bitcoin",
|
|
37
|
+
)
|
|
38
|
+
print(response.result)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or set the `TINYFISH_API_KEY` environment variable and omit `api_key`:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
client = TinyFish()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Methods
|
|
48
|
+
|
|
49
|
+
Every method below is available on both `TinyFish` (sync) and `AsyncTinyFish` (async). Async versions have the same signatures — just `await` them.
|
|
50
|
+
|
|
51
|
+
| Method | Description | Returns | Blocks? |
|
|
52
|
+
|--------|-------------|---------|---------|
|
|
53
|
+
| [`agent.run()`](#agentrun--block-until-done) | Run an automation, wait for the result | `AgentRunResponse` | Yes |
|
|
54
|
+
| [`agent.queue()`](#agentqueue--fire-and-forget) | Start an automation, return immediately | `AgentRunAsyncResponse` | No |
|
|
55
|
+
| [`agent.stream()`](#agentstream--real-time-events) | Stream live SSE events as the agent works | `AgentStream` | No |
|
|
56
|
+
| [`runs.get()`](#runsget--retrieve-a-single-run) | Retrieve a single run by ID | `Run` | — |
|
|
57
|
+
| [`runs.list()`](#runslist--list-and-filter-runs) | List runs with filtering, sorting, pagination | `RunListResponse` | — |
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
### `agent.run()` — block until done
|
|
62
|
+
|
|
63
|
+
Sends the automation and waits for it to finish. Returns the full result in one shot.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from tinyfish import TinyFish, RunStatus, BrowserProfile, ProxyConfig, ProxyCountryCode
|
|
67
|
+
|
|
68
|
+
client = TinyFish()
|
|
69
|
+
|
|
70
|
+
response = client.agent.run(
|
|
71
|
+
goal="Extract the top 5 headlines", # required — what to do on the page
|
|
72
|
+
url="https://news.ycombinator.com", # required — URL to open
|
|
73
|
+
browser_profile=BrowserProfile.STEALTH, # optional — "lite" (default) or "stealth"
|
|
74
|
+
proxy_config=ProxyConfig( # optional — proxy settings
|
|
75
|
+
enabled=True,
|
|
76
|
+
country_code=ProxyCountryCode.US, # optional — US, GB, CA, DE, FR, JP, AU
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if response.status == RunStatus.COMPLETED:
|
|
81
|
+
print(response.result)
|
|
82
|
+
else:
|
|
83
|
+
print(f"Failed: {response.error.message}")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Returns `AgentRunResponse`:**
|
|
87
|
+
|
|
88
|
+
| Field | Type | Description |
|
|
89
|
+
|-------|------|-------------|
|
|
90
|
+
| `status` | `RunStatus` | `COMPLETED`, `FAILED`, etc. |
|
|
91
|
+
| `run_id` | `str \| None` | Unique run identifier |
|
|
92
|
+
| `result` | `dict \| None` | Extracted data (`None` if failed) |
|
|
93
|
+
| `error` | `RunError \| None` | Error details (`None` if succeeded) |
|
|
94
|
+
| `num_of_steps` | `int` | Number of steps the agent took |
|
|
95
|
+
| `started_at` | `datetime \| None` | When the run started |
|
|
96
|
+
| `finished_at` | `datetime \| None` | When the run finished |
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### `agent.queue()` — fire and forget
|
|
101
|
+
|
|
102
|
+
Starts the automation in the background and returns a `run_id` immediately. Poll with `runs.get()` when you're ready for the result.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import time
|
|
106
|
+
from tinyfish import TinyFish, RunStatus
|
|
107
|
+
|
|
108
|
+
client = TinyFish()
|
|
109
|
+
|
|
110
|
+
queued = client.agent.queue(
|
|
111
|
+
goal="Extract the top 5 headlines", # required — what to do on the page
|
|
112
|
+
url="https://news.ycombinator.com", # required — URL to open
|
|
113
|
+
browser_profile=None, # optional — "lite" (default) or "stealth"
|
|
114
|
+
proxy_config=None, # optional — proxy settings
|
|
115
|
+
)
|
|
116
|
+
print(f"Run started: {queued.run_id}")
|
|
117
|
+
|
|
118
|
+
# Poll for completion
|
|
119
|
+
while True:
|
|
120
|
+
run = client.runs.get(queued.run_id)
|
|
121
|
+
if run.status in (RunStatus.COMPLETED, RunStatus.FAILED):
|
|
122
|
+
break
|
|
123
|
+
time.sleep(5)
|
|
124
|
+
|
|
125
|
+
print(run.result)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Returns `AgentRunAsyncResponse`:**
|
|
129
|
+
|
|
130
|
+
| Field | Type | Description |
|
|
131
|
+
|-------|------|-------------|
|
|
132
|
+
| `run_id` | `str \| None` | Run ID to poll with `runs.get()` |
|
|
133
|
+
| `error` | `RunError \| None` | Error if queuing itself failed |
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### `agent.stream()` — real-time events
|
|
138
|
+
|
|
139
|
+
Opens a Server-Sent Events stream. You get live progress updates as the agent works, plus a WebSocket URL for a live browser preview.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from tinyfish import TinyFish, CompleteEvent, ProgressEvent
|
|
143
|
+
|
|
144
|
+
client = TinyFish()
|
|
145
|
+
|
|
146
|
+
with client.agent.stream(
|
|
147
|
+
goal="Extract the top 5 headlines", # required — what to do on the page
|
|
148
|
+
url="https://news.ycombinator.com", # required — URL to open
|
|
149
|
+
browser_profile=None, # optional — "lite" (default) or "stealth"
|
|
150
|
+
proxy_config=None, # optional — proxy settings
|
|
151
|
+
on_started=lambda e: print(f"Started: {e.run_id}"), # optional — called when run starts
|
|
152
|
+
on_streaming_url=lambda e: print(f"Watch: {e.streaming_url}"), # optional — called with live browser URL
|
|
153
|
+
on_progress=lambda e: print(f" > {e.purpose}"), # optional — called on each step
|
|
154
|
+
on_heartbeat=lambda e: None, # optional — called on keepalive pings
|
|
155
|
+
on_complete=lambda e: print(f"Done: {e.status}"), # optional — called when run finishes
|
|
156
|
+
) as stream:
|
|
157
|
+
for event in stream:
|
|
158
|
+
# Callbacks fire automatically during iteration.
|
|
159
|
+
# You can also inspect events directly:
|
|
160
|
+
if isinstance(event, CompleteEvent):
|
|
161
|
+
print(event.result_json)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Returns `AgentStream`** — a context manager you iterate over. Events arrive in order: `STARTED` → `STREAMING_URL` → `PROGRESS` (repeated) → `COMPLETE`.
|
|
165
|
+
|
|
166
|
+
See the [Streaming Guide](docs/streaming-guide.md) for the full event lifecycle, event types, and advanced patterns.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### `runs.get()` — retrieve a single run
|
|
171
|
+
|
|
172
|
+
Fetch the full details of a run by its ID.
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
run = client.runs.get(
|
|
176
|
+
"run_abc123", # required — the run ID
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
print(run.status) # PENDING, RUNNING, COMPLETED, FAILED, CANCELLED
|
|
180
|
+
print(run.result)
|
|
181
|
+
print(run.goal)
|
|
182
|
+
print(run.streaming_url) # live browser URL (while RUNNING)
|
|
183
|
+
print(run.browser_config) # proxy/browser settings that were used
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Returns `Run`:**
|
|
187
|
+
|
|
188
|
+
| Field | Type | Description |
|
|
189
|
+
|-------|------|-------------|
|
|
190
|
+
| `run_id` | `str` | Unique identifier |
|
|
191
|
+
| `status` | `RunStatus` | `PENDING`, `RUNNING`, `COMPLETED`, `FAILED`, `CANCELLED` |
|
|
192
|
+
| `goal` | `str` | The goal that was given |
|
|
193
|
+
| `result` | `dict \| None` | Extracted data (`None` if not completed) |
|
|
194
|
+
| `error` | `RunError \| None` | Error details (`None` if succeeded) |
|
|
195
|
+
| `streaming_url` | `str \| None` | Live browser URL (available while running) |
|
|
196
|
+
| `browser_config` | `BrowserConfig \| None` | Proxy/browser settings used |
|
|
197
|
+
| `created_at` | `datetime` | When the run was created |
|
|
198
|
+
| `started_at` | `datetime \| None` | When execution started |
|
|
199
|
+
| `finished_at` | `datetime \| None` | When execution finished |
|
|
200
|
+
|
|
201
|
+
**Raises:** `ValueError` if `run_id` is empty. `NotFoundError` if no run exists with that ID.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### `runs.list()` — list and filter runs
|
|
206
|
+
|
|
207
|
+
List runs with optional filtering, sorting, and cursor-based pagination. All parameters are optional.
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from tinyfish import RunStatus, SortDirection
|
|
211
|
+
|
|
212
|
+
response = client.runs.list(
|
|
213
|
+
status=RunStatus.COMPLETED, # optional — filter by status
|
|
214
|
+
goal="headlines", # optional — filter by goal text
|
|
215
|
+
created_after="2025-01-01T00:00:00Z", # optional — ISO 8601 lower bound
|
|
216
|
+
created_before="2025-12-31T23:59:59Z", # optional — ISO 8601 upper bound
|
|
217
|
+
sort_direction=SortDirection.DESC, # optional — "asc" or "desc"
|
|
218
|
+
limit=10, # optional — max runs per page
|
|
219
|
+
cursor=None, # optional — pagination cursor from previous response
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
for run in response.data:
|
|
223
|
+
print(f"{run.run_id} | {run.goal}")
|
|
224
|
+
|
|
225
|
+
# Pagination
|
|
226
|
+
if response.pagination.has_more:
|
|
227
|
+
next_page = client.runs.list(cursor=response.pagination.next_cursor)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Returns `RunListResponse`:**
|
|
231
|
+
|
|
232
|
+
| Field | Type | Description |
|
|
233
|
+
|-------|------|-------------|
|
|
234
|
+
| `data` | `list[Run]` | List of runs |
|
|
235
|
+
| `pagination.total` | `int` | Total runs matching filters |
|
|
236
|
+
| `pagination.has_more` | `bool` | Whether more pages exist |
|
|
237
|
+
| `pagination.next_cursor` | `str \| None` | Pass to `cursor=` for the next page |
|
|
238
|
+
|
|
239
|
+
See the [Pagination Guide](docs/pagination-guide.md) for full pagination loop examples.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Sync vs Async
|
|
244
|
+
|
|
245
|
+
Use `AsyncTinyFish` when you're in an async context (FastAPI, aiohttp, etc.):
|
|
246
|
+
|
|
247
|
+
**Sync:**
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from tinyfish import TinyFish
|
|
251
|
+
|
|
252
|
+
client = TinyFish()
|
|
253
|
+
response = client.agent.run(goal="...", url="...")
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Async:**
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
from tinyfish import AsyncTinyFish
|
|
260
|
+
|
|
261
|
+
client = AsyncTinyFish()
|
|
262
|
+
response = await client.agent.run(goal="...", url="...")
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
All five methods (`agent.run()`, `agent.queue()`, `agent.stream()`, `runs.get()`, `runs.list()`) work the same way — same parameters, just `await`-ed.
|
|
266
|
+
|
|
267
|
+
## Configuration
|
|
268
|
+
|
|
269
|
+
### Client options
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
client = TinyFish(
|
|
273
|
+
api_key="your-api-key", # optional — or set TINYFISH_API_KEY env var
|
|
274
|
+
base_url="https://agent.tinyfish.ai", # optional — default shown
|
|
275
|
+
timeout=600.0, # optional — seconds (default: 600)
|
|
276
|
+
max_retries=2, # optional — retry attempts (default: 2)
|
|
277
|
+
)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The SDK retries `408`, `429`, and `5xx` errors automatically with exponential backoff (0.5s multiplier, max 8s wait).
|
|
281
|
+
|
|
282
|
+
### Browser profiles
|
|
283
|
+
|
|
284
|
+
Control the browser environment with `browser_profile`:
|
|
285
|
+
|
|
286
|
+
- **`lite`** (default) — fast, lightweight. Good for most sites.
|
|
287
|
+
- **`stealth`** — anti-detection mode. Use for sites with bot protection.
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
from tinyfish import BrowserProfile
|
|
291
|
+
|
|
292
|
+
response = client.agent.run(
|
|
293
|
+
goal="...",
|
|
294
|
+
url="...",
|
|
295
|
+
browser_profile=BrowserProfile.STEALTH,
|
|
296
|
+
)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Proxy configuration
|
|
300
|
+
|
|
301
|
+
Route requests through a proxy, optionally pinned to a country:
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
from tinyfish import ProxyConfig, ProxyCountryCode
|
|
305
|
+
|
|
306
|
+
response = client.agent.run(
|
|
307
|
+
goal="...",
|
|
308
|
+
url="...",
|
|
309
|
+
proxy_config=ProxyConfig(enabled=True, country_code=ProxyCountryCode.US),
|
|
310
|
+
)
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Available countries: `US`, `GB`, `CA`, `DE`, `FR`, `JP`, `AU`.
|
|
314
|
+
|
|
315
|
+
See the [Proxy & Browser Profiles Guide](docs/proxy-and-browser-profiles.md) for more details.
|
|
316
|
+
|
|
317
|
+
## Error handling
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
from tinyfish import TinyFish, AuthenticationError, RateLimitError, SDKError
|
|
321
|
+
|
|
322
|
+
client = TinyFish()
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
response = client.agent.run(goal="...", url="...")
|
|
326
|
+
except AuthenticationError:
|
|
327
|
+
print("Invalid API key")
|
|
328
|
+
except RateLimitError:
|
|
329
|
+
print("Rate limited (retries exhausted)")
|
|
330
|
+
except SDKError:
|
|
331
|
+
print("Something else went wrong")
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
The SDK automatically retries transient errors (`408`, `429`, `5xx`) up to `max_retries` times with exponential backoff. Non-retryable errors (`401`, `400`, `404`) raise immediately.
|
|
335
|
+
|
|
336
|
+
For the full exception hierarchy and internal architecture, see [docs/internal/exceptions-and-errors-guide.md](docs/internal/exceptions-and-errors-guide.md).
|
|
337
|
+
|
|
338
|
+
## Guides
|
|
339
|
+
|
|
340
|
+
- [Streaming Guide](docs/streaming-guide.md) — event lifecycle, callbacks vs iteration, event type reference
|
|
341
|
+
- [Proxy & Browser Profiles](docs/proxy-and-browser-profiles.md) — stealth mode, proxy countries
|
|
342
|
+
- [Pagination Guide](docs/pagination-guide.md) — filtering, sorting, cursor-based pagination
|
|
343
|
+
- [Exceptions & Error Handling (internal)](docs/internal/exceptions-and-errors-guide.md) — layer-by-layer architecture
|
|
344
|
+
- [Testing Guide](tests/testing_guide.md) — running and writing tests
|
|
@@ -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,7 +1,8 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tinyfish"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
description = "Official Python SDK for the TinyFish API"
|
|
5
|
+
readme = "README.md"
|
|
5
6
|
requires-python = ">=3.11"
|
|
6
7
|
dependencies = [
|
|
7
8
|
"httpx>=0.27.0",
|
|
@@ -32,6 +33,10 @@ asyncio_default_fixture_loop_scope = "session"
|
|
|
32
33
|
[tool.ruff]
|
|
33
34
|
src = ["src"]
|
|
34
35
|
line-length = 120
|
|
36
|
+
[tool.ruff.format]
|
|
37
|
+
quote-style = "double"
|
|
38
|
+
docstring-code-format = true
|
|
39
|
+
docstring-code-line-length = 20
|
|
35
40
|
|
|
36
41
|
[tool.ruff.lint]
|
|
37
42
|
select = ["E", "F", "I", "UP", "PYI", "TID"]
|
|
@@ -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
|
|
45
|
-
|
|
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
|
|
68
|
-
|
|
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
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
73
|
-
|
|
72
|
+
if (
|
|
73
|
+
response.status
|
|
74
|
+
== "COMPLETED"
|
|
75
|
+
):
|
|
76
|
+
print(
|
|
77
|
+
response.result
|
|
78
|
+
)
|
|
74
79
|
else:
|
|
75
|
-
print(
|
|
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(
|
|
112
|
-
|
|
120
|
+
run = client.runs.get(
|
|
121
|
+
response.run_id
|
|
122
|
+
)
|
|
123
|
+
print(
|
|
124
|
+
f"Status: {run.status}"
|
|
125
|
+
)
|
|
113
126
|
```
|
|
114
127
|
"""
|
|
115
128
|
|
|
@@ -184,3 +184,46 @@ def test_repr_short_key():
|
|
|
184
184
|
r = repr(c)
|
|
185
185
|
assert "****" in r
|
|
186
186
|
assert "short" not in r
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ─── TF_API_INTEGRATION env var ───────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# TF_API_INTEGRATION env var injects api_integration into request body
|
|
193
|
+
@pytest.mark.respx(base_url=BASE_URL)
|
|
194
|
+
def test_integration_in_body_via_env(respx_mock: MockRouter):
|
|
195
|
+
with mock.patch.dict(os.environ, {"TF_API_INTEGRATION": "n8n"}):
|
|
196
|
+
c = TinyFish(api_key=API_KEY, base_url=BASE_URL, max_retries=0)
|
|
197
|
+
route = respx_mock.post(_PATH).mock(return_value=httpx.Response(200, json=_RUN_OK))
|
|
198
|
+
c.agent.run(**_BODY)
|
|
199
|
+
import json
|
|
200
|
+
|
|
201
|
+
body = json.loads(route.calls[0].request.content)
|
|
202
|
+
assert body["api_integration"] == "n8n"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# Without TF_API_INTEGRATION, api_integration is not in request body
|
|
206
|
+
@pytest.mark.respx(base_url=BASE_URL)
|
|
207
|
+
def test_no_integration_in_body_by_default(respx_mock: MockRouter):
|
|
208
|
+
with mock.patch.dict(os.environ, {}, clear=False):
|
|
209
|
+
os.environ.pop("TF_API_INTEGRATION", None)
|
|
210
|
+
c = TinyFish(api_key=API_KEY, base_url=BASE_URL, max_retries=0)
|
|
211
|
+
route = respx_mock.post(_PATH).mock(return_value=httpx.Response(200, json=_RUN_OK))
|
|
212
|
+
c.agent.run(**_BODY)
|
|
213
|
+
import json
|
|
214
|
+
|
|
215
|
+
body = json.loads(route.calls[0].request.content)
|
|
216
|
+
assert "api_integration" not in body
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# Async client injects api_integration into request body when env var is set
|
|
220
|
+
@pytest.mark.respx(base_url=BASE_URL)
|
|
221
|
+
async def test_async_integration_in_body_via_env(respx_mock: MockRouter):
|
|
222
|
+
with mock.patch.dict(os.environ, {"TF_API_INTEGRATION": "dify"}):
|
|
223
|
+
route = respx_mock.post(_PATH).mock(return_value=httpx.Response(200, json=_RUN_OK))
|
|
224
|
+
async with AsyncTinyFish(api_key=API_KEY, base_url=BASE_URL, max_retries=0) as c:
|
|
225
|
+
await c.agent.run(**_BODY)
|
|
226
|
+
import json
|
|
227
|
+
|
|
228
|
+
body = json.loads(route.calls[0].request.content)
|
|
229
|
+
assert body["api_integration"] == "dify"
|
tinyfish-0.2.2/PKG-INFO
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|