Sin embargo, hay una trampa. Tenés que estar absolutamente seguro que la cosa que estás
decodeando es un string de bytes, y no un objeto unicode. Porque los objetos unicode tienen
un método decode pero es totalmente inútil, y su único propósito en la vida es
causar este error peculiar:
>>> u'á'.decode('utf8')Traceback (most recent call last):File "<stdin>", line 1, in <module>File "/usr/lib/python2.7/encodings/utf_8.py", line 16, in decode return codecs.utf_8_decode(input, errors, True)UnicodeEncodeError: 'ascii' codec can't encode character u'\xe1'in position 0: ordinal not in range(128)
¿Porqué es peculiar? Porque es un error de Encode. Causado por llamar a decode.
Lo que pasa es que en los objetos unicode, decode, en la práctica es algo así:
El usuario quiere un objeto unicode. Él tiene un objeto unicde. Por
definición, no existe un cosa que sea "decodear como utf-8 un objeto unicode". No tiene sentido. Es como peinar un pescado, o escalar una laguna.
¡Lo que debería devolver es self! Además es suamente molesto que la
única manera de evitar el error es chequear el tipo del objeto, lo que es
activamente antipitónico.
Aún mejor, no tengamos un método decode en objetos unicode, que creo es la
situación en python 3, pero nunca lo va a ser en python 2.
Separé código y configuración.
El último fué el más complicado. Y para que se vea como es, ésta es la
configuración completa, excepto pedacitos de HTML que no valen la pena ver, como
el código de google custom search. ¡Espero que sea claro!
# -*- coding: utf-8 -*-# post_pages contains (wildcard, destination, template) tuples.## The wildcard is used to generate a list of reSt source files (whatever/thing.txt)# That fragment must have an associated metadata file (whatever/thing.meta),# and opcionally translated files (example for spanish, with code "es"):# whatever/thing.txt.es and whatever/thing.meta.es## From those files, a set of HTML fragment files will be generated:# whatever/thing.html (and maybe whatever/thing.html.es)## These files are combinated with the template to produce rendered# pages, which will be placed at# output / TRANSLATIONS[lang] / destination / pagename.html## where "pagename" is specified in the metadata file.#post_pages=(("posts/*.txt","weblog/posts","post.tmpl"),("stories/*.txt","stories","post.tmpl"),)# What is the default language?DEFAULT_LANG="en"# What languages do you have?# If a specific post is not translated to a language, then the version# in the default language will be shown instead.# The format is {"translationcode" : "path/to/translation" }# the path will be used as a prefix for the generated pages locationTRANSLATIONS={"en":"","es":"tr/es",}# Data about this siteBLOG_TITLE="Lateral Opinion"BLOG_URL="//ralsina.me"BLOG_EMAIL="ralsina@kde.org"BLOG_DESCRIPTION="I write free software. I have an opinion on almost "\
"everything. I write quickly. A weblog was inevitable."# Paths for different autogenerated bits. These are combined with the translation# paths.# Final locations are:# output / TRANSLATION[lang] / TAG_PATH / index.html (list of tags)# output / TRANSLATION[lang] / TAG_PATH / tag.html (list of posts for a tag)# output / TRANSLATION[lang] / TAG_PATH / tag.xml (RSS feed for a tag)TAG_PATH="categories"# Final location is output / TRANSLATION[lang] / INDEX_PATH / index-*.htmlINDEX_PATH="weblog"# Final locations for the archives are:# output / TRANSLATION[lang] / ARCHIVE_PATH / archive.html# output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / index.htmlARCHIVE_PATH="weblog"# Final locations are:# output / TRANSLATION[lang] / RSS_PATH / rss.xmlRSS_PATH="weblog"# A HTML fragment describing the license, for the sidebar.LICENSE=""" <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/2.5/ar/"> <img alt="Creative Commons License" style="border-width:0; margin-bottom:12px;" src="http://i.creativecommons.org/l/by-nc-sa/2.5/ar/88x31.png"></a>"""# A search form to search this site, for the sidebar. Has to be a <li># for the default template (base.tmpl).SEARCH_FORM=""" <!-- google custom search --> <!-- End of google custom search -->"""# Google analytics or whatever else you use. Added to the bottom of <body># in the default template (base.tmpl).ANALYTICS=""" <!-- Start of StatCounter Code --> <!-- End of StatCounter Code --> <!-- Start of Google Analytics --> <!-- End of Google Analytics -->"""# Put in global_context things you want available on all your templates.# It can be anything, data, functions, modules, etc.GLOBAL_CONTEXT={'analytics':ANALYTICS,'blog_title':BLOG_TITLE,'blog_url':BLOG_URL,'translations':TRANSLATIONS,'license':LICENSE,'search_form':SEARCH_FORM,# Locale-dependent links'archives_link':{'es':'<a href="/tr/es/weblog/archive.html">Archivo</a>','en':'<a href="/weblog/archive.html">Archives</a>',},'tags_link':{'es':'<a href="/tr/es/categories/index.html">Tags</a>','en':'<a href="/categories/index.html">Tags</a>',},}execfile("nikola/nikola.py")
Cambié todo el software y los templates para este blog.
Sí, está todo sin terminar.
El nuevo software se llama Nikola.
Sí, creo que está bueno.
¿Porqué cambiar?
¿En serio? EL generador anterior (Son of BartleBlog) no estaba en buen
estado. Los archivos cubrían sólo 2000-2010, el link "posts anteriores"
era una quiniela, y a la versión en español le faltaban páginas enteras.
¿Qué es Nikola?
Nikola es un generador de sitios estáticos. Una cosa interesante de este
sitio es que es, y siempre ha sio, puro HTML. Cada cosa "dinámica" que veas acá,
como ser comentarios, es un servicio de terceros. Este sitio es nada más
que unas carpetas llenas de HTML.
¿Cómo funciona Nikola?
Nikola toma una carpeta llena de archivos txt escritos en restructured text, y genera fragmentos
de HTML.
Esos fragmentos, con un poco de metadata (título, tags, nombre del archivo de
salida, links a fuentes externas) y unos Mako Templates
crean páginas HTML.
Esas páginas usan bootstrap para no
ser una reverenda bazofia (nunca dije ser un diseñador).
Para asegurarme de no hacer trabajo inútil, doit
se encarga de recrear lo mínimo indispensable.
¿Por qué no usar <esto>?
Porque, por diversas razones, quería mantener exactamente las URLs que siempre
tuve.
Si muevo una página, mantener asociados los comentarios de Disqus es un bardo.
Puede haber gente que tega bookmarks.
Además quiero:
Mako templates (porque me gustan)
Restructured text (porque tengo más de 1000 posts escritos en eso)
Python (para hackearlo)
Fácil de hackear (Nikola está por las 600 LOC, y es casi feature complete)
Soporte de blogs multilingües como este.
Y por supuesto:
Sonaba como un proyecto corto y divertido. Tenía la sospecha que con un poco
de pegamento las herramientas existentes hacían el 90% del trabajo. Parece
que tenía razón, ya que lo pude escribir en unos pocos dias.
¿Lo vas a mantener?
Y, lo estoy usando...
¿Es útil para alguien más?
No por ahora, porque hace montones de suposiciones válidas sólo para este sitio.
hay que limpiarlo un poco antes de que quede lindo.
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 busbus=dbus.SessionBus():::# Get the credentials proxy and interfaceself.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 happensself.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 credentialsself._credentials=Noneself.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!
defget_credentials(self):# Do we have them already? If not, get'emifnotself._credentials:self.creds_proxy.find_credentials()# Return what we've got, could be Nonereturnself._credentialsdefcreds_found(self,data):# Received credentials, save them.print"creds_found",dataself._credentials=data# Don't worry about get_quota yet ;-)ifnotself._quota_info:self.get_quota()defcreds_not_found(self,data):# No credentials, remove old ones.print"creds_not_found",dataself._credentials=Nonedefcreds_error(self,data):# No credentials, remove old ones.print"creds_error",dataself._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:
defsign_uri(self,uri,parameters=None):# Without credentials, return unsigned URLifnotself._credentials:returnuriifisinstance(uri,unicode):uri=bytes(iri2uri(uri))print"uri:",urimethod="GET"credentials=self._credentialsconsumer=oauth.OAuthConsumer(credentials["consumer_key"],credentials["consumer_secret"])token=oauth.OAuthToken(credentials["token"],credentials["token_secret"])ifnotparameters:_,_,_,_,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())returnrequest.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):::defget_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:
¿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 nowself._last_status=self.process_status(self.status_proxy.current_status())
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.