Basics
The shortest complete programs in the repository. Each one logs in and does a single thing.
For the prose that explains these, see Posting, Reading content, and The social graph.
Send a post
Send rich text
Links, mentions, and hashtags are byte ranges over the post text, not markup. TextBuilder computes them for you.
from atproto import Client, client_utils
# To send links as "link card" or "quote post" look at the advanced_usage/send_embed.py example.
# There is a more advanced way to send rich text without helper class in the advanced_usage/send_rich_text.py example.
def main() -> None:
client = Client()
client.login('my-handle', 'my-password')
text_builder = client_utils.TextBuilder()
text_builder.tag('This is a rich message. ', 'atproto')
text_builder.text('I can mention ')
text_builder.mention('account', 'did:plc:kvwvcn5iqfooopmyzvb4qzba')
text_builder.text(' and add clickable ')
text_builder.link('link', 'https://atproto.blue/')
# You can pass instance of TextBuilder instead of str to the "text" argument.
client.send_post(text_builder) # same with send_image method
# Same with chaining:
client.send_post(client_utils.TextBuilder().text('Test msg using ').link('Python SDK', 'https://atproto.blue/'))
if __name__ == '__main__':
main()
Send an image
Always set alt text.
from atproto import Client, models
def main() -> None:
client = Client()
client.login('my-handle', 'my-password')
# replace the path to your image file
with open('cat.jpg', 'rb') as f:
img_data = f.read()
# Add image aspect ratio to prevent default 1:1 aspect ratio
# Replace with your desired aspect ratio
aspect_ratio = models.AppBskyEmbedDefs.AspectRatio(height=100, width=100)
client.send_image(
text='Post with image from Python SDK',
image=img_data,
image_alt='Text version of the image (ALT)',
image_aspect_ratio=aspect_ratio,
)
if __name__ == '__main__':
main()
Send several images
from atproto import Client, models
def main() -> None:
client = Client()
client.login('my-handle', 'my-password')
# replace the path to your image file
paths = ['cat.jpg', 'dog.jpg', 'bird.jpg']
image_alts = [
'Text version',
'of the image (ALT)',
'This parameter is optional',
]
# Add image aspect ratio to prevent default 1:1 aspect ratio
# Replace with your desired aspect ratio
image_aspect_ratios = [
models.AppBskyEmbedDefs.AspectRatio(height=1, width=1),
models.AppBskyEmbedDefs.AspectRatio(height=4, width=3),
models.AppBskyEmbedDefs.AspectRatio(height=16, width=9),
]
images = []
for path in paths:
with open(path, 'rb') as f:
images.append(f.read())
client.send_images(
text='Post with image from Python SDK',
images=images,
image_alts=image_alts,
image_aspect_ratios=image_aspect_ratios,
)
if __name__ == '__main__':
main()
Send a video
from atproto import Client, models
def main() -> None:
client = Client()
client.login('my-handle', 'my-password')
# replace the path to your video file
with open('video.mp4', 'rb') as f:
vid_data = f.read()
# Add video aspect ratio to prevent default 1:1 aspect ratio
# Replace with your desired aspect ratio
aspect_ratio = models.AppBskyEmbedDefs.AspectRatio(height=100, width=100)
client.send_video(
text='Post with video from Python SDK',
video=vid_data,
video_alt='Text version of the video (ALT)',
video_aspect_ratio=aspect_ratio,
)
if __name__ == '__main__':
main()
Delete a post
from atproto import Client
def main() -> None:
client = Client()
client.login('my-handle', 'my-password')
# same with the like_post.py example we need to keep a reference to the post
post_ref = client.send_post('Test send-delete from Python SDK')
print('Post reference:', post_ref)
# this method returns True/False depends on the response
print('Deleted successfully:', client.delete_post(post_ref.uri))
if __name__ == '__main__':
main()
Read your home timeline
from atproto import Client
def main() -> None:
client = Client()
client.login('my-handle', 'my-password')
print('Home (Following):\n')
# Get "Home" page. Use pagination (cursor + limit) to fetch all posts
timeline = client.get_timeline(algorithm='reverse-chronological')
for feed_view in timeline.feed:
action = 'New Post'
if feed_view.reason:
action_by = feed_view.reason.by.handle
action = f'Reposted by @{action_by}'
post = feed_view.post.record
author = feed_view.post.author
print(f'[{action}] {author.display_name}: {post.text}')
if __name__ == '__main__':
main()
Read someoneโs posts
from atproto import Client
def main(client: Client, handle: str) -> None:
print(f'\nProfile Posts of {handle}:\n\n')
# Get profile's posts. Use pagination (cursor + limit) to fetch all
profile_feed = client.get_author_feed(actor=handle)
for feed_view in profile_feed.feed:
print('-', feed_view.post.record.text)
if __name__ == '__main__':
at_client = Client()
at_client.login('my-handle', 'my-password')
while True:
input_handle = input('\nPlease, enter the handle of the user: ')
main(at_client, input_handle)
Like and unlike
Liking creates a record. Keep the URI it returns, because undoing the like means deleting that record.
from atproto import Client
def main() -> None:
client = Client()
client.login('my-handle', 'my-password')
post = client.send_post(text='Hello World from Python SDK!')
print('Post reference:', post)
print('Like reference:', client.like(uri=post.uri, cid=post.cid))
if __name__ == '__main__':
main()
from atproto import Client
def main() -> None:
client = Client()
client.login('my-handle', 'my-password')
post = client.send_post('Test like-unlike from Python SDK')
print('Post reference:', post)
like = client.like(uri=post.uri, cid=post.cid)
print('Like reference:', like)
# this method return True/False depends on the response. could throw exceptions too
print(client.unlike(like.uri))
if __name__ == '__main__':
main()
Repost
from atproto import Client
def main() -> None:
client = Client()
client.login('my-handle', 'my-password')
post_ref = client.send_post(text='Hello World from Python SDK!')
print('Post reference:', post_ref)
print('Reposted post reference:', client.repost(uri=post_ref.uri, cid=post_ref.cid))
if __name__ == '__main__':
main()
Process notifications
from time import sleep
from atproto import Client
# how often we should check for new notifications
FETCH_NOTIFICATIONS_DELAY_SEC = 3
def main() -> None:
client = Client()
client.login('my-handle', 'my-password')
# fetch new notifications
while True:
# save the time in UTC when we fetch notifications
last_seen_at = client.get_current_time_iso()
response = client.app.bsky.notification.list_notifications()
for notification in response.notifications:
if not notification.is_read:
print(f'Got new notification! Type: {notification.reason}; from: {notification.author.did}')
# example: "Got new notification! Type: like; from: did:plc:hlorqa2iqfooopmyzvb4byaz"
# mark notifications as processed (isRead=True)
client.app.bsky.notification.update_seen({'seen_at': last_seen_at})
print('Successfully process notification. Last seen at:', last_seen_at)
sleep(FETCH_NOTIFICATIONS_DELAY_SEC)
if __name__ == '__main__':
main()