1
2
3
4
5
6 """ properties used by Document object """
7
8 from calendar import timegm
9 import decimal
10 import datetime
11 import re
12 import time
13
14 from couchdbkit.exceptions import *
15
16 __all__ = ['ALLOWED_PROPERTY_TYPES', 'Property', 'StringProperty',
17 'IntegerProperty', 'DecimalProperty', 'BooleanProperty',
18 'FloatProperty', 'DateTimeProperty', 'DateProperty',
19 'TimeProperty', 'DictProperty', 'ListProperty',
20 'StringListProperty', 'dict_to_json', 'list_to_json',
21 'value_to_json', 'MAP_TYPES_PROPERTIES', 'value_to_python',
22 'dict_to_python', 'list_to_python', 'convert_property',
23 'value_to_property', 'LazyDict', 'LazyList']
24
25 ALLOWED_PROPERTY_TYPES = set([
26 basestring,
27 str,
28 unicode,
29 bool,
30 int,
31 long,
32 float,
33 datetime.datetime,
34 datetime.date,
35 datetime.time,
36 decimal.Decimal,
37 dict,
38 list,
39 type(None)
40 ])
41
42 re_date = re.compile('^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$')
43 re_time = re.compile('^([01]\d|2[0-3])\D?([0-5]\d)\D?([0-5]\d)?\D?(\d{3})?$')
44 re_datetime = re.compile('^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])(\D?([01]\d|2[0-3])\D?([0-5]\d)\D?([0-5]\d)?\D?(\d{3})?([zZ]|([\+-])([01]\d|2[0-3])\D?([0-5]\d)?)?)?$')
45 re_decimal = re.compile('^(\d+)\.(\d+)$')
48 """ Property base which all other properties
49 inherit."""
50 creation_counter = 0
51
52 - def __init__(self, verbose_name=None, name=None,
53 default=None, required=False, validators=None,
54 choices=None):
55 """ Default constructor for a property.
56
57 :param verbose_name: str, verbose name of field, could
58 be use for description
59 :param name: str, name of field
60 :param default: default value
61 :param required: True if field is required, default is False
62 :param validators: list of callable or callable, field validators
63 function that are executed when document is saved.
64 """
65 self.verbose_name = verbose_name
66 self.name = name
67 self.default = default
68 self.required = required
69 self.validators = validators
70 self.choices = choices
71 self.creation_counter = Property.creation_counter
72 Property.creation_counter += 1
73
75 self.document_class = document_class
76 if self.name is None:
77 self.name = property_name
78
80 """ method used to set value of the property when
81 we create the document. Don't check required. """
82 if value is not None:
83 value = self.to_json(self.validate(value, required=False))
84 document_instance._doc[self.name] = value
85
86 - def __get__(self, document_instance, document_class):
87 if document_instance is None:
88 return self
89
90 value = document_instance._doc.get(self.name)
91 if value is not None:
92 value = self._to_python(value)
93
94 return value
95
96 - def __set__(self, document_instance, value):
99
102
104 """ return default value """
105
106 default = self.default
107 if callable(default):
108 default = default()
109 return default
110
111 - def validate(self, value, required=True):
112 """ validate value """
113 if required and self.empty(value):
114 if self.required:
115 raise BadValueError("Property %s is required." % self.name)
116 else:
117 if self.choices and value is not None:
118 if isinstance(self.choices, list): choice_list = self.choices
119 if isinstance(self.choices, dict): choice_list = self.choices.keys()
120 if isinstance(self.choices, tuple): choice_list = [key for (key, name) in self.choices]
121
122 if value not in choice_list:
123 raise BadValueError('Property %s is %r; must be one of %r' % (
124 self.name, value, choice_list))
125 if self.validators:
126 if isinstance(self.validators, (list, tuple,)):
127 for validator in self.validators:
128 if callable(validator):
129 validator(value)
130 elif callable(self.validators):
131 self.validators(value)
132 return value
133
135 """ test if value is empty """
136 return not value or value is None
137
139 if value == None:
140 return value
141 return self.to_python(value)
142
144 if value == None:
145 return value
146 return self.to_json(value)
147
149 """ convert to python type """
150 return unicode(value)
151
153 """ convert to json, Converted value is saved in couchdb. """
154 return self.to_python(value)
155
156 data_type = None
157
159 """ string property str or unicode property
160
161 *Value type*: unicode
162 """
163
164 to_python = unicode
165
166 - def validate(self, value, required=True):
167 value = super(StringProperty, self).validate(value,
168 required=required)
169
170 if value is None:
171 return value
172
173 if not isinstance(value, basestring):
174 raise BadValueError(
175 'Property %s must be unicode or str instance, not a %s' % (self.name, type(value).__name__))
176 return value
177
178 data_type = unicode
179
181 """ Integer property. map to int
182
183 *Value type*: int
184 """
185 to_python = int
186
189
190 - def validate(self, value, required=True):
191 value = super(IntegerProperty, self).validate(value,
192 required=required)
193
194 if value is None:
195 return value
196
197 if value is not None and not isinstance(value, (int, long,)):
198 raise BadValueError(
199 'Property %s must be %s or long instance, not a %s'
200 % (self.name, type(self.data_type).__name__,
201 type(value).__name__))
202
203 return value
204
205 data_type = int
206 LongProperty = IntegerProperty
209 """ Float property, map to python float
210
211 *Value type*: float
212 """
213 to_python = float
214 data_type = float
215
216 - def validate(self, value, required=True):
217 value = super(FloatProperty, self).validate(value,
218 required=required)
219
220 if value is None:
221 return value
222
223 if not isinstance(value, float):
224 raise BadValueError(
225 'Property %s must be float instance, not a %s'
226 % (self.name, type(value).__name__))
227
228 return value
229 Number = FloatProperty
232 """ Boolean property, map to python bool
233
234 *ValueType*: bool
235 """
236 to_python = bool
237 data_type = bool
238
239 - def validate(self, value, required=True):
240 value = super(BooleanProperty, self).validate(value,
241 required=required)
242
243 if value is None:
244 return value
245
246 if value is not None and not isinstance(value, bool):
247 raise BadValueError(
248 'Property %s must be bool instance, not a %s'
249 % (self.name, type(value).__name__))
250
251 return value
252
254 """test if boolean is empty"""
255 return value is None
256
258 """ Decimal property, map to Decimal python object
259
260 *ValueType*: decimal.Decimal
261 """
262 data_type = decimal.Decimal
263
266
269
271 """DateTime property. It convert iso3339 string
272 to python and vice-versa. Map to datetime.datetime
273 object.
274
275 *ValueType*: datetime.datetime
276 """
277
278 - def __init__(self, verbose_name=None, auto_now=False, auto_now_add=False,
279 **kwds):
280 super(DateTimeProperty, self).__init__(verbose_name, **kwds)
281 self.auto_now = auto_now
282 self.auto_now_add = auto_now_add
283
284 - def validate(self, value, required=True):
285 value = super(DateTimeProperty, self).validate(value, required=required)
286
287 if value is None:
288 return value
289
290 if value and not isinstance(value, self.data_type):
291 raise BadValueError('Property %s must be a %s, current is %s' %
292 (self.name, self.data_type.__name__, type(value).__name__))
293 return value
294
299
301 if isinstance(value, basestring):
302 try:
303 value = value.split('.', 1)[0]
304 value = value[0:19]
305 value = datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S')
306 except ValueError, e:
307 raise ValueError('Invalid ISO date/time %r' % value)
308 return value
309
311 if self.auto_now:
312 value = self.now()
313
314 if value is None:
315 return value
316 return value.replace(microsecond=0).isoformat() + 'Z'
317
318 data_type = datetime.datetime
319
320 @staticmethod
323
325 """ Date property, like DateTime property but only
326 for Date. Map to datetime.date object
327
328 *ValueType*: datetime.date
329 """
330 data_type = datetime.date
331
332 @staticmethod
335
337 if isinstance(value, basestring):
338 try:
339 value = datetime.date(*time.strptime(value, '%Y-%m-%d')[:3])
340 except ValueError, e:
341 raise ValueError('Invalid ISO date %r' % value)
342 return value
343
345 if value is None:
346 return value
347 return value.isoformat()
348
350 """ Date property, like DateTime property but only
351 for time. Map to datetime.time object
352
353 *ValueType*: datetime.time
354 """
355
356 data_type = datetime.time
357
358 @staticmethod
361
363 if isinstance(value, basestring):
364 try:
365 value = value.split('.', 1)[0]
366 value = datetime.time(*time.strptime(value, '%H:%M:%S')[3:6])
367 except ValueError, e:
368 raise ValueError('Invalid ISO time %r' % value)
369 return value
370
372 if value is None:
373 return value
374 return value.replace(microsecond=0).isoformat()
375
378 """ A property that stores a dict of things"""
379
380 - def __init__(self, verbose_name=None, default=None,
381 required=False, **kwds):
382 """
383 :args verbose_name: Optional verbose name.
384 :args default: Optional default value; if omitted, an empty list is used.
385 :args**kwds: Optional additional keyword arguments, passed to base class.
386
387 Note that the only permissible value for 'required' is True.
388 """
389
390 if default is None:
391 default = {}
392
393 Property.__init__(self, verbose_name, default=default,
394 required=required, **kwds)
395
396 data_type = dict
397
398 - def validate(self, value, required=True):
405
406 - def validate_dict_contents(self, value):
407 try:
408 value = validate_dict_content(value)
409 except BadValueError:
410 raise BadValueError(
411 'Items of %s dict must all be in %s' %
412 (self.name, ALLOWED_PROPERTY_TYPES))
413 return value
414
416 """Default value for list.
417
418 Because the property supplied to 'default' is a static value,
419 that value must be shallow copied to prevent all fields with
420 default values from sharing the same instance.
421
422 Returns:
423 Copy of the default value.
424 """
425 value = super(DictProperty, self).default_value()
426 if value is None:
427 value = {}
428 return dict(value)
429
432
435
439 """A property that stores a list of things.
440
441 """
442 - def __init__(self, verbose_name=None, default=None,
443 required=False, item_type=None, **kwds):
444 """Construct ListProperty.
445
446
447 :args verbose_name: Optional verbose name.
448 :args default: Optional default value; if omitted, an empty list is used.
449 :args**kwds: Optional additional keyword arguments, passed to base class.
450
451
452 """
453 if default is None:
454 default = []
455
456 if item_type is not None and item_type not in ALLOWED_PROPERTY_TYPES:
457 raise ValueError('item_type %s not in %s' % (item_type, ALLOWED_PROPERTY_TYPES))
458 self.item_type = item_type
459
460 Property.__init__(self, verbose_name, default=default,
461 required=required, **kwds)
462
463 data_type = list
464
465 - def validate(self, value, required=True):
472
473 - def validate_list_contents(self, value):
474 value = validate_list_content(value, item_type=self.item_type)
475 try:
476 value = validate_list_content(value, item_type=self.item_type)
477 except BadValueError:
478 raise BadValueError(
479 'Items of %s list must all be in %s' %
480 (self.name, ALLOWED_PROPERTY_TYPES))
481 return value
482
484 """Default value for list.
485
486 Because the property supplied to 'default' is a static value,
487 that value must be shallow copied to prevent all fields with
488 default values from sharing the same instance.
489
490 Returns:
491 Copy of the default value.
492 """
493 value = super(ListProperty, self).default_value()
494 if value is None:
495 value = []
496 return list(value)
497
499 return LazyList(value, item_type=self.item_type)
500
503
506 """ shorthand for list that should containe only unicode"""
507
508 - def __init__(self, verbose_name=None, default=None,
509 required=False, **kwds):
510 super(StringListProperty, self).__init__(verbose_name=verbose_name,
511 default=default, required=required, item_type=basestring, **kwds)
512
516 """ object to make sure we keep updated of dict
517 in _doc. We just override a dict and maintain change in
518 doc reference (doc[keyt] obviously).
519
520 if init_vals is specified, doc is overwritten
521 with the dict given. Otherwise, the values already in
522 doc are used.
523 """
524
525 - def __init__(self, doc, item_type=None, init_vals=None):
526 dict.__init__(self)
527 self.item_type = item_type
528
529 self.doc = doc
530 if init_vals is None:
531 self._wrap()
532 else:
533 for key, value in init_vals.items():
534 self[key] = value
535
537 for key, json_value in self.doc.items():
538 if isinstance(json_value, dict):
539 value = LazyDict(json_value, item_type=self.item_type)
540 elif isinstance(json_value, list):
541 value = LazyList(json_value, item_type=self.item_type)
542 else:
543 value = value_to_python(json_value, self.item_type)
544 dict.__setitem__(self, key, value)
545
547 if isinstance(value, dict):
548 self.doc[key] = {}
549 value = LazyDict(self.doc[key], item_type=self.item_type, init_vals=value)
550 elif isinstance(value, list):
551 self.doc[key] = []
552 value = LazyList(self.doc[key], item_type=self.item_type, init_vals=value)
553 else:
554 self.doc.update({key: value_to_json(value, item_type=self.item_type) })
555 super(LazyDict, self).__setitem__(key, value)
556
560
561 - def pop(self, key, default=None):
562 del self.doc[key]
563 return super(LazyDict, self).pop(key, default=default)
564
571
573 for k, v in value.items():
574 self[k] = v
575
580
584
586 """ object to make sure we keep update of list
587 in _doc. We just override a list and maintain change in
588 doc reference (doc[index] obviously).
589
590 if init_vals is specified, doc is overwritten
591 with the list given. Otherwise, the values already in
592 doc are used.
593 """
594
595 - def __init__(self, doc, item_type=None, init_vals=None):
596 list.__init__(self)
597
598 self.item_type = item_type
599 self.doc = doc
600 if init_vals is None:
601
602 self._wrap()
603 else:
604
605
606 del self.doc[:]
607 for item in init_vals:
608 self.append(item)
609
611 for json_value in self.doc:
612 if isinstance(json_value, dict):
613 value = LazyDict(json_value, item_type=self.item_type)
614 elif isinstance(json_value, list):
615 value = LazyList(json_value, item_type=self.item_type)
616 else:
617 value = value_to_python(json_value, self.item_type)
618 list.append(self, value)
619
623
634
635
639
642
646
648 jvalue = value_to_json(value)
649 for m in self.doc:
650 if m == value: return True
651 return False
652
653 - def append(self, *args, **kwargs):
654 if args:
655 assert len(args) == 1
656 value = args[0]
657 else:
658 value = kwargs
659
660 index = len(self)
661 if isinstance(value, dict):
662 self.doc.append({})
663 value = LazyDict(self.doc[index], item_type=self.item_type, init_vals=value)
664 elif isinstance(value, list):
665 self.doc.append([])
666 value = LazyList(self.doc[index], item_type=self.item_type, init_vals=value)
667 else:
668 self.doc.append(value_to_json(value, item_type=self.item_type))
669 super(LazyList, self).append(value)
670
671 - def index(self, x, *args):
674
676 del self[self.index(x)]
677
678 - def sort(self, cmp=None, key=None, reverse=False):
681
685
686
687
688
689 MAP_TYPES_PROPERTIES = {
690 decimal.Decimal: DecimalProperty,
691 datetime.datetime: DateTimeProperty,
692 datetime.date: DateProperty,
693 datetime.time: TimeProperty,
694 str: StringProperty,
695 unicode: StringProperty,
696 bool: BooleanProperty,
697 int: IntegerProperty,
698 long: LongProperty,
699 float: FloatProperty,
700 list: ListProperty,
701 dict: DictProperty
702 }
710
719
720
721
722 -def validate_list_content(value, item_type=None):
723 """ validate type of values in a list """
724 return [validate_content(item, item_type=item_type) for item in value]
725
726 -def validate_dict_content(value, item_type=None):
727 """ validate type of values in a dict """
728 return dict([(k, validate_content(v,
729 item_type=item_type)) for k, v in value.iteritems()])
730
731 -def validate_content(value, item_type=None):
732 """ validate a value. test if value is in supported types """
733 if isinstance(value, list):
734 value = validate_list_content(value, item_type=item_type)
735 elif isinstance(value, dict):
736 value = validate_dict_content(value, item_type=item_type)
737 elif item_type is not None and not isinstance(value, item_type):
738 raise BadValueError(
739 'Items must all be in %s' % item_type)
740 elif type(value) not in ALLOWED_PROPERTY_TYPES:
741 raise BadValueError(
742 'Items must all be in %s' %
743 (ALLOWED_PROPERTY_TYPES))
744 return value
745
747 """ convert a dict to json """
748 return dict([(k, value_to_json(v, item_type=item_type)) for k, v in value.iteritems()])
749
751 """ convert a list to json """
752 return [value_to_json(item, item_type=item_type) for item in value]
753
755 """ convert a value to json using appropriate regexp.
756 For Dates we use ISO 8601. Decimal are converted to string.
757
758 """
759 if isinstance(value, datetime.datetime) and is_type_ok(item_type, datetime.datetime):
760 value = value.replace(microsecond=0).isoformat() + 'Z'
761 elif isinstance(value, datetime.date) and is_type_ok(item_type, datetime.date):
762 value = value.isoformat()
763 elif isinstance(value, datetime.time) and is_type_ok(item_type, datetime.time):
764 value = value.replace(microsecond=0).isoformat()
765 elif isinstance(value, decimal.Decimal) and is_type_ok(item_type, decimal.Decimal):
766 value = unicode(value)
767 elif isinstance(value, list):
768 value = list_to_json(value, item_type)
769 elif isinstance(value, dict):
770 value = dict_to_json(value, item_type)
771 return value
772
774 return item_type is None or item_type == value_type
775
803
805 """ convert a list of json values to python list """
806 return [value_to_python(item, item_type=item_type) for item in value]
807
809 """ convert a json object values to python dict """
810 return dict([(k, value_to_python(v, item_type=item_type)) for k, v in value.iteritems()])
811