plumbum

или как писать shell-скрипты на питоне

Работа на локальной машине

In [1]:
from plumbum import local

Из объекта local можно получать питонские объекты, представляющие внешние программы.

In [2]:
ls=local["ls"]
print(ls)
/bin/ls

Их можно вызывать.

In [3]:
print(ls())
C1.pyx
C2.pyx
C3.pyx
Untitled.ipynb
Untitled1.ipynb
Untitled2.ipynb
Untitled3.ipynb
Zskim.root
__pycache__
cfib.c
cfib.h
cfib.o
cfoo.c
cfoo.h
cfoo.o
d1
du
fac.py
foo.c
foo.o
foo.pxd
foo.pyx
foo.so
google-python-exercises
ind.gle
ind.png
iterators_generators.ipynb
minuit.html
minuit.ipynb
mpmath.html
mpmath.ipynb
newtext.txt
osc.ipynb
p1
pandas.html
pandas.ipynb
poster
python.png
python0.png
python1.html
python1.ipynb
python2.html
python2.ipynb
python3.html
python3.ipynb
python4.html
python4.ipynb
python5.html
python5.ipynb
python6.html
python6.ipynb
python7.html
python7.ipynb
python8.html
python8.ipynb
root.ipynb
rpyc.html
rpyc.ipynb
rpyc_old.ipynb
sh.ipynb
sympy.html
sympy.ipynb
tasks
text.txt
text2.txt
wrap.c
wrap.o
wrap.pyx
wrap.so

Они возвращают строки, которые можна присваивать переменным или ещё как-то использовать.

In [4]:
s=ls()
s.split()
Out[4]:
['C1.pyx',
 'C2.pyx',
 'C3.pyx',
 'Untitled.ipynb',
 'Untitled1.ipynb',
 'Untitled2.ipynb',
 'Untitled3.ipynb',
 'Zskim.root',
 '__pycache__',
 'cfib.c',
 'cfib.h',
 'cfib.o',
 'cfoo.c',
 'cfoo.h',
 'cfoo.o',
 'd1',
 'du',
 'fac.py',
 'foo.c',
 'foo.o',
 'foo.pxd',
 'foo.pyx',
 'foo.so',
 'google-python-exercises',
 'ind.gle',
 'ind.png',
 'iterators_generators.ipynb',
 'minuit.html',
 'minuit.ipynb',
 'mpmath.html',
 'mpmath.ipynb',
 'newtext.txt',
 'osc.ipynb',
 'p1',
 'pandas.html',
 'pandas.ipynb',
 'poster',
 'python.png',
 'python0.png',
 'python1.html',
 'python1.ipynb',
 'python2.html',
 'python2.ipynb',
 'python3.html',
 'python3.ipynb',
 'python4.html',
 'python4.ipynb',
 'python5.html',
 'python5.ipynb',
 'python6.html',
 'python6.ipynb',
 'python7.html',
 'python7.ipynb',
 'python8.html',
 'python8.ipynb',
 'root.ipynb',
 'rpyc.html',
 'rpyc.ipynb',
 'rpyc_old.ipynb',
 'sh.ipynb',
 'sympy.html',
 'sympy.ipynb',
 'tasks',
 'text.txt',
 'text2.txt',
 'wrap.c',
 'wrap.o',
 'wrap.pyx',
 'wrap.so']

Можно вызывать их с аргументами.

In [5]:
print(ls('-l','d1'))
итого 12
drwxr-xr-x 2 grozin grozin 4096 ноя  4  2015 __pycache__
drwxr-xr-x 3 grozin grozin 4096 ноя  4  2015 d2
-rw-r--r-- 1 grozin grozin   23 ноя  4  2015 m1.py

Следующая строчка означает в точности то же самое, что

cat=local['cat']
grep=local['grep']

(модуль plumbum.cmd использует чёрную магию для переопределения импорта из него).

In [6]:
from plumbum.cmd import cat,grep

Это объект, представляющий внешнюю программу с привязанными аргументами.

In [7]:
ll=ls['-l']
print(ll)
/bin/ls -l
In [9]:
print(ll())
итого 17856
-rw-r--r-- 1 grozin grozin     290 дек 10  2015 C1.pyx
-rw-r--r-- 1 grozin grozin     208 дек 10  2015 C2.pyx
-rw-r--r-- 1 grozin grozin     310 дек 10  2015 C3.pyx
-rw-r--r-- 1 grozin grozin   18997 ноя 12  2015 Untitled.ipynb
-rw-r--r-- 1 grozin grozin    1588 ноя 29  2015 Untitled1.ipynb
-rw-r--r-- 1 grozin grozin     841 дек 13  2015 Untitled2.ipynb
-rw-r--r-- 1 grozin grozin   16752 дек 28  2015 Untitled3.ipynb
-rw-r--r-- 1 grozin grozin 9231391 дек 13  2015 Zskim.root
drwxr-xr-x 2 grozin grozin    4096 ноя  4  2015 __pycache__
-rw-r--r-- 1 grozin grozin     157 дек  6  2015 cfib.c
-rw-r--r-- 1 grozin grozin      19 дек  6  2015 cfib.h
-rw-r--r-- 1 grozin grozin    1312 ноя 16 19:08 cfib.o
-rw-r--r-- 1 grozin grozin     243 дек 10  2015 cfoo.c
-rw-r--r-- 1 grozin grozin     130 дек 10  2015 cfoo.h
-rw-r--r-- 1 grozin grozin    1752 ноя 16 19:45 cfoo.o
drwxr-xr-x 4 grozin grozin    4096 ноя  4  2015 d1
drwxr-xr-x 3 grozin grozin    4096 окт  9 21:13 du
-rwxr-xr-x 1 grozin grozin     401 ноя  4  2015 fac.py
-rw-r--r-- 1 grozin grozin   85006 ноя 16 19:45 foo.c
-rw-r--r-- 1 grozin grozin   21512 ноя 16 19:45 foo.o
-rw-r--r-- 1 grozin grozin     164 дек 10  2015 foo.pxd
-rw-r--r-- 1 grozin grozin     282 дек 10  2015 foo.pyx
-rwxr-xr-x 1 grozin grozin   24200 ноя 16 19:45 foo.so
drwxr-xr-x 6 grozin grozin    4096 сен 28 16:50 google-python-exercises
-rw-r--r-- 1 grozin grozin     630 окт 11  2015 ind.gle
-rw-r--r-- 1 grozin grozin    3945 окт 12  2015 ind.png
-rw-r--r-- 1 grozin grozin   11154 ноя  2 16:25 iterators_generators.ipynb
-rw-r--r-- 1 grozin grozin  421451 дек 25  2015 minuit.html
-rw-r--r-- 1 grozin grozin  294985 ноя  9 17:37 minuit.ipynb
-rw-r--r-- 1 grozin grozin  543403 ноя  2 20:51 mpmath.html
-rw-r--r-- 1 grozin grozin  266855 ноя  2 19:39 mpmath.ipynb
-rw-r--r-- 1 grozin grozin      15 ноя  8  2015 newtext.txt
-rw-r--r-- 1 grozin grozin   10665 дек  3  2015 osc.ipynb
drwxr-xr-x 4 grozin grozin    4096 ноя  8  2015 p1
-rw-r--r-- 1 grozin grozin  299444 дек 25  2015 pandas.html
-rw-r--r-- 1 grozin grozin  130941 ноя  2 18:51 pandas.ipynb
drwxr-xr-x 2 grozin grozin    4096 авг 29 15:48 poster
-rw-r--r-- 1 grozin grozin   67230 окт 11  2015 python.png
-rw-r--r-- 1 grozin grozin  146956 окт 10  2015 python0.png
-rw-r--r-- 1 grozin grozin  371477 сен 17 23:16 python1.html
-rw-r--r-- 1 grozin grozin   81013 сен 21 19:23 python1.ipynb
-rw-r--r-- 1 grozin grozin  373780 сен 28 16:20 python2.html
-rw-r--r-- 1 grozin grozin   68480 сен 28 22:18 python2.ipynb
-rw-r--r-- 1 grozin grozin  295939 ноя  5  2015 python3.html
-rw-r--r-- 1 grozin grozin   47236 окт 22 10:51 python3.ipynb
-rw-r--r-- 1 grozin grozin  291364 ноя  5  2015 python4.html
-rw-r--r-- 1 grozin grozin   58657 окт 12 19:46 python4.ipynb
-rw-r--r-- 1 grozin grozin  349899 дек  5  2015 python5.html
-rw-r--r-- 1 grozin grozin  101188 окт 19 19:47 python5.ipynb
-rw-r--r-- 1 grozin grozin  919499 дек  5  2015 python6.html
-rw-r--r-- 1 grozin grozin 1018131 окт 26 19:37 python6.ipynb
-rw-r--r-- 1 grozin grozin  390422 дек 19  2015 python7.html
-rw-r--r-- 1 grozin grozin  730517 ноя 16 18:48 python7.ipynb
-rw-r--r-- 1 grozin grozin  240728 дек 24  2015 python8.html
-rw-r--r-- 1 grozin grozin   30472 ноя 16 19:49 python8.ipynb
-rw-r--r-- 1 grozin grozin   16206 дек 14  2015 root.ipynb
-rw-r--r-- 1 grozin grozin  243066 дек 28  2015 rpyc.html
-rw-r--r-- 1 grozin grozin   26991 ноя 23 18:36 rpyc.ipynb
-rw-r--r-- 1 grozin grozin   23252 дек 28  2015 rpyc_old.ipynb
-rw-r--r-- 1 grozin grozin    7492 ноя 23 18:31 sh.ipynb
-rw-r--r-- 1 grozin grozin  378705 дек 25  2015 sympy.html
-rw-r--r-- 1 grozin grozin  332000 дек 25  2015 sympy.ipynb
drwxr-xr-x 3 grozin grozin    4096 дек  3  2015 tasks
-rw-r--r-- 1 grozin grozin      15 окт 11  2015 text.txt
-rw-r--r-- 1 grozin grozin       5 ноя 23 15:09 text2.txt
-rw-r--r-- 1 grozin grozin   73021 ноя 16 19:11 wrap.c
-rw-r--r-- 1 grozin grozin   16216 ноя 16 19:11 wrap.o
-rw-r--r-- 1 grozin grozin      86 дек  6  2015 wrap.pyx
-rwxr-xr-x 1 grozin grozin   19048 ноя 16 19:11 wrap.so

Из таких объектов можно строить цепочки.

In [10]:
chain=ll | grep['ipynb']
print(chain)
/bin/ls -l | /bin/grep ipynb

Цепочки можно вызывать.

In [11]:
print(chain())
-rw-r--r-- 1 grozin grozin   18997 ноя 12  2015 Untitled.ipynb
-rw-r--r-- 1 grozin grozin    1588 ноя 29  2015 Untitled1.ipynb
-rw-r--r-- 1 grozin grozin     841 дек 13  2015 Untitled2.ipynb
-rw-r--r-- 1 grozin grozin   16752 дек 28  2015 Untitled3.ipynb
-rw-r--r-- 1 grozin grozin   11154 ноя  2 16:25 iterators_generators.ipynb
-rw-r--r-- 1 grozin grozin  294985 ноя  9 17:37 minuit.ipynb
-rw-r--r-- 1 grozin grozin  266855 ноя  2 19:39 mpmath.ipynb
-rw-r--r-- 1 grozin grozin   10665 дек  3  2015 osc.ipynb
-rw-r--r-- 1 grozin grozin  130941 ноя  2 18:51 pandas.ipynb
-rw-r--r-- 1 grozin grozin   81013 сен 21 19:23 python1.ipynb
-rw-r--r-- 1 grozin grozin   68480 сен 28 22:18 python2.ipynb
-rw-r--r-- 1 grozin grozin   47236 окт 22 10:51 python3.ipynb
-rw-r--r-- 1 grozin grozin   58657 окт 12 19:46 python4.ipynb
-rw-r--r-- 1 grozin grozin  101188 окт 19 19:47 python5.ipynb
-rw-r--r-- 1 grozin grozin 1018131 окт 26 19:37 python6.ipynb
-rw-r--r-- 1 grozin grozin  730517 ноя 16 18:48 python7.ipynb
-rw-r--r-- 1 grozin grozin   30472 ноя 16 19:49 python8.ipynb
-rw-r--r-- 1 grozin grozin   16206 дек 14  2015 root.ipynb
-rw-r--r-- 1 grozin grozin   32413 ноя 23 18:38 rpyc.ipynb
-rw-r--r-- 1 grozin grozin   23252 дек 28  2015 rpyc_old.ipynb
-rw-r--r-- 1 grozin grozin    7492 ноя 23 18:31 sh.ipynb
-rw-r--r-- 1 grozin grozin  332000 дек 25  2015 sympy.ipynb

Можно использовать перенаправления ввода-вывода.

In [12]:
!cat newtext.txt
abcd
efgh
ijkl
In [13]:
chain=(grep['ab'] < 'newtext.txt') > 'text2.txt'
print(chain)
/bin/grep ab < newtext.txt > text2.txt

(скобки здесь обязательны)

In [14]:
chain()
print(cat('text2.txt'))
abcd

Если нужно послать текст в stdin внешней программы, используется оператор <<.

In [15]:
print((grep['ab'] << 'xxx\nabc\nyyy\n')())
abc

Работа на удалённой машине

Допустим, в локальной сети есть машина eeepc (192.168.0.105), доступная по ssh.

In [16]:
ip='192.168.43.95'
In [17]:
from plumbum import SshMachine
eeepc=SshMachine(ip)

Теперь мы можем выполнять на ней команды.

In [18]:
eeepc_ls=eeepc['ls']
print(eeepc_ls)
/bin/ls
In [19]:
print(eeepc_ls('rpyc'))
__pycache__
mymodule.py
rpyc.txt

Можно строить цепочки из локальных и удалённых команд.

In [20]:
chain=eeepc_ls['rpyc'] | grep[r'\.py']
print(chain)
/bin/ls rpyc | /bin/grep '\.py'
In [22]:
chain
Out[22]:
Pipeline(BoundCommand(RemoteCommand(<SshMachine ssh://192.168.43.95>, <RemotePath /bin/ls>), ['rpyc']), BoundCommand(LocalCommand(/bin/grep), ['\\.py']))
In [21]:
print(chain())
mymodule.py

In [23]:
eeepc_grep=eeepc['grep']
print((eeepc_grep['ab'] < 'newtext.txt')())
abcd

In [24]:
print((eeepc_grep['ab'] << 'xxx\nabc\nyyy\n')())
abc

In [25]:
print((cat['newtext.txt'] | eeepc_grep['ab'])())
abcd

Теперь закроем связь с eeepc.

In [26]:
eeepc.close()

Сеанс работы с eeepc удобно записать в виде

with SshMachine('192.168.0.105') as eeeps:
    eeepc['ls']()
    # и так далее

RPyC

Remote Python Call http://rpyc.sourceforge.net/ - очень простой пакет для организации распределённых вычислений. Он работает на любой платформе, где есть питон, так что Вы можете объединить в вычислительный кластер всё, что подвернётся под руку - Linux, Windows, Mac, хоть свой телефон.

На всех компьютерах, которые мы хотим использовать, запускаются rpyc серверы. Клиент обращается к ним и поручает выполнить какую-нибудь работу. Есть два типа серверов - классический (или slave-сервер) и современный. Классический сервер может (по поручению клиента) делать всё, что может делать интерпретатор питон (от имени того пользователя, который запустил сервер). Он не производит аутентификацию клиента. Поэтому его можно запускать в защищённой локальной сети (возможно виртуальной), или он должен принимать соединения только с localhost (а другие машины получают к нему доступ через ssh туннели. В отличие от классического сервера (который поставляется в пакете RPyC), современный сервер должен быть написан пользователем под конкретную задачу. Он предоставляет клиентам некоторый набор сервисов; клиенты могут вызывать их. Если эти сервисы безопасны, такой сервер может работать и в открытом интернете.

Есть более простой способ использования RPyC, который даже не требует, чтобы этот пакет был установлен на всех машинах - достаточно иметь его на клиентской (локальной) машине. Установим ssh связь с удалённой машиной.

In [27]:
eeepc=SshMachine(ip)

Функция DeployedServer передаёт на eeepc нужные исходные тексты, запускает там классический rpyc сервер, принимающий соединения только с локальной машины, и создаёт ssh туннель на eeepc.

In [28]:
import rpyc
from rpyc.utils.zerodeploy import DeployedServer
server=DeployedServer(eeepc)

Теперь мы можем установить связь с этим сервером.

In [29]:
eee=server.classic_connect()

Теперь я могу выполнять различные действия на eeepc:

In [30]:
eee.execute('n=2')
In [31]:
eee.eval('n+1')
Out[31]:
3

Я могу использовать встроенные функции питона.

In [32]:
eee_file=eee.builtins.open('/home/grozin/rpyc/mymodule.py')

В отличие от простых неизменяемых объектов (чисел, строк и т.д.), которые передаются между машинами по значению, изменяемые объекты передаются по ссылке. То есть на eeepc создался файловый объект; на локальной машине создалась сетевая ссылка на него - прокси-объект eee_file. Мы можем производить над ним любые действия, доступные для файлового объекта. Все они переадресутся объекту на eeepc.

In [33]:
print(eee_file.read())
#!/usr/bin/env python
from time import sleep

class MyClass:

    def __init__(self,t):
        self.t=t

    def f(self,n):
        sleep(self.t)
        return n+1

In [34]:
eee_file.close()

Этот прокси-объект можно подставить в любую программу, ожидающую иметь файловый объект. По принципу утиной типизации этот объект - файл.

Я могу использовать функции и прочие объекты из библиотечных модулей питона на eeepc:

In [35]:
eee_path=eee.modules.sys.path
print(eee_path)
['/home/grozin/tmp.hK717huHzN', '/home/grozin/tmp.hK717huHzN', '/usr/lib/python35.zip', '/usr/lib/python3.5', '/usr/lib/python3.5/plat-linux', '/usr/lib/python3.5/lib-dynload', '/usr/lib/python3.5/site-packages']
In [36]:
eee_path.append('/home/grozin/rpyc')

eee_path - это прокси-объект для sys.path на eeepc; любые изменения этого объекта сразу передаются туда. Теперь, расширив path, я могу использовать файл из этой директории на eeepc:

In [37]:
eee_object=eee.modules.mymodule.MyClass(0)
In [38]:
eee_object.f(3)
Out[38]:
4

eee_object - это прокси-объект (сетевая ссылка) для объекта класса MyClass на машине eeepc. Его метод f прибавляет 1 к аргументу; чтобы у нас была возможность моделировать длительные вычисления, он это делает за t секунд, где t - атрибут этого объекта.

In [39]:
eee_object.t=2
eee_object.f(4)
Out[39]:
5

Теперь нам пришлось ждать 2 секунды.

Можно передавать удалённым функциям в качестве параметров любые объекты, в частности, локальные функции. Определим

In [40]:
def loc(n):
    print('loc',n)
    return n+1

Тогда

In [41]:
list(eee.builtins.map(loc,[1,2,3]))
loc 1
loc 2
loc 3
Out[41]:
[2, 3, 4]

То есть функция map на eeepc на каждом шаге вызывает функцию loc на локальной машине (callback).

Всё, что мы до сих пор обсуждали, несомненно, красиво - разные объекты могут жить на разных машинах, и единая программа работает с ними, не замечая этого. Но все эти операции синхронные - одна машина просит другую что-то сделать и ждёт, когда та вернёт ей результат. Для организации распределённых вычислений нужны асинхронные операции:

In [42]:
eee_object.t=10
async_f=rpyc.async(eee_object.f)
res=async_f(1)
res.ready
Out[42]:
False
In [43]:
res.ready
Out[43]:
False
In [45]:
res.ready
Out[45]:
True
In [46]:
res.value
Out[46]:
2

Это уже лучше. Клиент может время от времени спрашивать, готов ли результат, и когда он будет готов, забрать его. Если запросить res.value когда результат ещё не готов, то клиент блокируется до момента, когда он будет готов:

In [47]:
res=async_f(2)
res.value
Out[47]:
3

(после res.value 10 секунд ожидания, потом появляется ответ).

Но ещё лучше определить callback-функцию, которая будет вызвана на локальной машине, когда результат будет готов:

In [48]:
def callback(res):
    print(res.value)

Эта функция может быть вызвана только в отдельном thread-е:

In [49]:
thr=rpyc.BgServingThread(eee)
res=async_f(3)
res.add_callback(callback)
In [50]:
n=1
In [51]:
n
Out[51]:
1
In [52]:
n+1
Out[52]:
2
4

Это печать из функции callback из другого thread-а. Теперь это thread можно и остановить.

In [53]:
thr.stop()

Например, на клиентской машине может работать графический пользовательский интерфейс (на питоне легко написать такой интерфейс, причём он будет работать на любой платформе - Linux, Windows, Mac - без малейших изменений в программе). Эта клиентская программа обращается к нескольким мощным серверам для проведения длинных вычислений, и регистрирует callback функции, которые, наприер, добавляют очередную точку на график.

Наконец, закроем связь с машиной eeepc:

In [54]:
eee.close()

Сеанс связи с rpyc сервером удобно записывать как

with server.classic_connection() as eee:
    eee.execute('n=2')
    # и так далее

Наконец, закроем ssh связь с eeepc.

In [55]:
eeepc.close()
In [ ]: