Alisson Machado
07 July 2016

Extraindo dados do ZODB

Eae Galera! Essa semana tive um desafio de fazer uma migração de um ambiente de ensino a distância que estava todo em Plone para o Moodle. O primeiro desafio que encontrei foi como buscar os dados na base do Plone, uma vez que ele usa o ZODB para a persistências de dados. O ZODB é um banco de dados orientado a objetos, então dentro dele não temos como fazer Select, ou fazer um find() com é o caso do MongoDB. Estudei um pouco sozinho sobre Zope, Plone e ZODB, até consegui fazer umas coisas, mas o que me salvou mesmo foi o Github, mais especificamente esse projeto: https://github.com/davisagli/eye Esse projeto pega um arquivo de banco de dados, normalmente dentro do diretório do Plone ele é encontrado como Data.fs . Mas só visualizar não era necessário, eu precisava extrair esses dados para criar um robo que iria efetuar a migração. Então vou mostrar o meu script como era inicialmente e o que eu precisei extrair para poder fazer funcionar. Para conectar em servidores remotos do ZOPE é preciso utilizar uma biblioteca do Python chamada ZEO, e dentro dela importar um módulo chamado ClientStorage.
#!/usr/bin/python

from ZEO.ClientStorage import ClientStorage
from ZODB import DB
storage = ClientStorage(('127.0.0.1',8100))

db = DB(storage)
connection = db.open()

root = connection.root()
app = root['Application']

No script acima, foi feita a conexão com o ZeoServer, aberto o banco e retornado o principal objeto que é o Application. A partir dele vamos conseguir buscar todos os dados. Para descobrir o que tem dentro desse objeto, pode ser utilizado a função dir() do Python, ela vai retornar todos os métodos existentes dentro de um objeto. Então abaixo continuando o script:
#!/usr/bin/python

from ZEO.ClientStorage import ClientStorage
from ZODB import DB
storage = ClientStorage(('127.0.0.1',8100))

db = DB(storage)
connection = db.open()

root = connection.root()
app = root['Application']

print dir(app)

A saída retornada será parecida com essa:
['__Broken_Persistent__', '__Broken_initargs__', '__Broken_newargs__', '__Broken_state__', '__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__getnewargs__', '__getstate__', '__hash__', '__implemented__', '__init__', '__module__', '__name__', '__new__', '__providedBy__', '__provides__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_p_activate', '_p_changed', '_p_deactivate', '_p_delattr', '_p_estimated_size', '_p_getattr', '_p_invalidate', '_p_jar', '_p_mtime', '_p_oid', '_p_serial', '_p_setattr', '_p_state', '_p_status', '_p_sticky']
O método que precisamos buscar é o __Broken_state__. Mas o quê ele faz? Esse método guarda o último estado de um objeto no ZODB, ou seja, ele vai ter todos os valores do atributos de um objeto da última vez em que ele foi modificado. Então eu posso fazer a seguinte maneira:
#!/usr/bin/python

from ZEO.ClientStorage import ClientStorage
from ZODB import DB
import pprint

storage = ClientStorage(('127.0.0.1',8100))

db = DB(storage)
connection = db.open()

root = connection.root()
app = root['Application']

pprint.pprint(app.__Broken_state__)

O pprint foi utilizado para uma saída mais amigável na tela. A saída será parecida com essa:
{'Control_Panel': ,
 'Plone': ,
 '__allow_groups__': ,
 '__before_publishing_traverse__': ,
 '__before_traverse__': {(25, 'Virtual Host Monster'): ,
                         (50, 'SessionDataManager'): ,
                         (99, 'Pluggable Auth Service/acl_users'): },
 '__error_log__': ,
 '_initializer_registry': {'browser_id_manager': 1,
                           'error_log': 1,
                           'session_data_manager': 1,
                           'temp_folder': 1,
                           'virtual_hosting': 1},
 '_key_manager': ,
 '_mount_points': {'temp_folder': },
 '_objects': ({'id': 'Control_Panel', 'meta_type': 'Control Panel'},
              {'id': 'temp_folder', 'meta_type': 'Temporary Folder'},
              {'id': 'session_data_manager',
               'meta_type': 'Session Data Manager'},
              {'id': 'browser_id_manager', 'meta_type': 'Browser Id Manager'},
              {'id': 'error_log', 'meta_type': 'Site Error Log'},
              {'id': 'standard_error_message', 'meta_type': 'DTML Method'},
              {'id': 'favicon.ico', 'meta_type': 'Image'},
              {'id': 'index_html', 'meta_type': 'Page Template'},
              {'id': 'virtual_hosting', 'meta_type': 'Virtual Host Monster'},
              {'id': 'Plone', 'meta_type': 'Plone Site'},
              {'id': 'acl_users', 'meta_type': 'Pluggable Auth Service'}),
 '_standard_objects_have_been_added': 1,
 '_upgraded_acl_users': 1,
 'acl_users': ,
 'browser_id_manager': ,
 'error_log': ,
 'favicon.ico': ,
 'index_html': ,
 'session_data_manager': ,
 'standard_error_message': ,
 'temp_folder': ,
 'virtual_hosting': }

Como foi visto acima, é retornado um dicionário com todos o último estado desse objeto. O importante para nós é o valor da chave Plone, para retornar esse objeto em específico, é necessário buscar essa chave no dicionário. Assim o script fica da seguinte maneira:
#!/usr/bin/python

from ZEO.ClientStorage import ClientStorage
from ZODB import DB
import pprint

storage = ClientStorage(('127.0.0.1',8100))

db = DB(storage)
connection = db.open()

root = connection.root()
app = root['Application']

pprint.pprint(app.__Broken_state__.get("Plone").__Broken_state__)

Agora o caminho ficou mais fácil, então sabendo que o método __Broken_state__ traz o último estado de um objeto, é só ir buscando a chave no dicionário e depois fazer um __Broken_state__ para trazer os dados. Mas isso infelizmente não vai servir para todos os objetos, por exemplo no caso de um documento do Plone, que é basicamente uma página e o seu conteúdo. Na minha instalação do Plone, eu criei basicamente 3 páginas, sendo elas:
  • Cursos e Cartilhas
  • Quem Somos
  • Parcerias
Que eram basicamente as páginas que o cliente tinha, mas na hora de verifica o último estado desses objetos ocorria um erro:
No handlers could be found for logger "ZODB.Connection"
Traceback (most recent call last):
  File "alisson.py", line 45, in 
    pprint.pprint(app.__Broken_state__.get("Plone").__Broken_state__.get("cursos-e-cartilhas-1").__Broken_state__)
  File "/usr/local/lib/python2.7/dist-packages/ZODB/Connection.py", line 901, in setstate
    self._setstate(obj, oid)
  File "/usr/local/lib/python2.7/dist-packages/ZODB/Connection.py", line 958, in _setstate
    self._reader.setGhostState(obj, p)
  File "/usr/local/lib/python2.7/dist-packages/ZODB/serialize.py", line 622, in setGhostState
    state = self.getState(pickle)
  File "/usr/local/lib/python2.7/dist-packages/ZODB/serialize.py", line 615, in getState
    return unpickler.load()
  File "/usr/local/lib/python2.7/dist-packages/zope/interface/declarations.py", line 488, in Provides
    spec = ProvidesClass(*interfaces)
  File "/usr/local/lib/python2.7/dist-packages/zope/interface/declarations.py", line 456, in __init__
    Declaration.__init__(self, *(interfaces + (implementedBy(cls), )))
  File "/usr/local/lib/python2.7/dist-packages/zope/interface/declarations.py", line 65, in __init__
    Specification.__init__(self, _normalizeargs(interfaces))
  File "/usr/local/lib/python2.7/dist-packages/zope/interface/declarations.py", line 840, in _normalizeargs
    _normalizeargs(v, output)
  File "/usr/local/lib/python2.7/dist-packages/zope/interface/declarations.py", line 839, in _normalizeargs
    for v in sequence:
TypeError: ("'type' object is not iterable", , (<class 'plone.app.contenttypes.content.Document'>, <class 'Products.CMFEditions.interfaces.IVersioned'>))


Esse era o script executado.
#!/usr/bin/python

from ZEO.ClientStorage import ClientStorage
from ZODB import DB
import pprint

storage = ClientStorage(('127.0.0.1',8100))

db = DB(storage)
connection = db.open()

root = connection.root()
app = root['Application']

pprint.pprint(app.__Broken_state__.get("Plone").__Broken_state__.get("cursos-e-cartilhas-1").__Broken_state__)

Mas por que isso acontecia? Por que o Python não conseguia entender os tipos de objetos específicos do Plone, então era necessária a conversão desses objetos para os tipos conhecidos do python, como Listas, Dicionários, Tuplas e assim por diante. Então comecei a analisar o código do projeto Eye, e vi que para ele criar a visualização desses objetos, era necessário fazer uma normalização dos dados. Essa normalização foi encontrada no arquivo patch.py dentro do projeto. Então basicamente e peguei o código dessa função e o adicionei no início do meu script. Abaixo segue o código da função:
def patched_normalizeargs(sequence, output = None):
    """Normalize declaration arguments

    Normalization arguments might contain Declarions, tuples, or single
    interfaces.

    Anything but individial interfaces or implements specs will be expanded.
    """
    if output is None:
        output = []

    if Broken in getattr(sequence, '__bases__', ()):
        return [sequence]

    cls = sequence.__class__
    if InterfaceClass in cls.__mro__ or zope.interface.declarations.Implements in cls.__mro__:
        output.append(sequence)
    else:
        for v in sequence:
            patched_normalizeargs(v, output)

    return output

zope.interface.declarations._normalizeargs = patched_normalizeargs
Isso automaticamente faria a conversão dos objetos do Plone para os tipos que eu precisava. Então a partir dai eu consegui utilizar o método __Broken_state__ para qualquer objeto que eu quisesse e ver os dados no formato de listas e dicionários. Então o meu script ficou da seguinte maneira:
from ZEO.ClientStorage import ClientStorage
from ZODB import DB
import zope.interface.declarations
from zope.interface.interface import InterfaceClass
from ZODB.broken import Broken
import pprint

def patched_normalizeargs(sequence, output = None):
    """Normalize declaration arguments

    Normalization arguments might contain Declarions, tuples, or single
    interfaces.

    Anything but individial interfaces or implements specs will be expanded.
    """
    if output is None:
        output = []

    if Broken in getattr(sequence, '__bases__', ()):
        return [sequence]

    cls = sequence.__class__
    if InterfaceClass in cls.__mro__ or zope.interface.declarations.Implements in cls.__mro__:
        output.append(sequence)
    else:
        for v in sequence:
            patched_normalizeargs(v, output)

    return output

zope.interface.declarations._normalizeargs = patched_normalizeargs

storage = ClientStorage(('127.0.0.1',8100))

db = DB(storage)
connection = db.open()

root = connection.root()
app = root['Application']

pprint.pprint(app.__Broken_state__.get("Plone").__Broken_state__.get("cursos-e-cartilhas-1").__Broken_state__)

E agora o erro sumiu e me retornou os dados no seguinte formato:
{'_Access_contents_information_Permission': ('Manager',
                                             'Owner',
                                             'Editor',
                                             'Reader',
                                             'Contributor',
                                             'Site Administrator'),
 '_Change_portal_events_Permission': ('Manager',
                                      'Owner',
                                      'Editor',
                                      'Site Administrator'),
 '_Modify_portal_content_Permission': ('Manager',
                                       'Owner',
                                       'Editor',
                                       'Site Administrator'),
 '_View_Permission': ('Manager',
                      'Owner',
                      'Editor',
                      'Reader',
                      'Contributor',
                      'Site Administrator'),
 '__ac_local_roles__': {'admin': ['Owner']},
 '__provides__': ,
 '_plone.uuid': 'c4a47a4286be4856a0388271c3505176',
 'cmf_uid': ,
 'creation_date': ,
 'creators': ('admin',),
 'description': u'Descri\xe7\xe3o dos cursos e cartilhas',
 'id': 'cursos-e-cartilhas-1',
 'language': u'pt-br',
 'location_id': 0,
 'modification_date': ,
 'portal_type': 'Document',
 'relatedItems': [],
 'rights': None,
 'table_of_contents': False,
 'text': ,
 'title': u'Cursos e Cartilhas',
 'version_id': 0,
 'workflow_history': }
E a partir dai foi só fazer alguns loopings de repetição e gravar em arquivos para automatizar a migração. Mas basicamente o caminho das pedras foi esse. Até mais!