domingo, 28 de marzo de 2010

Python para móviles, en español, y 1.

Hola a todos. He incluido en El Viaje Del Navegante un gadget contador gráfico de visitas, que indica la procedencia de los usuarios que visitan el blog. Muchas gracias a todos!!!! De verdad, ver que hay gente que lee las cosas que yo escribo desde lugares tan dispares del mundo me llena de orgullo, y sobretodo y más importante, me da ánimos para seguir, así que lo dicho, MUCHAS GRACIAS A TODOS LOS PYTHÓNICOS!!!

(Todo aquel que lee cosas de Python lo considero pythónico, es un bautismo in fact!)

En este artículo voy a hablar de Python y cómo crear aplicaciones para móviles (o celulares si estamos en latinoamérica) para la plataforma S60.

Decir que las pruebas las voy a realizar sobre un Nokia 5800, con sistema operativo Symbian (evidentemente) y con el intérprete de Python.

INSTALACIÓN DE PYTHON EN UN MÓVIL

¿Por dónde empezamos? Más bien por el principio. La página de referencia para empezar a trabajar en programación para dispositivos móviles con Python es el foro de Nokia. Aquí encontraréis información para saber lo que nos vamos a instalar, y porqué instalarlo.

Suponiendo que tenemos un móvil que admite la plataforma S60 y no tenemos Python incluido en él, vamos a buscar el intérprete para instalarlo en el dispositivo. El proyecto Python for S60 se encuentra alojado en Maemo Garage, más concretamente en https://garage.maemo.org/frs/?group_id=854. Aquí podemos encontrar la documentación en PDF (S60 Module Reference Release 2.0.0), así como el fichero de instalación de Python for S60. En mi caso, que estoy trabajando en plataforma Windows, me instalo PythonForS60_2.0.0_Setup.exe. Una vez se descarga y se instala obtenemos lo siguiente:

Lo segundo es conectar el móvil al PC. Para ello utiliza lo que tengas disponible, esto es, por USB, Bluetooth, o la forma de conexión de la que disponga tu dispositivo.

Tercero, utilizar un software que pueda cargar los ficheros SIS (archivo instalador de un proyecto escrito en Python, parecido a JAD en J2ME). En mi caso, al tener un Nokia, me instalé en primer lugar el Nokia Pc Suite. Más tarde opté por el Nokia Ovi Suite.


Cuarto paso. Instalar Python en el móvil. ¿Cómo? Si nos vamos a la instalación del Python for S60 que hemos realizado anteriormente, hay una carpeta llamada PyS60 Dependencies. Aquí se encuentran los ficheros SIS que hay que instalar. Si tienes instalado un programa como Nokia Ovi Suite, por ejemplo, al hacer doble click sobre ellos en principio se instalarán automáticamente en tu móvil (darse cuenta que el móvil tiene que estar conectado al PC de alguna forma).

¿Qué ficheros instalar? Python 2.0.0 y pips son fundamentales. Con esto debería bastar. Pero recomiendo al lector además instalarse stdioserver, SSL y el PythonScriptShell. Vamos, instalarse todo si la memoria de vuestro móvil os deja. Esta información está en el Quick Start Guide que se os instala con el Python for S60.


Si todo ha ido bien (que no tiene porqué ir mal), se os instala Python en vuestro móvil.

CREAR UN HOLA MUNDO EN PYTHON MOBILE

Vamos a crear una aplicación Hola Mundo de toda la vida, y la vamos a instalar en nuestro móvil. Esta explicación está exactamente igual que en el Quick Start Guide, solo que aquí la vamos a hacer paso a paso. Si lo hago es para que el lector se anime a hacerlo, ya que sé por experiencia que, ver las cosas antes de probarlas anima a probarlas.

Paso 1: Nos vamos a nuestro editor favorito y creamos un script en Python.


Paso 2: Nos vamos a PyS60 Application Packager:

Y lo ejecutamos.

Como hemos creado un fichero script Python, seleccionamos ScriptFile (que viene seleccionado por defecto) y buscamos con Browse el fichero que contiene el "Hola Mundo". Una vez seleccionado hacemos click en el botón Create, y ¡¡¡magia!!!, ya lo tenemos:


Darse cuenta de los comentarios que aparecen. En primer lugar el fichero SIS se ha creado, que es nuestro programa listo para ser ejecutado en un móvil con intérprete Python. En las siguientes líneas nos aparecen unos warnings que nos dicen que no hay certificado, ni UID, ni versión de aplicación. Esto se puede configurar, como se verá más adelante.

Si hacemos click en Open Folder, vemos el fichero SIS que se ha generado:


Finalmente, se instala, haciendo click en el botón Install de la ventana que nos dice que se ha generado el fichero, ó directamente haciedo click en el archivo SIS.



En nuestro móvil aceptamos la instalación, e instalamos donde más nos guste,si es que disponemos de tarjeta externa. En la instalación en el móvil podría ser que se nos informará de los riesgos de instalación de aplicaciones no firmadas ó algo por el estilo. Como la aplicación la hemos hecho nosotros, no hay problema.

RESULTADOS
Aquí os presento una foto de la aplicación ejecutándose en un Nokia 5800. Perdonad si se ve mal, pero no tenía una mejor cámara para realizar la foto. Espero podáis disculparme. Esto lo he hecho solo para certificaros que efectivamente funciona.


CONCLUSIONES

No es demasiado complicado instalar Python for Mobile para empezar a crear nuestras aplicaciones para dispositivos móviles en plataformas S60. Además, es muy sencillo empezar a crear aplicaciones. Cabe destacar que no es necesario tener grandes conocimientos sobre este lenguaje para programar con este tipo de dispositivos. Aquí es más importante la imaginación de qué aplicaciones hacer para un dispositivo móvil que cómo hacerlo, ya que esto último es muy sencillo, como veremos en siguientes artículos.

Si el lector quiere buscar más información ,además de la documentación en PDF que viene suministrada, hay un libro bastante interesante, llamado Mobile Python: Rapid Prototyping of Applications on the Mobile Platform, bastante bueno y recomendado. Hay numerosos ejemplos y el código fuente se puede descargar gratuitamente de http://www.mobilepythonbook.com/.

Saludos.

jueves, 25 de marzo de 2010

from modulo import *


(Dagobah, jungla, pantanos, y niebla.)


LUKE: ¿Es mejor from module import * que los imports explícitos?

YODA: No, no mejor. Más rápido, más fácil, más seductor.

LUKE: Pero ¿cómo sabré por qué los imports explícitos son mejores que la forma con el carácter comodín?

YODA: Saberlo tu podrás cuando tu código intentes leer seis meses después.

David Goodger, Code Like a Pythonista: Idiomatic Python
(Traducido por Raúl González Duque)





Los programas deben escribirse para que los lean las personas, y sólo de forma circunstancial para que los ejecuten las máquinas.

Abelson y Sussman, Estructura e Interpretación de Programas de Computadora

...y es que en el estilo de programación la legibilidad cuenta.

miércoles, 24 de marzo de 2010

Traspasar datos entre bases de datos diferentes con Python

En este post vamos a ver cómo traspasar información de una base de datos a otra, aunque solo la estructura y el contenido de las tablas.

Si bien existen en el mercado diferentes aplicaciones que realizan este trabajo, podemos de una manera rápida y sencilla crear un script en Python que realice esta tarea.

Evidentemente, si tenemos conocimientos de ETL, con SQLServer y paquetes DTS ó Integration Services, dependiendo de la versión, se pueden hacer este tipo de operaciones. Pero, ¿y si no tenemos este tipo de herramientas? ¿Y si estamos en plataformas no Windows? Aquí vamos a utilizar Python como "pegamento", esto es, una navaja suiza, para poder traspasar datos. ¿Ventajas? En unas pocas líneas de texto vamos a crear una solución potente y sencilla de traspaso de información.

Como fuente de origen vamos a utilizar una base de datos Firebird, que es un sistema gestor de bases de datos gratuita. Nunca he trabajado con este sistema, y solo lo conocía de oídas, al hablar de Delphi. Sin embargo, en unas dos o tres horas podemos hacernos mas ó menos una vista bastante general de cómo funciona. No es objetivo de este post el explicar las ventajas e inconvenientes de Firebird, pero solo decir que es una opción bastante interesante para aplicar a entornos de producción.

El destino será una base de datos en MySQL, de la que he hablado tanto en este blog.

¿Qué es lo que vamos a hacer? Vamos a crear varios scripts, que lo que van a realizar es:

  1. Conectar con una base de datos Firebird, y obtener los nombres de las tablas de dicha base de datos. A continuación generamos un fichero script SQL con los CREATE TABLE de las tablas obtenidas. Puesto que una tabla no se puede crear sin al menos un campo, crearemos un campo TESTIGO, que luego podrá ser eliminado. Decir que Firebird tiene un módulo para Python, denominado KInterbasDB. A continuación obtener todas las columnas de todas las tablas. Aquí hay que tener cuidado, ya que los tipos de datos de Firebird no son los mismos que los tipos de datos de MySQL. Es más, FireBird utiliza en muchas ocasiones DOMINIOS, para identificar tipos de datos en los campos, que es incompatible con MySQL (está en el estándar ANSI SQL92). De esta manera nos curamos en salud. Este script Python genera 2 ficheros script SQL. El anteriormente dicho y otro con los ALTER TABLE para incluir los campos que nos faltan.
  2. Bien, tenemos las estructuras en ficheros SQL. ¿Qué nos falta? Evidentemente los datos. Para ello nos servimos de un segundo script de Python, que lo que hace es obtener los datos de cada una de las tablas de Firebird (mediante una conexión) y guardarlas en un fichero de script SQL con un formato INSERT INTO.
  3. Finalmente tenemos 3 ficheros script SQL (creación de tablas, inserción de campos en dichas tablas e inserción de datos). Ahora podemos hacer dos cosas, o bien cargar en MySQL estos ficheros script SQL y lanzarnos con una herramienta llamada Query Browser ó crearnos una lanzadera en Python, que haga el trabajo. Vamos a realizar esta última operación como ejemplo, para ver que se puede hacer.
Empezamos, a continuacións se presenta el script Python de obtención de datos de la base de datos origen, que está en una Firebird:



# -*- coding: cp1252 -*-
import kinterbasdb
import os

# Fichero de creación de tablas y adición de campos.
fichero1 = 'crear_tablas.sql'
fichero2 = 'add_campos.sql'

# Ruta de conexión.
rutaAcceso = 'localhost:C:\basedatos_origen\bd_origen_firebird.gdb'

# Intentamos conectar con FireBird.
try:
conexion= kinterbasdb.connect(dsn=rutaAcceso,\
user='sysdba', password='123456',dialect=3, charset='DOS437')
conectado = True
print "Conectado!"
except:
print "No he podido conectar con ", rutaAcceso

# Estamos conectados, a trabajar!
if conectado:
# Obtenemos todas las tablas.
cadenaSQL = '''
select rdb$relation_name
from rdb$relations
where rdb$view_blr is null
and (rdb$system_flag is null or rdb$system_flag = 0);
'''
# Inicializamos cursor.
cursor = conexion.cursor()
print "Recuperando nombres de tablas....!"
# Ejecutamos cursor.
cursor.execute(cadenaSQL)
# Guardamos los nombres de las tablas en una lista.
tablas = []
for fila in cursor:
tablas.append(fila[0])
print "OK!"
# Cadena que obtiene tablas con campos y tipos de datos.
cadenaSQL = '''
select rf.rdb$relation_name, rf.rdb$field_name,
rf.rdb$field_source,
f.rdb$field_type, t.rdb$type_name,
f.rdb$field_sub_type, f.rdb$character_length, f.rdb$field_scale,
f.rdb$field_length, st.rdb$type_name as
rdb$sub_type_name,
case f.rdb$field_type
when 7 then 'smallint'
when 8 then 'integer'
when 16 then 'bigint'
when 9 then 'quad'
when 10 then 'float'
when 11 then 'd_float'
when 17 then 'boolean'
when 27 then 'double'
when 12 then 'datetime'
when 13 then 'time'
when 35 then 'timestamp'
when 261 then 'blob'
when 37 then 'varchar'
when 14 then 'char'
when 40 then 'cstring'
when 45 then 'blob_id'
end as "ActualType",
case f.rdb$field_type
when 7 then
case f.rdb$field_sub_type
when 1 then 'numeric'
when 2 then 'decimal'
end
when 8 then
case f.rdb$field_sub_type
when 1 then 'numeric'
when 2 then 'decimal'
end
when 16 then
case f.rdb$field_sub_type
when 1 then 'numeric'
when 2 then 'decimal'
else 'bigint'
end
when 14 then
case f.rdb$field_sub_type
when 0 then 'unspecified'
when 1 then 'binary'
when 3 then 'acl'
else
case
when f.rdb$field_sub_type is null then 'unspecified'
end
end
when 37 then
case f.rdb$field_sub_type
when 0 then 'unspecified'
when 1 then 'text'
when 3 then 'acl'
else
case
when f.rdb$field_sub_type is null then 'unspecified'
end
end
when 261 then
case f.rdb$field_sub_type
when 0 then 'unspecified'
when 1 then 'text'
when 2 then 'blr'
when 3 then 'acl'
when 4 then 'reserved'
when 5 then 'encoded-meta-data'
when 6 then 'irregular-finished-multi-db-tx'
when 7 then 'transactional_description'
when 8 then 'external_file_description'
end
end as "ActualSubType"
from rdb$relation_fields rf
join rdb$fields f
on f.rdb$field_name = rf.rdb$field_source
left join rdb$types t
on t.rdb$type = f.rdb$field_type
and t.rdb$field_name = 'RDB$FIELD_TYPE'
left join rdb$types st
on st.rdb$type = f.rdb$field_sub_type
and st.rdb$field_name = 'RDB$FIELD_SUB_TYPE'
where rf.rdb$view_context is null
and (rf.rdb$system_flag is null or rf.rdb$system_flag = 0)
order by
rf.rdb$relation_name,
rf.rdb$field_position ;
'''
# Inicializamos cursor.
cursor.close()
cursor = conexion.cursor()
# Info.
print "Recuperando información de campos de tablas!"
# Ejecutamos cursor.
cursor.execute(cadenaSQL)
# Guardamos los nombres de las tablas en una lista.
campos_tablas = []
# Posiciones: 0 nombre tabla, 1 nombre campo, 2 long. campo, 10 tipo dato.
for fila in cursor:
campos_tablas.append((fila[0],fila[1],fila[8],fila[10]))
# Info.
print "OK!"
print "Guardando información en ficheros SQL!"
# Creamos fichero.
f = open(os.path.realpath(fichero1), "w")
# Creamos los create table.
for tabla in tablas:
cadenaSQL = 'CREATE TABLE ' + str(tabla).strip() +' ( campo_testigo char(1) ) ;'
f.write(cadenaSQL+"\n")
# Cerramos fichero.
f.close()
# Creamos fichero.
f2 = open(os.path.realpath(fichero2), "w")
# Creamos alter table.
for campo in campos_tablas:
if str(campo[3]).strip() == 'datetime' or \
str(campo[3]).strip() == 'timestamp' or \
str(campo[3]).strip() == 'double' or \
str(campo[3]).strip() == 'time':
cadenaSQL = 'ALTER TABLE ' + str(campo[0]).strip() + ' ADD ' + str(campo[1]).strip() + \
' ' + str(campo[3]).strip() + ' ;'
else:
cadenaSQL = 'ALTER TABLE ' + str(campo[0]).strip() + ' ADD ' + str(campo[1]).strip() + \
' ' + str(campo[3]).strip() + '(' + str(campo[2]).strip()+ ') ;'
f2.write(cadenaSQL+"\n")
# Cerramos fichero.
f2.close()
# Info.
print "Terminada exportación de datos!"
# Cerramos conexiones de cursor y base de datos.
print "Cerrando conexiones!"
cursor.close()
conexion.close()


Observaciones:

Este script genera dos ficheros script SQL, crear_tablas.SQL y add_campos.SQL que contienen sentencias SQL, CREATE TABLE y ALTER TABLE. Se puede ver que para obtener información de Firebird he tenido que utilizar unos SELECT que atacan a tablas del sistema. Estas sentencias las he tenido que buscar en Internet con San Google y modificarlas levemente, para poder obtener los datos que necesitaba. Darse cuenta en el SELECT que busca los campos de las tablas que hay que hacer una correspondencia, mas o menos acertada entre tipos de datos. ¿Qué significa esto? Pues que si queremos que el destino no sea MySQL sino otro sistema gestor bastará con cambiar ahí las correspondencias. Por ejemplo, en SQL Server de Microsoft un entero es int (no integer).

En el fichero crear_tablas.SQL tendremos en cada línea información como esta:

CREATE TABLE ejemplo1 campo_testigo char(1) ;

En el fichero add_campos.SQL tendremos en cada línea información como esta:

ALTER TABLE ejemplo1 ADD campo1 integer(4) ;
ALTER TABLE ejemplo1 ADD campo2 double ;

Continuamos. En este segundo script Python, como hemos dicho anteriormente, obtenemos la información de las tablas. Dicha información se guardará en un fichero script SQL con un formato de INSERT INTO. Veámoslo a continuación:



# -*- coding: utf-8 -*-
import kinterbasdb
import os

def reemplazar_caracteres(cadena):
cadena = str(cadena)
# Reemplazamos \ por /.
cadena = cadena.replace('\\','/')
# Quitamos comillas dobles.
cadena = cadena.replace('"','')
return cadena

# Info.
print "Carga de datos en el sistema cliente"

# Fichero de carga de datos mediante insert.
fichero1 = 'insert.sql'

# Ruta de conexión del origen.
rutaAcceso = 'localhost:C:\basedatos_origen\bd_origen_firebird.gdb'

# Inicializamos testigo.
conectado = False

# Intentamos conectar con FireBird.
try:
conexion= kinterbasdb.connect(dsn=rutaAcceso,\
user='sysdba', password='123456',dialect=3, charset='DOS437')
conectado = True
print "Conectado con FireBird!"
except:
print "No he podido conectar con ", rutaAcceso

# Estamos conectados, a trabajar!
if conectado:
# Obtenemos todas las tablas.
cadenaSQL = '''
select rdb$relation_name
from rdb$relations
where rdb$view_blr is null
and (rdb$system_flag is null or rdb$system_flag = 0);
'''
# Inicializamos cursor.
cursor = conexion.cursor()
print "Recuperando nombres de tablas....!"
# Ejecutamos cursor.
cursor.execute(cadenaSQL)
# Guardamos los nombres de las tablas en una list
tablas = []
for fila in cursor:
tablas.append(fila[0])
print "OK!"
# Creamos fichero de script en donde se guardarán los insert.
f3 = open(os.path.realpath(fichero1), "w")
# Info.
print "Generando sentencias INSERT para volcado de datos."
# Vamos obteniendo información por cada tabla para generar sentencias SQL INSERT.
for tabla in tablas:
# info.
print "Formando los INSERT para tabla ", tabla
# Tabla actual a tratar.
tabla_actual = tabla
# Info.
print "Recuperando información... esta operación puede tardar..."
# Obtenemos todas las filas de la tabla.
sentenciaSelect = "select * from "
cursor.execute(sentenciaSelect + " " + tabla_actual)
aux = cursor.fetchall()
# Info.
print "Formando sentencias INSERT y guardándolas en fichero script... un momento..."
# Para cada fila, vamos obteniendo la información para construir las INSERT.
for i in aux:
cadenaSQL = 'INSERT INTO ' + tabla_actual + ' VALUES('
aux2 = ''
for j in i:
# Si nos encontramos con un registro muy grande podría interesar obviarlo. OJO!!!!!
if len(str(j)) > 2000:
j = "DATOS NO CARGADOS"
print "Cadena demasiada larga, no se guardará!"
if aux2 == '':
j = reemplazar_caracteres(j)
aux2 = 'null' if j == 'None' else '"' + str(j) + '"'
else:
j = reemplazar_caracteres(j)
k = 'null' if str(j) == 'None' else '"' + str(j) + '"'
aux2 = str(aux2) + ', ' + k
cadenaSQL = str(cadenaSQL) + str(aux2) + ') ;'
f3.write(cadenaSQL+"\n")
# Cerramos fichero.
f3.close()
# Cerramos conexiones de cursor y base de datos.
print "Cerrando conexiones de FireBird!"
cursor.close()
conexion.close()

Observaciones:

Vemos que este script Python genera un script SQL llamado insert.SQL. Este fichero tendrá, por cada línea la siguiente información:

INSERT INTO nombre_tabla VALUES('DATO 1','DATO 2',....,'DATO N') ;

El lector tiene que apreciar lo sencillo que es hacer herramientas potentes con unas pocas líneas de código.

Ya tenemos los 3 ficheros script SQL listos para ser ejecutados en el Query Browser de MySQL. Y si no queremos hacerlo pues nos montamos una lanzadera y listo. Voy a escribir el código para una lanzadera, que puede servir para estos tres scripts SQL creados. Aquí lo presento para insert.SQL, aunque evidentemente sirve para los tres, con los cambios apropiados (que son triviales y dejo al lector para no extender más este artículo). Aquí está el último script en Python, la lanzadera:



# -*- coding: utf-8 -*-
import MySQLdb
import os

# Info.
print "Lanzadera de sentencias SQL."

# Fichero a lanzar.
fichero1 = 'insert.sql'
fichero_error = 'error_lanzadera.sql'

# Base de datos.
base_de_datos = 'bd_destino_MySQL'

# Inicializamos testigo.
conectado = False

# Intentamos conectar con sistema gestor (MySQL).
try:
conexionMySQL = MySQLdb.Connect("localhost", "root","1234",base_de_datos)
cursorMySQL = conexionMySQL.cursor()
conectado = True
print "Conectado con MySQL!"
except:
print "No he podido conectar con base de datos MySQL: ", base_de_datos

# Estamos conectados, a trabajar!
if conectado:
# Abrimos fichero de sentencias SQL.
f = open(os.path.realpath(fichero1),'r')
# Abrimos fichero de posibles errores.
f1 = open(os.path.realpath(fichero_error),'w')
# Ejecutamos sentencia SQL, por cada línea.
while True:
# Línea de fichero.
cadenaSQL = f.readline()
if not cadenaSQL:
print "TERMINÉ DE LANZAR SENTENCIAS SQL. SALIENDO DEL SCRIPT DE PYTHON!!!"
break
try:
# Lanzamos Sentencia.
cursorMySQL.execute(cadenaSQL)
# Confirmamos escritura.
conexionMySQL.commit()
except:
# Hubo un error, se informa, hay que parar.
conexionMySQL.rollback()
# info.
print "Hubo un error en la instrucción: ", cadenaSQL
print "Guardando fallo en fichero!"
f1.write(cadenaSQL + "\n")
# Cerramos fichero.
f.close()
f1.close()
# Cerramos conexiones de cursor y base de datos.
print "Cerrando conexiones de MySQL!"
cursorMySQL.close()
conexionMySQL.close()


Observaciones:

Este script lanza un script SQL dado. Además genera un fichero con las posibles sentencias SQL que han fallado. Darse cuenta que aunque haya sentencias que fallen, el script Python sigue ejecutándose. De este modo en el fichero de errores tendremos únicamente las que han fallado, de modo que lo tendremos todo controlado, y sabremos lo que ha funcionado y lo que no. En el fichero de errores se guardan las sentencias enteras.

CONCLUSIONES:

No es difícil crear con Python herramientas que nos saquen de más de un apuro. En este caso crear un trasvase de información entre bases de datos de diferentes fabricantes. Cabría destacar que se podría eliminar el campo testigo que se ha utilizado, mediante el correspondiente script SQL (generado en Python), que se deja al lector para su desarrollo. Cuestiones negativas son las codificaciones en las que se encuentren las bases de datos, ya que nos podemos llevar una sorpresa desagradable si se encuentran con caracteres extraños (el eterno problema de las codificaciones en Python, algo que sigue sin gustarme).
De todas formas este tipo de operaciones se pueden realizar con herramientas que están en el mercado, y muy potentes. Aunque si no las tienes a mano, y necesitas salir del apuro... Python está ahí para algo.

Y es que Python aquí, funciona como un verdadero pegamento.

sábado, 20 de marzo de 2010

La paradoja Python, ¿tú que opinas?


A falta de un nombre mejor lo llamaré la paradoja Python: si una compañía decide escribir su software en un lenguaje poco utilizado comparativamente, serán capaces de contratar a mejores programadores, porque atraerán sólo a aquellos que se molestaron en aprenderlo…

Hasta ahora sólo unas pocas compañías han sido suficientemente inteligentes para darse cuenta de esto. Pero hay una especie de selección natural trabajando: son exactamente las mismas compañías en las que a los programadores les gustaría trabajar. Google, por ejemplo. Cuando ofertan puestos para programar en Java, también piden experiencia con Python.

Paul Graham

Esta cita la he rescatado de mundogeek.net, en http://mundogeek.net/archivos/2007/10/23/la-paradoja-python/.

Me ha parecido interesante este post y los comentarios de internautas sobre la naturaleza de un buen programador. A excepción de comentarios fuera de tono e insultos innecesarios me parece que es sumamente interesante este post y la discusión posterior en mundogeek.

En mi humilde opinión un buen programador se hace con el tiempo, a base de trabajar y depurar, y con el constante estudio. Evidentemente hay que llevar una buena base, y si te has sacado la carrera de informática, una base buena has de tener. En ella se aprende mucho de algorítmica y estructuras de datos, que es fundamental para cualquier buen programador. Por otra parte, la experiencia en diversos lenguajes, con diversos paradigmas, ayuda y enriquece en el estilo de programación, aportando ideas y soluciones nuevas a problemas planteados.

Un buen programador lo hace la formación inicial, la experiencia, y el contínuo reciclado de conocimientos, ya que en esta bendita profesión, todo es muy cambiante. Yo salí de la facultad sin saber programación orientada a objetos, por ejemplo, y fue en el mundo laboral donde aprendí. Sin embargo, las nociones de programación estructurada las tenía perfectamente asimiladas. Con ello quiero decir que hay que aprender y evolucionar. Quien sabe si el día de mañana no aparece otro paradigma de programación diferente a POO. Siempre hay que investigar, indagar y preocuparse por lo nuevo que aparece en este mundo.

¿Python? ¿Por qué no? Yo jamás he trabajado con Python en las empresas donde he estado, y sin embargo lo estoy aprendiendo (nunca paro de aprender en Python, siempre hay algo nuevo, de verdad) porque me gusta cómo hace las cosas, cómo implementa los paradigmas de programación de los que dispone, la facilidad de uso, la curva de aprendizaje tan exigua, y las enormes aplicaciones que tiene (escritorio, web, móviles), entre otras muchas. Y si algún día estoy en una empresa que utilice Python, estaremos todos de enhorabuena; esto es, yo por trabajar en una plataforma excelente y favorita, y la empresa porque va a tener un trabajador disfrutando con su trabajo, con las implicaciones que eso conlleva (rendimiento elevado, por ejemplo).

¿Tú que opinas?

sábado, 13 de marzo de 2010

Crear documentos PDF en Python, y 2.

Hola a todos. Vamos a seguir en este post con la temática de crear documentos PDF con Python, que empecé hace unas semanas. Os recomiendo que si no habéis leído el primer post sobre ReportLab, lo hagáis antes, para no perderos. En este link.

Anteriormente vimos como insertar imágenes. Ahora toca como insertar texto. Hay varias maneras, pero como no quiero alargar en exceso los artículos, solo veremos una de las formas. Más adelante (en otros post) veremos otras y como juntar todo en una aplicación pequeña con wxPython. Incluso se podría ver con GTK, o TKinter. Estoy abierto a sugerencias, así que si os animáis a participar estaría encantado.

Hay varias formas de escribir texto en PDF con ReportLab. Como hay que ser ordenados, vamos a empezar por la primera. Recordar que es mejor que el lector lea previamente el post número 1 de ReportLab, ya que doy por sentado ciertos conocimientos que ahí vienen descritos.

Utilizando el objeto Text

La interfaz del objeto Text (sus métodos) da un control detallado de la escritura de texto. Un objeto Text mantiene un cursor de texto que se mueve a lo largo de la página cuando el texto comienza a dibujarse.

ReportLab, en principio, no se lleva bien con el español. Esto es, volvemos al problema de siempre con las codificaciones. Para solventar el asunto y que podamos escribir tildes y nuestra "ñ", en latin-1 ó iso-8859-1, hay que utilizar el comando unicode, tal como aparece en el ejemplo. Si no se utiliza, te dará un error de CodeDecodeError: ' utf8' codec can´t decode byes in position... ¿No me crees? Haz la prueba.

En el siguiente ejemplo vamos a utilizar los siguientes métodos:

beginText(): Para instanciar el objeto Text.
setTextOrigin
(x,y): Mueve el cursor a una posición dada por las coordenadas (x,y).
setFont(tipoLetra,tamañoLetra, Leading = None): Cambia el tipo de letra.
textLine(linea_texto): Inserta una de texto en el objeto y baja el cursor a la línea siguiente.
textLines(lineas_texto): Inserta líneas de texto en el objeto y baja el cursor después de la última línea.
setFillGray(g): Ajusta el grado de gris al color del texto (0 <= g <= 1). drawText(): Dibuja el texto del objeto.

Así que tenemos:

from reportlab.pdfgen import canvas

oracion = ['Y en tu ausencia las paredes','se pintarán de tristeza',\
'y enjaularé mi corazón entre tus huesos.']


aux = canvas.Canvas("prueba.pdf")

textobject = aux.beginText()
textobject.setTextOrigin(100, 500)
textobject.setFont("Courier", 14)
for line in oracion:
uniLine = unicode(line, 'latin-1')
textobject.textLine(uniLine)
textobject.setFillGray(0.5)
lineas_texto = '''
Hola a todos. En este post vamos a ver
como escribir texto con Python y ReportLab.
Más información en El Viaje del Navegante.
'''
lineas_texto = unicode(lineas_texto,'latin-1')
textobject.textLines(lineas_texto)
aux.drawText(textobject)

# Salvamos.
aux.showPage()
aux.save()

Darse cuenta del movimiento automático del cursor a la hora de ubicar el texto una vez que el origen ha sido dado. El resultado sería el siguiente:

Como vemos en este ejemplo, instanciamos un objeto, y mediante sus métodos vamos contruyendo el texto que queremos, para luego pasarlo a un fichero PDF. La cuestión aquí es que utilizamos un objeto para realizar todas escrituras, y nos olvidamos de la posición del cursor, pues son los propios métodos vistos los que se encargan de todo.

Tipos de letra. En este caso he ido directo al código de ReportLab, e indagando me he encontrado con un archivo que contiene los tipos de letra. Este fichero se puede encontrar en: Lib\site-packages\reportlab\pdfbase\_fontdata.py.
Esto es lo que he encontrado:

standardFonts = ( 'Courier', 'Courier-Bold', 'Courier-Oblique', 'Courier-BoldOblique', 'Helvetica', 'Helvetica-Bold', 'Helvetica-Oblique', 'Helvetica-BoldOblique', 'Times-Roman', 'Times-Bold', 'Times-Italic', 'Times-BoldItalic', 'Symbol','ZapfDingbats')

Estos 14 tipos de letra estándar siempre están disponibles, y no necesitan guardarse en el PDF, ya que se nos garantiza que están presentes en Acrobat Reader. Investigar está bien, pero ver los tipos de letra con los métodos apropiados está mejor. Podemos ver esta misma información mediante el siguiente ejemplo:


from reportlab.pdfgen import canvas

cadena_texto = "Tipo de letra con ReportLab en El Viaje del Navegante"
aux = canvas.Canvas("tipos-letra.pdf")

textobject = aux.beginText()
textobject.setTextOrigin(20, 500)
for tipo_letra in aux.getAvailableFonts():
textobject.setFont(tipo_letra, 12)
texto = unicode(cadena_texto, 'latin-1')
textobject.textLine(tipo_letra + ": " + texto)
aux.drawText(textobject)

# Salvamos.
aux.showPage()
aux.save()
Y como resultado obtenemos lo siguiente:

En el siguiente ejemplo utilizamos el método moveCursor(dx, dy). De esta forma vamos a poder controlar el movimiento del cursor de forma más explícita. Dicho método mueve el cursor como un desplazamiento (offset) desde el principio de la actual linea, no el cursor actual. Los movimientos de desplazamientos positivos (y) son hacia abajo (en contraposición con la geometría normal donde el movimiento positivo de y mueve hacia arriba). Darse cuenta que el método textOut no mueve hacia abajo la línea, en contraposicion con el método textLine, que sí lo hace.

from reportlab.pdfgen import canvas

oracion = ['Y en tu ausencia las paredes','se pintarán de tristeza',\
'y enjaularé mi corazón entre tus huesos.']


aux = canvas.Canvas("prueba2.pdf")

textobject = aux.beginText()
textobject.setTextOrigin(100, 500)
textobject.setFont("Courier", 16)
for line in oracion:
uniLine = unicode(line, 'latin-1')
textobject.textOut(uniLine)
# "Y" es positivo y mueve el cursor hacia abajo.
textobject.moveCursor(20,15)
textobject.setFillColorRGB(0.2,0,0.6)
aux.drawText(textobject)

# Salvamos.
aux.showPage()
aux.save()


Y el resultado es el siguiente:

Hay dos métodos especiales para separar tanto palabras del texto con espacios como para separar las letras de las palabras que conforman el texto. Ellos son setWordSpace(wordSpace) y setCharSpace(charSpace) respectivamente. Veámoslo con el siguiente ejemplo:

from reportlab.pdfgen import canvas

oracion = ['Y en tu ausencia las paredes','se pintarán de tristeza',\
'y enjaularé mi corazón entre tus huesos.']


aux = canvas.Canvas("prueba3.pdf")

textobject = aux.beginText()
textobject.setTextOrigin(25, 500)
textobject.setFont("Courier", 14)
charspace = 0
for line in oracion:
textobject.setCharSpace(charspace)
uniLine = unicode(line, 'latin-1')
textobject.textLine("Espacio %s : %s " % (charspace,uniLine))
charspace = charspace + 1
lineas_texto = '''
Ejemplo de espaciado de palabras:
Hola a todos. En este post vamos a ver
como escribir texto con Python y ReportLab.
Más información en El Viaje del Navegante.
'''
textobject.setFillGray(0.5)
textobject.setWordSpace(8)
lineas_texto = unicode(lineas_texto,'latin-1')
textobject.textLines(lineas_texto)
aux.drawText(textobject)
# Salvamos.
aux.showPage()
aux.save()

Con el consecuente resultado:


Darse cuenta que las primeras 3 líneas lo que se está haciendo es separar las letras. En el segundo bloque, lo que se hace es separar las palabras. Como no quiero hacer este post demasiado largo, por el momento es todo. En siguientes artículos seguiré presentando varias características de escritura en ReportLab, así como otras formas de hacerlo.

miércoles, 10 de marzo de 2010

Crear mantenimiento básico con Python y wxPython

Vamos a ver en este post como crear un mantenimiento de clientes, creando, actualizando, insertando y borrando registros de una base de datos MySQL. Como interfaz, utilizaremos wxPython, y como lenguaje, Python (¡sorpresa!).

Lo primero es crear la interfaz. Para ello utilizamos wxGlade. Hace tiempo escribí un post de cómo crear aplicaciones en este diseñador de pantallas, así que aquí pongo una captura de cómo es la pantalla, y a continuación el código que genera wxGlade, el cual he modificado (variables a1, a2, a3 ...) para poder llamar a los manejadores de eventos correspondientes.. Los iconos del frame los he buscado en Internet, ya que no vienen en ninguna distribución de wxGlade. Quedaría así:


El funcionamiento es muy simple. Están los típicos botones de crear, eliminar, borrar y guardar cliente. Además, mediante los botones de flechas (azules) se puede navegar a través de la tabla de clientes.

El código que genera wxGlade (y que he modificado) lo he guardado en el fichero cliente_frame.py, y es el siguiente:

#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-
# generated by wxGlade 0.6.3 on Tue Mar 09 23:59:26 2010

import wx

# begin wxGlade: extracode
# end wxGlade



class MyFrame(wx.Frame):
def __init__(self, *args, **kwds):
# begin wxGlade: MyFrame.__init__
kwds["style"] = wx.DEFAULT_FRAME_STYLE
wx.Frame.__init__(self, *args, **kwds)
self.panel = wx.Panel(self, -1)

# Menu Bar
self.barra_menu = wx.MenuBar()
self.SetMenuBar(self.barra_menu)
# Menu Bar end
self.barra_estado = self.CreateStatusBar(1, wx.ST_SIZEGRIP)

# Tool Bar
self.barra_herramientas = wx.ToolBar(self, -1)
self.SetToolBar(self.barra_herramientas)
a1 = self.barra_herramientas.AddLabelTool(wx.NewId(), "nuevo", \
wx.Bitmap("d:\\python\\iconos\\on\\filenew.gif", wx.BITMAP_TYPE_ANY) \
, wx.NullBitmap, wx.ITEM_NORMAL, "Nuevo", "Crear nuevo cliente")
a2 = self.barra_herramientas.AddLabelTool(wx.NewId(), "guardar", \
wx.Bitmap("d:\\python\\iconos\\on\\filesave.gif", wx.BITMAP_TYPE_ANY)\
, wx.Bitmap("d:\\python\\iconos\\off\\filesave.gif", wx.BITMAP_TYPE_ANY), \
wx.ITEM_NORMAL, "Guardar", "Guardar cliente en base de datos")
a3 = self.barra_herramientas.AddLabelTool(wx.NewId(), "cancelar", \
wx.Bitmap("d:\\python\\iconos\\on\\cancel.gif", wx.BITMAP_TYPE_ANY) \
, wx.Bitmap("d:\\python\\iconos\\off\\cancel.gif", wx.BITMAP_TYPE_ANY),\
wx.ITEM_NORMAL, "Cancelar", "Cancelar operación")
a4 = self.barra_herramientas.AddLabelTool(wx.NewId(), "editar", \
wx.Bitmap("d:\\python\\iconos\\on\\edit.gif", wx.BITMAP_TYPE_ANY)\
, wx.Bitmap("d:\\python\\iconos\\off\\edit.gif", wx.BITMAP_TYPE_ANY),\
wx.ITEM_NORMAL, "Editar", "Editar cliente actual")
a5 = self.barra_herramientas.AddLabelTool(wx.NewId(), "buscar",\
wx.Bitmap("d:\\python\\iconos\\on\\find.gif", wx.BITMAP_TYPE_ANY)\
, wx.Bitmap("d:\\python\\iconos\\off\\find.gif", wx.BITMAP_TYPE_ANY),\
wx.ITEM_NORMAL, "Buscar", "Buscar cliente en base de datos")
a6 = self.barra_herramientas.AddLabelTool(wx.NewId(), "eliminar", \
wx.Bitmap("d:\\python\\iconos\\on\\remove.gif", wx.BITMAP_TYPE_ANY)\
, wx.Bitmap("d:\\python\\iconos\\off\\remove.gif", wx.BITMAP_TYPE_ANY),\
wx.ITEM_NORMAL, "Eliminar", "Eliminar cliente de base de datos")
a7 = self.barra_herramientas.AddLabelTool(wx.NewId(), "recargar", \
wx.Bitmap("d:\\python\\iconos\\on\\reload.gif", wx.BITMAP_TYPE_ANY)\
, wx.Bitmap("d:\\python\\iconos\\off\\reload.gif", wx.BITMAP_TYPE_ANY),\
wx.ITEM_NORMAL, "Recargar", "Recargar información de cliente")
a8 = self.barra_herramientas.AddLabelTool(wx.NewId(), "izda_final",\
wx.Bitmap("d:\\python\\iconos\\on\\player_start.gif", wx.BITMAP_TYPE_ANY)\
, wx.Bitmap("d:\\python\\iconos\\off\\player_start.gif", wx.BITMAP_TYPE_ANY),\
wx.ITEM_NORMAL, "Inicio", "Primer registro")
a9 = self.barra_herramientas.AddLabelTool(wx.NewId(), "izda_rapido", \
wx.Bitmap("d:\\python\\iconos\\on\\2leftarrow.gif", wx.BITMAP_TYPE_ANY)\
, wx.Bitmap("d:\\python\\iconos\\off\\2leftarrow.gif",\
wx.BITMAP_TYPE_ANY), wx.ITEM_NORMAL, u"Avance rápido",\
u"Avance rápido de registros a la izquierda")
a10 = self.barra_herramientas.AddLabelTool(wx.NewId(), "izda", \
wx.Bitmap("d:\\python\\iconos\\on\\1leftarrow.gif", wx.BITMAP_TYPE_ANY)\
, wx.Bitmap("d:\\python\\iconos\\off\\1leftarrow.gif", wx.BITMAP_TYPE_ANY), \
wx.ITEM_NORMAL, "Izquierda", "Avance de un registro a la izquierda")
a11 = self.barra_herramientas.AddLabelTool(wx.NewId(), "drcha", \
wx.Bitmap("d:\\python\\iconos\\on\\1rightarrow.gif", wx.BITMAP_TYPE_ANY)\
, wx.Bitmap("d:\\python\\iconos\\off\\1rightarrow.gif", wx.BITMAP_TYPE_ANY),\
wx.ITEM_NORMAL, "Derecha", "Avance de un registro a la derecha")
a12 = self.barra_herramientas.AddLabelTool(wx.NewId(), "drcha_rapido", \
wx.Bitmap("d:\\python\\iconos\\on\\2rightarrow.gif", wx.BITMAP_TYPE_ANY),\
wx.Bitmap("d:\\python\\iconos\\off\\2rightarrow.gif", wx.BITMAP_TYPE_ANY), \
wx.ITEM_NORMAL, u"Avance rápido", u"Avance rápido de registros a la derecha")
a13 = self.barra_herramientas.AddLabelTool(wx.NewId(), "drcha_final", \
wx.Bitmap("d:\\python\\iconos\\on\\player_end.gif", wx.BITMAP_TYPE_ANY), \
wx.Bitmap("d:\\python\\iconos\\off\\player_end.gif", wx.BITMAP_TYPE_ANY),\
wx.ITEM_NORMAL, "Final", u"Último registro")
a14 = self.barra_herramientas.AddLabelTool(wx.NewId(), "ayuda", \
wx.Bitmap("d:\\python\\iconos\\on\\help.gif", wx.BITMAP_TYPE_ANY), \
wx.Bitmap("d:\\python\\iconos\\off\\help.gif", wx.BITMAP_TYPE_ANY),\
wx.ITEM_NORMAL, "Ayuda", "Programa clientes escrito en Python, wxPython y MySQL.")
a15 = self.barra_herramientas.AddLabelTool(wx.NewId(), "salir", \
wx.Bitmap("d:\\python\\iconos\\on\\exit.gif", wx.BITMAP_TYPE_ANY), \
wx.Bitmap("d:\\python\\iconos\\off\\exit.gif", wx.BITMAP_TYPE_ANY),\
wx.ITEM_NORMAL, "Salir", "Salir de la aplicación de clientes")
# Tool Bar end
self.label_1 = wx.StaticText(self.panel, -1, "ID")
self.text_ctrl_id = wx.TextCtrl(self.panel, -1, "", style=wx.TE_READONLY)
self.label_2 = wx.StaticText(self.panel, -1, "NOMBRE")
self.text_ctrl_nombre = wx.TextCtrl(self.panel, -1, "")
self.label_3 = wx.StaticText(self.panel, -1, "APELLIDOS")
self.text_ctrl_apellidos = wx.TextCtrl(self.panel, -1, "")
self.label_4 = wx.StaticText(self.panel, -1, "NIF")
self.text_ctrl_nif = wx.TextCtrl(self.panel, -1, "")
self.label_5 = wx.StaticText(self.panel, -1, u"CORREO ELECTRÓNICO")
self.text_ctrl_email = wx.TextCtrl(self.panel, -1, "")
self.label_6 = wx.StaticText(self.panel, -1, u"DIRECCIÓN WEB")
self.text_ctrl_web = wx.TextCtrl(self.panel, -1, "")
self.label_7 = wx.StaticText(self.panel, -1, "OBSERVACIONES")
self.text_ctrl_observ = wx.TextCtrl(self.panel, -1, "", \
style=wx.TE_MULTILINE|wx.HSCROLL)

self.__set_properties()
self.__do_layout()

self.Bind(wx.EVT_TOOL, self.Onnuevo, a1)
self.Bind(wx.EVT_TOOL, self.Onguardar, a2)
self.Bind(wx.EVT_TOOL, self.Oncancelar, a3)
self.Bind(wx.EVT_TOOL, self.Oneditar, a4)
self.Bind(wx.EVT_TOOL, self.Onbuscar, a5)
self.Bind(wx.EVT_TOOL, self.Oneliminar, a6)
self.Bind(wx.EVT_TOOL, self.Onrecargar, a7)
self.Bind(wx.EVT_TOOL, self.Onizda_final, a8)
self.Bind(wx.EVT_TOOL, self.Onizda_rapido, a9)
self.Bind(wx.EVT_TOOL, self.Onizda, a10)
self.Bind(wx.EVT_TOOL, self.Ondrcha, a11)
self.Bind(wx.EVT_TOOL, self.Ondrcha_rapido, a12)
self.Bind(wx.EVT_TOOL, self.Ondrcha_final, a13)
self.Bind(wx.EVT_TOOL, self.Onayuda, a14)
self.Bind(wx.EVT_TOOL, self.Onsalir, a15)
# end wxGlade

def __set_properties(self):
# begin wxGlade: MyFrame.__set_properties
self.SetTitle("Mantenimiento de clientes")
_icon = wx.EmptyIcon()
_icon.CopyFromBitmap(wx.Bitmap("D:\\python\\iconos\\aplicacion\\icon_history.gif", \
wx.BITMAP_TYPE_ANY))
self.SetIcon(_icon)
self.SetSize((436, 386))
self.barra_estado.SetStatusWidths([-1])
# statusbar fields
barra_estado_fields = ["http://elviajedelnavegante.blogspot.com"]
for i in range(len(barra_estado_fields)):
self.barra_estado.SetStatusText(barra_estado_fields[i], i)
self.barra_herramientas.Realize()
self.text_ctrl_id.SetMinSize((250, 20))
self.text_ctrl_nombre.SetMinSize((250, 20))
self.text_ctrl_apellidos.SetMinSize((250, 20))
self.text_ctrl_nif.SetMinSize((250, 20))
self.text_ctrl_email.SetMinSize((250, 20))
self.text_ctrl_web.SetMinSize((250, 20))
self.text_ctrl_observ.SetMinSize((250, 100))
# end wxGlade

def __do_layout(self):
# begin wxGlade: MyFrame.__do_layout
sizer = wx.BoxSizer(wx.VERTICAL)
grid_sizer_1 = wx.FlexGridSizer(7, 2, 0, 0)
grid_sizer_1.Add(self.label_1, 0, 0, 0)
grid_sizer_1.Add(self.text_ctrl_id, 0, 0, 0)
grid_sizer_1.Add(self.label_2, 0, 0, 0)
grid_sizer_1.Add(self.text_ctrl_nombre, 0, 0, 0)
grid_sizer_1.Add(self.label_3, 0, 0, 0)
grid_sizer_1.Add(self.text_ctrl_apellidos, 0, 0, 0)
grid_sizer_1.Add(self.label_4, 0, 0, 0)
grid_sizer_1.Add(self.text_ctrl_nif, 0, 0, 0)
grid_sizer_1.Add(self.label_5, 0, 0, 0)
grid_sizer_1.Add(self.text_ctrl_email, 0, 0, 0)
grid_sizer_1.Add(self.label_6, 0, 0, 0)
grid_sizer_1.Add(self.text_ctrl_web, 0, 0, 0)
grid_sizer_1.Add(self.label_7, 0, 0, 0)
grid_sizer_1.Add(self.text_ctrl_observ, 0, 0, 0)
self.panel.SetSizer(grid_sizer_1)
grid_sizer_1.AddGrowableRow(0)
grid_sizer_1.AddGrowableRow(1)
grid_sizer_1.AddGrowableRow(2)
grid_sizer_1.AddGrowableRow(3)
grid_sizer_1.AddGrowableRow(4)
grid_sizer_1.AddGrowableRow(5)
grid_sizer_1.AddGrowableRow(6)
grid_sizer_1.AddGrowableCol(0)
grid_sizer_1.AddGrowableCol(1)
sizer.Add(self.panel, 1, wx.EXPAND, 0)
self.SetSizer(sizer)
sizer.SetSizeHints(self)
self.Layout()
self.Centre()
# end wxGlade

def Onnuevo(self, event): # wxGlade: MyFrame.
print "Event handler `Onnuevo' not implemented!"
#event.Skip()

def Onguardar(self, event): # wxGlade: MyFrame.
print "Event handler `Onguardar' not implemented!"
event.Skip()

def Oncancelar(self, event): # wxGlade: MyFrame.
print "Event handler `Oncancelar' not implemented!"
event.Skip()

def Oneditar(self, event): # wxGlade: MyFrame.
print "Event handler `Oneditar' not implemented!"
event.Skip()

def Onbuscar(self, event): # wxGlade: MyFrame.
print "Event handler `Onbuscar' not implemented!"
event.Skip()

def Oneliminar(self, event): # wxGlade: MyFrame.
print "Event handler `Oneliminar' not implemented!"
event.Skip()

def Onrecargar(self, event): # wxGlade: MyFrame.
print "Event handler `Onrecargar' not implemented!"
event.Skip()

def Onizda_final(self, event): # wxGlade: MyFrame.
print "Event handler `Onizda_final' not implemented!"
event.Skip()

def Onizda_rapido(self, event): # wxGlade: MyFrame.
print "Event handler `Onizda_rapido' not implemented!"
event.Skip()

def Onizda(self, event): # wxGlade: MyFrame.
print "Event handler `Onizda' not implemented!"
event.Skip()

def Ondrcha(self, event): # wxGlade: MyFrame.
print "Event handler `Ondrcha' not implemented!"
event.Skip()

def Ondrcha_rapido(self, event): # wxGlade: MyFrame.
print "Event handler `Ondrcha_rapido' not implemented!"
event.Skip()

def Ondrcha_final(self, event): # wxGlade: MyFrame.
print "Event handler `Ondrcha_final' not implemented!"
event.Skip()

def Onayuda(self, event): # wxGlade: MyFrame.
print "Event handler `Onayuda' not implemented!"
event.Skip()

def Onsalir(self, event): # wxGlade: MyFrame.
print "Event handler `Onsalir' not implemented!"
event.Skip()

# end of class MyFrame
Podemos cambiar la ruta absoluta de los iconos del frame por rutas relativas, de manera que los iconoslos guardemos en una carpeta de recursos en el directorio en donde se encuentre nuestro fichero ejecutable Python. Nos aseguramos de esta manera que los iconos se encuentren siempre. Así, por ejemplo, para la siguiente línea:

self.barra_herramientas.AddLabelTool(wx.NewId(), "nuevo", wx.Bitmap("d:\\python\\iconos\\on\\filenew.gif", wx.BITMAP_TYPE_ANY), wx.NullBitmap, wx.ITEM_NORMAL, "Nuevo", "Ayuda larga nuevo")

La ruta absoluta del icono filenew.gif la podemos calcular de la siguiente forma:

os.path.realpath("iconos\\on\\filenew.gif")

También podemos hacer:

ruta_aux = "iconos\\"
os.path.realpath(aux_ruta+"on\\filenew.gif")


NOTA: Cuidado si estás programando IDLE y wxPython. No son nada compatibles. Te aconsejo que si utilizas wxPython no escribas nada con IDLE. Resultados inesperados, y cosas muy desagradables, como que se te cierre IDLE inesperadamente sin guardar cambios. Lo digo por experiencia. Yo, en mi caso, voy a utilizar como editor Gedit. Es un editor muy ligero, del proyecto GNOME, para Windows. Reconoce código Python, pintándolo con colores. No lleva completitud. La indentación de código la podemos hacer con el tabulador, que es configurable en espacios.

En esta primera parte tenemos toda la implementación de la vista del programa.

Segunda parte. Se supone que tenemos instalado un servidor MySQL ó el acceso a una máquina con dicho servicio activo.

Lo primero es crear la base de datos:

CREATE DATABASE `cliente_python` ;
Creamos una tabla clientes, de siguiente modo:
CREATE TABLE `cliente` (
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`nombre` VARCHAR( 50 ) NOT NULL ,
`apellidos` VARCHAR( 50 ) NOT NULL ,
`nif` VARCHAR( 9 ) NOT NULL ,
`email` VARCHAR( 50 ) NOT NULL ,
`web` VARCHAR( 50 ) NOT NULL ,
`observ` VARCHAR( 100 ) NOT NULL
) ENGINE = innodb COMMENT = 'Tablas de clientes';

Ya tenemos la parte de la base de datos. ¿Qué mas necesitamos? El intermediario entre la base de datos y Python. Para ello utilizamos mySQLdb. ¿Cómo instalar mySQLdb en Windows? Para esta plataforma hay un ejecutable que se puede descargar de http://sourceforge.net/projects/mysql-python/files/. En mi caso, que tengo Python 2.5, así que me he bajado el fichero MySQL-python-1.2.2.win32-py2.5.exe.

Una vez instalado, en el intérprete de Python ejecutamos import MySQLdb para ver si efectivamente se ha instalado de forma correcta.

Finalmente, creamos el fichero cliente.py, que será el ejecutable de nuestra aplicación. En él se hereda el frame que hemos descrito anteriormente. Leyendo el código no se debe de tener problemas para su entendimiento. No he implementado ciertas opciones, como la búsqueda, por falta de tiempo. Dejo al lector su desarrollo.

El contenido de cliente.py es el siguiente:

# -*- coding: cp1252 -*-
'''
# Progama de mantenimiento de clientes en una base de datos. Se utiliza wxPython
# para programar la interfaz gráfica y el motor MySQL para guardar datos.
# Plataforma: Windows
# Versión Python: 2.5.4
# Autor: Ángel Luis García García
# Blog: http://elviajedelnavegante.blogspot.com
'''

from cliente_frame import MyFrame
import MySQLdb
import wx

class frame_principal(MyFrame):
def __init__(self, *args, **kwds):
# Llamamos al constructor de la clase de la cual heredamos.
MyFrame.__init__(self, *args, **kwds)
# Creamos conexión con base de datos.
self.conexionMySQL = MySQLdb.connect("localhost", "root", \
"1234","cliente_python")
# Creamos cursor.
self.cursorMySQL = self.conexionMySQL.cursor()
# Nº máximo de registros.
self.__maxfilasMySQL = 0
# Cargamos registros.
self.cargarRegistro(self.cargarFilas())
# ASociamos el salir de la aplicación con su manejador de eventos.
self.Bind(wx.EVT_CLOSE, self.Onsalir)

def Onrecargar(self, event):
self.cargarFilas()

def Onnuevo(self, event): # wxGlade: MyFrame.
'''
Nuevo cliente.
'''
self.limpiarRegistros()

def Onguardar(self, event): # wxGlade: MyFrame.
ret = self.datosRegistro()
'''
Guardamos cliente.
'''
# Primero comprobamos si se trata de una edición. Si existe ID, se está editando, por lo que hay que actualizar.
id_aux = str(ret[0])
if len(id_aux) != 0:
# Actualizamos cliente.
try:
cadenaSQL = "UPDATE `cliente_python`.`cliente` SET `nombre` = %s, `apellidos` = %s, `nif` = %s, \
`email` = %s, `web` = %s, `observ` = %s WHERE `cliente`.`id` = %s ;"
self.cursorMySQL.execute(cadenaSQL, (ret[1], ret[2], ret[3], ret[4], ret[5], ret[6], ret[0]))
self.conexionMySQL.commit()
self.infoBarraEstado('Se actualizó el cliente actual.')
self.cargarFilas()
except:
self.conexionMySQL.rollback()
self.infoBarraEstado('No se pudo actualizar el cliente actual.')
else:
# Insertamos nuevo cliente.
try:
cadenaSQL = "INSERT INTO `cliente_python`.`cliente` (`id` ,`nombre` ," + \
"`apellidos` ,`nif` ,`email` ,`web` ,`observ`) VALUES (" + \
"NULL , %s, %s, %s, %s, %s, %s);"
self.cursorMySQL.execute(cadenaSQL, (ret[1], ret[2], ret[3], ret[4], ret[5], ret[6]))
self.conexionMySQL.commit()
self.infoBarraEstado('Se guardó el nuevo cliente.')
self.cargarFilas()
except:
self.conexionMySQL.rollback()
self.infoBarraEstado('No se pudo guardar al nuevo cliente')

def Oncancelar(self, event): # wxGlade: MyFrame.
self.limpiarRegistros()

def Oneditar(self, event): # wxGlade: MyFrame.
'''
Edición
'''
print "Sin implementar"

def Onbuscar(self, event): # wxGlade: MyFrame.
'''
Búsqueda de registros.
'''
print "Sin implementar"


def Oneliminar(self, event): # wxGlade: MyFrame.
'''
Eliminar el cliente actual.
'''
# Primero se comprueba si hay ID.
ret = self.datosRegistro()
aux = str(ret[0])
if len(aux) == 0:
self.infoBarraEstado('Para eliminar un cliente hay que cargarlo primero.')
return -1
# Hay un cliente.
ret = wx.MessageBox('¿Eliminar cliente?', "Atención", wx.YES_NO)
if ret == wx.NO:
self.infoBarraEstado('Se canceló la eliminación')
if ret == wx.YES:
# Borramos el cliente.
try:
cadenaSQL = 'DELETE FROM `cliente_python`.`cliente` WHERE `id` = %s'
self.cursorMySQL.execute(cadenaSQL, (aux))
self.conexionMySQL.commit()
self.infoBarraEstado('Se eliminó el cliente.')
self.cargarFilas()
except:
self.conexionMySQL.rollback()
self.infoBarraEstado('No se pudo eliminar el cliente')

def Onrecargar(self, event):
self.__maxfilasMySQL = self.cursorMySQL.execute("SELECT * FROM cliente")
self.cursorMySQL.scroll(0)
ret = self.cursorMySQL.fetchone()
self.cargarRegistro(ret)

def Onizda_final(self, event): # wxGlade: MyFrame.
'''
Primer elemento.
'''
self.cursorMySQL.scroll(0,'absolute')
ret = self.cursorMySQL.fetchone()
self.cargarRegistro(ret)

def Onizda_rapido(self, event): # wxGlade: MyFrame.
'''
Desplazamos elemento a la izquierda, de 3 en 3.
'''
try:
self.cursorMySQL.scroll(-5)
ret = self.cursorMySQL.fetchone()
except:
self.cursorMySQL.scroll(0,'absolute')
ret = self.cursorMySQL.fetchone()
self.cargarRegistro(ret)

def Onizda(self, event): # wxGlade: MyFrame.
'''
Desplazamos un elemento a la izquierda.
'''
try:
self.cursorMySQL.scroll(-2)
ret = self.cursorMySQL.fetchone()
except:
self.cursorMySQL.scroll(0,'absolute')
ret = self.cursorMySQL.fetchone()
self.cargarRegistro(ret)

def Ondrcha(self, event): # wxGlade: MyFrame.
'''
Desplazamos un elemento a la derecha
'''
try:
self.cursorMySQL.scroll(0)
ret = self.cursorMySQL.fetchone()
except:
self.cursorMySQL.scroll(self.__maxfilasMySQL - 1,'absolute')
ret = self.cursorMySQL.fetchone()
self.cargarRegistro(ret)

def Ondrcha_rapido(self, event): # wxGlade: MyFrame.
'''
Desplazamos un elemento a la derecha, de 3 en 3.
'''
try:
self.cursorMySQL.scroll(3)
ret = self.cursorMySQL.fetchone()
except:
self.cursorMySQL.scroll(self.__maxfilasMySQL - 1,'absolute')
ret = self.cursorMySQL.fetchone()
self.cargarRegistro(ret)


def Ondrcha_final(self, event): # wxGlade: MyFrame.
'''
Registro final.
'''
self.cursorMySQL.scroll(self.__maxfilasMySQL - 1,'absolute')
ret = self.cursorMySQL.fetchone()
self.cargarRegistro(ret)

def Onayuda(self, event): # wxGlade: MyFrame.
cadena = "Programa de mantenimiento de de clientes."+"\r\n"+\
"Se ha utilizado Python wxPython y MySQL."+"\r\n"+ \
"Escrito por Ángel Luis García García"+"\r\n"+ \
"http://elviajedelnavegante.blogspot.com"
wx.MessageBox(cadena, "Info")

def Onsalir(self, event): # wxGlade: MyFrame.
# Cerramos conexion
self.cursorMySQL.close()
self.conexionMySQL.close()
# Y nos vamos.
self.Destroy()

def cargarFilas(self):
self.__maxfilasMySQL = self.cursorMySQL.execute("SELECT * FROM cliente")
#self.cursorMySQL.scroll(0)
ret = self.cursorMySQL.fetchone()
return ret

def visualizarFila(self):
ret = self.cursorMySQL.fetchone()
return ret

def limpiarRegistros(self):
self.text_ctrl_id.Value = ""
self.text_ctrl_nombre.Value = ""
self.text_ctrl_apellidos.Value = ""
self.text_ctrl_nif.Value = ""
self.text_ctrl_email.Value = ""
self.text_ctrl_web.Value = ""
self.text_ctrl_observ.Value = ""

def datosRegistro(self):
ret = []
ret.append(self.text_ctrl_id.Value)
ret.append(self.text_ctrl_nombre.Value)
ret.append(self.text_ctrl_apellidos.Value)
ret.append(self.text_ctrl_nif.Value)
ret.append(self.text_ctrl_email.Value)
ret.append(self.text_ctrl_web.Value)
ret.append(self.text_ctrl_observ.Value)
return ret

def cargarRegistro(self, ret):
self.text_ctrl_id.Value = str(ret[0])
self.text_ctrl_nombre.Value = str(ret[1])
self.text_ctrl_apellidos.Value = str(ret[2])
self.text_ctrl_nif.Value = str(ret[3])
self.text_ctrl_email.Value = str(ret[4])
self.text_ctrl_web.Value = str(ret[5])
self.text_ctrl_observ.Value = str(ret[6])

def infoBarraEstado(self,texto):
barra_estado_fields = [texto]
for i in range(len(barra_estado_fields)):
self.barra_estado.SetStatusText(barra_estado_fields[i], i)




Espero que esto pueda resultaros de ayuda para introduciros de buen pie en el mundo de Python, wxPython, con MySQL.

Saludos.




domingo, 7 de marzo de 2010

Recursos documentales en la red

En Internet podemos encontrar toda una gama de recursos documentales y herramientas de trabajo para nuestros intereses. En este artículo presento los links que visito habitualmente, tanto para la formación, como para el desarrollo y aplicación de todo tipo de proyectos de informática, en el área de programación.

Decir que los sitios web que voy a enumerar son todos de una calidad excepcional, avalados a nivel académico universitario y en entorno empresarial de primer nivel.

Documentación
  • Proyecto LuCAS (LinUx en CAStellano). Servicio editorial para documentación libre en español. Lugar de referencia. Aquí se puede encontrar todo tipo de material, gratuito, sobre redes, lenguajes, programación, bases de datos, etc. Documentación de software libre en español.
  • dmoz. Directorio de sitios web relacionados con la informática del proyecto de directorio abierto. Aquí se puede encontrar lo mejor de la red clasificado por categorías.
  • La web del programador. Zona de recursos documentales, en donde se puede encontrar información, servicios para programadores, programas y multitud de foros para todo tipo de de temas (bases de datos, sistemas operativos, redes, lenguajes. etc).
Software

General
  • cdlibre.org. Recopilaciones de software libre. Página web, cuyo autor es el profesor Bartolomé Sintes Marco, en donde podemos encontrar cientos de aplicaciones de software libre. Este sitio ofrece, además de links de descarga, recopilaciones de imágenes de cd's y dvd's de software, para bajar directamente. Además, puede suscribirte para ver las últimas modificaciones ó actualizacinoes de software que se han realizado.
  • Sourceforge. Página de software libre por definición. Es el kit de recursos de software libre más grande que conozco. Aquí está prácticamente todo lo que quieras encontrar.
  • Softonic. Página web clásica en Internet para descarga de aplicaciones de toda índole, esto es, freeware, sharewhare; Linux, Mac, Windows, etc.
Específico

Python
PowerBuilder
  • pbdr.com. Sitio de recursos gratuitos para PowerBuilder.
Sistemas operativos
  • Ubuntu. Sitio web oficial del sistema operativo Ubuntu.
  • Microsoft. . Página web de Microsoft. Todo tipo de complementos para la gama de productos Windows.
Información general sobre informática
  • BULMA. Usuarios de Linux de las Islas Baleares.
Miscelánea
  • INSHT. Instituto Nacional de seguridad e higiene en el trabajo. España.
  • RAE. Real Academia Española de la Lengua. El diccionario de castellano oficial.

viernes, 5 de marzo de 2010

Información meteorológica en Python, ¡hoy hará sol!

Hola a todos (si es que alguien me lee, y si lo hace, por favor, algún comentario, ¡¡¡para darme ánimos a seguir escribiendo!!!). Esta vez vamos a crear un programita para obtener información meteorológica en España. y esta vez lo vamos a hacer con Python, ¡que hacía tiempo que no hablaba de este lenguaje de programación!.

Esta cuestión la voy a explicar desde cero, para que nadie se pierda y podamos todos seguirlo paso a paso. El módulo que nos da acceso a la información metereológica no tiene nada de particular, es más, es muy fácil, pero me gustaría hacer una aplicación, aunque sea en modo texto, sencilla, para que el lector (mucho más listo y con gran destreza, ¡y no lo digo en broma!) pueda mejorarlo, implementándolo en wxPython, ó en una página web.

Empezamos...

Lo primero de todo es cómo vamos a obtener información del tiempo (clima) y de donde vamos a obtenerla. Para ello nos ayuda el módulo pymetar.py, escrito por el señor Tobias Klausmann.
PyMETAR obtiene informes del tiempo a partir de un identificador de estación, decodifica el informe (report) y da, mediante una serie de métodos, un fácil acceso a todos los datos del informe.

La librería la podemos descargar desde http://schwarzvogel.de/software-pymetar.shtml. El fichero que me he descargado es pymetar-0.16.tar. Lo descomprimes en el directorio donde tengas instalado Python (en mi caso Python25). Te aparece un directorio, llamado pymetar-0.16. Si has llegado hasta aquí, estamos a punto de terminar la instalación. A ver, esto que voy a hacer es para plataformas Windows, no para Linux. Quiero instalar el módulo, y para eso, hay que cambiar en el setup.py que viene en el directorio una cosa, porque de lo contrario la instalación no se hará bien (podría hacerse a capón, pero las cosas, si se hacen bien, mejor). Darse cuenta que este módulo es para instalarlo tal cual en Linux.

En este fichero hay que cambiar , en data_files, la ruta en donde se quiere que se instale la documentación. En mi caso, yo tengo instalado Python en d:\python25, por lo tanto quedará tal como sigue (dependiendo de donde tengas instalado Python tendrás que poner una ruta u otra. Ten en cuenta las barras, que en Linux es / y en Windows es \\).

Ejecutamos python setup.py install en el CMD, y nuestro módulo PyMETAR se queda completamente instalado. Para ver que efectivamente se ha instalado todo puedes probar en el intérprete de Python, haciendo un import pymetar, y para ver si se ha generado la documentación correctamente, en el Module Docs de Python:

PyMETAR accede al servicio nacional de tiempo climático de Estados Unidos (NOAA's National Weather Service). Los datos meterológicos de esta página se nutren de localizaciones específicas. Dichas localizaciones son zonas de observación, estaciones de observación o simplemente estaciones. Estos sitios se identifican por un identificador numérico y/o un indicador de localización. Por tanto, lo que vamos a hacer a continuación es obtener estos identificadores de estaciones, en España, para poder obtener a partir de ellos la información del tiempo, yéndonos a esta página: http://www.nws.noaa.gov/tg/siteloc.shtml.


Si estamos en Estados Unidos, buscamos por "Display All Stations In a State." Yo, que estoy en Murcia, me voy más abajo, a buscar España.

Hacemos click en "Display all stations in" (Mostrar todas las estaciones en). Seleccionamos todos los ID (selección y copy):

Abrimos un fichero de texto con Notepad (o el que mas os guste), y le dais el nombre que querais (yo le he llamado id_zonas.txt), y pegáis todos los id.

Bien, ya tenemos una parte importante, esto es, todos los identificadores de estaciones repartidos por España. ¿Fácil, no? Esto no es Python, pero ¡hay que hacer de todo!. Decir que el fichero que hemos generado lo guardamos en la misma carpeta donde vamos a poner los ficheros con el código fuente.

Una vez tenemos descargado e instalado el PyMETAR y un fichero con todos los identificadores de estaciones de España, ¡vamos a programar!.

Según la documentacon de PyMETAR tenemos 3 clases principales ReportFetcher, ReportParser y WeatherReport. La primera clase (ReportFetcher) sirve para obener un informe "crudo", esto es, sin formatear, de los datos metereológicos (toda la información). La segunda clase (ReportParser) (utilizando la tercera [WeatherReport]) da formato a los datos del informe sin formatear, devolviendo los datos formateados. Una vez instanciada la clase, mediante los método apropiados podremos recuperar la información ya formateada. Es por ello que si queremos recuperar información tenemos que utilizar los método de WeatherReport, ya que esta clase se utiliza en ReportParser. Esto es lo más complicado que hay que entender. Lo demás lo vemos en el ejemplo.

Vamos a crear una aplicación, compuesta por 2 ficheros, tiempo.py y tiempo_auxiliar.py. El primer fichero será el ejecutable, el segundo el "cajón de sastre" de nuestra aplicación. ¡Hay que ser ordenados!

Veamos primero tiempo_auxiliar.py, que es en donde están las funciones principales.

Cabecera de toda la vida, y función para visualizar una los datos del tiempo que se exportan a una página web (aunque no he llegado a llamar a esta función en el programa, ¡tiene que funcionar a la fuerza!)

Función estrella de la aplicación. Aquí vemos como obtener datos del servidor del tiempo climático. Como puede apreciarse en el código no es difícil de implementar. (Quizás si hubiera creado un post sólo con esta función me hubiera ahorrado mucha escritura, pero es que me gusta explicar como me gustaría que a mi me explicasen).

Función para crear un fichero html a partir de los datos climáticos. Esta es la manera más rápida que he encontrado (si hay otra, por favor, comunícamelo). Y la función de continuar.

La función que carga el fichero con los identificadores de posicionamiento de la estaciones. Si lees el código no debes de tener problemas. Este tipo de función ya la he usado en algún que otro post.

Y finalmente, la función del menú principal.

Y el módulo que hace que "esto" funcione, tiempo.py, te lo muestro tal que así:

Como se puede observar, no es muy complicado (Python no lo es, por eso me gusta tanto) la creación de la aplicación. Veamos los resultados. Ejecutamos python tiempo.py.

Pulsamos 1, y Enter.

Y nos va mostrando, de 10 en 10, los posicionamientos. Pulsamos cero y se para.

Vamos a ver la temperatura que hace en Córdoba. Vamos a la opción 2, y escribimos el ID de Córdoba, tal como aparece en el resultado de la opción 1.

Como se puede observar, vemos los resultados que nos envía el servidor (temperatura, humedad, etc.) Esto es un ejemplo, hay mucha más información.

¡¡¡¡Vamos a llevar estos datos a la WEB!!!! Opción 3.

Esta vez, he escogido Barajas (Madrid), para ver el tiempo que hace.

Y por supuesto, San Javier, Murcia, mi tierra.

CONCLUSIONES

En este post hemos visto varias cosas. Primero, poder crear una aplicación (o prototipo) de recuperación de información, esta vez, meteorológica. PyMETAR nos ofrece una herramienta de acceso a información de tiempo climático sencilla y fácil de utilizar. Segundo, llevar esos datos a un fichero html para poder ser publicado en una web. Y último, la utilización del tipo de dato lista para poder manejar datos que vienen de ficheros externos, formateando su información como más nos convenga.