Skip to content

Commit

Permalink
write some docs
Browse files Browse the repository at this point in the history
these are still going to be a steep learning curve for folks
but I'm trying

Change-Id: I9c854e7066a1844aac5b02042df8edec4663a6bd
  • Loading branch information
zzzeek committed Jun 25, 2024
1 parent 3f97f98 commit e734159
Show file tree
Hide file tree
Showing 8 changed files with 623 additions and 167 deletions.
168 changes: 15 additions & 153 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,157 +10,19 @@ asyncio compatible approaches, but allowing intermediary code to remain
completely unchanged. Its primary use is to support code that is cross-compatible
with asyncio and non-asyncio runtime environments.


Synopsis
========

Consider the following multithreaded program, which sends and receives messages
from an echo server. The program is organized into three layers:

* ``send_receive_implementation`` - this is a low level layer that interacts
with the Python ``socket`` library directly

* ``send_receive_logic`` - this is logic code that responds to requests to
send and receive messages, given an implementation function

* ``send_receive_api`` - this is the front-facing API that is used by programs.

We present this example below, adding a ``main()`` function that spins up
five threads and calls upon ``send_receive_api()`` independently within each:

.. sourcecode:: python

import socket
import threading

messages = []

def send_receive_implementation(host, port, message):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
sock.sendall(message.encode("ascii"))
return sock.recv(1024).decode("utf-8")

def send_receive_logic(msg, host, port, implementation):
return implementation(host, port, f"message number {msg}\n")

def send_receive_api(msg):
messages.append(
send_receive_logic(msg, "tcpbin.com", 4242, send_receive_implementation)
)

def main():
threads = [
threading.Thread(target=send_receive_api, args=(msg,))
for msg in ["one", "two", "three", "four", "five"]
]
for t in threads:
t.start()
for t in threads:
t.join()
for msg in messages:
print(f"Got back echo response: {msg}")


main()

The goal we have now is to provide an all-new asynchronous API to this program.
That is, we want to remove the use of threads, and instead have calling code which
looks like this:

.. sourcecode:: python

async def main():
messages = await asyncio.gather(
*[
message_api(msg) for msg in
["one", "two", "three", "four", "five"]
]
)
for msg in messages:
print(f"Got back echo response: {msg}")
asyncio.run(main())

To do this, we would need to rewrite all of the above functions to use
``async`` and ``await``. But what if the vast majority of our code were
within ``send_receive_logic()`` - that code is only a pass through, receiving
data to and from an opaque implementation. Must we convert **all** our code
everywhere that acts as "pass through" to use ``async`` ``await``?

With awaitlet, we dont have to. awaitlet provides a **functional form
of the Python await call**, which can be invoked from non-async functions,
within an overall asyncio context. We can port our program above by:

* Writing a new ``send_receive_implementation`` function that uses asyncio, rather than sync
* Writing a new ``send_receive_api`` that uses asyncio
* Writing a sync adapter that can be passed along to ``send_receive_logic``

This program then looks like:

.. sourcecode:: python

import asyncio
import awaitlet


async def async_send_receive_implementation(host, port, message):
reader, writer = await asyncio.open_connection(host, port)
writer.write(message.encode("ascii"))
await writer.drain()
data = (await reader.read(1024)).decode("utf-8")
return data


def send_receive_logic(msg, host, port, implementation):
return implementation(host, port, f"message number {msg}\n")

async def send_receive_api(msg):
def adapt_async_implementation(host, port, message):
return awaitlet.awaitlet(
async_send_receive_implementation(host, port, message)
)

return await awaitlet.async_def(
send_receive_logic,
msg,
"tcpbin.com",
4242,
adapt_async_implementation
)

async def main():
messages = await asyncio.gather(
*[
send_receive_api(msg)
for msg in ["one", "two", "three", "four", "five"]
]
)
for msg in messages:
print(f"Got back echo response: {msg}")

asyncio.run(main())

Above, the front end and back end are ported to asyncio, but the
middle part stays the same; that is, the ``send_receive_logic()`` function
**did not change at all, no async/await keywords needed**. That's the point of awaitlet; **to eliminate
the async/await keyword tax applied to code that doesnt directly invoke
non-blocking functions.**

How does this work?
===================

The context shift feature of the Python ``await`` keyword is made available in a functional
way using the `greenlet <https://pypi.org/project/greenlet/>`_ library. The source code for
``async_def()`` and ``awaitlet()`` are a only a few dozen lines of code, using greenlet
to adapt ``awaitlet()`` function calls to real Python ``await`` keywords.

Has anyone used this before?
============================

Are you using `SQLAlchemy with asyncio <https://docs.sqlalchemy.org/en/latest/orm/extensions/asyncio.html>`_ anywhere? Then **you're using it right now**.
awaitlet is a port of SQLAlchemy's own greenlet/asyncio mediation layer pulled into its own package, with no
dependencies on SQLAlchemy. This code has been in widespread production use in thousands of environments for several
years, starting in 2020 with SQLAlchemy 1.4's first release.
awaitlet is intentionally fully compatible with SQLAlchemy's asyncio mediation
layer, and includes API patterns for:

* Converting any threaded program (no SQLAlchemy dependency necessary) to use
asyncio patterns for front facing APIs and backends, without modifying
intermediary code
* Converting threaded database-enabled programs to use asyncio patterns for
front facing APIs and backends, where those backends use SQLAlchemy's asyncio
API for database access
* Converting threaded database-enabled programs to use asyncio patterns for
front facing APIs and backends, without modifying intermediary code that uses
SQLAlchemy's synchronous API for database access

Documentation for awaitlet is within this source distribution and availble on
the web at https://awaitlet.sqlalchemy.org .

12 changes: 0 additions & 12 deletions changelog.rst

This file was deleted.

7 changes: 6 additions & 1 deletion docs/build/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
AUTOBUILD = sphinx-autobuild --port 8080 --watch ../../awaitlet
PAPER =
BUILDDIR = output

Expand All @@ -12,11 +13,12 @@ PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .

.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
.PHONY: help clean html autobuild dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest

help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " autobuild autobuild and run a webserver"
@echo " dist-html same as html, but places files in /doc"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " pickle to make pickle files"
Expand All @@ -36,6 +38,9 @@ html:
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."

autobuild:
$(AUTOBUILD) $(ALLSPHINXOPTS) $(BUILDDIR)/html

dist-html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
cp -R $(BUILDDIR)/html/* ../
Expand Down
11 changes: 11 additions & 0 deletions docs/build/api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
===
API
===

.. currentmodule:: awaitlet

.. autofunction:: async_def

.. autofunction:: awaitlet


11 changes: 11 additions & 0 deletions docs/build/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,19 @@
"sphinx.ext.intersphinx",
"changelog",
"sphinx_paramlinks",
"sphinx_copybutton",
]

copybutton_prompt_text = (
r">>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: "
)
copybutton_prompt_is_regexp = True
# workaround
# https://sphinx-copybutton-exclude-issue.readthedocs.io/en/v0.5.1-go/
# https://github.com/executablebooks/sphinx-copybutton/issues/185
copybutton_exclude = ".linenos"


changelog_sections = ["feature", "usecase", "bug"]

changelog_render_ticket = "https://github.com/sqlalchemy/awaitlet/issues/%s"
Expand Down
72 changes: 71 additions & 1 deletion docs/build/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,83 @@
Welcome to awaitlet's documentation!
==========================================

Awaitlet docs go here

Awaitlet allows non-async defs that invoke awaitables inside of asyncio applications.

awaitlet allows existing programs written to use threads and blocking APIs to
be ported to asyncio, by replacing frontend and backend code with asyncio
compatible approaches, but allowing intermediary code to remain completely
unchanged, with no addition of ``async`` or ``await`` keywords throughout the
entire codebase needed. Its primary use is to support code that is
cross-compatible with asyncio and non-asyncio runtime environments.

Awaitlet is a direct extract of SQLAlchemy's own `asyncio mediation layer
<https://docs.sqlalchemy.org/en/latest/orm/extensions/asyncio.html>`_, with no
dependencies on SQLAlchemy (but is also fully cross-compatible with
SQLAlchemy's mediation layer). This code has been in widespread production
use in thousands of environments for several years, starting in 2020 with
SQLAlchemy 1.4's first release.

awaitlet without any dependency or use of SQLAlchemy includes API patterns for:

* Converting any threaded program (no SQLAlchemy dependency necessary) to use
asyncio patterns for front facing APIs and backends, without modifying
intermediary code

For applications that do use SQLAlchemy, awaitlet provides additional
API patterns for:

* Converting threaded database-enabled programs to use asyncio patterns for
front facing APIs and backends, where those backends use SQLAlchemy's asyncio
API for database access
* Converting threaded database-enabled programs to use asyncio patterns for
front facing APIs and backends, without modifying intermediary code that uses
SQLAlchemy's synchronous API for database access

The two functions provided are :func:`.async_def` and :func:`.awaitlet`. Using
these, we can create an asyncio program using intermediary defs that do not use the `async`
or `await` keywords, but instead use functions::

import asyncio

import awaitlet

def asyncio_sleep():
return awaitlet.awaitlet(asyncio.sleep(5, result='hello'))

print(asyncio.run(awaitlet.async_def(asyncio_sleep)))

Above, the ``asyncio_sleep()`` def is run directly in asyncio and calls upon
the ``asyncio.sleep()`` async API call, but the function itself does not declare
itself as ``async``; instead, this is applied functionally using the
:func:`.async_def` function. Through this approach, a program can be made
to use asyncio for its front-facing API, talking to asyncio libraries for
non-blocking IO patterns, while not impacting intermediary code, which remains
compatible with non-asyncio use as well.

For a more complete example see :doc:`synopsis`.


How does this work?
===================

The context shift feature of the Python ``await`` keyword is made available in a functional
way using the `greenlet <https://pypi.org/project/greenlet/>`_ library. The source code for
:func:`.async_def` and :func:`.awaitlet` are a only a few dozen lines of code, using greenlet
to adapt :func:`.awaitlet` function calls to real Python ``await`` keywords.


.. toctree::
:maxdepth: 2

synopsis
sqlalchemy
api
changelog




Indices and tables
==================

Expand Down
Loading

0 comments on commit e734159

Please sign in to comment.