2012-03-13 02:17

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.

Comentarios

Comments powered by Disqus

Contents © 2000-2018 Roberto Alsina