sendgrid / sendgrid-python

The Official Twilio SendGrid Python API Library
https://sendgrid.com
MIT License
1.55k stars 714 forks source link

IMPROVEMENT: Support for asynchronous sending #296

Open Jakobovski opened 7 years ago

Jakobovski commented 7 years ago

Thanks for the great API and product!

It would be awesome if there was support for asynchronous api calls.

Use Case: When charging a user's credit card over my REST API I need to call the stripe API ( a few times), then call sendgrid API to send user confirmation message, then call sendgrid again to notify my sales team of a new sale. This takes a bit of time to wait for all the responses and causes my API it be rather slow.

Cheers.

thinkingserious commented 7 years ago

Hello @Jakobovski,

Thanks for taking the time to not only add the request, but for the additional details! And thanks for the kind words as well!

We will add this to our backlog. For it to rise in importance, we would require additional votes or a pull request.

With Best Regards,

Elmer

jussih commented 7 years ago

This is something I wouldn't expect in a library, but rather something that should be implemented in the project that uses the library. The de facto solution with Python is to write an asynchronous task to do the heavy lifting using Celery.

thinkingserious commented 7 years ago

Hi @jussih,

Would you mind creating documentation that demonstrates how this can be done? You would add your PR here and we would give you hacktoberfest credit at "difficulty: medium" for that. If not, no worries. We appreciate your feedback in any case :)

With Best Regards,

Elmer

LiYChristopher commented 7 years ago

Celery is a great tool for async or parallel processing. But if one is using Python 3.5+, the built-in asyncio library can be used to send email in a non-blocking manner. I created a gist to illustrate roughly how this might work.

SendGrid v3 Mail Send - Async Example

asyncio helps us execute mail sending in a separate context, allowing us to continue execution of business logic without waiting for all our emails to send first.

tr11 commented 7 years ago

I've been doing something like this for a while, but resorted to updating the http-client library instead of making any changes to this library. You can take a look at https://github.com/tr11/python-http-client, which uses aiohttp instead of the requests library to do the heavy lifting.

thinkingserious commented 7 years ago

@LiYChristopher,

Do you mind adding that example to this repo's USE_CASES.md?

That's awesome @tr11!

Do you mind making a PR for hacktoberfest on that library to demonstrate your example? You would create a USE_CASES.md based on the format of the one found in this repo.

With Best Regards,

Elmer

LiYChristopher commented 7 years ago

@thinkingserious

I have a PR open - #363 that adds this example to the USE_CASES.md file. If there are any other changes I need to make beyond contained in the change log, please let me know. Thanks!

dizlv commented 6 years ago

@LiYChristopher are you sure your code actually works? Client post will block scheduler, or I'm missing something?

response = sg.client.mail.send.post(request_body=email.get())

stalkerg commented 5 years ago

Hello. I suppose we can use the same approach what I used for https://github.com/stalkerg/pywebpush - sendgrid API will return just URL and data to send, and all sending mechanisms should be outside. Like

sg = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY'))
url, data = sg.prepare_send(message)
response = await your_async_method_for_send(url, data)
response = sg.process_response(response)

because we have many different libs (for me it's Tornado) to make an async request we should provide an easy way to integration with any of it.

ejm commented 5 years ago

If this is still on the table, I'd love to take it on as a Hacktoberfest project two years later following the idea of @stalkerg here. Would the method name prepare_send be okay? I wouldn't imagine a process_response method would be necessary as send just returns the response untouched.

I think send should keep the default client (for backwards compatibility) but prepare_send would just be another option for people who are using async or their own HTTP clients.

Taywee commented 3 years ago

This has been open for a while, and nobody has mentioned it, but you can run blocking network requests in parallel through the default executor. It's not an obvious solution (mentioned only in a corner of the asyncio documentation and referring to a method that doesn't directly refer to being run in a background thread, but the example code demonstrates it), and it's not perfect (because it's not actually using async IO, but executing parallel through threads behind the scenes) but this will work when run from within any async function, and will be properly parallelized without blocking or exposing thread safety issues:

mail = {
    # Mail body here
}
loop = asyncio.get_running_loop()  # or get_event_loop if you need to use Python 3.6
response = await loop.run_in_executor(
    None,
    functools.partial(
        api_client.client.mail.send.post,
        request_body=mail))

Like I said, not the most ideal, but for my uses, it allows backgrounding parallel IO to keep my application responsive and snappy.

The previous answer posted by @LiYChristopher will not work. Each request will still fully block the scheduler. I only mention it because it has a handful of positive reactions despite not functioning the way it looks like it does.

edit: I should mention that this is thread-safe in respect to asyncio, but it does involve threads running in parallel, so if sendgrid's API client is not thread-safe for any reason, this will actually be vulnerable to thread safety issues. I don't think this will be the case in cPython due to the GIL, but I can't assert that for certain. I have had exactly one 503 error from SendGrid while interacting in this way, but I don't think it's related to this parallelism and I haven't been able to replicate it.

stalkerg commented 3 years ago

@Taywee, after all, I started to communicate with REST API directly it's much easier than fighting with the library in the async environment.

Taywee commented 3 years ago

@stalkerg That's fine. The SendGrid API is pretty easy to work with directly. This library is synchronous, though, so the only way to get parallel IO with it currently is through a separate thread or process, whether using traditional threading, an asynchronous executor (including the default ThreadPoolExecutor that the default Python event loop makes available), or some other way of offloading the parallel work to a thread and waiting for it.

thinkingserious commented 3 years ago

I prefer @stalkerg's solution, mainly because there are several popular async libs and it would be great to better support them all. Then we can add usage examples in the most popular async libs in the docs.

Thank you to everyone on this thread (and linked threads) for the thoughtful conversation. And thank you @Taywee for the PR and explanations with sample code!

This issue has been added to our internal backlog to be prioritized. Pull requests and +1s on the issue summary will help it move up the backlog.

stalkerg commented 3 years ago

@thinkingserious this also should fix this issue #409 ;)

stalkerg commented 3 years ago

Interesting, this approach even formalized! https://sans-io.readthedocs.io/ you definitely should move into this side.

quantology commented 2 years ago

Since there aren't any working code examples posted above, I figured I'd share what I just cooked up:

import aiohttp
from sendgrid import SendGridAPIClient

async def send_async(client, message):
    if not isinstance(message, dict):
        message = message.get()
    async with aiohttp.ClientSession() as session:
        async with session.post(f"{client.host}/v3/mail/send", 
                                headers=client._default_headers,
                                json=message) as resp:
            resp.raise_for_status()
            return await resp.text()
SendGridAPIClient.send_async = send_async

Later, I call with: response = await sg_client.send_async(message).