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
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
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\-\_]*$')
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
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
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
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
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
175 """Override this method in subclasses"""
176 raise Exception('Abstract method of %s' % self.__class__)
177
179 """Override this method in subclasses"""
180 raise Exception('Abstract method of %s' % self.__class__)
181
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
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
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
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
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
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
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
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
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
311 """Method is called by L{NodeManager} to specify this class L{Field}s
312 set."""
313 cls._fields = fields
314
315 @classmethod
317 """Return all fields of this class (and its bases)"""
318 return cls._fields
319
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
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
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
367 setattr(self, '%s_dom' % (fname,), fnode)
368
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
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
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):
416
417 @classmethod
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
431
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
453
454
455
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
468 try:
469 return self.toxml()
470 except Exception:
471 pass
472 return self.__repr__()
473
474 @classmethod
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
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
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
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
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
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
562 """Override L{Field.__repr__} for documentation purposes"""
563 return 'List%s:[\n %s\n]' % (self._traits(),
564 self.list_item.__repr__())
565
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
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
602 """Override L{Field.__repr__} for documentation purposes."""
603 return 'Node%s:{ %s }' % (self._traits(), self.clazz.__name__)
604
606 """Any text value."""
607 - def __init__(self, path, max_length=None, **kwargs):
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
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
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)
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
665
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)
674 return ('%%.%df' % (self.precision,)) % (data,)
677
679 values = (True, False)
681 return (data and 'true') or 'false'
683 if text == 'true':
684 return True
685 if text == 'false':
686 return False
687 return 'invalid'
688
695
698
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 """
741 import re
742
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: ")
755
761
762 @apply_parent_validation(Pattern, error_prefix="Email: ")
765
768
772
778
779 @apply_parent_validation(Pattern, error_prefix="Phone: ")
782
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):
808
809 @apply_parent_validation(Pattern, error_prefix="Zip: ")
812
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 """
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: ")
858
859
861 empty = False
862
863 @apply_parent_validation(String, error_prefix="ID: ")
865 if len(data) == 0:
866 return "ID has to be non-empty"
867 return True
868
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 """
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):
894
895 - def load(self, node):
898
902
903
904
905
906
907
908
909
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
917 return data.isoformat()
919 import iso8601
920 return iso8601.parse_date(text)
921
922 if __name__ == "__main__":
924 import doctest
925 doctest.testmod()
926 run_doctests()
927