Meine neue Shell: xonsh

Manchmal macht man Sachen die man selber nicht so wirklich versteht. Zum Beispiel hatte ich mich lange quer gestellt ZSH einzusetzen. Wieso eine alternative Shell? BASH ist doch sowas wie ein standard auf den Servern auf denen ich arbeite. Vor ein paar Jahren dann die erste Installation. Total abgeschreckt von der Konfiguration (bin bis Heute nicht dahinter gekommen wie das alles funktioniert), war mir oh-my-zsh ein Steigbügelhalter in die Welt von ZSH. Es ist eine Paket aus verschiedenen Plugins und Konfigurationen die das erste ZSH-Erlebnis, instant, beeindruckend gestaltet. Irgendwann war es mir zu langsam und ich stieg um auf prezto. Hat sich alles ein wenig flotter angefühlt und ich habe noch weniger verstanden was da im Hintergrund abläuft. Ja, man sollte sich die Sachen genauer anschauen und dann wäre es auch kein Problem. Aber manchmal fehlt mir Zeit weil ich meine Nase in tausend anderen Projekten habe. Es soll einfach funktionieren und the world a better place machen tun... oder zumindest meinen Alltag.

Nun bin ich auf einen Beitrag auf der PyCon über xonsh gestossen.

Es ist eine alternative Shell die in Python geschrieben ist und das Bindeglied zwischen Shell und Python sein möchte. Und das beste: Endlich eine Syntax in der Shell die ich mir merken kann. Nie wieder nachschauen wie if oder for in Bash funktionieren. "Nie wieder" ist ein harter Ausdruck. Selbst ich installiere xonsh nicht auf all meinen Servern. Es setzt Python 3.4 oder 3.5 voraus.

Bevor ich ein paar Anwendungsbeispiele nenne, möchte ich auf mein Setup hinweisen. Auf GitHub befinden sich meine Ansible Roles um meine Rechner einzurichten. Unter anderen auch xonsh.

Python und Shell Kommandos gemeinsam nutzen

Hier ein Beispiel für ein xonsh-Script das den Output von allen Docker-Commands nimmt und den Output in Files schreibt. Dies habe ich gebraucht um meine xonsh Extension xonsh-docker-tabcomplete zu testen. Ein Completer für Docker Kommandos die auch direkt auf die Docker-API zugreift. Danke docker-py und Python als Shell.

"""Ein xonsh-Script um alle `--help` Outputs als Textfiles
abzuspeichern.
"""
import os
import re

# `parser` ist ein Modul mit ein paar Regex Parsern.
from docker_tabcomplete import parser

# Regex um Versionsnummer zu finden.
RE_VERSION = re.compile(r'((?:\d+\.)?(?:\d+\.)?(?:\*|\d+))')

# Hier nutze ich das erste mal ein wenig xonsh Magic.
# Das `$()` macht genau das gleiche wie in Bash-Scripten. Es führt
# Ein Kommando aus aus gibt den Output zurück. In diesem Fall den
# Output von `docker --version` und sucht in Python per Regex nach
# der Versionsnummer.
DOCKER_VERSION = re.search(RE_VERSION, $(docker --version)).group(1)

# Es wird ein Verzeichnis für die Docker Version angelegt falls es noch
# nicht existiert.
if not os.path.exists('../tests/data/{}'.format(DOCKER_VERSION)):
    os.makedirs('../tests/data/{}'.format(DOCKER_VERSION))

# `parser.commands` nimmt den Output von `docker --help` und parst
# nach allen Docker Kommandos. Wieder muss nicht mit `subprocess`
# gefummelt werden sondern wir nutzen einfach `$()` von xonsh um
# an den Output zu gelangen.
COMMANDS = parser.commands($(docker --help))

# Jetzt wird sogar Python mit Shell mit Python gemischt.
for command in COMMANDS:
    with open('../tests/data/{}/{}.stdout'.format(DOCKER_VERSION, command), 'w') as f:

        # Es wird die Python-Variabel `command`, die gerade in benutzung ist,
        # in das Shell-Kommando `docker ... --help` eingefügt. Magic.
        f.write($(docker @(command) --help))

Dieses Script speichert man als create_test_data.xsh und kann es mit xonsh create_test_data.xsh ausführen.

Meine ~/.xonshrc

Yeah, endlich eine Shell Konfiguration in Python...

import os
import shutil

from xonsh.environ import git_dirty_working_directory


# XONSH ENVIRONMENT VARIABLES ############

# Die Environment Variabeln werden wie Python Variabeln definiert.
# Hier einige Beispiele. Diese sind vor allem um das Verhalten von
# xonsh anzupassen.

# xonsh supportet die bestehenden bash completion files.
# Zumindestens die meisten. Ich hatte Probleme mit dem
# Docker-Completion File und habe deswegen mein eigenes geschrieben.
$BASH_COMPLETIONS = ['/etc/bash_completion.d', '/usr/share/bash-completion/completions']

$CASE_SENSITIVE_COMPLETIONS = False
$SHELL_TYPE = 'best'
$SUPPRESS_BRANCH_TIMEOUT_MESSAGE = True
$VC_BRANCH_TIMEOUT = 5
$XONSH_COLOR_STYLE = 'monokai'

# OTHER ENVIRONMENT VARIABLES ############
$EDITOR = 'vim'
$PYTHONIOENCODING = 'UTF-8'
$LC_ALL = 'C.UTF-8'
$SSL_CERT_FILE = '/etc/ssl/certs/ca-certificates.crt'

# ALIASES ################################

# Aliase sind in einem dict gespeichert das einfach erweitert werden kann.

aliases['exec'] = aliases['xexec']
aliases['ll'] = 'ls -la'
aliases['tmux'] = 'tmux -2'

# Jetzt wird es spannend: Wir können Funktionen oder Lambdas als
# Aliase definieren. Hier der Fall das Mosh eine bestimmte
# Environment Variabel gesetzt haben muss.
def _alias_mosh(args, stdin=None):
    """A function to use as alias to get mosh running.

    There is a strange problem with mosh and xonsh. I have to set $SHELL to
    /bin/bash before running it. It should work with this little hack.
    """
    os.environ['SHELL'] = '/bin/bash'
    args.insert(0, 'mosh')
    cmd = ' '.join(args)
    os.system(cmd)

aliases['mosh'] = _alias_mosh

# GIT ####################################
$FORMATTER_DICT['branch_color'] = lambda: ('{BOLD_INTENSE_RED}'
                                           if git_dirty_working_directory(include_untracked=True)
                                           else '{BOLD_INTENSE_GREEN}')

# PATH ###################################

# Die Path Variabel ist eine Liste die ich einfach extende.
$PATH.extend(
    [
        $HOME + '/.local/bin',
        $HOME + '/miniconda3/bin',
        $HOME + '/node_modules_global/bin'
    ]
)

# XONTRIB ################################

# Hier habe ich ein paar Erweiterungen enabled. Bei manchen schaue ich
# ob der Befehl auf denen sie beruhen überhaupt da und einsatzfähig ist
# bevor ich sie anschalte. Was nützt mir `apt-get` wenn ich nicht auf
# einem Debian/Ubuntu System bin oder Docker nicht installiert ist?
# Dies teste ich mit `shutil.which`.
xontrib autoxsh vox_tabcomplete

if shutil.which('apt-get'):
    xontrib apt_tabcomplete

if shutil.which('docker'):
    xontrib docker_tabcomplete

Ein wenig Rescue

Natürlich kann alles noch ein wenig buggy sein. Einmal gab es Probleme mit den Schreibrechten auf dem History-File und ich konnte kein Terminal mehr öffnen oder mich einloggen. Was da helfen kann: Per SSH und anderer Shell draufschalten und die Shell ändern.

ssh meinuser@meinrechner sh

Fazit

Das ist natürlich nur ein Bruchteil der Möglichkeiten und Sachen die man ausprobieren kann. Es ist ein Anfang und bis jetzt bin ich begeistert. Also einfach mal das Youtube Video anschauen und ausprobieren. Muss ja nicht gleich die Standard-Shell werden wie bei mir ;-).

Python, urllib und CERTIFICATE_VERIFY_FAILED

Da denkt man sich einfach "mal kurz mein Blogging-Container neu bauen" und schon funktioniert nichts mehr. Es fing schon damit an das mein Ansible Playbook Probleme hatte Files herrunterzuladen. urllib brach immer mit urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] ab. Wie aus dem nichts. Vor ein paar Wochen gab es keine Probleme mit der urllib. Es gab sogar ein StackOverflow Posting welches mir riet einfach mal die ca-certificates zu installieren. Das Paket war aber schon installiert. Nach viel googeln un verzweifeln bin ich auf eine obskure Seite gestoßen von der ich kaum etwas verstand. Es gab wohl die Möglichkeit urllib über eine Environment-Variabel den Ort der CA-Zertifikate mitzuteilen. Dies scheint auch in der PEP 476 so beschrieben. requests scheint dies per Default zu machen. Ein export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt fixed die Situation in Alpine Linux. Ich frage mich nur was sich in ein paar Wochen so verändert hat das dieses Problem auftritt.

Ansible Playbooks und variable Hosts

Ich benutze Ansible für viele Sachen. Unter anderem habe ich ein Set an Playbooks und Roles um meine Server so anzupassen das ich mich auf ihnen wohl fühle. Bis jetzt habe ich immer Ansible auch auf den Servern installiert und es dann lokal ausgeführt. Aber dabei nutze ich nicht das eigentlich Ansible Feature: das Ganze Remote über SSH auszurollen. Problem: In den Playbooks muss es die Zeile - hosts: geben. Aber eigentlich will ich das alles Variabel ausführen können. Zum Beispiel nutze ich die gleichen Files auch um meine Vagrant Container einzurichten. Wieso also beim Aufruf die Hosts dem Playbook nicht einfach übergeben? Die Lösung ist dann doch wieder einmal einfacher als man denkt. Man benutzt die Möglichkeit in Playbooks und Roles Variabeln einzusetzen.

---
- hosts: "{{ hosts }}"
  roles:
    - git
    - tmux
    - vim
    - zsh

Diese Variabel übergeben wir beim Aufruf von Ansible.

ansible-playbook base.yml --extra-vars="hosts=myservers"

Nochmal Serien Plots, XKCD Style

Immer noch ziemlich nutzlos aber diesmal mit XKCD-Eyecandy. Bokeh ist ja ganz schön, braucht aber eine ziemlich große JS-Komponente die immer geladen werden muss. Mit matplotlib kann man statische Grafen zeichnen und mit %matplotlib notebook vorher einen Auschnitt wählen.

Ich wollte mal schauen wie sich die Ratingverläufe der Staffeln einer Serie entfalten. Eventuell kann man ja ein daramaturgischen Verlauf erkennen. Ich als jemand der selber keine Ratings abgibt. Alles befindet sich doch ziemlich auf einem Level.

In [1]:
import pandas as pd
import matplotlib
import numpy as np
from trakt.tv import TVShow

Nach dem import der Libs führen wir folgendes Jupyter-Magic aus um die erzeugten Plots im Notebook noch leicht anpassen zu könne.

In [2]:
%matplotlib notebook

Wir aktivieren die XKCD-Style-Plots:

In [3]:
matplotlib.pyplot.xkcd()
Out[3]:
<matplotlib.rc_context at 0xaf0ef58c>

Nun kommt die Funktion die alle Daten einsammelt und ausgibt:

In [4]:
def season_ratings(name):
    tv_show = TVShow(name)
    
    data = {}
    for season in tv_show.seasons:
    
        if season.season == 0:
            continue
    
        ratings = []
        for episode in season.episodes:
            ratings.append(episode.rating)
    
        data[season.season] = pd.Series(ratings)

    df = pd.DataFrame(data)
    df.plot(colormap=matplotlib.cm.Accent)

Beispiele

In [5]:
season_ratings('The IT-Crowd')
In [6]:
season_ratings('Roswell')
In [7]:
season_ratings('Veronica Mars')
In [8]:
season_ratings('Breaking Bad')
In [9]:
season_ratings('Community')

Ratingverläufe unterschiedlicher Serien

Nun lassen wir mal ein paar Serien gegeneinander antreten. Dabei betrachten wir immer alle Episoden aller Staffeln.

In [12]:
def series_ratings(series):
    data = {}
    
    for serie in series:
        
        show = TVShow(serie)
        
        ratings = []
        for season in show.seasons:
            
            if season.season == 0:
                continue
            
            for episode in season.episodes:
                ratings.append(episode.rating)
        
        data[serie] = pd.Series(ratings)
        
    df = pd.DataFrame(data)
    df.plot(colormap=matplotlib.cm.Accent)

Beispiel

In [13]:
series_ratings(['Gotham', 'Flash', 'Arrow', 'Supergirl'])

Mir ist gerade nichts Besseres eingefallen. Es müssen ja in irgendeinen Sinn vergleichbare Serien sein. Da habe ich mir mal die vier aktuellen DC-Serien angeschaut. Da Arrow schon einige Episoden mehr hat, habe ich den Plot angepasst und hinten abgeschnitten. Glaubt man den Ratings, hätte ich Supergirl doch mehr Chancen als zwei Folgen geben sollen. War in den ersten Folgen einfach zu glatt gebügelt.

Fernsehserien, Trakt, Jupyter und Bokeh

Datenvisualisierung hat mich schon immer fasziniert. Leider hatte ich nie einen wirklichen Grund dafür es einzusetzen. Und doch wollte ich es mal ausprobieren. Ich habe mich sehr von Jemus inspirieren lassen. Da passiert natürlich viel viel mehr und ausführlicher. Ich habe erstmal das gemacht was ich auch verstehe :). Und zwar die Episoden Ratings darzustellen.

Als erstes importiere ich ein paar Sachen. Erstmal choice um per Zufall Farben aus einer Palette auszuwählen. Dann pandas für die DataFrames. Um die Daten aus der Trakt-API zu bekommen benutze ich trakt.py. Wieso alles selber bauen wenn es dieses schöne Modul gibt? Dann benutze ich Bokeh um das ganze darzustellen.

In [1]:
from random import choice

import pandas as pd
from trakt.tv import TVShow
from bokeh.plotting import figure, ColumnDataSource, output_notebook, show
from bokeh.models import HoverTool
from bokeh.palettes import Spectral9

Wir starten die Bokeh Ausgabe in einem jupyter-Notebook.

In [2]:
output_notebook()
BokehJS successfully loaded.

Hier ist die Funktion die die Daten besorgt, verarbeitet und darstellt.

In [3]:
def ratings(show_name):
    
    # Es wird die Show definiert
    tv_show = TVShow(show_name)
    
    # Um die Staffeln ausseinander zu halten, soll jede Episode eine unterschiedliche 
    # Staffelfarbe haben. Bokeh hat ein paar Farbpaletten dabei. Ich benutze Spectral9.
    # Dazu brauchen wir noch eine liste um alle Episoden-Staffel-Farben zu speichern
    colormap = Spectral9
    colors = []
    
    # Wir brauchen eine Episoden Liste
    episodes = []
    
    # Damit die Episoden hintereinander auf der X-Achse dargestellt werden, brauchen sie
    # eine fortlaufende Nummer
    episode_number = 1
    
    # Dann arbeiten wir uns mal durch die Staffeln
    for season in tv_show.seasons:
        
        # Season 0 beinhaltet die Specials. Die werden ingnoriert
        if season.season == 0:
            continue
        
        # Eine Staffel-Farbe wird per Zufall ausgewaehlt. Wir schmeissen immer die letzte Farbe
        # aus einer Kopie der Farbenliste damit sie nicht nochmal gewaehlt wird
        if colors:
            new_colormap = colormap[:]
            new_colormap.remove(colors[-1])
            color = choice(new_colormap)
            
        else:
            color = choice(colormap)
        
        # Die Episoden 
        for episode in season.episodes:
            
            # Wenn es eine Ausstrahlungsdatum gibt, wird der Zeit-Teil weggeschnitten
            if episode.first_aired:
                aired = episode.first_aired.split('T')[0]
            else:
                aired = None
            
            # Nun werden die gesammelten Daten als Tupel der Episodenliste angehangen
            episodes.append((episode_number,
                             episode.season,
                             '{}.{}'.format(episode.season,
                                            episode.number), 
                              episode.title, 
                              aired, 
                              episode.rating))
            
            # Die Episoden-Staffel-Farbe wird der Farbenliste angehangen
            colors.append(color)
            
            # Die Episodennummer wird erhoeht
            episode_number += 1
                
    # Es wird ein DataFrame erstellt
    df = pd.DataFrame(
        episodes, 
        columns=['episode_number', 'season', 'episode', 'title', 'aired', 'rating'])

    # Es wird ein source-Objekt gebraucht damit die tooltips funktionieren.
    # Der DataFrame genuegt nicht um die Daten fuer die tooltips zu benutzen
    source = ColumnDataSource(data=df)
    
    # Wir erstellen in figure und belabeln die Achsen
    fig = figure(title=show_name)
    fig.xaxis.axis_label = 'Episodes'
    fig.yaxis.axis_label = 'Rating'
    
    # Hier bauen wir den Scatter-Plot. Wir uebergeben X und Y plus unser source-Objekt
    # und Style Geschichten. Unter anderem unsere Episoden-Farbenliste
    fig.scatter(df['episode_number'], df['rating'], 
                source=source, name='main', 
                fill_alpha=0.8, line_color='#000000', size=10, color=colors)
    
    # Ein HoverTool-Object in dem wir das Layout fuer die Tooltips festlegen
    hover = HoverTool(tooltips=[('Episode', '@episode'),
                                ('Title', '@title'),
                                ('Rating', '@rating'),
                                ('Aired', '@aired')])
    
    # Das Tool wird hinzugefuegt
    fig.add_tools(hover)
    
    # Und nun angezeigt
    show(fig)

Ich gebe eigentlich nicht viel auf die Ratings. Ich selber habe über Trakt noch nie etwas bewertet. Trotzdem handelt es sich um gute Daten um ein paar Sachen auszuprobieren.

Hier ein paar Ratings von Serien die mit entweder viel bedeuten oder die ich gerade schaue.

Roswell

In [4]:
ratings('Roswell')

Die erste Serie bei der ich von einem persönlichen Fandom sprechen kann. Noch bevor ich wusste das es das Wort gibt. Ich habe damals sogar alle Bücher gelesen. Kurz hatte ich nachgedacht sie wegzuschmeissen. Leicht peinlich berührt meiner Serienliebe von damals. Habe mich doch dagegen entschieden. Die erste Staffel war grossartig. Der perfekte Mix aus Highschool und SciFi/Mystery. Danach nahm es für mich drastisch ab. Die dritte Staffel habe ich noch nicht einmal schauen können. Zu gross war die Angst vor dem Zerstören meiner Liebe zur Serie.

Veronica Mars

In [5]:
ratings('Veronica Mars')

Vielleicht meine absolute Lieblingsserie. Veronica Mars habe ich vielleicht 4 oder 5 mal durchgeschaut. Und ich glaube dabei wird es nicht bleiben. Es so eine Serie die immer herhalten muss wenn es mir mal nicht so gut geht. Ein paar Folgen Veronica Mars sind immer eine gute Ablenkung. Alle drei Staffeln haben ungefähr die gleiche Bewertung. Dies deckt sich auch mit meiner Meinung. Drei gleich starke Staffeln. Wieso nur wurde die Serie abgesetzt? Das College-Szenario hätte noch soviel zu bieten gehabt.

Akte X

In [6]:
ratings('The X-Files')

Mit einem Knall wurde auf einmal eine zehnte Staffel angekündigt und gestern lief davon die dritte Folge. Auch wenn ich nur ein wenig mit Akte X vertraut bin, die drei Episoden haben mir bis jetzt sehr gefallen. Anscheinend alle Staffeln mit gleichen Höhen und Tiefen.

Downton Abbey

In [7]:
ratings('Downton Abbey')

Ich hätte nicht gedacht das mich diese Geschichte so packt und emotional macht. Doch Downton Abbey wurde für mich zu einer Herzensangelegenheit. Jeden Staffeln mit viel Tränen. Den Abfall der Ratings nach hinten decken sich nicht mit meiner Meinung. Ich fand alle 6 ziemlich stark.

Californication

In [8]:
ratings('Californication')

Eher so mal reingeschnuppert weil auf Netflix. Teilweise sehr unterhaltsam. Sonst kann ich nicht viel darüber sagen. Bin auch erst bei Staffel 4. Danach war ich wieder gesund und hatte keine Zeit mehr ;-).

Modern Family

In [9]:
ratings('Modern Family')