igmapper 1.0.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.
File without changes
igmapper-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Author
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,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: igmapper
3
+ Version: 1.0.0
4
+ Summary: Unofficial Instagram API to request available data
5
+ Author: lucasoal
6
+ License: MIT
7
+ Project-URL: Homepage, https://pypi.org/project/igmapper
8
+ Project-URL: Documentation, https://github.com/lucasoal/igmapper/blob/main/doc/DOCUMENTATION.md
9
+ Project-URL: Repository, https://github.com/lucasoal/igmapper
10
+ Keywords: igmapper,instagram,unofficial-api
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ License-File: AUTHORS.md
20
+ Requires-Dist: requests
21
+ Requires-Dist: pydantic
22
+ Dynamic: license-file
23
+
24
+ <div align="center">
25
+ <picture>
26
+ <source media="(prefers-color-scheme: dark)" srcset="assets/igmapper.svg">
27
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/lucasoal/igmapper/refs/heads/main/assets/igmapper.svg">
28
+ <img src="https://raw.githubusercontent.com/lucasoal/igmapper/refs/heads/main/assets/igmapper.svg" width="100%">
29
+ </picture>
30
+ <br><br><br>
31
+ <hr>
32
+ <h1>igmapper: An Instagram Unofficial API</h1>
33
+
34
+ <img src="https://img.shields.io/badge/Author-lucasoal-blue?logo=github&logoColor=white"> <img src="https://img.shields.io/badge/License-MIT-750014.svg"> <!-- <img src="https://img.shields.io/badge/Status-Beta-DF1F72"> -->
35
+ <br>
36
+ <img src="https://img.shields.io/pypi/v/igmapper.svg?label=Version&color=white"> <img src="https://img.shields.io/pypi/pyversions/igmapper?logo=python&logoColor=white&label=Python"> <img src="https://img.shields.io/badge/Code Style-Black Formatter-111.svg">
37
+ <br>
38
+ <img src="https://static.pepy.tech/badge/igmapper">
39
+ <!-- <img src="https://img.shields.io/pypi/dm/igmapper.svg?label=PyPI Downloads"> -->
40
+
41
+ </div>
42
+
43
+ ## What is it?
44
+ **igmapper** is a high-performance Python library designed for Instagram data extraction,
45
+ providing structured access to profiles, feeds, and comments. It offers a flexible
46
+ architecture that allows data engineers to toggle between Requests and native CURL
47
+ transport, ensuring resilience against environment constraints.
48
+
49
+ <h2>Table of Contents</h2><br>
50
+
51
+ - [What is it?](#what-is-it)
52
+ - [Main Features](#main-features)
53
+ - [Where to get it / Install](#where-to-get-it--install)
54
+ - [Documentation](#documentation)
55
+ - [License](#license)
56
+ - [Dependencies](#dependencies)
57
+
58
+ ## Main Features
59
+ Here are just a few of the things that pandas does well:
60
+
61
+ - [`InstaClient`](doc/DOCUMENTATION.md#instaclient): Initializes the session and handles transport selection (Requests or CURL)
62
+ - [`get_profile_info()`](doc/DOCUMENTATION.md#get_profile_info): Scrapes profile metadata and returns a structured ProfileData object.
63
+ - [`get_feed()`](doc/DOCUMENTATION.md#get_feed): Retrieves user timeline posts with built-in pagination support.
64
+ - [`get_comments()`](doc/DOCUMENTATION.md#get_comments): Fetches media comments and automates cursor-based pagination.
65
+
66
+ ## Where to get it / Install
67
+ The source code is currently hosted on GitHub at: https://github.com/lucasoal/igmapper
68
+
69
+
70
+ > [!WARNING]
71
+ > It's essential to use [**Python 3.10** 🡽](https://www.python.org/downloads/release/python-310/) version
72
+ <!-- > It's essential to **upgrade pip** to the latest version to ensure compatibility with the library. -->
73
+ <!-- > ```sh
74
+ > # Requires the latest pip
75
+ > pip install --upgrade pip
76
+ > ``` -->
77
+
78
+ - [PyPI 🡽](https://pypi.org/project/igmapper/)
79
+ ```sh
80
+ # PyPI
81
+ pip install igmapper
82
+ ```
83
+ - GitHub
84
+ ```sh
85
+ # or GitHub
86
+ pip install git+https://github.com/lucasoal/igmapper.git
87
+ ```
88
+
89
+ ## Documentation
90
+ - [Documentation 🡽](https://github.com/lucasoal/igmapper/blob/main/doc/DOCUMENTATION.md).
91
+
92
+ ## License
93
+ - [MIT 🡽](https://github.com/lucasoal/igmapper/blob/main/LICENSE)
94
+
95
+ ## Dependencies
96
+ - [Requests](https://pypi.org/project/requests/) | [pydantic](https://pypi.org/project/pydantic/)
97
+
98
+ See the [full installation instructions](https://github.com/lucasoal/igmapper/blob/main/INSTALLATION.md) for minimum supported versions of required, recommended and optional dependencies.
99
+
100
+ <hr>
101
+
102
+ [⇧ Go to Top](#table-of-contents)
@@ -0,0 +1,79 @@
1
+ <div align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="assets/igmapper.svg">
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/lucasoal/igmapper/refs/heads/main/assets/igmapper.svg">
5
+ <img src="https://raw.githubusercontent.com/lucasoal/igmapper/refs/heads/main/assets/igmapper.svg" width="100%">
6
+ </picture>
7
+ <br><br><br>
8
+ <hr>
9
+ <h1>igmapper: An Instagram Unofficial API</h1>
10
+
11
+ <img src="https://img.shields.io/badge/Author-lucasoal-blue?logo=github&logoColor=white"> <img src="https://img.shields.io/badge/License-MIT-750014.svg"> <!-- <img src="https://img.shields.io/badge/Status-Beta-DF1F72"> -->
12
+ <br>
13
+ <img src="https://img.shields.io/pypi/v/igmapper.svg?label=Version&color=white"> <img src="https://img.shields.io/pypi/pyversions/igmapper?logo=python&logoColor=white&label=Python"> <img src="https://img.shields.io/badge/Code Style-Black Formatter-111.svg">
14
+ <br>
15
+ <img src="https://static.pepy.tech/badge/igmapper">
16
+ <!-- <img src="https://img.shields.io/pypi/dm/igmapper.svg?label=PyPI Downloads"> -->
17
+
18
+ </div>
19
+
20
+ ## What is it?
21
+ **igmapper** is a high-performance Python library designed for Instagram data extraction,
22
+ providing structured access to profiles, feeds, and comments. It offers a flexible
23
+ architecture that allows data engineers to toggle between Requests and native CURL
24
+ transport, ensuring resilience against environment constraints.
25
+
26
+ <h2>Table of Contents</h2><br>
27
+
28
+ - [What is it?](#what-is-it)
29
+ - [Main Features](#main-features)
30
+ - [Where to get it / Install](#where-to-get-it--install)
31
+ - [Documentation](#documentation)
32
+ - [License](#license)
33
+ - [Dependencies](#dependencies)
34
+
35
+ ## Main Features
36
+ Here are just a few of the things that pandas does well:
37
+
38
+ - [`InstaClient`](doc/DOCUMENTATION.md#instaclient): Initializes the session and handles transport selection (Requests or CURL)
39
+ - [`get_profile_info()`](doc/DOCUMENTATION.md#get_profile_info): Scrapes profile metadata and returns a structured ProfileData object.
40
+ - [`get_feed()`](doc/DOCUMENTATION.md#get_feed): Retrieves user timeline posts with built-in pagination support.
41
+ - [`get_comments()`](doc/DOCUMENTATION.md#get_comments): Fetches media comments and automates cursor-based pagination.
42
+
43
+ ## Where to get it / Install
44
+ The source code is currently hosted on GitHub at: https://github.com/lucasoal/igmapper
45
+
46
+
47
+ > [!WARNING]
48
+ > It's essential to use [**Python 3.10** 🡽](https://www.python.org/downloads/release/python-310/) version
49
+ <!-- > It's essential to **upgrade pip** to the latest version to ensure compatibility with the library. -->
50
+ <!-- > ```sh
51
+ > # Requires the latest pip
52
+ > pip install --upgrade pip
53
+ > ``` -->
54
+
55
+ - [PyPI 🡽](https://pypi.org/project/igmapper/)
56
+ ```sh
57
+ # PyPI
58
+ pip install igmapper
59
+ ```
60
+ - GitHub
61
+ ```sh
62
+ # or GitHub
63
+ pip install git+https://github.com/lucasoal/igmapper.git
64
+ ```
65
+
66
+ ## Documentation
67
+ - [Documentation 🡽](https://github.com/lucasoal/igmapper/blob/main/doc/DOCUMENTATION.md).
68
+
69
+ ## License
70
+ - [MIT 🡽](https://github.com/lucasoal/igmapper/blob/main/LICENSE)
71
+
72
+ ## Dependencies
73
+ - [Requests](https://pypi.org/project/requests/) | [pydantic](https://pypi.org/project/pydantic/)
74
+
75
+ See the [full installation instructions](https://github.com/lucasoal/igmapper/blob/main/INSTALLATION.md) for minimum supported versions of required, recommended and optional dependencies.
76
+
77
+ <hr>
78
+
79
+ [⇧ Go to Top](#table-of-contents)
@@ -0,0 +1,3 @@
1
+ from .client import InstaClient
2
+
3
+ __all__ = ["InstaClient"]
@@ -0,0 +1,126 @@
1
+ import json
2
+ import subprocess
3
+ import urllib.parse
4
+
5
+ from .models import FeedData, ProfileData, CommentsData
6
+ from .session import InstagramSession
7
+
8
+
9
+ class InstaClient:
10
+ def __init__(self, csrftoken, ds_user_id, sessionid, proxy=None, use_curl=False):
11
+ """
12
+ :param use_curl: Se True, utiliza chamadas via subprocess CURL em vez de requests.
13
+ """
14
+ self.state = InstagramSession(csrftoken, ds_user_id, sessionid, proxy=proxy)
15
+ self.use_curl = use_curl
16
+
17
+ def _execute_request(self, method, url, params=None):
18
+ """Gerencia se a requisição será via Requests ou CURL."""
19
+ if params:
20
+ url = f"{url}?{urllib.parse.urlencode(params)}"
21
+
22
+ if not self.use_curl:
23
+ return self.state.request_on_session(method, url)
24
+
25
+ return self._curl_request(method, url)
26
+
27
+ def _curl_request(self, method, url):
28
+ """Simula o comportamento do requests utilizando o binário curl do sistema."""
29
+ cookie_str = (
30
+ f"csrftoken={self.state.session.cookies.get('csrftoken')}; "
31
+ f"ds_user_id={self.state.session.cookies.get('ds_user_id')}; "
32
+ f"sessionid={self.state.session.cookies.get('sessionid')};"
33
+ )
34
+
35
+ command = [
36
+ "curl",
37
+ "-X",
38
+ method,
39
+ url,
40
+ "-H",
41
+ f"cookie: {cookie_str}",
42
+ "-H",
43
+ f"x-ig-app-id: {self.state.xigappid}",
44
+ "-sS",
45
+ ]
46
+
47
+ if self.state.session.proxies.get("https"):
48
+ command.extend(["-x", self.state.session.proxies["https"]])
49
+
50
+ result = subprocess.run(command, capture_output=True, text=True)
51
+
52
+ class MockResponse:
53
+ def __init__(self, text, status_code):
54
+ self.text = text
55
+ self.status_code = status_code
56
+
57
+ def json(self):
58
+ return json.loads(self.text)
59
+
60
+ status = 200 if result.returncode == 0 else 500
61
+ return MockResponse(result.stdout, status)
62
+
63
+ def get_profile_info(self, username: str, return_raw: bool = False):
64
+ url = f"https://www.instagram.com/api/v1/users/web_profile_info/"
65
+ params = {"username": username}
66
+ response = self._execute_request("GET", url, params=params)
67
+
68
+ if response.status_code != 200:
69
+ return None
70
+
71
+ data = response.json()
72
+ if return_raw:
73
+ return data
74
+
75
+ return ProfileData.parse_instagram_json(data)
76
+
77
+ def get_feed(self, username: str, max_id: str = "", return_raw: bool = False):
78
+ url = f"https://www.instagram.com/api/v1/feed/user/{username}/username/"
79
+ params = {"count": 33, "max_id": max_id}
80
+
81
+ response = self._execute_request("GET", url, params=params)
82
+
83
+ if response.status_code != 200:
84
+ return None
85
+
86
+ data = response.json()
87
+
88
+ if return_raw:
89
+ return data
90
+
91
+ items = data.get("items", [])
92
+ posts = [FeedData.parse_item(item) for item in items]
93
+
94
+ return FeedData(
95
+ posts=posts,
96
+ next_max_id=data.get("next_max_id"),
97
+ num_results=data.get("num_results", 0),
98
+ more_available=data.get("more_available", False),
99
+ )
100
+
101
+ def get_comments(self, media_id: str, next_min_id: str = None, return_raw: bool = False):
102
+ url = f"https://www.instagram.com/api/v1/media/{media_id}/comments/"
103
+
104
+ params = {"can_support_threading": "true"}
105
+ if next_min_id:
106
+ params["min_id"] = next_min_id
107
+
108
+ response = self._execute_request("GET", url, params=params)
109
+
110
+ if response.status_code != 200:
111
+ return None
112
+
113
+ data = response.json()
114
+
115
+ if return_raw:
116
+ return data
117
+
118
+ comments_data = data.get("comments", [])
119
+ comments = [CommentsData.parse_item(item) for item in comments_data]
120
+
121
+ return CommentsData(
122
+ comments=comments,
123
+ next_max_id=data.get("next_min_id"),
124
+ num_results=len(comments),
125
+ more_available=data.get("has_more_comments", False),
126
+ )
@@ -0,0 +1,211 @@
1
+ import json
2
+ from datetime import datetime
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from pydantic import BaseModel, Field, HttpUrl
6
+
7
+
8
+ class ProfileData(BaseModel):
9
+ """Modelo estruturado para informações completas do perfil."""
10
+
11
+ # Básico
12
+ id: str
13
+ username: str
14
+ full_name: Optional[str]
15
+ biography: Optional[str]
16
+ profile_pic_url: Optional[HttpUrl]
17
+
18
+ # Status
19
+ is_private: bool
20
+ is_verified: bool
21
+ is_business_account: bool
22
+ is_professional_account: bool
23
+
24
+ # Métricas
25
+ follower_count: int
26
+ following_count: int
27
+ total_posts: int
28
+ highlight_reel_count: int
29
+ mutual_followers: int
30
+
31
+ # Categoria
32
+ category_name: Optional[str]
33
+ business_category_name: Optional[str]
34
+ should_show_category: Optional[bool]
35
+
36
+ # Conteúdo
37
+ has_clips: Optional[bool]
38
+ has_guides: Optional[bool]
39
+ has_channel: Optional[bool]
40
+
41
+ # Links
42
+ external_url: Optional[HttpUrl]
43
+ bio_links: List[Dict[str, Any]] = []
44
+
45
+ # Localização (raw dict já parseado)
46
+ business_address: Optional[Dict[str, Any]]
47
+
48
+ # Contatos
49
+ should_show_public_contacts: Optional[bool]
50
+ business_email: Optional[str]
51
+ business_phone_number: Optional[str]
52
+
53
+ @classmethod
54
+ def parse_instagram_json(cls, data: dict):
55
+ """Helper para limpar o aninhamento profundo do JSON original."""
56
+ user = data.get("data", {}).get("user", {})
57
+ if not user:
58
+ return None
59
+
60
+ # Parse do endereço comercial (string JSON → dict)
61
+ business_address_raw = user.get("business_address_json")
62
+ parsed_address = None
63
+ if business_address_raw:
64
+ try:
65
+ parsed_address = json.loads(business_address_raw)
66
+ except Exception:
67
+ parsed_address = None
68
+
69
+ return cls(
70
+ # Básico
71
+ id=user.get("id"),
72
+ username=user.get("username"),
73
+ full_name=user.get("full_name"),
74
+ biography=user.get("biography"),
75
+ profile_pic_url=user.get("profile_pic_url_hd"),
76
+ # Status
77
+ is_private=user.get("is_private", False),
78
+ is_verified=user.get("is_verified", False),
79
+ is_business_account=user.get("is_business_account", False),
80
+ is_professional_account=user.get("is_professional_account", False),
81
+ # Métricas
82
+ follower_count=user.get("edge_followed_by", {}).get("count", 0),
83
+ following_count=user.get("edge_follow", {}).get("count", 0),
84
+ total_posts=user.get("edge_owner_to_timeline_media", {}).get("count", 0),
85
+ highlight_reel_count=user.get("highlight_reel_count", 0),
86
+ mutual_followers=user.get("edge_mutual_followed_by", {}).get("count", 0),
87
+ # Categoria
88
+ category_name=user.get("category_name"),
89
+ business_category_name=user.get("business_category_name"),
90
+ should_show_category=user.get("should_show_category"),
91
+ # Conteúdo
92
+ has_clips=user.get("has_clips"),
93
+ has_guides=user.get("has_guides"),
94
+ has_channel=user.get("has_channel"),
95
+ # Links
96
+ external_url=user.get("external_url"),
97
+ bio_links=user.get("bio_links", []),
98
+ # Localização
99
+ business_address=parsed_address,
100
+ # Contatos
101
+ should_show_public_contacts=user.get("should_show_public_contacts"),
102
+ business_email=user.get("business_email"),
103
+ business_phone_number=user.get("business_phone_number"),
104
+ )
105
+
106
+
107
+ class FeedData(BaseModel):
108
+ # Dados do Post
109
+ post_id: Optional[str] = Field(default=None, alias="id")
110
+ pk: Optional[str] = None
111
+ shortcode: Optional[str] = Field(default=None, alias="code")
112
+ created_at: Optional[datetime] = None
113
+ caption: Optional[str] = None
114
+ media_url: Optional[HttpUrl] = None
115
+ media_type: Optional[int] = None
116
+ like_count: Optional[int] = 0
117
+ comment_count: Optional[int] = 0
118
+ view_count: Optional[int] = None
119
+ carousel_media_count: Optional[int] = 0
120
+
121
+ # Dados do Feed
122
+ posts: Optional[List["FeedData"]] = None
123
+ next_max_id: Optional[str] = None
124
+ num_results: Optional[int] = None
125
+ more_available: Optional[bool] = None
126
+
127
+ @classmethod
128
+ def parse_item(cls, item: dict):
129
+ ts = item.get("taken_at") or (item.get("device_timestamp", 0) / 1_000_000)
130
+
131
+ caption_dict = item.get("caption")
132
+ caption_text = caption_dict.get("text") if caption_dict else None
133
+
134
+ candidates = item.get("image_versions2", {}).get("candidates", [])
135
+ best_image = candidates[0].get("url") if candidates else None
136
+
137
+ return cls(
138
+ id=item.get("id"),
139
+ pk=item.get("pk"),
140
+ code=item.get("code"),
141
+ created_at=datetime.fromtimestamp(ts) if ts else None,
142
+ caption=caption_text,
143
+ media_url=best_image,
144
+ media_type=item.get("media_type"),
145
+ like_count=item.get("like_count", 0),
146
+ comment_count=item.get("comment_count", 0),
147
+ view_count=item.get("view_count"),
148
+ carousel_media_count=item.get("carousel_media_count", 0),
149
+ )
150
+
151
+
152
+ class CommentsData(BaseModel):
153
+ # Dados do Comentário
154
+ comment_id: Optional[str] = Field(default=None, alias="pk")
155
+ media_id: Optional[str] = None
156
+ text: Optional[str] = None
157
+ created_at: Optional[datetime] = None
158
+ like_count: Optional[int] = Field(default=0, alias="comment_like_count")
159
+ content_type: Optional[str] = None
160
+ status: Optional[str] = None
161
+ is_ranked_comment: Optional[bool] = None
162
+ is_edited: Optional[bool] = None
163
+
164
+ # Dados do Autor
165
+ user_id: Optional[str] = None
166
+ username: Optional[str] = None
167
+ full_name: Optional[str] = None
168
+ profile_pic_url: Optional[HttpUrl] = None
169
+ is_private: Optional[bool] = None
170
+ is_verified: Optional[bool] = None
171
+
172
+ # Dados da Listagem
173
+ comments: Optional[List["CommentsData"]] = None
174
+ next_max_id: Optional[str] = None
175
+ num_results: Optional[int] = None
176
+ more_available: Optional[bool] = None
177
+
178
+ @classmethod
179
+ def parse_item(cls, item: dict):
180
+ ts = item.get("created_at")
181
+
182
+ user = item.get("user", {})
183
+
184
+ return cls(
185
+ pk=item.get("pk"),
186
+ media_id=item.get("media_id"),
187
+ text=item.get("text"),
188
+ created_at=datetime.fromtimestamp(ts) if ts else None,
189
+ comment_like_count=item.get("comment_like_count", 0),
190
+ content_type=item.get("content_type"),
191
+ status=item.get("status"),
192
+ is_ranked_comment=item.get("is_ranked_comment"),
193
+ is_edited=item.get("is_edited"),
194
+ user_id=user.get("id"),
195
+ username=user.get("username"),
196
+ full_name=user.get("full_name"),
197
+ profile_pic_url=user.get("profile_pic_url"),
198
+ is_private=user.get("is_private"),
199
+ is_verified=user.get("is_verified"),
200
+ )
201
+
202
+ @classmethod
203
+ def parse_response(cls, data: dict):
204
+ comments_data = data.get("comments", [])
205
+
206
+ return cls(
207
+ comments=[cls.parse_item(item) for item in comments_data],
208
+ next_max_id=data.get("next_max_id"),
209
+ num_results=data.get("num_results"),
210
+ more_available=data.get("more_available"),
211
+ )
@@ -0,0 +1,32 @@
1
+ import requests
2
+
3
+
4
+ class InstagramSession:
5
+ def __init__(self, csrftoken, ds_user_id, sessionid, xigappid="936619743392459", proxy=None):
6
+ self.session = requests.Session()
7
+ self.xigappid = xigappid
8
+ self.proxy = proxy
9
+
10
+ self.session.cookies.set("csrftoken", csrftoken)
11
+ self.session.cookies.set("ds_user_id", ds_user_id)
12
+ self.session.cookies.set("sessionid", sessionid)
13
+
14
+ self.default_headers = {
15
+ "x-ig-app-id": self.xigappid,
16
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
17
+ "accept": "*/*",
18
+ "accept-language": "en-US,en;q=0.9",
19
+ }
20
+ self.session.headers.update(self.default_headers)
21
+
22
+ if proxy:
23
+ self.session.proxies = {"http": proxy, "https": proxy}
24
+
25
+ def get_cookie_string(self):
26
+ """Retorna os cookies formatados para o cabeçalho do CURL."""
27
+ c = self.session.cookies.get_dict()
28
+ return "; ".join([f"{k}={v}" for k, v in c.items()])
29
+
30
+ def request_on_session(self, method, url, **kwargs):
31
+ """Método para requisições via Requests (Python)."""
32
+ return self.session.request(method, url, timeout=10, **kwargs)
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: igmapper
3
+ Version: 1.0.0
4
+ Summary: Unofficial Instagram API to request available data
5
+ Author: lucasoal
6
+ License: MIT
7
+ Project-URL: Homepage, https://pypi.org/project/igmapper
8
+ Project-URL: Documentation, https://github.com/lucasoal/igmapper/blob/main/doc/DOCUMENTATION.md
9
+ Project-URL: Repository, https://github.com/lucasoal/igmapper
10
+ Keywords: igmapper,instagram,unofficial-api
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ License-File: AUTHORS.md
20
+ Requires-Dist: requests
21
+ Requires-Dist: pydantic
22
+ Dynamic: license-file
23
+
24
+ <div align="center">
25
+ <picture>
26
+ <source media="(prefers-color-scheme: dark)" srcset="assets/igmapper.svg">
27
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/lucasoal/igmapper/refs/heads/main/assets/igmapper.svg">
28
+ <img src="https://raw.githubusercontent.com/lucasoal/igmapper/refs/heads/main/assets/igmapper.svg" width="100%">
29
+ </picture>
30
+ <br><br><br>
31
+ <hr>
32
+ <h1>igmapper: An Instagram Unofficial API</h1>
33
+
34
+ <img src="https://img.shields.io/badge/Author-lucasoal-blue?logo=github&logoColor=white"> <img src="https://img.shields.io/badge/License-MIT-750014.svg"> <!-- <img src="https://img.shields.io/badge/Status-Beta-DF1F72"> -->
35
+ <br>
36
+ <img src="https://img.shields.io/pypi/v/igmapper.svg?label=Version&color=white"> <img src="https://img.shields.io/pypi/pyversions/igmapper?logo=python&logoColor=white&label=Python"> <img src="https://img.shields.io/badge/Code Style-Black Formatter-111.svg">
37
+ <br>
38
+ <img src="https://static.pepy.tech/badge/igmapper">
39
+ <!-- <img src="https://img.shields.io/pypi/dm/igmapper.svg?label=PyPI Downloads"> -->
40
+
41
+ </div>
42
+
43
+ ## What is it?
44
+ **igmapper** is a high-performance Python library designed for Instagram data extraction,
45
+ providing structured access to profiles, feeds, and comments. It offers a flexible
46
+ architecture that allows data engineers to toggle between Requests and native CURL
47
+ transport, ensuring resilience against environment constraints.
48
+
49
+ <h2>Table of Contents</h2><br>
50
+
51
+ - [What is it?](#what-is-it)
52
+ - [Main Features](#main-features)
53
+ - [Where to get it / Install](#where-to-get-it--install)
54
+ - [Documentation](#documentation)
55
+ - [License](#license)
56
+ - [Dependencies](#dependencies)
57
+
58
+ ## Main Features
59
+ Here are just a few of the things that pandas does well:
60
+
61
+ - [`InstaClient`](doc/DOCUMENTATION.md#instaclient): Initializes the session and handles transport selection (Requests or CURL)
62
+ - [`get_profile_info()`](doc/DOCUMENTATION.md#get_profile_info): Scrapes profile metadata and returns a structured ProfileData object.
63
+ - [`get_feed()`](doc/DOCUMENTATION.md#get_feed): Retrieves user timeline posts with built-in pagination support.
64
+ - [`get_comments()`](doc/DOCUMENTATION.md#get_comments): Fetches media comments and automates cursor-based pagination.
65
+
66
+ ## Where to get it / Install
67
+ The source code is currently hosted on GitHub at: https://github.com/lucasoal/igmapper
68
+
69
+
70
+ > [!WARNING]
71
+ > It's essential to use [**Python 3.10** 🡽](https://www.python.org/downloads/release/python-310/) version
72
+ <!-- > It's essential to **upgrade pip** to the latest version to ensure compatibility with the library. -->
73
+ <!-- > ```sh
74
+ > # Requires the latest pip
75
+ > pip install --upgrade pip
76
+ > ``` -->
77
+
78
+ - [PyPI 🡽](https://pypi.org/project/igmapper/)
79
+ ```sh
80
+ # PyPI
81
+ pip install igmapper
82
+ ```
83
+ - GitHub
84
+ ```sh
85
+ # or GitHub
86
+ pip install git+https://github.com/lucasoal/igmapper.git
87
+ ```
88
+
89
+ ## Documentation
90
+ - [Documentation 🡽](https://github.com/lucasoal/igmapper/blob/main/doc/DOCUMENTATION.md).
91
+
92
+ ## License
93
+ - [MIT 🡽](https://github.com/lucasoal/igmapper/blob/main/LICENSE)
94
+
95
+ ## Dependencies
96
+ - [Requests](https://pypi.org/project/requests/) | [pydantic](https://pypi.org/project/pydantic/)
97
+
98
+ See the [full installation instructions](https://github.com/lucasoal/igmapper/blob/main/INSTALLATION.md) for minimum supported versions of required, recommended and optional dependencies.
99
+
100
+ <hr>
101
+
102
+ [⇧ Go to Top](#table-of-contents)
@@ -0,0 +1,15 @@
1
+ AUTHORS.md
2
+ LICENSE
3
+ README.md
4
+ pyproject.toml
5
+ igmapper/__init__.py
6
+ igmapper/client.py
7
+ igmapper/models.py
8
+ igmapper/session.py
9
+ igmapper.egg-info/PKG-INFO
10
+ igmapper.egg-info/SOURCES.txt
11
+ igmapper.egg-info/dependency_links.txt
12
+ igmapper.egg-info/requires.txt
13
+ igmapper.egg-info/top_level.txt
14
+ tests/test_feed.py
15
+ tests/test_profile.py
@@ -0,0 +1,2 @@
1
+ requests
2
+ pydantic
@@ -0,0 +1 @@
1
+ igmapper
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "igmapper"
7
+ version = "1.0.0"
8
+ description = "Unofficial Instagram API to request available data"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+
13
+ authors = [
14
+ { name = "lucasoal" }
15
+ ]
16
+
17
+ keywords = [
18
+ "igmapper", "instagram", "unofficial-api",
19
+ ]
20
+
21
+ classifiers = [
22
+ "Development Status :: 4 - Beta",
23
+ "Intended Audience :: Developers",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Topic :: Software Development :: Libraries"
27
+ ]
28
+
29
+ dependencies = [
30
+ "requests", "pydantic",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://pypi.org/project/igmapper"
35
+ Documentation = "https://github.com/lucasoal/igmapper/blob/main/doc/DOCUMENTATION.md"
36
+ Repository = 'https://github.com/lucasoal/igmapper'
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["."]
40
+ include = ["igmapper*"]
41
+
42
+ [tool.setuptools]
43
+ include-package-data = true
44
+
45
+ [tool.pytest.ini_options]
46
+ minversion = "6.0"
47
+ addopts = "-ra"
48
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,39 @@
1
+ import pytest
2
+ from _vars import *
3
+
4
+ from igmapper.client import InstaClient
5
+
6
+
7
+ # @pytest.mark.skipif(not IG_SESSIONID, reason="Sem login")
8
+ def test_get_feed_real():
9
+ client = InstaClient(IG_CSRFTOKEN, IG_DS_USER_ID, IG_SESSIONID)
10
+
11
+ feed = client.get_feed(IG_USERNAME)
12
+
13
+ assert feed is not None
14
+ assert len(feed.posts) > 0
15
+ assert feed.num_results > 0
16
+
17
+ # Verifica se o primeiro post tem dados consistentes
18
+ post = feed.posts[0]
19
+ assert post.post_id is not None
20
+ assert post.shortcode is not None
21
+ print(f"\nFeed capturado. Último post: {post.shortcode}")
22
+
23
+
24
+ @pytest.mark.skipif(not IG_CSRFTOKEN, reason="Credenciais não configuradas")
25
+ def test_get_feed_real_curl():
26
+ # Testa via CURL
27
+ client = InstaClient(IG_CSRFTOKEN, IG_DS_USER_ID, IG_SESSIONID, use_curl=True)
28
+
29
+ feed = client.get_feed(IG_USERNAME)
30
+
31
+ assert feed is not None
32
+ assert len(feed.posts) > 0
33
+ assert feed.num_results > 0
34
+
35
+ # Verifica se o primeiro post tem dados consistentes
36
+ post = feed.posts[0]
37
+ assert post.post_id is not None
38
+ assert post.shortcode is not None
39
+ print(f"\nFeed capturado. Último post: {post.shortcode}")
@@ -0,0 +1,29 @@
1
+ import os
2
+
3
+ import pytest
4
+ from _vars import *
5
+
6
+ from igmapper.client import InstaClient
7
+
8
+
9
+ @pytest.mark.skipif(not IG_SESSIONID, reason="Credenciais não configuradas")
10
+ def test_get_profile_info_real():
11
+ # Testa via REQUESTS
12
+ client = InstaClient(IG_CSRFTOKEN, IG_DS_USER_ID, IG_SESSIONID, use_curl=False)
13
+ profile = client.get_profile_info(IG_USERNAME)
14
+
15
+ assert profile is not None
16
+ assert profile.username == IG_USERNAME
17
+ assert profile.follower_count > 0
18
+ print(f"\n[Requests] Sucesso: {profile.full_name} tem {profile.follower_count} seguidores.")
19
+
20
+
21
+ @pytest.mark.skipif(not IG_CSRFTOKEN, reason="Credenciais não configuradas")
22
+ def test_get_profile_info_real_curl():
23
+ # Testa via CURL
24
+ client = InstaClient(IG_CSRFTOKEN, IG_DS_USER_ID, IG_SESSIONID, use_curl=True)
25
+ profile = client.get_profile_info(IG_USERNAME)
26
+
27
+ assert profile is not None
28
+ assert profile.username == IG_USERNAME
29
+ print(f"\n[CURL] Sucesso: {profile.full_name} capturado via binário do sistema.")