getsocialapi-instagram 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.
- getsocialapi_instagram/__init__.py +25 -0
- getsocialapi_instagram/client.py +31 -0
- getsocialapi_instagram/exceptions.py +80 -0
- getsocialapi_instagram/http.py +82 -0
- getsocialapi_instagram/instagram.py +257 -0
- getsocialapi_instagram-0.1.0.dist-info/METADATA +184 -0
- getsocialapi_instagram-0.1.0.dist-info/RECORD +10 -0
- getsocialapi_instagram-0.1.0.dist-info/WHEEL +5 -0
- getsocialapi_instagram-0.1.0.dist-info/licenses/LICENSE +21 -0
- getsocialapi_instagram-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .client import Client
|
|
2
|
+
from .exceptions import (
|
|
3
|
+
APIKeyError,
|
|
4
|
+
BadRequestError,
|
|
5
|
+
GetSocialAPIError,
|
|
6
|
+
InstagramError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
RateLimitError,
|
|
9
|
+
ServiceUnavailableError,
|
|
10
|
+
)
|
|
11
|
+
from .instagram import InstagramAPI
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Client",
|
|
17
|
+
"InstagramAPI",
|
|
18
|
+
"GetSocialAPIError",
|
|
19
|
+
"BadRequestError",
|
|
20
|
+
"APIKeyError",
|
|
21
|
+
"NotFoundError",
|
|
22
|
+
"RateLimitError",
|
|
23
|
+
"ServiceUnavailableError",
|
|
24
|
+
"InstagramError",
|
|
25
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from .http import DEFAULT_BASE_URL, Transport
|
|
4
|
+
from .instagram import InstagramAPI
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Client:
|
|
8
|
+
"""GetSocialAPI client.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from getsocialapi_instagram import Client
|
|
12
|
+
client = Client(api_key="gsa_live_...")
|
|
13
|
+
client.instagram.user_by_username("durov")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, api_key=None, base_url=DEFAULT_BASE_URL, timeout=30,
|
|
17
|
+
max_retries=3, session=None):
|
|
18
|
+
api_key = api_key or os.environ.get("GETSOCIALAPI_API_KEY")
|
|
19
|
+
if not api_key:
|
|
20
|
+
raise ValueError(
|
|
21
|
+
"api_key is required (pass api_key=... or set GETSOCIALAPI_API_KEY)"
|
|
22
|
+
)
|
|
23
|
+
self._transport = Transport(
|
|
24
|
+
base_url=base_url, api_key=api_key, timeout=timeout,
|
|
25
|
+
max_retries=max_retries, session=session,
|
|
26
|
+
)
|
|
27
|
+
self.instagram = InstagramAPI(self._transport)
|
|
28
|
+
|
|
29
|
+
def health(self):
|
|
30
|
+
"""Upstream liveness probe: {"ok": true, "state": "ready"}."""
|
|
31
|
+
return self._transport.request("GET", "/health", auth=False)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
class GetSocialAPIError(Exception):
|
|
2
|
+
"""Base error for all GetSocialAPI SDK failures."""
|
|
3
|
+
|
|
4
|
+
def __init__(self, message, response=None, body=None):
|
|
5
|
+
super().__init__(message)
|
|
6
|
+
self.response = response
|
|
7
|
+
self.body = body if body is not None else {}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _reason(body, fallback):
|
|
11
|
+
"""Extract a human-readable reason: {"error": "..."} | {"error": {"message"}} | {"message": ..."}."""
|
|
12
|
+
err = body.get("error")
|
|
13
|
+
if isinstance(err, dict):
|
|
14
|
+
err = err.get("message")
|
|
15
|
+
if isinstance(err, str) and err:
|
|
16
|
+
return err
|
|
17
|
+
msg = body.get("message")
|
|
18
|
+
if isinstance(msg, str) and msg:
|
|
19
|
+
return msg
|
|
20
|
+
return fallback
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BadRequestError(GetSocialAPIError):
|
|
24
|
+
"""400 — invalid parameters (body: {"error", "field"})."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, response=None, body=None):
|
|
27
|
+
body = body or {}
|
|
28
|
+
field = body.get("field")
|
|
29
|
+
msg = _reason(body, "Bad request")
|
|
30
|
+
if field:
|
|
31
|
+
msg = f"{msg} (field: {field})"
|
|
32
|
+
super().__init__(msg, response, body)
|
|
33
|
+
self.field = field
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class UnauthorizedError(GetSocialAPIError):
|
|
37
|
+
"""401 — invalid or missing API key."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, response=None, body=None):
|
|
40
|
+
super().__init__("Invalid API key", response, body)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class NotFoundError(GetSocialAPIError):
|
|
44
|
+
"""404 — target object does not exist. Do not retry."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, response=None, body=None):
|
|
47
|
+
body = body or {}
|
|
48
|
+
msg = _reason(body, "Not found")
|
|
49
|
+
super().__init__(msg, response, body)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RateLimitError(GetSocialAPIError):
|
|
53
|
+
"""429 — key rate limit exceeded. Honors Retry-After."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, response=None, body=None, retry_after=1):
|
|
56
|
+
super().__init__(f"Rate limit exceeded, retry after {retry_after}s", response, body)
|
|
57
|
+
self.retry_after = retry_after
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ServiceUnavailableError(GetSocialAPIError):
|
|
61
|
+
"""503 — no live accounts or the service is stopped."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, response=None, body=None, retry_after=None):
|
|
64
|
+
msg = "Service unavailable"
|
|
65
|
+
if retry_after:
|
|
66
|
+
msg += f", retry after {retry_after}s"
|
|
67
|
+
super().__init__(msg, response, body)
|
|
68
|
+
self.retry_after = retry_after
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class InstagramError(GetSocialAPIError):
|
|
72
|
+
"""Non-404 error returned by the Instagram private API (raw mode)."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, status, message, response=None, body=None):
|
|
75
|
+
super().__init__(f"Instagram API error {status}: {message}", response, body)
|
|
76
|
+
self.status = status
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Convenience alias
|
|
80
|
+
APIKeyError = UnauthorizedError
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from .exceptions import (
|
|
6
|
+
BadRequestError,
|
|
7
|
+
GetSocialAPIError,
|
|
8
|
+
InstagramError,
|
|
9
|
+
NotFoundError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
ServiceUnavailableError,
|
|
12
|
+
UnauthorizedError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
DEFAULT_BASE_URL = "https://api.getsocialapi.com/v1"
|
|
16
|
+
|
|
17
|
+
RETRYABLE_STATUS = {429, 500, 502, 503, 504}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Transport:
|
|
21
|
+
"""HTTP transport: auth, JSON parsing, retries with Retry-After."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, base_url=DEFAULT_BASE_URL, api_key=None, timeout=30,
|
|
24
|
+
max_retries=3, session=None):
|
|
25
|
+
self.base_url = base_url.rstrip("/")
|
|
26
|
+
self.api_key = api_key
|
|
27
|
+
self.timeout = timeout
|
|
28
|
+
self.max_retries = max_retries
|
|
29
|
+
self.session = session or requests.Session()
|
|
30
|
+
|
|
31
|
+
def request(self, method, path, params=None, auth=True):
|
|
32
|
+
url = f"{self.base_url}{path}"
|
|
33
|
+
headers = {"X-API-Key": self.api_key} if auth else {}
|
|
34
|
+
attempts = self.max_retries + 1
|
|
35
|
+
last_exc = None
|
|
36
|
+
for attempt in range(attempts):
|
|
37
|
+
try:
|
|
38
|
+
resp = self.session.request(
|
|
39
|
+
method, url, params=params, headers=headers, timeout=self.timeout,
|
|
40
|
+
)
|
|
41
|
+
except (requests.ConnectionError, requests.Timeout) as e:
|
|
42
|
+
last_exc = e
|
|
43
|
+
if attempt < attempts - 1:
|
|
44
|
+
time.sleep(min(2 ** attempt, 10))
|
|
45
|
+
continue
|
|
46
|
+
raise GetSocialAPIError(f"Network error: {e}") from e
|
|
47
|
+
|
|
48
|
+
if resp.status_code in RETRYABLE_STATUS and attempt < attempts - 1:
|
|
49
|
+
time.sleep(self._retry_after(resp))
|
|
50
|
+
continue
|
|
51
|
+
return self._parse(resp)
|
|
52
|
+
raise GetSocialAPIError("Request failed after retries") # pragma: no cover
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def _retry_after(resp):
|
|
56
|
+
try:
|
|
57
|
+
return min(max(int(float(resp.headers.get("Retry-After", 1))), 0), 30)
|
|
58
|
+
except (TypeError, ValueError):
|
|
59
|
+
return 1
|
|
60
|
+
|
|
61
|
+
def _parse(self, resp):
|
|
62
|
+
try:
|
|
63
|
+
body = resp.json()
|
|
64
|
+
except ValueError:
|
|
65
|
+
body = {"raw": resp.text}
|
|
66
|
+
|
|
67
|
+
if resp.status_code == 401:
|
|
68
|
+
raise UnauthorizedError(resp, body)
|
|
69
|
+
if resp.status_code == 400:
|
|
70
|
+
raise BadRequestError(resp, body)
|
|
71
|
+
if resp.status_code == 404:
|
|
72
|
+
raise NotFoundError(resp, body)
|
|
73
|
+
if resp.status_code == 429:
|
|
74
|
+
raise RateLimitError(resp, body, retry_after=self._retry_after(resp))
|
|
75
|
+
if resp.status_code == 503:
|
|
76
|
+
raise ServiceUnavailableError(resp, body, retry_after=self._retry_after(resp))
|
|
77
|
+
if resp.status_code >= 400:
|
|
78
|
+
# Instagram errors in raw mode: {"message": "...", "status": "fail"}
|
|
79
|
+
if isinstance(body, dict) and body.get("status") == "fail":
|
|
80
|
+
raise InstagramError(resp.status_code, body.get("message", "unknown"), resp, body)
|
|
81
|
+
raise GetSocialAPIError(f"HTTP {resp.status_code}: {body}", resp, body)
|
|
82
|
+
return body
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from urllib.parse import quote
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _quote(value):
|
|
6
|
+
return quote(str(value), safe="")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _media_arg(value, name="media id"):
|
|
10
|
+
"""Numeric media pk or shortcode (e.g. DehVsgBi3sH)."""
|
|
11
|
+
s = str(value)
|
|
12
|
+
if s.isdigit():
|
|
13
|
+
return _digits(s, name)
|
|
14
|
+
if re.fullmatch(r"[A-Za-z0-9_-]{5,64}", s):
|
|
15
|
+
return s
|
|
16
|
+
raise ValueError(f"{name} must be a numeric pk or a shortcode, "
|
|
17
|
+
f"got: {value!r}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _digits(value, name, max_len=20):
|
|
21
|
+
s = str(value)
|
|
22
|
+
if not s.isdigit() or not 1 <= len(s) <= max_len:
|
|
23
|
+
raise ValueError(f"{name} must be 1-{max_len} digits, got: {value!r}")
|
|
24
|
+
return s
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _length(value, name, lo, hi):
|
|
28
|
+
s = str(value)
|
|
29
|
+
if not lo <= len(s) <= hi:
|
|
30
|
+
raise ValueError(f"{name} must be {lo}-{hi} characters, got: {value!r}")
|
|
31
|
+
return s
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _tag(value):
|
|
35
|
+
return _quote(_length(str(value).lstrip("#"), "tag", 1, 64))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _count(value, lo=1, hi=50, default=None):
|
|
39
|
+
if value is None:
|
|
40
|
+
return default
|
|
41
|
+
if isinstance(value, bool) or not isinstance(value, int) or not lo <= value <= hi:
|
|
42
|
+
raise ValueError(f"count must be an integer {lo}-{hi}, got: {value!r}")
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class InstagramAPI:
|
|
47
|
+
"""Instagram endpoints of GetSocialAPI.
|
|
48
|
+
|
|
49
|
+
Methods map 1:1 to API routes and return the raw JSON responses as received.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, transport):
|
|
53
|
+
self._t = transport
|
|
54
|
+
|
|
55
|
+
# ---------- Profile ----------
|
|
56
|
+
|
|
57
|
+
def user_info(self, user_id):
|
|
58
|
+
"""GET /ig/user/info/{user_id} — profile by numeric id."""
|
|
59
|
+
return self._t.request("GET", f"/ig/user/info/{_digits(user_id, 'user_id')}")
|
|
60
|
+
|
|
61
|
+
def user_by_username(self, username):
|
|
62
|
+
"""GET /ig/user/username_info/{username} — profile by username."""
|
|
63
|
+
return self._t.request("GET", f"/ig/user/username_info/{_length(username, 'username', 1, 64)}")
|
|
64
|
+
|
|
65
|
+
def profile(self, user_id):
|
|
66
|
+
"""GET /profile/{user_id} — profile by numeric id."""
|
|
67
|
+
return self._t.request("GET", f"/profile/{_digits(user_id, 'user_id')}")
|
|
68
|
+
|
|
69
|
+
def user_about(self, user_id):
|
|
70
|
+
"""GET /user/about/{user_id} — About this account.
|
|
71
|
+
|
|
72
|
+
The "About this account" screen: country, registration month,
|
|
73
|
+
former usernames count, accounts-with-shared-followers count,
|
|
74
|
+
verification. Sparse accounts return null fields — check
|
|
75
|
+
`has_about_data` / `unavailable` before using them.
|
|
76
|
+
"""
|
|
77
|
+
return self._t.request("GET", f"/user/about/{_digits(user_id, 'user_id')}")
|
|
78
|
+
|
|
79
|
+
def search_username(self, query, count=10):
|
|
80
|
+
"""GET /search/username/{query} — search users.
|
|
81
|
+
|
|
82
|
+
`count` is forwarded to the service, but the response may include
|
|
83
|
+
one extra exact-match entry beyond it.
|
|
84
|
+
"""
|
|
85
|
+
return self._t.request("GET", f"/search/username/{_quote(query)}",
|
|
86
|
+
params={"count": _count(count, default=10)})
|
|
87
|
+
|
|
88
|
+
def similar_users(self, user_id):
|
|
89
|
+
"""GET /ig/user/similar/{user_id} — suggested accounts."""
|
|
90
|
+
return self._t.request("GET", f"/ig/user/similar/{_digits(user_id, 'user_id')}")
|
|
91
|
+
|
|
92
|
+
# ---------- User content ----------
|
|
93
|
+
|
|
94
|
+
def user_medias(self, user_id, count=12, max_id=None):
|
|
95
|
+
"""GET /ig/user/media/{user_id} — profile grid posts."""
|
|
96
|
+
return self._t.request("GET", f"/ig/user/media/{_digits(user_id, 'user_id')}",
|
|
97
|
+
params=_page(count, max_id, default=12))
|
|
98
|
+
|
|
99
|
+
def user_tags(self, user_id, count=12, max_id=None):
|
|
100
|
+
"""GET /ig/user/tags/{user_id} — posts where the user is tagged."""
|
|
101
|
+
return self._t.request("GET", f"/ig/user/tags/{_digits(user_id, 'user_id')}",
|
|
102
|
+
params=_page(count, max_id, default=12))
|
|
103
|
+
|
|
104
|
+
def user_stories(self, user_id):
|
|
105
|
+
"""GET /ig/user/stories/{user_id} — active stories."""
|
|
106
|
+
return self._t.request("GET", f"/ig/user/stories/{_digits(user_id, 'user_id')}")
|
|
107
|
+
|
|
108
|
+
def user_highlights(self, user_id):
|
|
109
|
+
"""GET /ig/user/highlights/{user_id} — highlights tray."""
|
|
110
|
+
return self._t.request("GET", f"/ig/user/highlights/{_digits(user_id, 'user_id')}")
|
|
111
|
+
|
|
112
|
+
def highlight_stories(self, highlight_id):
|
|
113
|
+
"""GET /ig/highlight/stories/{highlight_id} — media of a highlight.
|
|
114
|
+
|
|
115
|
+
Accepts the tray id as is — the leading `highlight:` prefix
|
|
116
|
+
(as returned by `user_highlights`) is trimmed automatically.
|
|
117
|
+
"""
|
|
118
|
+
hid = str(highlight_id)
|
|
119
|
+
if hid.startswith("highlight:"):
|
|
120
|
+
hid = hid[len("highlight:"):]
|
|
121
|
+
return self._t.request("GET", f"/ig/highlight/stories/{_quote(_length(hid, 'highlight_id', 1, 64))}")
|
|
122
|
+
|
|
123
|
+
# ---------- Followers / following ----------
|
|
124
|
+
|
|
125
|
+
def followers(self, user_id, count=50, max_id=None, query=None):
|
|
126
|
+
"""GET /followers/{user_id} — followers list.
|
|
127
|
+
|
|
128
|
+
On unverified accounts `count`, the `next_max_id` cursor and the
|
|
129
|
+
`query` filter work — nearly the full list is accessible and
|
|
130
|
+
searchable. On verified accounts (blue checkmark) Instagram exposes
|
|
131
|
+
no more than ~50 followers: one page, no cursor, `query` ignored.
|
|
132
|
+
"""
|
|
133
|
+
params = _page(count, max_id, default=50)
|
|
134
|
+
if query is not None:
|
|
135
|
+
params["query"] = query
|
|
136
|
+
return self._t.request("GET", f"/followers/{_digits(user_id, 'user_id')}",
|
|
137
|
+
params=params)
|
|
138
|
+
|
|
139
|
+
def following(self, user_id, count=25, max_id=None, query=None):
|
|
140
|
+
"""GET /following/{user_id} — following list.
|
|
141
|
+
|
|
142
|
+
Page size: the service returns the default 25 accounts per page and
|
|
143
|
+
at most 25 (values above 50 are rejected). The response carries
|
|
144
|
+
`next_max_id` — pass it as `max_id` for the next page. `query`
|
|
145
|
+
filters the list by name.
|
|
146
|
+
"""
|
|
147
|
+
params = _page(count, max_id, default=25, hi=50)
|
|
148
|
+
if query is not None:
|
|
149
|
+
params["query"] = query
|
|
150
|
+
return self._t.request("GET", f"/following/{_digits(user_id, 'user_id')}",
|
|
151
|
+
params=params)
|
|
152
|
+
|
|
153
|
+
# ---------- Media ----------
|
|
154
|
+
|
|
155
|
+
def media_info(self, media):
|
|
156
|
+
"""GET /ig/media/info/{pk|shortcode} — full media card.
|
|
157
|
+
|
|
158
|
+
Accepts a numeric pk or a post shortcode (e.g. "Deu3BoCBbDP"):
|
|
159
|
+
items[0] carries pk, code, media_type (1=photo, 2=video,
|
|
160
|
+
8=carousel), caption, like/comment/view counts, taken_at, owner.
|
|
161
|
+
"""
|
|
162
|
+
return self._t.request("GET", f"/ig/media/info/{_media_arg(media)}")
|
|
163
|
+
|
|
164
|
+
def media_likers(self, media):
|
|
165
|
+
"""GET /ig/media/likers/{pk|shortcode} — users who liked the media.
|
|
166
|
+
|
|
167
|
+
One shot of up to ~100 users; the route has no pagination.
|
|
168
|
+
"""
|
|
169
|
+
return self._t.request("GET", f"/ig/media/likers/{_media_arg(media)}")
|
|
170
|
+
|
|
171
|
+
def media_comments(self, media, count=None, sort_order="recent", min_id=None):
|
|
172
|
+
"""GET /ig/media/comments/{pk|shortcode} — media comments.
|
|
173
|
+
|
|
174
|
+
Accepts a numeric pk or a post shortcode (e.g. "Deu3BoCBbDP").
|
|
175
|
+
|
|
176
|
+
Full pagination works only with `sort_order="recent"` (default):
|
|
177
|
+
pages are strictly chronological (newer → older) and disjoint.
|
|
178
|
+
The cursor is `next_min_id` from the previous response — pass it
|
|
179
|
+
back as `min_id` verbatim (it is a JSON string with special
|
|
180
|
+
characters; the transport URL-encodes it). Collect until
|
|
181
|
+
`next_min_id` is null/empty or `comments` is an empty list.
|
|
182
|
+
|
|
183
|
+
Instagram never returns `next_max_id` for this route — `max_id`
|
|
184
|
+
pagination must not be used. `count` (1-50) is accepted by the
|
|
185
|
+
validator but the real page size is ~12-20 comments. Without
|
|
186
|
+
`sort_order` (ranked) the service returns a ~15 comment preview
|
|
187
|
+
whose pages overlap between pool accounts.
|
|
188
|
+
"""
|
|
189
|
+
params = {}
|
|
190
|
+
if count is not None:
|
|
191
|
+
params["count"] = _count(count)
|
|
192
|
+
if sort_order is not None:
|
|
193
|
+
if sort_order not in ("recent", "ranked"):
|
|
194
|
+
raise ValueError('sort_order must be "recent" or "ranked", '
|
|
195
|
+
f"got: {sort_order!r}")
|
|
196
|
+
params["sort_order"] = sort_order
|
|
197
|
+
if min_id is not None:
|
|
198
|
+
params["min_id"] = min_id
|
|
199
|
+
return self._t.request("GET", f"/ig/media/comments/{_media_arg(media)}",
|
|
200
|
+
params=params)
|
|
201
|
+
|
|
202
|
+
def search_clips(self, keyword):
|
|
203
|
+
"""GET /ig/media/search_clips/{keyword} — search reels by keyword.
|
|
204
|
+
|
|
205
|
+
The service returns its native page (~12 clips); a page-size
|
|
206
|
+
parameter is not supported by the API for this route.
|
|
207
|
+
"""
|
|
208
|
+
kw = _length(keyword, "keyword", 1, 100)
|
|
209
|
+
return self._t.request("GET", f"/ig/media/search_clips/{_quote(kw)}")
|
|
210
|
+
|
|
211
|
+
# ---------- Hashtags ----------
|
|
212
|
+
|
|
213
|
+
def hashtag_info(self, tag):
|
|
214
|
+
"""GET /ig/hashtag/info/{tag} — hashtag card. A leading # is trimmed."""
|
|
215
|
+
return self._t.request("GET", f"/ig/hashtag/info/{_tag(tag)}")
|
|
216
|
+
|
|
217
|
+
def hashtag_search(self, query):
|
|
218
|
+
"""GET /ig/hashtag/search/{query} — search hashtags.
|
|
219
|
+
|
|
220
|
+
Returns the full first page of tag matches; a page-size
|
|
221
|
+
parameter is not supported by the API for this route.
|
|
222
|
+
"""
|
|
223
|
+
return self._t.request("GET", f"/ig/hashtag/search/{_quote(query)}")
|
|
224
|
+
|
|
225
|
+
def hashtag_posts(self, tag, max_id=None):
|
|
226
|
+
"""GET /v2/hashtag/posts/{tag} — posts by hashtag. A leading # is trimmed.
|
|
227
|
+
|
|
228
|
+
The service returns its native page (~15-18 posts) plus `next_max_id`
|
|
229
|
+
for pagination via `max_id`; a page-size parameter is not supported.
|
|
230
|
+
"""
|
|
231
|
+
params = {"max_id": max_id} if max_id is not None else None
|
|
232
|
+
return self._t.request("GET", f"/v2/hashtag/posts/{_tag(tag)}",
|
|
233
|
+
params=params)
|
|
234
|
+
|
|
235
|
+
# ---------- Locations ----------
|
|
236
|
+
|
|
237
|
+
def location_info(self, pk):
|
|
238
|
+
"""GET /ig/location/info/{pk} — location card."""
|
|
239
|
+
return self._t.request("GET", f"/ig/location/info/{_digits(pk, 'pk')}")
|
|
240
|
+
|
|
241
|
+
def location_media(self, pk, max_id=None):
|
|
242
|
+
"""GET /ig/location/media/{pk} — media by location.
|
|
243
|
+
|
|
244
|
+
The service returns its native page; pass `next_max_id` from a
|
|
245
|
+
previous response as `max_id` to request the next page. The
|
|
246
|
+
response also carries `next_page` — a page counter, not a cursor.
|
|
247
|
+
"""
|
|
248
|
+
params = {"max_id": max_id} if max_id is not None else None
|
|
249
|
+
return self._t.request("GET", f"/ig/location/media/{_digits(pk, 'pk')}",
|
|
250
|
+
params=params)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _page(count, max_id, default=12, hi=50):
|
|
254
|
+
params = {"count": _count(count, default=default, hi=hi)}
|
|
255
|
+
if max_id is not None:
|
|
256
|
+
params["max_id"] = max_id
|
|
257
|
+
return params
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: getsocialapi-instagram
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: GetSocialAPI — client library for the Instagram API (no cookies, no accounts required)
|
|
5
|
+
Author: GetSocialAPI
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://getsocialapi.com
|
|
8
|
+
Project-URL: Documentation, https://getsocialapi.com/docs/
|
|
9
|
+
Keywords: instagram,api,scraping,sdk,getsocialapi
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: requests>=2.25
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# Instagram Scraper API
|
|
22
|
+
|
|
23
|
+
[](https://github.com/GetSocialAPI/Instagram-Scraper-API/actions/workflows/tests.yml)
|
|
24
|
+
[](LICENSE)
|
|
25
|
+
[](https://www.python.org)
|
|
26
|
+
|
|
27
|
+
Python client for the **[GetSocialAPI](https://getsocialapi.com/)** Instagram Scraper API.
|
|
28
|
+
|
|
29
|
+
- **No cookies required** — you never provide Instagram session cookies
|
|
30
|
+
- **No accounts needed** — zero ban risk for your own profiles
|
|
31
|
+
- Raw Instagram JSON responses, REST endpoints — no transformations
|
|
32
|
+
- Automatic retries with `Retry-After`, typed exceptions
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install getsocialapi-instagram
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Migration from `getsocialapi` (git installs)
|
|
41
|
+
|
|
42
|
+
Before the PyPI release this SDK was installed from git as `getsocialapi`.
|
|
43
|
+
Old imports keep working via a deprecated shim, but please migrate:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip uninstall getsocialapi
|
|
47
|
+
pip install getsocialapi-instagram
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# before
|
|
52
|
+
from getsocialapi import Client
|
|
53
|
+
# after
|
|
54
|
+
from getsocialapi_instagram import Client
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quickstart
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from getsocialapi_instagram import Client
|
|
61
|
+
|
|
62
|
+
# Get your API key at https://getsocialapi.com/dashboard/
|
|
63
|
+
client = Client(api_key="gsa_live_...") # or set GETSOCIALAPI_API_KEY
|
|
64
|
+
|
|
65
|
+
# Profile by numeric id (or user_by_username for usernames)
|
|
66
|
+
user = client.instagram.user_info(25025320)
|
|
67
|
+
print(user["user"]["follower_count"])
|
|
68
|
+
|
|
69
|
+
# Posts (grid)
|
|
70
|
+
medias = client.instagram.user_medias(25025320, count=12)
|
|
71
|
+
|
|
72
|
+
# Followers / following — paginated on unverified accounts: pass `next_max_id`
|
|
73
|
+
# back as `max_id`. Verified accounts (blue checkmark) expose ~50 followers max.
|
|
74
|
+
page = client.instagram.followers(43586162288, count=25)
|
|
75
|
+
page = client.instagram.followers(43586162288, max_id=page["next_max_id"])
|
|
76
|
+
|
|
77
|
+
# All comments of a post — cursor is next_min_id -> min_id (sort_order="recent" by default)
|
|
78
|
+
comments, cursor = [], None
|
|
79
|
+
while True:
|
|
80
|
+
page = client.instagram.media_comments(4012386322294878415, count=50, min_id=cursor)
|
|
81
|
+
comments += page["comments"]
|
|
82
|
+
cursor = page.get("next_min_id")
|
|
83
|
+
if not cursor:
|
|
84
|
+
break
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Base URL: `https://api.getsocialapi.com/v1` — authenticate with the API key
|
|
88
|
+
from your [dashboard](https://getsocialapi.com/dashboard/).
|
|
89
|
+
|
|
90
|
+
## Client
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
Client(api_key=None, base_url="https://api.getsocialapi.com/v1", timeout=30, max_retries=3)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
| Parameter | Default | Description |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| `api_key` | env `GETSOCIALAPI_API_KEY` | your API key |
|
|
99
|
+
| `base_url` | API endpoint | override only for testing |
|
|
100
|
+
| `timeout` | `30` | per-request timeout, seconds |
|
|
101
|
+
| `max_retries` | `3` | retries on 429/503/5xx and network errors |
|
|
102
|
+
|
|
103
|
+
## Methods
|
|
104
|
+
|
|
105
|
+
| Method | Route | Description |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| `profile(user_id)` | `/profile/<id>` | profile by numeric id |
|
|
108
|
+
| `user_info(user_id)` | `/ig/user/info/<id>` | profile by numeric id |
|
|
109
|
+
| `user_by_username(username)` | `/ig/user/username_info/<username>` | profile by username |
|
|
110
|
+
| `user_about(user_id)` | `/user/about/<id>` | About this account: country, joined, former usernames, shared-followers count, verified |
|
|
111
|
+
| `search_username(query, count=10)` | `/search/username/<q>` | search users, resolve username → pk |
|
|
112
|
+
| `similar_users(user_id)` | `/ig/user/similar/<id>` | suggested accounts |
|
|
113
|
+
| `user_medias(user_id, count=12, max_id=None)` | `/ig/user/media/<id>` | profile grid posts |
|
|
114
|
+
| `user_tags(user_id, count=12, max_id=None)` | `/ig/user/tags/<id>` | posts with the user tagged |
|
|
115
|
+
| `user_stories(user_id)` | `/ig/user/stories/<id>` | active stories |
|
|
116
|
+
| `user_highlights(user_id)` | `/ig/user/highlights/<id>` | highlights tray |
|
|
117
|
+
| `highlight_stories(highlight_id)` | `/ig/highlight/stories/<id>` | media of a highlight |
|
|
118
|
+
| `followers(user_id, count=50, max_id=None, query=None)` | `/followers/<id>` | followers list |
|
|
119
|
+
| `following(user_id, count=25, max_id=None, query=None)` | `/following/<id>` | following list |
|
|
120
|
+
| `media_info(media)` | `/ig/media/info/<pk\|shortcode>` | full media card; accepts numeric pk or shortcode |
|
|
121
|
+
| `media_likers(media)` | `/ig/media/likers/<pk\|shortcode>` | users who liked (up to ~100, no pagination) |
|
|
122
|
+
| `media_comments(media, count=None, sort_order="recent", min_id=None)` | `/ig/media/comments/<pk\|shortcode>` | media comments; cursor pagination `next_min_id` → `min_id` |
|
|
123
|
+
| `search_clips(keyword)` | `/ig/media/search_clips/<kw>` | search reels by keyword |
|
|
124
|
+
| `hashtag_info(tag)` | `/ig/hashtag/info/<tag>` | hashtag card |
|
|
125
|
+
| `hashtag_search(query)` | `/ig/hashtag/search/<q>` | search hashtags |
|
|
126
|
+
| `hashtag_posts(tag, max_id=None)` | `/v2/hashtag/posts/<tag>` | posts by hashtag |
|
|
127
|
+
| `location_info(pk)` | `/ig/location/info/<pk>` | location card |
|
|
128
|
+
| `location_media(pk, max_id=None)` | `/ig/location/media/<pk>` | media by location |
|
|
129
|
+
|
|
130
|
+
Every method returns the raw JSON of the API route as received — nothing is
|
|
131
|
+
unwrapped, filtered or renamed. Where the API supports pagination (`followers`*, `following`, `user_medias`,
|
|
132
|
+
`user_tags`, `hashtag_posts`, `location_media`), pass the cursor from the
|
|
133
|
+
previous response (`next_max_id`) as `max_id`.
|
|
134
|
+
`media_comments` paginates with a different cursor: always request with
|
|
135
|
+
`sort_order="recent"` (the default) and pass `next_min_id` back as `min_id`
|
|
136
|
+
until it comes back null — pages are chronological and disjoint. Instagram
|
|
137
|
+
never returns `next_max_id` for comments, so `max_id` must not be used there.
|
|
138
|
+
*`followers` works on unverified accounts (nearly the full list, plus the
|
|
139
|
+
`query` filter); verified accounts (blue checkmark) expose no more than ~50
|
|
140
|
+
followers — one page, no cursor, `query` ignored. For lists without cursors
|
|
141
|
+
(`search_clips`, `hashtag_search`) each call simply returns the service page.
|
|
142
|
+
|
|
143
|
+
Full request/response reference with live examples:
|
|
144
|
+
[docs.getsocialapi → API Reference](https://getsocialapi.com/docs/api/instagram/user/get_info_by_id/)
|
|
145
|
+
(see the Documentation section on [getsocialapi.com](https://getsocialapi.com/)).
|
|
146
|
+
|
|
147
|
+
## Errors
|
|
148
|
+
|
|
149
|
+
| Exception | HTTP | Notes |
|
|
150
|
+
| --- | --- | --- |
|
|
151
|
+
| `GetSocialAPIError` | — | base class; also network errors |
|
|
152
|
+
| `APIKeyError` | 401 | invalid or missing key |
|
|
153
|
+
| `BadRequestError` | 400 | `.field` points to the bad parameter |
|
|
154
|
+
| `NotFoundError` | 404 | object does not exist — never retried |
|
|
155
|
+
| `RateLimitError` | 429 | `.retry_after` seconds |
|
|
156
|
+
| `ServiceUnavailableError` | 503 | no live accounts, `.retry_after` |
|
|
157
|
+
| `InstagramError` | any | upstream Instagram error, `.status` |
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from getsocialapi_instagram import NotFoundError, RateLimitError
|
|
161
|
+
import time
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
client.instagram.user_info(25025320)
|
|
165
|
+
except NotFoundError:
|
|
166
|
+
... # object does not exist
|
|
167
|
+
except RateLimitError as e:
|
|
168
|
+
time.sleep(e.retry_after)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Only successful responses (HTTP `200` and `404`) count against your quota.
|
|
172
|
+
Rate limits and plans: [pricing](https://getsocialapi.com/#pricing).
|
|
173
|
+
|
|
174
|
+
## Support
|
|
175
|
+
|
|
176
|
+
- Telegram: [@getsocialapi](https://t.me/getsocialapi)
|
|
177
|
+
- Email: [hello@getsocialapi.com](mailto:hello@getsocialapi.com)
|
|
178
|
+
- Website: [getsocialapi.com](https://getsocialapi.com/)
|
|
179
|
+
- Documentation: [getsocialapi.com/docs](https://getsocialapi.com/docs/)
|
|
180
|
+
- Status: [getsocialapi.com/status](https://getsocialapi.com/status/)
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
getsocialapi_instagram/__init__.py,sha256=rVO7FwwDBbG1JwrSCmmSzJApGqRv3unhiW6KOTtvrH4,475
|
|
2
|
+
getsocialapi_instagram/client.py,sha256=UWySIdI0KaADaa2B_kSXCRSqo_tgN61KIC8ruTIzctU,1035
|
|
3
|
+
getsocialapi_instagram/exceptions.py,sha256=-pC5E7stZ0fdlVHmE4ro7TCyPObvg8usA-AoJvoUmNQ,2567
|
|
4
|
+
getsocialapi_instagram/http.py,sha256=tUnyMhVIdMxG56pD9QZutrpD1-0DkCXlFOTTwgtck-M,2961
|
|
5
|
+
getsocialapi_instagram/instagram.py,sha256=obR1WE2s1Aa_9C3G5cUoN5TJmXZoWp6epIf5581QK2M,10601
|
|
6
|
+
getsocialapi_instagram-0.1.0.dist-info/licenses/LICENSE,sha256=jgv_wwS6fmja7z85ssgPuXIDkB6DTDr-v03_ClRjkyY,1069
|
|
7
|
+
getsocialapi_instagram-0.1.0.dist-info/METADATA,sha256=kAqfy98Ov8ixniUqIMiOdXzfYaOZy_by4qTGR-GYDNE,8011
|
|
8
|
+
getsocialapi_instagram-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
9
|
+
getsocialapi_instagram-0.1.0.dist-info/top_level.txt,sha256=UWWVCJiWXLbG-OqIGGPFPcZ5zqFU193d9VMb_CFGu50,23
|
|
10
|
+
getsocialapi_instagram-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GetSocialAPI
|
|
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 @@
|
|
|
1
|
+
getsocialapi_instagram
|