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': , '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:
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 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.
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__': , '_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!
Bacana o Post quando vai vir o próximo?