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

Source Code for Module gchecky.gxml

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