Publicaciones sobre open source

APIs de Ubuntu One en Ejemplos (parte 1)

Una de las cosas lindas de trabajar en Canonical es que producimos software open source. Yo, específicamente, trabajo en el equipo que hace los clientes de escritorio de Ubuntu One que es un trabajo muy copado, y un software muy copado también. Sin embargo, una cosa que no se sabe lo suficiente es que tenemos unas excelentes APIs para terceros. Las necesitamos, ya que es lo que usamos nosotros!

Así que acá va un pequeño tutorial acerca de como usar algunas de esas APIs. Lo hice usando Python y PyQt por varios motivos:

  • Son excelentes herramientas para prototipos
  • Tienen excelente soporte para las cosas que necesito (DBus, HTTP, OAuth)
  • Es lo que sé y me gusta. Lo hice un domingo, no lo pienso hacer en PHP y Gtk.

Dicho eso, no hay nada específico de python o de Qt en este código. Donde hago un request HTTP usando QtNetwork, podés usar libsoup o lo que fuere.

Vayamos a los bifes entonces. Las piezas más importantes de Ubuntu One, desde el punto de vista de infraestructura, son Ubuntu SSO Client, que se encarga de login, registración, etc, y SyncDaemos que maneja la sincronización de archivos.

Para interactuar con ellas, en Linux, ofrecen interfaces DBus. Así que, por ejemplo, este es un fragmento mostrando como obtener las credenciales de Ubuntu One (esto normalmente sería parte del __init__ de un objeto):

# Get the session bus
bus = dbus.SessionBus()

:
:
:

# Get the credentials proxy and interface
self.creds_proxy = bus.get_object("com.ubuntuone.Credentials",
                        "/credentials",
                        follow_name_owner_changes=True)

# Connect to signals so you get a call when something
# credential-related happens
self.creds_iface = dbus.Interface(self.creds_proxy,
    "com.ubuntuone.CredentialsManagement")
self.creds_proxy.connect_to_signal('CredentialsFound',
    self.creds_found)
self.creds_proxy.connect_to_signal('CredentialsNotFound',
    self.creds_not_found)
self.creds_proxy.connect_to_signal('CredentialsError',
    self.creds_error)

# Call for credentials
self._credentials = None
self.get_credentials()

Tal vez notaste que get_credentials no devuelve las credenciales. Lo que hace es, le dice a SyncDaemon que las obtenga, y entonces, si/cuando aparecen, se emite una de esas señales, y uno de los métodos conectados se llama. Esto está bueno porque no tenemos que preocuparnos de que se nos bloquee la aplicación mientras SyncDaemon está buscando las credenciales.

¿Y qué hay en esos métodos? ¡No mucho!

def get_credentials(self):
    # Do we have them already? If not, get'em
    if not self._credentials:
        self.creds_proxy.find_credentials()
    # Return what we've got, could be None
    return self._credentials

def creds_found(self, data):
    # Received credentials, save them.
    print "creds_found", data
    self._credentials = data
    # Don't worry about get_quota yet ;-)
    if not self._quota_info:
        self.get_quota()

def creds_not_found(self, data):
    # No credentials, remove old ones.
    print "creds_not_found", data
    self._credentials = None

def creds_error(self, data):
    # No credentials, remove old ones.
    print "creds_error", data
    self._credentials = None

Así que básicamente, self._credentials contiene unas credenciales, o None. Felicitaciones, ya entramos a Ubuntu One.

¡Hagamos algo útil! ¿Que tal preguntar cuánto espacio libre hay en la cuenta? Para eso, no podemos usar las APIs locales, si no conectarnos a los servers, que son los que saben si estás excedido de quota o no.

El acceso se controla via OAuth, por lo que para acceder a esa API necesitamos firmar nuestros pedidos. Aquí se ve como se hace. No es particularmente iluminador, yo no lo escribí, solamente lo uso:

def sign_uri(self, uri, parameters=None):
    # Without credentials, return unsigned URL
    if not self._credentials:
        return uri
    if isinstance(uri, unicode):
        uri = bytes(iri2uri(uri))
    print "uri:", uri
    method = "GET"
    credentials = self._credentials
    consumer = oauth.OAuthConsumer(credentials["consumer_key"],
                                   credentials["consumer_secret"])
    token = oauth.OAuthToken(credentials["token"],
                             credentials["token_secret"])
    if not parameters:
        _, _, _, _, query, _ = urlparse(uri)
        parameters = dict(cgi.parse_qsl(query))
    request = oauth.OAuthRequest.from_consumer_and_token(
                                        http_url=uri,
                                        http_method=method,
                                        parameters=parameters,
                                        oauth_consumer=consumer,
                                        token=token)
    sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
    request.sign_request(sig_method, consumer, token)
    print "SIGNED:", repr(request.to_url())
    return request.to_url()

¿Y cómo pedimos el estado de quota? Accediendo al punto de entrada https://one.ubuntu.com/api/quota/ con la autorización adecuada, se obtiene un diccionario JSON con el espacio total y el usado. Acá hay una muestra de como hacerlo:

    # This is on __init__
    self.nam = QtNetwork.QNetworkAccessManager(self,
        finished=self.reply_finished)

:
:
:

def get_quota(self):
    """Launch quota info request."""
    uri = self.sign_uri(QUOTA_API)
    url = QtCore.QUrl()
    url.setEncodedUrl(uri)
    self.nam.get(QtNetwork.QNetworkRequest(url))

De nuevo: get_quota no devuelve la quota. Sólo lanza un pedido HTTP a los servers de Ubuntu One, que (eventualmente) responden con los datos. No querés que tu app se quede ahí trabada mientras tanto, por eso QNetworkAccessManager va a llamar a self.reply_finished cuando tenga la respuesta:

def reply_finished(self, reply):
    if unicode(reply.url().path()) == u'/api/quota/':
        # Handle quota responses
        self._quota_info = json.loads(unicode(reply.readAll()))
        print "Got quota: ", self._quota_info
        # Again, don't worry about update_menu yet ;-)
        self.update_menu()

¿Qué más queremos? ¿Qué tal notificación cuando cambia el status de SyncDaemon? Por ejemplo, cuando la sincronización está al día, o cuando te desconecta. De nuevo, esas son señales DBus a las que uno se conecta en __init__:

self.status_proxy = bus.get_object(
    'com.ubuntuone.SyncDaemon', '/status')
self.status_iface = dbus.Interface(self.status_proxy,
    dbus_interface='com.ubuntuone.SyncDaemon.Status')
self.status_iface.connect_to_signal(
    'StatusChanged', self.status_changed)

# Get the status as of right now
self._last_status = self.process_status(
    self.status_proxy.current_status())

Y esta es status_changed:

def status_changed(self, status):
    print "New status:", status
    self._last_status = self.process_status(status)
    self.update_menu()

la función process_status is código aburrido para convertir la info de status de syncdaemon en una cosa legible como "Sync is up-to-date", así que guardamos eso en self._last_status y actualizamos el menú.

¿Qué menú? ¡Un menú contextual de un QSystemTrayIcon! Lo que leyeron son las piezas principales que se necesitan para crear algo útil: una aplicación de SystemTray para Ubuntu One que se puede usar en KDE, XFCE u Openbox. O, si estás en unity y tenés sni-qt instalado, un app indicator.

http://ubuntuone.com/7iXTbysoMM9PIUS9Ai4TNn

El indicador en acción.

El código fuente del ejemplo completo está en mi proyecto u1-toys en launchpad y éste es el código fuente completo (excepto los iconos, bajense el repo, mejor)

Viniendo pronto (espero), más apps de ejemplo, y cosas copadas que se pueden hacer con nuestras APIs.

Creando un foro de la manera fácil (32 líneas)

Esto es la primera parte de un proyecto para crear el software de foro más sencillo posible (en cierta forma).

Aquí están algunos de los features que quiero:

  • Login via twitter / Facebook / Google / OpenID
  • Número ilimitado de threads
  • Soporte de like / dislike en threads y en posts
  • Avatares
  • HTML en los posts
  • Que mande mail al usuario si le responden
  • Feeds RSS para los threads

Se lo puede ver en acción en http://foro.netmanagers.com.ar (por un tiempo limitado ;-)

Y aquí está el código:

import bottle
import disqusapi as disqus
import json
shortname = 'magicmisteryforum'
api = disqus.DisqusAPI(open("key").read().strip())

@bottle.route('/', method='GET')
def index():
    msg = bottle.request.GET.get('msg', '')
    threads = api.forums.listThreads(forum=shortname, limit=100)
    print threads[0]
    return bottle.template('main.tpl', threads=threads, shortname=shortname, msg=msg)

@bottle.route('/new', method='POST')
def new():
    title = bottle.request.forms.get('title', None)
    if not title:
        bottle.redirect('/?msg=Missing%20Thread%20Name')
        return
    thread = api.threads.create(forum=shortname, title = title)
    thread_id = thread.__dict__['response']['id']
    # Redirecting to /thread/thread_id doesn't work
    # because threads take a few seconds to appear on the listing
    bottle.redirect('/')

@bottle.route('/thread/:id')
def thread(id):
    t = api.threads.details(thread=id)
    return bottle.template('thread.tpl', shortname=shortname, id=id, thread=t.__dict__['response'])

@bottle.route('/static/:path#.+#')
def server_static(path):
    return bottle.static_file(path, root='./static')

app = bottle.app()
app.catchall = False #Now most exceptions are re-raised within bottle.
bottle.run(host='184.82.108.14', port=80, app=app)

Requiere Bottle y la Disqus python API

Por supuesto que hay un poquito de templates, acá está main.tpl y thread.tpl. Como apesto para el HTML, usa Bluetrip CSS y es sencillo de customizar.

POR SUPUESTO QUE HAGO TRAMPA!

Esta cosa es apenas una capa de pintura encima de Disqus! Más un blog sin posts pero con comentarios que un foro! Pero... qué le falta para ser un foro de verdad? Funciona, no? Hasta se podrían usar categorías de Disqus para crear subforos...

Teniendo todo en cuenta, creo que es un hack bonito.

Y si esperás unos días, esto lleva a otra cosa que es mucho más mágica...

Código fuente completo en http://magicforum.googlecode.com

En 128 líneas de código entra exactamente ESTE browser.

Ya posteé un par de veces (1, 2) sobre De Vicenzo , un intento de implementar el resto de un web browser empezando con el WebKit de PyQt... sin pasar las 128 líneas de código.

Por supuesto, podría hacer más, pero hasta yo tengo mis standards!

  • No usar ;
  • No usar if whatever: f()

Salvo eso, hice algunos trucos sucios, pero, en este momento, es un browser bastante completo en 127 líneas de código según sloccount, así que ya jugué suficiente y mañana tengo trabajo que hacer.

Pero antes, consideremos como se implementaron algunos features (voy a cortar las líneas para que la página quede razonablemente angosta), y veamos también las versiones "normales" de lo mismo. La versión "normal" no está probada, avisen si está rota ;-)

Esto noes algo que deba aprenderse. De hecho es casi un tratado en como no hacer las cosas. Es el código menos pitónico y menos claro que vas a ver esta semana.

Es corto, es expresivo, pero es feo feo.

Voy a comentar sobre esta versión.

Soporte deProxy

Un browser no es gran cosa si no se puede usar con proxy. Por suerte el stack de red de Qt tiene buen soporte de proxy. El chiste es configurarlo.

De Vicenzo soporta proxies HTTP y SOCKS parseando la variable de entorno http_proxy y seteando el proxy a nivel aplicación en Qt:

 proxy_url = QtCore.QUrl(os.environ.get('http_proxy', ''))
 QtNetwork.QNetworkProxy.setApplicationProxy(QtNetwork.QNetworkProxy(\
 QtNetwork.QNetworkProxy.HttpProxy if unicode(proxy_url.scheme()).startswith('http')\
 else QtNetwork.QNetworkProxy.Socks5Proxy, proxy_url.host(),\
 proxy_url.port(), proxy_url.userName(), proxy_url.password())) if\
'http_proxy' in os.environ else None

Como es la versión normal de esa cosa?

if 'http_proxy' in os.environ:
    proxy_url = QtCore.QUrl(os.environ['http_proxy'])
    if unicode(proxy_url.scheme()).starstswith('http'):
        protocol = QtNetwork.QNetworkProxy.HttpProxy
    else:
        protocol = QtNetwork.QNetworkProxy.Socks5Proxy
    QtNetwork.QNetworkProxy.setApplicationProxy(
        QtNetwork.QNetworkProxy(
            protocol,
            proxy_url.host(),
            proxy_url.port(),
            proxy_url.userName(),
            proxy_url.password()))

Los abusos principales contra python son el uso del operador ternario para hacer un if de una línea (y anidarlo) y el largo de línea.

Cookies Persistentes

Esto es necesario porque querés permanecer logueado en los sitios de una sesión a otra. Para esto, primero tuve que hacer un pequeño mecanismo de persistencia, y guardar/leer los cookies de ahí.

Acá está como hice la persistencia (settings is una instancia de QSettings global):

def put(self, key, value):
    "Persist an object somewhere under a given key"
    settings.setValue(key, json.dumps(value))
    settings.sync()

def get(self, key, default=None):
    "Get the object stored under 'key' in persistent storage, or the default value"
    v = settings.value(key)
    return json.loads(unicode(v.toString())) if v.isValid() else default

No es código muy raro, salvo por usar el operador ternario al final. El uso de json me asegura que mientras meta cosas razonables, voy a obtener lo mismo de vuelta, con el mismo tipo, sin necesidad de convertirlo o llamar métodos especiales.

¿Entonces, como guardo/leo los cookies? Primero se necesita acceder el "cookie jar". No encontré si hay uno global o por view, así que creé un QNetworkCookieJar en la línea 24 y la asigno a cada página en la línea 107.

# Save the cookies, in the window's closeEvent
self.put("cookiejar", [str(c.toRawForm()) for c in self.cookies.allCookies()])

# Restore the cookies, in the window's __init__
self.cookies.setAllCookies([QtNetwork.QNetworkCookie.parseCookies(c)[0]\
for c in self.get("cookiejar", [])])

Confieso mi crimen de usar comprensiones de listas cuando la herramienta correcta era un for.

Uso el mismo truco al restaurar los tabs abiertos, con el moco agregado de usar una comprensión de lista y descartar el resultado:

# get("tabs") is a list of URLs
[self.addTab(QtCore.QUrl(u)) for u in self.get("tabs", [])]

Propiedades y Señales al crear un objeto

Este feature está en versiones recientes de PyQt: si pasás nombres de propiedades como argumentos con nombre, se les asigna el valor. Si pasás una señal como argumento con nombre, se conectan al valor.

Es un feature excelente, que te ayuda a crear código claro, local y conciso, y me encanta tenerlo. Pero si te querés ir a la banquina, es mandada a hacer.

Esto está por todos lados en De Vicenzo, éste es sólo un ejemplo (sí, es una sola línea):

QtWebKit.QWebView.__init__(self, loadProgress=lambda v:\
(self.pbar.show(), self.pbar.setValue(v)) if self.amCurrent() else\
None, loadFinished=self.pbar.hide, loadStarted=lambda:\
self.pbar.show() if self.amCurrent() else None, titleChanged=lambda\
t: container.tabs.setTabText(container.tabs.indexOf(self), t) or\
(container.setWindowTitle(t) if self.amCurrent() else None))

Por adonde empiezo...

Hay expresiones lambda usadas para definir los callbacks en el lugar en vez de conectarse con una función o método "de verdad".

Hya lambdas con el operador ternario:

loadStarted=lambda:\
    self.pbar.show() if self.amCurrent() else None

Hay lambdas que usan or o una tupla para engañar al intérprete y que haga más de una cosa en un solo lambda!

loadProgress=lambda v:\
(self.pbar.show(), self.pbar.setValue(v)) if self.amCurrent() else\
None

No voy ni a intentar desenredar esto con fines educativos, pero digamos que esa línea contiene cosas que deberían ser 3 métodos separados, y debería estar repartida en 6 líneas o mas.

Download Manager

Llamarlo un manager es exagerar porque no se puede parar una descarga después que empieza, pero bueno, te deja bajar cosas y seguir browseando, y te da un reporte de progreso!

Primero, en la línea 16 creé un diccionario bars para llevar registro de los downloads.

Después, tenía que delegar el contenido no soportado al método indicado, y eso se hace en las líneas 108 and 109

Básicamente, con eso cada vez que hacés click en algo que WebKit no puede manejar, se llama al método fetch con el pedido de red como argumento.

def fetch(self, reply):
    destination = QtGui.QFileDialog.getSaveFileName(self, \
        "Save File", os.path.expanduser(os.path.join('~',\
            unicode(reply.url().path()).split('/')[-1])))
    if destination:
        bar = QtGui.QProgressBar(format='%p% - ' +
            os.path.basename(unicode(destination)))
        self.statusBar().addPermanentWidget(bar)
        reply.downloadProgress.connect(self.progress)
        reply.finished.connect(self.finished)
        self.bars[unicode(reply.url().toString())] = [bar, reply,\
            unicode(destination)]

No hay mucho golf acá salvo las líneas largas, pero una vez que metés enters es la manera obvia de hacerlo:

  • Pedí un nombre de archivo
  • Creás un progressbar, lo ponés en el statusbar, y lo conectas a las señales de progreso de la descarga.

Entonces, por supuesto, está el slot progress que actualiza la barra:

progress = lambda self, received, total:\
    self.bars[unicode(self.sender().url().toString())][0]\
    .setValue(100. * received / total)

Sí, definí un método como lambda para ahorrar una línea. [facepalm]

Y elslot finished para cuando termina el download:

def finished(self):
    reply = self.sender()
    url = unicode(reply.url().toString())
    bar, _, fname = self.bars[url]
    redirURL = unicode(reply.attribute(QtNetwork.QNetworkRequest.\
        RedirectionTargetAttribute).toString())
    del self.bars[url]
    bar.deleteLater()
    if redirURL and redirURL != url:
        return self.fetch(redirURL, fname)
    with open(fname, 'wb') as f:
        f.write(str(reply.readAll()))

hasta soporta redirecciones correctamente! Más allá d eso, nada más esconde la barra, guarda los datos, fin del cuentito. La línea larga ni siquiera es mi culpa!

Hay un problema en que el archivo entero se mantiene en memoria hasta el fin de la descarga. Si te bajás un DVD, te va a doler.

Usar el with ahorra una línea y no pierde un file handle, comparado con las alternativas.

Impresión

De nuevo Qt me salva las papas, porque hacer esto a mano debe ser difícil. Sin embargo, resulta que el soporte de impresión... está hecho. Qt, especialmente usado vía PyQt es tan completo!

self.previewer = QtGui.QPrintPreviewDialog(\
    paintRequested=self.print_)
self.do_print = QtGui.QShortcut("Ctrl+p",\
    self, activated=self.previewer.exec_)

No necesité nada de golf. Eso es exactamente el código que se necesita, y es la manera recomendada de enganchar "Ctrl+p" con la impresión de la página.

Otros Trucos

No hay otros trucos. Todo lo que queda es crear widgets, conectar unas cosas con otras, y disfrutar la increíble experience de programar PyQt, donde podés escribir un web browser entero (salvo el motor) en 127 líneas de código.

De Vicenzo: un mini browser más copado

Parece que hubieran sido solo unos días desde que empecé este proyecto. Ah, epa, sí, fué hace unos días nomás!

Si no querés leer eso de nuevo, la idea es ver cuánto código falta para convertir el motor WebKit de Qt en un browser "en serio".

Para ello, me puse una meta completamente arbitraria de 128 líneas de código. En este momento lo declaro feature-complete (pero buggy).

Los nuevos features son:

  • Tabbed browsing (se puede agregar/sacar tabs)
  • Bookmarks (se pueden agregar/sacar y elegir de una lista)

Esto es lo que ya funcionaba:

  • Zoom in (Ctrl++)
  • Zoom out (Ctrl+-)
  • Reset Zoom (Ctrl+=)
  • Buscar (Ctrl+F)
  • Esconder búsqueda (Esc)
  • Botones de atrás/adelante y recargar
  • Entrada de URL que coincide con la página + autocompletado desde la historia + arregla la URL puesta a mano (agrega http://, esas cosas)
  • Plugins (incluído flash, que hay que bajar aparte ;-)
  • El título de la ventana muestra el título de la página (sin propaganda del browser)
  • Barra de progreso para la carga de la página
  • Barra de estado que muestra el destino de los links cuando pasas el mouse
  • Toma una URL en la línea de comando (o abre http://python.org
  • Multiplataforma (funciona donde funciona QtWebKit)

Y cuanto código es eso? 87 LINEAS.

O si preferís la versión que cumple con la PEP8: 115 LINEAS.

Me atajo antes que alguien lo diga: sí, el motor de rendering y el toolkit son enormes. Lo que escribí es el "chrome" alrededor de eso, igual que hacen Arora, Rekonq, Galeon, Epiphany, y muchos otros browsers.

Es un chrome simple y minimalista, pero funciona bastante bien, creo yo.

Aquí está el demo (buggy):

Mas o menos hace lo que esperaba que se puediera lograr, pero le faltan arreglos.

Para ver el código, vayan a su home page: http://devicenzo.googlecode.com

¿Cuanto browser entra en 128 líneas de código?

Hoy, charlando en IRC, traté de encontrar un browser de 42 líneas que escribí hace un tiempo. Lamentablemente el pastebin en que lo posteé estaba muerto, así que aprendí una lección: No es buena idea confiar en un pastebin como repositorio de código.

Lo que me gustaba de ese browser de 42 líneas era que no era el ejemplo típico, donde meten una vista de Webkit en una ventana, cargan una página y te tratan de convencer de que son unos bananas. Esa versión son 7 líneas:

import sys
from PyQt4 import QtGui,QtCore,QtWebKit
app=QtGui.QApplication(sys.argv)
wb=QtWebKit.QWebView()
wb.setUrl(QtCore.QUrl('http://www.python.org'))
wb.show()
sys.exit(app.exec_())

O 6 si lo soportara un poco más feo.

¡Pero igual, el de 42 se veía útil!

This 42-line web browser, courtesy of #python and #qt -- http... on Twitpic

Esos botones que se ven funcionaban correctamente, habilitando y deshabilitandose en el momento correcto, la entrada de URL cambiaba cuando hacías click en un link, y otras cositas así.

Ahí decidí empezar un pequeño proyecto intermitente de code golf: meter el mejor browser que pueda en 128 líneas de código (sin contar comentarios ni blancos), usando solo PyQt4.

Eso tiene un propósito útil: siempre sospeché que si uno asume PyQt como parte del sistema base, la mayoría de las aplicaciones entrarían en diskettes. Esta entra unas 500 veces en uno de 1.44MB (¡así que podés usar los de 360 de commodore sin duplidisk!)

hasta ahora van 50 líneas, y tiene los siguientes features:

  • Zoom in (Ctrl++)
  • Zoom out (Ctrl+-)
  • Reset Zoom (Ctrl+=)
  • Buscar (Ctrl+F)
  • Esconder búsqueda (Esc)
  • Botones de atrás/adelante y recargar
  • Entrada de URL que coincide con la página + autocompletado desde la historia + arregla la URL puesta a mano (agrega http://, esas cosas)
  • Plugins (incluído flash, que hay que bajar aparte ;-)
  • El título de la ventana muestra el título de la página (sin propaganda del browser)
  • Barra de progreso para la carga de la página
  • Barra de estado que muestra el destino de los links cuando pasas el mouse
  • Toma una URL en la línea de comando (o abre http://python.org
  • Multiplataforma (funciona donde funciona QtWebKit)

Faltan tabs y soporte de proxy. Espero que lleven unas 40 líneas más, pero creo que ya es el más capaz de todos estos browsers de ejemplo.

El código... no es tan terrible. Uso muchos lambdas, y los argumentos keyword de PyQt para conectar señales, que hacen que algunas líneas sean muy largas, pero no muy difíciles. Se podría achicar bastante todavía!

Aquí está en acción:

Y aquí está el código:

#!/usr/bin/env python
"A web browser that will never exceed 128 lines of code. (not counting blanks)"

import sys
from PyQt4 import QtGui,QtCore,QtWebKit

class MainWindow(QtGui.QMainWindow):
    def __init__(self, url):
        QtGui.QMainWindow.__init__(self)
        self.sb=self.statusBar()

        self.pbar = QtGui.QProgressBar()
        self.pbar.setMaximumWidth(120)
        self.wb=QtWebKit.QWebView(loadProgress = self.pbar.setValue, loadFinished = self.pbar.hide, loadStarted = self.pbar.show, titleChanged = self.setWindowTitle)
        self.setCentralWidget(self.wb)

        self.tb=self.addToolBar("Main Toolbar")
        for a in (QtWebKit.QWebPage.Back, QtWebKit.QWebPage.Forward, QtWebKit.QWebPage.Reload):
            self.tb.addAction(self.wb.pageAction(a))

        self.url = QtGui.QLineEdit(returnPressed = lambda:self.wb.setUrl(QtCore.QUrl.fromUserInput(self.url.text())))
        self.tb.addWidget(self.url)

        self.wb.urlChanged.connect(lambda u: self.url.setText(u.toString()))
        self.wb.urlChanged.connect(lambda: self.url.setCompleter(QtGui.QCompleter(QtCore.QStringList([QtCore.QString(i.url().toString()) for i in self.wb.history().items()]), caseSensitivity = QtCore.Qt.CaseInsensitive)))

        self.wb.statusBarMessage.connect(self.sb.showMessage)
        self.wb.page().linkHovered.connect(lambda l: self.sb.showMessage(l, 3000))

        self.search = QtGui.QLineEdit(returnPressed = lambda: self.wb.findText(self.search.text()))
        self.search.hide()
        self.showSearch = QtGui.QShortcut("Ctrl+F", self, activated = lambda: (self.search.show() , self.search.setFocus()))
        self.hideSearch = QtGui.QShortcut("Esc", self, activated = lambda: (self.search.hide(), self.wb.setFocus()))

        self.quit = QtGui.QShortcut("Ctrl+Q", self, activated = self.close)
        self.zoomIn = QtGui.QShortcut("Ctrl++", self, activated = lambda: self.wb.setZoomFactor(self.wb.zoomFactor()+.2))
        self.zoomOut = QtGui.QShortcut("Ctrl+-", self, activated = lambda: self.wb.setZoomFactor(self.wb.zoomFactor()-.2))
        self.zoomOne = QtGui.QShortcut("Ctrl+=", self, activated = lambda: self.wb.setZoomFactor(1))
        self.wb.settings().setAttribute(QtWebKit.QWebSettings.PluginsEnabled, True)

        self.sb.addPermanentWidget(self.search)
        self.sb.addPermanentWidget(self.pbar)
        self.wb.load(url)


if __name__ == "__main__":
    app=QtGui.QApplication(sys.argv)
    if len(sys.argv) > 1:
        url = QtCore.QUrl.fromUserInput(sys.argv[1])
    else:
        url = QtCore.QUrl('http://www.python.org')
    wb=MainWindow(url)
    wb.show()
    sys.exit(app.exec_())

The Outhouse and the Mall

Wearing the software engineer's hat: Code is the most trivial and the least important part of a feature.

—Michael Iatrou

Michael tweeted that, I replied, he replied, but what the heck, I think sometimes things can be better explained in more than 140 characters, thus this post [1].

So, why the mall and the outhouse? Because when we talk about software and code and features, we are not all talking about the same thing. Imagine if I told you that bricks are trivial. After all, they have existed in their current form for thousands of years, they are pretty simple to manufacture, and have no interesting features, really, except a certain resistence.

Now, suppose you are building an outhouse. Since you are a funny guy, you want to build an actual brick outhouse so you will use bricks to do it.

Now, since bricks are so boring, you may feel compelled to believe bricks are the least important part of your edifice, and that the overall design is more important. Should you carve a moon-shaped hole in the door? How deep should the latrine be?

However, that position is fatally flawed, since if you ignore those trivial, boring bricks, all you have is shit in a hole in the ground. That is because you are considering the bricks as just a mean to your end. You only care about the bricks insofar as they help you realize your grand outhouse vision. I am here to tell you that you are wrong.

The first way in which you are wrong is in that artificial separation between means and ends. Everyone is familiar with the ethical conundrum about whether the ends justify the means, but that's garbage. That'swhat you say when you try to convince yourself that doing things haphazardly is ok, because what you do is just the means to whatever other thing is the end. Life is not so easily divided into things that matter and things that don't.

Your work, your creation is not just some ideal isolated end towards which you travel across a sea of dirty means, trying to keep your silver armour clean. It's one whole thing. You are creating the means, you are creating your goal, you are responsible for both, and if you use shoddy bricks, your outhouse should shame you.

In the same way, if you do crappy code, your feature is demeaned. It may even work, but you will know it's built out of crap. You will know you will have to fix and maintain that crap for years to come, or, if you are lucky, ruin your karma by dumping it on the head of some poor sucker who follows your steps.

I am pretty much a materialist. If you remove the code, you don't have a feature, or software, you have a concept, maybe an idea, perhaps a design (or maybe not) but certainly not software, just like you don't have a brick outhouse without piling some damn bricks one on top of the other.

I always say, when I see someone calling himself a software engineer, that I am merely a software carpenter. I know my tools, I care about them, I use them as well as I can according to my lights [2] and I try to produce as good a piece of furniture as I can with what I am given.

This tends to produce humble software, but it's software that has one redeeming feature: it knows what it should do, and does it as well as I can make it. For example, I wrote rst2pdf. It's a program that takes some sort of text, and produces PDF files. It does that as well as I could manage. It does nothing else. It works well or not, but it is what it is, it has a purpose, a description and a goal, and I have tried to achieve that goal without embarrasing myself.

My programs are outhouses, made of carefully selected and considered bricks. They are not fancy, but they are what they are and you know it just by looking at them. And if you ever need an outhouse, well, an outhouse is what you should get.

Also, people tend to do weird stuff with them I never expected, but that's just the luck of the analogy.

But why did I mention malls in the title? Because malls are not outhouses. Malls are not done with a goal by themselves beyond making money for its builders. The actual function of a piece of mall is not even known when it's being built. Will this be a McDonalds, or will it be a comic book store? Who knows!

A mall is built quickly with whatever makes sense moneywise, and it should look bland and recognisable, to not scare the herd. It's a building made for pedestrians, but it's intended to confuse them and make the path form A to B as long and meandering as possible. The premises on which its design is based are all askew, corrupted and self-contradicting.

They also give builders a chance to make lots of money. Or to lose lots of money.

Nowadays, we live in an age of mall software. People build startups, get financing, build crappy software and sometimes they hit it big (Twitter, Facebook) or, more likely, fade into obscurity leaving behind nothing at all, except money lost and sad programmers who spent nights coding stuff noone will ever see or use, and not much else.

Far from me saying startups are not a noble or worthy endeavour. They are! It's just that people who work on them should realize that they are not building software. That's why code doesn't look important to them, because they are actually selling eyeballs to advertisers, or collected personal data from their users to whoever buys that, or captive public for game developers, or whatever your business model says (if you have one!).

They are building malls, where the value is not in the building, which is pretty ghastly and useless by itself, but on the people in it, those who rent space in the mall, those who will use the mall, the software, the social network, whatever it is you are building.

Twitter is not software, Facebook is not software. If they were, identi.ca and diaspora would be bigger! What they are is people in one place, like a mall is not a real building, but a collection of people under a roof.

So, there is nothing wrong with building malls. Just remember that your ends and your means are one and a whole, that code is important, that without code Facebook and Twitter don't work, and that without people they are a badland, and know what you are doing.

Because the only hard thing in life is knowing what you want to do. The rest is the easy part. And because malls without toilets suck.

[1] If you really want to see the whole conversation, it's here: http://bettween.com/ralsina/iatrou (if anyone knows a better conversation tracker please post it in a comment).
[2] Yet here I am, an Engineering Manager at Canonical. Sorry guys!

Una nueva etapa, bla bla bla

Deben haber visto un millón de posts como este. Hacker X empieza diciendo lo bien que la pasó en la compañia Y/la universidad/la casa de mamá/el circo y como siempre extrañará a sus compañeros, pero de todas formas espera con ansias los desafíos de su nueva vida en la compañía Z/desempleo/consultoría de feng shui freelance/el pujante negocio de recolección de excrementos de elefantes.

Bueno, este es uno de esos.

Hoy empecé a trabajar en Canonical. Sí, en Canonical. Los de Ubuntu. Tal vez te preguntes que va a hacer un KDEero de la primera hora ahí. Bueno, es un laburo, los 90s te piden que devuelvas el flamefest.

Soy el nuevo "Engineering Manager for the Desktop+ group". Qué miércoles es eso? Bueno, mi trabajo es ayudar a un grupo de gente que me cae bien (los que conozco al menos) a crear software copado.

Probablemente no programe demasiado, ya que este es un trabajo de adulto, en que lo único que se espera que desarrolle en persona es una úlcera gástrica y la superficie de mi pelada mientras arreo gatos hacia el corral mas cercano, pero probablemente me las arregle para hacer algunas cosas, a veces.

Me llega en un buen momento. Mi pibe cumple 4 el año que viene, y va a estar todo el día en el colegio. Que voy a hacer en casa todo el día? Ver animé? Construir robots asesinos? Planear como conquistar al mundo, Pinky?

Y que pasa con mi trabajo anterior? Bueno... todavía sigue ahí. Sigo siendo dueño de un pedazo de Net Managers (http://netmanagers.com.ar) pero me voy a apartar de la operación de la empresa.

Básicamente, pienso quedarme con el dinero y tirarles el trabajo a mis queridos socios (mentira). En cualquier caso la empresa funcionará igual, ya que si no saco plata podemos contratar un empleado para que haga lo que hacía yo, así que todo el mundo gana.

En otras noticias, sigo laburando en la misma mesa que los últimos 5 años, haciendo mas o menos lo mismo, con gente distinta. Dicho así no suena tan interesante... pero bueno, hay desafíos interesantes en este nuevo trabajo.

Resumiendo: Canonical, poco de programar, sigo sieno dueño de netmanagers, estoy contento.

Salió rst2pdf 0.16!

¡Por fin, un nuevo release de rst2pdf!

Puede obtenerse en el sitio: http://rst2pdf.googlecode.com

rst2pdf es una herramienta para convertir restructured text a PDF usando reportlab en vez de LaTeX.

Se ha usado para muchas cosas desde libros a revistas, a folletos, a manuales, a sitios web y tiene muchos features:

  • Font embedding (TTF or Type1 fonts)
  • Cascading Stylesheets
  • Arquitectura de plugins muy flexible (permite hacer cosas como crear los encabezados en base a SVG arbitrarios!)
  • Integración con Sphinx.
  • Múltiples layouts de página
  • Tapas customizables via templates
  • Y mucho, mucho más

El cambio más grande en 0.16 es probablemente el soporte de Sphinx 1.0.x, si estás usando Sphinx esta es la versión que querés.

Aparte de eso, una tonelada de bugs arreglados, y algunos features menores.

Este es el changelog completo:

  • Fixed Issue 343: Plugged memory leak in the RSON parser.
  • Fix for Issue 287: there is still a corner case if you have two sections with the same title, at the same level, in the same page, in different files where the links will break.
  • Fixed Issue 367: german-localized dates are MM. DD. YYYY so when used in sphinx's template cover they appeared weird, like a list item. Fixed with a minor workaround in the template.
  • Fixed Issue 366: links to "#" make no sense on a PDF file
  • Made definitions from definition lists more stylable.
  • Moved definition lists to SplitTables, so you can have very long definitions.
  • Fixed Issue 318: Implemented Domain specific indexes for Sphinx 1.0.x
  • Fixed Index links when using Sphinx/pdfbuilder.
  • Fixed Issue 360: Set literal.wordWrap to None by default so it doesn't inherit wordWrap CJK when you use the otherwise correct japanese settings. In any case, literal blocks are not supposed to wrap at all.
  • Switched pdfbuilder to use SplitTables by default (it made no sense not to do it)
  • Fixed Issue 365: some TTF fonts don't validate but they work anyway.
  • Set a valid default baseurl for Sphinx (makes it much faster!)
  • New feature: --use-numbered-links to show section numbers in links to sections, like "See section 2.3 Termination"
  • Added stylesheets for landscape paper sizes (i.e: a4-landscape.style)
  • Fixed Issue 364: Some options not respected when passed in per-doc options in sphinx.
  • Fixed Issue 361: multiple linebreaks in line blocks were collapsed.
  • Fixed Issue 363: strange characters in some cases in math directive.
  • Fixed Issue 362: Smarter auto-enclosing of equations in $...$
  • Fixed Issue 358: --real--footnotes defaults to False, but help text indicates default is True
  • Fixed Issue 359: Wrong --fit-background-mode help string
  • Fixed Issue 356: missing cells if a cell spawns rows and columns.
  • Fixed Issue 349: Work correctly with languages that are available in form aa_bb and not aa (example: zh_cn)
  • Fixed Issue 345: give file/line info when there is an error in a raw PDF directive.
  • Fixed Issue 336: JPEG images should work even without PIL (but give a warning because sizes will probably be wrong)
  • Fixed Issue 351: footnote/citation references were generated incorrectly, which caused problems if there was a citation with the same text as a heading.
  • Fixed Issue 353: better handling of graphviz, so that it works without vectorpdf but gives a warning about it.
  • Fixed Issue 354: make todo_node from sphinx customizable.
  • Fixed bug where nested lists broke page layout if the page was small.
  • Smarter --inline-links option
  • New extension: fancytitles, see //ralsina.me/weblog/posts/BB906.html
  • New feature: tab-width option in code-block directive (defaults to 8).
  • Fixed Issue 340: endnotes/footnotes were not styled.
  • Fixed Issue 339: class names using _ were not usable.
  • Fixed Issue 335: ugly crash when using images in some specific places (looks like a reportlab bug)
  • Fixed Issue 329: make the figure alignment/class attributes work more like LaTeX than HTML.
  • Fixed Issue 328: list item styles were being ignored.
  • Fixed Issue 186: new --use-floating-images makes images with :align: set work like in HTML, with the next flowable flowing beside it.
  • Fixed Issue 307: header/footer from stylesheet now supports inline rest markup and substitutions defined in the main document.
  • New pdf_toc_depth option for Sphinx/pdfbuilder
  • New pdf_use_toc option for Sphinx/pdfbuilder
  • Fixed Issue 308: compatibility with reportlab from SVN
  • Fixed Issue 323: errors in the config.sample made it work weird.
  • Fixed Issue 322: Image substitutions didn't work in document title.
  • Implemented Issue 321: underline and strikethrough available in stylesheet.
  • Fixed Issue 317: Ugly error message when file does not exist