Python, pip i virtualenvy
Witajcie ;)
Często dzieje się tak że nasze programy muszą działać cały czas. Przykładem mogą być np. jakieś serwery TCP, zbieracze danych, okresowe przeliczenia jakiś danych itp. Oczywiście możemy taki program uruchomić i on będzie sobie chodził w tle, ale w taki sposób ciężko nim zarządzać. Pisząc „zarządzać” mam na myśli zatrzymywać, restartować, podglądać stan procesu itp. Cykl dwóch wpisów pokaże jak można osiągnąć łatwe utrzymanie swoich programów.
A więc dziś napiszemy sobie mały program w Pythonie który będzie zbierał dane i wrzucał je do bazy danych. W drugiej części tego wpisu pokaże jak zarządzać naszą aplikacją. Po drodze zapoznamy się z paczkami i modułami Pythona, wirtualnymi środowiskami Pythona, programem pip – i tutaj zakończymy tę część. Druga część obejmie program do zarządzania procesami – supervisord i cron’a.
Python jest dostępny w dwóch „liniach wersji”, tzn. że możemy pisać w Pythonie 2.x i albo w Pythonie 3.x. Między tymi wersjami są różnice, nie tyle w samym języku, bo idea jest taka sama, co np. niektóre konstrukcje języka stały się funkcjami albo inne funkcje zwracają teraz jakiś inny typ. Na gołych serwerach wyposażonych w CentOSa 6.5 najczęściej dostaniecie Pythona 2.6.6 co w obliczu najnowszej jego wersji – 3.5.1 – jest dość starym Pythonem. Więc pierwsze co zrobimy to doinstalujemy Pythona w wersji 3.3.3.
1 2 3 4 5 | wget https://www.python.org/ftp/python/3.3.3/Python-3.3.3.tgz tar -xvf Python-3.3.3.tgz cd Python-3.3.3 ./configure --prefix=/usr/local make && make altinstall |
Tutaj mogą pojawić się problemy. Jeśli brakuje czegoś do skompilowania Pythona to warto wykonać przed tym jeszcze komendy dociągające podstawowe rzeczy:
1 2 3 | yum -y update yum groupinstall -y development yum install -y zlib-dev openssl-devel sqlite-devel bzip2-devel |
Po skompilowaniu i zbudowaniu Pythona komendą
1 | which python3.3 |
możemy zobaczyć że zainstalował się pod ścieżką jaką podaliśmy w opcji –prefix przed kompilacją Pythona.
Teraz doinstalujemy sobie pipa – czyli program który jest nieco jakby odpowiednikiem Javowego Maven’a – czyli pozwala nam łatwo dociągnąć zależności do naszych programów.
1 2 | wget https://bootstrap.pypa.io/get-pip.py python3.3 get-pip.py |
Po tym powinniśmy móc wykonać komendę
1 | pip3.3 freeze |
która pokaże nam aktualnie ściągnięte zależności. Jeśli nie mamy tam paczki „virtualenv” należy ją dosintalować:
1 | pip3.3 install virtualenv |
Co to jest ten virtualenv? Virtualenv, czyli wirtualne środowisko to nic innego jak rozwiązanie problemu nie zawsze kompatybilnych ze sobą wersji Pythona i bibliotek które, jeśli były pisane pod Pytona 2.x nie zawsze będą kompatybilne z Pythonem 3.x. Co więcej jeśli mamy np. trzy programy swoje napisane w Pythonie i dwa z nich korzystają z biblioteki X które korzystają z bibliotek Y ale w innej wersji to robi się problem. Środowisko wirtualne (virtualenv) to nic innego jak wydzielenie osobnego folderu dla bibliotek zainstalowanych globalnie i dla tych zainstalowanych w virtualenvie. Co więcej każdy taki virtualenv może korzystać z innej wersji Pythona zainstalowanej w systemie. Jak pewnie zauważyłeś – program pip ma swój sufix „3.3” który odpowiada wersji Pythona dla której jest zainstalowany. Tak samo jak Python – ta wersja która już była w systemie nadal jest dostępna
1 | python -V |
a ta wersja którą zainstalowaliśmy jest dostępna też pod nazwą python ale sufixem „3.3”:
1 | python3.3 -V |
Skoro wiemy już czym jest virtualenv to stwórzmy sobie go:
1 | virtualenv -p /usr/local/bin/python3.3 /opt/virtual_envs |
Takie polecenie utworzy środowisko wirtualne w folderze „/opt/virtual_envs” które będzie korzystało z Pythona 3.3 (podana ścieżka).
Aby „aktywować” takie wirtualne środowisko należy wykonać polecenie
1 | source /opt/virtual_envs/bin/activate |
po wykonaniu, w konsoli po lewej pojawi się nazwa naszego virtualenva. Aby wyłączyć go wpiszemy po prostu
1 | deactivate |
Skoro mamy już prawidłową wersję Pythona i mamy dla naszego małego projektu stworzony virtualenv to czas pisać kod. Napiszemy prosty kawałek kody który będzie pobierał dane o aktualnych wartościach cen złota, srebra, platyny i palladu i będzie zapisywał je do bazy danych. Zacznijmy od pliku konfiguracyjnego. Niech on wygląda np tak:
1 2 3 4 5 6 7 8 9 10 | { "remote_api_addr": "http://www.money.pl/gielda/surowce/", "frequency": 1, "log_path": "\var\log\financial.log", "hostname": "database.org", "user": "test", "password": "testpass", "database": "testuser", "test_mode" : false } |
Gdzie kolejne klucze oznaczają:
- remote_api_addr – adres zewnętrznego serwera z którego będzie pobierać dane
- frequency – częstotliwość pobierania
- log_path – ścieżka gdzie będziemy zapisywać logi
- hostname – host bazy danych
- user – user bazy danych
- password – hasło do użytkownika bazy danych
- database – nazwa bazy danych
- test_mode – czy aplikacja aktualnie pracuje w trybie developerskim czy produkcyjnym
A więc czas na kod klasy która będzie pobierać dane. Zacznijmy od konstruktora. Zakładamy że do obiektu konfiguracyjnego wstrzykniemy też obiekt loggera.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | from urllib.request import urlopen from json import loads from bs4 import BeautifulSoup class DataRetriever(object): def __init__(self, config): self.addr = config['remote_api_addr'] self.logger = config['logger'] self.test_mode = config["test_mode"] self.what_to_retrieve = ["gold", "palladium", "platinum", "silver"] def _retrieve_fake_date(self): return loads('''{ "gold": 39.59043, "palladium": 15.75386, "platinum": 29.67514, "silver": 0.47438 }''') def get_data(self): return self._retrieve_fake_date() if self.test_mode else self._retrieve_data() |
A więc określamy zapisujemy sobie dane z konfiguracji i info o tym jakie elementy chcemy pobrać. Poza tym przygotowujemy także fejkowy interfejs który posłuży nam do testów.
Dodajmy teraz właściwy kawałek kodu który będzie pobierał dane ze strony. Całość niech wygląda tak:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | class DataRetriever(object): def __init__(self, config): self.addr = config['remote_api_addr'] self.logger = config['logger'] self.test_mode = config["test_mode"] self.what_to_retrieve = ["gold", "palladium", "platinum", "silver"] def test(self, element): length = len(element.parent.attrs) return length > 0 and element.parent.get("id") in self.what_to_retrieve and len(element("b")) == 1 def _retrieve_data(self): result = urlopen(self.addr).read() self.logger.debug("Data loaded from API.") dom = BeautifulSoup(result) return dict( zip(self.what_to_retrieve, map(lambda x: float(x.getText().replace(',', '.')), filter(self.test,dom.findAll(attrs={'class' : 'ar'}))))) def _retrieve_fake_date(self): return loads('''{ "gold": 39.59043, "palladium": 15.75386, "platinum": 29.67514, "silver": 0.47438 }''') def get_data(self): return self._retrieve_fake_date() if self.test_mode else self._retrieve_data() |
Metoda _retrieve_data pobiera interesujące nas dane. Pierw ładujemy do pamięci całą stronę używając do tego urlopen, wypisujemy info o tym do loggera, tworzymy zmienną dom która będzie przechowywać pobraną zawartość strony w możliwym do parsowania formacie. Robimy to biblioteką BeautifulSoup.
Ten kawałek jest ciekawy:
1 2 3 4 | return dict( zip(self.what_to_retrieve, map(lambda x: float(x.getText().replace(',', '.')), filter(self.test,dom.findAll(attrs={'class' : 'ar'}))))) |
Idąc od „dołu”, ten mały funkcyjny potworek robi kilka przydatnych rzeczy. Pierw filtrujemy wszystkie elementy o atrybucie class równym „ar” używając do tego funkcji test która zwraca true (a więc zostawia tylko te elementy) jeśli rodzic tego elementu ma jakieś atrybuty i atrybut id ma wartość jednego z tych nazw które szukamy i czy znaleziony element posiada dziecko którym jest tag b. Skąd te warunki? Przeanalizuje kod strony która jest w pliku konfiguracyjnym a znajdziesz tam odpowiedź.
Następnie tak przefiltrowane elementy poddajemy funkcji map która wybiera wartość tekstową znalezionego atrybutu, zamienia separator dziesiętny na poprawny i konwertuje stringa na liczbę. Takie liczby „zipujemy” z tym czego szukamy – czyli tablicą [„gold”, „palladium”, „platinum”, „silver”] i całość zamieniamy na słownik czego rezultatem jest obiekt który ma taki sam format jak dane na sztywno wpisane w metodzie _retrieve_fake_date.
Klasa która natomiast zapisuje dane do bazy danych jest prostsza. I wyglądać może tak:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | import pymysql.cursors class DataCollector(object): def __init__(self, config): self.logger = config['logger'] self.config = config self.query_template = "INSERT INTO financial_data VALUES (NULL, now(), {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7})" def get_growth(self, new, old): return round(((new * 100) / old) - 100, 5) def connect(self): self.connection = pymysql.connect(host=self.config['hostname'], user=self.config['user'], password=self.config['password'], db=self.config['database'], charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor) def add_record(self, data): self.connect() with self.connection.cursor() as cursor: gold_growth = 0 silver_growth = 0 palladium_growth = 0 platinum_growth = 0 cursor.execute("SELECT * FROM financial_data WHERE id = (SELECT min(id) from financial_data)") last_data = cursor.fetchone() if last_data is not None: gold_growth = self.get_growth(data["gold"], last_data["gold"]) silver_growth = self.get_growth(data["silver"], last_data["silver"]) palladium_growth = self.get_growth(data["palladium"], last_data["palladium"]) platinum_growth = self.get_growth(data["platinum"], last_data["platinum"]) with self.connection.cursor() as cursor: cursor.execute(self.query_template.format(str(data["gold"]), str(data["silver"]), str(data["palladium"]), str(data["platinum"]), str(gold_growth), str(silver_growth), str(palladium_growth), str(platinum_growth))) self.logger.debug("Data updated successfully.") self.connection.commit() |
Wrzucamy do bazy danych wartości które dostaniemy na wejściu funkcji add_record plus obliczamy procentowy wzrost między dodawanym rekordem a pierwszym dodanym rekordem.
Oba te pliki muszą nazywać się __init__.py i być w osobnych folderach. To czyni z pakiety – czyli taki odpowiednik paczek z Javy. W pakiety grupujemy pojedyncze funkcjonalności. Pakiety takie, po zainstalowaniu naszej aplikacji możemy używać w innych aplikacjach. Czyli np. naszego DataRetrieve’era możemy w innym programie użyć już do innego celu.
Aby to działało potrzebny jest nam jeszcze jeden pakiet „główny” który będzie używać oby poprzednich pakietów:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | __author__ = 'Matt' from data_retriever import DataRetriever from data_collector import DataCollector import logging, time from json import loads config = loads(open("/opt/financial.conf").read()) config["logger"] = logging #wstrzykujemy loggera logging.basicConfig(format='%(asctime)s %(message)s', filename=config["log_path"], level=logging.DEBUG) data_retriever = DataRetriever(config) data_collector = DataCollector(config) def iterate(): logging.debug("Iteration.") data = data_retriever.get_data() data_collector.add_record(data) def run(): logging.debug("App started.") while True: iterate() time.sleep(config["frequency"]) if __name__ == '__main__': run() |
Tutaj nie ma co opisywać. Nic odkrywczego się nie dzieje. Ale poza tymi plikami, aby nasz program mógł być „instalowalny” należy utworzyć plik setup.py o zawartości np. takiej:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | __author__ = 'Matt' from distutils.core import setup setup(name='GoldDigger', version='1.7', description='Agregator notowan metali szlachetnych', author='Mateusz Mazurek', author_email='[email protected]', url='https://mmazur.eu.org', packages=['data_retriever', 'data_collector', 'run'], install_requires=['PyMySQL', 'beautifulsoup4'], entry_points={ 'console_scripts': [ 'gold_digger=run:run', ], }, ) |
gdzie najciekawsze są:
1 | packages=['data_retriever', 'data_collector', 'run'], |
wskazujemy jakie pakiety zawiera nasz moduł.
1 | install_requires=['PyMySQL', 'beautifulsoup4'], |
z jakich zależności korzystamy
1 2 3 4 5 | entry_points={ 'console_scripts': [ 'gold_digger=run:run', ], }, |
oraz tzw. punkty wejścia, czyli po prostu informacja jak możemy naszą aplikację uruchomić.
Mówimy tutaj że pod nazwą „gold_digger” ma się pojawiać funkcja run z pakietu run (pierwsze run to nazwa pakietu a drugie to nazwa funkcji).
No i taki kawałek kodu jest gotowy do instalacji.
Aby ją zainstalować musimy aktywować naszego virtualenva i wykonać, będą w folderze ze swoim projektem:
1 | pip install . |
A uruchomić możemy wykonując
1 | exec gold_digger |
oczywiście będąc w naszym virtualenvie.
No i to byłby koniec części pierwszej – w drugiej pokaże jak zarządzać naszym programem.
Druga część już opublikowana – chcę poczytać.
Mateusz Mazurek
Od wersji 3.3 Pythona virtualenv jest w standardowej dystrybucji, pod nazwą pyvenv-3.3 :)
https://docs.python.org/3/library/venv.html
PIP zaś jest dołączany do każdego Pythona 2 >=2.7.9 lub trójski od >=3.4