socialscrape-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.
- socialscrape_sdk-0.1.0/LICENSE +21 -0
- socialscrape_sdk-0.1.0/MANIFEST.in +2 -0
- socialscrape_sdk-0.1.0/PKG-INFO +246 -0
- socialscrape_sdk-0.1.0/README.md +230 -0
- socialscrape_sdk-0.1.0/pyproject.toml +38 -0
- socialscrape_sdk-0.1.0/setup.cfg +4 -0
- socialscrape_sdk-0.1.0/src/socialscrape/__init__.py +32 -0
- socialscrape_sdk-0.1.0/src/socialscrape/_base_client.py +488 -0
- socialscrape_sdk-0.1.0/src/socialscrape/_batch.py +80 -0
- socialscrape_sdk-0.1.0/src/socialscrape/_errors_base.py +89 -0
- socialscrape_sdk-0.1.0/src/socialscrape/_generated/__init__.py +3 -0
- socialscrape_sdk-0.1.0/src/socialscrape/_generated/endpoints.py +219 -0
- socialscrape_sdk-0.1.0/src/socialscrape/_generated/errors.py +182 -0
- socialscrape_sdk-0.1.0/src/socialscrape/_generated/models.py +617 -0
- socialscrape_sdk-0.1.0/src/socialscrape/_generated/resources.py +14103 -0
- socialscrape_sdk-0.1.0/src/socialscrape/_pagination.py +87 -0
- socialscrape_sdk-0.1.0/src/socialscrape/_response.py +55 -0
- socialscrape_sdk-0.1.0/src/socialscrape/_version.py +1 -0
- socialscrape_sdk-0.1.0/src/socialscrape/client.py +67 -0
- socialscrape_sdk-0.1.0/src/socialscrape/errors.py +23 -0
- socialscrape_sdk-0.1.0/src/socialscrape/models.py +3 -0
- socialscrape_sdk-0.1.0/src/socialscrape/py.typed +0 -0
- socialscrape_sdk-0.1.0/src/socialscrape_sdk.egg-info/PKG-INFO +246 -0
- socialscrape_sdk-0.1.0/src/socialscrape_sdk.egg-info/SOURCES.txt +25 -0
- socialscrape_sdk-0.1.0/src/socialscrape_sdk.egg-info/dependency_links.txt +1 -0
- socialscrape_sdk-0.1.0/src/socialscrape_sdk.egg-info/requires.txt +1 -0
- socialscrape_sdk-0.1.0/src/socialscrape_sdk.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The Casey Company LLC (SocialScrape)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: socialscrape-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SocialScrape API client: social media data (profiles, posts, comments, search, ads, transcripts) in one response shape.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://socialscrape.dev
|
|
7
|
+
Project-URL: Documentation, https://socialscrape.dev/docs
|
|
8
|
+
Keywords: socialscrape,social media,api,tiktok,instagram,youtube,transcripts
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Typing :: Typed
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: httpx<1,>=0.27
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# socialscrape (Python)
|
|
18
|
+
|
|
19
|
+
Social media data as an API: profiles, posts, comments, search, ads and
|
|
20
|
+
transcripts across platforms, in one response shape. This is the official Python
|
|
21
|
+
client for the [SocialScrape API](https://socialscrape.dev/docs).
|
|
22
|
+
|
|
23
|
+
- Sync (`SocialScrape`) and asyncio (`AsyncSocialScrape`) clients on `httpx`.
|
|
24
|
+
- Typed methods per endpoint, grouped by platform; responses typed with `TypedDict`s.
|
|
25
|
+
- Typed exceptions per error code, retries with backoff and `Retry-After`, timeouts.
|
|
26
|
+
- `credits_charged` and `credits_remaining` on every response.
|
|
27
|
+
- Cursor auto-pagination, long-job polling, batch calls.
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pip install socialscrape-sdk
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Python 3.11 or newer.
|
|
34
|
+
|
|
35
|
+
Your API key is a secret: keep it on your server, in an environment variable or a secrets manager, and never in client-side code, a public repository or a log.
|
|
36
|
+
|
|
37
|
+
## Quickstart
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
export SOCIALSCRAPE_API_KEY=ss_live_...
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from socialscrape import SocialScrape
|
|
45
|
+
|
|
46
|
+
client = SocialScrape() # reads SOCIALSCRAPE_API_KEY
|
|
47
|
+
|
|
48
|
+
res = client.tiktok.profile(handle="khaby.lame")
|
|
49
|
+
|
|
50
|
+
print(res.data["followers"]) # the normalised profile (a plain dict)
|
|
51
|
+
print(res.credits_charged) # 1
|
|
52
|
+
print(res.credits_remaining) # what is left
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Methods are grouped by platform and named after the endpoint, with the API's own
|
|
56
|
+
parameter names as keyword arguments: `client.tiktok.profile_videos(handle=...)`,
|
|
57
|
+
`client.instagram.post_comments(url=...)`, `client.youtube.video_transcript(url=...)`,
|
|
58
|
+
`client.apple_music.artist(...)`. `client.transcript(url=...)` takes a link from
|
|
59
|
+
any supported platform.
|
|
60
|
+
|
|
61
|
+
Async is the same, awaited:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import asyncio
|
|
65
|
+
from socialscrape import AsyncSocialScrape
|
|
66
|
+
|
|
67
|
+
async def main():
|
|
68
|
+
async with AsyncSocialScrape() as client:
|
|
69
|
+
res = await client.tiktok.profile(handle="khaby.lame")
|
|
70
|
+
print(res.data["followers"])
|
|
71
|
+
|
|
72
|
+
asyncio.run(main())
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## The response
|
|
76
|
+
|
|
77
|
+
Every method returns a `Response` with the envelope fields, typed:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
res.data # the endpoint's model (Profile, Post, ListPage[Post], ...)
|
|
81
|
+
res.credits_charged # 0 on a cache hit
|
|
82
|
+
res.credits_remaining # the team's balance after this call, or None
|
|
83
|
+
res.cached, res.cached_at
|
|
84
|
+
res.request_id # quote it to support
|
|
85
|
+
res.cursor, res.has_more # lists: copies of data["cursor"] / data["has_more"]
|
|
86
|
+
res.raw # only with raw=True: the platform's original object
|
|
87
|
+
res.http.status_code, res.http.headers, res.http.attempts
|
|
88
|
+
res.http.rate_limit_limit, res.http.rate_limit_remaining # both None: the api sends no x-ratelimit-* headers
|
|
89
|
+
res.http.ignored_params
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The client keeps count too: `client.credits_remaining` (last balance seen) and
|
|
93
|
+
`client.credits_charged_total`.
|
|
94
|
+
|
|
95
|
+
Every endpoint also accepts `trim=True` (smaller response), `raw=True` (adds the
|
|
96
|
+
platform's original object) and `max_age_days=7` (accept a cached result up to 7
|
|
97
|
+
days old for 0 credits). Models live in `socialscrape.models`.
|
|
98
|
+
|
|
99
|
+
## Errors
|
|
100
|
+
|
|
101
|
+
Failures are free (`credits_charged` is 0) and raise a class per error code. Branch
|
|
102
|
+
on the class or `code`, never on the message.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from socialscrape import errors
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
client.tiktok.profile(handle="no-such-account-123")
|
|
109
|
+
except errors.NotFoundError as e:
|
|
110
|
+
print(e.code, e.http_status, e.request_id, e.retryable) # not_found 404 req_... False
|
|
111
|
+
except errors.InsufficientCreditsError:
|
|
112
|
+
... # top up
|
|
113
|
+
except errors.APIError as e:
|
|
114
|
+
print(e.code, e.retryable)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
| Class | `code` | HTTP | Retryable |
|
|
118
|
+
|---|---|---|---|
|
|
119
|
+
| `InvalidParamsError` | `invalid_params` | 400 | no |
|
|
120
|
+
| `UnsupportedUrlError` | `unsupported_url` | 400 | no |
|
|
121
|
+
| `UnauthorizedError` | `unauthorized` | 401 | no |
|
|
122
|
+
| `InsufficientCreditsError` | `insufficient_credits` | 402 | no |
|
|
123
|
+
| `ForbiddenError` | `forbidden` | 403 | no |
|
|
124
|
+
| `PrivateError` | `private` | 403 | no |
|
|
125
|
+
| `AgeRestrictedError` | `age_restricted` | 403 | no |
|
|
126
|
+
| `NotFoundError` | `not_found` | 404 | no |
|
|
127
|
+
| `TranscriptUnavailableError` | `transcript_unavailable` | 404 | no |
|
|
128
|
+
| `ConflictError` | `conflict` | 409 | no |
|
|
129
|
+
| `RemovedError` | `removed` | 410 | no |
|
|
130
|
+
| `UnavailableInRegionError` | `unavailable_in_region` | 451 | no |
|
|
131
|
+
| `RateLimitedError` | `rate_limited` | 429 | yes |
|
|
132
|
+
| `UpstreamRateLimitedError` | `upstream_rate_limited` | 503 | yes |
|
|
133
|
+
| `UpstreamBlockedError` | `upstream_blocked` | 503 | yes |
|
|
134
|
+
| `OverloadedError` | `overloaded` | 503 | yes |
|
|
135
|
+
| `UpstreamErrorError` | `upstream_error` | 502 | yes |
|
|
136
|
+
| `UpstreamChangedError` | `upstream_changed` | 502 | no |
|
|
137
|
+
| `UpstreamTimeoutError` | `upstream_timeout` | 504 | yes |
|
|
138
|
+
| `InternalError` | `internal` | 500 | yes |
|
|
139
|
+
| `NotImplementedYetError` | `not_implemented` | 501 | no |
|
|
140
|
+
|
|
141
|
+
All extend `errors.APIError` (attributes `code`, `message`, `retryable`,
|
|
142
|
+
`http_status`, `request_id`, `retry_after`, `headers`, `body`, `credits_charged`).
|
|
143
|
+
A code this version does not know yet still arrives as a plain `APIError`. Beyond
|
|
144
|
+
the API: `SocialScrapeConnectionError`, `SocialScrapeTimeoutError` (and its
|
|
145
|
+
subclass `JobTimeoutError`), `UnexpectedResponseError` and `MissingApiKeyError`,
|
|
146
|
+
all extending `SocialScrapeError`.
|
|
147
|
+
|
|
148
|
+
## Retries, timeouts, idempotency
|
|
149
|
+
|
|
150
|
+
Retryable errors (the table above, network failures and client timeouts) are
|
|
151
|
+
retried up to `max_retries` times (default 2) with exponential backoff and jitter.
|
|
152
|
+
`Retry-After` is honoured; one longer than `max_retry_after` (60 s) is not waited
|
|
153
|
+
for and the error is raised. POST and DELETE calls carry an `Idempotency-Key`
|
|
154
|
+
(generated, and reused across retries) so a retried write never charges twice.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
client = SocialScrape(
|
|
158
|
+
api_key="ss_live_...", # default: SOCIALSCRAPE_API_KEY
|
|
159
|
+
base_url="https://api.socialscrape.dev", # default: SOCIALSCRAPE_BASE_URL, else this
|
|
160
|
+
timeout=60.0, # seconds, per attempt
|
|
161
|
+
max_retries=2,
|
|
162
|
+
retry_initial_delay=0.5,
|
|
163
|
+
retry_max_delay=8.0,
|
|
164
|
+
max_retry_after=60.0,
|
|
165
|
+
wait_for_jobs=True,
|
|
166
|
+
job_timeout=600.0,
|
|
167
|
+
http_client=my_httpx_client, # optional: an httpx.Client you own
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Per call:
|
|
171
|
+
client.tiktok.profile(handle="khaby.lame", request_options={"timeout": 10, "max_retries": 0})
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`request_options` takes `timeout`, `max_retries`, `idempotency_key`, `headers`,
|
|
175
|
+
`wait_for_job` and `job_timeout`.
|
|
176
|
+
|
|
177
|
+
## Pagination
|
|
178
|
+
|
|
179
|
+
List endpoints return `cursor` and `has_more`. Pass the cursor back, or let the
|
|
180
|
+
client walk it. **Every page is a request and costs what one call costs**, so cap
|
|
181
|
+
the walk with `max_items` or `max_pages`.
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
for video in client.paginate(client.tiktok.profile_videos, handle="khaby.lame", max_items=100):
|
|
185
|
+
print(video["id"], video["stats"]["views"])
|
|
186
|
+
|
|
187
|
+
# whole pages, with each page's credits and cursor
|
|
188
|
+
for page in client.paginate_pages(client.tiktok.profile_videos, handle="khaby.lame", max_pages=3):
|
|
189
|
+
print(len(page.data["items"]), page.credits_charged, page.cursor)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Async: `async for video in client.paginate(client.tiktok.profile_videos, handle=...)`.
|
|
193
|
+
Iteration stops at `has_more` false, at a repeated cursor and at your caps, and
|
|
194
|
+
never fetches a page it does not need to reach `max_items`.
|
|
195
|
+
|
|
196
|
+
## Long jobs (AI transcription, deep listings)
|
|
197
|
+
|
|
198
|
+
When an answer is not ready within a few seconds the API replies `202` with a job
|
|
199
|
+
id. The client follows it for you (polling is free; the finished job is charged
|
|
200
|
+
once) and returns the final response. Switch it off per call or globally:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
pending = client.youtube.video_transcript(url=url, request_options={"wait_for_job": False})
|
|
204
|
+
# pending.http.status_code == 202, pending.data["job_id"]
|
|
205
|
+
done = client.wait_for_job(pending.data["job_id"])
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
A job that outlives `job_timeout` raises `JobTimeoutError` with its `job_id`; it
|
|
209
|
+
keeps running and can be fetched later with `client.jobs.get(job_id=...)`.
|
|
210
|
+
|
|
211
|
+
## Batch and monitors
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
res = client.batch.run([
|
|
215
|
+
{"endpoint": "tiktok.profile", "params": {"handle": "khaby.lame"}},
|
|
216
|
+
{"endpoint": "tiktok.profile", "params": {"handle": "charlidamelio"}},
|
|
217
|
+
])
|
|
218
|
+
res.credits_charged # the sum of the items
|
|
219
|
+
res.data["results"][0]["response"] # each item is a full envelope
|
|
220
|
+
|
|
221
|
+
monitor = client.monitors.create(
|
|
222
|
+
endpoint="/v1/tiktok/profile/videos",
|
|
223
|
+
params={"handle": "khaby.lame"},
|
|
224
|
+
webhook_url="https://example.com/hooks/socialscrape",
|
|
225
|
+
)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Account
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
client.account.credits().data["credits_remaining"] # free
|
|
232
|
+
for row in client.paginate(client.account.usage): # request history
|
|
233
|
+
...
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Newer endpoints
|
|
237
|
+
|
|
238
|
+
Methods are generated from the API's endpoint registry, and only endpoints that
|
|
239
|
+
are built appear (coming-soon ones are hidden). To call a path this client version
|
|
240
|
+
does not know yet:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
res = client.request("GET", "/v1/some/new/endpoint", query={"handle": "x"})
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`socialscrape.ENDPOINTS` lists every endpoint the installed version knows.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# socialscrape (Python)
|
|
2
|
+
|
|
3
|
+
Social media data as an API: profiles, posts, comments, search, ads and
|
|
4
|
+
transcripts across platforms, in one response shape. This is the official Python
|
|
5
|
+
client for the [SocialScrape API](https://socialscrape.dev/docs).
|
|
6
|
+
|
|
7
|
+
- Sync (`SocialScrape`) and asyncio (`AsyncSocialScrape`) clients on `httpx`.
|
|
8
|
+
- Typed methods per endpoint, grouped by platform; responses typed with `TypedDict`s.
|
|
9
|
+
- Typed exceptions per error code, retries with backoff and `Retry-After`, timeouts.
|
|
10
|
+
- `credits_charged` and `credits_remaining` on every response.
|
|
11
|
+
- Cursor auto-pagination, long-job polling, batch calls.
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pip install socialscrape-sdk
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Python 3.11 or newer.
|
|
18
|
+
|
|
19
|
+
Your API key is a secret: keep it on your server, in an environment variable or a secrets manager, and never in client-side code, a public repository or a log.
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
export SOCIALSCRAPE_API_KEY=ss_live_...
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from socialscrape import SocialScrape
|
|
29
|
+
|
|
30
|
+
client = SocialScrape() # reads SOCIALSCRAPE_API_KEY
|
|
31
|
+
|
|
32
|
+
res = client.tiktok.profile(handle="khaby.lame")
|
|
33
|
+
|
|
34
|
+
print(res.data["followers"]) # the normalised profile (a plain dict)
|
|
35
|
+
print(res.credits_charged) # 1
|
|
36
|
+
print(res.credits_remaining) # what is left
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Methods are grouped by platform and named after the endpoint, with the API's own
|
|
40
|
+
parameter names as keyword arguments: `client.tiktok.profile_videos(handle=...)`,
|
|
41
|
+
`client.instagram.post_comments(url=...)`, `client.youtube.video_transcript(url=...)`,
|
|
42
|
+
`client.apple_music.artist(...)`. `client.transcript(url=...)` takes a link from
|
|
43
|
+
any supported platform.
|
|
44
|
+
|
|
45
|
+
Async is the same, awaited:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import asyncio
|
|
49
|
+
from socialscrape import AsyncSocialScrape
|
|
50
|
+
|
|
51
|
+
async def main():
|
|
52
|
+
async with AsyncSocialScrape() as client:
|
|
53
|
+
res = await client.tiktok.profile(handle="khaby.lame")
|
|
54
|
+
print(res.data["followers"])
|
|
55
|
+
|
|
56
|
+
asyncio.run(main())
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## The response
|
|
60
|
+
|
|
61
|
+
Every method returns a `Response` with the envelope fields, typed:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
res.data # the endpoint's model (Profile, Post, ListPage[Post], ...)
|
|
65
|
+
res.credits_charged # 0 on a cache hit
|
|
66
|
+
res.credits_remaining # the team's balance after this call, or None
|
|
67
|
+
res.cached, res.cached_at
|
|
68
|
+
res.request_id # quote it to support
|
|
69
|
+
res.cursor, res.has_more # lists: copies of data["cursor"] / data["has_more"]
|
|
70
|
+
res.raw # only with raw=True: the platform's original object
|
|
71
|
+
res.http.status_code, res.http.headers, res.http.attempts
|
|
72
|
+
res.http.rate_limit_limit, res.http.rate_limit_remaining # both None: the api sends no x-ratelimit-* headers
|
|
73
|
+
res.http.ignored_params
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The client keeps count too: `client.credits_remaining` (last balance seen) and
|
|
77
|
+
`client.credits_charged_total`.
|
|
78
|
+
|
|
79
|
+
Every endpoint also accepts `trim=True` (smaller response), `raw=True` (adds the
|
|
80
|
+
platform's original object) and `max_age_days=7` (accept a cached result up to 7
|
|
81
|
+
days old for 0 credits). Models live in `socialscrape.models`.
|
|
82
|
+
|
|
83
|
+
## Errors
|
|
84
|
+
|
|
85
|
+
Failures are free (`credits_charged` is 0) and raise a class per error code. Branch
|
|
86
|
+
on the class or `code`, never on the message.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from socialscrape import errors
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
client.tiktok.profile(handle="no-such-account-123")
|
|
93
|
+
except errors.NotFoundError as e:
|
|
94
|
+
print(e.code, e.http_status, e.request_id, e.retryable) # not_found 404 req_... False
|
|
95
|
+
except errors.InsufficientCreditsError:
|
|
96
|
+
... # top up
|
|
97
|
+
except errors.APIError as e:
|
|
98
|
+
print(e.code, e.retryable)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Class | `code` | HTTP | Retryable |
|
|
102
|
+
|---|---|---|---|
|
|
103
|
+
| `InvalidParamsError` | `invalid_params` | 400 | no |
|
|
104
|
+
| `UnsupportedUrlError` | `unsupported_url` | 400 | no |
|
|
105
|
+
| `UnauthorizedError` | `unauthorized` | 401 | no |
|
|
106
|
+
| `InsufficientCreditsError` | `insufficient_credits` | 402 | no |
|
|
107
|
+
| `ForbiddenError` | `forbidden` | 403 | no |
|
|
108
|
+
| `PrivateError` | `private` | 403 | no |
|
|
109
|
+
| `AgeRestrictedError` | `age_restricted` | 403 | no |
|
|
110
|
+
| `NotFoundError` | `not_found` | 404 | no |
|
|
111
|
+
| `TranscriptUnavailableError` | `transcript_unavailable` | 404 | no |
|
|
112
|
+
| `ConflictError` | `conflict` | 409 | no |
|
|
113
|
+
| `RemovedError` | `removed` | 410 | no |
|
|
114
|
+
| `UnavailableInRegionError` | `unavailable_in_region` | 451 | no |
|
|
115
|
+
| `RateLimitedError` | `rate_limited` | 429 | yes |
|
|
116
|
+
| `UpstreamRateLimitedError` | `upstream_rate_limited` | 503 | yes |
|
|
117
|
+
| `UpstreamBlockedError` | `upstream_blocked` | 503 | yes |
|
|
118
|
+
| `OverloadedError` | `overloaded` | 503 | yes |
|
|
119
|
+
| `UpstreamErrorError` | `upstream_error` | 502 | yes |
|
|
120
|
+
| `UpstreamChangedError` | `upstream_changed` | 502 | no |
|
|
121
|
+
| `UpstreamTimeoutError` | `upstream_timeout` | 504 | yes |
|
|
122
|
+
| `InternalError` | `internal` | 500 | yes |
|
|
123
|
+
| `NotImplementedYetError` | `not_implemented` | 501 | no |
|
|
124
|
+
|
|
125
|
+
All extend `errors.APIError` (attributes `code`, `message`, `retryable`,
|
|
126
|
+
`http_status`, `request_id`, `retry_after`, `headers`, `body`, `credits_charged`).
|
|
127
|
+
A code this version does not know yet still arrives as a plain `APIError`. Beyond
|
|
128
|
+
the API: `SocialScrapeConnectionError`, `SocialScrapeTimeoutError` (and its
|
|
129
|
+
subclass `JobTimeoutError`), `UnexpectedResponseError` and `MissingApiKeyError`,
|
|
130
|
+
all extending `SocialScrapeError`.
|
|
131
|
+
|
|
132
|
+
## Retries, timeouts, idempotency
|
|
133
|
+
|
|
134
|
+
Retryable errors (the table above, network failures and client timeouts) are
|
|
135
|
+
retried up to `max_retries` times (default 2) with exponential backoff and jitter.
|
|
136
|
+
`Retry-After` is honoured; one longer than `max_retry_after` (60 s) is not waited
|
|
137
|
+
for and the error is raised. POST and DELETE calls carry an `Idempotency-Key`
|
|
138
|
+
(generated, and reused across retries) so a retried write never charges twice.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
client = SocialScrape(
|
|
142
|
+
api_key="ss_live_...", # default: SOCIALSCRAPE_API_KEY
|
|
143
|
+
base_url="https://api.socialscrape.dev", # default: SOCIALSCRAPE_BASE_URL, else this
|
|
144
|
+
timeout=60.0, # seconds, per attempt
|
|
145
|
+
max_retries=2,
|
|
146
|
+
retry_initial_delay=0.5,
|
|
147
|
+
retry_max_delay=8.0,
|
|
148
|
+
max_retry_after=60.0,
|
|
149
|
+
wait_for_jobs=True,
|
|
150
|
+
job_timeout=600.0,
|
|
151
|
+
http_client=my_httpx_client, # optional: an httpx.Client you own
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Per call:
|
|
155
|
+
client.tiktok.profile(handle="khaby.lame", request_options={"timeout": 10, "max_retries": 0})
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`request_options` takes `timeout`, `max_retries`, `idempotency_key`, `headers`,
|
|
159
|
+
`wait_for_job` and `job_timeout`.
|
|
160
|
+
|
|
161
|
+
## Pagination
|
|
162
|
+
|
|
163
|
+
List endpoints return `cursor` and `has_more`. Pass the cursor back, or let the
|
|
164
|
+
client walk it. **Every page is a request and costs what one call costs**, so cap
|
|
165
|
+
the walk with `max_items` or `max_pages`.
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
for video in client.paginate(client.tiktok.profile_videos, handle="khaby.lame", max_items=100):
|
|
169
|
+
print(video["id"], video["stats"]["views"])
|
|
170
|
+
|
|
171
|
+
# whole pages, with each page's credits and cursor
|
|
172
|
+
for page in client.paginate_pages(client.tiktok.profile_videos, handle="khaby.lame", max_pages=3):
|
|
173
|
+
print(len(page.data["items"]), page.credits_charged, page.cursor)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Async: `async for video in client.paginate(client.tiktok.profile_videos, handle=...)`.
|
|
177
|
+
Iteration stops at `has_more` false, at a repeated cursor and at your caps, and
|
|
178
|
+
never fetches a page it does not need to reach `max_items`.
|
|
179
|
+
|
|
180
|
+
## Long jobs (AI transcription, deep listings)
|
|
181
|
+
|
|
182
|
+
When an answer is not ready within a few seconds the API replies `202` with a job
|
|
183
|
+
id. The client follows it for you (polling is free; the finished job is charged
|
|
184
|
+
once) and returns the final response. Switch it off per call or globally:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
pending = client.youtube.video_transcript(url=url, request_options={"wait_for_job": False})
|
|
188
|
+
# pending.http.status_code == 202, pending.data["job_id"]
|
|
189
|
+
done = client.wait_for_job(pending.data["job_id"])
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
A job that outlives `job_timeout` raises `JobTimeoutError` with its `job_id`; it
|
|
193
|
+
keeps running and can be fetched later with `client.jobs.get(job_id=...)`.
|
|
194
|
+
|
|
195
|
+
## Batch and monitors
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
res = client.batch.run([
|
|
199
|
+
{"endpoint": "tiktok.profile", "params": {"handle": "khaby.lame"}},
|
|
200
|
+
{"endpoint": "tiktok.profile", "params": {"handle": "charlidamelio"}},
|
|
201
|
+
])
|
|
202
|
+
res.credits_charged # the sum of the items
|
|
203
|
+
res.data["results"][0]["response"] # each item is a full envelope
|
|
204
|
+
|
|
205
|
+
monitor = client.monitors.create(
|
|
206
|
+
endpoint="/v1/tiktok/profile/videos",
|
|
207
|
+
params={"handle": "khaby.lame"},
|
|
208
|
+
webhook_url="https://example.com/hooks/socialscrape",
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Account
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
client.account.credits().data["credits_remaining"] # free
|
|
216
|
+
for row in client.paginate(client.account.usage): # request history
|
|
217
|
+
...
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Newer endpoints
|
|
221
|
+
|
|
222
|
+
Methods are generated from the API's endpoint registry, and only endpoints that
|
|
223
|
+
are built appear (coming-soon ones are hidden). To call a path this client version
|
|
224
|
+
does not know yet:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
res = client.request("GET", "/v1/some/new/endpoint", query={"handle": "x"})
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`socialscrape.ENDPOINTS` lists every endpoint the installed version knows.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "socialscrape-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "SocialScrape API client: social media data (profiles, posts, comments, search, ads, transcripts) in one response shape."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
keywords = ["socialscrape", "social media", "api", "tiktok", "instagram", "youtube", "transcripts"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Typing :: Typed",
|
|
17
|
+
]
|
|
18
|
+
dependencies = ["httpx>=0.27,<1"]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://socialscrape.dev"
|
|
22
|
+
Documentation = "https://socialscrape.dev/docs"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
where = ["src"]
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.package-data]
|
|
28
|
+
socialscrape = ["py.typed"]
|
|
29
|
+
|
|
30
|
+
# Tests run against the real SocialScrape app (memory stores), so the server
|
|
31
|
+
# packages from this repo are on the path too:
|
|
32
|
+
# .venv/bin/python -m pytest sdks/python/tests
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
addopts = "--import-mode=importlib -q"
|
|
35
|
+
testpaths = ["tests"]
|
|
36
|
+
asyncio_mode = "auto"
|
|
37
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
38
|
+
pythonpath = ["src", "tests", "../../packages", "../../services"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""SocialScrape: public social media data as an API.
|
|
2
|
+
|
|
3
|
+
from socialscrape import SocialScrape
|
|
4
|
+
|
|
5
|
+
client = SocialScrape() # reads SOCIALSCRAPE_API_KEY
|
|
6
|
+
profile = client.tiktok.profile(handle="khaby.lame")
|
|
7
|
+
print(profile.data["followers"], profile.credits_charged, profile.credits_remaining)
|
|
8
|
+
|
|
9
|
+
See https://socialscrape.dev/docs.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from . import errors, models
|
|
13
|
+
from ._batch import BatchRequest
|
|
14
|
+
from ._generated.endpoints import ENDPOINTS
|
|
15
|
+
from ._response import HttpInfo, RequestOptions, Response
|
|
16
|
+
from ._version import __version__
|
|
17
|
+
from .client import AsyncSocialScrape, SocialScrape
|
|
18
|
+
from .errors import (
|
|
19
|
+
APIError,
|
|
20
|
+
JobTimeoutError,
|
|
21
|
+
MissingApiKeyError,
|
|
22
|
+
SocialScrapeConnectionError,
|
|
23
|
+
SocialScrapeError,
|
|
24
|
+
SocialScrapeTimeoutError,
|
|
25
|
+
UnexpectedResponseError,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"SocialScrape", "AsyncSocialScrape", "Response", "HttpInfo", "RequestOptions", "BatchRequest", "ENDPOINTS",
|
|
30
|
+
"APIError", "SocialScrapeError", "MissingApiKeyError", "UnexpectedResponseError", "SocialScrapeConnectionError",
|
|
31
|
+
"SocialScrapeTimeoutError", "JobTimeoutError", "errors", "models", "__version__",
|
|
32
|
+
]
|