gchecky.gxml

Package gchecky :: Module gxml |
[hide private]
[frames] | no frames]

# Source Code for Module gchecky.gxml

      1  """
      2  Gchecky.gxml module provides an abstraction layer when dealing with Google
      3  Checkout API services (GC API). It translates XML messages into human-friendly python
      4  structures and vice versa.
      5
      6  In practice it means that when you have recieved
      7  a notification message from GC API, and you want to understand what's in that
      8  XML message, you simply pass it to gchecky and it parses (and automatically
      9  validates it for you) the XML text into python objects - instances of the class
     10  corresponding to the message type. Then that object is passed to your hook
     11  method along with extracted google order_id.
     12
     13  For example when an <order-state-change /> XML message is send to you by GC API
     14  gchecky will call on_order_state_change passing it an instance of
     15  C{gchecky.gxml.order_state_change_t} along with google order_id.
     16
     17  This is very convenient since you don't have to manipulate xml text, or xml DOM
     18  tree, neither do you have to validate the recieved message - it is already done
     19  by gchecky.
     20
     21  See L{gchecky.controller} module for information on how to provide hooks
     22  to the controller or customize it.
     23
     24  @cvar GOOGLE_CHECKOUT_API_XML_SCHEMA: the google checkout API messages xml
     25      schema location (more correctly it is the XML namesoace identificator for
     26      elements of the XML messages for google checkout API services).
     27
     28  @author: etarassov
     29  @version: $Revision: 126 $
     30  @contact: gchecky at gmail
     31  """
     32
     33  GOOGLE_CHECKOUT_API_XML_SCHEMA = 'http://checkout.google.com/schema/2'
     34
     35 -class Field(object):
     36      """Holds all the meta-information about mapping the current field value into/from
     37      the xml DOM tree.
     38
     39      An instance of the class specifies the exact path to the DOM
     40      node/subnode/attribute that contains the field value. It also holds other
     41      field traits such as:
     42        @ivar required: required or optional
     43        @ivar empty: weither the field value could be empty (an empty XML tag)
     44        @ivar values: the list of acceptable values
     45        @ivar default: the default value for the field
     46        @ivar path: the path to the xml DOM node/attribute to store the field data
     47        @ivar save_node_and_xml: a boolean that specifies if the original xml
     48                    and DOM element should be saved. Handly for fields that could
     49                    contain arbitrary data such as 'merchant-private-data' and
     50                    'merchant-private-item-data'.
     51                    The original xml text is saved into <field>_xml.
     52                    The corresponding DOM node is stored into <field>_dom.
     53      """
     54      path     = ''
     55      required = True
     56      empty    = False
     57      default  = None
     58      values   = None
     59      save_node_and_xml = False
     60
     61      @classmethod
     62 -    def deconstruct_path(cls, path):
     63          """Deconstruct a path string into pieces suitable for xml DOM processing.
     64              @param path: a string in the form of /chunk1/chunk2/.../chunk_n/@attribute.
     65                  It denotes a DOM node or an attibute which holds this fields value.
     66                  This corresponds to an hierarchy of type::
     67                    chunk1
     68                     \- chunk2
     69                        ...
     70                         \- chunk_n
     71                             \- @attribute
     72                  Where chunk_n are DOM nodes and @attribute is a DOM attribute.
     73
     74                  Chunks and @attribute are optional.
     75
     76                  An empty string denotes the current DOM node.
     77              @return: C{(chunks, attribute)} - a list of chunks and attribute
     78              value (or None).
     79              @see: L{reconstruct_path}"""
     80          chunks = [chunk for chunk in path.split('/') if len(chunk)]
     81          attribute = None
     82          if chunks and chunks[-1][:1] == '@':
     83              attribute = chunks.pop()[1:]
     84          import re
     85          xml_name = re.compile(r'^[a-zA-Z\_][a-zA-Z0-9\-\_]*$') # to be fixed
     86          assert attribute is None or xml_name.match(attribute)
     87          assert 0 == len([True for chunk in chunks
     88                           if xml_name.match(chunk) is None])
     89          return chunks, attribute
     90
     91      @classmethod
     92 -    def reconstruct_path(cls, chunks, attribute):
     93          """Reconstruct the path back into the original form using the deconstructed form.
     94             A class method.
     95
     96              @param chunks: a list of DOM sub-nodes.
     97              @param attribute: a DOM attribute.
     98              @return: a string path denoting the DOM node/attribute which should contain
     99                  the field value.
    100              @see: L{deconstruct_path}"""
    101          return '%s%s%s' % ('/'.join(chunks),
    102                             attribute and '@' or '',
    103                             attribute or '')
    104
    105 -    def __init__(self, path, **kwargs):
    106          """Specify initial parameters for this field instance. The list of
    107          actual parameters depends on the subclass.
    108          @param path: The path determines the DOM node/attribute to be used
    109          to store/retrieve the field data value. It will be directly passed to
    110          L{deconstruct_path}."""
    111          for pname, pvalue in kwargs.items():
    112              setattr(self, pname, pvalue)
    113          if path is None:
    114              raise Exception('Path is a required parameter')
    115          self.path = path
    116          self.path_nodes, self.path_attribute = Field.deconstruct_path(path)
    117
    118 -    def get_initial_value(self):
    119          if self.required:
    120              if self.default is not None:
    121                  return self.default
    122              elif self.values and len(self.values) > 0:
    123                  return self.values[0]
    124          return None
    125
    126 -    def save(self, node, data):
    127          """Save the field data value into the DOM node. The value is stored
    128          accordingly to the field path which could be the DOM node itself or
    129          its subnodes (which will be automatically created), or (a sub)node
    130          attribute.
    131          @param node: The DOM node which (or subnodes of which) will contain
    132              the field data value.
    133          @param data: The data value for the field to be stored.
    134          """
    135          str = self.data2str(data)
    136          if self.path_attribute is not None:
    137              node.setAttribute(self.path_attribute, str)
    138          else:
    139              if str is not None:
    140                  node.appendChild(node.ownerDocument.createTextNode(str))
    141
    142 -    def load(self, node):
    143          """Load the field data from the xml DOM node. The value is retrieved
    144          accordingly to the field path and  other traits.
    145            @param node: The xml NODE that (or subnodes or attribute of which)
    146            contains the field data value.
    147            @see L{save}, L{__init__}"""
    148          if self.path_attribute is not None:
    149              if not node.hasAttribute(self.path_attribute):
    150                  return None
    151              str = node.getAttribute(self.path_attribute)
    152          else:
    153              if node.nodeType == node.TEXT_NODE or node.nodeType == node.CDATA_SECTION_NODE:
    154                  str = node.data
    155              else:
    156                  str = ''.join([el.data for el in node.childNodes
    157                                 if (el.nodeType == node.TEXT_NODE
    158                                     or el.nodeType == node.CDATA_SECTION_NODE)])
    159          return self.str2data(str)
    160
    161 -    def validate(self, data):
    162          """
    163          Validate data according to this fields parameters.
    164
    165          @return True if data is ok, otherwise return a string (!) describing
    166                  why the data is invalid.
    167
    168          Note that this method returns either True or an error string, not False!
    169
    170          The Field class considers any data as valid and returns True.
    171          """
    172          return True
    173
    174 -    def data2str(self, data):
    175          """Override this method in subclasses"""
    176          raise Exception('Abstract method of %s' % self.__class__)
    177
    178 -    def str2data(self, str):
    179          """Override this method in subclasses"""
    180          raise Exception('Abstract method of %s' % self.__class__)
    181
    182 -    def create_node_for_path(self, parent, reuse_nodes=True):
    183          """Create (if needed) a XML DOM node that will hold this field data.
    184          @param parent: The parent node that should hold this fields data.
    185          @param reuse_nodes: Reuse the existing required node if it is already present.
    186          @return: Return the XML DOM node to hold this field's data. The node
    187            created as a subnode (or an attribute, or a grand-node, etc.) of
    188            parent.
    189          """
    190          for nname in self.path_nodes:
    191              # Should we reuse an existing node?
    192              if reuse_nodes:
    193                  nodes = parent.getElementsByTagName(nname)
    194                  if nodes.length == 1:
    195                      parent = nodes[0]
    196                      continue
    197              node = parent.ownerDocument.createElement(nname)
    198              parent.appendChild(node)
    199              parent = node
    200          return parent
    201
    202 -    def get_nodes_for_path(self, parent):
    203          """Retrieve all the nodes that hold data supposed to be assigned to this
    204          field. If this field path matches a subnode (or a 'grand' subnode, or
    205          an atribute, etc) of the 'parent' node, then it is included in
    206          the returned list.
    207          @param parent: The node to scan for this field data occurences.
    208          @return: The list of nodes that corresponds to this field."""
    209          elements = [parent]
    210          for nname in self.path_nodes:
    211              els = []
    212              for el in elements:
    213                  children = el.childNodes
    214                  for i in range(0, children.length):
    215                      item = children.item(i)
    216                      if item.nodeType == item.ELEMENT_NODE:
    217                          if item.tagName == nname:
    218                              els.append(item)
    219              elements = els
    220          return elements
    221
    222 -    def get_one_node_for_path(self, parent):
    223          """Same as 'get_nodes_path' but checks that there is exactly one result
    224          and returns it."""
    225          els = self.get_nodes_for_path(parent)
    226          if len(els) != 1:
    227              raise Exception('Multiple nodes where exactly one is expected %s' % (self.path_nodes,))
    228          return els[0]
    229
    230 -    def get_any_node_for_path(self, parent):
    231          """Same as 'get_nodes_path' but checks that there is no more than one
    232          result and returns it, or None if the list is empty."""
    233          els = self.get_nodes_for_path(parent)
    234          if len(els) > 1:
    235              raise Exception('Multiple nodes where at most one is expected %s' % (self.path_nodes,))
    236          if len(els) == 0:
    237              return None
    238          return els[0]
    239
    240 -    def _traits(self):
    241          """Return the string representing the field traits.
    242          @see: L{__repr__}"""
    243          str = ':PATH(%s)' % (Field.reconstruct_path(self.path_nodes,
    244                                                      self.path_attribute),)
    245          str += ':%s' % (self.required and 'REQ' or 'OPT',)
    246          if self.empty:
    247              str += ':EMPTY'
    248          if self.default:
    249              str += ':DEF(%s)' % (self.default,)
    250          if self.values:
    251              str += ':VALS("%s")' % ('","'.join(self.values),)
    252          return str
    253
    254 -    def __repr__(self):
    255          """Used in documentation. This method is called from subclasses
    256          __repr__ method to generate a human-readable description of the current
    257          field instance.
    258          """
    259          return '%s%s' % (self.__class__.__name__,
    260                           self._traits())
    261
    262 -class NodeManager(type):
    263      """The class keeps track of all the subclasses of C{Node} class.
    264
    265      It retrieves a C{Node} fields and provides this information to the class.
    266
    267      This class represents a hook on-Node-subclass-creation where 'creation'
    268      means the moment the class is first loaded. It allows dynamically do some
    269      stuff on class load. It could also be done statically but that way we avoid
    270      code and effort duplication, which is quite nice. :-)
    271
    272      @cvar nodes: The dictionary C{class_name S{rarr} class} keeps all the Node
    273      subclasses.
    274      """
    275      nodes = {}
    276 -    def __new__(cls, name, bases, attrs):
    277          """Dynamically do some stuff on a Node subclass 'creation'.
    278
    279          Specifically do the following:
    280            - create the class (via the standard type.__new__)
    281            - retrieve all the fields of the class (its own and inherited)
    282            - store the class reference in the L{nodes} dictionary
    283            - give the class itself the access to its field list
    284          """
    285          clazz = type.__new__(cls, name, bases, attrs)
    286          NodeManager.nodes[name] = clazz
    287          fields = {}
    288          for base in bases:
    289              if hasattr(base, 'fields'):
    290                  fields.update(base.fields())
    291          for fname, field in attrs.items():
    292              if isinstance(field, Field):
    293                  fields[fname] = field
    294          clazz.set_fields(fields)
    295          return clazz
    296
    297 -class Node(object):
    298      """The base class for any class which represents data that could be mapped
    299      into XML DOM structure.
    300
    301      This class provides some basic functionality and lets programmer avoid
    302      repetetive tasks by automating it.
    303
    304      @cvar _fields: list of meta-Fields of this class.
    305      @see: NodeManager
    306      """
    307      __metaclass__ = NodeManager
    308      _fields = {}
    309      @classmethod
    310 -    def set_fields(cls, fields):
    311          """Method is called by L{NodeManager} to specify this class L{Field}s
    312          set."""
    313          cls._fields = fields
    314
    315      @classmethod
    316 -    def fields(cls):
    317          """Return all fields of this class (and its bases)"""
    318          return cls._fields
    319
    320 -    def __new__(cls, **kwargs):
    321          """Creates a new instance of the class and initializes fields to
    322          suitable values. Note that for every meta-C{Field} found in the class
    323          itself, the instance will have a field initialized to the default value
    324          specified in the meta-L{Field}, or one of the L{Field} allowed values,
    325          or C{None}."""
    326          instance = object.__new__(cls)
    327          for fname, field in cls.fields().items():
    328              setattr(instance, fname, field.get_initial_value())
    329          return instance
    330
    331 -    def __init__(self, **kwargs):
    332          """Directly initialize the instance with
    333          values::
    334            price = price_t(value = 10, currency = 'USD')
    335          is equivalent to (and preferred over)::
    336            price = price_t()
    337            price.value = 10
    338            price.currency = 'USD'
    339          """
    340          for name, value in kwargs.items():
    341              setattr(self, name, value)
    342
    343 -    def write(self, node):
    344          """Store the L{Node} into an xml DOM node."""
    345          for fname, field in self.fields().items():
    346              data = getattr(self, fname, None)
    347              if data is None:
    348                  if field.required: raise Exception('Field <%s> is required, but data for it is None' % (fname,))
    349                  continue
    350              if (data != '' or not field.empty) and field.validate(data) != True:
    351                  raise Exception("Invalid data for <%s>: '%s'. Reason: %s" % (fname, data, field.validate(data)))
    352              field.save(field.create_node_for_path(node), data)
    353
    354 -    def read(self, node):
    355          """Load a L{Node} from an xml DOM node."""
    356          for fname, field in self.fields().items():
    357              try:
    358                  fnode = field.get_any_node_for_path(node)
    359
    360                  if fnode is None:
    361                      data = None
    362                  else:
    363                      data = field.load(fnode)
    364
    365                  if field.save_node_and_xml:
    366                      # Store the original DOM node
    367                      setattr(self, '%s_dom' % (fname,), fnode)
    368                      # Store the original XML text
    369                      xml_fragment = ''
    370                      if fnode is not None:
    371                          xml_fragment = fnode.toxml()
    372                      setattr(self, '%s_xml' % (fname,), xml_fragment)
    373
    374                  if data is None:
    375                      if field.required:
    376                          raise Exception('Field <%s> is required, but data for it is None' % (fname,))
    377                  elif data == '':
    378                      if field.required and not field.empty:
    379                          raise Exception('Field <%s> can not be empty, but data for it is ""' % (fname,))
    380                  else:
    381                      if field.validate(data) != True:
    382                          raise Exception("Invalid data for <%s>: '%s'. Reason: %s" % (fname, data, field.validate(data)))
    383                  setattr(self, fname, data)
    384              except Exception, exc:
    385                  raise Exception('%s\n%s' % ('While reading %s' % (fname,), exc))
    386
    387 -    def __eq__(self, other):
    388          if not isinstance(other, Node):
    389              return False
    390
    391          for field in self.fields():
    392              if not(hasattr(self, field) == hasattr(other, field)):
    393                  return False
    394              if hasattr(self, field) and not(getattr(self, field) == getattr(other, field)):
    395                  return False
    396          return True
    397 -    def __neq__(self, other):
    398          return not(self == other)
    399
    400 -class DocumentManager(NodeManager):
    401      """Keeps track of all the L{Document} subclasses. Similar to L{NodeManager}
    402      automates tasks needed to be donefor every L{Document} subclass.
    403
    404      The main purpose is to keep the list of all the classes and theirs
    405      correspongin xml tag names so that when an XML message is recieved it could
    406      be possible automatically determine the right L{Document} subclass
    407      the message corresponds to (and parse the message using the found
    408      document-class).
    409
    410      @cvar documents: The dictionary of all the documents."""
    411      documents = {}
    412
    413 -    def __new__(cls, name, bases, attrs):
    414          """Do some stuff for every created Document subclass."""
    415          clazz = NodeManager.__new__(cls, name, bases, attrs)
    416          DocumentManager.register_class(clazz, clazz.tag_name)
    417          return clazz
    418
    419      @classmethod
    420 -    def register_class(self, clazz, tag_name):
    421          """Register the L{Document} subclass."""
    422          if tag_name is None:
    423              raise Exception('Document %s has to have tag_name attribute' % (clazz,))
    424          self.documents[tag_name] = clazz
    425
    426      @classmethod
    427 -    def get_class(self, tag_name):
    428          """@return: the class by its xml tag name or raises an exception if
    429          no class was found for the tag name."""
    430          if not DocumentManager.documents.has_key(tag_name):
    431              raise Exception('There are no Document with tag_name(%s)' % (tag_name,))
    432          return self.documents[tag_name]
    433
    434 -class Document(Node):
    435      """A L{Node} which could be stored as a standalone xml document.
    436      Every L{Document} subclass has its own xml tag_name so that it could be
    437      automatically stored into/loaded from an XML document.
    438
    439      @ivar tag_name: The document's unique xml tag name."""
    440      __metaclass__ = DocumentManager
    441      tag_name = 'unknown'
    442
    443 -    def toxml(self, pretty=False):
    444          """@return: A string for the XML document representing the Document
    445          instance."""
    446          from xml.dom.minidom import getDOMImplementation
    447          dom_impl = getDOMImplementation()
    448
    449          tag_name = self.__class__.tag_name
    450          doc      = dom_impl.createDocument(GOOGLE_CHECKOUT_API_XML_SCHEMA,
    451                                             tag_name,
    452                                             None)
    453
    454          # TODO Fix this namespace problem that xml.dom.minidom has -- it does
    455          #      render the default namespace declaration for the newly created
    456          #      (not parsed) document. As a workaround we parse a dummy text
    457          #      with the wanted NS declaration and then fill it up with data.
    458          from xml.dom.minidom import parseString
    459          dummy_xml = '<?xml version="1.0"?><%s xmlns="%s"/>' % (tag_name,
    460                                                                 GOOGLE_CHECKOUT_API_XML_SCHEMA)
    461          doc = parseString(dummy_xml)
    462
    463          self.write(doc.documentElement)
    464
    465          if pretty:
    466              return doc.toprettyxml((pretty is True and '  ') or pretty)
    467          return doc.toxml()
    468
    469 -    def __str__(self):
    470          try:
    471              return self.toxml()
    472          except Exception:
    473              pass
    474          return self.__repr__()
    475
    476      @classmethod
    477 -    def fromxml(self, text):
    478          """Read the text (as an XML document) into a Document (or subclass)
    479          instance.
    480          @return: A fresh-new instance of a Document (of the right subclas
    481          determined by the xml document tag name)."""
    482          from xml.dom.minidom import parseString
    483          doc = parseString(text)
    484          root = doc.documentElement
    485          clazz = DocumentManager.get_class(root.tagName)
    486          instance = clazz()
    487          instance.read(root)
    488          return instance
    489
    490 -class List(Field):
    491      """The field describes a homogene list of values which could be stored
    492      as a set of XML nodes with the same tag names.
    493
    494      An example - list of strings which should be stored as
    495      <messages> <message />* </messages>?::
    496        class ...:
    497            ...
    498            messages = gxml.List('/messages', gxml.String('/message'), required=False)
    499
    500      @cvar list_item: a L{Field} instance describing this list items."""
    501      list_item = None
    502
    503      # TODO required => default=[]
    504 -    def __init__(self, path, list_item, empty_is_none=True, **kwargs):
    505          """Initializes the List instance.
    506          @param path: L{Field.path}
    507          @param list_item: a meta-L{Field} instance describing the list items
    508          @param empty_is_none: If True then when loading data an empty list []
    509                                would be treated as None value. True by default.
    510          """
    511          Field.__init__(self, path, **kwargs)
    512          if self.path_attribute is not None:
    513              raise Exception('List type %s cannot be written into an attribute %s' % (self.__class__, self.path_attribute))
    514          if list_item is None or not isinstance(list_item, Field):
    515              raise Exception('List item (%s) has to be a Field instance' % (list_item,))
    516          self.list_item = list_item
    517          self.empty_is_none = empty_is_none
    518
    519 -    def validate(self, data):
    520          """Checks that the data is a valid sequence."""
    521          from operator import isSequenceType
    522          if not isSequenceType(data):
    523              return "List data has to be a sequence."
    524          return True
    525
    526 -    def save(self, node, data):
    527          """Store the data list in a DOM node.
    528          @param node: the xml DOM node to hold the list
    529          @param data: a list of items to be stored"""
    530          # node = self.list_item.create_node_for_path(node)
    531          for item_data in data:
    532              if item_data is None:
    533                  if self.list_item.required: raise Exception('Required data is None')
    534                  continue
    535              item_validity = self.list_item.validate(item_data)
    536              if item_validity != True:
    537                  raise Exception("List contains an invalid value '%s': %s" % (item_data,
    538                                                                               item_validity))
    539              # reuse_nodes=False ensure that list items generate different nodes.
    540              inode = self.list_item.create_node_for_path(node, reuse_nodes=False)
    541              self.list_item.save(inode, item_data)
    542
    543 -    def load(self, node):
    544          """Load the list from the xml DOM node.
    545          @param node: the xml DOM node containing the list.
    546          @return: a list of items."""
    547          data = []
    548          for inode in self.list_item.get_nodes_for_path(node):
    549              if inode is None:
    550                  if self.list_item.required: raise Exception('Required data is None')
    551                  data.append(None)
    552              else:
    553                  idata = self.list_item.load(inode)
    554                  item_validity = self.list_item.validate(idata)
    555                  if item_validity != True:
    556                      raise Exception("List item can not have value '%s': %s" % (idata,
    557                                                                                 item_validity))
    558                  data.append(idata)
    559          if data == [] and (self.empty_is_none and not self.required):
    560              return None
    561          return data
    562
    563 -    def __repr__(self):
    564          """Override L{Field.__repr__} for documentation purposes"""
    565          return 'List%s:[\n    %s\n]' % (self._traits(),
    566                                          self.list_item.__repr__())
    567
    568 -class Complex(Field):
    569      """Represents a field which is not a simple POD but a complex data
    570      structure.
    571      An example - a price in USD::
    572          price = gxml.Complex('/unit-price', gxml.price_t)
    573      @cvar clazz: The class meta-L{Field} instance describing this field data.
    574      """
    575      clazz = None
    576
    577 -    def __init__(self, path, clazz, **kwargs):
    578          """Initialize the Complex instance.
    579          @param path: L{Field.path}
    580          @param clazz: a Node subclass descibing the field data values."""
    581          if not issubclass(clazz, Node):
    582              raise Exception('Complex type %s has to inherit from Node' % (clazz,))
    583          Field.__init__(self, path, clazz=clazz, **kwargs)
    584          if self.path_attribute is not None:
    585              raise Exception('Complex type %s cannot be written into an attribute %s' % (self.__class__, self.path_attribute))
    586
    587 -    def validate(self, data):
    588          """Checks if the data is an instance of the L{clazz}."""
    589          if not isinstance(data, self.clazz):
    590              return "Data(%s) is not of class %s" % (data, self.clazz)
    591          return True
    592
    593 -    def save(self, node, data):
    594          """Store the data as a complex structure."""
    595          data.write(node)
    596
    597 -    def load(self, node):
    598          """Load the complex data from an xml DOM node."""
    599          instance = self.clazz()
    600          instance.read(node)
    601          return instance
    602
    603 -    def __repr__(self):
    604          """Override L{Field.__repr__} for documentation purposes."""
    605          return 'Node%s:{ %s }' % (self._traits(), self.clazz.__name__)
    606
    607 -class String(Field):
    608      """
    609      A field representing a string value.
    610      """
    611 -    def __init__(self, path, max_length=None, empty=True, **kwargs):
    612          return super(String, self).__init__(path,
    613                                              max_length=max_length,
    614                                              empty=empty,
    615                                              **kwargs)
    616 -    def data2str(self, data):
    617          return str(data)
    618 -    def str2data(self, text):
    619          return text
    620 -    def validate(self, data):
    621          if (self.max_length != None) and len(str(data)) >= self.max_length:
    622              return "The string is too long (max_length=%d)." % (self.max_length,)
    623          return True
    624
    625 -def apply_parent_validation(clazz, error_prefix=None):
    626      """
    627      Decorator to automatically invoke parent class validation before applying
    628      custom validation rules. Usage::
    629
    630          class Child(Parent):
    631              @apply_parent_validation(Child, error_prefix="From Child: ")
    632              def validate(data):
    633                  # I can assume now that the parent validation method succeeded.
    634                  # ...
    635      """
    636      def decorator(func):
    637          def inner(self, data):
    638              base_validation = clazz.validate(self, data)
    639              if base_validation != True:
    640                  if error_prefix is not None:
    641                      return error_prefix + base_validation
    642                  return base_validation
    643              return func(self, data)
    644          return inner
    645      return decorator
    646
    647 -class Pattern(String):
    648      """A string matching a pattern.
    649      @ivar pattern: a regular expression to which a value has to confirm."""
    650      pattern = None
    651 -    def __init__(self, path, pattern, **kwargs):
    652          """
    653          Initizlizes a Pattern field.
    654          @param path: L{Field.path}
    655          @param pattern: a regular expression describing the format of the data
    656          """
    657          return super(Pattern, self).__init__(path=path, pattern=pattern, **kwargs)
    658
    659      @apply_parent_validation(String)
    660 -    def validate(self, data):
    661          """Checks if the pattern matches the data."""
    662          if self.pattern.match(data) is None:
    663              return "Does not matches the defined pattern"
    664          return True
    665
    666 -class Decimal(Field):
    667      default=0
    668 -    def data2str(self, data):
    669          return '%d' % data
    670 -    def str2data(self, text):
    671          return int(text)
    672
    673 -class Double(Field):
    674      """Floating point value"""
    675 -    def __init__(self, path, precision=3, **kwargs):
    676          """
    677          @param precision: Precision of the value
    678          """
    679          return super(Double, self).__init__(path=path, precision=precision, **kwargs)
    680 -    def data2str(self, data):
    681          return ('%%.%df' % (self.precision,)) % (data,)
    682 -    def str2data(self, text):
    683          return float(text)
    684
    685 -class Boolean(Field):
    686      values = (True, False)
    687 -    def data2str(self, data):
    688          return (data and 'true') or 'false'
    689 -    def str2data(self, text):
    690          if text == 'true':
    691              return True
    692          if text == 'false':
    693              return False
    694          return 'invalid'
    695
    696 -class Long(Field):
    697      default=0
    698 -    def data2str(self, data):
    699          return '%d' % (data,)
    700 -    def str2data(self, text):
    701          return long(text)
    702
    703 -class Integer(Long):
    704      pass
    705
    706 -class Url(Pattern):
    707      """
    708      Note: a 'http://localhost/' does not considered to be a valid url.
    709      So any other alias name that you migght use in your local network
    710      (and defined in your /etc/hosts file) could possibly be considered
    711      invalid.
    712
    713      >>> u = Url('dummy')
    714      >>> u.validate('http://google.com')
    715      True
    716      >>> u.validate('https://google.com')
    717      True
    718      >>> u.validate('http://google.com/')
    719      True
    720      >>> u.validate('http://google.com/some')
    721      True
    722      >>> u.validate('http://google.com/some/more')
    723      True
    724      >>> u.validate('http://google.com/even///more/')
    725      True
    726      >>> u.validate('http://google.com/url/?with=some&args')
    727      True
    728      >>> u.validate('http://google.com/empty/args/?')
    729      True
    730      >>> u.validate('http://google.com/some/;-)?a+b=c&&=11')
    731      True
    732      >>> u.validate('http:/google.com') != True
    733      True
    734      >>> u.validate('mailto://google.com') != True
    735      True
    736      >>> u.validate('http://.google.com') != True
    737      True
    738      >>> u.validate('http://google..com') != True
    739      True
    740      >>> u.validate('http://;-).google.com') != True
    741      True
    742      >>> u.validate('https://sandbox.google.com/checkout/view/buy?o=shoppingcart&shoppingcart=515556794648982')
    743      True
    744      >>> u.validate('http://127.0.0.1:8000/digital/order/continue/')
    745      True
    746      """
    747 -    def __init__(self, path, **kwargs):
    748          import re
    749          # Regular expression divided into chunks:
    750          protocol  = r'((http(s?)|ftp)\:\/\/|~/|/)?'
    751          user_pass = r'([\w]+:\w+@)?'
    752          domain    = r'(([a-zA-Z]{1}([\w\-]+\.)+([\w]{2,5}))|(([0-9]+\.){3}[0-9]+))'
    753          port      = r'(:[\d]{1,5})?'
    754          file    = r'(/[\w\.\+-;\(\)]*)*'
    755          params    = r'(\?.*)?'
    756          pattern = re.compile('^' + protocol + user_pass + domain + port + file + params + '$')
    757          Pattern.__init__(self, path, pattern=pattern, **kwargs)
    758
    759      @apply_parent_validation(Pattern, error_prefix="Url: ")
    760 -    def validate(self, data):
    761          return True
    762
    763 -class Email(Pattern):
    764 -    def __init__(self, path, **kwargs):
    765          import re
    766          pattern = re.compile(r'^[a-zA-Z0-9\.\_\%\-\+]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
    767          Pattern.__init__(self, path, pattern=pattern, **kwargs)
    768
    769      @apply_parent_validation(Pattern, error_prefix="Email: ")
    770 -    def validate(self, data):
    771          return True
    772
    773 -class Html(String):
    774      pass
    775
    776 -class LanguageCode(Pattern):
    777 -    def __init__(self, path):
    778          return super(LanguageCode, self).__init__(path=path, pattern='^en_US$')
    779
    780 -class Phone(Pattern):
    781 -    def __init__(self, path, **kwargs):
    782          import re
    783          pattern = re.compile(r'^[0-9\+\-\(\)\ ]+$')
    784          Pattern.__init__(self, path, pattern=pattern, **kwargs)
    785
    786      @apply_parent_validation(Pattern, error_prefix="Phone: ")
    787 -    def validate(self, data):
    788          return True
    789
    790 -class Zip(Pattern):
    791      """
    792      Represents a zip code.
    793
    794      >>> zip = Zip('dummy')
    795      >>> zip.validate('94043')
    796      True
    797      >>> zip.validate('abCD123')
    798      True
    799      >>> zip.validate('123*') != True
    800      True
    801      >>> zip.validate('E6 1EX')
    802      True
    803
    804      >>> zip_pattern = Zip('dummy', complete=False)
    805      >>> zip_pattern.validate('SW*')
    806      True
    807      """
    808 -    def __init__(self, path, complete=True, **kwargs):
    809          import re
    810          if complete:
    811              pattern = re.compile(r'^[0-9a-zA-Z- ]+$')
    812          else:
    813              pattern = re.compile(r'^[0-9a-zA-Z- \*]+$')
    814          Pattern.__init__(self, path, pattern=pattern, **kwargs)
    815
    816      @apply_parent_validation(Pattern, error_prefix="Zip: ")
    817 -    def validate(self, data):
    818          return True
    819
    820 -class IP(Pattern):
    821      """
    822      Represents an IP address.
    823
    824      Currently only IPv4 addresses in decimal notation are accepted.
    825
    826      >>> ip = IP('dummy')
    827      >>> ip.validate('127.0.0.1')
    828      True
    829      >>> ip.validate('10.0.0.1')
    830      True
    831      >>> ip.validate('255.17.101.199')
    832      True
    833      >>> ip.validate('1.1.1.1')
    834      True
    835      >>> ip.validate('1.2.3') != True
    836      True
    837      >>> ip.validate('1.2.3.4.5') != True
    838      True
    839      >>> ip.validate('1.2.3.256') != True
    840      True
    841      >>> ip.validate('1.2.3.-1') != True
    842      True
    843      >>> ip.validate('1.2..3') != True
    844      True
    845      >>> ip.validate('.1.2.3.4') != True
    846      True
    847      >>> ip.validate('1.2.3.4.') != True
    848      True
    849      >>> ip.validate('1.2.3.-') != True
    850      True
    851      >>> ip.validate('01.2.3.4') != True
    852      True
    853      >>> ip.validate('1.02.3.4') != True
    854      True
    855      """
    856 -    def __init__(self, path, **kwargs):
    857          import re
    858          num_pattern = r'(0|([1-9][0-9]?)|(1[0-9]{2})|(2((5[0-5])|([0-4][0-9]))))'
    859          pattern = re.compile(r'^%s\.%s\.%s\.%s$' % (num_pattern,num_pattern,num_pattern,num_pattern))
    860          Pattern.__init__(self, path, pattern=pattern, **kwargs)
    861
    862      @apply_parent_validation(Pattern, error_prefix="IP address: ")
    863 -    def validate(self, data):
    864          return True
    865
    866  # TODO
    867 -class ID(String):
    868      empty = False
    869
    870      @apply_parent_validation(String, error_prefix="ID: ")
    871 -    def validate(self, data):
    872          if len(data) == 0:
    873              return "ID has to be non-empty"
    874          return True
    875
    876 -class Any(Field):
    877      """Any text value. This field is tricky. Since any data could be stored in
    878      the field we can't handle all the cases.
    879      The class uses xml.marshal.generic to convert python-ic simple data
    880      structures into xml. By simple we mean any POD. Note that a class derived
    881      from object requires the marshaller to be extended that's why this field
    882      does not accept instance of such classes.
    883      When reading XML we consider node XML text as if it was previously
    884      generated by a xml marshaller facility (xml.marshal.generic.dumps).
    885      If it fails then we consider the data as if it was produced by some other
    886      external source and return False indicating that user Controller should
    887      parse the XML data itself. In such case field value is False.
    888      To access the original XML input two class member variables are populated:
    889      - <field>_xml contains the original XML text
    890      - <field>_dom contains the corresponding XML DOM node
    891      """
    892 -    def __init__(self, *args, **kwargs):
    893          obj = super(Any, self).__init__(*args, **kwargs)
    894          if self.path_attribute is not None:
    895              raise ValueError('gxml.Any field cannot be bound to an attribute!')
    896          return obj
    897
    898 -    def save(self, node, data):
    899          from gchecky.tools import encoder
    900          return encoder().serialize(data, node)
    901
    902 -    def load(self, node):
    903          from gchecky.tools import decoder
    904          return decoder().deserialize(node)
    905
    906 -    def validate(self, data):
    907          # Always return True, since any data is allowed
    908          return True
    909
    910  #class DateTime(Field):
    911  #    from datetime import datetime
    912  #    def validate(self, data):
    913  #        return isinstance(data, datetime)
    914  #    def data2str(self, data):
    915  #        pass
    916
    917 -class Timestamp(Field):
    918 -    def validate(self, data):
    919          from datetime import datetime
    920          if not isinstance(data, datetime):
    921              return "Timestamp has to be an instance of datetime.datetime"
    922          return True
    923 -    def data2str(self, data):
    924          return data.isoformat()
    925 -    def str2data(self, text):
    926          import iso8601
    927          return iso8601.parse_date(text)
    928
    929  if __name__ == "__main__":
    930 -    def run_doctests():
    931          import doctest
    932          doctest.testmod()
    933      run_doctests()
    934