Pythonista: Zitatesammlung

Als ich morgens in der Ringbahn durch den App Store klickte, fielen mir einige Apps auf, die nichts anderes machten, als ein paar Zitate anzuzeigen. Da dachte ich mir, so etwas doch auch mal in Python klöppeln zu können. Gesagt, getan. Zumindest der Anfang ist gemacht.

# -*- coding: utf-8 -*-
from scene import *
from random import randrange, choice
 
class MyScene (Scene):
	def setup(self):
		self._changebackground()
		self._shownewquote()
		
	def _changebackground(self):
		background(randrange(50, 90)/100.0, 
		           randrange(50, 90)/100.0,
		           randrange(50, 90)/100.0)
	
	def _truncatestring(self, s, length=25):
		# Split into quote and author
		s, author = s
		# Truncate quote to fixed length, but
		# keeping word boundaries.
		output = []
		while len(s) > 0:
			if len(s) < length:
				output.append(s)
				break
			elif s[length - 1] == ' 'and len(s) > length:
				output.append(s[:length])
				s = s[length:]
			else:
				adjust = s[length:].find(' ')
				if adjust == -1:
					output.append(s)
					s = ''
				else:
					output.append(s[:length + adjust])
					s = s[length + adjust + 1:]
		# Strip all unnecessary blank spaces
		# from resulting strings.
		output = [line.strip() for line in output]
		# Righta-djust author and append to output
		w = max([len(data) for data in output])
		output.append(author.rjust(w))
		return '\n'.join(output)
		
	def _shownewquote(self):
		text(self._truncatestring(rq.next(), 20), x=self.size.w/2, y=self.size.h/2, font_name='DejaVuSansMono', font_size=12)
	
	def touch_began(self, touch):
		self._changebackground()
		self._shownewquote()
 
class RandomQuotes():
	def __init__(self):
		self.quotes = list(open('quotes.txt'))
		self.currentquote = self._newquote()
 
	def next(self):
		while True:
			next = self._newquote()
			if next != self.currentquote:
				self.currentquote = next
				break
		return self.currentquote
		
	def _newquote(self):
		 return choice(self.quotes).split('#-#')
		
if __name__ == '__main__':
	rq = RandomQuotes()
	run(MyScene())

Das Script nimmt sich aus einer weiteren Textdatei (quotes.txt), die auch auf Gist liegt, einige Zitate* und gibt sie dann aus. Bei jedem Touch auf das Display wird ein neues Zitat angezeigt und die Hintergrundfarbe geändert. Feinschliff ist noch überhaupt nicht angesetzt, dafür musste ich zu früh aus der Bahn aussteigen. Aber es tut immerhin ganz ordentlich.

* Die Zitate selbst stammen von Wikiquote.

Drafts-Rezepte IX: Serverstatus bei Github

Da auch den guten Leuten von github mal die Server wegbrechen, gibt es dort eine kleine API. Es ist nur eine klitzekleine Schnittstelle, aber sie wirft raus, wie es gerade und in letzter Zeit um die Erreichbarkeit von github steht. Also basteln wir uns doch mal ein kleines Script, mit dem wir die Daten abrufen können.

from console import alert
from json import loads
from sys import exit
from urllib import urlopen, quote
import webbrowser

def error_dialog(title, message):
    '''A diaolog box for error messages.'''
    try:
        alert(title, message)
    except KeyboardInterrupt:
        pass
    webbrowser.open('drafts://')
    exit(message)

def formatdate(date):
    '''bring the date into shape'''
    return date[:-1].replace('T', ' ')

def request(method):
    '''a request to github's status API.'''
    api_url_base = 'https://status.github.com/api/{method}'
    try:
        response = urlopen(api_url_base.format(method=method))
    except IOError:
        error_dialog('Connection Error', 'Unable to perform request.')
    if response.getcode() != 200:
        error_dialog('Error', 'Status code: {0} - Message: {1}'.format(response.getcode(), response.read()))
    return loads(response.read())

def github_status():
    '''get information and admin messages for github servers.'''
    status = request('status.json')
    messages = request('messages.json')
    output = '''
GITHUB STATUS
================
{date}: {status}

Last messages:
    '''.format(date=formatdate(status['last_updated']),
               status=status['status'])
    for item in messages:
        output += '\n{date}: {status}\n{comment}\n'.format(date=formatdate(item['created_on']),
                                                             status=item['status'],
                                                             comment=item['body'])
    output += '\nFor further information: https://status.github.com/'
    webbrowser.open('drafts://x-callback-url/create?text={0}'.format(quote(output)))

if __name__ == '__main__':
    github_status()

Gist

Die Action in Drafts ist auch keine Neuerung mehr.

pythonista://githubstatus?action=run

Import-Link

Seitdem Drafts auch Actions ausführt, wenn ein Draft keine Zeichen enthält, muss auch nur die Action gefeuert werden. Daraufhin rödelt Pythonista etwas rum, wirft dann aber das Ergebnis als Text in einen neuen Draft aus. Darin stehen dann in knapper Form die Informationen.

Drafts-Rezepte VII: Postleitzahlen mit Zippopotamus II

Der zweite Teil zu Postleitzahlen mit Zippopotamus geht in die entgegengesetzte Richtung. Statt nun also eine Postleitzahl zu ermitteln, geht es nun um die Städte oder Stadtteile hinter eine Postleitzahl.

Das Script ist wieder genauso einfach wie das erste. In Drafts muss eine neue Notiz nur die folgende Syntax einhalten:

[PLZ],[LÄNDERKÜRZEL]

Beispielsweise würde die folgende Eingabe der berühmten Postleitzahl eine Markdown-Liste in Drafts öffnen.

90210 , us

Auch hier kommt ein kleiner Haken ins Spiel, auch wenn er nicht ganz so lästig ist. Denn die API von Zippopotamus erlaubt es derzeit nicht, das Land nicht anzugeben. Unter Umständen müssen wir also mehr Infos angeben, als wir haben. Das Python-Script sieht derzeit so aus:

# -*- coding: utf-8 -*-
from console import alert
from json import loads
from sys import argv, exit
from urllib import urlopen, quote
import webbrowser

def error_dialog(title, message):
    '''
    a diaolog box for error messages.
    '''
    try:
        alert(title, message)
    except KeyboardInterrupt:
        pass
    webbrowser.open('drafts://')
    exit(message)

def handle_data(data):
    '''
    process json response from zippopotamus.
    will return a markdown list of items.
    '''
    city_json = loads(data)
    output = ''
    for item in city_json['places']:
        output += '- Ort: {place}, Region: {state}\n'.format(place=item['place name'], state=item['state'])
    return output

def get_by_postalcode(data):
    '''
    get all possible cities for a postal code in
    the given country.
    '''
    api_url_base = 'http://api.zippopotam.us/{country}/{postcode}'
    try:
        postcode_, country_= [item.strip() for item in data.split(',')]
    except Exception as err:
        error_dialog(str(err.__class__), err.message)

    try:
        response = urlopen(api_url_base.format(country=country_, postcode=postcode_))
    except IOError:
        error_dialog('Connection Error', 'Unable to perform request.')
    if response.getcode() == 200:
        postcode_data = handle_data(response.read())
        webbrowser.open('drafts://x-callback-url/create?text={0}'.format(quote(postcode_data)))
    else:
        error_dialog('Error', 'Status code: {0} - Message: {1}'.format(response.getcode(), response.read()))

if __name__ == '__main__':
    get_by_postalcode(argv[1])

Gist

Die dazugehörige Action für Drafts ist:

pythonista://city-by-postalcode?action=run&argv=[[draft]]

Import-Link

Drafts-Rezepte VI: Postleitzahlen mit Zippopotamus I

Den kleinen Gag kann ich mir nicht verkneifen. Ich mache zu Zippopotamus, einem guten, kleinen Dienst für Postleitzahlen in einer Vielzahl von Ländern, zwei Teile. Damit artet die Kleinteiligkeit aus wie der überbordende Song-Kosmos von Coheed & Cambria.

Das Script ist wieder sehr einfach. In Drafts muss eine neue Notiz nur die folgende Syntax einhalten:

[STADT], [REGIONSKÜRZEL], [LÄNDERKÜRZEL]

Beispielsweise würde die folgende Eingabe die berühmte Postleitzahlen für die Stadt als Markdown-Liste in Drafts öffenen.

beverly hills, ca, us

Leider kommt diese Sache nicht ohne einen kleinen Haken aus. Denn die API von Zippopotamus erlaubt es derzeit nicht, die Region und das Land nicht anzugeben. Unter Umständen müssen wir also mehr Infos angeben, als wir haben. Ich bin dabei, mir dafür eine Lösung zu überlegen. Das Python-Script sieht derzeit so aus:

# -*- coding: utf-8 -*-
from console import alert
from json import loads
from sys import argv, exit
from urllib import urlopen, quote
import webbrowser

def error_dialog(title, message):
    '''
    a diaolog box for error messages.
    '''
    try:
        alert(title, message)
    except KeyboardInterrupt:
        pass
    webbrowser.open('drafts://')
    exit(message)

def handle_data(data):
    '''
    process json response from zippopotamus.
    will return a markdown list of items.
    '''
    city_json = loads(data)
    output = ''
    for item in city_json['places']:
        output += '- City: {place}, Post code: {postcode}\n'.format(place=item['place name'], postcode=item['post code'])
    return output

def get_by_city(data):
    '''
    get all possible post codes for a city in
    the given country.
    '''
    api_url_base = 'http://api.zippopotam.us/{country}/{state}/{city}'
    try:
        city_, state_, country_= [item.strip() for item in data.split(',')]
    except Exception as err:
        error_dialog(str(err.__class__), err.message)

    try:
        response = urlopen(api_url_base.format(country=country_, state=state_, city=city_))
    except IOError:
        error_dialog('Connection Error', 'Unable to perform request.')
    if response.getcode() == 200:
        city_data = handle_data(response.read())
        webbrowser.open('drafts://x-callback-url/create?text={0}'.format(quote(city_data)))
    else:
        error_dialog('Error', 'Status code: {0} - Message: {1}'.format(response.getcode(), response.read()))

if __name__ == '__main__':
    get_by_city(argv[1])

Gist

Die dazugehörige Action für Drafts ist hier:

pythonista://postalcode-by-city?action=run&argv=[[draft]]

Import-Link

Drafts-Rezepte V: Wetterdaten mit OpenWeatherMap

Ich habe mir wieder ein kleines Skript (current_weather.py) gebastelt, mit dem ich für einen beliebigen Ort die aktuelle Temperatur und Luftfeuchtigkeit erhalte. Als Datenquelle dient die API von OpenWeatherMap, abgerufen wird es wieder über Pythonista. Das Skript sieht folgendermaßen aus:

# -*- coding: utf-8 -*-
import clipboard
from console import alert
from json import loads
from sys import argv, exit
from urllib import urlopen, quote
import webbrowser

def error_dialog(title, message):
  '''A diaolog box for error messages.'''
	try:
		alert(title, message)
	except KeyboardInterrupt:
		pass
	webbrowser.open('drafts://')
	exit(message)

def filter_data(data):
	'''create output string from response'''
	weather = loads(data)
	temperature = weather['main']['temp']
	humidity = weather['main']['humidity']
	output = 'Temperatur: {0} Celsius\nLuftfeuchtigkeit: {1}%'.format(temperature, humidity)
	return output

def get_current_weather_in(data):
	'''get current weather data'''
	api_url_base = 'http://api.openweathermap.org/data/2.5/weather?q={0}&units=metric&lang=de'
	try:
		response = urlopen(api_url_base.format(data))
	except IOError:
		error_dialog('Connection Error', 'Unable to perform request.')
	if response.getcode() == 200:
		weather_data = filter_data(response.read())
		webbrowser.open('drafts://x-callback-url/create?text={0}'.format(quote(weather_data)))
	else:
		error_dialog('Error', 'Status code: {0} - Message: {1}'.format(response.getcode(), response.read()))

if __name__ == '__main__':
	get_current_weather_in(argv[1])

Gist

Das Skript kann dann über die Drafts-Action ausgeführt werden:

pythonista://current_weather?action=run&argv=[[draft]]

Import-Link

Im Grunde war das auch schon alles. In Drafts muss dann in einer neuen Notiz nur der Name der Stadt eingegeben werden. Gegebenenfalls sollte sicherheitshalber noch das Land hinzugefügt werden. So würde Hamburg, de mir die aktuelle Temperatur und Luftfeuchtigkeit für Hamburg geben.

Drafts-Rezepte IIII: Hashes berechnen

Dann geht es fröhlich in der Reihe kleiner Skripte für Drafts und Pythonista weiter. Da ich hin und wieder mal Hashes als Prüfsummen benötige, hatte ich immer eigene Apps dafür auf dem iPhone. Da diese allerdings für ihren eigentlichen Zweck zu groß waren – sie waren allesamt auch noch hässlich -, habe ich sie nun durch folgendes Skript ersetzt. Das läuft bei mir ganz ordentlich.

Hierzu brauche ich wieder einen Action in Drafts, die in diesem Fall auf das Skript hasher in Pythonista zugreifen soll. Das sieht so aus:

pythonista://hasher?action=run&argv=[[draft]]

Import-Link

Etwas umfangreicher, daher auch erklärungsbedürftiger ist das Skript in Python. Dieses habe ich mal im Grunde angelegt, dass es wie ein Programm für die Shell genutzt werden kann.

# -*- coding: utf-8 -*-
import argparse
import clipboard
import hashlib
from sys import argv
import webbrowser

def hash_data(hashstring, hashfunction, url):
    '''Get the hash of given string.'''
    # Determine if a method for hashfunction
    # exists in hashlib. If no attribute/method is
    # found, default to sha1.   
    if hasattr(hashlib, hashfunction):
      hash_method = getattr(hashlib, hashfunction)
    else:
    	hash_method = hashlib.sha1

    # Put hash to clipboard, if hashstring exists.
    if hashstring:
    	clipboard.set(hash_method(hashstring).hexdigest())
    else:
        raise ValueError

    # Pythonista doesn't support x-callback.
    # So this is a pragmatic approach to calling
    # another app after hashing the string.
    webbrowser.open(url)

def parse_input(data):
    '''Parse input from Drafts command-line-like.'''
    parser = argparse.ArgumentParser(description='input a string to hash.')

    # Expects strings to hash.
    parser.add_argument('inputstring',
                        metavar='STRING',
                        nargs='*',
                        help='the string to hash')

    # Set the hash function.
    parser.add_argument('-hs', '--hs', '-hash', '--hash',
                        metavar='HASH-NAME',
                        default='sha1',
                        dest='hash',
                        help='the hash function of hashlib to use. defaults to sha1')

    # Intended to set a callback-like action.
    # Use to open a specific app via url scheme, if necessary. Otherwise will open Drafts.
    parser.add_argument('-u', '--u', '-url', '--url',
                        metavar='URL',
                        default='drafts://',
                        dest='url',
                        help='url scheme to call after hashing. use to call an app.')

    args = parser.parse_args(data)
    hash_data(' '.join(args.inputstring), args.hash, args.url)

if __name__ == '__main__':
    parse_input(argv[1].split(' '))

Gist

Ich muss also etwas erklären, was das Skript macht. Es kann drei Sorten von Input verarbeiten:

-hs (–hs, -hash, –hash) HASH_BEZEICHNUNG: Dieser Befehl ist optional und gibt die zu verwendenden Hashfunktion* an. Ist die Bezeichnung unbekannt oder falsch, wird der Standardwert ’sha1′ angenommen.

-u (–u, -url, –url) URL_SCHEMA: Dieser Befehl ist optional und gibt das URL-Schema einer aufzurufenden Anwendung an. Standardmäßig ist ‚drafts://‘ vorgegeben.

inputstring *STRING: Alle übrigen Übergabewerte werden zum Inputstring hinzugefügt, der gehasht wird.

Es lässt sich doch besser mit einigen Beispielen erklären. Nehmen wir mal folgenden Eingaben in Drafts an, wobei jede Zeile einer Eingabe entspricht:

Test

Test -s md5

Test -u tweetbot://

Test und noch mehr Test -s sha512

Die erste Eingabe würde den SHA1-Hash von ‚Test‘ berechnen, die zweite würde denselben String als MD5-Hash berechnen. Beide Eingaben führen dazu, dass nach der Berechnung in Pythonista wieder Drafts aufgerufen wird. Die dritte Eingabe berechnet den Standard, also SHA1, von ‚Test, kehrt aber nicht nach Drafts zurück, sondern öffnet Tweetbot (sofern es installiert wurde). Die letzte Eingabe nimmt ‚Test und noch mehr Test‘ und berechnet den SHA512-Hashwert für die Zeichenfolge.

* Laut der Dokumentation für Python 2.7, das von Pythonista genutzt wird, werden folgende Hashfunktionen unterstützt:

This module implements a common interface to many different secure hash and message digest algorithms. Included are the FIPS secure hash algorithms SHA1, SHA224, SHA256, SHA384, and SHA512 (defined in FIPS 180-2) as well as RSA’s MD5 algorithm (defined in Internet RFC 1321).

Drafts-Rezepte II: Kurzlinks mit is.gd

Es ist ja durchaus sinnvoll, Links ab und an mal zu verkürzen, jedoch dafür extra eine App zu installieren erscheint mir etwas übertrieben. Das lässt sich zwar auch im Browser erledigen, artet aber in eine Klickorgie aus, auf die ich auch keine Lust habe. Mit Pythonista und Drafts ist das im Handumdrehen erledigt.

Dieses kleine Script habe ich als isgd.py im Dokumentenverzeichnis von Pythonista angelegt:

import clipboard
from console import alert
from sys import argv, exit
from urllib import urlopen
import webbrowser

def error_dialog(title, message):
	'''A diaolog box for error messages.'''
	try:
		alert(title, message)
	except KeyboardInterrupt:
		pass
	webbrowser.open('drafts://')
	exit(message)

def shorten_with_isgd(long_url):
	'''basic link-shortening via is.gd.'''
	api_url_base = 'http://is.gd/create.php?format=simple&url='
	try:
		response = urlopen(api_url_base + long_url)
	except IOError:
		error_dialog('Connection Error', 'Unable to perform request.')
	if response.getcode() == 200:
		short_link = response.read()
		clipboard.set(short_link)
	else:
		error_dialog('Error', 'Status code: {0} - Message: {1}'.format(response.getcode(), response.read()))
	webbrowser.open('drafts://')

if __name__ == '__main__':
	shorten_with_isgd(argv[1])

Gist

Es ist alles recht simpel gehalten, die Fehler werden nur rudimentär behandelt, aber es sollte ganz gut aufgehen. is.gd habe ich für den Anfang als Dienst gewählt, weil dort nicht großartig Registrierungen notwendig sind, um mit der API Links zu kürzen.

In Drafts muss dann nur die folgende Action angelegt werden, um das Script auszuführen.

pythonista://isgd?action=run&argv=[[draft]]

Import

Die Handhabung ist nun denkbar einfach: In Drafts in einer neuen Notiz die lange URL eingeben, danach die Action auslösen. Es wird sich Pythonista öffnen. Sofern keine Fehlermeldung aufpoppt, ist der Kurzlink in die Zwischenablage kopiert worden. Auf jeden Fall wird aber wieder Drafts geöffnet.

Drafts: Ich automatisiere mich in Grund und Boden

Seit gestern bastele ich mit Drafts für iOS herum, das nicht mit Draft für Android zu verwechseln ist. Drafts ist weit mehr als ein Texteditor, was mir bisher aber nicht aufgegangen ist. Ich hatte übersehen, dass Drafts zu einer Schaltzentrale für Texte und die automatisierte Verarbeitung wird. Drafts unterstützt nicht nur URI Schemes, eigentlich basiert die ganze App darauf, sich über das Schema einen Workflow zu erstellen, bei dem verschiedene Apps, sofern sie das Schema unterstützen, miteinander verzahnt werden.

Eine simple Anwendung von Drafts‘ Fähigkeiten ist es beispielsweise, einen Text zu erstellen, der per selbstgeschriebener URL-Schema-Aktion bei mehreren Microblogging-Diensten gleichzeitig gepostet wird. Wir bauen uns also einen einen Kettenblitz für Twitter und app.net, wobei wir im URL-Schema einen Callback in einen Callback bauen:

drafts://x-callback-url/create?text=[[draft]]&action={{Tweet: DoctorProk}}&afterSuccess=Delete&x-success={{drafts://x-callback-url/create?text=[[draft]]&afterSuccess=Delete&action=Post%20to%20App.net}}

Damit schlage ich zwei Fliegen mit einer Drafts-Klappe. Es ist alles noch einwenig holprig in der Kette, tut seinen Dienst aber schon mehr als ordentlich.

Je umfangreicher die Callback-Kette wird, desto unübersichtlicher wird es natürlich. Mit der Zeit entstehen so URL-Schlangen, die an die abstrakte Kunst erinnern, die von BWLern in Excel fabriziert werden, weil sie nicht auf VBA umschwenken wollen. Diese Ungetüme kann kein Mensch mehr warten. Damit hat Drafts also doch eine natürliche Grenze bei den Aktionen, sobald es sich nicht mehr realistisch überblicken und warten lässt. Nicht ganz.

Es gibt noch ein weiteres Workflow-Genie auf iOS. Pythonista ist eine Perle. Schöner, praktischer und schneller geht Python auf Mobilgeräten nicht. Und manche übersehen schnell Pythonista ausgefeilte URL-Schema-Schnittstelle. Jedes Skript in Pythonista ist von externen Apps ausgührbar, sogar mit Argumenten. So können komplexere Aufgaben leicht und locker an Pythonista ausgelagert werden. Ist das Skript fertig, ruft es einfach Drafts auf, übergibt die Bearbeitung und es kann wieder weitergehen.

Mit Drafts wir schon vieles einfacher, zusammen mit Pythonista wird aus Textverarbeitung auf iOS ganz schnell elaboriertes Anrühren von Zaubertränke mit komplizierten Ingredenzien.

Databin: Tabellen für Freunde

Wenn mir der Schweiß nicht auf der Stirn stünde, ich würde mehr zu Databin schreiben. Ich befürchte ja, was da verdunstet, ist Lebenskraft, nicht Wasser. Was wollte ich sagen? Ja, genau: Databin ist ein kleines Örtchen, an dem schnell mal Tabellen abgeladen werden können, die ins Silo rauschen und sofort in ganz brauchbare Form fallen. Ich weiß auf jeden Fall, wofür so was gut sein kann.

databin_screenshot

Ich darf sogar an nichts rummeckern, denn der Quellcode liegt hier, und da es im hübschen Python daherkommt, ist zumindest nicht ausgeschlossen, dass ich es an meine Bedürfnisse anpassen könnte. Pastebin für Daten ist ein schöne Sache. Aber jetzt: Eisdusche.

Pythonista: Längere Leine unter iOS

Die Botschaft in kurz lautet: Pythonista ist ein in Erfüllung gegangener Traum. Aber der Reihe nach, ich greife mir selbst voraus. Der Traum drehte sich darum, auf iOS-Geräten eine lauffähige Python-Umgebung zu haben. Dieser Traum war bescheiden, ging es doch nur darum, mal eben ein paar Zeilen testen zu können. Einfach mal eine Eingebung in Python skizzieren, damit sie später ausgearbeitet werden kann, das wäre schön genug. Es gibt da auch einige Möglichkeiten wie etwa Python for iOS, aus diversen Gründen fühlten sich diese nie auch nur ansatzweise rund an. Es klemmte an vielen Ecken.

Pythons großes Plus, neben vielen anderen, die um den Titel der größten Stärke konkurrieren, ist die konzeptionelle Schlichtheit der Sprache. Alles in Python ist auf Effizienz getrimmt, wobei die Syntax dabei im Vorbeigehen schönen Code produziert. Python ist ebenso elegant wie schlicht, keine der Apps für iOS hat diese Eigenschaften Pythons bisher zur Geltung gebracht. Manche nennen es das Zen von Python, was begrifflich mindestens eine Etage oberhalb dessen ist, was meine absolute Schmerzgrenze ist; ich teile also den Begriff nicht, weiß aber genau, was gemeint ist. Wie auch immer ich es nennen würde, Pythonista hat es.

Wo anfangen bei aller Begeisterung? Pythonistas Code-Editor ist so bequem wie es eben geht auf einem mobilen Gerät mit virtueller Tastatur. Der Editor verfügt über Syntax-Highlighting, was ich mindestens erwarte, die Themes sind dann schon eher schönes Beiwerk. Doch die Code-Vervollständigung ist eine Erleichterung, auf die zu hoffen ich nicht gewagt hätte. Über der erweiterten Tastaturleiste, in der syntaxrelevante Zeichen schneller zu erreichen sind, blitzen bei Bedarf einfach die Schlüsselwörter und Modulnamen auf. Antippen, weiter coden. Auf dem Desktop erwarte ich das von der IDE meiner Wahl, in Pythonista beeindruckt es mich wegen der Hilfe, die es ist, aber auch wegen der trotz aller Funktionalität immer aufgeräumten Darstellung. Es wirkt einfach nicht überfrachtet, nichts fehlt und alles hat seinen Platz.

Und so bedienerfreundlich geht es weiter, da bringt Pythonista nicht nur die ausführliche Sprachdokumentation mit, sondern wertet auch diese weiter auf. Beispielsweise können Code-Snippets aus der Dokumentation heraus sofort im Editor geöffnet, was mir Trial-and-error-Lerner ordentlich Unterstützung bietet. Es ist fast so, als komme Pythonista immer den einen Schritt extra entgegen, der mich wohlfühlen lässt. Daher verwundert es kaum noch, muss aber umso deutlicher unterstrichen werden, dass Pythonista nicht nur die umfassende Sammlung der Module aus der Standardbibliothek mitliefert, sondern auch noch nützliche Module, die sich als Quasi-Standards etabliert haben. Requests, BeautifulSoup, feedparser, PIL oder Dropbox sind im alltäglichen Gebrauch unverzichtbar geworden, daher werden sie in Pythonista unterstützt. Aber heißt das jetzt wirklich das, was sich einige jetzt darunter schon vorstellen können?

Genau das heißt es. Pythonista ist kein einfacher Editor mit eingebautem Prompt, der unter iOS in seiner eigenen Kapsel agiert. Die Abschottung von der Außenwelt reißt Pythonista nieder. Natürlich können damit HTTP-Requests an Web-APIs geschickt oder eben grundsätzliche Bildverarbeitungsschritte automatisiert werden. Spätestens an dieser Stelle war ich von Pythonista und seinen Möglichkeiten gebannt. Doch da hört es noch nicht auf. Es gibt aber noch eine dritte Sammlung von Modulen, die Pythonista vollends zum unverzichtbaren Scripting-Tool auf iOS macht. In eigens für die App geschriebenen Modulen wird die Interaktion mit dem Betriebssystem – so weit es Apple erlaubt – möglich. Die Zwischenablage ist nun per Script zugänglich, Töne können ausgegeben werden und auch einige der bekannten Systemdialoge stehen bereit. Aber das ist noch immer nicht das Ende, denn Pythonista stellt noch einen Canvas zur Verfügung, auf dem eigene UI möglich wird, mitsamt der nötigen Touchkontrolle.

Ich fasse zusammen: Erstklassiger Editor, hervorragende Dokumentation, Zusatz-Module, das alles bietet Pythonista und setzt trotzdem noch einen drauf. Denn der Editor selbst ist erweiterbar, schließlich bringt Pythonista noch ein Modul für den Editor selbst mit, damit kann die Arbeit im noch an eigene Bedürfnisse und Gewohnheiten angepasst werden. In den Einstellungen werden die Scripts dann als Erweiterung des Editors eingebettet, nichts leichter als das. Pythonista gibt damit ein Stück Freiheit unter iOS zurück, die ich oft schmerzlich vermisst habe.

Dabei ist noch nicht alles perfekt, aber meist liegt das an Beschränkungen durch Apple. Zwar könnte ich prinzipiell damit Spiele und Apps erstellen, oder immerhin Prototypen davon, aber die Interaktion zwischen den Apps ist, von Apple gewollt, dürftig. Die kurze Leine, an die Apple uns bietet, nutzt Pythonista immerhin voll aus, also auch das URL-Schema für rudimentären Datenaustausch zwischen einzelnen Apps. Deshalb kann ich mir nun immerhin Scripte schreiben, die meine tägliche Arbeit auf dem iPhone oder iPad erleichtern. Aber nicht nur mir geht das so, auch andere haben die Möglichkeiten erkannt. Und im Forum schlagen schon massenweise grandiose Scripte auf, die noch viel mehr versprechen.Da liegen schon erste Ansätze für Datei-Manager, Spiele, Heimautomatisierung über WLAN mit dem Raspberry Pi sowie Editorerweiterungen. Auf einmal kann ich dank Pythonista auf iOS von ganz anderen Dingen träumen, die Freiheit ist wieder da.