Cron i Gnome

Pod koniec dnia pracy mam potrzebę przesłać sobie katalog roboczy na serwer. Niby proste. Można użyć scp, można użyć rsync, tylko trzeba o tym pamiętać, a zdarza mi się zapomnieć.

Skoro taki jestem zapominalski, to zrobiłem sobie crony.

16 16 * * 1-5 /usr/bin/rsync -vaxAXHSz --delete /home/lukasz/wazny_katalog/ login@domena.pl:/home/lukasz/dane/wazny_katalog/ &> /home/lukasz/programy/rsync/rsync.log

19 16 * * 1-5 cd /home/lukasz/programy/rsync ; /usr/bin/python3 powiadomienie.py &>/dev/null

Cron z godziny 16:16 to jest jeszcze proste. Rsync kopiuje (lub nie, jak mu coś nie wyjdzie) zawartość katalogu. Komunikaty zapisuje w pliku rsync.log.

O godzinie 16:19 do akcji wchodzi skrypt Pythona, który uruchamia komunikaty pulpitu Gnome.

#!/usr/bin/env python3

from gi.repository import Notify
Notify.init('Rsync plików')

with open('rsync.log','r') as rsync_log:
  rsync_log_txt = rsync_log.read()
  if 'failed' not in rsync_log_txt:
    Notify.Notification.new('Przesyłanie plików OK').show()
  else:
    Notify.Notification.new('BŁĄD PRZESYŁANIA plików').show()

Rsync ma to do siebie, że nieudane przesyłanie plików opatruje komunikatem zawierającym słowo kluczowe „failed”, jaki ten błąd by nie był. Jeżeli przesyłanie plików pójdzie dobrze, a tak zazwyczaj jest, pojawi się komunikat „Przesyłanie plików OK”. Jeżeli tak się nie stanie i zobaczę „BŁĄD PRZESYŁANIA PLIKÓW”, muszę powtórzyć rsynca ręcznie w terminalu i tyle. Może i to taka pierdoła, ale od tego w końcu komputer jest, żeby o nich pamiętać.

Mała strona z Pythonem

Ostatnio dłubię sobie stronę do obsługi programu javowego MegaMek. Jest tutaj: https://github.com/seem8/astech. Oczywiście pierwszym problemem było: w jakim pythonowym frameworku to zrobić? Jest Django, jest Flask, jest Bottle, jest Cherrypy, jest Pyramid i jeszcze jakieś 20 działających.

Zauważyłem, że są generalnie dwie kategorie stron internetowych:

  • operujące na dużych ilościach danych,
  • robiące coś fajnego.

Najistotniejsze w pierwszej kategorii jest utrzymanie spójności danych i bardzo szybkie na nich operowanie. Mamy kolekcję rzeczy, albo sklep, albo terminarz, albo bardzo dużo plików, albo coś takiego. I wówczas lepiej sprawdzają się duże frameworki, które pozwalają na tym się właśnie skupić, upraszczające tworzenie formularzy, wyszukiwarek, itp…

W drugiej kategorii ważne jest pozostawienie programiście swobody i generalnie zostawienie go w spokoju. Zauważyłem, że bardzo to lubię. Dlaczego? Bo można po prostu programować w Pythonie i nie przejmować się w ogóle faktem, że będzie to obsługiwane przez przeglądarkę.

Przedstawiam małe demo, które obrazuje powyższe:

#!/usr/bin/env python3

def read_logs(log_file):

  try:
    open(log_file,'r').close()
  except FileNotFoundError:
    open(log_file,'w').close()
    
  with open(log_file,'r') as myfile:
    mylines = myfile.readlines()
    lastlog = mylines[len(mylines)-50 : len(mylines)]
    lastlog.reverse()

    return lastlog

Powiedzmy, że mamy coś takiego. Zwykły kod, który działa wszędzie. Skorzystajmy więc z tej funkcji w programie tekstowym:

#!/usr/bin/env python3

def read_logs(log_file):

  try:
    open(log_file,'r').close()
  except FileNotFoundError:
    open(log_file,'w').close()

  with open(log_file,'r') as myfile:
    mylines = myfile.readlines()
    lastlog = mylines[len(mylines)-50 : len(mylines)]
    lastlog.reverse()

    return lastlog


important_logs = read_logs('journal.txt')

for i in important_logs:
  print(i)

Znowu nic niezwykłego. Wczytałem fragment logów systemowych i wypisałem 50 ostatnich linijek, korzystając z funkcji read_logs. Teraz zróbmy tak samo, ale żeby logi pokazywały się na stronie interenetowej.

#!/usr/bin/env python3

from bottle import template, route, run
def read_logs(log_file):

  try:
    open(log_file,'r').close()
  except FileNotFoundError:
    open(log_file,'w').close()

  with open(log_file,'r') as myfile:
    mylines = myfile.readlines()
    lastlog = mylines[len(mylines)-50 : len(mylines)]
    lastlog.reverse()

    return lastlog

@route('/')
def index_page():
  important_logs = read_logs('journal.txt')
  return template('index_page', logs=important_logs)

run(host='localhost', port=8080)

Tyle Pythona. Bottle używa dekoratorów, żeby określić jaki rodzaj funkcji deklarujemy: get, post, route, czy jeszcze jakiś inny. Jako „parametr” dekoratora wpisujemy ścieżkę, którą wpiszemy do przeglądarki. W tym wypadku po wpisaniu w pasek adresu localhost:8080 pokażą się nasze logi. Template potrzebuje co najmniej jednej informacji: jak ma się nazywać plik z końcówką .tpl, w którym będzie kod html. Oprócz tego możemy przekazać dowolną ilość argumentów, które pozwolą szablonowi na dostęp do naszych zmiennych oraz funkcji. Zmienna jest dla szablonu, a wartość to coś z naszego kodu Pythona, którą może być wszystko: zmienna, funkcja z parametrami, egzemplarz klasy, czy cokolwiek sobie umyślimy.

Musi to być jeszcze zrozumiałe dla przeglądarki, więc mamy do tego, wspomniany w kodzie, osobny plik index_page.tpl:

<html>
<head>
<title>Important Logs</title>
<head>
<body>

% for i in logs:
<p>{{i}}</p>
% end

</body>
</html>

Jest tutaj odrobina magicznych rzeczy:

  • znakiem % rozpoczynamy linijkę zawierającą kod Pythona,
  • na zmiennych w kodzie html operujemy poprzez konstrukcję {{zmienna}},
  • jako, że html ignoruje białe znaki, a Python ich właśnie używa do „kończenia” instrukcji for/if/def/itd…, korzystamy ze słowa kluczowego end, żeby wyjść z pętli.

Ścieżką może to być ‚/’, ‚/login’, albo ‚/read_logs/<log_file>’, co pozwala na przekazywanie argumentów na poziomie hiperłączy. Mógłbym więc rozpisać funkcję index_page w ten sposób:

@route('/read_logs/<log_file>')
def index_page(log_file):
  important_logs = read_logs(log_file)
  return template('index_page', logs=important_logs)

Wówczas łącze do przeglądarki wyglądałoby tak: http://localhost:8080/read_logs/journal.txt. Oczywiście jeżeli pozostawiamy cokolwiek użytkownikowi, zwłaszcza w internecie, trzeba się odpowiednio zabezpieczyć, ale o ciasteczkach i logowaniu napiszę innym razem.

Jeżeli ktoś widział kiedyś kod Django, to zapewne czuje już różnicę. To jest bardzo, bardzo proste. Pozwala mi skupić się na logice programu, który czasem robi dziwne rzeczy i nie stawia mi na drodze plików konfiguracyjnych oraz mnóstwa modułów. W dużym programie prędzej czy później taka swoboda odbija się czkawką (i poczułem to na sobie), ale w małym programie, tak do 1000 wierszy, robi świetną robotę.

Baszarek 6 – Wsteczna Refaktoryzacja

Zapraszam do posłuchania o programiście, który chciał za dużo na raz i jak się uratować z tej sytuacji. Może to też sposób dla Ciebie, może nie, ale mi pomogło i jestem ponownie szczęśliwym człowiekiem z projektem, który po długiej przerwie (a właściwie zatoczywszy duże koło) ruszył do przodu.

ściągnij mp3

Nie ma Pythona 2

Szukając Pythona do ściągnięcia ze strony Python.org trafiłem na ciekawostkę:

A gdzie 2? 🙁

nie ma Pythona 2. Jeżeli się trochę przeklikać, trafimy oczywiście na wersję 2.7.14 oraz wszystkie wcześniejsze, ale nie są w żaden sposób wyeksponowane. Jako, że zegar tyka coraz mocniej, nie ma się czemu dziwić, ale zaczynałem na Pythona 2.2 i jakoś jednak jest dziwnie. 🙂 To jak jakby po latach się zorientować, że nie można już kupić Windowsa 2000, albo gumy do żucia Turbo.

Bottle.py (1)

Pomyślałem sobie, że napiszę o Bottle i o tym, jak zwyczajnie ułatwia życie. Niewiele programów zostało napisanych z myślą o ułatwianiu życia, ale mamy szczęśliwie kilka wyjątków.

Czym się różni wróbelek? Tym, że ma jedną nóżkę bardziej.

W Pythonie frameworki webowe piszą się same. Ludzie tworzą ich pełno, czasem nawet do najbardziej trywialnych zastosowań, jak na przykład tylko i wyłącznie do wysyłania plików na serwer. Pierwszy raz zainteresowałem się nimi bardziej dzięki prelekcji https://www.youtube.com/watch?v=AYjPIMe0BhA. O niektórych z nich wiedziałem, ale kilka było nowością.

Co było ważne dla mnie? Tylko kilka rzeczy:

  • prostota instalacji i minimalna ilość zależności,
  • swoboda kodzenia bez konieczności tworzenia wielu plików i katalogów,
  • musi działać idealnie w Pythonie 3,
  • dokumentacja w archiwum do ściągnięcia, najlepiej w pdf (najczęściej pracuję bez dostępu do internetu).

Bottle spełnia wszystko idealnie: nie ma żadnych zależności i jest po prostu plikiem bottle.py, który trzeba skopiować do katalogu z programem (co bardzo lubię), ewentualnie zainstalować pipem. Dokumentację można ściągnąć jako jeden duży plik pdf.

Do czego to jest?

Jeżeli kiedykolwiek miałe(a)ś stronę na www.republika.pl z dwoma megabajtami na twoje dane w darmowym pakiecie, a menu składało się z apletów javy, możesz pamiętać, że rzeczy wpisywane w pasek adresu przeglądarki rzeczywiście znajdowały się w podanym katalogu na serwerze. HTML działa tak do dzisiaj i nie ma w tym nic złego.

Wyobraźcie sobie jednak, że łącza na stronie nie mają nic wspólnego z plikami na dysku. Co zyskujemy? Ano to, że możemy mieć klasycznie działające podstrony, lub podstrony, które wywołują jakieś funkcje, na przykład sprawdzają ciasteczka, albo włączają subprocesy, albo kasują pliki z dysku, a zaraz potem przekierowują użytkownika na „normalną” stronę z kodem HTML dla przeglądarki. Nie ma tu granic możliwości i za to lubię programowanie.

No to zaczynamy.

Najpierw zajmiemy się przygotowaniem „środowiska programistycznego” i programem Witaj Świecie.

mkdir bottle
cd bottle
wget https://github.com/bottlepy/bottle/raw/master/bottle.py

I skoro instalację mamy za sobą, jedziemy z kodem. Jestem trochę leniwy, więc na początku zajmiemy się przykładowym kodem ze strony głównej Bottle.

from bottle import route, run, template

@route('/hello/<name>')
def index(name):
    return template('Hello {{name}}!', name=name)

run(host='localhost', port=8080)

Bottle składa się z wielu niezależnie od siebie działających funkcji, które zwykle hurtowo importujemy zaraz na początku. Najważniejsze są dla nas właśnie te trzy:

  • route to właśnie podstrony wywołujące funkcje,
  • run to wbudowany miniserwer www,
  • template to szablony stron, czyli html z bardzo prostą obsługą pythonowych zmiennych, warunków, pętli oraz kilku innych rzeczy.

@route(‚/hello/<name>’) to właśnie tego typu podstrona. Oznacza to, że możesz wpisać w pasku przeglądarki twojastrona.pl/hello, żeby skrypt wywołał funkcję i coś zrobił. <name> to parametr fukcji, który trzeba wpisać w pasek adresu, albo zawrzeć w łączu z innego miejsca.

def index(name): to zwykła, pytonowa funkcja.  Można ją nazwać wszystko jedno jak, może pobierać parametry i nie ma co do tego żadnych ograniczeń. W tym przypadku funkcja index wymaga podania parametru, który ustawia sobie pod zmienną name. Jak jej to podać, skoro mamy tylko przeglądarkę? Ano tak: http://localhost:8080/hello/lukasz. 🙂

return template(‚Hello {{name}}!’, name=name)… Tutaj zaczyna się magia. Co zwraca funkcja hello? Jest to szablon strony. ‚Hello {{name}}’ jest po prostu kodem HTML (no, prawie) ze zmienną name ukrytą w nawiasach, żeby można było ją jakoś odróżnić od tagów tabel, czy akapitów.

Dalej jest name=name. Masło maślane, przecież wiadomo. Lewa strona równania to jednak zmienna używana przez szablon strony (który może być osobnym plikiem), a prawa zmienną z kodu Pythona, który właśnie robimy. Nie ma jednak żadnego powodu, żeby nie nazywać ich tak samo. Kod Pythona oraz szablony to osobne przestrzenie nazw, więc łatwiej napisać i potem pamiętać jedną nazwę zmiennej.

Tyle na początek. Następnym razem weźmiemy się za przygotowanie ładnego inaczej (nie jestem stylarzem) kodu html, w który opakujemy nasz pythonowy kod.

Stronka z bottle.py

Potrzebowałem małej strony internetowej do włączania i wyłączania programu w Javie. Okazało się, że wyszło z tego nieco więcej, a wszystko dzięki Pythonowi i Bottle.

Python dobrze się rymuje z html, to wiemy: Django, Flask, Cherrypy, nawet stare Plone daje radę. Przez lata narobiło się wiele mniejszych skryptów, pozwalających na przesyłanie plików, albo wyświetlanie stron. Bottle.py jest czymś więcej: ma szablony stron, obsługuje ciasteczka, przekierowania, itd… a wszystko to w jednym pliku, który wystarczy skopiować do katalogu z programem. Jako, że nie mam wielkiego doświadczenia z pythonowymi stronami internetowymi, wybrałem na początek to, co najprostsze.

W sieci można znaleźć wiele przykładowych Hello World, ale było to dla mnie za mało. Zapraszam więc na githuba: https://github.com/seem8/astech. W kilku kolejnych wpisach rozłożę programik na części i wyjaśnię jak działa.

Python i g++

Podczas kompilacji Pythona na Debianie skrypt configure nie jest w stanie znaleźć kompilatora c++, więc trzeba mu trochę pomóc:

./configure --prefix=/opt/py352 --with-cxx-main=/usr/bin/g++

Chyba nie ma jeszcze bloga bez tego wpisu, ale zawsze o tym zapominam, więc napiszę też u siebie.

Pythonowe logowanie

Coś ostatnio dużo o Pythonie, ale jakoś tak wyszło. Jego siła leży w bibliotece standardowej i jednym z modułów się teraz zajmiemy:

import logging
logging.basicConfig(filename="log.txt", \
     format="%(asctime)s - %(levelname)s - %(message)s, \
     level=logging.DEBUG)

# no to coś zalogujmy
logging.info("piszemy do pliku log")

Konstrukcja %(coś)s odpowiada mniej więcej str(coś) i sporo osób tego używa. Dostajemy teraz plik log.txt z napisem 2016-02-08 18:45:01,622 – INFO – piszemy do pliku log. Proste, czytelne i na temat. Załóżmy teraz, że chcielibyśmy zalogować wyjątek. Utworzyłem plik plik.txt i zabrałem sobie prawa do zapisu. Ciekawe, co Python na to wymyśli:

try:
  plik = open("plik.txt", "w")
except:
  err = sys.exc_info() # zapisujemy komunikat błędu do zmienej
  logging.error("Nie można otworzyć pliku plik.txt do zapisu.")
  logging.error(str(err[1].args[0]) + " " + err[1].args[1])
  sys.exit()

Przedostatnia linijka jest dosyć tajemnicza. Python, jak to ma w zwyczaju, podaje komunikat błędu jako zagnieżdżoną listę. Zerowym elementem jest numer błędu, a pierwszym komunikat. Co teraz znajdzie się w log.txt?

2016-02-08 19:02:28,965 – ERROR – Nie można otworzyć pliku plik.txt do zapisu.
2016-02-08 19:02:28,966 – ERROR – 13 Permission denied

Znowu: proste, czytelne i na temat, czyli dokładnie to, co powinien zawierać log.
Na samym początku skryptu, przy funkcji basicConfig można ustawić szczegółowość logowania na poziomy DEBUG, INFO, WARNING, ERROR oraz CRITICAL. Można tu sprawdzić parametry podane przy uruchamianiu skryptu i ustawić jakąś wartość domyślną. Można zresztą wszystko, ale to już oddzielny temat.