Skip to main content

Pretzel asynchronous python framework

Project description

Pretzel
-------

| |Build Status|
| |Coverage Status|

Is an asynchronous application framework for python

Features
--------

- C# like async/await(async/yield) paradigm for asynchronous
programming (monad base)
- Cool asynchronous I/O loop implementation
- Uniform asynchronous stream implementation for sockets and pipes
- Interact with subprocesses asynchronously
- Greenlet support (but not required)
- Remote code executing over ssh or in child process (with only
requirements python and ssh)
- Python 2/3, PyPy (starting from 2.0) compatible
- Asynchronous python shell ``python -mpretzel.apps.shell`` (requires
greenlet)

Installation
------------

| As git submodule:
| ``git submodule add git://github.com/aslpavel/pretzel.git <path_to_submodule>``
| Pip from git:
| ``pip install git+git://github.com/aslpavel/pretzel-pkg.git``
| Pip from PyPI
| ``pip install pretzel``

Approach
--------

| Usage of asynchronous functions is similar to C# async/await but
instead of
| ``async`` attribute it uses ``@async`` decorator and instead of
``await`` keyword,
| ``yield`` is used. Internally unit of asynchrony is implemented as
continuation
| monad ``Cont`` with embedded ``Result`` monad (similar to Haskell's
``Cont`` and
| ``Either`` monads) as its value. One important difference of ``Cont``
monad from C#
| ``Task`` object, is that ``Task`` represents already running
asynchronous operation,
| but continuation monad is a sequence of computations, and this
computations are
| not started. ``.future()`` method on instance of ``Cont`` can be used
to create
| ``Task`` like object. To use this library you don't have to understand
notion of
| the monad. Simple asynchronous function would look like this.
| \`\`\`python
| from pretzel.monad import async
| from pretzel.core imoprt sleep

| @async
| def print\_after(delay, \*args, \*\ *kwargs):
"""Calls print function after the lapse of ``delay`` sedonds.
"""
yield sleep(delay) # execution will be resumed in delay seconds
print(*\ args, \*\*kwargs)
| ``To return something meaningful in python3 you can just use `return` keyword, but in python2 you have to use `do_return` function (it will also work in python3) as `return` with value cannot be used inside a generator function. Result of such asynchronous function is again a continuation monad, if exception is thrown during execution of its body, it is marshaled to receiver of the result and can be processed correctly. For example.``\ python
| @async
| def process\_error():
| @async
| def trhow\_after(delay, error):
| yield sleep(delay)
| raise error

| try:
| yield throw\_after(1, ValueError('test error'))
| except ValueError as error:
| # process error in a meaningfull way
| do\_return('done') # exectly equivalent to: return 'done'
| ``Asynchronous values (continuation monads) can be composed with two helper functions `async_all` and `async_any`.``\ python
| @async
| def composition\_example():
| yield async\_all([sleep(1), sleep(2)]) # will be resumed in 2 seconds
| yield async\_any([sleep(1), sleep(2)]) # will be resumed in 1 sedond

| result\_all = yield async\_all([func1(), func2()]) # = (result1,
result2)
| reuslt\_any = yield async\_any([func1(), func2()]) # = result1 \|
result2
| ```Cont` monad can also be called with callback function as its argument, in this case, on completion of asynchronous operation, callback will be called with `Result` monad. If callback function is not specified default, then default continuation callback will be used which only reports errors if any.``\ python

| sleep(1)(print)
| Result(val:1374307530.015137)
| sleep(None)()
| [continuation] error in coroutine started from
| File "", line 1, in
| Traceback (most recent call last):
| File "pretzel/monad/do.py", line 26, in do\_block
| return value(block(\*a, \*\*kw))
| File "pretzel/core/core.py", line 118, in sleep
| do\_done(self.time\_queue.on(time() + delay))
| TypeError: unsupported operand type(s) for +: 'float' and
'NoneType'
| ``Inside body of asynchronous function you can `yield` not only `Cont` monad directly, but any object implementing `.__monad__()` method which returns `Cont` monad. There are many such types in this library, for example `Event```\ python
| @async
| def func():
| print(1)
| yield event
| print(2)
| print((yield event))
| event = Event()
| func()() # 1 is printed
| event('e0') # 2 is printed
| event('e1') # 'e1' is printed
| \`\`\`

Main loop
---------

| ``Core`` class implements I/O loop, and it is used internally to
implement
| asynchronous streams, timers and more. Previously used ``sleep``
function will
| work correctly only in presence of running I/O loop. Simplest way to
| intialize and use ``Core`` object is to use ``@app`` decorator.
| \`\`\`python
| """Minimal pretzel application

| Sleeps for one second, then prints 'done' and exits.
| """
| from pretzel.app import app
| from pretzel.core import sleep

| @app
| def main():
| yield sleep(1)
| print('done')

| if **name** == '**main**\ ':
| main()
| \`\`\`

Remoting
--------

| Main reason for creation of this framework was to execute code on a
set of
| machines via ssh connection. And its achieved by usage of
``SSHConnection`` class.
| ``SSHConnection`` object a callable object which returns proxy object
for its
| argument. You can call proxy object, get its attributes or items
``proxy[item]``,
| result of such operations is again a proxy object with this embedded
operations.
| Proxy implements monad interface, and to get result of embedded chain
of
| operations you can yield it inside asynchronous function. In this
example we
| create proxy for ``os.getpid`` function, call it and then execute on
remote
| process by yielding it. There is no need for pretzel to be installed
on remote
| machine.
| \`\`\`python
| import os
| from pretzel.app import app
| from pretzel.remoting import SSHConnection

| @app
| def main():
| """Connect to localhost via ssh and print remote process's pid

| Note:
| You have to be able to login to the remote host without
| entering any password (by means of ssh keys) otherwise
| connecition will fail.
| """
| with (yield SSHConnection('localhost')) as conn:
| print((yield conn(os.getpid)()))

| if **name** == '**main**\ ':
| main()
| ``Connection can marshal any pickle-able object, or `Sender` object plus any object which is reducible to set of pickle-able and `Sender` objects. `Proxy` and `Connection` itself are examples of such objects. You can also create proxy object from any arbitrary object with `proxify` or `proxify_func`.``\ python
| import os
| from pretzel.app import app
| from pretzel.remoting import SSHConnection, proxify

| class Remote(object):
| """Object which will be used remotely
| """
| def **init**\ (self):
| self.value = 0

| def next(self):
| self.value += 1
| return self.value

| def getpid(self):
| return os.getpid()

| @app
| def main():
| with (yield SSHConnection('localhost')) as conn:
| with (yield proxify(conn(Remote)())) as o: # remote object proxy
| print(os.getpid(), (yield o.getpid())) # prints two different pids
| print((yield o.next())) # prints 1
| print((yield o.next())) # prints 2

| if **name** == '**main**\ ':
| main()
| ``But `Cont` monad is not marshallable, that is why there is special operation on proxy object `~` which is equivalent to `yield` inside asynchronous function. Here is an example of remote execution of asynchronous function.``\ python
| from pretzel.app import app
| from pretzel.process import process\_call
| from pretzel.remoting import SSHConnection

| @app
| def main():
| """Execute 'ls' on remote machine and show result of the execution
| """
| with (yield SSHConnection('localhost')) as conn:
| out, err, code = yield ~conn(process\_call)('ls')
| print(out.decode())

| if **name** == '**main**\ ':
| main()
| ``There is also a way to work with multiple connections as if it one, by means of `composite_ssh_conn`. It accepts list of hosts and returns composite connection, which behaves as ordinary connection but returns set of results.``\ python
| import os
| from pretzel.app import app
| from pretzel.remoting import composite\_ssh\_conn

| @app
| def main():
| hosts = ['localhost', 'localhost']
| with (yield composite\_ssh\_conn(hosts)) as conns:
| result = yield conns(os.getpid)()
| print(result) # List(25163, 25162) - iterable object of pids

| if **name** == '**main**\ ':
| main()
| ``Remoting submodule can be used as workaround for python's GIL, in a similar fashion to `multiprocessing` module. You can use `ForkConnection` (or `composite_fork_conn`) which behaves as `SSHConnection` but instead of connecting via ssh, it just spawns new process.``\ python
| import time
| from pretzel.app import app
| from pretzel.remoting import composite\_fork\_conn

| def computation\_heavy\_task():
| """Some computation intensive task
| """
| start\_time = time.time()
| time.sleep(10)
| stop\_time = time.time()
| return int(stop\_time - start\_time)

| @app
| def main():
| with (yield composite\_fork\_conn(10)) as conns: # create 10
connections
| result = yield conns(computation\_heavy\_task)()
| print(result) # prints List(10, 10, 10, 10, 10, 10, 10, 10, 10, 10)

| if **name** == '**main**\ ':
| main()
| \`\`\`

Examples
--------

- `Simple echo server <https://gist.github.com/aslpavel/5635559>`__
- `Cat remote file over
ssh <https://gist.github.com/aslpavel/5635610>`__

.. |Build Status| image:: https://api.travis-ci.org/aslpavel/pretzel.png
:target: https://travis-ci.org/aslpavel/pretzel
.. |Coverage Status| image:: https://coveralls.io/repos/aslpavel/pretzel/badge.png?branch=master
:target: https://coveralls.io/r/aslpavel/pretzel?branch=master

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pretzel-1.0.7.tar.gz (81.9 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page