"""
Gchecky.gxml module provides an abstraction layer when dealing with Google
Checkout API services (GC API). It translates XML messages into human-friendly python
structures and vice versa.
In practice it means that when you have recieved
a notification message from GC API, and you want to understand what's in that
XML message, you simply pass it to gchecky and it parses (and automatically
validates it for you) the XML text into python objects - instances of the class
corresponding to the message type. Then that object is passed to your hook
method along with extracted google order_id.
For example when an XML message is send to you by GC API
gchecky will call on_order_state_change passing it an instance of
C{gchecky.gxml.order_state_change_t} along with google order_id.
This is very convenient since you don't have to manipulate xml text, or xml DOM
tree, neither do you have to validate the recieved message - it is already done
by gchecky.
See L{gchecky.controller} module for information on how to provide hooks
to the controller or customize it.
@cvar GOOGLE_CHECKOUT_API_XML_SCHEMA: the google checkout API messages xml
schema location (more correctly it is the XML namesoace identificator for
elements of the XML messages for google checkout API services).
@author: etarassov
@version: $Revision$
@contact: gchecky at gmail
"""
GOOGLE_CHECKOUT_API_XML_SCHEMA = 'http://checkout.google.com/schema/2'
class Field(object):
"""Holds all the meta-information about mapping the current field value into/from
the xml DOM tree.
An instance of the class specifies the exact path to the DOM
node/subnode/attribute that contains the field value. It also holds other
field traits such as:
@ivar required: required or optional
@ivar empty: weither the field value could be empty (an empty XML tag)
@ivar values: the list of acceptable values
@ivar default: the default value for the field
@ivar path: the path to the xml DOM node/attribute to store the field data
@ivar save_node_and_xml: a boolean that specifies if the original xml
and DOM element should be saved. Handly for fields that could
contain arbitrary data such as 'merchant-private-data' and
'merchant-private-item-data'.
The original xml text is saved into _xml.
The corresponding DOM node is stored into _dom.
"""
path = ''
required = True
empty = False
default = None
values = None
save_node_and_xml = False
@classmethod
def deconstruct_path(cls, path):
"""Deconstruct a path string into pieces suitable for xml DOM processing.
@param path: a string in the form of /chunk1/chunk2/.../chunk_n/@attribute.
It denotes a DOM node or an attibute which holds this fields value.
This corresponds to an hierarchy of type::
chunk1
\- chunk2
...
\- chunk_n
\- @attribute
Where chunk_n are DOM nodes and @attribute is a DOM attribute.
Chunks and @attribute are optional.
An empty string denotes the current DOM node.
@return: C{(chunks, attribute)} - a list of chunks and attribute
value (or None).
@see: L{reconstruct_path}"""
chunks = [chunk for chunk in path.split('/') if len(chunk)]
attribute = None
if chunks and chunks[-1][:1] == '@':
attribute = chunks.pop()[1:]
import re
xml_name = re.compile(r'^[a-zA-Z\_][a-zA-Z0-9\-\_]*$') # to be fixed
assert attribute is None or xml_name.match(attribute)
assert 0 == len([True for chunk in chunks
if xml_name.match(chunk) is None])
return chunks, attribute
@classmethod
def reconstruct_path(cls, chunks, attribute):
"""Reconstruct the path back into the original form using the deconstructed form.
A class method.
@param chunks: a list of DOM sub-nodes.
@param attribute: a DOM attribute.
@return: a string path denoting the DOM node/attribute which should contain
the field value.
@see: L{deconstruct_path}"""
return '%s%s%s' % ('/'.join(chunks),
attribute and '@' or '',
attribute or '')
def __init__(self, path, **kwargs):
"""Specify initial parameters for this field instance. The list of
actual parameters depends on the subclass.
@param path: The path determines the DOM node/attribute to be used
to store/retrieve the field data value. It will be directly passed to
L{deconstruct_path}."""
for pname, pvalue in kwargs.items():
setattr(self, pname, pvalue)
if path is None:
raise Exception('Path is a required parameter')
self.path = path
self.path_nodes, self.path_attribute = Field.deconstruct_path(path)
def get_initial_value(self):
if self.required:
if self.default is not None:
return self.default
elif self.values and len(self.values) > 0:
return self.values[0]
return None
def save(self, node, data):
"""Save the field data value into the DOM node. The value is stored
accordingly to the field path which could be the DOM node itself or
its subnodes (which will be automatically created), or (a sub)node
attribute.
@param node: The DOM node which (or subnodes of which) will contain
the field data value.
@param data: The data value for the field to be stored.
"""
str = self.data2str(data)
if self.path_attribute is not None:
node.setAttribute(self.path_attribute, str)
else:
if str is not None:
node.appendChild(node.ownerDocument.createTextNode(str))
def load(self, node):
"""Load the field data from the xml DOM node. The value is retrieved
accordingly to the field path and other traits.
@param node: The xml NODE that (or subnodes or attribute of which)
contains the field data value.
@see L{save}, L{__init__}"""
if self.path_attribute is not None:
if not node.hasAttribute(self.path_attribute):
return None
str = node.getAttribute(self.path_attribute)
else:
if node.nodeType == node.TEXT_NODE or node.nodeType == node.CDATA_SECTION_NODE:
str = node.data
else:
str = ''.join([el.data for el in node.childNodes
if (el.nodeType == node.TEXT_NODE
or el.nodeType == node.CDATA_SECTION_NODE)])
return self.str2data(str)
def validate(self, data):
"""
Validate data according to this fields parameters.
@return True if data is ok, otherwise return a string (!) describing
why the data is invalid.
Note that this method returns either True or an error string, not False!
The Field class considers any data as valid and returns True.
"""
return True
def data2str(self, data):
"""Override this method in subclasses"""
raise Exception('Abstract method of %s' % self.__class__)
def str2data(self, str):
"""Override this method in subclasses"""
raise Exception('Abstract method of %s' % self.__class__)
def create_node_for_path(self, parent, reuse_nodes=True):
"""Create (if needed) a XML DOM node that will hold this field data.
@param parent: The parent node that should hold this fields data.
@param reuse_nodes: Reuse the existing required node if it is already present.
@return: Return the XML DOM node to hold this field's data. The node
created as a subnode (or an attribute, or a grand-node, etc.) of
parent.
"""
for nname in self.path_nodes:
# Should we reuse an existing node?
if reuse_nodes:
nodes = parent.getElementsByTagName(nname)
if nodes.length == 1:
parent = nodes[0]
continue
node = parent.ownerDocument.createElement(nname)
parent.appendChild(node)
parent = node
return parent
def get_nodes_for_path(self, parent):
"""Retrieve all the nodes that hold data supposed to be assigned to this
field. If this field path matches a subnode (or a 'grand' subnode, or
an atribute, etc) of the 'parent' node, then it is included in
the returned list.
@param parent: The node to scan for this field data occurences.
@return: The list of nodes that corresponds to this field."""
elements = [parent]
for nname in self.path_nodes:
els = []
for el in elements:
children = el.childNodes
for i in range(0, children.length):
item = children.item(i)
if item.nodeType == item.ELEMENT_NODE:
if item.tagName == nname:
els.append(item)
elements = els
return elements
def get_one_node_for_path(self, parent):
"""Same as 'get_nodes_path' but checks that there is exactly one result
and returns it."""
els = self.get_nodes_for_path(parent)
if len(els) != 1:
raise Exception('Multiple nodes where exactly one is expected %s' % (self.path_nodes,))
return els[0]
def get_any_node_for_path(self, parent):
"""Same as 'get_nodes_path' but checks that there is no more than one
result and returns it, or None if the list is empty."""
els = self.get_nodes_for_path(parent)
if len(els) > 1:
raise Exception('Multiple nodes where at most one is expected %s' % (self.path_nodes,))
if len(els) == 0:
return None
return els[0]
def _traits(self):
"""Return the string representing the field traits.
@see: L{__repr__}"""
str = ':PATH(%s)' % (Field.reconstruct_path(self.path_nodes,
self.path_attribute),)
str += ':%s' % (self.required and 'REQ' or 'OPT',)
if self.empty:
str += ':EMPTY'
if self.default:
str += ':DEF(%s)' % (self.default,)
if self.values:
str += ':VALS("%s")' % ('","'.join(self.values),)
return str
def __repr__(self):
"""Used in documentation. This method is called from subclasses
__repr__ method to generate a human-readable description of the current
field instance.
"""
return '%s%s' % (self.__class__.__name__,
self._traits())
class NodeManager(type):
"""The class keeps track of all the subclasses of C{Node} class.
It retrieves a C{Node} fields and provides this information to the class.
This class represents a hook on-Node-subclass-creation where 'creation'
means the moment the class is first loaded. It allows dynamically do some
stuff on class load. It could also be done statically but that way we avoid
code and effort duplication, which is quite nice. :-)
@cvar nodes: The dictionary C{class_name S{rarr} class} keeps all the Node
subclasses.
"""
nodes = {}
def __new__(cls, name, bases, attrs):
"""Dynamically do some stuff on a Node subclass 'creation'.
Specifically do the following:
- create the class (via the standard type.__new__)
- retrieve all the fields of the class (its own and inherited)
- store the class reference in the L{nodes} dictionary
- give the class itself the access to its field list
"""
clazz = type.__new__(cls, name, bases, attrs)
NodeManager.nodes[name] = clazz
fields = {}
for base in bases:
if hasattr(base, 'fields'):
fields.update(base.fields())
for fname, field in attrs.items():
if isinstance(field, Field):
fields[fname] = field
clazz.set_fields(fields)
return clazz
class Node(object):
"""The base class for any class which represents data that could be mapped
into XML DOM structure.
This class provides some basic functionality and lets programmer avoid
repetetive tasks by automating it.
@cvar _fields: list of meta-Fields of this class.
@see: NodeManager
"""
__metaclass__ = NodeManager
_fields = {}
@classmethod
def set_fields(cls, fields):
"""Method is called by L{NodeManager} to specify this class L{Field}s
set."""
cls._fields = fields
@classmethod
def fields(cls):
"""Return all fields of this class (and its bases)"""
return cls._fields
def __new__(cls, **kwargs):
"""Creates a new instance of the class and initializes fields to
suitable values. Note that for every meta-C{Field} found in the class
itself, the instance will have a field initialized to the default value
specified in the meta-L{Field}, or one of the L{Field} allowed values,
or C{None}."""
instance = object.__new__(cls)
for fname, field in cls.fields().items():
setattr(instance, fname, field.get_initial_value())
return instance
def __init__(self, **kwargs):
"""Directly initialize the instance with
values::
price = price_t(value = 10, currency = 'USD')
is equivalent to (and preferred over)::
price = price_t()
price.value = 10
price.currency = 'USD'
"""
for name, value in kwargs.items():
setattr(self, name, value)
def write(self, node):
"""Store the L{Node} into an xml DOM node."""
for fname, field in self.fields().items():
data = getattr(self, fname, None)
if data is None:
if field.required: raise Exception('Field <%s> is required, but data for it is None' % (fname,))
continue
if (data != '' or not field.empty) and field.validate(data) != True:
raise Exception("Invalid data for <%s>: '%s'. Reason: %s" % (fname, data, field.validate(data)))
field.save(field.create_node_for_path(node), data)
def read(self, node):
"""Load a L{Node} from an xml DOM node."""
for fname, field in self.fields().items():
try:
fnode = field.get_any_node_for_path(node)
if fnode is None:
data = None
else:
data = field.load(fnode)
if field.save_node_and_xml:
# Store the original DOM node
setattr(self, '%s_dom' % (fname,), fnode)
# Store the original XML text
xml_fragment = ''
if fnode is not None:
xml_fragment = fnode.toxml()
setattr(self, '%s_xml' % (fname,), xml_fragment)
if data is None:
if field.required:
raise Exception('Field <%s> is required, but data for it is None' % (fname,))
elif data == '':
if field.required and not field.empty:
raise Exception('Field <%s> can not be empty, but data for it is ""' % (fname,))
else:
if field.validate(data) != True:
raise Exception("Invalid data for <%s>: '%s'. Reason: %s" % (fname, data, field.validate(data)))
setattr(self, fname, data)
except Exception, exc:
raise Exception('%s\n%s' % ('While reading %s' % (fname,), exc))
def __eq__(self, other):
if not isinstance(other, Node):
return False
for field in self.fields():
if not(hasattr(self, field) == hasattr(other, field)):
return False
if hasattr(self, field) and not(getattr(self, field) == getattr(other, field)):
return False
return True
def __neq__(self, other):
return not(self == other)
class DocumentManager(NodeManager):
"""Keeps track of all the L{Document} subclasses. Similar to L{NodeManager}
automates tasks needed to be donefor every L{Document} subclass.
The main purpose is to keep the list of all the classes and theirs
correspongin xml tag names so that when an XML message is recieved it could
be possible automatically determine the right L{Document} subclass
the message corresponds to (and parse the message using the found
document-class).
@cvar documents: The dictionary of all the documents."""
documents = {}
def __new__(cls, name, bases, attrs):
"""Do some stuff for every created Document subclass."""
clazz = NodeManager.__new__(cls, name, bases, attrs)
DocumentManager.register_class(clazz, clazz.tag_name)
return clazz
@classmethod
def register_class(self, clazz, tag_name):
"""Register the L{Document} subclass."""
if tag_name is None:
raise Exception('Document %s has to have tag_name attribute' % (clazz,))
self.documents[tag_name] = clazz
@classmethod
def get_class(self, tag_name):
"""@return: the class by its xml tag name or raises an exception if
no class was found for the tag name."""
if not DocumentManager.documents.has_key(tag_name):
raise Exception('There are no Document with tag_name(%s)' % (tag_name,))
return self.documents[tag_name]
class Document(Node):
"""A L{Node} which could be stored as a standalone xml document.
Every L{Document} subclass has its own xml tag_name so that it could be
automatically stored into/loaded from an XML document.
@ivar tag_name: The document's unique xml tag name."""
__metaclass__ = DocumentManager
tag_name = 'unknown'
def toxml(self, pretty=False):
"""@return: A string for the XML document representing the Document
instance."""
from xml.dom.minidom import getDOMImplementation
dom_impl = getDOMImplementation()
tag_name = self.__class__.tag_name
doc = dom_impl.createDocument(GOOGLE_CHECKOUT_API_XML_SCHEMA,
tag_name,
None)
# TODO Fix this namespace problem that xml.dom.minidom has -- it does
# render the default namespace declaration for the newly created
# (not parsed) document. As a workaround we parse a dummy text
# with the wanted NS declaration and then fill it up with data.
from xml.dom.minidom import parseString
dummy_xml = '<%s xmlns="%s"/>' % (tag_name,
GOOGLE_CHECKOUT_API_XML_SCHEMA)
doc = parseString(dummy_xml)
self.write(doc.documentElement)
if pretty:
return doc.toprettyxml((pretty is True and ' ') or pretty)
return doc.toxml()
def __str__(self):
try:
return self.toxml()
except Exception:
pass
return self.__repr__()
@classmethod
def fromxml(self, text):
"""Read the text (as an XML document) into a Document (or subclass)
instance.
@return: A fresh-new instance of a Document (of the right subclas
determined by the xml document tag name)."""
from xml.dom.minidom import parseString
doc = parseString(text)
root = doc.documentElement
clazz = DocumentManager.get_class(root.tagName)
instance = clazz()
instance.read(root)
return instance
class List(Field):
"""The field describes a homogene list of values which could be stored
as a set of XML nodes with the same tag names.
An example - list of strings which should be stored as
* ?::
class ...:
...
messages = gxml.List('/messages', gxml.String('/message'), required=False)
@cvar list_item: a L{Field} instance describing this list items."""
list_item = None
# TODO required => default=[]
def __init__(self, path, list_item, empty_is_none=True, **kwargs):
"""Initializes the List instance.
@param path: L{Field.path}
@param list_item: a meta-L{Field} instance describing the list items
@param empty_is_none: If True then when loading data an empty list []
would be treated as None value. True by default.
"""
Field.__init__(self, path, **kwargs)
if self.path_attribute is not None:
raise Exception('List type %s cannot be written into an attribute %s' % (self.__class__, self.path_attribute))
if list_item is None or not isinstance(list_item, Field):
raise Exception('List item (%s) has to be a Field instance' % (list_item,))
self.list_item = list_item
self.empty_is_none = empty_is_none
def validate(self, data):
"""Checks that the data is a valid sequence."""
from operator import isSequenceType
if not isSequenceType(data):
return "List data has to be a sequence."
return True
def save(self, node, data):
"""Store the data list in a DOM node.
@param node: the xml DOM node to hold the list
@param data: a list of items to be stored"""
# node = self.list_item.create_node_for_path(node)
for item_data in data:
if item_data is None:
if self.list_item.required: raise Exception('Required data is None')
continue
item_validity = self.list_item.validate(item_data)
if item_validity != True:
raise Exception("List contains an invalid value '%s': %s" % (item_data,
item_validity))
# reuse_nodes=False ensure that list items generate different nodes.
inode = self.list_item.create_node_for_path(node, reuse_nodes=False)
self.list_item.save(inode, item_data)
def load(self, node):
"""Load the list from the xml DOM node.
@param node: the xml DOM node containing the list.
@return: a list of items."""
data = []
for inode in self.list_item.get_nodes_for_path(node):
if inode is None:
if self.list_item.required: raise Exception('Required data is None')
data.append(None)
else:
idata = self.list_item.load(inode)
item_validity = self.list_item.validate(idata)
if item_validity != True:
raise Exception("List item can not have value '%s': %s" % (idata,
item_validity))
data.append(idata)
if data == [] and (self.empty_is_none and not self.required):
return None
return data
def __repr__(self):
"""Override L{Field.__repr__} for documentation purposes"""
return 'List%s:[\n %s\n]' % (self._traits(),
self.list_item.__repr__())
class Complex(Field):
"""Represents a field which is not a simple POD but a complex data
structure.
An example - a price in USD::
price = gxml.Complex('/unit-price', gxml.price_t)
@cvar clazz: The class meta-L{Field} instance describing this field data.
"""
clazz = None
def __init__(self, path, clazz, **kwargs):
"""Initialize the Complex instance.
@param path: L{Field.path}
@param clazz: a Node subclass descibing the field data values."""
if not issubclass(clazz, Node):
raise Exception('Complex type %s has to inherit from Node' % (clazz,))
Field.__init__(self, path, clazz=clazz, **kwargs)
if self.path_attribute is not None:
raise Exception('Complex type %s cannot be written into an attribute %s' % (self.__class__, self.path_attribute))
def validate(self, data):
"""Checks if the data is an instance of the L{clazz}."""
if not isinstance(data, self.clazz):
return "Data(%s) is not of class %s" % (data, self.clazz)
return True
def save(self, node, data):
"""Store the data as a complex structure."""
data.write(node)
def load(self, node):
"""Load the complex data from an xml DOM node."""
instance = self.clazz()
instance.read(node)
return instance
def __repr__(self):
"""Override L{Field.__repr__} for documentation purposes."""
return 'Node%s:{ %s }' % (self._traits(), self.clazz.__name__)
class String(Field):
"""
A field representing a string value.
"""
def __init__(self, path, max_length=None, empty=True, **kwargs):
return super(String, self).__init__(path,
max_length=max_length,
empty=empty,
**kwargs)
def data2str(self, data):
return str(data)
def str2data(self, text):
return text
def validate(self, data):
if (self.max_length != None) and len(str(data)) >= self.max_length:
return "The string is too long (max_length=%d)." % (self.max_length,)
return True
def apply_parent_validation(clazz, error_prefix=None):
"""
Decorator to automatically invoke parent class validation before applying
custom validation rules. Usage::
class Child(Parent):
@apply_parent_validation(Child, error_prefix="From Child: ")
def validate(data):
# I can assume now that the parent validation method succeeded.
# ...
"""
def decorator(func):
def inner(self, data):
base_validation = clazz.validate(self, data)
if base_validation != True:
if error_prefix is not None:
return error_prefix + base_validation
return base_validation
return func(self, data)
return inner
return decorator
class Pattern(String):
"""A string matching a pattern.
@ivar pattern: a regular expression to which a value has to confirm."""
pattern = None
def __init__(self, path, pattern, **kwargs):
"""
Initizlizes a Pattern field.
@param path: L{Field.path}
@param pattern: a regular expression describing the format of the data
"""
return super(Pattern, self).__init__(path=path, pattern=pattern, **kwargs)
@apply_parent_validation(String)
def validate(self, data):
"""Checks if the pattern matches the data."""
if self.pattern.match(data) is None:
return "Does not matches the defined pattern"
return True
class Decimal(Field):
default=0
def data2str(self, data):
return '%d' % data
def str2data(self, text):
return int(text)
class Double(Field):
"""Floating point value"""
def __init__(self, path, precision=3, **kwargs):
"""
@param precision: Precision of the value
"""
return super(Double, self).__init__(path=path, precision=precision, **kwargs)
def data2str(self, data):
return ('%%.%df' % (self.precision,)) % (data,)
def str2data(self, text):
return float(text)
class Boolean(Field):
values = (True, False)
def data2str(self, data):
return (data and 'true') or 'false'
def str2data(self, text):
if text == 'true':
return True
if text == 'false':
return False
return 'invalid'
class Long(Field):
default=0
def data2str(self, data):
return '%d' % (data,)
def str2data(self, text):
return long(text)
class Integer(Long):
pass
class Url(Pattern):
"""
Note: a 'http://localhost/' does not considered to be a valid url.
So any other alias name that you migght use in your local network
(and defined in your /etc/hosts file) could possibly be considered
invalid.
>>> u = Url('dummy')
>>> u.validate('http://google.com')
True
>>> u.validate('https://google.com')
True
>>> u.validate('http://google.com/')
True
>>> u.validate('http://google.com/some')
True
>>> u.validate('http://google.com/some/more')
True
>>> u.validate('http://google.com/even///more/')
True
>>> u.validate('http://google.com/url/?with=some&args')
True
>>> u.validate('http://google.com/empty/args/?')
True
>>> u.validate('http://google.com/some/;-)?a+b=c&&=11')
True
>>> u.validate('http:/google.com') != True
True
>>> u.validate('mailto://google.com') != True
True
>>> u.validate('http://.google.com') != True
True
>>> u.validate('http://google..com') != True
True
>>> u.validate('http://;-).google.com') != True
True
>>> u.validate('https://sandbox.google.com/checkout/view/buy?o=shoppingcart&shoppingcart=515556794648982')
True
>>> u.validate('http://127.0.0.1:8000/digital/order/continue/')
True
"""
def __init__(self, path, **kwargs):
import re
# Regular expression divided into chunks:
protocol = r'((http(s?)|ftp)\:\/\/|~/|/)?'
user_pass = r'([\w]+:\w+@)?'
domain = r'(([a-zA-Z]{1}([\w\-]+\.)+([\w]{2,5}))|(([0-9]+\.){3}[0-9]+))'
port = r'(:[\d]{1,5})?'
file = r'(/[\w\.\+-;\(\)]*)*'
params = r'(\?.*)?'
pattern = re.compile('^' + protocol + user_pass + domain + port + file + params + '$')
Pattern.__init__(self, path, pattern=pattern, **kwargs)
@apply_parent_validation(Pattern, error_prefix="Url: ")
def validate(self, data):
return True
class Email(Pattern):
def __init__(self, path, **kwargs):
import re
pattern = re.compile(r'^[a-zA-Z0-9\.\_\%\-\+]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
Pattern.__init__(self, path, pattern=pattern, **kwargs)
@apply_parent_validation(Pattern, error_prefix="Email: ")
def validate(self, data):
return True
class Html(String):
pass
class LanguageCode(Pattern):
def __init__(self, path):
return super(LanguageCode, self).__init__(path=path, pattern='^en_US$')
class Phone(Pattern):
def __init__(self, path, **kwargs):
import re
pattern = re.compile(r'^[0-9\+\-\(\)\ ]+$')
Pattern.__init__(self, path, pattern=pattern, **kwargs)
@apply_parent_validation(Pattern, error_prefix="Phone: ")
def validate(self, data):
return True
class Zip(Pattern):
"""
Represents a zip code.
>>> zip = Zip('dummy')
>>> zip.validate('94043')
True
>>> zip.validate('abCD123')
True
>>> zip.validate('123*') != True
True
>>> zip.validate('E6 1EX')
True
>>> zip_pattern = Zip('dummy', complete=False)
>>> zip_pattern.validate('SW*')
True
"""
def __init__(self, path, complete=True, **kwargs):
import re
if complete:
pattern = re.compile(r'^[0-9a-zA-Z- ]+$')
else:
pattern = re.compile(r'^[0-9a-zA-Z- \*]+$')
Pattern.__init__(self, path, pattern=pattern, **kwargs)
@apply_parent_validation(Pattern, error_prefix="Zip: ")
def validate(self, data):
return True
class IP(Pattern):
"""
Represents an IP address.
Currently only IPv4 addresses in decimal notation are accepted.
>>> ip = IP('dummy')
>>> ip.validate('127.0.0.1')
True
>>> ip.validate('10.0.0.1')
True
>>> ip.validate('255.17.101.199')
True
>>> ip.validate('1.1.1.1')
True
>>> ip.validate('1.2.3') != True
True
>>> ip.validate('1.2.3.4.5') != True
True
>>> ip.validate('1.2.3.256') != True
True
>>> ip.validate('1.2.3.-1') != True
True
>>> ip.validate('1.2..3') != True
True
>>> ip.validate('.1.2.3.4') != True
True
>>> ip.validate('1.2.3.4.') != True
True
>>> ip.validate('1.2.3.-') != True
True
>>> ip.validate('01.2.3.4') != True
True
>>> ip.validate('1.02.3.4') != True
True
"""
def __init__(self, path, **kwargs):
import re
num_pattern = r'(0|([1-9][0-9]?)|(1[0-9]{2})|(2((5[0-5])|([0-4][0-9]))))'
pattern = re.compile(r'^%s\.%s\.%s\.%s$' % (num_pattern,num_pattern,num_pattern,num_pattern))
Pattern.__init__(self, path, pattern=pattern, **kwargs)
@apply_parent_validation(Pattern, error_prefix="IP address: ")
def validate(self, data):
return True
# TODO
class ID(String):
empty = False
@apply_parent_validation(String, error_prefix="ID: ")
def validate(self, data):
if len(data) == 0:
return "ID has to be non-empty"
return True
class Any(Field):
"""Any text value. This field is tricky. Since any data could be stored in
the field we can't handle all the cases.
The class uses xml.marshal.generic to convert python-ic simple data
structures into xml. By simple we mean any POD. Note that a class derived
from object requires the marshaller to be extended that's why this field
does not accept instance of such classes.
When reading XML we consider node XML text as if it was previously
generated by a xml marshaller facility (xml.marshal.generic.dumps).
If it fails then we consider the data as if it was produced by some other
external source and return False indicating that user Controller should
parse the XML data itself. In such case field value is False.
To access the original XML input two class member variables are populated:
- _xml contains the original XML text
- _dom contains the corresponding XML DOM node
"""
def __init__(self, *args, **kwargs):
obj = super(Any, self).__init__(*args, **kwargs)
if self.path_attribute is not None:
raise ValueError('gxml.Any field cannot be bound to an attribute!')
return obj
def save(self, node, data):
from gchecky.tools import encoder
return encoder().serialize(data, node)
def load(self, node):
from gchecky.tools import decoder
return decoder().deserialize(node)
def validate(self, data):
# Always return True, since any data is allowed
return True
#class DateTime(Field):
# from datetime import datetime
# def validate(self, data):
# return isinstance(data, datetime)
# def data2str(self, data):
# pass
class Timestamp(Field):
def validate(self, data):
from datetime import datetime
if not isinstance(data, datetime):
return "Timestamp has to be an instance of datetime.datetime"
return True
def data2str(self, data):
return data.isoformat()
def str2data(self, text):
import iso8601
return iso8601.parse_date(text)
if __name__ == "__main__":
def run_doctests():
import doctest
doctest.testmod()
run_doctests()