gofetch-client 0.1.0__py3-none-any.whl
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.
- gofetch/__init__.py +94 -0
- gofetch/actor.py +312 -0
- gofetch/client.py +305 -0
- gofetch/constants.py +93 -0
- gofetch/dataset.py +219 -0
- gofetch/exceptions.py +182 -0
- gofetch/http.py +411 -0
- gofetch/log.py +72 -0
- gofetch/py.typed +0 -0
- gofetch/run.py +229 -0
- gofetch/scrapers/__init__.py +7 -0
- gofetch/scrapers/base.py +48 -0
- gofetch/types.py +181 -0
- gofetch/webhook.py +268 -0
- gofetch/webhook_client.py +277 -0
- gofetch_client-0.1.0.dist-info/METADATA +456 -0
- gofetch_client-0.1.0.dist-info/RECORD +20 -0
- gofetch_client-0.1.0.dist-info/WHEEL +5 -0
- gofetch_client-0.1.0.dist-info/licenses/LICENSE +21 -0
- gofetch_client-0.1.0.dist-info/top_level.txt +1 -0
gofetch/__init__.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GoFetch Python Client - Social Media Scraping API
|
|
3
|
+
|
|
4
|
+
A drop-in replacement for apify-client that uses the GoFetch.io API.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from gofetch import GoFetchClient
|
|
8
|
+
|
|
9
|
+
client = GoFetchClient(api_key="sk_scr_...")
|
|
10
|
+
|
|
11
|
+
# Sync execution (blocks until complete)
|
|
12
|
+
actor = client.actor("instagram")
|
|
13
|
+
run = actor.call(run_input={"directUrls": ["https://instagram.com/nike"]})
|
|
14
|
+
|
|
15
|
+
# Fetch results
|
|
16
|
+
dataset = client.dataset(run["defaultDatasetId"])
|
|
17
|
+
items = list(dataset.iterate_items())
|
|
18
|
+
|
|
19
|
+
# Async execution (returns immediately, uses webhooks)
|
|
20
|
+
run = actor.start(
|
|
21
|
+
run_input={"directUrls": ["https://instagram.com/nike"]},
|
|
22
|
+
webhooks=[{"request_url": "https://...", "event_types": ["ACTOR.RUN.SUCCEEDED"]}]
|
|
23
|
+
)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from gofetch.actor import ActorClient, AsyncActorClient
|
|
27
|
+
from gofetch.client import AsyncGoFetchClient, GoFetchClient
|
|
28
|
+
from gofetch.dataset import AsyncDatasetClient, DatasetClient
|
|
29
|
+
from gofetch.exceptions import (
|
|
30
|
+
APIError,
|
|
31
|
+
AuthenticationError,
|
|
32
|
+
GoFetchError,
|
|
33
|
+
JobError,
|
|
34
|
+
RateLimitError,
|
|
35
|
+
TimeoutError,
|
|
36
|
+
ValidationError,
|
|
37
|
+
)
|
|
38
|
+
from gofetch.log import AsyncLogClient, LogClient
|
|
39
|
+
from gofetch.run import AsyncRunClient, RunClient
|
|
40
|
+
from gofetch.types import (
|
|
41
|
+
JobStatus,
|
|
42
|
+
RunStatus,
|
|
43
|
+
ScraperType,
|
|
44
|
+
)
|
|
45
|
+
from gofetch.webhook import (
|
|
46
|
+
WebhookEventType,
|
|
47
|
+
generate_webhook_config,
|
|
48
|
+
transform_webhook_payload,
|
|
49
|
+
verify_webhook_signature,
|
|
50
|
+
)
|
|
51
|
+
from gofetch.webhook_client import (
|
|
52
|
+
AsyncWebhookClient,
|
|
53
|
+
AsyncWebhookCollectionClient,
|
|
54
|
+
WebhookClient,
|
|
55
|
+
WebhookCollectionClient,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Apify compatibility alias
|
|
59
|
+
ApifyClient = GoFetchClient
|
|
60
|
+
|
|
61
|
+
__version__ = "0.1.0"
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"APIError",
|
|
65
|
+
"ActorClient",
|
|
66
|
+
"ApifyClient",
|
|
67
|
+
"AsyncActorClient",
|
|
68
|
+
"AsyncDatasetClient",
|
|
69
|
+
"AsyncGoFetchClient",
|
|
70
|
+
"AsyncLogClient",
|
|
71
|
+
"AsyncRunClient",
|
|
72
|
+
"AsyncWebhookClient",
|
|
73
|
+
"AsyncWebhookCollectionClient",
|
|
74
|
+
"AuthenticationError",
|
|
75
|
+
"DatasetClient",
|
|
76
|
+
"GoFetchClient",
|
|
77
|
+
"GoFetchError",
|
|
78
|
+
"JobError",
|
|
79
|
+
"JobStatus",
|
|
80
|
+
"LogClient",
|
|
81
|
+
"RateLimitError",
|
|
82
|
+
"RunClient",
|
|
83
|
+
"RunStatus",
|
|
84
|
+
"ScraperType",
|
|
85
|
+
"TimeoutError",
|
|
86
|
+
"ValidationError",
|
|
87
|
+
"WebhookClient",
|
|
88
|
+
"WebhookCollectionClient",
|
|
89
|
+
"WebhookEventType",
|
|
90
|
+
"__version__",
|
|
91
|
+
"generate_webhook_config",
|
|
92
|
+
"transform_webhook_payload",
|
|
93
|
+
"verify_webhook_signature",
|
|
94
|
+
]
|
gofetch/actor.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Actor client for GoFetch API.
|
|
3
|
+
|
|
4
|
+
Provides Apify-compatible interface for running scraper jobs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
import warnings
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from gofetch.constants import (
|
|
15
|
+
DEFAULT_POLL_INTERVAL,
|
|
16
|
+
GOFETCH_TO_APIFY_STATUS,
|
|
17
|
+
MAX_POLL_INTERVAL,
|
|
18
|
+
POLL_BACKOFF_FACTOR,
|
|
19
|
+
)
|
|
20
|
+
from gofetch.webhook import APIFY_TO_GOFETCH_EVENTS
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from gofetch.http import AsyncHTTPClient, HTTPClient
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
TERMINAL_STATUSES = frozenset({"completed", "failed", "timed_out", "cancelled"})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _is_terminal(status: str) -> bool:
|
|
31
|
+
return status in TERMINAL_STATUSES
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _next_poll_interval(current: float) -> float:
|
|
35
|
+
return min(current * POLL_BACKOFF_FACTOR, MAX_POLL_INTERVAL)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _format_job_as_apify_run(
|
|
39
|
+
job: dict[str, Any],
|
|
40
|
+
scraper_type: str | None = None,
|
|
41
|
+
extra_fields: dict[str, Any] | None = None,
|
|
42
|
+
) -> dict[str, Any]:
|
|
43
|
+
"""Convert GoFetch job dict to Apify run format.
|
|
44
|
+
|
|
45
|
+
IMPORTANT: Any extra fields on the job dict (e.g., scraper_metadata)
|
|
46
|
+
are preserved on the returned run dict.
|
|
47
|
+
"""
|
|
48
|
+
scraper_type = scraper_type or job.get("scraper_type", "unknown")
|
|
49
|
+
status = GOFETCH_TO_APIFY_STATUS.get(job.get("status", ""), "RUNNING")
|
|
50
|
+
|
|
51
|
+
run: dict[str, Any] = {
|
|
52
|
+
"id": job["id"],
|
|
53
|
+
"actId": f"gofetch/{scraper_type}",
|
|
54
|
+
"status": status,
|
|
55
|
+
"defaultDatasetId": job["id"],
|
|
56
|
+
"startedAt": job.get("started_at"),
|
|
57
|
+
"finishedAt": job.get("completed_at"),
|
|
58
|
+
"buildId": None,
|
|
59
|
+
"buildNumber": None,
|
|
60
|
+
"exitCode": 0 if status == "SUCCEEDED" else None,
|
|
61
|
+
"defaultKeyValueStoreId": None,
|
|
62
|
+
"defaultRequestQueueId": None,
|
|
63
|
+
"_gofetch_job": job,
|
|
64
|
+
}
|
|
65
|
+
if extra_fields:
|
|
66
|
+
run.update(extra_fields)
|
|
67
|
+
return run
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _translate_webhooks(webhooks: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
71
|
+
"""Translate Apify-format webhooks to GoFetch format.
|
|
72
|
+
|
|
73
|
+
Apify format:
|
|
74
|
+
{"request_url": "https://...", "event_types": ["ACTOR.RUN.SUCCEEDED"]}
|
|
75
|
+
or camelCase:
|
|
76
|
+
{"requestUrl": "https://...", "eventTypes": ["ACTOR.RUN.SUCCEEDED"]}
|
|
77
|
+
|
|
78
|
+
GoFetch format:
|
|
79
|
+
{"url": "https://...", "events": ["job.completed"]}
|
|
80
|
+
"""
|
|
81
|
+
translated = []
|
|
82
|
+
for wh in webhooks:
|
|
83
|
+
url = wh.get("request_url") or wh.get("requestUrl")
|
|
84
|
+
event_types = wh.get("event_types") or wh.get("eventTypes", [])
|
|
85
|
+
translated.append({
|
|
86
|
+
"url": url,
|
|
87
|
+
"events": [
|
|
88
|
+
APIFY_TO_GOFETCH_EVENTS.get(et, et)
|
|
89
|
+
for et in event_types
|
|
90
|
+
],
|
|
91
|
+
})
|
|
92
|
+
return translated
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ActorClient:
|
|
96
|
+
"""
|
|
97
|
+
Actor client that wraps GoFetch job API with Apify-compatible interface.
|
|
98
|
+
|
|
99
|
+
Provides the same methods as Apify's ActorClient:
|
|
100
|
+
- call() for synchronous execution
|
|
101
|
+
- start() for asynchronous execution with webhooks
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
http: HTTPClient,
|
|
107
|
+
scraper_type: str,
|
|
108
|
+
) -> None:
|
|
109
|
+
self._http = http
|
|
110
|
+
self._scraper_type = scraper_type
|
|
111
|
+
|
|
112
|
+
def call(
|
|
113
|
+
self,
|
|
114
|
+
run_input: dict[str, Any],
|
|
115
|
+
*,
|
|
116
|
+
wait_secs: int | None = None,
|
|
117
|
+
timeout_secs: int | None = None,
|
|
118
|
+
memory_mbytes: int | None = None,
|
|
119
|
+
build: str | None = None,
|
|
120
|
+
webhooks: list[dict[str, Any]] | None = None,
|
|
121
|
+
) -> dict[str, Any]:
|
|
122
|
+
"""Run actor synchronously (blocking).
|
|
123
|
+
|
|
124
|
+
Matches Apify's behavioral contract:
|
|
125
|
+
- Returns run dict regardless of final status (never raises on failure/timeout)
|
|
126
|
+
- On timeout, returns the current run state (status may be "RUNNING")
|
|
127
|
+
- Default wait_secs=None means wait indefinitely
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
run_input: Scraper configuration parameters
|
|
131
|
+
wait_secs: Maximum wait time in seconds (None = indefinite)
|
|
132
|
+
timeout_secs: Deprecated alias for wait_secs
|
|
133
|
+
memory_mbytes: Ignored (Apify compatibility)
|
|
134
|
+
build: Ignored (Apify compatibility)
|
|
135
|
+
webhooks: Per-run webhooks to register
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Dict in Apify run format
|
|
139
|
+
"""
|
|
140
|
+
_ = memory_mbytes, build
|
|
141
|
+
|
|
142
|
+
effective_wait = self._resolve_wait_secs(wait_secs, timeout_secs)
|
|
143
|
+
|
|
144
|
+
job = self._create_job(run_input, webhooks=webhooks)
|
|
145
|
+
job_id = job["id"]
|
|
146
|
+
|
|
147
|
+
return self._wait_for_completion(job_id, effective_wait)
|
|
148
|
+
|
|
149
|
+
def start(
|
|
150
|
+
self,
|
|
151
|
+
run_input: dict[str, Any],
|
|
152
|
+
*,
|
|
153
|
+
webhooks: list[dict[str, Any]] | None = None,
|
|
154
|
+
wait_secs: int | None = None,
|
|
155
|
+
timeout_secs: int | None = None,
|
|
156
|
+
memory_mbytes: int | None = None,
|
|
157
|
+
build: str | None = None,
|
|
158
|
+
) -> dict[str, Any]:
|
|
159
|
+
"""Start actor asynchronously (non-blocking).
|
|
160
|
+
|
|
161
|
+
Returns immediately after creating the job.
|
|
162
|
+
"""
|
|
163
|
+
_ = wait_secs, timeout_secs, memory_mbytes, build
|
|
164
|
+
|
|
165
|
+
job = self._create_job(run_input, webhooks=webhooks)
|
|
166
|
+
return _format_job_as_apify_run(job, scraper_type=self._scraper_type)
|
|
167
|
+
|
|
168
|
+
def _create_job(
|
|
169
|
+
self,
|
|
170
|
+
run_input: dict[str, Any],
|
|
171
|
+
webhooks: list[dict[str, Any]] | None = None,
|
|
172
|
+
) -> dict[str, Any]:
|
|
173
|
+
config = self._transform_input(run_input)
|
|
174
|
+
payload: dict[str, Any] = {
|
|
175
|
+
"scraper_type": self._scraper_type,
|
|
176
|
+
"config": config,
|
|
177
|
+
}
|
|
178
|
+
if webhooks:
|
|
179
|
+
payload["webhooks"] = _translate_webhooks(webhooks)
|
|
180
|
+
|
|
181
|
+
return self._http.post("/api/v1/jobs/create/", json=payload)
|
|
182
|
+
|
|
183
|
+
def _transform_input(self, run_input: dict[str, Any]) -> dict[str, Any]:
|
|
184
|
+
return run_input.copy()
|
|
185
|
+
|
|
186
|
+
def _wait_for_completion(
|
|
187
|
+
self,
|
|
188
|
+
job_id: str,
|
|
189
|
+
wait_secs: int | None,
|
|
190
|
+
) -> dict[str, Any]:
|
|
191
|
+
"""Poll job status until terminal or timeout.
|
|
192
|
+
|
|
193
|
+
Returns Apify-format run dict in all cases (never raises).
|
|
194
|
+
On timeout, returns current run state. On 404, returns minimal dict.
|
|
195
|
+
"""
|
|
196
|
+
start_time = time.monotonic()
|
|
197
|
+
poll_interval = DEFAULT_POLL_INTERVAL
|
|
198
|
+
|
|
199
|
+
while True:
|
|
200
|
+
job = self._http.get(f"/api/v1/jobs/{job_id}/")
|
|
201
|
+
|
|
202
|
+
if _is_terminal(job.get("status", "")):
|
|
203
|
+
return _format_job_as_apify_run(job, scraper_type=self._scraper_type)
|
|
204
|
+
|
|
205
|
+
if wait_secs is not None and (time.monotonic() - start_time) >= wait_secs:
|
|
206
|
+
return _format_job_as_apify_run(job, scraper_type=self._scraper_type)
|
|
207
|
+
|
|
208
|
+
time.sleep(poll_interval)
|
|
209
|
+
poll_interval = _next_poll_interval(poll_interval)
|
|
210
|
+
|
|
211
|
+
@staticmethod
|
|
212
|
+
def _resolve_wait_secs(
|
|
213
|
+
wait_secs: int | None,
|
|
214
|
+
timeout_secs: int | None,
|
|
215
|
+
) -> int | None:
|
|
216
|
+
if timeout_secs is not None and wait_secs is None:
|
|
217
|
+
warnings.warn(
|
|
218
|
+
"timeout_secs is deprecated, use wait_secs instead",
|
|
219
|
+
DeprecationWarning,
|
|
220
|
+
stacklevel=3,
|
|
221
|
+
)
|
|
222
|
+
return timeout_secs
|
|
223
|
+
return wait_secs
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class AsyncActorClient:
|
|
227
|
+
"""Async actor client for GoFetch API."""
|
|
228
|
+
|
|
229
|
+
def __init__(
|
|
230
|
+
self,
|
|
231
|
+
http: AsyncHTTPClient,
|
|
232
|
+
scraper_type: str,
|
|
233
|
+
) -> None:
|
|
234
|
+
self._http = http
|
|
235
|
+
self._scraper_type = scraper_type
|
|
236
|
+
|
|
237
|
+
async def call(
|
|
238
|
+
self,
|
|
239
|
+
run_input: dict[str, Any],
|
|
240
|
+
*,
|
|
241
|
+
wait_secs: int | None = None,
|
|
242
|
+
timeout_secs: int | None = None,
|
|
243
|
+
memory_mbytes: int | None = None,
|
|
244
|
+
build: str | None = None,
|
|
245
|
+
webhooks: list[dict[str, Any]] | None = None,
|
|
246
|
+
) -> dict[str, Any]:
|
|
247
|
+
"""Run actor synchronously (blocking). Async version."""
|
|
248
|
+
_ = memory_mbytes, build
|
|
249
|
+
|
|
250
|
+
effective_wait = ActorClient._resolve_wait_secs(wait_secs, timeout_secs)
|
|
251
|
+
|
|
252
|
+
job = await self._create_job(run_input, webhooks=webhooks)
|
|
253
|
+
job_id = job["id"]
|
|
254
|
+
|
|
255
|
+
return await self._wait_for_completion(job_id, effective_wait)
|
|
256
|
+
|
|
257
|
+
async def start(
|
|
258
|
+
self,
|
|
259
|
+
run_input: dict[str, Any],
|
|
260
|
+
*,
|
|
261
|
+
webhooks: list[dict[str, Any]] | None = None,
|
|
262
|
+
wait_secs: int | None = None,
|
|
263
|
+
timeout_secs: int | None = None,
|
|
264
|
+
memory_mbytes: int | None = None,
|
|
265
|
+
build: str | None = None,
|
|
266
|
+
) -> dict[str, Any]:
|
|
267
|
+
"""Start actor asynchronously (non-blocking). Async version."""
|
|
268
|
+
_ = wait_secs, timeout_secs, memory_mbytes, build
|
|
269
|
+
|
|
270
|
+
job = await self._create_job(run_input, webhooks=webhooks)
|
|
271
|
+
return _format_job_as_apify_run(job, scraper_type=self._scraper_type)
|
|
272
|
+
|
|
273
|
+
async def _create_job(
|
|
274
|
+
self,
|
|
275
|
+
run_input: dict[str, Any],
|
|
276
|
+
webhooks: list[dict[str, Any]] | None = None,
|
|
277
|
+
) -> dict[str, Any]:
|
|
278
|
+
config = self._transform_input(run_input)
|
|
279
|
+
payload: dict[str, Any] = {
|
|
280
|
+
"scraper_type": self._scraper_type,
|
|
281
|
+
"config": config,
|
|
282
|
+
}
|
|
283
|
+
if webhooks:
|
|
284
|
+
payload["webhooks"] = _translate_webhooks(webhooks)
|
|
285
|
+
|
|
286
|
+
return await self._http.post("/api/v1/jobs/create/", json=payload)
|
|
287
|
+
|
|
288
|
+
def _transform_input(self, run_input: dict[str, Any]) -> dict[str, Any]:
|
|
289
|
+
return run_input.copy()
|
|
290
|
+
|
|
291
|
+
async def _wait_for_completion(
|
|
292
|
+
self,
|
|
293
|
+
job_id: str,
|
|
294
|
+
wait_secs: int | None,
|
|
295
|
+
) -> dict[str, Any]:
|
|
296
|
+
"""Poll job status until terminal or timeout. Async version."""
|
|
297
|
+
import asyncio
|
|
298
|
+
|
|
299
|
+
start_time = time.monotonic()
|
|
300
|
+
poll_interval = DEFAULT_POLL_INTERVAL
|
|
301
|
+
|
|
302
|
+
while True:
|
|
303
|
+
job = await self._http.get(f"/api/v1/jobs/{job_id}/")
|
|
304
|
+
|
|
305
|
+
if _is_terminal(job.get("status", "")):
|
|
306
|
+
return _format_job_as_apify_run(job, scraper_type=self._scraper_type)
|
|
307
|
+
|
|
308
|
+
if wait_secs is not None and (time.monotonic() - start_time) >= wait_secs:
|
|
309
|
+
return _format_job_as_apify_run(job, scraper_type=self._scraper_type)
|
|
310
|
+
|
|
311
|
+
await asyncio.sleep(poll_interval)
|
|
312
|
+
poll_interval = _next_poll_interval(poll_interval)
|
gofetch/client.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main GoFetch client.
|
|
3
|
+
|
|
4
|
+
Provides the primary entry point for interacting with the GoFetch API.
|
|
5
|
+
Designed as a drop-in replacement for ApifyClient.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import NoReturn
|
|
11
|
+
|
|
12
|
+
from gofetch.actor import ActorClient, AsyncActorClient
|
|
13
|
+
from gofetch.constants import DEFAULT_BASE_URL, DEFAULT_TIMEOUT
|
|
14
|
+
from gofetch.dataset import AsyncDatasetClient, DatasetClient
|
|
15
|
+
from gofetch.http import AsyncHTTPClient, HTTPClient
|
|
16
|
+
from gofetch.run import AsyncRunClient, RunClient
|
|
17
|
+
from gofetch.types import resolve_actor_url
|
|
18
|
+
from gofetch.webhook_client import (
|
|
19
|
+
AsyncWebhookClient,
|
|
20
|
+
AsyncWebhookCollectionClient,
|
|
21
|
+
WebhookClient,
|
|
22
|
+
WebhookCollectionClient,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GoFetchClient:
|
|
27
|
+
"""
|
|
28
|
+
GoFetch API client - drop-in replacement for ApifyClient.
|
|
29
|
+
|
|
30
|
+
This client provides the same interface as Apify's SDK, allowing
|
|
31
|
+
you to switch from Apify to GoFetch with minimal code changes.
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
# Initialize client
|
|
35
|
+
client = GoFetchClient(api_key="sk_scr_myorg_xxxx")
|
|
36
|
+
|
|
37
|
+
# Get actor client (same as Apify)
|
|
38
|
+
actor = client.actor("apify/instagram-scraper") # Apify URL works!
|
|
39
|
+
# Or use GoFetch scraper type directly
|
|
40
|
+
actor = client.actor("instagram")
|
|
41
|
+
|
|
42
|
+
# Run synchronously (blocking)
|
|
43
|
+
run = actor.call(run_input={"directUrls": ["https://instagram.com/nike"]})
|
|
44
|
+
print(f"Job completed: {run['id']}")
|
|
45
|
+
|
|
46
|
+
# Fetch results
|
|
47
|
+
dataset = client.dataset(run["defaultDatasetId"])
|
|
48
|
+
for item in dataset.iterate_items():
|
|
49
|
+
print(item)
|
|
50
|
+
|
|
51
|
+
# Run asynchronously (with webhooks)
|
|
52
|
+
run = actor.start(
|
|
53
|
+
run_input={"directUrls": ["https://instagram.com/nike"]},
|
|
54
|
+
webhooks=[{
|
|
55
|
+
"request_url": "https://myapp.com/webhook",
|
|
56
|
+
"event_types": ["ACTOR.RUN.SUCCEEDED"]
|
|
57
|
+
}]
|
|
58
|
+
)
|
|
59
|
+
print(f"Job started: {run['id']}")
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
base_url: The API base URL
|
|
63
|
+
timeout: Request timeout in seconds
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
api_key: str | None = None,
|
|
69
|
+
token: str | None = None, # Alias for api_key (Apify compat)
|
|
70
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
71
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
72
|
+
max_retries: int = 3,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Initialize the GoFetch client.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
api_key: GoFetch API key (format: sk_scr_...)
|
|
79
|
+
token: Alias for api_key (for Apify compatibility)
|
|
80
|
+
base_url: API base URL (default: https://api.go-fetch.io)
|
|
81
|
+
timeout: Request timeout in seconds (default: 30)
|
|
82
|
+
max_retries: Maximum retries for failed requests (default: 3)
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
ValueError: If neither api_key nor token is provided
|
|
86
|
+
"""
|
|
87
|
+
# Support both api_key and token for Apify compatibility
|
|
88
|
+
self._api_key = api_key or token
|
|
89
|
+
if not self._api_key:
|
|
90
|
+
raise ValueError("Either 'api_key' or 'token' must be provided")
|
|
91
|
+
|
|
92
|
+
self._base_url = base_url.rstrip("/")
|
|
93
|
+
self._timeout = timeout
|
|
94
|
+
self._max_retries = max_retries
|
|
95
|
+
|
|
96
|
+
self._http = HTTPClient(
|
|
97
|
+
api_key=self._api_key,
|
|
98
|
+
base_url=self._base_url,
|
|
99
|
+
timeout=self._timeout,
|
|
100
|
+
max_retries=self._max_retries,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def base_url(self) -> str:
|
|
105
|
+
"""Get the API base URL."""
|
|
106
|
+
return self._base_url
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def timeout(self) -> float:
|
|
110
|
+
"""Get the request timeout."""
|
|
111
|
+
return self._timeout
|
|
112
|
+
|
|
113
|
+
def actor(self, actor_url: str) -> ActorClient:
|
|
114
|
+
"""
|
|
115
|
+
Get an actor client for a specific scraper.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
actor_url: Apify-style actor URL or GoFetch scraper type.
|
|
119
|
+
Supported values:
|
|
120
|
+
- "apify/instagram-scraper" -> instagram
|
|
121
|
+
- "apify/instagram-profile-scraper" -> instagram_profile
|
|
122
|
+
- "clockworks/tiktok-profile-scraper" -> tiktok
|
|
123
|
+
- "streamers/youtube-scraper" -> youtube
|
|
124
|
+
- Or direct: "instagram", "tiktok", "youtube"
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
ActorClient instance for the specified scraper
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
ValueError: If actor_url is empty
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
# Using Apify URL (for compatibility)
|
|
134
|
+
actor = client.actor("apify/instagram-scraper")
|
|
135
|
+
|
|
136
|
+
# Using GoFetch type directly
|
|
137
|
+
actor = client.actor("instagram")
|
|
138
|
+
"""
|
|
139
|
+
if not actor_url:
|
|
140
|
+
raise ValueError("actor_id must not be empty")
|
|
141
|
+
scraper_type = resolve_actor_url(actor_url)
|
|
142
|
+
return ActorClient(
|
|
143
|
+
http=self._http,
|
|
144
|
+
scraper_type=scraper_type,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def dataset(self, dataset_id: str) -> DatasetClient:
|
|
148
|
+
"""
|
|
149
|
+
Get a dataset client to fetch results.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
dataset_id: Dataset/Job ID. In GoFetch, the job ID serves
|
|
153
|
+
as the dataset ID (they are equivalent).
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
DatasetClient instance for fetching results
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
run = actor.call(run_input={...})
|
|
160
|
+
dataset = client.dataset(run["defaultDatasetId"])
|
|
161
|
+
items = list(dataset.iterate_items())
|
|
162
|
+
"""
|
|
163
|
+
return DatasetClient(
|
|
164
|
+
http=self._http,
|
|
165
|
+
job_id=dataset_id,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def run(self, run_id: str) -> RunClient:
|
|
169
|
+
"""Get a run client for a specific job."""
|
|
170
|
+
return RunClient(http=self._http, run_id=run_id)
|
|
171
|
+
|
|
172
|
+
def webhook(self, webhook_id: str) -> WebhookClient:
|
|
173
|
+
"""Get a webhook client for a specific webhook."""
|
|
174
|
+
return WebhookClient(http=self._http, webhook_id=webhook_id)
|
|
175
|
+
|
|
176
|
+
def webhooks(self) -> WebhookCollectionClient:
|
|
177
|
+
"""Get webhook collection client for listing/creating webhooks."""
|
|
178
|
+
return WebhookCollectionClient(http=self._http)
|
|
179
|
+
|
|
180
|
+
def key_value_store(self, store_id: str) -> NoReturn:
|
|
181
|
+
"""Not supported in GoFetch.
|
|
182
|
+
|
|
183
|
+
Apify's key-value store has no GoFetch equivalent.
|
|
184
|
+
"""
|
|
185
|
+
raise NotImplementedError(
|
|
186
|
+
f"GoFetch does not have a key-value store. "
|
|
187
|
+
f"key_value_store('{store_id}') cannot be used. "
|
|
188
|
+
f"Media uploaded by GoFetch scrapers is accessible via direct URLs "
|
|
189
|
+
f"and does not require manual cleanup."
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def close(self) -> None:
|
|
193
|
+
"""Close the client and release resources."""
|
|
194
|
+
self._http.close()
|
|
195
|
+
|
|
196
|
+
def __enter__(self) -> GoFetchClient:
|
|
197
|
+
"""Context manager entry."""
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
def __exit__(self, *args: object) -> None:
|
|
201
|
+
"""Context manager exit."""
|
|
202
|
+
self.close()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class AsyncGoFetchClient:
|
|
206
|
+
"""
|
|
207
|
+
Async GoFetch API client.
|
|
208
|
+
|
|
209
|
+
Same interface as GoFetchClient but uses async/await.
|
|
210
|
+
|
|
211
|
+
Usage:
|
|
212
|
+
async with AsyncGoFetchClient(api_key="...") as client:
|
|
213
|
+
actor = client.actor("instagram")
|
|
214
|
+
run = await actor.call(run_input={...})
|
|
215
|
+
dataset = client.dataset(run["defaultDatasetId"])
|
|
216
|
+
items = await dataset.list_items()
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
def __init__(
|
|
220
|
+
self,
|
|
221
|
+
api_key: str | None = None,
|
|
222
|
+
token: str | None = None,
|
|
223
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
224
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
225
|
+
max_retries: int = 3,
|
|
226
|
+
) -> None:
|
|
227
|
+
"""Initialize the async GoFetch client."""
|
|
228
|
+
self._api_key = api_key or token
|
|
229
|
+
if not self._api_key:
|
|
230
|
+
raise ValueError("Either 'api_key' or 'token' must be provided")
|
|
231
|
+
|
|
232
|
+
self._base_url = base_url.rstrip("/")
|
|
233
|
+
self._timeout = timeout
|
|
234
|
+
self._max_retries = max_retries
|
|
235
|
+
|
|
236
|
+
self._http = AsyncHTTPClient(
|
|
237
|
+
api_key=self._api_key,
|
|
238
|
+
base_url=self._base_url,
|
|
239
|
+
timeout=self._timeout,
|
|
240
|
+
max_retries=self._max_retries,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def base_url(self) -> str:
|
|
245
|
+
"""Get the API base URL."""
|
|
246
|
+
return self._base_url
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def timeout(self) -> float:
|
|
250
|
+
"""Get the request timeout."""
|
|
251
|
+
return self._timeout
|
|
252
|
+
|
|
253
|
+
def actor(self, actor_url: str) -> AsyncActorClient:
|
|
254
|
+
"""Get an async actor client for a specific scraper."""
|
|
255
|
+
if not actor_url:
|
|
256
|
+
raise ValueError("actor_id must not be empty")
|
|
257
|
+
scraper_type = resolve_actor_url(actor_url)
|
|
258
|
+
return AsyncActorClient(
|
|
259
|
+
http=self._http,
|
|
260
|
+
scraper_type=scraper_type,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def dataset(self, dataset_id: str) -> AsyncDatasetClient:
|
|
264
|
+
"""Get an async dataset client to fetch results."""
|
|
265
|
+
return AsyncDatasetClient(
|
|
266
|
+
http=self._http,
|
|
267
|
+
job_id=dataset_id,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def run(self, run_id: str) -> AsyncRunClient:
|
|
271
|
+
"""Get an async run client for a specific job."""
|
|
272
|
+
return AsyncRunClient(http=self._http, run_id=run_id)
|
|
273
|
+
|
|
274
|
+
def webhook(self, webhook_id: str) -> AsyncWebhookClient:
|
|
275
|
+
"""Get an async webhook client for a specific webhook."""
|
|
276
|
+
return AsyncWebhookClient(http=self._http, webhook_id=webhook_id)
|
|
277
|
+
|
|
278
|
+
def webhooks(self) -> AsyncWebhookCollectionClient:
|
|
279
|
+
"""Get async webhook collection client for listing/creating webhooks."""
|
|
280
|
+
return AsyncWebhookCollectionClient(http=self._http)
|
|
281
|
+
|
|
282
|
+
def key_value_store(self, store_id: str) -> NoReturn:
|
|
283
|
+
"""Not supported in GoFetch."""
|
|
284
|
+
raise NotImplementedError(
|
|
285
|
+
f"GoFetch does not have a key-value store. "
|
|
286
|
+
f"key_value_store('{store_id}') cannot be used. "
|
|
287
|
+
f"Media uploaded by GoFetch scrapers is accessible via direct URLs "
|
|
288
|
+
f"and does not require manual cleanup."
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
async def close(self) -> None:
|
|
292
|
+
"""Close the client and release resources."""
|
|
293
|
+
await self._http.close()
|
|
294
|
+
|
|
295
|
+
async def __aenter__(self) -> AsyncGoFetchClient:
|
|
296
|
+
"""Async context manager entry."""
|
|
297
|
+
return self
|
|
298
|
+
|
|
299
|
+
async def __aexit__(self, *args: object) -> None:
|
|
300
|
+
"""Async context manager exit."""
|
|
301
|
+
await self.close()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# Alias for Apify compatibility
|
|
305
|
+
ApifyClient = GoFetchClient
|