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.
Files changed (38) hide show
  1. tinyfish-0.2.4/.pre-commit-config.yaml +23 -0
  2. tinyfish-0.2.4/CLAUDE.md +60 -0
  3. tinyfish-0.2.4/PKG-INFO +344 -0
  4. tinyfish-0.2.4/docs/internal/api-integration-header.md +58 -0
  5. {tinyfish-0.2.2 → tinyfish-0.2.4}/pyproject.toml +6 -1
  6. {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/_utils/client/_base.py +12 -1
  7. {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/_utils/client/async_.py +2 -0
  8. {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/_utils/client/sync.py +2 -0
  9. {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/agent/__init__.py +50 -12
  10. {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/agent/types.py +21 -8
  11. {tinyfish-0.2.2 → tinyfish-0.2.4}/tests/test_client.py +43 -0
  12. {tinyfish-0.2.2 → tinyfish-0.2.4}/uv.lock +1 -1
  13. tinyfish-0.2.2/PKG-INFO +0 -8
  14. {tinyfish-0.2.2 → tinyfish-0.2.4}/.gitignore +0 -0
  15. {tinyfish-0.2.2 → tinyfish-0.2.4}/README.md +0 -0
  16. {tinyfish-0.2.2 → tinyfish-0.2.4}/RELEASE.md +0 -0
  17. {tinyfish-0.2.2 → tinyfish-0.2.4}/docs/exceptions-and-errors-guide.md +0 -0
  18. {tinyfish-0.2.2 → tinyfish-0.2.4}/docs/internal/exceptions-and-errors-guide.md +0 -0
  19. {tinyfish-0.2.2 → tinyfish-0.2.4}/docs/internal/publishing-private-guide.md +0 -0
  20. {tinyfish-0.2.2 → tinyfish-0.2.4}/docs/pagination-guide.md +0 -0
  21. {tinyfish-0.2.2 → tinyfish-0.2.4}/docs/proxy-and-browser-profiles.md +0 -0
  22. {tinyfish-0.2.2 → tinyfish-0.2.4}/docs/streaming-guide.md +0 -0
  23. {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/__init__.py +0 -0
  24. {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/_utils/__init__.py +0 -0
  25. {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/_utils/client/__init__.py +0 -0
  26. {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/_utils/exceptions.py +0 -0
  27. {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/_utils/resource.py +0 -0
  28. {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/_utils/sse_parser.py +0 -0
  29. {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/client.py +0 -0
  30. {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/py.typed +0 -0
  31. {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/runs/__init__.py +0 -0
  32. {tinyfish-0.2.2 → tinyfish-0.2.4}/src/tinyfish/runs/types.py +0 -0
  33. {tinyfish-0.2.2 → tinyfish-0.2.4}/tests/__init__.py +0 -0
  34. {tinyfish-0.2.2 → tinyfish-0.2.4}/tests/conftest.py +0 -0
  35. {tinyfish-0.2.2 → tinyfish-0.2.4}/tests/test_agent.py +0 -0
  36. {tinyfish-0.2.2 → tinyfish-0.2.4}/tests/test_errors.py +0 -0
  37. {tinyfish-0.2.2 → tinyfish-0.2.4}/tests/test_runs.py +0 -0
  38. {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)$
@@ -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.
@@ -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.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 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
 
@@ -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"
@@ -322,7 +322,7 @@ wheels = [
322
322
 
323
323
  [[package]]
324
324
  name = "tinyfish"
325
- version = "0.2.2"
325
+ version = "0.2.4"
326
326
  source = { editable = "." }
327
327
  dependencies = [
328
328
  { name = "httpx" },
tinyfish-0.2.2/PKG-INFO DELETED
@@ -1,8 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: tinyfish
3
- Version: 0.2.2
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
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