Name | pythreads JSON |
Version |
0.2.1
JSON |
| download |
home_page | None |
Summary | A Python wrapper of Meta's Threads API |
upload_time | 2024-07-15 00:58:28 |
maintainer | None |
docs_url | None |
author | None |
requires_python | >=3.8 |
license | MIT License
Copyright (c) 2024-present Marc Love <copyright@marclove.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
keywords |
meta
sdk
threads
|
VCS |
|
bugtrack_url |
|
requirements |
No requirements were recorded.
|
Travis-CI |
No Travis.
|
coveralls test coverage |
No coveralls.
|
# PyThreads
[![PyPI - Version](https://img.shields.io/pypi/v/pythreads.svg)](https://pypi.org/project/pythreads)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pythreads.svg)](https://pypi.org/project/pythreads)
![Code Coverage](https://img.shields.io/badge/coverage-93%25-brightgreen)
[Official Documentation](https://marclove.com/pythreads)
PyThreads is a Python wrapper for Meta's Threads API. It is still in beta,
but is well-tested and covers all the published endpoints documented by Meta.
Since it is in pre-release, the API is not yet guaranteed to be stable. Once
there's been some opportunity to weed out any bugs and identify any DX
inconveniences, a v1 with a stable API will be released. This project
follows [Semantic Versioning](https://semver.org/).
-----
## Table of Contents
- [Installation](#installation)
- [Environment Variables](#environment-variables)
- [Authentication & Authorization](#authentication--authorization)
- [Making Requests](#making-requests)
- [API Methods](#api-methods)
- [Roadmap](#roadmap)
- [License](#license)
## Installation
```console
pip install pythreads
```
## Environment Variables
You will need to create an app in the [developer console](https://developers.facebook.com/docs/development/create-an-app/threads-use-case) which has the Threads Use Case enabled and add the the following variables to your environment. The redirect URI needs to be the URL in your application where you call the `complete_authorization` method.
```
THREADS_REDIRECT_URI=
THREADS_APP_ID=
THREADS_API_SECRET=
```
## Authentication & Authorization
Authenticating with PyThreads is very simple:
1. Generate an authorization url and state key. Make the auth_url the
href of a link or button and **store the state_key** (which is
a randomly generated, opaque string used to mitigate CSRF attacks)
somewhere you can reference later when Threads redirects the user back
to your redirect URI.
```python
auth_url, state_key = Threads.authorization_url()
```
2. When the user clicks the link/button, they will be sent to Threads to
authenticate and authorize your application access to their account.
Upon authorization, they will be sent to your THREADS_REDIRECT_URI. At your
THREADS_REDIRECT_URI endpoint, call `complete_authorization` with the full
URL of the request made to your server (which contains the auth code) and
the `state_key`, which was generated in the previous step:
```python
credentials = Threads.complete_authorization(requested_url, state_key)
```
3. This method automatically exchanges the auth code for a short-lived token
and immediately exchanges the short-lived token for a long-lived token.
The method returns `Credentials` object, which contains this long-lived
token and details about the user. The `Credentials` object can be
serialized and deserialized to/from JSON, making it easy for you to
persist it in some data store or store it encrypted in the user's
session.
```python
json = credentials.to_json()
```
```json
{
"user_id": "someid",
"scopes": ["threads_basic"],
"short_lived": false,
"access_token": "someaccesstoken",
"expiration": "2024-06-23T18:25:43.511Z"
}
```
```python
Credentials.from_json(json)
# or
Credentials(
user_id="someid",
scopes=["threads_basic"],
short_lived=false,
access_token="someaccesstoken",
expiration=datetime.datetime(2024, 6, 23, 18, 25, 43, 121680, tzinfo=datetime.timezone.utc)
)
```
5. Long-lived tokens last 60 days, and are refreshable, as long as the
token hasn't expired yet. Implement your own application logic to
determine when it makes sense to refresh users' long-lived tokens, which
you can do with:
```python
refreshed_credentials = Threads.refresh_long_lived_token(old_credentials)
```
A `Credentials` object has convenience methods to make it easier for
you to determine whether the token is still valid and how much longer
it is valid for.
```python
credentials.expired()
>>> False
credentials.expires_in()
>>> 7200 # seconds
```
Of course, you can always check the expiration time directly. It is
stored in UTC time:
```python
credentials.expiration
>>> datetime(2024, 7, 11, 10, 50, 32, 870181, tzinfo=datetime.timezone.utc)
```
If you call an API method using expired credentials, a `ThreadsAccessTokenExpired`
exception will be raised.
## Making Requests
Once you have a valid `Credentials` object, you can use an `API` object to
call the Threads API. The `API` object uses an `aiohttp.ClientSession` to make
async HTTP calls. You may either supply your own session or let the library
create and manage one itself.
If you do not supply your own session, PyThreads will create one and take
responsibility for closing it. You must use the `API` object as an async
context manager if you want it to manage a session for you:
```python
# Retrieve the user's credentials from whereever you're storing them:
credentials = Credentials.from_json(stored_json)
async with API(credentials=credentials) as api:
await api.threads()
```
If you want to create and manage your own session (e.g. you're already
using `aiohttp` elsewhere in your application and want to use a single
session for all requests):
```python
# Create an `aiohttp.ClientSession` at some point:
session = aiohttp.ClientSession()
# Retrieve the user's credentials from whereever you're storing them:
credentials = Credentials.from_json(stored_json)
api = API(credentials=credentials, session=session)
threads = await api.threads()
# If you supply your own session, you are responsible for closing it:
session.close()
```
## API Methods
Most of the methods follow [Meta's API](https://developers.facebook.com/docs/threads)
closely, with required/optional arguments matching API required/optional
parameters.
### Making a text-only post
Making a text-only post is a two-step process. Create a container and then
publish it:
```python
async with API(credentials) as api:
container_id = await api.create_container("A text-only post")
result_id = await api.publish_container(container_id)
# container_id == result_id
```
### Making a post with a single media file
Making a post with a single media file is also a two-step process. Create a
container with the media file and any post text and then publish it:
```python
async with API(credentials) as api:
# Create a video container. You must put media resources at a publicly-accessible URL where Threads can download it.
a_video = Media(type=MediaType.VIDEO, url="https://mybucket.s3.amazonaws.com/video.mp4")
container_id = await api.create_container(media=a_video)
# Video containers need to complete processing before you can publish them
await asyncio.sleep(15)
# Check the status to see if it's finished processing
status = await api.container_status(container_id)
# >>> ContainerStatus(id='14781862679302648', status=<PublishingStatus.FINISHED: 'FINISHED'>, error=None)
# Publish the video container
result_id = await api.publish_container(container_id)
# container_id == result_id
```
### Making a carousel post
Making a post with a media carousel is a three-step process. Create a container
for each media file in the carousel, then create a container for the carousel
(attaching the media containers as children), and publish the carousel container:
```python
async with API(self.credentials) as api:
# Create an image container
an_image = Media(type=MediaType.IMAGE, url="https://mybucket.s3.amazonaws.com/python.png")
image_id = await api.create_container(media=an_image, is_carousel_item=True)
# Create a video container
a_video = Media(type=MediaType.VIDEO, url="https://mybucket.s3.amazonaws.com/video.mp4")
video_id = await api.create_container(media=a_video, is_carousel_item=True)
# Video containers need to complete processing before you can publish them
await asyncio.sleep(15)
# Check the status to see if the containers are finished processing
status_1 = await api.container_status(image_id)
status_2 = await api.container_status(video_id)
# >>> ContainerStatus(id='14781862679302648', status=<PublishingStatus.FINISHED: 'FINISHED'>, error=None)
# >>> ContainerStatus(id='14823646267930264', status=<PublishingStatus.FINISHED: 'FINISHED'>, error=None)
# Create the carousel container, which wraps the media containers as children
carousel_id = await api.create_carousel_container(containers=[status_1, status_2], text="Here's a carousel")
await asyncio.sleep(15)
# Check the carousel container status
carousel_status = await api.container_status(carousel_id)
# >>> ContainerStatus(id='15766826793021848', status=<PublishingStatus.FINISHED: 'FINISHED'>, error=None)
# Publish the carousel container
result_id = await api.publish_container(carousel_id)
# carousel_id == result_id
```
A few key things to point out above:
1. Creating media containers requires you to put the image or video at a
publicly-accessible URL. Meta retrieves the media file from that URL.
1. When creating a media container with a video, Threads requires you to
wait for the video to be processed before you can either publish them or
attach them to a carousel container. This can take seconds or minutes.
You are **strongly encouraged** to read Meta's documentation regarding the posting
process:
- [Post to Threads](https://developers.facebook.com/docs/threads/posts)
- [Understanding Container Status](https://developers.facebook.com/docs/threads/troubleshooting#publishing-does-not-return-a-media-id)
## Roadmap
- [ ] Improve documentation of `API` methods and publish the docs.
- [ ] Type the return values of the `API` methods. They currently all return `Any`.
- [ ] Add integration with S3 and R2 storage. The Threads API doesn't take media uploads directly. You have to upload files to a publicly accessible URL and pass the URL in the API response. This integration would handle the upload to cloud storage and passing of the URL to the Threads API for you.
- [ ] Explore adding JSON fixtures of expected responses to specs.
## License
`pythreads` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
Raw data
{
"_id": null,
"home_page": null,
"name": "pythreads",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": "Marc Love <copyright@marclove.com>",
"keywords": "meta, sdk, threads",
"author": null,
"author_email": "Marc Love <copyright@marclove.com>",
"download_url": "https://files.pythonhosted.org/packages/7d/fb/1cf14fefed2df17145ba7096afcaff485759546286431e47d3c92c52ff30/pythreads-0.2.1.tar.gz",
"platform": null,
"description": "# PyThreads\n\n[![PyPI - Version](https://img.shields.io/pypi/v/pythreads.svg)](https://pypi.org/project/pythreads)\n[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pythreads.svg)](https://pypi.org/project/pythreads)\n![Code Coverage](https://img.shields.io/badge/coverage-93%25-brightgreen)\n\n[Official Documentation](https://marclove.com/pythreads)\n\nPyThreads is a Python wrapper for Meta's Threads API. It is still in beta, \nbut is well-tested and covers all the published endpoints documented by Meta.\n\nSince it is in pre-release, the API is not yet guaranteed to be stable. Once \nthere's been some opportunity to weed out any bugs and identify any DX \ninconveniences, a v1 with a stable API will be released. This project \nfollows [Semantic Versioning](https://semver.org/).\n\n-----\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Environment Variables](#environment-variables)\n- [Authentication & Authorization](#authentication--authorization)\n- [Making Requests](#making-requests)\n- [API Methods](#api-methods)\n- [Roadmap](#roadmap)\n- [License](#license)\n\n## Installation\n\n```console\npip install pythreads\n```\n\n## Environment Variables\n\nYou will need to create an app in the [developer console](https://developers.facebook.com/docs/development/create-an-app/threads-use-case) which has the Threads Use Case enabled and add the the following variables to your environment. The redirect URI needs to be the URL in your application where you call the `complete_authorization` method.\n\n```\nTHREADS_REDIRECT_URI=\nTHREADS_APP_ID=\nTHREADS_API_SECRET=\n```\n\n## Authentication & Authorization\n\nAuthenticating with PyThreads is very simple:\n\n1. Generate an authorization url and state key. Make the auth_url the\n href of a link or button and **store the state_key** (which is\n a randomly generated, opaque string used to mitigate CSRF attacks)\n somewhere you can reference later when Threads redirects the user back\n to your redirect URI.\n\n ```python\n auth_url, state_key = Threads.authorization_url()\n ```\n\n2. When the user clicks the link/button, they will be sent to Threads to\n authenticate and authorize your application access to their account.\n Upon authorization, they will be sent to your THREADS_REDIRECT_URI. At your\n THREADS_REDIRECT_URI endpoint, call `complete_authorization` with the full\n URL of the request made to your server (which contains the auth code) and\n the `state_key`, which was generated in the previous step:\n\n ```python\n credentials = Threads.complete_authorization(requested_url, state_key)\n ```\n\n3. This method automatically exchanges the auth code for a short-lived token\n and immediately exchanges the short-lived token for a long-lived token.\n The method returns `Credentials` object, which contains this long-lived\n token and details about the user. The `Credentials` object can be\n serialized and deserialized to/from JSON, making it easy for you to\n persist it in some data store or store it encrypted in the user's\n session.\n\n ```python\n json = credentials.to_json()\n ```\n ```json\n { \n \"user_id\": \"someid\", \n \"scopes\": [\"threads_basic\"], \n \"short_lived\": false, \n \"access_token\": \"someaccesstoken\", \n \"expiration\": \"2024-06-23T18:25:43.511Z\"\n }\n ```\n\n ```python\n Credentials.from_json(json)\n\n # or\n\n Credentials(\n user_id=\"someid\", \n scopes=[\"threads_basic\"], \n short_lived=false, \n access_token=\"someaccesstoken\", \n expiration=datetime.datetime(2024, 6, 23, 18, 25, 43, 121680, tzinfo=datetime.timezone.utc)\n )\n ```\n\n5. Long-lived tokens last 60 days, and are refreshable, as long as the\n token hasn't expired yet. Implement your own application logic to\n determine when it makes sense to refresh users' long-lived tokens, which\n you can do with:\n\n ```python\n refreshed_credentials = Threads.refresh_long_lived_token(old_credentials)\n ```\n\n A `Credentials` object has convenience methods to make it easier for\n you to determine whether the token is still valid and how much longer\n it is valid for.\n\n ```python\n credentials.expired()\n >>> False\n\n credentials.expires_in()\n >>> 7200 # seconds\n ```\n\n Of course, you can always check the expiration time directly. It is\n stored in UTC time:\n\n ```python\n credentials.expiration\n >>> datetime(2024, 7, 11, 10, 50, 32, 870181, tzinfo=datetime.timezone.utc)\n ```\n\n If you call an API method using expired credentials, a `ThreadsAccessTokenExpired`\n exception will be raised. \n\n## Making Requests\n\nOnce you have a valid `Credentials` object, you can use an `API` object to\ncall the Threads API. The `API` object uses an `aiohttp.ClientSession` to make \nasync HTTP calls. You may either supply your own session or let the library\ncreate and manage one itself.\n\nIf you do not supply your own session, PyThreads will create one and take\nresponsibility for closing it. You must use the `API` object as an async\ncontext manager if you want it to manage a session for you:\n\n```python\n# Retrieve the user's credentials from whereever you're storing them:\ncredentials = Credentials.from_json(stored_json)\n\nasync with API(credentials=credentials) as api:\n await api.threads()\n```\n\nIf you want to create and manage your own session (e.g. you're already\nusing `aiohttp` elsewhere in your application and want to use a single\nsession for all requests):\n\n```python\n# Create an `aiohttp.ClientSession` at some point:\nsession = aiohttp.ClientSession()\n\n# Retrieve the user's credentials from whereever you're storing them:\ncredentials = Credentials.from_json(stored_json)\n\napi = API(credentials=credentials, session=session)\nthreads = await api.threads()\n\n# If you supply your own session, you are responsible for closing it:\nsession.close()\n```\n\n## API Methods\n\nMost of the methods follow [Meta's API](https://developers.facebook.com/docs/threads)\nclosely, with required/optional arguments matching API required/optional\nparameters.\n\n### Making a text-only post\nMaking a text-only post is a two-step process. Create a container and then\npublish it:\n\n```python\nasync with API(credentials) as api:\n container_id = await api.create_container(\"A text-only post\")\n result_id = await api.publish_container(container_id)\n # container_id == result_id\n```\n\n### Making a post with a single media file\nMaking a post with a single media file is also a two-step process. Create a\ncontainer with the media file and any post text and then publish it:\n\n```python\nasync with API(credentials) as api:\n # Create a video container. You must put media resources at a publicly-accessible URL where Threads can download it.\n a_video = Media(type=MediaType.VIDEO, url=\"https://mybucket.s3.amazonaws.com/video.mp4\")\n container_id = await api.create_container(media=a_video)\n\n # Video containers need to complete processing before you can publish them\n await asyncio.sleep(15)\n\n # Check the status to see if it's finished processing\n status = await api.container_status(container_id)\n # >>> ContainerStatus(id='14781862679302648', status=<PublishingStatus.FINISHED: 'FINISHED'>, error=None)\n\n # Publish the video container\n result_id = await api.publish_container(container_id)\n # container_id == result_id\n```\n\n### Making a carousel post\nMaking a post with a media carousel is a three-step process. Create a container\nfor each media file in the carousel, then create a container for the carousel\n(attaching the media containers as children), and publish the carousel container:\n\n```python\nasync with API(self.credentials) as api:\n # Create an image container\n an_image = Media(type=MediaType.IMAGE, url=\"https://mybucket.s3.amazonaws.com/python.png\")\n image_id = await api.create_container(media=an_image, is_carousel_item=True)\n\n # Create a video container\n a_video = Media(type=MediaType.VIDEO, url=\"https://mybucket.s3.amazonaws.com/video.mp4\")\n video_id = await api.create_container(media=a_video, is_carousel_item=True)\n\n # Video containers need to complete processing before you can publish them\n await asyncio.sleep(15)\n\n # Check the status to see if the containers are finished processing\n status_1 = await api.container_status(image_id)\n status_2 = await api.container_status(video_id)\n # >>> ContainerStatus(id='14781862679302648', status=<PublishingStatus.FINISHED: 'FINISHED'>, error=None)\n # >>> ContainerStatus(id='14823646267930264', status=<PublishingStatus.FINISHED: 'FINISHED'>, error=None)\n\n # Create the carousel container, which wraps the media containers as children\n carousel_id = await api.create_carousel_container(containers=[status_1, status_2], text=\"Here's a carousel\")\n\n await asyncio.sleep(15)\n\n # Check the carousel container status\n carousel_status = await api.container_status(carousel_id)\n # >>> ContainerStatus(id='15766826793021848', status=<PublishingStatus.FINISHED: 'FINISHED'>, error=None)\n\n # Publish the carousel container\n result_id = await api.publish_container(carousel_id)\n # carousel_id == result_id\n```\n\nA few key things to point out above:\n\n1. Creating media containers requires you to put the image or video at a\npublicly-accessible URL. Meta retrieves the media file from that URL.\n\n1. When creating a media container with a video, Threads requires you to\nwait for the video to be processed before you can either publish them or\nattach them to a carousel container. This can take seconds or minutes.\n\nYou are **strongly encouraged** to read Meta's documentation regarding the posting\nprocess:\n\n- [Post to Threads](https://developers.facebook.com/docs/threads/posts)\n- [Understanding Container Status](https://developers.facebook.com/docs/threads/troubleshooting#publishing-does-not-return-a-media-id)\n\n## Roadmap\n- [ ] Improve documentation of `API` methods and publish the docs.\n- [ ] Type the return values of the `API` methods. They currently all return `Any`.\n- [ ] Add integration with S3 and R2 storage. The Threads API doesn't take media uploads directly. You have to upload files to a publicly accessible URL and pass the URL in the API response. This integration would handle the upload to cloud storage and passing of the URL to the Threads API for you.\n- [ ] Explore adding JSON fixtures of expected responses to specs.\n\n## License\n\n`pythreads` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.\n",
"bugtrack_url": null,
"license": "MIT License\n \n Copyright (c) 2024-present Marc Love <copyright@marclove.com>\n \n Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n \n The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n \n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
"summary": "A Python wrapper of Meta's Threads API",
"version": "0.2.1",
"project_urls": {
"Documentation": "https://github.com/marclove/pythreads#readme",
"Homepage": "https://github.com/marclove/pythreads",
"Issues": "https://github.com/marclove/pythreads/issues",
"Repository": "https://github.com/marclove/pythreads"
},
"split_keywords": [
"meta",
" sdk",
" threads"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "0c5c3e608dd40e1ae83029cb3f2470f91f4f594f00694ada3e26d03b22bba7c9",
"md5": "a139197ddcdd3c24d651a20cb5fa20c9",
"sha256": "fadbf15bc8ca7b985d276a7f198c50c1e0c7d18fdab68a0e93c5c8194d50ef2f"
},
"downloads": -1,
"filename": "pythreads-0.2.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "a139197ddcdd3c24d651a20cb5fa20c9",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 17904,
"upload_time": "2024-07-15T00:58:29",
"upload_time_iso_8601": "2024-07-15T00:58:29.977587Z",
"url": "https://files.pythonhosted.org/packages/0c/5c/3e608dd40e1ae83029cb3f2470f91f4f594f00694ada3e26d03b22bba7c9/pythreads-0.2.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "7dfb1cf14fefed2df17145ba7096afcaff485759546286431e47d3c92c52ff30",
"md5": "2401e311d4f53060c342569bbe887745",
"sha256": "921d13b5fc91f1c463c25746ec013a935b9bf10642a98727eeb57f7c206894cc"
},
"downloads": -1,
"filename": "pythreads-0.2.1.tar.gz",
"has_sig": false,
"md5_digest": "2401e311d4f53060c342569bbe887745",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 26202,
"upload_time": "2024-07-15T00:58:28",
"upload_time_iso_8601": "2024-07-15T00:58:28.761453Z",
"url": "https://files.pythonhosted.org/packages/7d/fb/1cf14fefed2df17145ba7096afcaff485759546286431e47d3c92c52ff30/pythreads-0.2.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-07-15 00:58:28",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "marclove",
"github_project": "pythreads#readme",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "pythreads"
}