As I hope you know, if you get a string of bytes, and want the text in it, and that text may
be non-ascii, what you need to do is decode the string using the correct encoding name:
>>> 'á'.decode('utf8')u'\xe1'
However, there is a gotcha there. You have to be absolutely sure that the thing you are decoding
is a string of bytes, and not a unicode object. Because unicode objects also have a decode method
but it's an incredibly useless one, whose only purpose in life is causing this peculiar error:
>>> 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)
Why peculiar? Because it's an Encode error. Caused by calling decode. You see,
on unicode objects, decode does something like this:
The user wants a unicode object. He has a unicode object. By definition,
there is no such thing as a way to utf-8-decode a unicode object. It just
makes NO SENSE. It's like asking for a way to comb a fish, or climb a
lake.
What it should return is self! Also, it's annoying as all hell in that
the only way to avoid it is to check for type, which is totally unpythonic.
Or even better, let's just not have a decode method on unicode objects, which
I think is the case in python 3, and I know we will never get on python 2.
I added a very minor feature to the site. Up here ^ you should be able to see a link that says "reSt".
If you click on it, it will show you the "source code" for the page.
I did this for a few reasons:
Because a comment seemed to suggest it ;-)
Because it seems like a nice thing to do. Since I so like reSt, I would like others to use it, too. And showing how easy it is
to write using it, is cool.
It's the "free software-y" thing to do. I am providing you the preferred way to modify my posts.
It was ridiculously easy to add.
Also, if you see something missing, or something you would like to have on the site, please comment, I will try to add it.
The last one was the trickiest. And as a teaser, here is the full configuration
file to create this site, except HTML bits for analytics, google custom search
and whatever that would make no sense on other sites. I hope it's somewhat clear.
# -*- 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")
If you see this, you may notice some changes in the site.
So, here is a short explanation:
I changed the software and the templates for this blog.
Yes, it's a work in progress.
The new software is called Nikola.
Yes, it's pretty cool.
Why change?
Are you kidding? My previous blog-generator (Son of BartleBlog) was not in
good shape. The archives only covered 2000-2010, the "previous posts" links
were a lottery, and the spanish version of the site was missing whole sections.
So, what's Nikola?
Nikola is a static website generator. One thing about this
site is that it is, and has always been, just HTML. Every "dynamic" thing
you see in it, like comments, is a third party service. This site is just
a bunch of HTML files sitting in a folder.
So, how does Nikola work?
Nikola takes a folder full of txt files written in restructured text, and
generates HTML fragments.
Those fragments plus some light metadata (title, tags, desired output filename,
external links to sources) and Some Mako Templates
create HTML pages.
Those HTML pages use bootstrap to not
look completely broken (hey, I never claimed to be a designer).
To make sure I don't do useless work, doit
makes sure only the required files are recreated.
Why not use <whatever>?
Because, for diverse reasons, I wanted to keep the exact URLs I have been using:
If I move a page, keeping the Disqus comments attached gets tricky
Some people may have bookmarked them
Also, I wanted:
Mako templates (because I like Mako)
Restructured text (Because I have over 1000 posts written in it)
Python (so I could hack it)
Easy to hack (currently Nikola is under 600 LOC, and is almost feature complete)
Support for a multilingual blog like this one.
And of course:
It sounded like a fun, short project. I had the suspicion that with a bit of
glue, existing tools did 90% of the work. Looks like I was right, since I
wrote it in a few days.
Are you going to maintain it?
Sure, since I am using it.
Is it useful for other people?
Probably not right now, because it makes a ton of assumptions for my site. I need
to clean it up a bit before it's really nice.
One of the nice things about working at Canonical is that we produce open source software.
I, specifically, work in the team that does the desktop clients for Ubuntu One which is a really cool job, and a really cool piece
of software. However, one thing not enough people know, is that we offer damn nice APIs
for developers. We have to, since all our client code is open source, so we need those
APIs for ourselves.
So, here is a small tutorial about using some of those APIs. I did it using Python and
PyQt for several reasons:
Both are great tools for prototyping
Both have good support for the required stuff (DBus, HTTP, OAuth)
It's what I know and enjoy. Since I did this code on a sunday, I am
not going to use other things.
Having said that, there is nothing python-specific or Qt-specific in the code. Where
I do a HTTP request using QtNetwork, you are free to use libsoup, or whatever.
So, on to the nuts and bolts. The main pieces of Ubuntu One, from a infrastructure
perspective, are Ubuntu SSO Client, that handles user registration and login, and
SyncDaemon, which handles file synchronization.
To interact with them, on Linux, they offer DBus interfaces. So, for example, this is
a fragment of code showing a way to get the Ubuntu One credentials (this would normally
be part of an object's __init__):
# 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()
You may have noticed that get_credentials doesn't actually return the credentials. What
it does is, it tells SyncDaemon to fetch the credentials, and then, when/if they are there,
one of the signals will be emitted, and one of the connected methods will be called. This
is nice, because it means you don't have to worry about your app blocking while SyncDaemon
is doing all this.
But what's in those methods we used? Not much, really!
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
So, basically, self._credentials will hold a set of credentials, or None. Congratulations, we
are now logged into Ubuntu One, so to speak.
So, let's do something useful! How about asking for how much free space there is in
the account? For that, we can't use the local APIs, we have to connect to the servers, who
are, after all, the ones who decide if you are over quota or not.
Access is controlled via OAuth. So, to access the API, we need to sign our requests. Here
is how it's done. It's not particularly enlightening, and I did not write it, I just use it:
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()
And how do we ask for the quota usage? By accessing the https://one.ubuntu.com/api/quota/ entry point
with the proper authorization, we would get a JSON dictionary with total and used space.
So, here's a simple way to do it:
# 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))
Again, see how get_quota doesn't return the quota? What happens is that get_quota will
launch a HTTP request to the Ubuntu One servers, which will, eventually, reply with the data.
You don't want your app to block while you do that. So, QNetworkAccessManager will call
self.reply_finished when it gets the response:
What else would be nice to have? How about getting a call whenever the status of syncdaemon
changes? For example, when sync is up to date, or when you get disconnected? Again, those are
DBus signals we are connecting in our __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())
The process_status function is boring code to convert the info from
syncdaemon's status into a human-readable thing like "Sync is up-to-date". So we
store that in self._last_status and update the menu.
What menu? Well, a QSystemTrayIcon's context menu! What you have read are the main pieces
you need to create something useful: a Ubuntu One tray app you can use in KDE, XFCE or openbox.
Or, if you are on unity and install sni-qt, a Ubuntu One app indicator!