# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
from collections import deque
from django.utils.encoding import force_text
from lxml import etree
from filemaker.exceptions import FileMakerServerError
[docs]class FMDocument(dict):
'''
A dictionary subclass for containing a FileMaker result whose keys can
be accessed as attributes.
'''
def __getattr__(self, name):
return self.get(name)
def __setattr__(self, name, value):
self[name] = value
class XMLNode(object):
def __init__(self, name, attrs):
self.name = name.replace(
'{http://www.filemaker.com/xml/fmresultset}', '')
self.attrs = attrs
self.text = ''
self.children = []
def __getitem__(self, key):
return self.attrs.get(key)
def __setitem__(self, key, value):
self.attrs[key] = value
def __delitem__(self, key):
del self.attrs[key]
def add_child(self, element):
self.children.append(element)
def get_data(self):
return self.text.strip() if hasattr(self.text, 'strip') else ''
def get_elements(self, name=''):
if not name:
return self.children
elements = []
for element in self.children:
if element.name == name:
elements.append(element)
return elements
def get_element(self, name=''):
if not name:
return self.children[0]
for element in self.children:
if element.name == name:
return element
class FMXMLTarget(object):
def __init__(self):
# It shouldn't make much difference unless you have massive
# nested elements in a layout, but a deque should be faster than
# a list here
self.stack = deque()
self.root = None
def start(self, name, attrs):
element = XMLNode(name, attrs)
if self.stack:
parent = self.stack[-1]
parent.add_child(element)
else:
self.root = element
self.stack.append(element)
def end(self, name):
self.stack.pop()
def data(self, content):
content = force_text(content)
element = self.stack[-1]
element.text += content
def comment(self, text): # pragma: no cover
pass
def close(self):
root = self.root
self.stack = deque()
self.root = None
return root
[docs]class FMXMLObject(object):
'''
A python container container for results returned from a FileMaker request.
The following attributes are provided:
.. py:attribute:: data
Contains the raw XML data returned from filemaker.
.. py:attribute:: errorcode
Contains the ``errorcode`` returned from FileMaker. Note that if this
value is not zero when the data is parsed at instantiation, then a
:py:exc:`filemaker.exceptions.FileMakerServerError` will be raised.
.. py:attribute:: product
A dictionary containing the FileMaker product details returned from
the server.
.. py:attribute:: database
A dictionary containing the FileMaker database information returned
from the server.
.. py:attribute:: metadata
A dictionary containing any metadata returned by the FileMaker server.
.. py:attribute:: resultset
A list containing any results returned from the FileMaker server as
:py:class:`FMDocument` instances.
.. py:attribute:: field_names
A list of field names returned by the server.
.. py:attribute:: target
The target class used by lxml to parse the XML response from the
server. By default this is an instance of :py:class:`FMXMLTarget`, but
this can be overridden in subclasses.
'''
target = FMXMLTarget()
def __init__(self, data):
self.data = data
self.errorcode = -1
self.product = {}
self.database = {}
self.metadata = {}
self.resultset = []
self.field_names = []
self._parse_resultset()
def __getitem__(self, key):
return self.resultset[key]
def __len__(self):
return len(self.resultset)
def _parse_xml(self):
parser = etree.XMLParser(target=self.target)
try:
xml_obj = etree.XML(self.data, parser)
if xml_obj.get_elements('ERRORCODE'):
self.errorcode = \
int(xml_obj.get_elements('ERRORCODE')[0].get_data())
else:
self.errorcode = int(xml_obj.get_elements('error')[0]['code'])
except (KeyError, IndexError, TypeError,
ValueError, etree.XMLSyntaxError):
raise FileMakerServerError(954)
if self.errorcode == 401:
# Object not found on filemaker so return None which we pick
# up later as no objects having been found
return
if not self.errorcode == 0:
raise FileMakerServerError(self.errorcode)
return xml_obj
def _parse_resultset(self):
data = self._parse_xml()
if data is None:
self.resultset = []
return
self.product = data.get_element('product').attrs
self.database = data.get_element('datasource').attrs
definitions = data.get_element(
'metadata').get_elements('field-definition')
for definition in definitions:
self.metadata[definition['name']] = definition.attrs
self.field_names.append(definition['name'])
results = data.get_element('resultset')
for result in results.get_elements('record'):
record = FMDocument()
for column in result.get_elements('field'):
field_name = column['name']
column_data = None
if column.get_element('data'):
column_data = column.get_element('data').get_data()
if '::' in field_name:
sub_field, sub_name = field_name.split('::', 1)
if not sub_field in record:
record[sub_field] = FMDocument()
record[sub_field][sub_name] = column_data
else:
record[field_name] = column_data
record['RECORDID'] = int(result['record-id'])
record['MODID'] = int(result['mod-id'])
for sub_node in result.get_elements('relatedset'):
sub_node_name = sub_node['table']
try:
cnt = int(sub_node['count'])
except (TypeError, ValueError):
cnt = 0
if cnt > 0:
record[sub_node_name] = []
for sub_result in sub_node.get_elements('record'):
sub_record = FMDocument()
for sub_column in sub_result.get_elements('field'):
field_name = re.sub(
r'^{0}::'.format(sub_node_name), '',
sub_column['name'])
if not sub_column.get_element('data'):
continue
column_data = sub_column.get_element('data').get_data()
if '::' in field_name:
sub_field, sub_name = field_name.split('::', 1)
if not sub_field in sub_record:
sub_record[sub_field] = FMDocument()
sub_record[sub_field][sub_name] = column_data
else:
sub_record[field_name] = column_data
sub_record['RECORDID'] = int(sub_result['record-id'])
sub_record['MODID'] = int(sub_result['mod-id'])
record[sub_node_name].append(sub_record)
self.resultset.append(record)