You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1071 lines
37 KiB
1071 lines
37 KiB
|
|
#
|
|
# spyne - Copyright (C) Spyne contributors.
|
|
#
|
|
# This library is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU Lesser General Public
|
|
# License as published by the Free Software Foundation; either
|
|
# version 2.1 of the License, or (at your option) any later version.
|
|
#
|
|
# This library is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
# Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public
|
|
# License along with this library; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
|
#
|
|
|
|
"""This module contains the ModelBase class and other building blocks for
|
|
defining models.
|
|
"""
|
|
|
|
from __future__ import print_function
|
|
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
import re
|
|
import decimal
|
|
import threading
|
|
|
|
import spyne.const.xml
|
|
|
|
from copy import deepcopy
|
|
from collections import OrderedDict
|
|
|
|
from spyne import const
|
|
from spyne.util import Break, six
|
|
from spyne.util.cdict import cdict
|
|
from spyne.util.odict import odict
|
|
|
|
from spyne.const.xml import DEFAULT_NS
|
|
|
|
|
|
def _decode_pa_dict(d):
|
|
"""Decodes dict passed to prot_attrs.
|
|
|
|
>>> _decode_pa_dict({})
|
|
cdict({})
|
|
>>> _decode_pa_dict({1: 2)})
|
|
cdict({1: 2})
|
|
>>> _decode_pa_dict({(1,2): 3)})
|
|
cdict({1: 3, 2: 3})
|
|
"""
|
|
|
|
retval = cdict()
|
|
for k, v in d.items():
|
|
if isinstance(k, (frozenset, tuple)):
|
|
for subk in k:
|
|
retval[subk] = v
|
|
|
|
for k, v in d.items():
|
|
if not isinstance(k, (frozenset, tuple)):
|
|
retval[k] = v
|
|
|
|
return retval
|
|
|
|
|
|
class AttributesMeta(type(object)):
|
|
NULLABLE_DEFAULT = True
|
|
|
|
def __new__(cls, cls_name, cls_bases, cls_dict):
|
|
# Mapper args should not be inherited.
|
|
if not 'sqla_mapper_args' in cls_dict:
|
|
cls_dict['sqla_mapper_args'] = None
|
|
|
|
rd = {}
|
|
for k in list(cls_dict.keys()):
|
|
if k in ('parser', 'cast'):
|
|
rd['parser'] = cls_dict.pop(k)
|
|
continue
|
|
|
|
if k in ('sanitize', 'sanitizer'):
|
|
rd['sanitizer'] = cls_dict.pop(k)
|
|
continue
|
|
|
|
if k == 'logged':
|
|
rd['logged'] = cls_dict.pop(k)
|
|
continue
|
|
|
|
retval = super(AttributesMeta, cls).__new__(cls, cls_name, cls_bases,
|
|
cls_dict)
|
|
|
|
for k, v in rd.items():
|
|
if v is None:
|
|
setattr(retval, k, None)
|
|
else:
|
|
setattr(retval, k, staticmethod(v))
|
|
|
|
return retval
|
|
|
|
def __init__(self, cls_name, cls_bases, cls_dict):
|
|
# you will probably want to look at ModelBase._s_customize as well.
|
|
if not hasattr(self, '_method_config_do'):
|
|
self._method_config_do = None
|
|
|
|
nullable = cls_dict.get('nullable', None)
|
|
nillable = cls_dict.get('nillable', None)
|
|
if nullable is not None:
|
|
assert nillable is None or nullable == nillable
|
|
self._nullable = nullable
|
|
|
|
elif nillable is not None:
|
|
assert nullable is None or nullable == nillable
|
|
self._nullable = nillable
|
|
|
|
if not hasattr(self, '_nullable'):
|
|
self._nullable = None
|
|
|
|
if not hasattr(self, '_default_factory'):
|
|
self._default_factory = None
|
|
|
|
if not hasattr(self, '_html_cloth'):
|
|
self._html_cloth = None
|
|
if not hasattr(self, '_html_root_cloth'):
|
|
self._html_root_cloth = None
|
|
|
|
if 'html_cloth' in cls_dict:
|
|
self.set_html_cloth(cls_dict.pop('html_cloth'))
|
|
if 'html_root_cloth' in cls_dict:
|
|
self.set_html_cloth(cls_dict.pop('html_root_cloth'))
|
|
|
|
if not hasattr(self, '_xml_cloth'):
|
|
self._xml_cloth = None
|
|
if not hasattr(self, '_xml_root_cloth'):
|
|
self._xml_root_cloth = None
|
|
|
|
if 'xml_cloth' in cls_dict:
|
|
self.set_xml_cloth(cls_dict.pop('xml_cloth'))
|
|
|
|
if 'xml_root_cloth' in cls_dict:
|
|
self.set_xml_cloth(cls_dict.pop('xml_root_cloth'))
|
|
|
|
if 'method_config_do' in cls_dict and \
|
|
cls_dict['method_config_do'] is not None:
|
|
cls_dict['method_config_do'] = \
|
|
staticmethod(cls_dict['method_config_do'])
|
|
|
|
super(AttributesMeta, self).__init__(cls_name, cls_bases, cls_dict)
|
|
|
|
def get_nullable(self):
|
|
return (self._nullable if self._nullable is not None else
|
|
self.NULLABLE_DEFAULT)
|
|
|
|
def set_nullable(self, what):
|
|
self._nullable = what
|
|
|
|
nullable = property(get_nullable, set_nullable)
|
|
|
|
def get_nillable(self):
|
|
return self.nullable
|
|
|
|
def set_nillable(self, what):
|
|
self.nullable = what
|
|
|
|
nillable = property(get_nillable, set_nillable)
|
|
|
|
def get_default_factory(self):
|
|
return self._default_factory
|
|
|
|
def set_default_factory(self, what):
|
|
self._default_factory = staticmethod(what)
|
|
|
|
default_factory = property(get_default_factory, set_default_factory)
|
|
|
|
def get_html_cloth(self):
|
|
return self._html_cloth
|
|
def set_html_cloth(self, what):
|
|
from spyne.protocol.cloth.to_cloth import ClothParserMixin
|
|
cm = ClothParserMixin.from_html_cloth(what)
|
|
if cm._root_cloth is not None:
|
|
self._html_root_cloth = cm._root_cloth
|
|
elif cm._cloth is not None:
|
|
self._html_cloth = cm._cloth
|
|
else:
|
|
raise Exception("%r is not a suitable cloth", what)
|
|
html_cloth = property(get_html_cloth, set_html_cloth)
|
|
|
|
def get_html_root_cloth(self):
|
|
return self._html_root_cloth
|
|
html_root_cloth = property(get_html_root_cloth)
|
|
|
|
def get_xml_cloth(self):
|
|
return self._xml_cloth
|
|
def set_xml_cloth(self, what):
|
|
from spyne.protocol.cloth.to_cloth import ClothParserMixin
|
|
cm = ClothParserMixin.from_xml_cloth(what)
|
|
if cm._root_cloth is not None:
|
|
self._xml_root_cloth = cm._root_cloth
|
|
elif cm._cloth is not None:
|
|
self._xml_cloth = cm._cloth
|
|
else:
|
|
raise Exception("%r is not a suitable cloth", what)
|
|
xml_cloth = property(get_xml_cloth, set_xml_cloth)
|
|
|
|
def get_xml_root_cloth(self):
|
|
return self._xml_root_cloth
|
|
xml_root_cloth = property(get_xml_root_cloth)
|
|
|
|
def get_method_config_do(self):
|
|
return self._method_config_do
|
|
def set_method_config_do(self, what):
|
|
if what is None:
|
|
self._method_config_do = None
|
|
else:
|
|
self._method_config_do = staticmethod(what)
|
|
method_config_do = property(get_method_config_do, set_method_config_do)
|
|
|
|
|
|
class ModelBaseMeta(type(object)):
|
|
def __getitem__(self, item):
|
|
return self.customize(**item)
|
|
|
|
def customize(self, **kwargs):
|
|
"""Duplicates cls and overwrites the values in ``cls.Attributes`` with
|
|
``**kwargs`` and returns the new class."""
|
|
|
|
cls_name, cls_bases, cls_dict = self._s_customize(**kwargs)
|
|
|
|
return type(cls_name, cls_bases, cls_dict)
|
|
|
|
|
|
@six.add_metaclass(ModelBaseMeta)
|
|
class ModelBase(object):
|
|
"""The base class for type markers. It defines the model interface for the
|
|
interface generators to use and also manages class customizations that are
|
|
mainly used for defining constraints on input values.
|
|
"""
|
|
|
|
__orig__ = None
|
|
"""This holds the original class the class .customize()d from. Ie if this is
|
|
None, the class is not a customize()d one."""
|
|
|
|
__extends__ = None
|
|
"""This holds the original class the class inherited or .customize()d from.
|
|
This is different from __orig__ because it's only set when
|
|
``cls.is_default(cls) == False``"""
|
|
|
|
__namespace__ = None
|
|
"""The public namespace of this class. Use ``get_namespace()`` instead of
|
|
accessing it directly."""
|
|
|
|
__type_name__ = None
|
|
"""The public type name of the class. Use ``get_type_name()`` instead of
|
|
accessing it directly."""
|
|
|
|
Value = type(None)
|
|
"""The value of this type is an instance of this class"""
|
|
|
|
# These are not the xml schema defaults. The xml schema defaults are
|
|
# considered in XmlSchema's add() method. the defaults here are to reflect
|
|
# what people seem to want most.
|
|
#
|
|
# Please note that min_occurs and max_occurs must be validated in the
|
|
# ComplexModelBase deserializer.
|
|
@six.add_metaclass(AttributesMeta)
|
|
class Attributes(object):
|
|
"""The class that holds the constraints for the given type."""
|
|
|
|
_wrapper = False
|
|
# when skip_wrappers=True is passed to a protocol, these objects
|
|
# are skipped. just for internal use.
|
|
|
|
_explicit_type_name = False
|
|
# set to true when type_name is passed to customize() call.
|
|
|
|
out_type = None
|
|
"""Override serialization type. Usually, this designates the return type
|
|
of the callable in the `sanitizer` attribute. If this is a two-way type,
|
|
it may be a good idea to also use the `parser` attribute to perform
|
|
reverse conversion."""
|
|
|
|
default = None
|
|
"""The default value if the input is None.
|
|
|
|
Please note that this default is UNCONDITIONALLY applied in class
|
|
initializer. It's recommended to at least make an effort to use this
|
|
only in customized classes and not in original models.
|
|
"""
|
|
|
|
default_factory = None
|
|
"""The callable that produces a default value if the value is None.
|
|
|
|
The warnings in ``default`` apply here as well."""
|
|
|
|
db_default = None
|
|
"""The default value used only when persisting the value if it is
|
|
``None``.
|
|
|
|
Only works for primitives. Unlike ``default`` this can also be set to a
|
|
callable that takes no arguments according to SQLAlchemy docs."""
|
|
|
|
nillable = None
|
|
"""Set this to false to reject null values. Synonyms with
|
|
``nullable``. True by default. The default value can be changed by
|
|
setting ``AttributesMeta.NULLABLE_DEFAULT``."""
|
|
|
|
min_occurs = 0
|
|
"""Set this to 1 to make this object mandatory. Can be set to any
|
|
positive integer. Note that an object can still be null or empty, even
|
|
if it's there."""
|
|
|
|
max_occurs = 1
|
|
"""Can be set to any strictly positive integer. Values greater than 1
|
|
will imply an iterable of objects as native python type. Can be set to
|
|
``decimal.Decimal("inf")`` for arbitrary number of arguments."""
|
|
|
|
schema_tag = spyne.const.xml.XSD('element')
|
|
"""The tag used to add a primitives as child to a complex type in the
|
|
xml schema."""
|
|
|
|
translations = None
|
|
"""A dict that contains locale codes as keys and translations of field
|
|
names to that language as values.
|
|
"""
|
|
|
|
sub_ns = None
|
|
"""An Xml-specific attribute that specifies which namespace should be
|
|
used for field names in classes.
|
|
"""
|
|
|
|
sub_name = None
|
|
"""This specifies which string should be used as field name when this
|
|
type is seriazed under a ComplexModel.
|
|
"""
|
|
|
|
wsdl_part_name = None
|
|
"""This specifies which string should be used as wsdl message part name when this
|
|
type is serialized under a ComplexModel ie."parameters".
|
|
"""
|
|
|
|
sqla_column_args = None
|
|
"""A dict that will be passed to SQLAlchemy's ``Column`` constructor as
|
|
``**kwargs``.
|
|
"""
|
|
|
|
exc_mapper = False
|
|
"""If true, this field will be excluded from the table mapper of the
|
|
parent class.
|
|
"""
|
|
|
|
exc_table = False
|
|
"""DEPRECATED !!! Use ``exc_db`` instead."""
|
|
|
|
exc_db = False
|
|
"""If ``True``, this field will not be persisted to the database. This
|
|
attribute only makes sense in a subfield of a ``ComplexModel`` subclass.
|
|
"""
|
|
|
|
exc_interface = False
|
|
"""If `True`, this field will be excluded from the interface
|
|
document."""
|
|
|
|
exc = False
|
|
"""If `True`, this field will be excluded from all serialization or
|
|
deserialization operations. See `prot_attrs` to make this only apply to
|
|
a specific protocol class or instance."""
|
|
|
|
logged = True
|
|
"""If `False`, this object will be ignored in ``log_repr``, mostly used
|
|
for logging purposes.
|
|
|
|
* Primitives can have logger=``'...'`` which will
|
|
always log the value as ``(...)``.
|
|
|
|
* ``AnyDict`` can have one of
|
|
``('keys', 'keys-full', 'values', 'values-full, 'full')`` as logger
|
|
value where for ``'keys'`` and ``'values'`` the output of ``keys()``
|
|
and ``values()`` will be logged up to MAX_DICT_ELEMENT_NUM number of
|
|
elements and for ``'full'`` variants, all of the contents of the dict
|
|
will be logged will be logged
|
|
|
|
* ``Array`` can also have ``logger='full'`` where all of the value
|
|
will be logged where as for simple ``logger=True`` only
|
|
MAX_ARRAY_ELEMENT_NUM elements will be logged.
|
|
|
|
* For ``ComplexModel`` subclasses sent as first value to log_repr,
|
|
``logger=False`` means a string of form ``ClassName(...)`` will be
|
|
logged.
|
|
"""
|
|
|
|
sanitizer = None
|
|
"""A callable that takes the associated native type and returns the
|
|
parsed value. Only called during serialization."""
|
|
|
|
parser = None
|
|
"""A callable that takes the associated native type and returns the
|
|
parsed value. Only called during deserialization."""
|
|
|
|
unique = None
|
|
"""If True, this object will be set as unique in the database schema
|
|
with default indexing options. If the value is a string, it will be
|
|
used as the indexing method to create the unique index. See sqlalchemy
|
|
documentation on how to create multi-column unique constraints.
|
|
"""
|
|
|
|
db_type = None
|
|
"""When not None, it overrides Spyne's own mapping from Spyne types to
|
|
SQLAlchemy types. It's a standard SQLAlchemy type marker, e.g.
|
|
``sqlalchemy.Integer``.
|
|
"""
|
|
|
|
table_name = None
|
|
"""Database table name."""
|
|
|
|
xml_choice_group = None
|
|
"""When not None, shares the same <choice> tag with fields with the same
|
|
xml_choice_group value.
|
|
"""
|
|
|
|
index = None
|
|
"""Can be ``True``, a string, or a tuple of two strings.
|
|
|
|
* If True, this object will be set as indexed in the database schema
|
|
with default options.
|
|
|
|
* If the value is a string, the value will denote the indexing method
|
|
used by the database. Should be one of:
|
|
|
|
('btree', 'gin', 'gist', 'hash', 'spgist')
|
|
|
|
See: http://www.postgresql.org/docs/9.2/static/indexes-types.html
|
|
|
|
* If the value is a tuple of two strings, the first value will denote
|
|
the index name and the second value will denote the indexing method as
|
|
above.
|
|
"""
|
|
|
|
read_only = False
|
|
"""If True, the attribute won't be initialized from outside values.
|
|
Set this to ``True`` for e.g. read-only properties."""
|
|
|
|
prot_attrs = None
|
|
"""Customize child attributes for protocols. It's a dict of dicts.
|
|
The key is either a ProtocolBase subclass or a ProtocolBase instance.
|
|
Instances override classes."""
|
|
|
|
pa = None
|
|
"""Alias for prot_attrs."""
|
|
|
|
empty_is_none = False
|
|
"""When the incoming object is empty (e.g. '' for strings) treat it as
|
|
None. No effect (yet) for outgoing values."""
|
|
|
|
order = None
|
|
"""An integer that's passed to ``_type_info.insert()`` as first argument
|
|
when not None. ``.append()`` is used otherwise."""
|
|
|
|
validate_on_assignment = False
|
|
"""Perform validation on assignment (i.e. all the time) instead of on
|
|
just serialization"""
|
|
|
|
polymap = {}
|
|
"""A dict of classes that override polymorphic substitions for classes
|
|
given as keys to classes given as values."""
|
|
|
|
|
|
class Annotations(object):
|
|
"""The class that holds the annotations for the given type."""
|
|
|
|
__use_parent_doc__ = False
|
|
"""If equal to True and doc is empty, Annotations will use __doc__
|
|
from parent. Set it to False to avoid this mechanism. This is a
|
|
convenience option"""
|
|
|
|
doc = ""
|
|
"""The public documentation for the given type."""
|
|
|
|
appinfo = None
|
|
"""Any object that carries app-specific info."""
|
|
|
|
class Empty(object):
|
|
pass
|
|
|
|
_force_own_namespace = None
|
|
|
|
@classmethod
|
|
def ancestors(cls):
|
|
"""Returns a list of parent classes in child-to-parent order."""
|
|
|
|
retval = []
|
|
|
|
extends = cls.__extends__
|
|
while extends is not None:
|
|
retval.append(extends)
|
|
extends = extends.__extends__
|
|
|
|
return retval
|
|
|
|
@staticmethod
|
|
def is_default(cls):
|
|
return True
|
|
|
|
@classmethod
|
|
def get_namespace_prefix(cls, interface):
|
|
"""Returns the namespace prefix for the given interface. The
|
|
get_namespace_prefix of the interface class generates a prefix if none
|
|
is defined.
|
|
"""
|
|
|
|
ns = cls.get_namespace()
|
|
|
|
retval = interface.get_namespace_prefix(ns)
|
|
|
|
return retval
|
|
|
|
@classmethod
|
|
def get_namespace(cls):
|
|
"""Returns the namespace of the class. Defaults to the python module
|
|
name."""
|
|
|
|
return cls.__namespace__
|
|
|
|
@classmethod
|
|
def _fill_empty_type_name(cls, parent_ns, parent_tn, k):
|
|
cls.__namespace__ = parent_ns
|
|
|
|
cls.__type_name__ = "%s_%s%s" % (parent_tn, k, const.TYPE_SUFFIX)
|
|
extends = cls.__extends__
|
|
while extends is not None and extends.__type_name__ is ModelBase.Empty:
|
|
cls.__extends__._fill_empty_type_name(cls.get_namespace(),
|
|
cls.get_type_name(), k + const.PARENT_SUFFIX)
|
|
extends = extends.__extends__
|
|
|
|
# TODO: rename to "resolve_identifier"
|
|
@staticmethod
|
|
def resolve_namespace(cls, default_ns, tags=None):
|
|
"""This call finalizes the namespace assignment. The default namespace
|
|
is not available until the application calls populate_interface method
|
|
of the interface generator.
|
|
"""
|
|
|
|
if tags is None:
|
|
tags = set()
|
|
elif cls in tags:
|
|
return False
|
|
tags.add(cls)
|
|
|
|
if cls.__namespace__ is spyne.const.xml.DEFAULT_NS:
|
|
cls.__namespace__ = default_ns
|
|
|
|
if (cls.__namespace__ in spyne.const.xml.PREFMAP and
|
|
not cls.is_default(cls)):
|
|
cls.__namespace__ = default_ns
|
|
|
|
if cls.__namespace__ is None:
|
|
ret = []
|
|
for f in cls.__module__.split('.'):
|
|
if f.startswith('_'):
|
|
break
|
|
else:
|
|
ret.append(f)
|
|
|
|
cls.__namespace__ = '.'.join(ret)
|
|
|
|
if cls.__namespace__ is None or len(cls.__namespace__) == 0:
|
|
cls.__namespace__ = default_ns
|
|
|
|
if cls.__namespace__ is None or len(cls.__namespace__) == 0:
|
|
raise ValueError("You need to explicitly set %r.__namespace__" % cls)
|
|
|
|
# print(" resolve ns for %r to %r" % (cls, cls.__namespace__))
|
|
|
|
if getattr(cls, '__extends__', None) != None:
|
|
cls.__extends__.resolve_namespace(cls.__extends__, default_ns, tags)
|
|
|
|
return True
|
|
|
|
@classmethod
|
|
def get_type_name(cls):
|
|
"""Returns the class name unless the __type_name__ attribute is defined.
|
|
"""
|
|
|
|
retval = cls.__type_name__
|
|
if retval is None:
|
|
retval = cls.__name__
|
|
|
|
return retval
|
|
|
|
# FIXME: Rename this to get_type_name_with_ns_pref
|
|
@classmethod
|
|
def get_type_name_ns(cls, interface):
|
|
"""Returns the type name with a namespace prefix, separated by a column.
|
|
"""
|
|
|
|
if cls.get_namespace() != None:
|
|
return "%s:%s" % (cls.get_namespace_prefix(interface),
|
|
cls.get_type_name())
|
|
|
|
@classmethod
|
|
def get_element_name(cls):
|
|
return cls.Attributes.sub_name or cls.get_type_name()
|
|
|
|
@classmethod
|
|
def get_wsdl_part_name(cls):
|
|
return cls.Attributes.wsdl_part_name or cls.get_element_name()
|
|
|
|
@classmethod
|
|
def get_element_name_ns(cls, interface):
|
|
ns = cls.Attributes.sub_ns or cls.get_namespace()
|
|
if ns is DEFAULT_NS:
|
|
ns = interface.get_tns()
|
|
if ns is not None:
|
|
pref = interface.get_namespace_prefix(ns)
|
|
return "%s:%s" % (pref, cls.get_element_name())
|
|
|
|
@classmethod
|
|
def to_bytes(cls, value):
|
|
"""
|
|
Returns str(value). This should be overridden if this is not enough.
|
|
"""
|
|
return six.binary_type(value)
|
|
|
|
@classmethod
|
|
def to_unicode(cls, value):
|
|
"""
|
|
Returns unicode(value). This should be overridden if this is not enough.
|
|
"""
|
|
return six.text_type(value)
|
|
|
|
@classmethod
|
|
def get_documentation(cls):
|
|
if cls.Annotations.doc:
|
|
return cls.Annotations.doc
|
|
elif cls.Annotations.__use_parent_doc__:
|
|
return cls.__doc__
|
|
else:
|
|
return ''
|
|
|
|
@classmethod
|
|
def _s_customize(cls, **kwargs):
|
|
"""Sanitizes customization parameters of the class it belongs to.
|
|
Doesn't perform any actual customization.
|
|
"""
|
|
|
|
def _log_debug(s, *args):
|
|
logger.debug("\t%s: %s" % (cls.get_type_name(), s), *args)
|
|
|
|
cls_dict = odict({'__module__': cls.__module__, '__doc__': cls.__doc__})
|
|
|
|
if getattr(cls, '__orig__', None) is None:
|
|
cls_dict['__orig__'] = cls
|
|
else:
|
|
cls_dict['__orig__'] = cls.__orig__
|
|
|
|
class Attributes(cls.Attributes):
|
|
_explicit_type_name = False
|
|
|
|
if cls.Attributes.translations is None:
|
|
Attributes.translations = {}
|
|
|
|
if cls.Attributes.sqla_column_args is None:
|
|
Attributes.sqla_column_args = (), {}
|
|
else:
|
|
Attributes.sqla_column_args = deepcopy(
|
|
cls.Attributes.sqla_column_args)
|
|
|
|
cls_dict['Attributes'] = Attributes
|
|
|
|
# properties get reset every time a new class is defined. So we need
|
|
# to reinitialize them explicitly.
|
|
for k in ('nillable', '_xml_cloth', '_xml_root_cloth', '_html_cloth',
|
|
'_html_root_cloth'):
|
|
v = getattr(cls.Attributes, k)
|
|
if v is not None:
|
|
setattr(Attributes, k, v)
|
|
|
|
class Annotations(cls.Annotations):
|
|
pass
|
|
cls_dict['Annotations'] = Annotations
|
|
|
|
# get protocol attrs
|
|
prot = kwargs.get('protocol', None)
|
|
if prot is None:
|
|
prot = kwargs.get('prot', None)
|
|
|
|
if prot is None:
|
|
prot = kwargs.get('p', None)
|
|
|
|
if prot is not None and len(prot.type_attrs) > 0:
|
|
# if there is a class customization from protocol, do it
|
|
|
|
type_attrs = prot.type_attrs.copy()
|
|
type_attrs.update(kwargs)
|
|
_log_debug("kwargs %r => %r from prot typeattr %r",
|
|
kwargs, type_attrs, prot.type_attrs)
|
|
kwargs = type_attrs
|
|
|
|
# the ones that wrap values in staticmethod() should be added to
|
|
# AttributesMeta initializer
|
|
for k, v in kwargs.items():
|
|
if k.startswith('_'):
|
|
_log_debug("ignoring '%s' because of leading underscore", k)
|
|
continue
|
|
|
|
if k in ('protocol', 'prot', 'p'):
|
|
Attributes.prot = v
|
|
_log_debug("setting prot=%r", v)
|
|
|
|
elif k in ('voa', 'validate_on_assignment'):
|
|
Attributes.validate_on_assignment = v
|
|
_log_debug("setting voa=%r", v)
|
|
|
|
elif k in ('parser', 'in_cast'):
|
|
setattr(Attributes, 'parser', staticmethod(v))
|
|
_log_debug("setting %s=%r", k, v)
|
|
|
|
elif k in ('sanitize', 'sanitizer', 'out_cast'):
|
|
setattr(Attributes, 'sanitizer', staticmethod(v))
|
|
_log_debug("setting %s=%r as sanitizer", k, v)
|
|
|
|
elif k == 'logged':
|
|
setattr(Attributes, 'logged', staticmethod(v))
|
|
_log_debug("setting %s=%r as log sanitizer", k, v)
|
|
|
|
elif k in ("doc", "appinfo"):
|
|
setattr(Annotations, k, v)
|
|
_log_debug("setting Annotations.%s=%r", k, v)
|
|
|
|
elif k in ('primary_key', 'pk'):
|
|
setattr(Attributes, 'primary_key', v)
|
|
Attributes.sqla_column_args[-1]['primary_key'] = v
|
|
_log_debug("setting primary_key=%r", v)
|
|
|
|
elif k in ('protocol_attrs', 'prot_attrs', 'pa'):
|
|
setattr(Attributes, 'prot_attrs', _decode_pa_dict(v))
|
|
_log_debug("setting prot_attrs=%r", v)
|
|
|
|
elif k in ('foreign_key', 'fk'):
|
|
from sqlalchemy.schema import ForeignKey
|
|
t, d = Attributes.sqla_column_args
|
|
fkt = (ForeignKey(v),)
|
|
new_v = (t + fkt, d)
|
|
Attributes.sqla_column_args = new_v
|
|
_log_debug("setting sqla_column_args=%r", new_v)
|
|
|
|
elif k in ('autoincrement', 'onupdate', 'server_default'):
|
|
Attributes.sqla_column_args[-1][k] = v
|
|
_log_debug("adding %s=%r to Attributes.sqla_column_args", k, v)
|
|
|
|
elif k == 'values_dict':
|
|
assert not 'values' in v, "`values` and `values_dict` can't be" \
|
|
"specified at the same time"
|
|
|
|
if not isinstance(v, dict):
|
|
# our odict has one nasty implicit behaviour: setitem on
|
|
# int keys is treated as array indexes, not dict keys. so
|
|
# dicts with int indexes can't work with odict. so we use
|
|
# the one from stdlib
|
|
v = OrderedDict(v)
|
|
|
|
Attributes.values = list(v.keys())
|
|
Attributes.values_dict = v
|
|
_log_debug("setting values=%r, values_dict=%r",
|
|
Attributes.values, Attributes.values_dict)
|
|
|
|
elif k == 'exc_table':
|
|
Attributes.exc_table = v
|
|
Attributes.exc_db = v
|
|
_log_debug("setting exc_table=%r, exc_db=%r", v, v)
|
|
|
|
elif k == 'max_occurs' and v in ('unbounded', 'inf', float('inf')):
|
|
new_v = decimal.Decimal('inf')
|
|
setattr(Attributes, k, new_v)
|
|
_log_debug("setting max_occurs=%r", new_v)
|
|
|
|
elif k == 'type_name':
|
|
Attributes._explicit_type_name = True
|
|
_log_debug("setting _explicit_type_name=True because "
|
|
"we have 'type_name'")
|
|
|
|
else:
|
|
setattr(Attributes, k, v)
|
|
_log_debug("setting %s=%r", k, v)
|
|
|
|
return (cls.__name__, (cls,), cls_dict)
|
|
|
|
@staticmethod
|
|
def validate_string(cls, value):
|
|
"""Override this method to do your own input validation on the input
|
|
string. This is called before converting the incoming string to the
|
|
native python value."""
|
|
|
|
return (cls.Attributes.nillable or value is not None)
|
|
|
|
@staticmethod
|
|
def validate_native(cls, value):
|
|
"""Override this method to do your own input validation on the native
|
|
value. This is called after converting the incoming string to the
|
|
native python value."""
|
|
|
|
return (cls.Attributes.nullable or value is not None)
|
|
|
|
|
|
class Null(ModelBase):
|
|
pass
|
|
|
|
|
|
class SimpleModelAttributesMeta(AttributesMeta):
|
|
def __init__(self, cls_name, cls_bases, cls_dict):
|
|
super(SimpleModelAttributesMeta, self).__init__(cls_name, cls_bases,
|
|
cls_dict)
|
|
if getattr(self, '_pattern', None) is None:
|
|
self._pattern = None
|
|
|
|
def get_pattern(self):
|
|
return self._pattern
|
|
|
|
def set_pattern(self, pattern):
|
|
self._pattern = pattern
|
|
if pattern is not None:
|
|
self._pattern_re = re.compile(pattern)
|
|
|
|
pattern = property(get_pattern, set_pattern)
|
|
|
|
def get_unicode_pattern(self):
|
|
return self._pattern
|
|
|
|
def set_unicode_pattern(self, pattern):
|
|
self._pattern = pattern
|
|
if pattern is not None:
|
|
self._pattern_re = re.compile(pattern, re.UNICODE)
|
|
|
|
unicode_pattern = property(get_unicode_pattern, set_unicode_pattern)
|
|
upattern = property(get_unicode_pattern, set_unicode_pattern)
|
|
|
|
|
|
class SimpleModel(ModelBase):
|
|
"""The base class for primitives."""
|
|
|
|
__namespace__ = "http://www.w3.org/2001/XMLSchema"
|
|
|
|
@six.add_metaclass(SimpleModelAttributesMeta)
|
|
class Attributes(ModelBase.Attributes):
|
|
"""The class that holds the constraints for the given type."""
|
|
|
|
values = set()
|
|
"""The set of possible values for this type."""
|
|
|
|
# some hacks are done in _s_customize to make `values_dict`
|
|
# behave like `values`
|
|
values_dict = dict()
|
|
"""The dict of possible values for this type. Dict keys are values and
|
|
dict values are either a single string or a translation dict."""
|
|
|
|
_pattern_re = None
|
|
|
|
def __new__(cls, **kwargs):
|
|
"""Overriden so that any attempt to instantiate a primitive will return
|
|
a customized class instead of an instance.
|
|
|
|
See spyne.model.base.ModelBase for more information.
|
|
"""
|
|
|
|
return cls.customize(**kwargs)
|
|
|
|
@classmethod
|
|
def customize(cls, **kwargs):
|
|
"""Duplicates cls and overwrites the values in ``cls.Attributes`` with
|
|
``**kwargs`` and returns the new class."""
|
|
|
|
cls_name, cls_bases, cls_dict = cls._s_customize(**kwargs)
|
|
|
|
retval = type(cls_name, cls_bases, cls_dict)
|
|
|
|
if not retval.is_default(retval):
|
|
retval.__extends__ = cls
|
|
retval.__type_name__ = kwargs.get("type_name", ModelBase.Empty)
|
|
if 'type_name' in kwargs:
|
|
logger.debug("Type name for %r was overridden as '%s'",
|
|
retval, retval.__type_name__)
|
|
|
|
retval.resolve_namespace(retval, kwargs.get('__namespace__'))
|
|
|
|
return retval
|
|
|
|
@staticmethod
|
|
def is_default(cls):
|
|
return (cls.Attributes.values == SimpleModel.Attributes.values)
|
|
|
|
@staticmethod
|
|
def validate_native(cls, value):
|
|
return (ModelBase.validate_native(cls, value)
|
|
and (
|
|
cls.Attributes.values is None or
|
|
len(cls.Attributes.values) == 0 or (
|
|
(value is None and cls.Attributes.nillable) or
|
|
(value is not None and value in cls.Attributes.values)
|
|
)
|
|
)
|
|
)
|
|
|
|
|
|
class PushBase(object):
|
|
def __init__(self, callback=None, errback=None):
|
|
self.orig_thread = threading.current_thread()
|
|
|
|
self._cb = callback
|
|
self._eb = errback
|
|
|
|
self.length = 0
|
|
self.ctx = None
|
|
self.app = None
|
|
self.gen = None
|
|
self._cb_finish = None
|
|
self._eb_finish = None
|
|
self.interim = False
|
|
|
|
def _init(self, ctx, gen, _cb_finish, _eb_finish, interim):
|
|
self.length = 0
|
|
|
|
self.ctx = ctx
|
|
self.app = ctx.app
|
|
|
|
self.gen = gen
|
|
|
|
self._cb_finish = _cb_finish
|
|
self._eb_finish = _eb_finish
|
|
|
|
self.interim = interim
|
|
|
|
def init(self, ctx, gen, _cb_finish, _eb_finish, interim):
|
|
self._init(ctx, gen, _cb_finish, _eb_finish, interim)
|
|
if self._cb is not None:
|
|
return self._cb(self)
|
|
|
|
def __len__(self):
|
|
return self.length
|
|
|
|
def append(self, inst):
|
|
self.gen.send(inst)
|
|
self.length += 1
|
|
|
|
def extend(self, insts):
|
|
for inst in insts:
|
|
self.gen.send(inst)
|
|
self.length += 1
|
|
|
|
def close(self):
|
|
try:
|
|
self.gen.throw(Break())
|
|
except (Break, StopIteration, GeneratorExit):
|
|
pass
|
|
self._cb_finish()
|
|
|
|
|
|
class xml:
|
|
"""Compound option object for xml serialization. It's meant to be passed to
|
|
:func:`ComplexModelBase.Attributes.store_as`.
|
|
|
|
:param root_tag: Root tag of the xml element that contains the field values.
|
|
:param no_ns: When true, the xml document is stripped from namespace
|
|
information. This is generally a stupid thing to do. Use with caution.
|
|
"""
|
|
|
|
def __init__(self, root_tag=None, no_ns=False, pretty_print=False):
|
|
self.root_tag = root_tag
|
|
self.no_ns = no_ns
|
|
self.pretty_print = pretty_print
|
|
|
|
|
|
class table:
|
|
"""Compound option object for for storing the class instance as in row in a
|
|
table in a relational database. It's meant to be passed to
|
|
:func:`ComplexModelBase.Attributes.store_as`.
|
|
|
|
:param multi: When False, configures a one-to-many relationship where the
|
|
child table has a foreign key to the parent. When not ``False``,
|
|
configures a many-to-many relationship by creating an intermediate
|
|
relation table that has foreign keys to both parent and child classes
|
|
and generates a table name automatically. When ``True``, the table name
|
|
is generated automatically. Otherwise, it should be a string, as the
|
|
value is used as the name of the intermediate table.
|
|
:param left: Name of the left join column.
|
|
:param right: Name of the right join column.
|
|
:param backref: See https://docs.sqlalchemy.org/en/13/orm/relationship_api.html?highlight=lazy#sqlalchemy.orm.relationship.params.backref
|
|
:param cascade: https://docs.sqlalchemy.org/en/13/orm/relationship_api.html?highlight=lazy#sqlalchemy.orm.relationship.params.cascade
|
|
:param lazy: See https://docs.sqlalchemy.org/en/13/orm/relationship_api.html?highlight=lazy#sqlalchemy.orm.relationship.params.lazy
|
|
:param back_populates: See https://docs.sqlalchemy.org/en/13/orm/relationship_api.html?highlight=lazy#sqlalchemy.orm.relationship.params.back_populates
|
|
"""
|
|
|
|
def __init__(self, multi=False, left=None, right=None, backref=None,
|
|
id_backref=None, cascade=False, lazy='select', back_populates=None,
|
|
fk_left_deferrable=None, fk_left_initially=None,
|
|
fk_right_deferrable=None, fk_right_initially=None,
|
|
fk_left_ondelete=None, fk_left_onupdate=None,
|
|
fk_right_ondelete=None, fk_right_onupdate=None,
|
|
explicit_join=False, order_by=False, single_parent=None):
|
|
self.multi = multi
|
|
self.left = left
|
|
self.right = right
|
|
self.backref = backref
|
|
self.id_backref = id_backref
|
|
self.cascade = cascade
|
|
self.lazy = lazy
|
|
self.back_populates = back_populates
|
|
self.fk_left_deferrable = fk_left_deferrable
|
|
self.fk_left_initially = fk_left_initially
|
|
self.fk_right_deferrable = fk_right_deferrable
|
|
self.fk_right_initially = fk_right_initially
|
|
self.fk_left_ondelete = fk_left_ondelete
|
|
self.fk_left_onupdate = fk_left_onupdate
|
|
self.fk_right_ondelete = fk_right_ondelete
|
|
self.fk_right_onupdate = fk_right_onupdate
|
|
self.explicit_join = explicit_join
|
|
self.order_by = order_by
|
|
self.single_parent = single_parent
|
|
|
|
|
|
class json:
|
|
"""Compound option object for json serialization. It's meant to be passed to
|
|
:func:`ComplexModelBase.Attributes.store_as`.
|
|
|
|
Make sure you don't mix this with the json package when importing.
|
|
"""
|
|
|
|
def __init__(self, ignore_wrappers=True, complex_as=dict):
|
|
if ignore_wrappers != True:
|
|
raise NotImplementedError("ignore_wrappers != True")
|
|
self.ignore_wrappers = ignore_wrappers
|
|
self.complex_as = complex_as
|
|
|
|
|
|
class jsonb:
|
|
"""Compound option object for jsonb serialization. It's meant to be passed
|
|
to :func:`ComplexModelBase.Attributes.store_as`.
|
|
"""
|
|
|
|
def __init__(self, ignore_wrappers=True, complex_as=dict):
|
|
if ignore_wrappers != True:
|
|
raise NotImplementedError("ignore_wrappers != True")
|
|
self.ignore_wrappers = ignore_wrappers
|
|
self.complex_as = complex_as
|
|
|
|
|
|
class msgpack:
|
|
"""Compound option object for msgpack serialization. It's meant to be passed
|
|
to :func:`ComplexModelBase.Attributes.store_as`.
|
|
|
|
Make sure you don't mix this with the msgpack package when importing.
|
|
"""
|
|
def __init__(self):
|
|
pass
|
|
|
|
|
|
PSSM_VALUES = {'json': json, 'jsonb': jsonb, 'xml': xml,
|
|
'msgpack': msgpack, 'table': table}
|
|
|
|
|
|
def apply_pssm(val):
|
|
if val is not None:
|
|
val_c = PSSM_VALUES.get(val, None)
|
|
if val_c is None:
|
|
assert isinstance(val, tuple(PSSM_VALUES.values())), \
|
|
"'store_as' should be one of: %r or an instance of %r not %r" \
|
|
% (tuple(PSSM_VALUES.keys()), tuple(PSSM_VALUES.values()), val)
|
|
|
|
return val
|
|
return val_c()
|