diff --git a/facho/model/__init__.py b/facho/model/__init__.py new file mode 100644 index 0000000..1f6c14b --- /dev/null +++ b/facho/model/__init__.py @@ -0,0 +1,62 @@ + +class ModelMeta(type): + def __new__(cls, name, bases, ns): + new = type.__new__(cls, name, bases, ns) + if '__name__' in ns: + new.__name__ = ns['__name__'] + if '__namespace__' in ns: + new.__namespace__ = ns['__namespace__'] + else: + new.__namespace__ = {} + + return new + +class ModelBase(object, metaclass=ModelMeta): + + def __new__(cls, *args, **kwargs): + obj = super().__new__(cls, *args, **kwargs) + obj._xml_attributes = {} + obj._fields = {} + obj._text = "" + obj._namespace_prefix = None + + return obj + + def __setitem__(self, key, val): + self._xml_attributes[key] = val + + def __getitem__(self, key): + return self._xml_attributes[key] + + def to_xml(self): + tag = self.__name__ + ns = '' + if self._namespace_prefix is not None: + ns = "%s:" % (self._namespace_prefix) + + pair_attributes = ["%s=\"%s\"" % (k, v) for (k, v) in self._xml_attributes.values()] + + for (prefix, url) in self.__namespace__.items(): + pair_attributes.append("xmlns:%s=\"%s\"" % (prefix, url)) + attributes = "" + if pair_attributes: + attributes = " " + " ".join(pair_attributes) + + content = "" + + for name, value in self._fields.items(): + if hasattr(value, 'to_xml'): + print(self._fields) + content += value.to_xml() + elif isinstance(value, str): + content += value + content += self._text + + if content == "": + return "<%s%s%s/>" % (ns, tag, attributes) + else: + return "<%s%s%s>%s" % (ns, tag, attributes, content, ns, tag) + +class Model(ModelBase): + pass + diff --git a/facho/model/fields/__init__.py b/facho/model/fields/__init__.py new file mode 100644 index 0000000..f368aaf --- /dev/null +++ b/facho/model/fields/__init__.py @@ -0,0 +1,5 @@ +from .attribute import Attribute +from .many2one import Many2One +from .model import Model + +__all__ = [Attribute, Many2One, Model] diff --git a/facho/model/fields/attribute.py b/facho/model/fields/attribute.py new file mode 100644 index 0000000..2ad9f73 --- /dev/null +++ b/facho/model/fields/attribute.py @@ -0,0 +1,17 @@ +from .field import Field + +class Attribute(Field): + def __init__(self, tag): + self.tag = tag + + def __get__(self, inst, cls): + if inst is None: + return self + + assert self.name is not None + (tag, value) = inst._xml_attributes[self.name] + return value + + def __set__(self, inst, value): + assert self.name is not None + inst._xml_attributes[self.name] = (self.tag, value) diff --git a/facho/model/fields/field.py b/facho/model/fields/field.py new file mode 100644 index 0000000..251223e --- /dev/null +++ b/facho/model/fields/field.py @@ -0,0 +1,21 @@ +class Field: + def __set_name__(self, owner, name): + self.name = name + + def __get__(self, inst, cls): + if inst is None: + return self + assert self.name is not None + return inst._fields[self.name] + + def __set__(self, inst, value): + assert self.name is not None + inst._fields[self.name] = value + + def _set_namespace(self, inst, name, namespaces): + if name is None: + return + + if name not in namespaces: + raise KeyError("namespace %s not found" % (name)) + inst._namespace_prefix = name diff --git a/facho/model/fields/many2one.py b/facho/model/fields/many2one.py new file mode 100644 index 0000000..7f5e35f --- /dev/null +++ b/facho/model/fields/many2one.py @@ -0,0 +1,35 @@ +from .field import Field + +class Many2One(Field): + def __init__(self, model, setter=None, namespace=None): + self.model = model + self.setter = setter + self.namespace = namespace + + def __set_name__(self, owner, name): + self.name = name + + def __get__(self, inst, cls): + if inst is None: + return self + assert self.name is not None + return inst._fields[self.name] + + def __set__(self, inst, value): + assert self.name is not None + class_model = self.model + inst_model = class_model() + + self._set_namespace(inst_model, self.namespace, inst.__namespace__) + inst._fields[self.name] = inst_model + + # si hay setter manual se ejecuta + # de lo contrario se asigna como texto del elemento + setter = getattr(inst, self.setter or '', None) + if callable(setter): + setter(inst_model, value) + else: + inst_model._text = str(value) + + + diff --git a/facho/model/fields/model.py b/facho/model/fields/model.py new file mode 100644 index 0000000..162be39 --- /dev/null +++ b/facho/model/fields/model.py @@ -0,0 +1,26 @@ +from .field import Field + +class Model(Field): + def __init__(self, model, namespace=None): + self.model = model + self.namespace = namespace + + def __get__(self, inst, cls): + if inst is None: + return self + assert self.name is not None + return self._create_model(inst) + + def __set__(self, inst, value): + obj = self._create_model(inst) + obj._text = str(value) + + def _create_model(self, inst): + try: + return inst._fields[self.name] + except KeyError: + obj = self.model() + self._set_namespace(obj, self.namespace, inst.__namespace__) + inst._fields[self.name] = obj + return obj + diff --git a/tests/test_model.py b/tests/test_model.py new file mode 100644 index 0000000..8aeb7e1 --- /dev/null +++ b/tests/test_model.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# This file is part of facho. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. + +"""Tests for `facho` package.""" + +import pytest + +import facho.model +import facho.model.fields as fields + +def test_model_to_element(): + class Person(facho.model.Model): + __name__ = 'Person' + + person = Person() + + assert "" == person.to_xml() + +def test_model_to_element_with_attribute(): + class Person(facho.model.Model): + __name__ = 'Person' + id = fields.Attribute('id') + + person = Person() + person.id = 33 + + personb = Person() + personb.id = 44 + + assert "" == person.to_xml() + assert "" == personb.to_xml() + +def test_model_to_element_with_attribute_as_element(): + class ID(facho.model.Model): + __name__ = 'ID' + + class Person(facho.model.Model): + __name__ = 'Person' + + id = fields.Many2One(ID) + + person = Person() + person.id = 33 + assert "33" == person.to_xml() + +def test_many2one_with_custom_attributes(): + class TaxAmount(facho.model.Model): + __name__ = 'TaxAmount' + + currencyID = fields.Attribute('currencyID') + + class TaxTotal(facho.model.Model): + __name__ = 'TaxTotal' + + amount = fields.Many2One(TaxAmount) + + tax_total = TaxTotal() + tax_total.amount = 3333 + tax_total.amount.currencyID = 'COP' + assert '3333' == tax_total.to_xml() + +def test_many2one_with_custom_setter(): + + class PhysicalLocation(facho.model.Model): + __name__ = 'PhysicalLocation' + + id = fields.Attribute('ID') + + class Party(facho.model.Model): + __name__ = 'Party' + + location = fields.Many2One(PhysicalLocation, setter='location_setter') + + def location_setter(self, field, value): + field.id = value + + party = Party() + party.location = 99 + assert '' == party.to_xml() + +def test_field_model(): + class ID(facho.model.Model): + __name__ = 'ID' + + class Person(facho.model.Model): + __name__ = 'Person' + + id = fields.Model(ID) + + person = Person() + person.id = ID() + person.id = 33 + assert "33" == person.to_xml() + +def test_field_model_failed_initialization(): + class ID(facho.model.Model): + __name__ = 'ID' + + class Person(facho.model.Model): + __name__ = 'Person' + + id = fields.Model(ID) + + + person = Person() + person.id = 33 + assert "33" == person.to_xml() + +def test_field_model_default_initialization_with_attributes(): + class ID(facho.model.Model): + __name__ = 'ID' + + reference = fields.Attribute('REFERENCE') + + class Person(facho.model.Model): + __name__ = 'Person' + + id = fields.Model(ID) + + person = Person() + person.id = 33 + person.id.reference = 'haber' + assert '33' == person.to_xml() + +def test_model_with_xml_namespace(): + class Person(facho.model.Model): + __name__ = 'Person' + __namespace__ = { + 'facho': 'http://lib.facho.cyou' + } + + person = Person() + assert '' + +def test_model_with_xml_namespace_nested(): + class ID(facho.model.Model): + __name__ = 'ID' + + class Person(facho.model.Model): + __name__ = 'Person' + __namespace__ = { + 'facho': 'http://lib.facho.cyou' + } + + id = fields.Many2One(ID, namespace='facho') + + person = Person() + person.id = 33 + assert '33' == person.to_xml() + +def test_field_model_with_namespace(): + class ID(facho.model.Model): + __name__ = 'ID' + + class Person(facho.model.Model): + __name__ = 'Person' + __namespace__ = { + "facho": "http://lib.facho.cyou" + } + id = fields.Model(ID, namespace="facho") + + + person = Person() + person.id = 33 + assert '33' == person.to_xml()