oc-facho/facho/facho.py
bit4bit@riseup.net 95b79407a8 facho/cli.py: nuevo comando generate-invoice, el permite crear imprimir una factura xml desde un script
FossilOrigin-Name: ee656a60b05a0efb5c1f4e3ea517e28e01d7cd6b95fc5074c9f7cbf70b16c1d6
2020-05-25 17:52:47 +00:00

206 lines
6.4 KiB
Python

# This file is part of facho. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from lxml import etree
from lxml.etree import Element, SubElement, tostring
import re
class FachoXMLExtension:
def build(self, fachoxml):
raise NotImplementedError
class LXMLBuilder:
"""
extrae la manipulacion de XML
"""
# TODO buscar el termino mas adecuado
# ya que son varios lo procesos que se
# exponen en la misma clase
# * creacion
# * busquedad
# * comparacion
def __init__(self, nsmap):
self.nsmap = nsmap
self._re_node_expr = re.compile(r'^(?P<path>((?P<ns>\w+):)?(?P<tag>[a-zA-Z0-9_-]+))(?P<attrs>\[.+\])?')
self._re_attrs = re.compile(r'(\w+)\s*=\s*\"?(\w+)\"?')
def match_expression(self, node_expr):
match = re.search(self._re_node_expr, node_expr)
return match.groupdict()
@classmethod
def from_string(cls, content, clean_namespaces=False):
if clean_namespaces:
content = re.sub(r'\<\s*[a-zA-Z\-0-9]+\s*:', '<', content)
content = re.sub(r'\<\/\s*[a-zA-Z\-0-9]+\s*:', '</', content)
return etree.fromstring(content)
@classmethod
def build_element_from_string(cls, string, nsmap):
return Element(string, nsmap=nsmap)
def build_element(self, tag, ns=None, attribs={}):
attribs['nsmap'] = ns
if ns:
tag = '{%s}%s' % (self.nsmap[ns], tag)
return Element(tag, **attribs)
def build_from_expression(self, node_expr):
match = re.search(self._re_node_expr, node_expr)
expr = match.groupdict()
attrs = dict(re.findall(self._re_attrs, expr['attrs'] or ''))
attrs['nsmap'] = None
if expr['ns'] and expr['tag']:
ns = expr['ns']
tag = expr['tag']
if self.nsmap:
node = Element('{%s}%s' % (self.nsmap[ns], tag), **attrs)
else:
node = Element(tag, **attrs)
return node
return Element(expr['tag'], **attrs)
def _normalize_tag(self, tag):
return re.sub(r'^(\{.+\}|.+:)', '', tag)
def get_tag(self, elem):
return self._normalize_tag(elem.tag)
def same_tag(self, a, b):
return self._normalize_tag(a) \
== self._normalize_tag(b)
def find_relative(self, elem, xpath, ns):
return elem.find(xpath, ns)
def append(self, elem, child):
elem.append(child)
def set_text(self, elem, text):
elem.text = text
def get_text(self, elem):
return elem.text
def set_attribute(self, elem, key, value):
elem.attrib[key] = value
def tostring(self, elem):
return tostring(elem).decode('utf-8')
class FachoXML:
"""
Decora XML con funciones de consulta XPATH de un solo elemento
"""
def __init__(self, root, builder=None, nsmap=None):
if builder is None:
self.builder = LXMLBuilder(nsmap)
else:
self.builder = builder
self.nsmap = nsmap
if isinstance(root, str):
self.root = self.builder.build_element_from_string(root, nsmap)
else:
self.root = root
self.xpath_for = {}
self.extensions = []
def add_extension(self, extension):
self.extensions.append(extension)
def attach_extensions(self):
root_tag = self.builder.get_tag(self.root)
# construir las extensiones o adicionar en caso de indicar
for extension in self.extensions:
xpath, elements = extension.build(self)
if isinstance(elements, str):
elem = self.set_element('/'+ root_tag + xpath, elements)
else:
for new_element in elements:
elem = self.find_or_create_element('/'+ root_tag + xpath)
self.builder.append(elem, new_element)
def fragment(self, xpath, append=False):
parent = self.find_or_create_element(xpath, append=append)
return FachoXML(parent, nsmap=self.nsmap)
def register_alias_xpath(self, alias, xpath):
self.xpath_for[alias] = xpath
def _normalize_xpath(self, xpath):
return xpath.replace('//', '/')
def find_or_create_element(self, xpath, append=False):
"""
@param xpath ruta xpath para crear o consultar de un solo elemendo
@param append True si se debe adicionar en la ruta xpath indicada
@return elemento segun self.builder
"""
xpath = self._normalize_xpath(xpath)
if xpath in self.xpath_for:
xpath = self.xpath_for[xpath]
node_paths = xpath.split('/')
node_paths.pop(0) #remove empty /
root_node = self.builder.build_from_expression(node_paths[0])
if not self.builder.same_tag(root_node.tag, self.root.tag):
raise ValueError('xpath %s must be absolute to /%s' % (xpath, self.root.tag))
else:
node_paths.pop(0)
# crea jerarquia segun xpath indicado
parent = None
current_elem = self.root
for node_path in node_paths:
node_expr = self.builder.match_expression(node_path)
node = self.builder.build_from_expression(node_path)
child = self.builder.find_relative(current_elem, node_expr['path'], self.nsmap)
parent = current_elem
if child is not None:
current_elem = child
else:
self.builder.append(current_elem, node)
current_elem = node
# se fuerza la adicion como un nuevo elemento
if append:
node = self.builder.build_from_expression(node_paths[-1])
self.builder.append(parent, node)
return node
return current_elem
def set_element(self, xpath, content, **attrs):
xpath = self._normalize_xpath(xpath)
format_ = attrs.pop('format_', '%s')
elem = self.find_or_create_element(xpath)
if content:
self.builder.set_text(elem, format_ % content)
for k, v in attrs.items():
self.builder.set_attribute(elem, k, v)
return elem
def get_element_text(self, xpath, format_=str):
xpath = self._normalize_xpath(xpath)
text = self.builder.get_text(self.find_or_create_element(xpath))
return format_(text)
def tostring(self):
return self.builder.tostring(self.root)
def __str__(self):
return self.tostring()