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
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
398 return not(self == other)
399
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):
418
419 @classmethod
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
433
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
455
456
457
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
470 try:
471 return self.toxml()
472 except Exception:
473 pass
474 return self.__repr__()
475
476 @classmethod
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
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
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
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
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
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
564 """Override L{Field.__repr__} for documentation purposes"""
565 return 'List%s:[\n %s\n]' % (self._traits(),
566 self.list_item.__repr__())
567
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
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
604 """Override L{Field.__repr__} for documentation purposes."""
605 return 'Node%s:{ %s }' % (self._traits(), self.clazz.__name__)
606
608 """
609 A field representing a string value.
610 """
611 - def __init__(self, path, max_length=None, empty=True, **kwargs):
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
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
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)
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
672
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)
681 return ('%%.%df' % (self.precision,)) % (data,)
684
686 values = (True, False)
688 return (data and 'true') or 'false'
690 if text == 'true':
691 return True
692 if text == 'false':
693 return False
694 return 'invalid'
695
702
705
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 """
748 import re
749
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: ")
762
768
769 @apply_parent_validation(Pattern, error_prefix="Email: ")
772
775
779
785
786 @apply_parent_validation(Pattern, error_prefix="Phone: ")
789
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):
815
816 @apply_parent_validation(Pattern, error_prefix="Zip: ")
819
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 """
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: ")
865
866
868 empty = False
869
870 @apply_parent_validation(String, error_prefix="ID: ")
872 if len(data) == 0:
873 return "ID has to be non-empty"
874 return True
875
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 """
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):
901
902 - def load(self, node):
905
909
910
911
912
913
914
915
916
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
924 return data.isoformat()
926 import iso8601
927 return iso8601.parse_date(text)
928
929 if __name__ == "__main__":
931 import doctest
932 doctest.testmod()
933 run_doctests()
934