Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add method to send post with multiple attached photos #313

Merged
merged 12 commits into from
Mar 30, 2024
22 changes: 22 additions & 0 deletions examples/send_images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from atproto import Client


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']
images = []
for path in paths:
with open(path, 'rb') as f:
images.append(f.read())

client.send_images(
text='Post with image from Python',
images=images,
image_alts=['Text version', 'of the image (ALT)', 'This parameter is optional'])
ryoryo25 marked this conversation as resolved.
Show resolved Hide resolved


if __name__ == '__main__':
main()
62 changes: 57 additions & 5 deletions packages/atproto_client/client/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import typing as t
from threading import Lock

from atproto_core.exceptions import AtProtocolError
from atproto_core.uri import AtUri

from atproto_client import models
Expand Down Expand Up @@ -173,6 +174,58 @@ def delete_post(self, post_uri: str) -> bool:
uri = AtUri.from_str(post_uri)
return self.app.bsky.feed.post.delete(uri.hostname, uri.rkey)

def send_images(
self,
text: t.Union[str, TextBuilder],
images: t.List[bytes],
image_alts: t.Optional[t.List[str]] = None,
profile_identify: t.Optional[str] = None,
reply_to: t.Optional['models.AppBskyFeedPost.ReplyRef'] = None,
langs: t.Optional[t.List[str]] = None,
facets: t.Optional[t.List['models.AppBskyRichtextFacet.Main']] = None,
) -> 'models.AppBskyFeedPost.CreateRecordResponse':
"""Send post with multiple attached images (up to 4 images).

Note:
If `profile_identify` is not provided will be sent to the current profile.

Args:
text: Text of the post.
images: List of binary images to attach. The length must be less than or equal to 4.
image_alts: List of text version of the images. The length must be equal to the length of `images`.
profile_identify: Handle or DID. Where to send post.
reply_to: Root and parent of the post to reply to.
langs: List of used languages in the post.
facets: List of facets (rich text items).

Returns:
:obj:`models.AppBskyFeedPost.CreateRecordResponse`: Reference to the created record.

Raises:
:class:`atproto.exceptions.AtProtocolError`: Base exception.
"""
if image_alts is None:
image_alts = [''] * len(images)

# validation
if len(images) != len(image_alts):
raise AtProtocolError('The lengths of images & image_alts must be equal.')
ryoryo25 marked this conversation as resolved.
Show resolved Hide resolved

uploads = [self.upload_blob(i) for i in images]
ryoryo25 marked this conversation as resolved.
Show resolved Hide resolved
embed_images = [
models.AppBskyEmbedImages.Image(alt=a, image=u.blob)
for a, u in zip(image_alts, uploads)
ryoryo25 marked this conversation as resolved.
Show resolved Hide resolved
]

return self.send_post(
text,
profile_identify=profile_identify,
reply_to=reply_to,
embed=models.AppBskyEmbedImages.Main(images=embed_images),
langs=langs,
facets=facets,
)

def send_image(
self,
text: t.Union[str, TextBuilder],
Expand All @@ -191,7 +244,7 @@ def send_image(
Args:
text: Text of the post.
image: Binary image to attach.
image_alt: Text version of the image
image_alt: Text version of the image.
profile_identify: Handle or DID. Where to send post.
reply_to: Root and parent of the post to reply to.
langs: List of used languages in the post.
Expand All @@ -203,13 +256,12 @@ def send_image(
Raises:
:class:`atproto.exceptions.AtProtocolError`: Base exception.
"""
upload = self.com.atproto.repo.upload_blob(image)
images = [models.AppBskyEmbedImages.Image(alt=image_alt, image=upload.blob)]
return self.send_post(
return self.send_images(
text,
images=[image],
image_alts=[image_alt],
profile_identify=profile_identify,
reply_to=reply_to,
embed=models.AppBskyEmbedImages.Main(images=images),
langs=langs,
facets=facets,
)
Expand Down
7 changes: 7 additions & 0 deletions packages/atproto_codegen/clients/generate_async_client.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from pathlib import Path

from atproto_codegen.consts import DISCLAIMER
Expand Down Expand Up @@ -40,6 +41,12 @@ def gen_client(input_filename: str, output_filename: str) -> None:
code = code.replace(f'self.{method}(', f'await self.{method}(')
code = code.replace(f'super().{method}(', f'await super().{method}(')

code = code.replace('from asyncio', 'import asyncio\nfrom asyncio')
ryoryo25 marked this conversation as resolved.
Show resolved Hide resolved
code = re.sub(
r'(\[self\.upload_blob.*\])',
r'await asyncio.gather(*\1)',
code)

code = DISCLAIMER + code

write_code(_CLIENT_DIR.joinpath(output_filename), code)
Expand Down
Loading