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