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
fac.py
fig1.png
fig2.png
fig3.png
fig4.png
fig5.png
fig6.png
fig7.png
fig8.png
foo.c
foo.o
foo.pxd
foo.pyx
foo.so
ind.gle
ind.png
minuit.html
minuit.ipynb
newtext.txt
osc.ipynb
p1
pandas.html
pandas.ipynb
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.ipynb
rpyc_old.ipynb
sh.ipynb
sympy.html
sympy.ipynb
tasks
text.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',
 'fac.py',
 'fig1.png',
 'fig2.png',
 'fig3.png',
 'fig4.png',
 'fig5.png',
 'fig6.png',
 'fig7.png',
 'fig8.png',
 'foo.c',
 'foo.o',
 'foo.pxd',
 'foo.pyx',
 'foo.so',
 'ind.gle',
 'ind.png',
 'minuit.html',
 'minuit.ipynb',
 'newtext.txt',
 'osc.ipynb',
 'p1',
 'pandas.html',
 'pandas.ipynb',
 '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.ipynb',
 'rpyc_old.ipynb',
 'sh.ipynb',
 'sympy.html',
 'sympy.ipynb',
 'tasks',
 'text.txt',
 'wrap.c',
 'wrap.o',
 'wrap.pyx',
 'wrap.so']

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

In [5]:
print(ls('-l','d1'))
итого 12
drwxr-xr-x 2 grozin grozin 4096 ноя  4 22:07 __pycache__
drwxr-xr-x 3 grozin grozin 4096 ноя  4 22:08 d2
-rw-r--r-- 1 grozin grozin   23 ноя  4 22:07 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 [8]:
chain=ll | grep['ipynb']
print(chain)
/bin/ls -l | /bin/grep ipynb

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

In [9]:
print(chain())
-rw-r--r-- 1 grozin grozin   18997 ноя 12 20:47 Untitled.ipynb
-rw-r--r-- 1 grozin grozin    1588 ноя 29 09:11 Untitled1.ipynb
-rw-r--r-- 1 grozin grozin     841 дек 13 15:35 Untitled2.ipynb
-rw-r--r-- 1 grozin grozin   16752 дек 28 12:01 Untitled3.ipynb
-rw-r--r-- 1 grozin grozin  216294 дек 25 23:29 minuit.ipynb
-rw-r--r-- 1 grozin grozin   10665 дек  3 20:08 osc.ipynb
-rw-r--r-- 1 grozin grozin  104639 дек 25 14:17 pandas.ipynb
-rw-r--r-- 1 grozin grozin   79081 ноя  1 17:03 python1.ipynb
-rw-r--r-- 1 grozin grozin   63539 дек  6 12:40 python2.ipynb
-rw-r--r-- 1 grozin grozin   60240 ноя  4 18:38 python3.ipynb
-rw-r--r-- 1 grozin grozin   67231 ноя  8 11:31 python4.ipynb
-rw-r--r-- 1 grozin grozin   86734 дек  5 17:39 python5.ipynb
-rw-r--r-- 1 grozin grozin  699573 дек  5 22:12 python6.ipynb
-rw-r--r-- 1 grozin grozin  339840 дек 19 10:57 python7.ipynb
-rw-r--r-- 1 grozin grozin   29969 дек 24 14:38 python8.ipynb
-rw-r--r-- 1 grozin grozin   16206 дек 14 12:42 root.ipynb
-rw-r--r-- 1 grozin grozin    8245 дек 28 12:29 rpyc.ipynb
-rw-r--r-- 1 grozin grozin   23252 дек 25 16:38 rpyc_old.ipynb
-rw-r--r-- 1 grozin grozin    6841 дек 25 15:38 sh.ipynb
-rw-r--r-- 1 grozin grozin  332000 дек 25 22:20 sympy.ipynb

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

In [10]:
chain=(grep['ab'] < 'newtext.txt') > 'text2.txt'
print(chain)
/bin/grep ab < newtext.txt > text2.txt

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

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

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

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

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

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

In [13]:
from plumbum import SshMachine
eeepc=SshMachine('192.168.0.105')

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

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 [21]:
print(chain())
mymodule.py

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

In [23]:
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 [29]:
eeepc=SshMachine('192.168.0.105')

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

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

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

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

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

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

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

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

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

In [35]:
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 [36]:
eee_file.close()

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

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

In [37]:
eee_path=eee.modules.sys.path
print(eee_path)
['/home/grozin/tmp.2a7XjB1Acb', '/home/grozin/tmp.2a7XjB1Acb', '/usr/lib/python34.zip', '/usr/lib/python3.4', '/usr/lib/python3.4/plat-linux', '/usr/lib/python3.4/lib-dynload', '/usr/lib/python3.4/site-packages']
In [38]:
eee_path.append('/home/grozin/rpyc')

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

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

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

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

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

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

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

Тогда

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

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

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

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

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

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

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

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

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

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

In [64]:
thr=rpyc.BgServingThread(eee)
res=async_f(3)
res.add_callback(callback)
In [65]:
n=1
In [66]:
n
Out[66]:
1
In [67]:
n+1
4
Out[67]:
2

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

In [68]:
thr.stop()

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

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

In [69]:
eee.close()

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

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

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

In [70]:
eeepc.close()