sábado, 29 de septiembre de 2012

Migración de datos entre distintas instancias de OpenERP usando OpenETL

En mi anterior post (o aqui) realicé una introducción a OpenETL. También desarrollé un ejemplo de carga de datos desde una archivo .csv a OpenERP.
En este post voy a profundizar un poco más, realizando una migración de datos de OpenERP a OpenERP en los cuales hay tablas relacionadas.

El escenario

Nuestro entorno de migración constará de las siguientes características:
  • BD_inicial contiene los datos que queremos migrar. Los datos serán los clientes y proveedores con sus direcciones.
  • No todos los clientes o proveedores tienen una dirección asociada, por lo que hay que controlar la excepción.
  • Para simplificar el ejemplo voy a migrar sólo el contenido de los campos name, title y partner_id, siendo title el campo que contiene la relación con la tabla res_partner_title y partner_id el campo relacionado con la tabla res_partner.
Relación simplificada de relaciones de objetos res_partner_address, res_partner_title y res_partner.

A tener en cuenta

Cuando migras un contenido desde OpenERP a un archivo .csv el sistema suele funcionar sin complicaciones. Sin embargo cuando se realiza la migración de OpenERP a OpenERP es fácil obtener excepciones tal como

File "/usr/lib/python2.6/xmlrpclib.py", line 838, in close
    raise Fault(**self._stack[0])
xmlrpclib.Fault: 
Esto ocurre porque OpenERP al leer un campo sin valor le asigna por defecto un booleano inicializado a False.
En el ejemplo data_map.py después de leer los valores de ejemplo desde el .csv el autor realiza una transformación de los mismos antes de mostrarlos en el log. Basándose en ese ejemplo, es fácil inicializar los campos con valores adecuados:
def preprocess(self, channels):
    cdict = {}
    for trans in channels['modificacion']:
        for d in trans:            
            if d["name"] == False:
                d["name"] = ""
            cdict[d['id']] = d
    return {'resultado':cdict}  
Otra cosa que también puede producir muchos quebraderos de cabeza es que en los campos relacionados no se va a poner el id de la tupla relacionada, sino el valor de la misma. Nuestra función quedaría así:
def preprocess(self, channels):
    cdict = {}
    for trans in channels['modificacion']:
        for d in trans:
            if d["title"] == False: # Es una relacion, ej: 'title': [5, 'Sir'] , con res_partner_title
                d["title"] = '' # Si quiero dejarlo sin valor, le dejo las comillas
            else:
                d["title"] = d["title"][1]  # No se coge el 0, que es el id, sino el valor. El id se ajusta automatico :)
            if d["name"] == False:
                d["name"] = ""
                
            if d["partner_id"] == False:
                d["partner_id"] = ""
            else:    
                d["partner_id"] = d["partner_id"][1]
                            
            cdict[d['id']] = d
    return {'resultado':cdict}     

El resultado

El código completo, con conectores, componentes, transiciones, etc. se muestra a continuación. Nótese que la función de procesamiento es llamada desde una transición openetl.component.transform.map(map_keys,preprocess), en la que se pasa también un parámetro map. Hay más ejemplos parecidos en data_map.py y m2m_into_oo.py.
#!/usr/bin/python

import sys
sys.path.append('..')

import openetl
from openetl import transformer

# Conectores
ooconnector_in = openetl.connector.openobject_connector('http://localhost:8069', 'BD_inicial', 'admin', 'admin', con_type='xmlrpc')
ooconnector_out = openetl.connector.openobject_connector('http://localhost:8069', 'BD_final', 'admin', 'admin', con_type='xmlrpc')

# Componentes
openobject_in1 = openetl.component.input.openobject_in(
                 ooconnector_in,'res.partner.address',
                 fields=['id','title','name','partner_id'],
                 )


openobject_in2 = openetl.component.input.openobject_in(
                 ooconnector_in,'res.partner',
                 fields=['id','name'],
                 )



openobject_out1 = openetl.component.output.openobject_out(
     ooconnector_out,
     'res.partner.address',
     {'name':'name','title':'title','partner_id':'partner_id'}
    )

openobject_out2 = openetl.component.output.openobject_out(
     ooconnector_out,
     'res.partner',
     {'name':'name'}
    )

log=openetl.component.transform.logger(name='Recien leido:Read Partner File ')

# Soporte transformaciones

map_keys = {'main': {
    'name': "resultado[main['id']]['name']",
    'title': "resultado[main['id']]['title']",
    'partner_id': "resultado[main['id']]['partner_id']",
}}



def preprocess(self, channels):
    cdict = {}
    for trans in channels['modificacion']:
        for d in trans:
            if d["title"] == False: # Es una relacion, ej: 'title': [5, 'Sir'] , con res_partner_title
                d["title"] = '' # Si quiero dejarlo sin valor, le dejo las comillas
            else:
                d["title"] = d["title"][1]  # No se coge el 0, que es el id, sino el valor. El id se ajusta automatico :)
            if d["name"] == False:
                d["name"] = ""
                
            if d["partner_id"] == False:
                d["partner_id"] = ""
            else:    
                d["partner_id"] = d["partner_id"][1]
                            
            cdict[d['id']] = d
    return {'resultado':cdict}               

map=openetl.component.transform.map(map_keys,preprocess)


# Transiciones
tran1=openetl.transition(openobject_in1,map, channel_destination='modificacion')
tran3=openetl.transition(openobject_in1,log)

tran_res_partner01=openetl.transition(openobject_in2, openobject_out2)

tran4=openetl.transition(openobject_in1, map)
tran4=openetl.transition(map, openobject_out1)


# Definicion de trabajo y ejecucion
job1=openetl.job([openobject_in1,map,openobject_out1,openobject_in2,openobject_out2])
job1.run()

Este código funciona y realiza la migración de datos sin ningún problema siempre que en las tablas relacionadas no haya ningún dato con igual campo “valor relacionado” repetido. ¿Y qué pasa si el “valor relacionado” sí está repetido? Lo que ocurre en este caso es que el sistema creará la relación con la tupla con id más pequeño. Para corregir esta situación bastaría con añadir alguna condición más a la función preprocess, ayudarnos de alguna otra función en python auxiliar, etc. Si se diera ese caso los ejemplos sql_in_example.py, csv_diff_example.py, join_example.py, podrían servir como base en función del tratamiento que quisiéramos hacer.



miércoles, 19 de septiembre de 2012

Uso de Yield en Python

Se usa para funciones generadoras. Funciona como un return, pero la próxima vez que se llame el código continúa justo debajo de yield.

Código de ejemplo:

 def contador_yield(max2):
     print "Inicio contador_yield"
     n = 0
     while (n < max2):
         print "Deltro del while de contador_yield"
         yield n
         n +=1    
         print "Sigo dentro del metodo"
         print "Sigo dentro del metodo22"
         print "Sigo dentro del metodo233"
         
 c = contador_yield(3)

 print c.next()
 print "Ya he realizado la llamada"
 print c.next()
 print c.next()
 print c.next()

Salida:

Inicio contador_yield
 Deltro del while de contador_yield
 0
 Ya he realizado la llamada
 Sigo dentro del metodo
 Sigo dentro del metodo22
 Sigo dentro del metodo233
 Deltro del while de contador_yield
 1
 Sigo dentro del metodo
 Sigo dentro del metodo22
 Sigo dentro del metodo233
 Deltro del while de contador_yield
 2
 Sigo dentro del metodo
 Sigo dentro del metodo22
 Sigo dentro del metodo233
 Traceback (most recent call last):
   File "/home/carlos/workspace/test_yield.py", line 19, in 
     print c.next()
 StopIteration

Documentación:

Blog Python Manía. Uso de yield

Python Doc#yield

Related Posts Plugin for WordPress, Blogger...