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: 102 $
     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
    398 -class DocumentManager(NodeManager):
    399      """Keeps track of all the L{Document} subclasses. Similar to L{NodeManager}
    400      automates tasks needed to be donefor every L{Document} subclass.
    401
    402      The main purpose is to keep the list of all the classes and theirs
    403      correspongin xml tag names so that when an XML message is recieved it could
    404      be possible automatically determine the right L{Document} subclass
    405      the message corresponds to (and parse the message using the found
    406      document-class).
    407
    408      @cvar documents: The dictionary of all the documents."""
    409      documents = {}
    410
    411 -    def __new__(cls, name, bases, attrs):
    412          """Do some stuff for every created Document subclass."""
    413          clazz = NodeManager.__new__(cls, name, bases, attrs)
    414          DocumentManager.register_class(clazz, clazz.tag_name)
    415          return clazz
    416
    417      @classmethod
    418 -    def register_class(self, clazz, tag_name):
    419          """Register the L{Document} subclass."""
    420          if tag_name is None:
    421              raise Exception('Document %s has to have tag_name attribute' % (clazz,))
    422          self.documents[tag_name] = clazz
    423
    424      @classmethod
    425 -    def get_class(self, tag_name):
    426          """@return: the class by its xml tag name or raises an exception if
    427          no class was found for the tag name."""
    428          if not DocumentManager.documents.has_key(tag_name):
    429              raise Exception('There are no Document with tag_name(%s)' % (tag_name,))
    430          return self.documents[tag_name]
    431
    432 -class Document(Node):
    433      """A L{Node} which could be stored as a standalone xml document.
    434      Every L{Document} subclass has its own xml tag_name so that it could be
    435      automatically stored into/loaded from an XML document.
    436
    437      @ivar tag_name: The document's unique xml tag name."""
    438      __metaclass__ = DocumentManager
    439      tag_name = 'unknown'
    440
    441 -    def toxml(self, pretty=False):
    442          """@return: A string for the XML document representing the Document
    443          instance."""
    444          from xml.dom.minidom import getDOMImplementation
    445          dom_impl = getDOMImplementation()
    446
    447          tag_name = self.__class__.tag_name
    448          doc      = dom_impl.createDocument(GOOGLE_CHECKOUT_API_XML_SCHEMA,
    449                                             tag_name,
    450                                             None)
    451
    452          # TODO Fix this namespace problem that xml.dom.minidom has -- it does
    453          #      render the default namespace declaration for the newly created
    454          #      (not parsed) document. As a workaround we parse a dummy text
    455          #      with the wanted NS declaration and then fill it up with data.
    456          from xml.dom.minidom import parseString
    457          dummy_xml = '<?xml version="1.0"?><%s xmlns="%s"/>' % (tag_name,
    458                                                                 GOOGLE_CHECKOUT_API_XML_SCHEMA)
    459          doc = parseString(dummy_xml)
    460
    461          self.write(doc.documentElement)
    462
    463          if pretty:
    464              return doc.toprettyxml((pretty is True and '  ') or pretty)
    465          return doc.toxml()
    466
    467 -    def __str__(self):
    468          try:
    469              return self.toxml()
    470          except Exception:
    471              pass
    472          return self.__repr__()
    473
    474      @classmethod
    475 -    def fromxml(self, text):
    476          """Read the text (as an XML document) into a Document (or subclass)
    477          instance.
    478          @return: A fresh-new instance of a Document (of the right subclas
    479          determined by the xml document tag name)."""
    480          from xml.dom.minidom import parseString
    481          doc = parseString(text)
    482          root = doc.documentElement
    483          clazz = DocumentManager.get_class(root.tagName)
    484          instance = clazz()
    485          instance.read(root)
    486          return instance
    487
    488 -class List(Field):
    489      """The field describes a homogene list of values which could be stored
    490      as a set of XML nodes with the same tag names.
    491
    492      An example - list of strings which should be stored as
    493      <messages> <message />* </messages>?::
    494        class ...:
    495            ...
    496            messages = gxml.List('/messages', gxml.String('/message'), required=False)
    497
    498      @cvar list_item: a L{Field} instance describing this list items."""
    499      list_item = None
    500
    501      # TODO required => default=[]
    502 -    def __init__(self, path, list_item, empty_is_none=True, **kwargs):
    503          """Initializes the List instance.
    504          @param path: L{Field.path}
    505          @param list_item: a meta-L{Field} instance describing the list items
    506          @param empty_is_none: If True then when loading data an empty list []
    507                                would be treated as None value. True by default.
    508          """
    509          Field.__init__(self, path, **kwargs)
    510          if self.path_attribute is not None:
    511              raise Exception('List type %s cannot be written into an attribute %s' % (self.__class__, self.path_attribute))
    512          if list_item is None or not isinstance(list_item, Field):
    513              raise Exception('List item (%s) has to be a Field instance' % (list_item,))
    514          self.list_item = list_item
    515          self.empty_is_none = empty_is_none
    516
    517 -    def validate(self, data):
    518          """Checks that the data is a valid sequence."""
    519          from operator import isSequenceType
    520          if not isSequenceType(data):
    521              return "List data has to be a sequence."
    522          return True
    523
    524 -    def save(self, node, data):
    525          """Store the data list in a DOM node.
    526          @param node: the xml DOM node to hold the list
    527          @param data: a list of items to be stored"""
    528          # node = self.list_item.create_node_for_path(node)
    529          for item_data in data:
    530              if item_data is None:
    531                  if self.list_item.required: raise Exception('Required data is None')
    532                  continue
    533              item_validity = self.list_item.validate(item_data)
    534              if item_validity != True:
    535                  raise Exception("List contains an invalid value '%s': %s" % (item_data,
    536                                                                               item_validity))
    537              # reuse_nodes=False ensure that list items generate different nodes.
    538              inode = self.list_item.create_node_for_path(node, reuse_nodes=False)
    539              self.list_item.save(inode, item_data)
    540
    541 -    def load(self, node):
    542          """Load the list from the xml DOM node.
    543          @param node: the xml DOM node containing the list.
    544          @return: a list of items."""
    545          data = []
    546          for inode in self.list_item.get_nodes_for_path(node):
    547              if inode is None:
    548                  if self.list_item.required: raise Exception('Required data is None')
    549                  data.append(None)
    550              else:
    551                  idata = self.list_item.load(inode)
    552                  item_validity = self.list_item.validate(idata)
    553                  if item_validity != True:
    554                      raise Exception("List item can not have value '%s': %s" % (idata,
    555                                                                                 item_validity))
    556                  data.append(idata)
    557          if data == [] and (self.empty_is_none and not self.required):
    558              return None
    559          return data
    560
    561 -    def __repr__(self):
    562          """Override L{Field.__repr__} for documentation purposes"""
    563          return 'List%s:[\n    %s\n]' % (self._traits(),
    564                                          self.list_item.__repr__())
    565
    566 -class Complex(Field):
    567      """Represents a field which is not a simple POD but a complex data
    568      structure.
    569      An example - a price in USD::
    570          price = gxml.Complex('/unit-price', gxml.price_t)
    571      @cvar clazz: The class meta-L{Field} instance describing this field data.
    572      """
    573      clazz = None
    574
    575 -    def __init__(self, path, clazz, **kwargs):
    576          """Initialize the Complex instance.
    577          @param path: L{Field.path}
    578          @param clazz: a Node subclass descibing the field data values."""
    579          if not issubclass(clazz, Node):
    580              raise Exception('Complex type %s has to inherit from Node' % (clazz,))
    581          Field.__init__(self, path, clazz=clazz, **kwargs)
    582          if self.path_attribute is not None:
    583              raise Exception('Complex type %s cannot be written into an attribute %s' % (self.__class__, self.path_attribute))
    584
    585 -    def validate(self, data):
    586          """Checks if the data is an instance of the L{clazz}."""
    587          if not isinstance(data, self.clazz):
    588              return "Data(%s) is not of class %s" % (data, self.clazz)
    589          return True
    590
    591 -    def save(self, node, data):
    592          """Store the data as a complex structure."""
    593          data.write(node)
    594
    595 -    def load(self, node):
    596          """Load the complex data from an xml DOM node."""
    597          instance = self.clazz()
    598          instance.read(node)
    599          return instance
    600
    601 -    def __repr__(self):
    602          """Override L{Field.__repr__} for documentation purposes."""
    603          return 'Node%s:{ %s }' % (self._traits(), self.clazz.__name__)
    604
    605 -class String(Field):
    606      """Any text value."""
    607 -    def __init__(self, path, max_length=None, **kwargs):
    608          return super(String, self).__init__(path, max_length=max_length, **kwargs)
    609 -    def data2str(self, data):
    610          return str(data)
    611 -    def str2data(self, text):
    612          return text
    613 -    def validate(self, data):
    614          if (self.max_length != None) and len(str(data)) >= self.max_length:
    615              return "The string is too long (max_length=%d)." % (self.max_length,)
    616          return True
    617
    618 -def apply_parent_validation(clazz, error_prefix=None):
    619      """
    620      Decorator to automatically invoke parent class validation before applying
    621      custom validation rules. Usage::
    622
    623          class Child(Parent):
    624              @apply_parent_validation(Child, error_prefix="From Child: ")
    625              def validate(data):
    626                  # I can assume now that the parent validation method succeeded.
    627                  # ...
    628      """
    629      def decorator(func):
    630          def inner(self, data):
    631              base_validation = clazz.validate(self, data)
    632              if base_validation != True:
    633                  if error_prefix is not None:
    634                      return error_prefix + base_validation
    635                  return base_validation
    636              return func(self, data)
    637          return inner
    638      return decorator
    639
    640 -class Pattern(String):
    641      """A string matching a pattern.
    642      @ivar pattern: a regular expression to which a value has to confirm."""
    643      pattern = None
    644 -    def __init__(self, path, pattern, **kwargs):
    645          """
    646          Initizlizes a Pattern field.
    647          @param path: L{Field.path}
    648          @param pattern: a regular expression describing the format of the data
    649          """
    650          return super(Pattern, self).__init__(path=path, pattern=pattern, **kwargs)
    651
    652      @apply_parent_validation(String)
    653 -    def validate(self, data):
    654          """Checks if the pattern matches the data."""
    655          if self.pattern.match(data) is None:
    656              return "Does not matches the defined pattern"
    657          return True
    658
    659 -class Decimal(Field):
    660      default=0
    661 -    def data2str(self, data):
    662          return '%d' % data
    663 -    def str2data(self, text):
    664          return int(text)
    665
    666 -class Double(Field):
    667      """Floating point value"""
    668 -    def __init__(self, path, precision=3, **kwargs):
    669          """
    670          @param precision: Precision of the value
    671          """
    672          return super(Double, self).__init__(path=path, precision=precision, **kwargs)
    673 -    def data2str(self, data):
    674          return ('%%.%df' % (self.precision,)) % (data,)
    675 -    def str2data(self, text):
    676          return float(text)
    677
    678 -class Boolean(Field):
    679      values = (True, False)
    680 -    def data2str(self, data):
    681          return (data and 'true') or 'false'
    682 -    def str2data(self, text):
    683          if text == 'true':
    684              return True
    685          if text == 'false':
    686              return False
    687          return 'invalid'
    688
    689 -class Long(Field):
    690      default=0
    691 -    def data2str(self, data):
    692          return '%d' % (data,)
    693 -    def str2data(self, text):
    694          return long(text)
    695
    696 -class Integer(Long):
    697      pass
    698
    699 -class Url(Pattern):
    700      """
    701      Note: a 'http://localhost/' does not considered to be a valid url.
    702      So any other alias name that you migght use in your local network
    703      (and defined in your /etc/hosts file) could possibly be considered
    704      invalid.
    705
    706      >>> u = Url('dummy')
    707      >>> u.validate('http://google.com')
    708      True
    709      >>> u.validate('https://google.com')
    710      True
    711      >>> u.validate('http://google.com/')
    712      True
    713      >>> u.validate('http://google.com/some')
    714      True
    715      >>> u.validate('http://google.com/some/more')
    716      True
    717      >>> u.validate('http://google.com/even///more/')
    718      True
    719      >>> u.validate('http://google.com/url/?with=some&args')
    720      True
    721      >>> u.validate('http://google.com/empty/args/?')
    722      True
    723      >>> u.validate('http://google.com/some/;-)?a+b=c&&=11')
    724      True
    725      >>> u.validate('http:/google.com') != True
    726      True
    727      >>> u.validate('mailto://google.com') != True
    728      True
    729      >>> u.validate('http://.google.com') != True
    730      True
    731      >>> u.validate('http://google..com') != True
    732      True
    733      >>> u.validate('http://;-).google.com') != True
    734      True
    735      >>> u.validate('https://sandbox.google.com/checkout/view/buy?o=shoppingcart&shoppingcart=515556794648982')
    736      True
    737      >>> u.validate('http://127.0.0.1:8000/digital/order/continue/')
    738      True
    739      """
    740 -    def __init__(self, path, **kwargs):
    741          import re
    742          # Regular expression divided into chunks:
    743          protocol  = r'((http(s?)|ftp)\:\/\/|~/|/)?'
    744          user_pass = r'([\w]+:\w+@)?'
    745          domain    = r'(([a-zA-Z]{1}([\w\-]+\.)+([\w]{2,5}))|(([0-9]+\.){3}[0-9]+))'
    746          port      = r'(:[\d]{1,5})?'
    747          file    = r'(/[\w\.\+-;\(\)]*)*'
    748          params    = r'(\?.*)?'
    749          pattern = re.compile('^' + protocol + user_pass + domain + port + file + params + '$')
    750          Pattern.__init__(self, path, pattern=pattern, **kwargs)
    751
    752      @apply_parent_validation(Pattern, error_prefix="Url: ")
    753 -    def validate(self, data):
    754          return True
    755
    756 -class Email(Pattern):
    757 -    def __init__(self, path, **kwargs):
    758          import re
    759          pattern = re.compile(r'^[a-zA-Z0-9\.\_\%\-\+]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
    760          Pattern.__init__(self, path, pattern=pattern, **kwargs)
    761
    762      @apply_parent_validation(Pattern, error_prefix="Email: ")
    763 -    def validate(self, data):
    764          return True
    765
    766 -class Html(String):
    767      pass
    768
    769 -class LanguageCode(Pattern):
    770 -    def __init__(self, path):
    771          return super(LanguageCode, self).__init__(path=path, pattern='^en_US$')
    772
    773 -class Phone(Pattern):
    774 -    def __init__(self, path, **kwargs):
    775          import re
    776          pattern = re.compile(r'^[0-9\+\-\(\)\ ]+$')
    777          Pattern.__init__(self, path, pattern=pattern, **kwargs)
    778
    779      @apply_parent_validation(Pattern, error_prefix="Phone: ")
    780 -    def validate(self, data):
    781          return True
    782
    783 -class Zip(Pattern):
    784      """
    785      Represents a zip code.
    786
    787      >>> zip = Zip('dummy')
    788      >>> zip.validate('94043')
    789      True
    790      >>> zip.validate('abCD123')
    791      True
    792      >>> zip.validate('123*') != True
    793      True
    794      >>> zip.validate('E6 1EX')
    795      True
    796
    797      >>> zip_pattern = Zip('dummy', complete=False)
    798      >>> zip_pattern.validate('SW*')
    799      True
    800      """
    801 -    def __init__(self, path, complete=True, **kwargs):
    802          import re
    803          if complete:
    804              pattern = re.compile(r'^[0-9a-zA-Z- ]+$')
    805          else:
    806              pattern = re.compile(r'^[0-9a-zA-Z- \*]+$')
    807          Pattern.__init__(self, path, pattern=pattern, **kwargs)
    808
    809      @apply_parent_validation(Pattern, error_prefix="Zip: ")
    810 -    def validate(self, data):
    811          return True
    812
    813 -class IP(Pattern):
    814      """
    815      Represents an IP address.
    816
    817      Currently only IPv4 addresses in decimal notation are accepted.
    818
    819      >>> ip = IP('dummy')
    820      >>> ip.validate('127.0.0.1')
    821      True
    822      >>> ip.validate('10.0.0.1')
    823      True
    824      >>> ip.validate('255.17.101.199')
    825      True
    826      >>> ip.validate('1.1.1.1')
    827      True
    828      >>> ip.validate('1.2.3') != True
    829      True
    830      >>> ip.validate('1.2.3.4.5') != True
    831      True
    832      >>> ip.validate('1.2.3.256') != True
    833      True
    834      >>> ip.validate('1.2.3.-1') != True
    835      True
    836      >>> ip.validate('1.2..3') != True
    837      True
    838      >>> ip.validate('.1.2.3.4') != True
    839      True
    840      >>> ip.validate('1.2.3.4.') != True
    841      True
    842      >>> ip.validate('1.2.3.-') != True
    843      True
    844      >>> ip.validate('01.2.3.4') != True
    845      True
    846      >>> ip.validate('1.02.3.4') != True
    847      True
    848      """
    849 -    def __init__(self, path, **kwargs):
    850          import re
    851          num_pattern = r'(0|([1-9][0-9]?)|(1[0-9]{2})|(2((5[0-5])|([0-4][0-9]))))'
    852          pattern = re.compile(r'^%s\.%s\.%s\.%s$' % (num_pattern,num_pattern,num_pattern,num_pattern))
    853          Pattern.__init__(self, path, pattern=pattern, **kwargs)
    854
    855      @apply_parent_validation(Pattern, error_prefix="IP address: ")
    856 -    def validate(self, data):
    857          return True
    858
    859  # TODO
    860 -class ID(String):
    861      empty = False
    862
    863      @apply_parent_validation(String, error_prefix="ID: ")
    864 -    def validate(self, data):
    865          if len(data) == 0:
    866              return "ID has to be non-empty"
    867          return True
    868
    869 -class Any(Field):
    870      """Any text value. This field is tricky. Since any data could be stored in
    871      the field we can't handle all the cases.
    872      The class uses xml.marshal.generic to convert python-ic simple data
    873      structures into xml. By simple we mean any POD. Note that a class derived
    874      from object requires the marshaller to be extended that's why this field
    875      does not accept instance of such classes.
    876      When reading XML we consider node XML text as if it was previously
    877      generated by a xml marshaller facility (xml.marshal.generic.dumps).
    878      If it fails then we consider the data as if it was produced by some other
    879      external source and return False indicating that user Controller should
    880      parse the XML data itself. In such case field value is False.
    881      To access the original XML input two class member variables are populated:
    882      - <field>_xml contains the original XML text
    883      - <field>_dom contains the corresponding XML DOM node
    884      """
    885 -    def __init__(self, *args, **kwargs):
    886          obj = super(Any, self).__init__(*args, **kwargs)
    887          if self.path_attribute is not None:
    888              raise ValueError('gxml.Any field cannot be bound to an attribute!')
    889          return obj
    890
    891 -    def save(self, node, data):
    892          from gchecky.tools import encoder
    893          return encoder().serialize(data, node)
    894
    895 -    def load(self, node):
    896          from gchecky.tools import decoder
    897          return decoder().deserialize(node)
    898
    899 -    def validate(self, data):
    900          # Always return True, since any data is allowed
    901          return True
    902
    903  #class DateTime(Field):
    904  #    from datetime import datetime
    905  #    def validate(self, data):
    906  #        return isinstance(data, datetime)
    907  #    def data2str(self, data):
    908  #        pass
    909
    910 -class Timestamp(Field):
    911 -    def validate(self, data):
    912          from datetime import datetime
    913          if not isinstance(data, datetime):
    914              return "Timestamp has to be an instance of datetime.datetime"
    915          return True
    916 -    def data2str(self, data):
    917          return data.isoformat()
    918 -    def str2data(self, text):
    919          import iso8601
    920          return iso8601.parse_date(text)
    921
    922  if __name__ == "__main__":
    923 -    def run_doctests():
    924          import doctest
    925          doctest.testmod()
    926      run_doctests()
    927