1e7a6581eSFilipe David Manana# -*- coding: utf-8 -*-
2e7a6581eSFilipe David Manana#
3e7a6581eSFilipe David Manana# Copyright (C) 2007-2009 Christopher Lenz
4e7a6581eSFilipe David Manana# All rights reserved.
5e7a6581eSFilipe David Manana#
6e7a6581eSFilipe David Manana# This software is licensed as described in the file COPYING, which
7e7a6581eSFilipe David Manana# you should have received as part of this distribution.
8e7a6581eSFilipe David Manana
9e7a6581eSFilipe David Manana"""Mapping from raw JSON data structures to Python objects and vice versa.
10e7a6581eSFilipe David Manana
11e7a6581eSFilipe David Manana>>> from couchdb import Server
12e7a6581eSFilipe David Manana>>> server = Server()
13e7a6581eSFilipe David Manana>>> db = server.create('python-tests')
14e7a6581eSFilipe David Manana
15e7a6581eSFilipe David MananaTo define a document mapping, you declare a Python class inherited from
16e7a6581eSFilipe David Manana`Document`, and add any number of `Field` attributes:
17e7a6581eSFilipe David Manana
18e7a6581eSFilipe David Manana>>> from couchdb.mapping import TextField, IntegerField, DateField
19e7a6581eSFilipe David Manana>>> class Person(Document):
20e7a6581eSFilipe David Manana...     name = TextField()
21e7a6581eSFilipe David Manana...     age = IntegerField()
22e7a6581eSFilipe David Manana...     added = DateTimeField(default=datetime.now)
23e7a6581eSFilipe David Manana>>> person = Person(name='John Doe', age=42)
24e7a6581eSFilipe David Manana>>> person.store(db) #doctest: +ELLIPSIS
25e7a6581eSFilipe David Manana<Person ...>
26e7a6581eSFilipe David Manana>>> person.age
27e7a6581eSFilipe David Manana42
28e7a6581eSFilipe David Manana
29e7a6581eSFilipe David MananaYou can then load the data from the CouchDB server through your `Document`
30e7a6581eSFilipe David Mananasubclass, and conveniently access all attributes:
31e7a6581eSFilipe David Manana
32e7a6581eSFilipe David Manana>>> person = Person.load(db, person.id)
33e7a6581eSFilipe David Manana>>> old_rev = person.rev
34e7a6581eSFilipe David Manana>>> person.name
35e7a6581eSFilipe David Mananau'John Doe'
36e7a6581eSFilipe David Manana>>> person.age
37e7a6581eSFilipe David Manana42
38e7a6581eSFilipe David Manana>>> person.added                #doctest: +ELLIPSIS
39e7a6581eSFilipe David Mananadatetime.datetime(...)
40e7a6581eSFilipe David Manana
41e7a6581eSFilipe David MananaTo update a document, simply set the attributes, and then call the ``store()``
42e7a6581eSFilipe David Mananamethod:
43e7a6581eSFilipe David Manana
44e7a6581eSFilipe David Manana>>> person.name = 'John R. Doe'
45e7a6581eSFilipe David Manana>>> person.store(db)            #doctest: +ELLIPSIS
46e7a6581eSFilipe David Manana<Person ...>
47e7a6581eSFilipe David Manana
48e7a6581eSFilipe David MananaIf you retrieve the document from the server again, you should be getting the
49e7a6581eSFilipe David Mananaupdated data:
50e7a6581eSFilipe David Manana
51e7a6581eSFilipe David Manana>>> person = Person.load(db, person.id)
52e7a6581eSFilipe David Manana>>> person.name
53e7a6581eSFilipe David Mananau'John R. Doe'
54e7a6581eSFilipe David Manana>>> person.rev != old_rev
55e7a6581eSFilipe David MananaTrue
56e7a6581eSFilipe David Manana
57e7a6581eSFilipe David Manana>>> del server['python-tests']
58e7a6581eSFilipe David Manana"""
59e7a6581eSFilipe David Manana
60e7a6581eSFilipe David Mananaimport copy
61e7a6581eSFilipe David Manana
62e7a6581eSFilipe David Mananafrom calendar import timegm
63e7a6581eSFilipe David Mananafrom datetime import date, datetime, time
64e7a6581eSFilipe David Mananafrom decimal import Decimal
65e7a6581eSFilipe David Mananafrom time import strptime, struct_time
66e7a6581eSFilipe David Manana
67e7a6581eSFilipe David Mananafrom couchdb.design import ViewDefinition
68e7a6581eSFilipe David Manana
69e7a6581eSFilipe David Manana__all__ = ['Mapping', 'Document', 'Field', 'TextField', 'FloatField',
70e7a6581eSFilipe David Manana           'IntegerField', 'LongField', 'BooleanField', 'DecimalField',
71e7a6581eSFilipe David Manana           'DateField', 'DateTimeField', 'TimeField', 'DictField', 'ListField',
72e7a6581eSFilipe David Manana           'ViewField']
73e7a6581eSFilipe David Manana__docformat__ = 'restructuredtext en'
74e7a6581eSFilipe David Manana
75e7a6581eSFilipe David MananaDEFAULT = object()
76e7a6581eSFilipe David Manana
77e7a6581eSFilipe David Manana
78e7a6581eSFilipe David Mananaclass Field(object):
79e7a6581eSFilipe David Manana    """Basic unit for mapping a piece of data between Python and JSON.
80e7a6581eSFilipe David Manana
81e7a6581eSFilipe David Manana    Instances of this class can be added to subclasses of `Document` to describe
82e7a6581eSFilipe David Manana    the mapping of a document.
83e7a6581eSFilipe David Manana    """
84e7a6581eSFilipe David Manana
85e7a6581eSFilipe David Manana    def __init__(self, name=None, default=None):
86e7a6581eSFilipe David Manana        self.name = name
87e7a6581eSFilipe David Manana        self.default = default
88e7a6581eSFilipe David Manana
89e7a6581eSFilipe David Manana    def __get__(self, instance, owner):
90e7a6581eSFilipe David Manana        if instance is None:
91e7a6581eSFilipe David Manana            return self
92e7a6581eSFilipe David Manana        value = instance._data.get(self.name)
93e7a6581eSFilipe David Manana        if value is not None:
94e7a6581eSFilipe David Manana            value = self._to_python(value)
95e7a6581eSFilipe David Manana        elif self.default is not None:
96e7a6581eSFilipe David Manana            default = self.default
97e7a6581eSFilipe David Manana            if callable(default):
98e7a6581eSFilipe David Manana                default = default()
99e7a6581eSFilipe David Manana            value = default
100e7a6581eSFilipe David Manana        return value
101e7a6581eSFilipe David Manana
102e7a6581eSFilipe David Manana    def __set__(self, instance, value):
103e7a6581eSFilipe David Manana        if value is not None:
104e7a6581eSFilipe David Manana            value = self._to_json(value)
105e7a6581eSFilipe David Manana        instance._data[self.name] = value
106e7a6581eSFilipe David Manana
107e7a6581eSFilipe David Manana    def _to_python(self, value):
108e7a6581eSFilipe David Manana        return unicode(value)
109e7a6581eSFilipe David Manana
110e7a6581eSFilipe David Manana    def _to_json(self, value):
111e7a6581eSFilipe David Manana        return self._to_python(value)
112e7a6581eSFilipe David Manana
113e7a6581eSFilipe David Manana
114e7a6581eSFilipe David Mananaclass MappingMeta(type):
115e7a6581eSFilipe David Manana
116e7a6581eSFilipe David Manana    def __new__(cls, name, bases, d):
117e7a6581eSFilipe David Manana        fields = {}
118e7a6581eSFilipe David Manana        for base in bases:
119e7a6581eSFilipe David Manana            if hasattr(base, '_fields'):
120e7a6581eSFilipe David Manana                fields.update(base._fields)
121e7a6581eSFilipe David Manana        for attrname, attrval in d.items():
122e7a6581eSFilipe David Manana            if isinstance(attrval, Field):
123e7a6581eSFilipe David Manana                if not attrval.name:
124e7a6581eSFilipe David Manana                    attrval.name = attrname
125e7a6581eSFilipe David Manana                fields[attrname] = attrval
126e7a6581eSFilipe David Manana        d['_fields'] = fields
127e7a6581eSFilipe David Manana        return type.__new__(cls, name, bases, d)
128e7a6581eSFilipe David Manana
129e7a6581eSFilipe David Manana
130e7a6581eSFilipe David Mananaclass Mapping(object):
131e7a6581eSFilipe David Manana    __metaclass__ = MappingMeta
132e7a6581eSFilipe David Manana
133e7a6581eSFilipe David Manana    def __init__(self, **values):
134e7a6581eSFilipe David Manana        self._data = {}
135e7a6581eSFilipe David Manana        for attrname, field in self._fields.items():
136e7a6581eSFilipe David Manana            if attrname in values:
137e7a6581eSFilipe David Manana                setattr(self, attrname, values.pop(attrname))
138e7a6581eSFilipe David Manana            else:
139e7a6581eSFilipe David Manana                setattr(self, attrname, getattr(self, attrname))
140e7a6581eSFilipe David Manana
141e7a6581eSFilipe David Manana    def __iter__(self):
142e7a6581eSFilipe David Manana        return iter(self._data)
143e7a6581eSFilipe David Manana
144e7a6581eSFilipe David Manana    def __len__(self):
145e7a6581eSFilipe David Manana        return len(self._data or ())
146e7a6581eSFilipe David Manana
147e7a6581eSFilipe David Manana    def __delitem__(self, name):
148e7a6581eSFilipe David Manana        del self._data[name]
149e7a6581eSFilipe David Manana
150e7a6581eSFilipe David Manana    def __getitem__(self, name):
151e7a6581eSFilipe David Manana        return self._data[name]
152e7a6581eSFilipe David Manana
153e7a6581eSFilipe David Manana    def __setitem__(self, name, value):
154e7a6581eSFilipe David Manana        self._data[name] = value
155e7a6581eSFilipe David Manana
156e7a6581eSFilipe David Manana    def get(self, name, default=None):
157e7a6581eSFilipe David Manana        return self._data.get(name, default)
158e7a6581eSFilipe David Manana
159e7a6581eSFilipe David Manana    def setdefault(self, name, default):
160e7a6581eSFilipe David Manana        return self._data.setdefault(name, default)
161e7a6581eSFilipe David Manana
162e7a6581eSFilipe David Manana    def unwrap(self):
163e7a6581eSFilipe David Manana        return self._data
164e7a6581eSFilipe David Manana
165e7a6581eSFilipe David Manana    @classmethod
166e7a6581eSFilipe David Manana    def build(cls, **d):
167e7a6581eSFilipe David Manana        fields = {}
168e7a6581eSFilipe David Manana        for attrname, attrval in d.items():
169e7a6581eSFilipe David Manana            if not attrval.name:
170e7a6581eSFilipe David Manana                attrval.name = attrname
171e7a6581eSFilipe David Manana            fields[attrname] = attrval
172e7a6581eSFilipe David Manana        d['_fields'] = fields
173e7a6581eSFilipe David Manana        return type('AnonymousStruct', (cls,), d)
174e7a6581eSFilipe David Manana
175e7a6581eSFilipe David Manana    @classmethod
176e7a6581eSFilipe David Manana    def wrap(cls, data):
177e7a6581eSFilipe David Manana        instance = cls()
178e7a6581eSFilipe David Manana        instance._data = data
179e7a6581eSFilipe David Manana        return instance
180e7a6581eSFilipe David Manana
181e7a6581eSFilipe David Manana    def _to_python(self, value):
182e7a6581eSFilipe David Manana        return self.wrap(value)
183e7a6581eSFilipe David Manana
184e7a6581eSFilipe David Manana    def _to_json(self, value):
185e7a6581eSFilipe David Manana        return self.unwrap()
186e7a6581eSFilipe David Manana
187e7a6581eSFilipe David Manana
188e7a6581eSFilipe David Mananaclass ViewField(object):
189e7a6581eSFilipe David Manana    r"""Descriptor that can be used to bind a view definition to a property of
190e7a6581eSFilipe David Manana    a `Document` class.
191e7a6581eSFilipe David Manana
192e7a6581eSFilipe David Manana    >>> class Person(Document):
193e7a6581eSFilipe David Manana    ...     name = TextField()
194e7a6581eSFilipe David Manana    ...     age = IntegerField()
195e7a6581eSFilipe David Manana    ...     by_name = ViewField('people', '''\
196e7a6581eSFilipe David Manana    ...         function(doc) {
197e7a6581eSFilipe David Manana    ...             emit(doc.name, doc);
198e7a6581eSFilipe David Manana    ...         }''')
199e7a6581eSFilipe David Manana    >>> Person.by_name
200e7a6581eSFilipe David Manana    <ViewDefinition '_design/people/_view/by_name'>
201e7a6581eSFilipe David Manana
202e7a6581eSFilipe David Manana    >>> print Person.by_name.map_fun
203e7a6581eSFilipe David Manana    function(doc) {
204e7a6581eSFilipe David Manana        emit(doc.name, doc);
205e7a6581eSFilipe David Manana    }
206e7a6581eSFilipe David Manana
207e7a6581eSFilipe David Manana    That property can be used as a function, which will execute the view.
208e7a6581eSFilipe David Manana
209e7a6581eSFilipe David Manana    >>> from couchdb import Database
210e7a6581eSFilipe David Manana    >>> db = Database('python-tests')
211e7a6581eSFilipe David Manana
212e7a6581eSFilipe David Manana    >>> Person.by_name(db, count=3)
213e7a6581eSFilipe David Manana    <ViewResults <PermanentView '_design/people/_view/by_name'> {'count': 3}>
214e7a6581eSFilipe David Manana
215e7a6581eSFilipe David Manana    The results produced by the view are automatically wrapped in the
216e7a6581eSFilipe David Manana    `Document` subclass the descriptor is bound to. In this example, it would
217e7a6581eSFilipe David Manana    return instances of the `Person` class. But please note that this requires
218e7a6581eSFilipe David Manana    the values of the view results to be dictionaries that can be mapped to the
219e7a6581eSFilipe David Manana    mapping defined by the containing `Document` class. Alternatively, the
220e7a6581eSFilipe David Manana    ``include_docs`` query option can be used to inline the actual documents in
221e7a6581eSFilipe David Manana    the view results, which will then be used instead of the values.
222e7a6581eSFilipe David Manana
223e7a6581eSFilipe David Manana    If you use Python view functions, this class can also be used as a
224e7a6581eSFilipe David Manana    decorator:
225e7a6581eSFilipe David Manana
226e7a6581eSFilipe David Manana    >>> class Person(Document):
227e7a6581eSFilipe David Manana    ...     name = TextField()
228e7a6581eSFilipe David Manana    ...     age = IntegerField()
229e7a6581eSFilipe David Manana    ...
230e7a6581eSFilipe David Manana    ...     @ViewField.define('people')
231e7a6581eSFilipe David Manana    ...     def by_name(doc):
232e7a6581eSFilipe David Manana    ...         yield doc['name'], doc
233e7a6581eSFilipe David Manana
234e7a6581eSFilipe David Manana    >>> Person.by_name
235e7a6581eSFilipe David Manana    <ViewDefinition '_design/people/_view/by_name'>
236e7a6581eSFilipe David Manana
237e7a6581eSFilipe David Manana    >>> print Person.by_name.map_fun
238e7a6581eSFilipe David Manana    def by_name(doc):
239e7a6581eSFilipe David Manana        yield doc['name'], doc
240e7a6581eSFilipe David Manana    """
241e7a6581eSFilipe David Manana
242e7a6581eSFilipe David Manana    def __init__(self, design, map_fun, reduce_fun=None, name=None,
243e7a6581eSFilipe David Manana                 language='javascript', wrapper=DEFAULT, **defaults):
244e7a6581eSFilipe David Manana        """Initialize the view descriptor.
245e7a6581eSFilipe David Manana
246e7a6581eSFilipe David Manana        :param design: the name of the design document
247e7a6581eSFilipe David Manana        :param map_fun: the map function code
248e7a6581eSFilipe David Manana        :param reduce_fun: the reduce function code (optional)
249e7a6581eSFilipe David Manana        :param name: the actual name of the view in the design document, if
250e7a6581eSFilipe David Manana                     it differs from the name the descriptor is assigned to
251e7a6581eSFilipe David Manana        :param language: the name of the language used
252e7a6581eSFilipe David Manana        :param wrapper: an optional callable that should be used to wrap the
253e7a6581eSFilipe David Manana                        result rows
254e7a6581eSFilipe David Manana        :param defaults: default query string parameters to apply
255e7a6581eSFilipe David Manana        """
256e7a6581eSFilipe David Manana        self.design = design
257e7a6581eSFilipe David Manana        self.name = name
258e7a6581eSFilipe David Manana        self.map_fun = map_fun
259e7a6581eSFilipe David Manana        self.reduce_fun = reduce_fun
260e7a6581eSFilipe David Manana        self.language = language
261e7a6581eSFilipe David Manana        self.wrapper = wrapper
262e7a6581eSFilipe David Manana        self.defaults = defaults
263e7a6581eSFilipe David Manana
264e7a6581eSFilipe David Manana    @classmethod
265e7a6581eSFilipe David Manana    def define(cls, design, name=None, language='python', wrapper=DEFAULT,
266e7a6581eSFilipe David Manana               **defaults):
267e7a6581eSFilipe David Manana        """Factory method for use as a decorator (only suitable for Python
268e7a6581eSFilipe David Manana        view code).
269e7a6581eSFilipe David Manana        """
270e7a6581eSFilipe David Manana        def view_wrapped(fun):
271e7a6581eSFilipe David Manana            return cls(design, fun, language=language, wrapper=wrapper,
272e7a6581eSFilipe David Manana                       **defaults)
273e7a6581eSFilipe David Manana        return view_wrapped
274e7a6581eSFilipe David Manana
275e7a6581eSFilipe David Manana    def __get__(self, instance, cls=None):
276e7a6581eSFilipe David Manana        if self.wrapper is DEFAULT:
277e7a6581eSFilipe David Manana            wrapper = cls._wrap_row
278e7a6581eSFilipe David Manana        else:
279e7a6581eSFilipe David Manana            wrapper = self.wrapper
280e7a6581eSFilipe David Manana        return ViewDefinition(self.design, self.name, self.map_fun,
281e7a6581eSFilipe David Manana                              self.reduce_fun, language=self.language,
282e7a6581eSFilipe David Manana                              wrapper=wrapper, **self.defaults)
283e7a6581eSFilipe David Manana
284e7a6581eSFilipe David Manana
285e7a6581eSFilipe David Mananaclass DocumentMeta(MappingMeta):
286e7a6581eSFilipe David Manana
287e7a6581eSFilipe David Manana    def __new__(cls, name, bases, d):
288e7a6581eSFilipe David Manana        for attrname, attrval in d.items():
289e7a6581eSFilipe David Manana            if isinstance(attrval, ViewField):
290e7a6581eSFilipe David Manana                if not attrval.name:
291e7a6581eSFilipe David Manana                    attrval.name = attrname
292e7a6581eSFilipe David Manana        return MappingMeta.__new__(cls, name, bases, d)
293e7a6581eSFilipe David Manana
294e7a6581eSFilipe David Manana
295e7a6581eSFilipe David Mananaclass Document(Mapping):
296e7a6581eSFilipe David Manana    __metaclass__ = DocumentMeta
297e7a6581eSFilipe David Manana
298e7a6581eSFilipe David Manana    def __init__(self, id=None, **values):
299e7a6581eSFilipe David Manana        Mapping.__init__(self, **values)
300e7a6581eSFilipe David Manana        if id is not None:
301e7a6581eSFilipe David Manana            self.id = id
302e7a6581eSFilipe David Manana
303e7a6581eSFilipe David Manana    def __repr__(self):
304e7a6581eSFilipe David Manana        return '<%s %r@%r %r>' % (type(self).__name__, self.id, self.rev,
305e7a6581eSFilipe David Manana                                  dict([(k, v) for k, v in self._data.items()
306e7a6581eSFilipe David Manana                                        if k not in ('_id', '_rev')]))
307e7a6581eSFilipe David Manana
308e7a6581eSFilipe David Manana    def _get_id(self):
309e7a6581eSFilipe David Manana        if hasattr(self._data, 'id'): # When data is client.Document
310e7a6581eSFilipe David Manana            return self._data.id
311e7a6581eSFilipe David Manana        return self._data.get('_id')
312e7a6581eSFilipe David Manana    def _set_id(self, value):
313e7a6581eSFilipe David Manana        if self.id is not None:
314e7a6581eSFilipe David Manana            raise AttributeError('id can only be set on new documents')
315e7a6581eSFilipe David Manana        self._data['_id'] = value
316e7a6581eSFilipe David Manana    id = property(_get_id, _set_id, doc='The document ID')
317e7a6581eSFilipe David Manana
318e7a6581eSFilipe David Manana    @property
319e7a6581eSFilipe David Manana    def rev(self):
320e7a6581eSFilipe David Manana        """The document revision.
321e7a6581eSFilipe David Manana
322e7a6581eSFilipe David Manana        :rtype: basestring
323e7a6581eSFilipe David Manana        """
324e7a6581eSFilipe David Manana        if hasattr(self._data, 'rev'): # When data is client.Document
325e7a6581eSFilipe David Manana            return self._data.rev
326e7a6581eSFilipe David Manana        return self._data.get('_rev')
327e7a6581eSFilipe David Manana
328e7a6581eSFilipe David Manana    def items(self):
329e7a6581eSFilipe David Manana        """Return the fields as a list of ``(name, value)`` tuples.
330e7a6581eSFilipe David Manana
331e7a6581eSFilipe David Manana        This method is provided to enable easy conversion to native dictionary
332e7a6581eSFilipe David Manana        objects, for example to allow use of `mapping.Document` instances with
333e7a6581eSFilipe David Manana        `client.Database.update`.
334e7a6581eSFilipe David Manana
335e7a6581eSFilipe David Manana        >>> class Post(Document):
336e7a6581eSFilipe David Manana        ...     title = TextField()
337e7a6581eSFilipe David Manana        ...     author = TextField()
338e7a6581eSFilipe David Manana        >>> post = Post(id='foo-bar', title='Foo bar', author='Joe')
339e7a6581eSFilipe David Manana        >>> sorted(post.items())
340e7a6581eSFilipe David Manana        [('_id', 'foo-bar'), ('author', u'Joe'), ('title', u'Foo bar')]
341e7a6581eSFilipe David Manana
342e7a6581eSFilipe David Manana        :return: a list of ``(name, value)`` tuples
343e7a6581eSFilipe David Manana        """
344e7a6581eSFilipe David Manana        retval = []
345e7a6581eSFilipe David Manana        if self.id is not None:
346e7a6581eSFilipe David Manana            retval.append(('_id', self.id))
347e7a6581eSFilipe David Manana            if self.rev is not None:
348e7a6581eSFilipe David Manana                retval.append(('_rev', self.rev))
349e7a6581eSFilipe David Manana        for name, value in self._data.items():
350e7a6581eSFilipe David Manana            if name not in ('_id', '_rev'):
351e7a6581eSFilipe David Manana                retval.append((name, value))
352e7a6581eSFilipe David Manana        return retval
353e7a6581eSFilipe David Manana
354e7a6581eSFilipe David Manana    @classmethod
355e7a6581eSFilipe David Manana    def load(cls, db, id):
356e7a6581eSFilipe David Manana        """Load a specific document from the given database.
357e7a6581eSFilipe David Manana
358e7a6581eSFilipe David Manana        :param db: the `Database` object to retrieve the document from
359e7a6581eSFilipe David Manana        :param id: the document ID
360e7a6581eSFilipe David Manana        :return: the `Document` instance, or `None` if no document with the
361e7a6581eSFilipe David Manana                 given ID was found
362e7a6581eSFilipe David Manana        """
363e7a6581eSFilipe David Manana        doc = db.get(id)
364e7a6581eSFilipe David Manana        if doc is None:
365e7a6581eSFilipe David Manana            return None
366e7a6581eSFilipe David Manana        return cls.wrap(doc)
367e7a6581eSFilipe David Manana
368e7a6581eSFilipe David Manana    def store(self, db):
369e7a6581eSFilipe David Manana        """Store the document in the given database."""
370e7a6581eSFilipe David Manana        db.save(self._data)
371e7a6581eSFilipe David Manana        return self
372e7a6581eSFilipe David Manana
373e7a6581eSFilipe David Manana    @classmethod
374e7a6581eSFilipe David Manana    def query(cls, db, map_fun, reduce_fun, language='javascript', **options):
375e7a6581eSFilipe David Manana        """Execute a CouchDB temporary view and map the result values back to
376e7a6581eSFilipe David Manana        objects of this mapping.
377e7a6581eSFilipe David Manana
378e7a6581eSFilipe David Manana        Note that by default, any properties of the document that are not
379e7a6581eSFilipe David Manana        included in the values of the view will be treated as if they were
380e7a6581eSFilipe David Manana        missing from the document. If you want to load the full document for
381e7a6581eSFilipe David Manana        every row, set the ``include_docs`` option to ``True``.
382e7a6581eSFilipe David Manana        """
383e7a6581eSFilipe David Manana        return db.query(map_fun, reduce_fun=reduce_fun, language=language,
384e7a6581eSFilipe David Manana                        wrapper=cls._wrap_row, **options)
385e7a6581eSFilipe David Manana
386e7a6581eSFilipe David Manana    @classmethod
387e7a6581eSFilipe David Manana    def view(cls, db, viewname, **options):
388e7a6581eSFilipe David Manana        """Execute a CouchDB named view and map the result values back to
389e7a6581eSFilipe David Manana        objects of this mapping.
390e7a6581eSFilipe David Manana
391e7a6581eSFilipe David Manana        Note that by default, any properties of the document that are not
392e7a6581eSFilipe David Manana        included in the values of the view will be treated as if they were
393e7a6581eSFilipe David Manana        missing from the document. If you want to load the full document for
394e7a6581eSFilipe David Manana        every row, set the ``include_docs`` option to ``True``.
395e7a6581eSFilipe David Manana        """
396e7a6581eSFilipe David Manana        return db.view(viewname, wrapper=cls._wrap_row, **options)
397e7a6581eSFilipe David Manana
398e7a6581eSFilipe David Manana    @classmethod
399e7a6581eSFilipe David Manana    def _wrap_row(cls, row):
400e7a6581eSFilipe David Manana        doc = row.get('doc')
401e7a6581eSFilipe David Manana        if doc is not None:
402e7a6581eSFilipe David Manana            return cls.wrap(doc)
403e7a6581eSFilipe David Manana        data = row['value']
404e7a6581eSFilipe David Manana        data['_id'] = row['id']
405e7a6581eSFilipe David Manana        return cls.wrap(data)
406e7a6581eSFilipe David Manana
407e7a6581eSFilipe David Manana
408e7a6581eSFilipe David Mananaclass TextField(Field):
409e7a6581eSFilipe David Manana    """Mapping field for string values."""
410e7a6581eSFilipe David Manana    _to_python = unicode
411e7a6581eSFilipe David Manana
412e7a6581eSFilipe David Manana
413e7a6581eSFilipe David Mananaclass FloatField(Field):
414e7a6581eSFilipe David Manana    """Mapping field for float values."""
415e7a6581eSFilipe David Manana    _to_python = float
416e7a6581eSFilipe David Manana
417e7a6581eSFilipe David Manana
418e7a6581eSFilipe David Mananaclass IntegerField(Field):
419e7a6581eSFilipe David Manana    """Mapping field for integer values."""
420e7a6581eSFilipe David Manana    _to_python = int
421e7a6581eSFilipe David Manana
422e7a6581eSFilipe David Manana
423e7a6581eSFilipe David Mananaclass LongField(Field):
424e7a6581eSFilipe David Manana    """Mapping field for long integer values."""
425e7a6581eSFilipe David Manana    _to_python = long
426e7a6581eSFilipe David Manana
427e7a6581eSFilipe David Manana
428e7a6581eSFilipe David Mananaclass BooleanField(Field):
429e7a6581eSFilipe David Manana    """Mapping field for boolean values."""
430e7a6581eSFilipe David Manana    _to_python = bool
431e7a6581eSFilipe David Manana
432e7a6581eSFilipe David Manana
433e7a6581eSFilipe David Mananaclass DecimalField(Field):
434e7a6581eSFilipe David Manana    """Mapping field for decimal values."""
435e7a6581eSFilipe David Manana
436e7a6581eSFilipe David Manana    def _to_python(self, value):
437e7a6581eSFilipe David Manana        return Decimal(value)
438e7a6581eSFilipe David Manana
439e7a6581eSFilipe David Manana    def _to_json(self, value):
440e7a6581eSFilipe David Manana        return unicode(value)
441e7a6581eSFilipe David Manana
442e7a6581eSFilipe David Manana
443e7a6581eSFilipe David Mananaclass DateField(Field):
444e7a6581eSFilipe David Manana    """Mapping field for storing dates.
445e7a6581eSFilipe David Manana
446e7a6581eSFilipe David Manana    >>> field = DateField()
447e7a6581eSFilipe David Manana    >>> field._to_python('2007-04-01')
448e7a6581eSFilipe David Manana    datetime.date(2007, 4, 1)
449e7a6581eSFilipe David Manana    >>> field._to_json(date(2007, 4, 1))
450e7a6581eSFilipe David Manana    '2007-04-01'
451e7a6581eSFilipe David Manana    >>> field._to_json(datetime(2007, 4, 1, 15, 30))
452e7a6581eSFilipe David Manana    '2007-04-01'
453e7a6581eSFilipe David Manana    """
454e7a6581eSFilipe David Manana
455e7a6581eSFilipe David Manana    def _to_python(self, value):
456e7a6581eSFilipe David Manana        if isinstance(value, basestring):
457e7a6581eSFilipe David Manana            try:
458e7a6581eSFilipe David Manana                value = date(*strptime(value, '%Y-%m-%d')[:3])
459e7a6581eSFilipe David Manana            except ValueError:
460e7a6581eSFilipe David Manana                raise ValueError('Invalid ISO date %r' % value)
461e7a6581eSFilipe David Manana        return value
462e7a6581eSFilipe David Manana
463e7a6581eSFilipe David Manana    def _to_json(self, value):
464e7a6581eSFilipe David Manana        if isinstance(value, datetime):
465e7a6581eSFilipe David Manana            value = value.date()
466e7a6581eSFilipe David Manana        return value.isoformat()
467e7a6581eSFilipe David Manana
468e7a6581eSFilipe David Manana
469e7a6581eSFilipe David Mananaclass DateTimeField(Field):
470e7a6581eSFilipe David Manana    """Mapping field for storing date/time values.
471e7a6581eSFilipe David Manana
472e7a6581eSFilipe David Manana    >>> field = DateTimeField()
473e7a6581eSFilipe David Manana    >>> field._to_python('2007-04-01T15:30:00Z')
474e7a6581eSFilipe David Manana    datetime.datetime(2007, 4, 1, 15, 30)
475e7a6581eSFilipe David Manana    >>> field._to_json(datetime(2007, 4, 1, 15, 30, 0, 9876))
476e7a6581eSFilipe David Manana    '2007-04-01T15:30:00Z'
477e7a6581eSFilipe David Manana    >>> field._to_json(date(2007, 4, 1))
478e7a6581eSFilipe David Manana    '2007-04-01T00:00:00Z'
479e7a6581eSFilipe David Manana    """
480e7a6581eSFilipe David Manana
481e7a6581eSFilipe David Manana    def _to_python(self, value):
482e7a6581eSFilipe David Manana        if isinstance(value, basestring):
483e7a6581eSFilipe David Manana            try:
484e7a6581eSFilipe David Manana                value = value.split('.', 1)[0] # strip out microseconds
485e7a6581eSFilipe David Manana                value = value.rstrip('Z') # remove timezone separator
486e7a6581eSFilipe David Manana                value = datetime(*strptime(value, '%Y-%m-%dT%H:%M:%S')[:6])
487e7a6581eSFilipe David Manana            except ValueError:
488e7a6581eSFilipe David Manana                raise ValueError('Invalid ISO date/time %r' % value)
489e7a6581eSFilipe David Manana        return value
490e7a6581eSFilipe David Manana
491e7a6581eSFilipe David Manana    def _to_json(self, value):
492e7a6581eSFilipe David Manana        if isinstance(value, struct_time):
493e7a6581eSFilipe David Manana            value = datetime.utcfromtimestamp(timegm(value))
494e7a6581eSFilipe David Manana        elif not isinstance(value, datetime):
495e7a6581eSFilipe David Manana            value = datetime.combine(value, time(0))
496e7a6581eSFilipe David Manana        return value.replace(microsecond=0).isoformat() + 'Z'
497e7a6581eSFilipe David Manana
498e7a6581eSFilipe David Manana
499e7a6581eSFilipe David Mananaclass TimeField(Field):
500e7a6581eSFilipe David Manana    """Mapping field for storing times.
501e7a6581eSFilipe David Manana
502e7a6581eSFilipe David Manana    >>> field = TimeField()
503e7a6581eSFilipe David Manana    >>> field._to_python('15:30:00')
504e7a6581eSFilipe David Manana    datetime.time(15, 30)
505e7a6581eSFilipe David Manana    >>> field._to_json(time(15, 30))
506e7a6581eSFilipe David Manana    '15:30:00'
507e7a6581eSFilipe David Manana    >>> field._to_json(datetime(2007, 4, 1, 15, 30))
508e7a6581eSFilipe David Manana    '15:30:00'
509e7a6581eSFilipe David Manana    """
510e7a6581eSFilipe David Manana
511e7a6581eSFilipe David Manana    def _to_python(self, value):
512e7a6581eSFilipe David Manana        if isinstance(value, basestring):
513e7a6581eSFilipe David Manana            try:
514e7a6581eSFilipe David Manana                value = value.split('.', 1)[0] # strip out microseconds
515e7a6581eSFilipe David Manana                value = time(*strptime(value, '%H:%M:%S')[3:6])
516e7a6581eSFilipe David Manana            except ValueError:
517e7a6581eSFilipe David Manana                raise ValueError('Invalid ISO time %r' % value)
518e7a6581eSFilipe David Manana        return value
519e7a6581eSFilipe David Manana
520e7a6581eSFilipe David Manana    def _to_json(self, value):
521e7a6581eSFilipe David Manana        if isinstance(value, datetime):
522e7a6581eSFilipe David Manana            value = value.time()
523e7a6581eSFilipe David Manana        return value.replace(microsecond=0).isoformat()
524e7a6581eSFilipe David Manana
525e7a6581eSFilipe David Manana
526e7a6581eSFilipe David Mananaclass DictField(Field):
527e7a6581eSFilipe David Manana    """Field type for nested dictionaries.
528e7a6581eSFilipe David Manana
529e7a6581eSFilipe David Manana    >>> from couchdb import Server
530e7a6581eSFilipe David Manana    >>> server = Server()
531e7a6581eSFilipe David Manana    >>> db = server.create('python-tests')
532e7a6581eSFilipe David Manana
533e7a6581eSFilipe David Manana    >>> class Post(Document):
534e7a6581eSFilipe David Manana    ...     title = TextField()
535e7a6581eSFilipe David Manana    ...     content = TextField()
536e7a6581eSFilipe David Manana    ...     author = DictField(Mapping.build(
537e7a6581eSFilipe David Manana    ...         name = TextField(),
538e7a6581eSFilipe David Manana    ...         email = TextField()
539e7a6581eSFilipe David Manana    ...     ))
540e7a6581eSFilipe David Manana    ...     extra = DictField()
541e7a6581eSFilipe David Manana
542e7a6581eSFilipe David Manana    >>> post = Post(
543e7a6581eSFilipe David Manana    ...     title='Foo bar',
544e7a6581eSFilipe David Manana    ...     author=dict(name='John Doe',
545e7a6581eSFilipe David Manana    ...                 email='john@doe.com'),
546e7a6581eSFilipe David Manana    ...     extra=dict(foo='bar'),
547e7a6581eSFilipe David Manana    ... )
548e7a6581eSFilipe David Manana    >>> post.store(db) #doctest: +ELLIPSIS
549e7a6581eSFilipe David Manana    <Post ...>
550e7a6581eSFilipe David Manana    >>> post = Post.load(db, post.id)
551e7a6581eSFilipe David Manana    >>> post.author.name
552e7a6581eSFilipe David Manana    u'John Doe'
553e7a6581eSFilipe David Manana    >>> post.author.email
554e7a6581eSFilipe David Manana    u'john@doe.com'
555e7a6581eSFilipe David Manana    >>> post.extra
556e7a6581eSFilipe David Manana    {'foo': 'bar'}
557e7a6581eSFilipe David Manana
558e7a6581eSFilipe David Manana    >>> del server['python-tests']
559e7a6581eSFilipe David Manana    """
560e7a6581eSFilipe David Manana    def __init__(self, mapping=None, name=None, default=None):
561e7a6581eSFilipe David Manana        default = default or {}
562e7a6581eSFilipe David Manana        Field.__init__(self, name=name, default=lambda: default.copy())
563e7a6581eSFilipe David Manana        self.mapping = mapping
564e7a6581eSFilipe David Manana
565e7a6581eSFilipe David Manana    def _to_python(self, value):
566e7a6581eSFilipe David Manana        if self.mapping is None:
567e7a6581eSFilipe David Manana            return value
568e7a6581eSFilipe David Manana        else:
569e7a6581eSFilipe David Manana            return self.mapping.wrap(value)
570e7a6581eSFilipe David Manana
571e7a6581eSFilipe David Manana    def _to_json(self, value):
572e7a6581eSFilipe David Manana        if self.mapping is None:
573e7a6581eSFilipe David Manana            return value
574e7a6581eSFilipe David Manana        if not isinstance(value, Mapping):
575e7a6581eSFilipe David Manana            value = self.mapping(**value)
576e7a6581eSFilipe David Manana        return value.unwrap()
577e7a6581eSFilipe David Manana
578e7a6581eSFilipe David Manana
579e7a6581eSFilipe David Mananaclass ListField(Field):
580e7a6581eSFilipe David Manana    """Field type for sequences of other fields.
581e7a6581eSFilipe David Manana
582e7a6581eSFilipe David Manana    >>> from couchdb import Server
583e7a6581eSFilipe David Manana    >>> server = Server()
584e7a6581eSFilipe David Manana    >>> db = server.create('python-tests')
585e7a6581eSFilipe David Manana
586e7a6581eSFilipe David Manana    >>> class Post(Document):
587e7a6581eSFilipe David Manana    ...     title = TextField()
588e7a6581eSFilipe David Manana    ...     content = TextField()
589e7a6581eSFilipe David Manana    ...     pubdate = DateTimeField(default=datetime.now)
590e7a6581eSFilipe David Manana    ...     comments = ListField(DictField(Mapping.build(
591e7a6581eSFilipe David Manana    ...         author = TextField(),
592e7a6581eSFilipe David Manana    ...         content = TextField(),
593e7a6581eSFilipe David Manana    ...         time = DateTimeField()
594e7a6581eSFilipe David Manana    ...     )))
595e7a6581eSFilipe David Manana
596e7a6581eSFilipe David Manana    >>> post = Post(title='Foo bar')
597e7a6581eSFilipe David Manana    >>> post.comments.append(author='myself', content='Bla bla',
598e7a6581eSFilipe David Manana    ...                      time=datetime.now())
599e7a6581eSFilipe David Manana    >>> len(post.comments)
600e7a6581eSFilipe David Manana    1
601e7a6581eSFilipe David Manana    >>> post.store(db) #doctest: +ELLIPSIS
602e7a6581eSFilipe David Manana    <Post ...>
603e7a6581eSFilipe David Manana    >>> post = Post.load(db, post.id)
604e7a6581eSFilipe David Manana    >>> comment = post.comments[0]
605e7a6581eSFilipe David Manana    >>> comment['author']
606e7a6581eSFilipe David Manana    'myself'
607e7a6581eSFilipe David Manana    >>> comment['content']
608e7a6581eSFilipe David Manana    'Bla bla'
609e7a6581eSFilipe David Manana    >>> comment['time'] #doctest: +ELLIPSIS
610e7a6581eSFilipe David Manana    '...T...Z'
611e7a6581eSFilipe David Manana
612e7a6581eSFilipe David Manana    >>> del server['python-tests']
613e7a6581eSFilipe David Manana    """
614e7a6581eSFilipe David Manana
615e7a6581eSFilipe David Manana    def __init__(self, field, name=None, default=None):
616e7a6581eSFi