Skip to content
This repository was archived by the owner on Oct 1, 2023. It is now read-only.

Commit ffd0e6a

Browse files
authored
Pydantic types for API responses (#59)
1 parent 8a755c1 commit ffd0e6a

File tree

7 files changed

+576
-124
lines changed

7 files changed

+576
-124
lines changed

README.md

Lines changed: 89 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,17 @@ Inspired by [NPM Threads-API](https://github.com/junhoyeo/threads-api)
1919

2020
Threads API is an unofficial Python client for Meta's Threads API. It allows you to interact with the API to login, read and publish posts, view who liked a post, retrieve user profile information, follow/unfollow and much more.
2121

22-
It allows you to configure the session object. Choose between:
23-
* `aiohttp` - Python library to ease asynchronous execution of the API, for ⚡ super-fast ⚡ results. (*default*)
24-
* `requests` - Python library for standard ease of use (supports `HTTP_PROXY` env var functionality)
25-
* `instagrapi` - utilize the same connection all the way for private api
26-
* (Advanced) Implement your own and call ThreadsAPI like this: `ThreadsAPI(http_session_class=YourOwnHTTPSessionImpl)`
22+
✅ Configurable underlying HTTP Client (`aiohttp` / `requests` / `instagrapi`'s client / implement your own)
23+
24+
✅ Authentication Methods supported via `instagrapi`
25+
26+
✅ Stores token and settings locally, to reduce login-attempts (*uses the same token for all authenticated requests for up to 24 hrs*)
27+
28+
✅ Pydantic structures for API responses (for IDE auto-completion) (at [types.py](https://github.com/Danie1/threads-api/blob/main/threads_api/src/types.py))
29+
30+
✅ Actively Maintained since Threads.net Release (responsive in Github Issues, try it out!)
2731

28-
> **Note** Since v1.1.10 you can use `requests` or `instagrapi` as HTTP clients, not just `aiohttp`.
2932

30-
> **Note** Since v1.1.12 a `.session.json` file will be created by-default to save default settings (to reduce risk of being flagged). You can disable it by passing `ThreadsAPI(settings_path=None)`
3133

3234
> **Important Tip** Use the same `cached_token_path` for connections, to reduce the number of actual login attempts. When needed, threads-api will reconnect and update the file in `cached_token_path`.
3335
@@ -36,12 +38,12 @@ Table of content:
3638
* [Demo](#demo)
3739
* [Getting started](#getting-started)
3840
* [Installation](#installation)
39-
* [Set Log Level](#set-desired-log-level)
41+
* [Set Log Level & Troubleshooting](#set-desired-log-level)
4042
* [Set HTTP Client](#choose-a-different-http-client)
41-
* [Contributions](#contributing-to-danie1threads-api)
4243
* [Supported Features](#supported-features)
4344
* [Usage Examples](#usage-examples)
4445
* [Roadmap](#📌-roadmap)
46+
* [Contributions](#contributing-to-danie1threads-api)
4547
* [License](#license)
4648

4749
# Demo
@@ -119,42 +121,45 @@ export LOG_LEVEL=INFO
119121
export LOG_LEVEL=DEBUG
120122
```
121123

122-
# Contributing to Danie1/threads-api
123-
## Getting Started
124+
<details>
125+
<summary>Example of Request when LOG_LEVEL=DEBUG</summary>
124126

125-
With Poetry (*Recommended*)
126127
``` bash
127-
# Step 1: Clone the project
128-
git clone git@github.com:Danie1/threads-api.git
128+
<---- START ---->
129+
Keyword arguments:
130+
[title]: ["PUBLIC REQUEST"]
131+
[type]: ["GET"]
132+
[url]: ["https://www.instagram.com/instagram"]
133+
[headers]: [{
134+
"Authority": "www.threads.net",
135+
"Accept": "*/*",
136+
"Accept-Language": "en-US,en;q=0.9",
137+
"Cache-Control": "no-cache",
138+
"Content-Type": "application/x-www-form-urlencoded",
139+
"Origin": "https://www.threads.net",
140+
"Pragma": "no-cache",
141+
"Sec-Fetch-Dest": "document",
142+
"Sec-Fetch-Mode": "navigate",
143+
"Sec-Fetch-Site": "cross-site",
144+
"Sec-Fetch-User": "?1",
145+
"Upgrade-Insecure-Requests": "1",
146+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.2 Safari/605.1.15",
147+
"X-ASBD-ID": "129477",
148+
"X-IG-App-ID": "238260118697367"
149+
}]
150+
<---- END ---->
129151

130-
# Step 2: Install dependencies to virtual environment
131-
poetry install
132-
133-
# Step 3: Activate virtual environment
134-
poetry shell
135152
```
136-
or
137-
138-
Without Poetry
139153

140-
``` bash
141-
# Step 1: Clone the project
142-
git clone git@github.com:Danie1/threads-api.git
143-
144-
# Step 2: Create virtual environment
145-
python3 -m venv env
154+
</details>
146155

147-
# Step 3 (Unix/MacOS): Activate virtual environment
148-
source env/bin/activate # Unix/MacOS
156+
### Troubleshooting threads-api
157+
Upon unexpected error, or upon receiving an exception with: `Oops, this is an error that hasn't yet been properly handled.\nPlease open an issue on Github at https://github.com/Danie1/threads-api.`
149158

150-
# Step 3 (Windows): Activate virtual environment
151-
.\env\Scripts\activate # Windows
152-
153-
# Step 4: Install dependencies
154-
pip install -r requirements.txt
155-
```
159+
Please open a Github Issue with all of the information you can provide, which includes the last Request and Response (Set `LOG_LEVEL=DEBUG`)
156160

157161
# Supported Features
162+
- [x] ✅ Pydantic typing API responses at [types.py](https://github.com/Danie1/threads-api/blob/main/threads_api/src/types.py)
158163
- [x] ✅ Login functionality, including 2FA 🔒
159164
- [x] ✅ Cache login token securely (reduce login requests / due to restrictive limits)
160165
- [x] ✅ Saves settings locally, such as device information and timezone to use along your sessions
@@ -200,25 +205,63 @@ pip install -r requirements.txt
200205
- [x] ✅ Instagrapi
201206

202207
## Usage Examples
203-
View [examples/public_api_examples.py](https://github.com/Danie1/threads-api/blob/main/examples/public_api_examples.py) for Public API code examples. For the Private API usage (requires login), head over to [examples/private_api_examples.py](https://github.com/Danie1/threads-api/blob/main/examples/private_api_examples.py)
204-
205-
At the end of the file you will be able to uncomment and run the individual examples with ease.
208+
View [examples/public_api_examples.py](https://github.com/Danie1/threads-api/blob/main/examples/public_api_examples.py) for Public API code examples.
206209

207-
Then simply run as:
208-
```
210+
Run as:
211+
``` bash
209212
python3 examples/public_api_examples.py
213+
```
210214

211-
# or
215+
View [examples/private_api_examples.py](https://github.com/Danie1/threads-api/blob/main/examples/private_api_examples.py) for Private API code examples. (🔒 Requires Authentication 🔒)
212216

213-
# Pass the credentials as environment variables
217+
Run as:
218+
```
214219
USERNAME=<Instagram Username> PASSWORD=<Instagram Password> python3 examples/private_api_examples.py
215220
```
216221

222+
> **Note:**
223+
> At the end of the file you will be able to uncomment and run the individual examples with ease.
224+
217225
## 📌 Roadmap
218-
- [ ] 🚧 Post text and share a video
226+
- [ ] 🚧 Share a video
219227
- [ ] 🚧 Documentation Improvements
220-
- [ ] 🚧 CI/CD Improvements
221-
- [ ] 🚧 Add coverage Pytest + Coverage Widget to README
228+
- [ ] 🚧 Add coverage Pytest + Coverage Widget to README
229+
230+
231+
# Contributing to Danie1/threads-api
232+
## Getting Started
233+
234+
With Poetry (*Recommended*)
235+
``` bash
236+
# Step 1: Clone the project
237+
git clone git@github.com:Danie1/threads-api.git
238+
239+
# Step 2: Install dependencies to virtual environment
240+
poetry install
241+
242+
# Step 3: Activate virtual environment
243+
poetry shell
244+
```
245+
or
246+
247+
Without Poetry
248+
249+
``` bash
250+
# Step 1: Clone the project
251+
git clone git@github.com:Danie1/threads-api.git
252+
253+
# Step 2: Create virtual environment
254+
python3 -m venv env
255+
256+
# Step 3 (Unix/MacOS): Activate virtual environment
257+
source env/bin/activate # Unix/MacOS
258+
259+
# Step 3 (Windows): Activate virtual environment
260+
.\env\Scripts\activate # Windows
261+
262+
# Step 4: Install dependencies
263+
pip install -r requirements.txt
264+
```
222265

223266
# License
224267
This project is licensed under the MIT license.

examples/private_api_examples.py

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
from threads_api.src.threads_api import ThreadsAPI
22
import asyncio
33
import os
4-
import logging
54
from threads_api.src.http_sessions.instagrapi_session import InstagrapiSession
65
from threads_api.src.http_sessions.requests_session import RequestsSession
76
from threads_api.src.http_sessions.aiohttp_session import AioHTTPSession
87

9-
from threads_api.src.anotherlogger import format_log
10-
118
# Code example of logging in while storing token encrypted on the file-system
129
async def login_with_cache():
1310

@@ -33,9 +30,9 @@ async def login_with_cache():
3330
# Asynchronously posts a message
3431
async def post(api):
3532
result = await api.post("Hello World!")
36-
37-
if result:
38-
print("Post has been successfully posted")
33+
34+
if result.media.pk:
35+
print(f"Post has been successfully posted with id: [{result.media.pk}]")
3936
else:
4037
print("Unable to post.")
4138

@@ -46,8 +43,8 @@ async def post_and_quote(api):
4643

4744
result = await api.post("What do you think of this?", quoted_post_id=post_id)
4845

49-
if result:
50-
print("Post has been successfully posted")
46+
if result.media.pk:
47+
print(f"Post has been successfully posted with id: [{result.media.pk}]")
5148
else:
5249
print("Unable to post.")
5350

@@ -56,17 +53,17 @@ async def post_and_quote(api):
5653
async def post_include_image(api):
5754
result = await api.post("Hello World with an image!", image_path=".github/logo.jpg")
5855

59-
if result:
60-
print("Post has been successfully posted")
56+
if result.media.pk:
57+
print(f"Post has been successfully posted with id: [{result.media.pk}]")
6158
else:
6259
print("Unable to post.")
6360

6461
# Asynchronously posts a message with an image
6562
async def post_include_image_from_url(api):
6663
result = await api.post("Hello World with an image!", image_path="https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/280px-PNG_transparency_demonstration_1.png")
6764

68-
if result:
69-
print("Post has been successfully posted")
65+
if result.media.pk:
66+
print(f"Post has been successfully posted with id: [{result.media.pk}]")
7067
else:
7168
print("Unable to post.")
7269

@@ -76,8 +73,8 @@ async def post_include_multiple_images(api):
7673
"https://upload.wikimedia.org/wikipedia/commons/b/b5/Baby.tux.sit-black-800x800.png",
7774
"https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/280px-PNG_transparency_demonstration_1.png"])
7875

79-
if result:
80-
print("Post has been successfully posted")
76+
if result.media.pk:
77+
print(f"Post has been successfully posted with id: [{result.media.pk}]")
8178
else:
8279
print("Unable to post.")
8380

@@ -87,8 +84,8 @@ async def post_include_url(api):
8784
result = False
8885
result = await api.post("Hello World with a link!", url="https://threads.net")
8986

90-
if result:
91-
print("Post has been successfully posted")
87+
if result.media.pk:
88+
print(f"Post has been successfully posted with id: [{result.media.pk}]")
9289
else:
9390
print("Unable to post.")
9491

@@ -184,13 +181,13 @@ async def block_and_unblock_user(api):
184181
return
185182

186183
async def get_timeline(api):
187-
def _print_post(post):
188-
caption = post['thread_items'][0]['post']['caption']
184+
def _print_post(timeline_item):
185+
caption = timeline_item.thread_items[0].post.caption
189186

190187
if caption == None:
191188
caption = "<Unable to print non-textual posts>"
192189
else:
193-
caption = caption['text']
190+
caption = caption.text
194191
print(f"Post -> Caption: [{caption}]\n")
195192

196193
async def _print_posts_in_feed(next_max_id=None, posts_to_go=0):
@@ -200,11 +197,12 @@ async def _print_posts_in_feed(next_max_id=None, posts_to_go=0):
200197
else:
201198
resp = await api.get_timeline()
202199

203-
for post in resp['items'][:resp['num_results']]:
204-
_print_post(post)
200+
201+
for timeline_item in resp.items[:resp.num_results]:
202+
_print_post(timeline_item)
205203

206-
posts_to_go -= resp['num_results']
207-
await _print_posts_in_feed(resp['next_max_id'], posts_to_go)
204+
posts_to_go -= resp.num_results
205+
await _print_posts_in_feed(resp.next_max_id, posts_to_go)
208206

209207
await _print_posts_in_feed(posts_to_go=20)
210208

@@ -216,25 +214,33 @@ async def get_user_threads(api):
216214
user_id = await api.get_user_id_from_username(username)
217215

218216
if user_id:
219-
resp = await api.get_user_threads(user_id)
217+
threads = await api.get_user_threads(user_id)
218+
220219
print(f"The threads for user '{username}' are:")
221-
for thread in resp['threads']:
222-
print(f"{username}'s Post: {thread['thread_items'][0]['post']['caption']['text']} || Likes: {thread['thread_items'][0]['post']['like_count']}")
220+
for thread in threads.threads:
221+
for thread_item in thread.thread_items:
222+
print(f"{username}'s Post: {thread_item.post.caption.text} || Likes: {thread_item.post.like_count}")
223223
else:
224224
print(f"User ID not found for username '{username}'")
225225

226226
# Asynchronously gets the replies for a user
227-
async def get_user_replies(api):
227+
async def get_user_replies(api : ThreadsAPI):
228228
username = "zuck"
229229
user_id = await api.get_user_id_from_username(username)
230230

231231
if user_id:
232232
threads = await api.get_user_replies(user_id)
233233
print(f"The replies for user '{username}' are:")
234-
for thread in threads['threads']:
235-
print(f"-\n{thread['thread_items'][0]['post']['user']['username']}'s Post: {thread['thread_items'][0]['post']['caption']['text']} || Likes: {thread['thread_items'][0]['post']['like_count']}")
234+
for thread in threads.threads:
235+
post = thread.thread_items[0].post
236+
print(f"-\n{post.user.username}'s Post: {post.caption.text} || Likes: {post.like_count}")
237+
238+
if len(thread.thread_items) > 1:
239+
first_reply = thread.thread_items[1].post
240+
print(f"{username}'s Reply: {first_reply.caption.text} || Likes: {first_reply.like_count}\n-")
241+
else:
242+
print(f"-> You will need to sign up / login to see more.")
236243

237-
print(f"{username}'s Reply: {thread['thread_items'][1]['post']['caption']['text']} || Likes: {thread['thread_items'][1]['post']['like_count']}\n-")
238244
else:
239245
print(f"User ID not found for username '{username}'")
240246

@@ -244,24 +250,26 @@ async def get_post(api : ThreadsAPI):
244250

245251
post_id = await api.get_post_id_from_url(post_url)
246252

247-
thread = await api.get_post(post_id)
248-
print(f"{thread['containing_thread']['thread_items'][0]['post']['user']['username']}'s post {thread['containing_thread']['thread_items'][0]['post']['caption']}:")
253+
response = await api.get_post(post_id)
254+
255+
thread = response.containing_thread.thread_items[0].post
256+
print(f"{thread.user.username}'s post {thread.caption.text}: || Likes: {thread.like_count}")
249257

250-
for thread in thread["reply_threads"]:
251-
if 'thread_items' in thread and len(thread['thread_items']) > 1:
252-
print(f"-\n{thread['thread_items'][0]['post']['user']['username']}'s Reply: {thread['thread_items'][0]['post']['caption']} || Likes: {thread['thread_items'][0]['post']['like_count']}")
258+
for reply in response.reply_threads:
259+
if reply.thread_items is not None and len(reply.thread_items) >= 1:
260+
print(f"-\n{reply.thread_items[0].post.user.username}'s Reply: {reply.thread_items[0].post.caption.text} || Likes: {reply.thread_items[0].post.like_count}")
253261

254262
# Asynchronously gets the likes for a post
255263
async def get_post_likes(api : ThreadsAPI):
256264
post_url = "https://www.threads.net/t/CuZsgfWLyiI"
257265

258266
post_id = await api.get_post_id_from_url(post_url)
259267

260-
likes = await api.get_post_likes(post_id)
268+
users_list = await api.get_post_likes(post_id)
261269
number_of_likes_to_display = 10
262270

263-
for user_info in likes['users'][:number_of_likes_to_display]:
264-
print(f'Username: {user_info["username"]} || Full Name: {user_info["full_name"]}')
271+
for user in users_list.users[:number_of_likes_to_display]:
272+
print(f'Username: {user.username} || Full Name: {user.full_name} || Follower Count: {user.follower_count} ')
265273

266274
# Asynchronously reposts and deletes the repost
267275
async def repost_and_delete(api : ThreadsAPI):

0 commit comments

Comments
 (0)