Hack Manso: Mejorar la velocidad de SQLite en Google App Engine SDK

Estaba tratando de inicializar una base de datos en local, con el Google App Engine SDK y me estaba volviendo loco.

Ni siquiera era factible usar –use_sqlite que introdujeron en la versión 1.3.3. La velocidad de los inserts a la base de datos iban superlentos, en plan, diez por segundo. Infumable.

Vale, vale, es el SDK, y el objetivo es desarrollar la aplicación y la velocidad no es lo más importante… pero si para insertar 1000 datos, tienen que pasar 10 minutos, pues mal vamos. Si me dijeras 1 millón, pues aún lo entendería minimamente… pero ¿1000 miseros inserts? ¿10 minutos? ¿estamos todos locos?

Así que, partiendo de que SQLite parece funcionar de puta madre, pues no me cuadraba… Mirando el FAQ de SQLite, me he encontrado con que la pregunta 19:

(19) INSERT is really slow – I can only do few dozen INSERTs per second

La respuesta es, SQLite puede hacer fácil 50.000 inserts/segundo peeeeeeeero para garantizar la integridad de los datos, se espera a que el disco confirme que los datos se han escrito bien, por si se va la luz, etc… Vamos, un cuello de botella enorme, cuando a mi los datos me dan un poco igual porque es un entorno de prueba, lo que quiero es que vayan rápido los inserts porque si no me muero del asco desarrollando.

En fin, que la solución que dan es usar “PRAGMA synchronous=OFF“. La única forma de usar ese pragma, es editar el SDK del Google App Engine, y ejecutar esa línea tras inicializar la base de datos.

Hay que editar el archivo:

google_appengine/google/appengine/datastore/datastore_sqlite_stub.py

Vamos al constructor “__init__”, y después de que inicialize la conexión a la base de datos “self.__connection = sqlite3.connect [...]” pues metemos la siguiente linea:

self.__connection.execute(‘PRAGMA synchronous=OFF’)

Y, afotunadamente, cambiando esta línea he pasado de 10 inserts por segundo a más de 100 inserts por segundo. Más de 10x más rápido cambiando sólo una linea.

Ahora ya puedo continuar con lo que estaba haciendo.

, , , , ,

Truco Manso: obtener una cadena hexadecimal aleatoria en python

Obtener una cadena de bytes aleatoria es una de esas cosas que hacen falta de vez en cuando.
La opción más sencilla en python es importar uuid y hacer la siguiente llamada:

uuid.uuid4().hex

Ale, con esa llamada ya tenemos 32 caracteres hexadecimales aleatorios (16 bytes aleatorios).

¿Sabes de algún otro método?

,

TrucoManso: Transformar el tiempo en formato 24h a formato 12h (Python)

Truco manso para transformar una cadena de tiempo en formato de 24h en formato de 12h (AM/PM)


def ampmformat (hhmmss):
  """
    This method converts time in 24h format to 12h format
    Example:   "00:32" is "12:32 AM"
               "13:33" is "01:33 PM"
  """
  ampm = hhmmss.split (":")
  if (len(ampm) == 0) or (len(ampm) > 3):
    return hhmmss

  # is AM? from [00:00, 12:00[
  hour = int(ampm[0]) % 24
  isam = (hour >= 0) and (hour < 12)

  # 00:32 should be 12:32 AM not 00:32
  if isam:
    ampm[0] = ('12' if (hour == 0) else "%02d" % (hour))
  else:
    ampm[0] = ('12' if (hour == 12) else "%02d" % (hour-12))

  return ':'.join (ampm) + (' AM' if isam else ' PM')

Y un ejemplo de uso:

ampmformat ("00:00:00") # devuelve "12:00:00 AM"
ampmformat ("12:00:00") # devuelve "12:00:00 PM"

ampmformat ("01:23:45") # devuelve "01:23:45 AM"
ampmformat ("13:23:45") # devuelve "01:23:45 PM"
ampmformat ("05:43:21") # devuelve "05:43:21 AM"
ampmformat ("17:43:21") # devuelve "05:43:21 PM"

ampmformat ("11:59:59") # devuelve "11:59:59 AM"
ampmformat ("23:59:59") # devuelve "11:59:59 PM"

, ,

Google App Engine SDK 1.4.0 released

Ya está disponible la versión 1.4.0 del SDK de Google App Engine.

Particularmente resaltaría 3 cosas de este release:

  • Los taskqueues ya forman parte del API oficial, han dejado de ser experimentales ;)
  • Han multiplicado por 20 los tiempos máximos de ejecución de las tareas cron y de las tareas del task queue de 30 segundos a 600 segundos (10 minutos). Esto es fantástico, aunque puede ser peligroso. Habrá que usarlo sabiamente ;)
  • El uso del Channel API ya está disponible para todo el mundo de forma oficial.

El Channel API me impresionó cuando le eché un vistazo por allá por Mayo. A continuación el video del Channel API. Es bastante técnico y sólo tiene sentido si programas en GAE o si por ejemplo quieres hacer un API para comunicación Comet entre cliente-servidor:

Enlaces:

, , ,

Truco Manso: Obtener la latitud/longitud a partir de una dirección

Para un proyecto que espero que pronto vea la luz, he estado investigando cómo determinar la localización geográfica de cualquier sitio. Esto es, determinar la latitud/longitud a partir de una frase en plan “Elche, Alicante, Spain”

A esto se le llama Geocoding y a poco que he buscado tanto Google como Yahoo tienen APIs REST que te permiten enchufar queries en un servidor (aunque con unos términos de uso limitados).

En el caso de Yahoo, hace falta darse de alta para que te den un appid y poder hacer los queries.

En el caso de Google, no es así, y preguntar la latitud y longitud de “Elche, Alicante, Spain” es tan fácil como:

http://maps.googleapis.com/maps/api/geocode/json?address=Elche,+Alicante,+Spain&sensor=false

Esto devuelve un churrasco en JSON (también lo puedes pedir en XML) y dentro de este churrasco, dice que la latitud/longitud es:
“lat”: 38.2671765,
“lng”: -0.6952196

También existe GeoNames, que es menos preciso, pero puede resultar interesante también (ver enlace más abajo).

Para más información:

, , , , ,

Soporte de unicode en Python: Frustraciones y Soluciones

Personalmente el soporte de unicode en Python anterior a Python 3, siendo finos, es una puta mierda. Casi prefiero el soporte que tiene PHP 5.x de unicode (cero patatero).

Sinceramente es para volverse loco, porque cosas que a priori van, luego petan, o realmente no petan, sólo petan cuando intentas mostrarlas por la consola haciendo print…

En fin, que tratando de solucionar mis frustraciones con python y el unicode, he encontrado el siguiente enlace:

Overcoming unicode frustrations in Python 2

El enlace anterior es lectura obligada para cualquiera que de vez en cuando le salte una excepción tipo UnicodeEncodeError, como:

UnicodeEncodeError: 'ascii' codec can't encode character u'\xe1' in position 654: ordinal not in range(128)

Finalmente, mi esperanza es que en Python 3 todo esto se solucione, y a juzgar por lo que he ido leyendo parece que si… pero ¡habrá que ver para creer!

, , ,

Serializando datos nativos en Python

Resulta que para un proyecto en Python en el que estoy trabajando necesitaba serializar algunas estructuras. El tema es que únicamente hacía falta serializar tipos nativos como listas, diccionarios, cadenas, enteros, etc…

Lo que me hacía falta era:

  • representación compacta
  • serialización/deserialización rápida
  • legible / editable por un humano

Así que después de pensar un rato, he encontrado 3 posibles soluciones para serializar datos arbitrarios:

  • pickle/cPickle que serializa cualquier cosa, viene con python, pero no es legible/editable
  • simplejson que serializa en formato JSON (el resultado es legible y editable)
  • PyYAML que serializa en YAML (el resultado es legible y editable)

El siguiente paso era coger un buen montón de datos, y hacer tests para ver que tal funcionan los encoders/decoders en cada uno de esos módulos.

A continuación puedes ver los gráficos de los tiempos de encoding/decoding para dos conjuntos de datos empleando cada uno de los formatos anteriores:

Python Serialization Speed Test (Tiny Dataset)

Python Serialization Speed Test (Huge Dataset)

A parte de la velocidad, también me interesaba medir la longitud de las cadenas generadas (cuanto menos, mejor):

  • yaml   tiny = 32604  // huge = 912912
  • json   tiny = 31305 // huge = 876540
  • pickle-raw    tiny = 34504 // huge = 986531
  • pickle-highest   tiny = 37541 // huge = 1101480
Conclusión:

Como se puede ver en las gráficas, y en la tabla anterior, considero que JSON es el que me hace mejor papel. Codifica / decodifica ligeramente más despacio que pickle (un 4% más despacio) pero la cadena resultante es legible y editable y además ocupa menos espacio que picke y que yaml.

Todos los tests han sido realizados con Python 2.5

El código:

Finalmente, aquí dejo un fragmento de código usado para obtener los tiempos que aparecen en los gráficos anteriores:

    start = time.time()
    for i in range (0, 100):
      yaml.safe_load (yaml.safe_dump (data))
    self.write ("YAML TIME = %.2f\n" % (time.time() - start))
 
    start = time.time()
    for i in range (0, 100):
      JSONDecoder().decode (JSONEncoder().encode (data))
    self.write ("JSON TIME = %.2f\n" % (time.time() - start))
 
    start = time.time()
    for i in range (0, 100):
      pickle.loads (pickle.dumps (data, pickle.HIGHEST_PROTOCOL))
    self.write ("PICKLE TIME = %.2f\n" % (time.time() - start))
 
    start = time.time()
    for i in range (0, 100):
      pickle.loads (pickle.dumps (data, 0))
    self.write ("PICKLE-RAW TIME = %.2f\n" % (time.time() - start))

, , , , ,

bbcodeutils: BBCode parser and BBCode to HTML for Python

Ayer anduve buscando algún módulo para Python para parsear Bulletin Board Code (bbcode para los amigos) o que fuera capaz de transformarlo en HTML.

Como ninguna solución cumplía mis necesidades al 100% he creado bbcodeutils que no es mas que un conjunto de clases en Python para parsear, generar y transformar bbcode. He intentado que sea lo más simple de usar posible.

Dentro de bbcodeutils encontrarás estas clases:

  • bbcodeparser: permite parsear y corregir código bbcode
  • bbcode2html: permite generar HTML a partir de un código BBCode parseado
  • bbcodebuilder: permite generar BBCode a partir de sentencias en Python

Aquí teneis el enlace para descargar bbcodeutils v1.0. Puedes usar este módulo libremente (ver readme.txt para más detalles).

A continuación voy a poner algunos ejemplos de uso de las operaciones más típicas.

Ejemplos:

En los siguientes ejemplos se asume que habrás hecho algo para cargar los módulos, en plan:

from bbcodeutils import bbcodebuilder, bbcodeparser
Parsear cadenas bbcode

Parsear una cadena BBCode se puede hacer de dos maneras, usando el constructor, o invocando a parse:

> bbcode = bbcodeparser('[b]bold string[/b]')

o bien:

> bbcode = bbcodeparser()
> bbcode.parse ('[b]first string[/b]')

Esto tampoco tiene más utilidad que la de ilustrar como parsear una cadena, pero al final uno lo que querrá será bien corregir el código BBCode, o bien producir HTML válido a partir de la cadena parseada

Corregir una cadena BBCode invalida

Para corregir una cadena BBCode mal formada, símplemente habría que parsear la cadena en cuestión y llamar al método bbcode que construye la cadena de nuevo, o bien transformar el objeto a cadena.

> bbcode = bbcodeparser("This is [b]bold and [i]this bold and italics[/b].[/color]")
> bbcode.bbcode()
"This is [b]bold and [i]this bold and italics[/i][/b]."
 
> str(bbcode)
"This is [b]bold and [i]this bold and italics[/i][/b]."

Nótese como se elimina el tag [/color] y se añade el tag [/i] que faltaba.

Transformar una cadena BBCode en HTML

La clase bbcodeparser implementa el método html que transforma el código parseado
en HTML válido.

> bbcode = bbcodeparser('[b]bold string[/b]')
> bbcode.html()
"<b>bold string</b>"
Generar código BBCode
> bbcode = bbcodebuilder()
> bbcode.b('string in bold')
"[b]string in bold[/b]"
 
> bbcode.url('http://www.codigomanso.com/')
"[url]http://www.codigomanso.com/[/url]"
 
> bbcode.url('Welcome to Codigo Manso', 'http://www.codigomanso.com/')
"[url=http://www.codigomanso.com/]Welcome to Codigo Manso[/url]"
 
> bbcode.list('item 1', 'item 2')
"[list]
   [*]item 1
   [*]item 2
[/list]"
Operaciones típicas en una linea de código

Corregir cadenas inválidas en una linea:

> str(bbcodeparser("[color=red]Fix this [b]string"))
"[color=red]Fix this [b]string[/b][/color]"

Transformar bbcode a HTML en una linea:

> str(bbcodeparser("[color=red]Fix this [b]string"))
'<span style="color: red;">Fix this <b>string</b></span>'

Ale, ¡Ya está!

Si alguien se anima a usar este módulo y quiere algún ejemplo más, o tiene alguna duda sobre su utilización, le animo a que deje un comentario en este post.

Enlaces de interés:

, , ,

[SOLVED] Añadir claves únicas en Google App Engine en 3 lineas

El problema:

Google App Engine mola, y la verdad es que programar en Python se me está empezando a hacer agradable, aunque me sigue gustando más la indentación con llaves…

En fin, la base de datos que usa Google es superpotente y supersencilla, si bien tiene algunas limitaciones con las que hay que aprender a vivir. Con el SDK que Google da, puedes indicar fácilmente qué atributos indexar (indexed=True), puedes forzar a que un atributo sea definido cuando vas a añadir un elemento a la base de datos (required=True), pero no puedes forzar a que un atributo o conjunto de atributos sean únicos (no hay un unique=True).

Por poner un ejemplo, es el típico campo de nick o e-mail en una base de datos de usuario SQL de toda la vida. No quieres que dos usuarios puedan tener el mismo e-mail o el mismo nick porque quieres identificar univocamente a un usuario por su nick o su e-mail. Es de cajón :p

¿Putada? El SDK de Google App Engine no te permite hacer esto… sin embargo la solución es trivial ;)

La solución:

Lo único que hay que hacer es redefinir la función put dentro de nuestro modelo y lanzar una excepción cuando se cree un elemento cuyas restricciones no se cumplan.

Por simplificar, creemos un modelo User con los atributos name, email, language y creation_date:

class User (db.Model): 
   creation_date = db.DateTimeProperty (auto_now_add=True)
   name          = db.StringProperty (required = True)
   email         = db.EmailProperty (required = True)
   language      = db.StringProperty (default="en")

Tal y como está definida la clase, si ejecutáramos el siguiente código, Google App Engine crearía dos registros distintos:

User (name="Mark Johnson", email="test@codigomanso.com").put()
User (name="JohnMarkinson", email="test@codigomanso.com", language="es").put()

No queremos dos usuarios compartiendo el mismo e-mail!! El segundo put debería fallar. ¿Cómo lo arreglamos? Tan simple como parece, redefinimos put para que cuando se vaya a crear un elemento con un atributo duplicado en la base de datos lance una excepción. Por lo tanto, la nueva clase una vez añadido el método put quedaría:

class User (db.Model): 
   creation_date = db.DateTimeProperty (auto_now_add=True)
   name          = db.StringProperty (required = True)
   email         = db.EmailProperty (required = True)
   language      = db.StringProperty (default="en")
 
   def put (self):
     # Make sure e-mails are unique for each user
     if (not self.is_saved()) and (User.gql ('WHERE email = :1', self.email).count() > 0):
       raise DuplicatedInstanceError ('User.email', self.email)
 
     # call the parent method
     db.Model.put (self)

Si te das cuenta, son sólo 3 lineas de código, 4 contando la linea del def… si es que más fácil no se puede.

Problemas varios

Después de meditarlo un poco, esta implementación no es perfecta. Yo le veo principalmente dos inconvenientes:

  • Hay que realizar un query extra cuando se pretende insertar un elemento (si actualizas un elemento la función se comporta como siempre).
  • Aún con esta solución puede ocurrir que acabes con dos usuarios con el mismo nick en la base de datos. ¿Cómo puede ser? La operación count-put no es atómica, por lo tanto, pueden llegar dos usuarios al mismo tiempo, realizar un count, la función devolver cero en ambos casos, y luego se insertan los dos.

El primer inconveniente, no lo es tanto, pero es una cosa a tener en cuenta.

El segundo inconveniente puede ser una putada. Para solucionarlo se me ocurre que se podría tratar de ejecutar el count-put dentro de una transacción, o bien crear un mutex (se puede?) y que el count-put se realicen dentro del mútex. En fin, a mi ese caso de momento no me preocupa, pero está bien saber que existe.

Apéndice: DuplicatedInstanceError

Si eres avispado te habrás dado cuenta que he lanzado una excepción de tipo DuplicatedInstanceError. Esta clase no existe. La he definido yo para poder detectar cuando ocurre un caso de este tipo.

class DuplicatedInstanceError (Exception):
  def __init__(self, attrpath, value = None):
    '''
    Accept a dict of attribute and value pairs, or just one attribute and a value:
 
    Example:
       # user unique constraint has been violated (User.nick to be unique)
       DuplicatedInstanceError ('User.nick', 'jsmith')
 
       # student unique constraint has been violated (student nick's are fin for different schools)
       DuplicatedInstanceError ({
         'Student.nick'   : 'jsmith',
         'Student.school' : 'demo-primary-school'
       })
    '''
    self.attrpath = ''
 
    # accept a dict as the only parameter
    if isinstance (attrpath, dict):
      for (k, v) in attrpath.items():
        self.attrpath += ', ' + str (k) + ' = ' + str(v)
      self.attrpath = self.attrpath[2:]
    # accept a couple of strings
    else:
      self.attrpath = str(attrpath) + ' = ' + str(value) 
 
    self.attrpath = '(' + self.attrpath + ')'
    return
 
  def __str__ (self):
    return str (self.attrpath)

Ahora a traducir el texto a inglés… Ale, otro día más!

, , , ,

[SOLVED] El teclado numérico no funciona en Ubuntu

Es la segunda vez que me toca corregir el problema de que el teclado numérico no funcione en Ubuntu (y por lo que he visto en foros, también pasa en otras distribuciones de linux).

Solución en 4 simples pasos:
– Abrir Sistema -> Preferencias -> Teclado
– Click en la Pestaña de Teclas del Ratón
– Deseleccionar "Permitir controlar el puntero usando el teclado numérico"
– Y finalmente, hacer click en Cerrar

, ,

prev posts