Extraindo dados do ZODB

0 Flares Twitter 0 Facebook 0 Filament.io 0 Flares ×

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.

1
2
3
4
5
6
7
8
9
10
11
#!/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:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/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:

1
['__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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{'Control_Panel': <persistent broken App.ApplicationManager.ApplicationManager instance '\x00\x00\x00\x00\x00\x00\x00\x03'>,
 'Plone': <persistent broken Products.CMFPlone.Portal.PloneSite instance '\x00\x00\x00\x00\x00\x00\x00\x15'>,
 '__allow_groups__': <persistent broken Products.PluggableAuthService.PluggableAuthService.PluggableAuthService instance '\x00\x00\x00\x00\x00\x00\x00\x14'>,
 '__before_publishing_traverse__': <broken ZPublisher.BeforeTraverse.MultiHook instance>,
 '__before_traverse__': {(25, 'Virtual Host Monster'): <broken ZPublisher.BeforeTraverse.NameCaller instance>,
                         (50, 'SessionDataManager'): <persistent broken Products.Sessions.SessionDataManager.SessionDataManagerTraverser instance '\x00\x00\x00\x00\x00\x00\x00\x07'>,
                         (99, 'Pluggable Auth Service/acl_users'): <broken ZPublisher.BeforeTraverse.NameCaller instance>},
 '__error_log__': <persistent broken Products.SiteErrorLog.SiteErrorLog.SiteErrorLog instance '\x00\x00\x00\x00\x00\x00\x00\x0b'>,
 '_initializer_registry': {'browser_id_manager': 1,
                           'error_log': 1,
                           'session_data_manager': 1,
                           'temp_folder': 1,
                           'virtual_hosting': 1},
 '_key_manager': <broken plone.keyring.keymanager.KeyManager instance>,
 '_mount_points': {'temp_folder': <persistent broken Products.ZODBMountPoint.MountedObject.MountedObject instance '\x00\x00\x00\x00\x00\x00\x00\x06'>},
 '_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': <persistent broken Products.PluggableAuthService.PluggableAuthService.PluggableAuthService instance '\x00\x00\x00\x00\x00\x00\x00\x14'>,
 'browser_id_manager': <persistent broken Products.Sessions.BrowserIdManager.BrowserIdManager instance '\x00\x00\x00\x00\x00\x00\x00\t'>,
 'error_log': <persistent broken Products.SiteErrorLog.SiteErrorLog.SiteErrorLog instance '\x00\x00\x00\x00\x00\x00\x00\x0b'>,
 'favicon.ico': <persistent broken OFS.Image.Image instance '\x00\x00\x00\x00\x00\x00\x00\x0c'>,
 'index_html': <persistent broken Products.PageTemplates.ZopePageTemplate.ZopePageTemplate instance '\x00\x00\x00\x00\x00\x00\x00\r'>,
 'session_data_manager': <persistent broken Products.Sessions.SessionDataManager.SessionDataManager instance '\x00\x00\x00\x00\x00\x00\x00\x08'>,
 'standard_error_message': <persistent broken OFS.DTMLMethod.DTMLMethod instance '\x00\x00\x00\x00\x00\x00\x00\x0e'>,
 'temp_folder': <persistent broken Products.ZODBMountPoint.MountedObject.MountedObject instance '\x00\x00\x00\x00\x00\x00\x00\x06'>,
 'virtual_hosting': <persistent broken Products.SiteAccess.VirtualHostMonster.VirtualHostMonster instance '\x00\x00\x00\x00\x00\x00\x00\x0f'>}

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
No handlers could be found for logger "ZODB.Connection"
Traceback (most recent call last):
  File "alisson.py", line 45, in <module>
    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", <function Provides at 0x7f4790db0d70>, (<class 'plone.app.contenttypes.content.Document'>, <class 'Products.CMFEditions.interfaces.IVersioned'>))

Esse era o script executado.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{'_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__': <zope.interface.Provides object at 0x7f7b101913d0>,
 '_plone.uuid': 'c4a47a4286be4856a0388271c3505176',
 'cmf_uid': <persistent broken Products.CMFUid.UniqueIdAnnotationTool.UniqueIdAnnotation instance '\x00\x00\x00\x00\x00\x00\x17\x99'>,
 'creation_date': <broken DateTime.DateTime.DateTime instance>,
 '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': <broken DateTime.DateTime.DateTime instance>,
 'portal_type': 'Document',
 'relatedItems': [],
 'rights': None,
 'table_of_contents': False,
 'text': <broken plone.app.textfield.value.RichTextValue instance>,
 'title': u'Cursos e Cartilhas',
 'version_id': 0,
 'workflow_history': <persistent broken Persistence.mapping.PersistentMapping instance '\x00\x00\x00\x00\x00\x00\x17\x8f'>}

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!

0 Flares Twitter 0 Facebook 0 Filament.io 0 Flares ×
1 ano ago

1 Comment

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *