Jak trzymać konfigurację skryptów?

Jak trzymać konfigurację skryptów? Żeby było bezpiecznie i wygodnie?

Przy prostych zadaniach konfiguracyjne parametry można (ale nie należy!) trzymać inline w kodzie. Ale lepiej w zmiennych (właściwie stałych) zdefiniowanych na początku pliku (i odpowiednio nazwanych, żeby wiedzieć że to stałe związane z konfiguracją). A najlepiej – w pliku zewnętrznym (lub zmiennych systemowych, ale tym się dzisiaj nie zajmiemy).

Photo by Ferenc Almasi on Unsplash

 

Dlaczego w pliku zewnętrznym? Z co najmniej dwóch powodów:

  • prosta przenośność – można przenieść skrypt (albo cały ich zestaw, cały projekt) z jednego komputera na drugi i na przykład zmienić tylko ścieżki do plików z danymi. Można zmienić też adres (i inne dane dostępowe) bazy danych i dzięki temu nie zmieniając ani jednego znaku w oprogramowaniu przejść ze środowiska testowego na produkcyjne.
  • bezpieczeństwo – w czasach kiedy kod (nawet firmowy) trzymany jest na GitHubie czy innych tego typu repozytoriach (nawet wewnętrznego BitBucketa to dotyczy) bezpieczeństwo jest ważne i dane dostępowe (loginy, hasła, klucze api itd. itp.) nie powinny być udostępniane. Żeby nie były – nie mogą być w kodzie, muszą być gdzieś poza kodem. W dużym skrócie popularne rozwiązanie wygląda tak, że developerzy nie znają danych dostępowych do produkcji, każdy trzyma lokalnie swoją konfigurację (z np. swoją bazą danych na potrzeby developmentu), a w pliku .gitignore wpisany jest plik konfiguracyjny. To powoduje, że plik z konfiguracją nie trafia do repozytorium.

No dobrze, ale skoro konfiguracja ma być w plikach zewnętrznych to w jakich? Najlepiej takich, które trzymają się jakiegoś standardu.

Standardy są właściwie cztery, my zajmiemy się trzema. I to zajmiemy się w R oraz Pythonie.

 

Konfiguracja .INI

Pierwszy standard to plik .ini. To plik tekstowy (jak wszystkie inne) o przykładowej strukturze jak poniżej:

Co my tutaj widzimy? Mamy dwie sekcje nazwane modul1 i modul2, które mogą odpowiadać za na przykład różne elementy konfiguracyjne – bazę danych, ścieżki do plików, klucze api do różnych systemów. Takie moduły ułatwiają edycję konfiguracji.

W każdym z modułów mamy po dwie zmienne. W pierwszym są to zwykłe, pojedyncze zmienne, a w drugim – listy. Listy mogą być przydatne (na przykład lista adresów mailowych, odbiorców raportów).

Dodatkowo mamy możliwość komentowania w pliku – czasem to się przydaje. A jak działa?

Jak użyć takiego pliku? Zacznijmy od Pythona.

W Pythonie mamy wbudowany pakiet configparser, który bardzo dobrze radzi sobie z czytaniem (oraz zapisywaniem, ale to dzisiaj odpuszczamy) plików .ini. Przykładowy kod, który wczyta i pokaże nam wybrane pozycje z konfiguracji:

Jak wygląda wczytany obiekt?

Ma to swoją klasę, do elementów wewnątrz można dobrać się poprzez metodę get():

Ale można sięgnąć po wartość też tak jak (nie powinno się) w standardowych słownikach:

Ups – na końcu został nam komentarz. Cała linia (po znaku równości) traktowana jest jako wartość konkretnej zmiennej. Trzeba o tym pamiętać.

A czy da się dostać do jednego elementu na liście?

To nie jest jakiś konkretny element z listy (zobacz wyżej jak zdefiniowana jest zmienna_arr_num)! To jest drugi (indeksowanie od zera) znak w zmiennej zmienna_arr_num potraktowanej jako string. Bo tutaj wszystko jest stringiem:

Tak, ta liczba też jest stringiem. Więc trzeba ewentualnie zrobić jawny casting do int lub float jeśli potrzebujecie.

Wszystko poza tymi niedogodnościami wygląda ok, prawda? I tak musimy znać ścieżki (jaki moduł, jaka zmienna) poszczególnych parametrów, więc nic dziwnego że odwołujemy się do nich po konkretnych nazwach.

Zwróć uwagę na to co otrzymujemy w zmienna_txt z modul1. W konfiguracji wartość zmiennej jest w cudzysłowach, aby było jasne że to ciąg tekstowy… ale te cudzysłowy trafiają do naszej zmiennej! No słabo trochę (z jednej strony, z drugiej ok). No i trzeba odpowiednio to w konfiguracji wyeskejpować.

A jak to wygląda w R? Podobnie jak w Pythonie – mamy gotową bibliotekę (już nie wbudowaną, trzeba ją sobie zainstalować przez install.packages("configr")) – configr. Ta z kolei jest na tyle uniwersalna, że posłuży nam przy wszystkich omawianych dzisiaj formatach plików konfiguracyjnych. Przykładowy skrypt wyciągający dane z pliku .ini:

Tutaj podobnie jak w Pythonie – komentarz inline nie jest obsługiwany (w sumie .ini nie mają jako takiej sztywnej definicji, a gdyby miały – taki komentarz byłby niedozwolony).

Uwaga – wynik ponownie jest stringiem a nie listą, co widać tutaj:

tutaj:

oraz tutaj:

Z grubsza więc jest tak samo jak w Pythonie.

Podsumowując – pliki INI:

  • pozwalają na komentarze – ale tylko jako jedna linia, bez inline
  • każda wartość przypisana do zmiennej jest typu string
  • nie można użyć zagnieżdżeń

 

 

Konfiguracja .JSON

Pliki .ini nie pozwalają na zagnieżdżanie elementów – mamy płaską listę zmiennych (dla wygody upakowaną w moduły, o ile istnieje taka potrzeba). A pliki .json już takie zagnieżdżanie umożliwiają. Po co to? Ano na przykład w jednym pliku możemy trzymać konfigurację wszystkich środowisk (zagnieżdżony obiekt to konfiguracja jednego ze środowisk) i tylko jedną zmienną (też w pliku konfiguracyjnym) decydować którego środowiska używamy. Średnio to bezpieczne rozwiązanie, ale się da.

Zbudujmy więc kolejną konfigurację, tym razem już szerszą – taką, która pozwala na wykorzystanie zarówno zagnieżdżenia pojedynczych obiektów jak i na zbudowanie listy obiektów:

W powyższej konfiguracji modul1 oraz modul2 są żywcem przeniesione z pliku .ini, a kolejne dwa moduły pokazują rzeczy (cuda, o jakich nie śniło się filozofom! ;-) na które format INI nie pozwala.

W modul3 mamy dwa obiekty zagnieżdżone pod modułem. Oba obiekty z grubsza są takie same, ale są to dwa odrębne byty. Aby dostać się do nazwy obiektu trzeba przejść po drzewie: moduł -> obiekt -> pole -> wartość.

Z kolei w modul4 mamy cztery obiekty zagnieżdżone pod modułem ale w formie listy. Zatem aby dostać się do nazwy konkretnego elementu trzeba przejść ścieżką: moduł -> jeden z elementów listy -> pole -> wartość.

Odczytajmy taką konfigurację w Pythonie. Będzie bardzo prosto, o ile potrafisz odczytać pliki JSON.

Wygląda (z grubsza, jeśli pominąć formatowanie) jak nasz źródłowy JSON, prawda?

Jak dostać się do wartości kolejnych elementów? Tak jak do elementów słownika (dict) w Pythonie. Tutaj akurat słownik słowników:

A jak mamy dodatkowo listę to po prostu używamy indeksu:

Dla listy obiektów – najpierw indeks na liście, a potem nazwa elementu:

A co z typem? Jeśli liczba nie jest w ciapkach to…

…jest tego typu którego być powinna (int lub float).

Wszystko zgodnie z intuicją – dokładnie tak, jak można się spodziewać. Czy w R jest tak samo?

Sięgamy do konkretnego elementu tablicy!

Uwaga – inna kolejność parametrów niż w pliku konfiguracyjnym: najpierw mamy nazwę pola, a potem numer na liście (indeks) obiektów. Może to wprowadzić zamieszanie.

Sprawdźmy jeszcze czy zachowane są typy danych:

Jest tak jak być powinno – liczby całkowite są typu integer, a zmiennoprzecinkowe – numeric.

Zwróć też uwagę, że config$modul4 to właściwie gotowy data frame… co ma swoje zalety. W Pythonie z użyciem Pandas też łatwo data frame’a z JSONa zbudować.

Podsumowując – pliki JSON:

  • nie pozwalają na komentarze
  • zachowane są typy danych
  • stringi trzeba ubierać w ciapki
  • można użyć zagnieżdżeń i list elementów/obiektów

 

 

Konfiguracja .YAML

Pliki YAML pozwalają na wszystko to co JSON, ale mają dwa miłe dodatki. Po pierwsze – są chyba nieco bardziej czytelne – nie ma tylu nawiasów. Ale trzeba dbać o wcięcia. Na szczęście większość edytorów potrafi tego pilnować (albo ma wtyczki do tego).

Druga zaleta – komentarze.

Zobaczmy przykładową konfigurację – identyczną z tą z JSONa:

Przeczytajmy to w Pythonie. Potrzebujemy pakietu pyyaml – jeśli go nie masz to zainstaluj via pip lub conda (jeśli używasz Anacondy).

Porównaj wynik z tym jak ciąg został wpisany w konfigurację – jest znak w znak tak samo. To bardzo wygodne.

Jak widać klasy danych liczbowych są zachowane. To również wygodne, ale nic szczególnego w porównaniu z JSONem.

Właściwie nie ma czego komentować – wszystko zachowuje się tak jak powinno. Typy zmiennych są rozpoznawane, stringi nie potrzebują ciapek, listy da się indeksować – nie ważne czy to listy elementów jednego typu czy całych obiektów.

A jak czytać to w R? Przy użyciu pakietu configr właściwie ciągle tak samo:

Jak widzimy zmienia się tylko delikatnie (w porównaniu do JSONa) struktura obiektu stworzonego po wczytaniu YAMLa. Jak sięgać do środka?

Sięganie do elementu tablicy jest intuicyjne:

Nawet jeśli to tablica bardziej złożonych obiektów:

Uwaga – inna kolejność sięgania do parametrów niż w przypadku sięgania do nich wczytanych z JSONa, ale tutaj zgodna z logiką.

Trudniej z YAMLa (w porównaniu do JSONa) uzyskać data frame. Ale YAML to format do konfiguracji a nie przekazywana danych. Do danych JSON sprawdza się zdecydowanie lepiej (co widać we wszelakich API).

Podsumowując – pliki YAML:

  • pozwalają na komentarze na każdym poziomie
  • zachowane są typy danych
  • stringów mogą być bez ciapek
  • można użyć zagnieżdżeń właściwie na każdym poziomie
  • format daje nam trochę swobody w pisaniu (np. definiowanie tablic) i czyta się podobnie do naturalnego języka

No ideał! ;)

GitHub

Żeby było łatwiej i nie trzeba było kopiować kodu z tego wpisu wszystko wrzuciłem na swojego GitHuba – śmiało forkuj, jeśli potrzebujesz.

A jeśli jesteś już tutaj to możesz też wykonać jeden klik i zapisać się na newsletter.

1 myśl na “Jak trzymać konfigurację skryptów?”

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *