Взаимодействие на основе патчей. Работа с сетью
TODO Эта лекция закончилась за час — возможно, стоит питонью часть делать подробнее (не копипастить код коротких примеров, а писать эго в прямом эфире)?
Ещё про работу с историей
Немного лайфхаков:
Отложенные правки: git-stash (Статья на Skillbox)
Потерянные и случайно убитые коммиты: git-reflog
Адресация истории от последнего (подписанного) тега: git-describe
Применение изменений из произвольных коммитов: git-cherry-pick
Статья на Хабре про стратегии git merge
…
Работа с патчами и наборами патчей
Немного о формате
(в частности, diff -u)
- понятие контекста 
- fuzzy контекст
 Примеры работы patch, в том числе при нечётком контексте
TODO переделать попроще
 BTW, [g]vimdiff и прочие mergetool-ы умеют распознавать синтаксис patch-файла
Патчи и Git:
git-format-patch и git-am / git-apply
Замечание: git не умеет в fuzzy (и правильно!)
⇒ иногда уместнее patch -u или patch --git
- Серия коммитов ⇒ серия патчей, условная нестрогость порядка
 
Патч или набор с точки зрения GIT — это сериализация коммитов, превращение их в пригодный для передачи формат.
TODO Пример взаимодействия: сделать format-patch, «переслать», применить к другому репозиторию
BTW: difflib
Простейший сетевой сервер
Простейший низкоуровневый сервер с помощью socket
   1 import socket
   2 import sys
   3 
   4 host = "localhost" if len(sys.argv) < 2 else sys.argv[1]
   5 port = 1337 if len(sys.argv) < 3 else int(sys.argv[2])
   6 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
   7     s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
   8     s.bind((host, port))
   9     s.listen()
  10     conn, addr = s.accept()
  11     with conn:
  12         print('Connected by', addr)
  13         while data := conn.recv(1024):
  14             conn.sendall(data.swapcase())
Флаг SO_REUSEADDR устанавливается для того, чтобы система освободила сокет немедленно после его закрытия. В некоторых случаях (TODO каких?) она может так не делать.
Главный недостаток: в каждый момент обрабатывает не более одного подключения
В действительности всю логику после установления соединения (s.accept()) надо выполнять в отдельном процессе / / потоке / корутине:
   1 import socket
   2 import sys
   3 import multiprocessing
   4 
   5 def serve(conn, addr):
   6     with conn:
   7       print('Connected by', addr)
   8       while data := conn.recv(1024):
   9             conn.sendall(data.swapcase())
  10 
  11 host = "localhost" if len(sys.argv) < 2 else sys.argv[1]
  12 port = 1337 if len(sys.argv) < 3 else int(sys.argv[2])
  13 with socket.create_server((host, port)) as s:
  14     s.listen()
  15     while True:
  16         conn, addr = s.accept()
  17         multiprocessing.Process(target=serve, args=(conn, addr)).start()
Тупой аналог netcat с помощью socket:
   1 import sys
   2 import socket
   3 
   4 host = "localhost" if len(sys.argv) < 2 else sys.argv[1]
   5 port = 1337 if len(sys.argv) < 3 else int(sys.argv[2])
   6 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
   7     s.connect((host, port))
   8     while msg := sys.stdin.buffer.readline():
   9         s.sendall(msg)
  10         print(s.recv(1024).rstrip().decode())
Обязателен «ответ» после «запроса» (send() и receive() чередуются, именно поэтому в большинстве старых протоколов так принято делать!)
В общем случае программа сильно сложнее. Мы будем пользоваться netcat, netcat или ntsh
Asyncio Streams
Недостаток threading — необходимость следить за thread safe и синхронизация по доступу к данным; недостаток multiprocessing — требует отдельной дисциплины по передаче обработанных данных из процесса в процесс.
Выход: asyncio. Посмотрим в примеры. В частности, echo-сервер:
   1 import asyncio
   2 
   3 async def echo(reader, writer):
   4     while data := await reader.readline():
   5         writer.write(data.swapcase())
   6     writer.close()
   7     await writer.wait_closed()
   8 
   9 async def main():
  10     server = await asyncio.start_server(echo, '0.0.0.0', 1337)
  11     async with server:
  12         await server.serve_forever()
  13 
  14 asyncio.run(main())
Это asyncio (вспоминаем):
await вместо yield from и в целом Python async
явный yield выбрасывает корутину в скрытый управляющий цикл,
В примере тоже строго чередуются send() и receive()
- Сервер принимает несколько TCP-соединений 
- обрабатываются они не в отдельных тредах (и не в отдельных процессах), а асинхронно в одном треде 
- ⇒ любое исключение в одной из запущенных корутин останавливает весь сервер
 - → Из двух зол выбрано меньшее: все исключения молча игнорируются
 
 
 - обрабатываются они не в отдельных тредах (и не в отдельных процессах), а асинхронно в одном треде 
 
Используем Streams для написания «общего чата».
- Входящий поток от одного соединения сервер ретранслирует на все остальные 
- Поскольку нет многопоточности, гонки могут возникнуть только на неатомарных операциях
 
 Например, stream — это последовательность байтов, и мы используем .readline() для того, чтобы считывать из неё по одной строке. А вот клиент, использующий socket.recv(1024), примет несколько строк разом. 
 Ретрансляцию сделаем с помощью asyncio.Queue для каждого клиента
ID клиента — это IP+порт (.get_extra_info('peername'))
- Нет простого способа узнать, есть ли входящие данные в Stream 
⇒ асинхронно запустим send() (из Stream) и receive() (из Queue), какой успеет первым, такой и обработаем
 для планировщика asyncio нужно обмазать корутину Task-ом (заданием), иначе:
DeprecationWarning: The explicit passing of coroutine objects to asyncio.wait() is deprecated since Python 3.8, and scheduled for removal in Python 3.11.
- При отключении клиента остановим оба необработанных Task-а (иначе они останутся в планировщике)
 
   1 #!/usr/bin/env python3
   2 import asyncio
   3 
   4 clients = {}
   5 
   6 async def chat(reader, writer):
   7     me = "{}:{}".format(*writer.get_extra_info('peername'))
   8     print(me)
   9     clients[me] = asyncio.Queue()
  10     send = asyncio.create_task(reader.readline())
  11     receive = asyncio.create_task(clients[me].get())
  12     while not reader.at_eof():
  13         done, pending = await asyncio.wait([send, receive], return_when=asyncio.FIRST_COMPLETED)
  14         for q in done:
  15             if q is send:
  16                 send = asyncio.create_task(reader.readline())
  17                 for out in clients.values():
  18                     if out is not clients[me]:
  19                         await out.put(f"{me} {q.result().decode().strip()}")
  20             elif q is receive:
  21                 receive = asyncio.create_task(clients[me].get())
  22                 writer.write(f"{q.result()}\n".encode())
  23                 await writer.drain()
  24     send.cancel()
  25     receive.cancel()
  26     print(me, "DONE")
  27     del clients[me]
  28     writer.close()
  29     await writer.wait_closed()
  30 
  31 async def main():
  32     server = await asyncio.start_server(chat, '0.0.0.0', 1337)
  33     async with server:
  34         await server.serve_forever()
  35 
  36 asyncio.run(main())
Д/З
Почитать про asyncio
…в частности про разработку и отладку для asyncio
Про читать про Streams и поупражняться в них
- Превратить «общий чат» в «коровий» следующим образом: 
- Вводимые строки состоят из команды с возможными параметрами
 Вместо get_extra_info('peername') уникальным идентификатором пользователя является название коровы из python-cowsay, под которым он регистрируется
- Пока пользователь не зарегистрировался, он не имеет право ни писать, ни получать сообщения
 
Сообщения оформляются с помощью cowsay() из модуля python-cowsay
- Команды: 
who — просмотр зарегистрированных пользователей
cows — просмотр свободных имён коров
login название_коровы — зарегистрироваться под именем название_коровы
say название_коровы текст сообщения — послать сообщение пользователю название_коровы
yield текст сообщения — послать сообщение всем зарегистрированным пользователям
quit — отключиться
 
 Вместо клиента можно пользоваться либо системным netcat, либо скриптом из простого модуля netcat, либо чуть более продвинутым ntsh
Разработку вести согласно дисциплине оформления коммитов в подкаталоге 05_DiffPatchNet отчётного репозитория по Д/З
Предполагается, что модуль python-cowsay устанавливается в окружение с помощью pipenv, в каталоге должен присутствовать соответствующий Pipfile
