scrapyio-sdk 0.1.0__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.
@@ -0,0 +1,64 @@
1
+ # Binaries
2
+ *.exe
3
+ *.exe~
4
+ *.dll
5
+ *.so
6
+ *.dylib
7
+ bin/
8
+ dist/
9
+
10
+ # Test binary
11
+ *.test
12
+
13
+ # Output of the go coverage tool
14
+ *.out
15
+
16
+ # Dependency directories
17
+ vendor/
18
+
19
+ # Go workspace file
20
+ go.work
21
+
22
+ # Environment variables
23
+ .env
24
+ .env.local
25
+ .env.*.local
26
+
27
+ # IDE
28
+ .idea/
29
+ .vscode/
30
+ *.swp
31
+ *.swo
32
+ *~
33
+
34
+ # OS
35
+ .DS_Store
36
+ Thumbs.db
37
+
38
+ # Project specific
39
+ stashedfiles/
40
+ data/
41
+ tmp/
42
+ logs/
43
+ *.log
44
+
45
+ # Config (if contains secrets)
46
+ configs/*.local.yaml
47
+
48
+ # Node.js
49
+ node_modules/
50
+
51
+ # Next.js
52
+ .next/
53
+ out/
54
+
55
+ # Build
56
+ **/dist/
57
+ backend/.env.example
58
+
59
+ # Admin
60
+ frontend/src/lib/api/admin.ts
61
+ backend/src/services/s3Image.ts
62
+ backend/src/routes/admin.ts
63
+ frontend/src/app/admin/upload-client.tsx
64
+ **/upload-client.tsx
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.5
2
+ Name: scrapyio-sdk
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Scrapy.io Platform API and publisher execution APIs
5
+ Project-URL: Homepage, https://scrapy.io
6
+ Project-URL: Documentation, https://docs.scrapy.io
7
+ Project-URL: Repository, https://github.com/scrapyio/public-dd
8
+ Author: Scrapy.io
9
+ License-Expression: MIT
10
+ Keywords: scrapy.io,scrapyio,sdk,web-scraping
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: httpx>=0.27
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8.0; extra == 'dev'
24
+ Requires-Dist: respx>=0.21; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # scrapyio-sdk
28
+
29
+ Official Python SDK for [Scrapy.io](https://scrapy.io).
30
+
31
+ **Server-side only.** Keep API keys off client devices.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install scrapyio-sdk
37
+ ```
38
+
39
+ ## Authentication
40
+
41
+ ```python
42
+ import os
43
+ from scrapyio_sdk import ScrapyIO
44
+
45
+ client = ScrapyIO(api_key=os.environ["SCRAPYIO_API_KEY"])
46
+ ```
47
+
48
+ Always sends `Authorization: Bearer <api_key>`.
49
+
50
+ ## First request
51
+
52
+ ```python
53
+ tools = client.tools.list(q="instagram", limit=20)
54
+ # {"items", "total", "offset", "limit"}
55
+
56
+ tool = client.tools.get("datadoping", "instagram-profile-scraper")
57
+ # or:
58
+ same = client.tool("datadoping/instagram-profile-scraper").get()
59
+ ```
60
+
61
+ ## Run a scraper
62
+
63
+ ### Sync (`/v1/api`)
64
+
65
+ ```python
66
+ result = client.tool("datadoping/instagram-profile-scraper").call({
67
+ "username": "nasa",
68
+ })
69
+ ```
70
+
71
+ ### Async (`/v1/scraper`) + poll
72
+
73
+ ```python
74
+ run = client.tool("datadoping/instagram-profile-scraper").start({
75
+ "usernames": ["nasa"],
76
+ })
77
+
78
+ finished = client.run(run["id"]).wait(
79
+ poll_interval_ms=3000,
80
+ timeout_ms=15 * 60_000,
81
+ )
82
+
83
+ page = client.run(run["id"]).list_items(offset=0, limit=100)
84
+ ```
85
+
86
+ ## Schedules
87
+
88
+ ```python
89
+ schedule = client.schedules.create(
90
+ {
91
+ "publisher": "datadoping",
92
+ "slug": "instagram-profile-scraper",
93
+ "runName": "nightly-ig",
94
+ "timezone": "UTC",
95
+ "frequency": "one-time",
96
+ "date": "2026-09-20",
97
+ "time": "23:50",
98
+ "inputs": ["nasa"],
99
+ },
100
+ idempotency_key="nightly-ig-v1",
101
+ )
102
+
103
+ client.schedules.update(schedule["id"], {"isActive": False})
104
+ client.schedules.delete(schedule["id"])
105
+ ```
106
+
107
+ ## Errors
108
+
109
+ ```python
110
+ from scrapyio_sdk import ScrapyIO, ScrapyIOError
111
+
112
+ try:
113
+ client.tools.get("nope", "missing")
114
+ except ScrapyIOError as err:
115
+ # err.type, err.status, err.message, err.doc_url
116
+ ...
117
+ ```
118
+
119
+ ## Mental model
120
+
121
+ ```text
122
+ client
123
+ ├── tools.list / tools.get
124
+ ├── tool("publisher/slug").get / .call / .start
125
+ ├── runs.list / runs.get
126
+ ├── run(id).get / .wait / .list_items
127
+ └── schedules.list / get / create / update / delete
128
+ ```
129
+
130
+ ## Development
131
+
132
+ ```bash
133
+ cd sdk/python
134
+ python -m pip install -e ".[dev]"
135
+ pytest
136
+ ```
137
+
138
+ ## Notes
139
+
140
+ - GET requests may retry on transient 5xx/429; execution POSTs are not auto-retried.
141
+ - `list_items()` is JSON-only in v0.1.
142
+ - Companion JS package: [`@scrapyio/sdk`](https://www.npmjs.com/package/@scrapyio/sdk).
@@ -0,0 +1,116 @@
1
+ # scrapyio-sdk
2
+
3
+ Official Python SDK for [Scrapy.io](https://scrapy.io).
4
+
5
+ **Server-side only.** Keep API keys off client devices.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install scrapyio-sdk
11
+ ```
12
+
13
+ ## Authentication
14
+
15
+ ```python
16
+ import os
17
+ from scrapyio_sdk import ScrapyIO
18
+
19
+ client = ScrapyIO(api_key=os.environ["SCRAPYIO_API_KEY"])
20
+ ```
21
+
22
+ Always sends `Authorization: Bearer <api_key>`.
23
+
24
+ ## First request
25
+
26
+ ```python
27
+ tools = client.tools.list(q="instagram", limit=20)
28
+ # {"items", "total", "offset", "limit"}
29
+
30
+ tool = client.tools.get("datadoping", "instagram-profile-scraper")
31
+ # or:
32
+ same = client.tool("datadoping/instagram-profile-scraper").get()
33
+ ```
34
+
35
+ ## Run a scraper
36
+
37
+ ### Sync (`/v1/api`)
38
+
39
+ ```python
40
+ result = client.tool("datadoping/instagram-profile-scraper").call({
41
+ "username": "nasa",
42
+ })
43
+ ```
44
+
45
+ ### Async (`/v1/scraper`) + poll
46
+
47
+ ```python
48
+ run = client.tool("datadoping/instagram-profile-scraper").start({
49
+ "usernames": ["nasa"],
50
+ })
51
+
52
+ finished = client.run(run["id"]).wait(
53
+ poll_interval_ms=3000,
54
+ timeout_ms=15 * 60_000,
55
+ )
56
+
57
+ page = client.run(run["id"]).list_items(offset=0, limit=100)
58
+ ```
59
+
60
+ ## Schedules
61
+
62
+ ```python
63
+ schedule = client.schedules.create(
64
+ {
65
+ "publisher": "datadoping",
66
+ "slug": "instagram-profile-scraper",
67
+ "runName": "nightly-ig",
68
+ "timezone": "UTC",
69
+ "frequency": "one-time",
70
+ "date": "2026-09-20",
71
+ "time": "23:50",
72
+ "inputs": ["nasa"],
73
+ },
74
+ idempotency_key="nightly-ig-v1",
75
+ )
76
+
77
+ client.schedules.update(schedule["id"], {"isActive": False})
78
+ client.schedules.delete(schedule["id"])
79
+ ```
80
+
81
+ ## Errors
82
+
83
+ ```python
84
+ from scrapyio_sdk import ScrapyIO, ScrapyIOError
85
+
86
+ try:
87
+ client.tools.get("nope", "missing")
88
+ except ScrapyIOError as err:
89
+ # err.type, err.status, err.message, err.doc_url
90
+ ...
91
+ ```
92
+
93
+ ## Mental model
94
+
95
+ ```text
96
+ client
97
+ ├── tools.list / tools.get
98
+ ├── tool("publisher/slug").get / .call / .start
99
+ ├── runs.list / runs.get
100
+ ├── run(id).get / .wait / .list_items
101
+ └── schedules.list / get / create / update / delete
102
+ ```
103
+
104
+ ## Development
105
+
106
+ ```bash
107
+ cd sdk/python
108
+ python -m pip install -e ".[dev]"
109
+ pytest
110
+ ```
111
+
112
+ ## Notes
113
+
114
+ - GET requests may retry on transient 5xx/429; execution POSTs are not auto-retried.
115
+ - `list_items()` is JSON-only in v0.1.
116
+ - Companion JS package: [`@scrapyio/sdk`](https://www.npmjs.com/package/@scrapyio/sdk).
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "scrapyio-sdk"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the Scrapy.io Platform API and publisher execution APIs"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Scrapy.io" }]
13
+ keywords = ["scrapyio", "scrapy.io", "web-scraping", "sdk"]
14
+ dependencies = [
15
+ "httpx>=0.27",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Typing :: Typed",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://scrapy.io"
31
+ Documentation = "https://docs.scrapy.io"
32
+ Repository = "https://github.com/scrapyio/public-dd"
33
+
34
+ [project.optional-dependencies]
35
+ dev = ["pytest>=8.0", "respx>=0.21"]
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ only-include = ["scrapyio_sdk"]
39
+
40
+ [tool.hatch.build.targets.sdist]
41
+ include = ["src/scrapyio_sdk", "README.md", "pyproject.toml", "tests"]
42
+
43
+ [tool.pytest.ini_options]
44
+ testpaths = ["tests"]
45
+ pythonpath = ["src"]
@@ -0,0 +1,20 @@
1
+ """Official Python SDK for Scrapy.io."""
2
+
3
+ from .client import ScrapyIO
4
+ from .errors import ScrapyIOError
5
+ from .pagination import Page
6
+ from .run import RunHandle
7
+ from .tool import ToolHandle
8
+ from .types import TERMINAL_RUN_STATUSES, is_terminal_run_status
9
+
10
+ __all__ = [
11
+ "ScrapyIO",
12
+ "ScrapyIOError",
13
+ "Page",
14
+ "RunHandle",
15
+ "ToolHandle",
16
+ "TERMINAL_RUN_STATUSES",
17
+ "is_terminal_run_status",
18
+ ]
19
+
20
+ __version__ = "0.1.0"
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+
5
+ from .errors import ScrapyIOError
6
+ from .platform import PlatformClient
7
+ from .run import RunHandle
8
+ from .runs import RunsResource
9
+ from .schedules import SchedulesResource
10
+ from .tool import ToolHandle
11
+ from .tools import ToolsResource
12
+
13
+ DEFAULT_BASE_URL = "https://api.scrapy.io/v1"
14
+
15
+
16
+ class ScrapyIO:
17
+ """Official Scrapy.io Python SDK.
18
+
19
+ API keys must stay server-side. Do not embed keys in client apps.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ *,
25
+ api_key: str,
26
+ base_url: str = DEFAULT_BASE_URL,
27
+ transport: httpx.BaseTransport | None = None,
28
+ max_retries: int = 2,
29
+ retry_backoff_ms: int = 250,
30
+ timeout: float = 60.0,
31
+ ) -> None:
32
+ key = (api_key or "").strip()
33
+ if not key:
34
+ raise ScrapyIOError(
35
+ "ScrapyIO requires an api_key (e.g. os.environ['SCRAPYIO_API_KEY'])",
36
+ type="sdk_error",
37
+ )
38
+
39
+ self._api_key = key
40
+ self._transport = transport
41
+ self._platform = PlatformClient(
42
+ base_url=base_url,
43
+ api_key=key,
44
+ transport=transport,
45
+ max_retries=max_retries,
46
+ retry_backoff_ms=retry_backoff_ms,
47
+ timeout=timeout,
48
+ )
49
+ self.tools = ToolsResource(self._platform)
50
+ self.runs = RunsResource(self._platform)
51
+ self.schedules = SchedulesResource(self._platform)
52
+
53
+ def close(self) -> None:
54
+ self._platform.close()
55
+
56
+ def __enter__(self) -> ScrapyIO:
57
+ return self
58
+
59
+ def __exit__(self, *args: object) -> None:
60
+ self.close()
61
+
62
+ def tool(self, ref: str) -> ToolHandle:
63
+ return ToolHandle(
64
+ self._platform,
65
+ api_key=self._api_key,
66
+ transport=self._transport,
67
+ ref=ref,
68
+ )
69
+
70
+ def run(self, run_id: str) -> RunHandle:
71
+ return RunHandle(self._platform, run_id)
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ PlatformErrorType = Literal[
6
+ "unauthorized",
7
+ "forbidden",
8
+ "not_found",
9
+ "validation_error",
10
+ "insufficient_credits",
11
+ "conflict",
12
+ "rate_limit_exceeded",
13
+ "internal_error",
14
+ "timeout",
15
+ "publisher_error",
16
+ "sdk_error",
17
+ ]
18
+
19
+ _PLATFORM_ERROR_TYPES = frozenset(
20
+ {
21
+ "unauthorized",
22
+ "forbidden",
23
+ "not_found",
24
+ "validation_error",
25
+ "insufficient_credits",
26
+ "conflict",
27
+ "rate_limit_exceeded",
28
+ "internal_error",
29
+ }
30
+ )
31
+
32
+
33
+ class ScrapyIOError(Exception):
34
+ def __init__(
35
+ self,
36
+ message: str,
37
+ *,
38
+ type: PlatformErrorType = "sdk_error",
39
+ status: int | None = None,
40
+ doc_url: str | None = None,
41
+ body: Any = None,
42
+ ) -> None:
43
+ super().__init__(message)
44
+ self.message = message
45
+ self.type = type
46
+ self.status = status
47
+ self.doc_url = doc_url
48
+ self.body = body
49
+
50
+ def __str__(self) -> str:
51
+ return self.message
52
+
53
+
54
+ def is_platform_error_type(value: str) -> bool:
55
+ return value in _PLATFORM_ERROR_TYPES
56
+
57
+
58
+ def error_from_platform_body(
59
+ status: int,
60
+ body: Any,
61
+ fallback_message: str = "Platform API request failed",
62
+ ) -> ScrapyIOError:
63
+ if isinstance(body, dict):
64
+ err = body.get("error")
65
+ if isinstance(err, dict):
66
+ type_raw = err.get("type") if isinstance(err.get("type"), str) else "internal_error"
67
+ err_type: PlatformErrorType = (
68
+ type_raw if is_platform_error_type(type_raw) else "internal_error" # type: ignore[assignment]
69
+ )
70
+ msg = err.get("message")
71
+ return ScrapyIOError(
72
+ msg if isinstance(msg, str) and msg else fallback_message,
73
+ type=err_type,
74
+ status=status,
75
+ doc_url=err.get("doc_url") if isinstance(err.get("doc_url"), str) else None,
76
+ body=body,
77
+ )
78
+ return ScrapyIOError(
79
+ fallback_message,
80
+ type="unauthorized" if status == 401 else "internal_error",
81
+ status=status,
82
+ body=body,
83
+ )
84
+
85
+
86
+ def error_from_publisher_body(
87
+ status: int,
88
+ body: Any,
89
+ fallback_message: str = "Publisher execution request failed",
90
+ ) -> ScrapyIOError:
91
+ if isinstance(body, dict):
92
+ if body.get("error") == "INSUFFICIENT_CREDITS":
93
+ msg = body.get("message")
94
+ return ScrapyIOError(
95
+ msg if isinstance(msg, str) and msg else "Insufficient credits",
96
+ type="insufficient_credits",
97
+ status=status or 402,
98
+ body=body,
99
+ )
100
+ err = body.get("error")
101
+ if isinstance(err, str) and err:
102
+ message = body.get("message")
103
+ full = f"{err}: {message}" if isinstance(message, str) and message else err
104
+ if status == 401:
105
+ err_type: PlatformErrorType = "unauthorized"
106
+ elif status == 404:
107
+ err_type = "not_found"
108
+ elif status == 400:
109
+ err_type = "validation_error"
110
+ elif status == 409:
111
+ err_type = "conflict"
112
+ else:
113
+ err_type = "publisher_error"
114
+ return ScrapyIOError(full, type=err_type, status=status, body=body)
115
+ if body.get("success") is False and isinstance(body.get("error"), str):
116
+ return ScrapyIOError(
117
+ body["error"],
118
+ type="unauthorized" if status == 401 else "publisher_error",
119
+ status=status,
120
+ body=body,
121
+ )
122
+ return ScrapyIOError(fallback_message, type="publisher_error", status=status, body=body)
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+ from .errors import ScrapyIOError, error_from_publisher_body
9
+ from .types import JsonObject, RunStatus
10
+
11
+
12
+ def unwrap_publisher_success(body: Any) -> Any:
13
+ current = body
14
+ for _ in range(4):
15
+ if not isinstance(current, dict) or "success" not in current:
16
+ return current
17
+ if current.get("success") is False:
18
+ raise error_from_publisher_body(400, current)
19
+ if current.get("success") is not True:
20
+ return current
21
+ current = current.get("data")
22
+ return current
23
+
24
+
25
+ def normalize_publisher_status(raw: str | None) -> RunStatus:
26
+ if not raw:
27
+ return "queued"
28
+ if raw == "completed":
29
+ return "succeeded"
30
+ if raw in {"queued", "running", "succeeded", "failed", "cancelled", "partial"}:
31
+ return raw # type: ignore[return-value]
32
+ return "queued"
33
+
34
+
35
+ def execute_call(
36
+ *,
37
+ api_key: str,
38
+ api_url: str,
39
+ input: JsonObject,
40
+ transport: httpx.BaseTransport | None = None,
41
+ timeout: float = 120.0,
42
+ ) -> Any:
43
+ with httpx.Client(transport=transport, timeout=timeout) as client:
44
+ response = client.post(
45
+ api_url,
46
+ headers={
47
+ "Authorization": f"Bearer {api_key}",
48
+ "Content-Type": "application/json",
49
+ "Accept": "application/json",
50
+ },
51
+ json=input,
52
+ )
53
+ body = _read_json(response)
54
+ if not response.is_success:
55
+ raise error_from_publisher_body(response.status_code, body)
56
+ return unwrap_publisher_success(body)
57
+
58
+
59
+ def execute_start(
60
+ *,
61
+ api_key: str,
62
+ scraper_url: str,
63
+ input: JsonObject,
64
+ transport: httpx.BaseTransport | None = None,
65
+ timeout: float = 120.0,
66
+ ) -> dict[str, Any]:
67
+ with httpx.Client(transport=transport, timeout=timeout) as client:
68
+ response = client.post(
69
+ scraper_url,
70
+ headers={
71
+ "Authorization": f"Bearer {api_key}",
72
+ "Content-Type": "application/json",
73
+ "Accept": "application/json",
74
+ },
75
+ json=input,
76
+ )
77
+ body = _read_json(response)
78
+ if not response.is_success:
79
+ raise error_from_publisher_body(response.status_code, body)
80
+ if not isinstance(body, dict) or not isinstance(body.get("taskId"), str) or not body["taskId"]:
81
+ raise ScrapyIOError(
82
+ "Publisher did not return a taskId",
83
+ type="publisher_error",
84
+ status=response.status_code,
85
+ body=body,
86
+ )
87
+ status = normalize_publisher_status(body.get("status") if isinstance(body.get("status"), str) else None)
88
+ return {
89
+ "id": body["taskId"],
90
+ "kind": "async_batch",
91
+ "status": status,
92
+ "toolId": "",
93
+ "runName": body.get("runName"),
94
+ "totalItems": body.get("totalItems"),
95
+ "processedItems": body.get("processedItems"),
96
+ "completedItems": body.get("completedItems"),
97
+ "failedItems": body.get("failedItems"),
98
+ "estimatedCost": body.get("estimatedCost"),
99
+ "billedAmount": None,
100
+ "publisher": None,
101
+ "toolSlug": None,
102
+ "createdAt": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
103
+ "completedAt": None,
104
+ }
105
+
106
+
107
+ def _read_json(response: httpx.Response) -> Any:
108
+ if not response.content:
109
+ return None
110
+ try:
111
+ return response.json()
112
+ except ValueError:
113
+ return response.text