Merge lp:~jderose/microfiber/doctest into lp:microfiber

Proposed by Jason Gerard DeRose
Status: Merged
Merged at revision: 228
Proposed branch: lp:~jderose/microfiber/doctest
Merge into: lp:microfiber
Diff against target: 1423 lines (+828/-380)
4 files modified
.bzrignore (+1/-1)
doc/couchdb_api.rst (+679/-287)
doc/microfiber.rst (+133/-92)
setup.py (+15/-0)
To merge this branch: bzr merge lp:~jderose/microfiber/doctest
Reviewer Review Type Date Requested Status
David Jordan Approve
Review via email: mp+207814@code.launchpad.net

Description of the change

At long last, this is getting fixed! A few notes to help understand the diff:

1) We've always run doctests for examples in the docstrings in the Microfiber source code itself... this change only enables doctests for examples in the Sphinx documentation (doc/*.rst), and fixes those examples so they actually pass

2) When the Microfiber documentation was originally written, we didn't have a way to create throw-away CouchDB instances for unit testing and for docstrings, but now we have usercouch.misc.TempCouch, so lot of the diff is just adding this sort of thing:

>>> from usercouch.misc import TempCouch
>>> couch = TempCouch()
>>> env = couch.bootstrap()
>>> from microfiber import Server, Database
>>> server = Server(env)
>>> db = Database('mydb', env)

3) `setup.py test` will now run the Sphinx doctests under the invoking Python interpreter; the reason for this is so these tests get run all `py3versions` we're building for; for example, currently in trusty `setup.py test` will get called both for python3.3 and python3.4; however, we only build the HTML documentation once, under python3.4, which is the default python3 version in Trusty; this will make more sense if you look at our debian/rules:

  http://bazaar.launchpad.net/~microfiber/microfiber/trunk/view/head:/debian/rules

4) The big motivator is now that Microfiber has it's own CouchDB replicator, this is a good opportunity to clearly document the CouchDB replication protocol with examples that get automatically tested! (But note the replicator docstring examples are currently only in microfiber/replicator.py, aren't yet in the Sphinx documentation.)

And some things that still need to be fixed, but were left out as this merge is already pretty big:

1) doc/microfiber.rst still has a lot of "doctest: +SKIP" in places where we should have unit tests that actually run thanks to TempCouch; but for now, I tried to focus on the minimal change needed to get all existing Sphinx doctests running and passing

2) Likewise, microfiber/__init__.py still has a lot of "doctest: +SKIP" in places where we should have unit tests that actually run thanks to TempCouch

3) I couldn't resist doing at least some stylistic improvement in doc/couchdb_api.rst, but I restrained myself from doing this everywhere for the sake of keeping this merge of a sane size; in a later merge we should finish this work and make all the documentation consistent.

To post a comment you must log in.
Revision history for this message
Zygmunt Krynicki (zyga) wrote :

1401 +def run_sphinx_doctest():
1402 + sphinx_build = '/usr/share/sphinx/scripts/python3/sphinx-build'

How about executing setup.py with build_shpinx -b doctest?

Revision history for this message
Jason Gerard DeRose (jderose) wrote :

Zygmunt,

So do you mean add a custom "build_sphinx" command to setup.py, or is there some magical setup command included in sphinx that I should be using?

I have mixed feelings about how I'm currently running the doctests from setup.py, but I think it works well enough for a first-pass anyway. I hate using the hard-coded sphinx-build path, but that's the only way to get it to do the right thing if you have both python3-sphinx and python-sphinx installed (which is a common scenario for developers, I think).

On the build servers it's not a problem because python-sphinx wont be installed, as we don't build-depend on it.

Anyway, thanks for taking a look!

Revision history for this message
David Jordan (dmj726) wrote :

Approved.

review: Approve
Revision history for this message
Zygmunt Krynicki (zyga) wrote :

Jason: there is a build_sphinx command in setup() if you use setuptools, yes. If you're building documentation depending on python3-sphinx is a simple way to get that done. See lp:checkbox (see plainbox inside for an example)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2013-02-21 01:24:23 +0000
3+++ .bzrignore 2014-02-23 01:42:08 +0000
4@@ -1,2 +1,2 @@
5 build/
6-doc/_build/
7+doc/_build/*
8
9=== modified file 'doc/couchdb_api.rst'
10--- doc/couchdb_api.rst 2012-09-22 12:46:46 +0000
11+++ doc/couchdb_api.rst 2014-02-23 01:42:08 +0000
12@@ -4,11 +4,17 @@
13
14 .. py:currentmodule:: microfiber
15
16-This is a quick tour of the CouchDB REST API, as you would use it from
17-Microfiber. This is indented as a quick reference, and as such not all the API
18-is documented here. For more details, see the full `CouchDB REST API`_.
19-
20-.. _`CouchDB REST API`: http://couchdb.readthedocs.org/en/latest/index.html
21+This is a tour of some key aspects of CouchDB REST API, as used from Microfiber.
22+This is intended as a quick reference, and as such not all the API is documented
23+here. For that, see the full `CouchDB REST API`_ documentation.
24+
25+All the examples below need the following setup:
26+
27+>>> from usercouch.misc import TempCouch
28+>>> from microfiber import Database, Server, dumps
29+
30+.. _`CouchDB REST API`: http://couchdb.readthedocs.org/en/latest/api/index.html
31+
32
33
34 Databases
35@@ -18,108 +24,201 @@
36 instance, but you can do the same using a :class:`Server` instance.
37
38
39-Create
40-------
41-
42-**PUT /db**
43+``PUT /db``
44+-----------
45
46 This will create a new CouchDB database. A :exc:`PreconditionFailed` exception
47 is raised if a database with the same name already exists.
48
49-Using a :class:`Database`:
50-
51->>> db = Database('mydb')
52->>> db.put(None)
53-{'ok': True}
54-
55-
56-Or using a :class:`Server`:
57-
58->>> s = Server()
59->>> s.put(None, 'mydb')
60-{'ok': True}
61-
62-
63-Retrieve
64---------
65-
66-**GET /db**
67-
68-This will retrieve useful information about the database. A :exc:`NotFound`
69-exception is raised if the database does not exist.
70-
71-Using a :class:`Database`:
72-
73->>> db = Database('mydb')
74+As setup for our examples, we'll do this:
75+
76+>>> couch = TempCouch()
77+>>> env = couch.bootstrap()
78+>>> server = Server(env)
79+>>> db = Database('db2', env)
80+
81+Now we'll create "db1" using our :class:`Server`:
82+
83+>>> server.put(None, 'db1')
84+{'ok': True}
85+
86+And then we'll create "db2" using our :class:`Database`:
87+
88+>>> db.put(None)
89+{'ok': True}
90+
91+A :exc:`PreconditionFailed` will be raised if we try again to create "db1":
92+
93+>>> server.put(None, 'db1')
94+Traceback (most recent call last):
95+ ...
96+microfiber.PreconditionFailed: 412 Precondition Failed: PUT /db1
97+
98+Likewise, a :exc:`PreconditionFailed` will be raised if we try again to
99+create "db2":
100+
101+>>> db.put(None)
102+Traceback (most recent call last):
103+ ...
104+microfiber.PreconditionFailed: 412 Precondition Failed: PUT /db2/
105+
106+
107+``GET /db``
108+-----------
109+
110+This will return a ``dict`` with useful information about the database. A
111+:exc:`NotFound` exception is raised if the database does not exist.
112+
113+Using a :class:`Database`, when the database does *not* exist:
114+
115+>>> couch = TempCouch()
116+>>> db = Database('mydb', couch.bootstrap())
117 >>> db.get()
118-{'update_seq': 0, 'disk_size': 79, 'purge_seq': 0, 'doc_count': 0, 'compact_running': False, 'db_name': 'mydb', 'doc_del_count': 0, 'instance_start_time': '1314934043214745', 'committed_update_seq': 0, 'disk_format_version': 5}
119-
120-
121-Or using a :class:`Server`:
122-
123->>> s = Server(env)
124+Traceback (most recent call last):
125+ ...
126+microfiber.NotFound: 404 Object Not Found: GET /mydb/
127+
128+Or using a :class:`Database`, when the database exists:
129+
130+>>> db.put(None)
131+{'ok': True}
132+>>> sorted(db.get())
133+['committed_update_seq', 'compact_running', 'data_size', 'db_name', 'disk_format_version', 'disk_size', 'doc_count', 'doc_del_count', 'instance_start_time', 'purge_seq', 'update_seq']
134+
135+Using a :class:`Server`, when the database does *not* exist:
136+
137+>>> couch = TempCouch()
138+>>> s = Server(couch.bootstrap())
139 >>> s.get('mydb')
140-{'update_seq': 0, 'disk_size': 79, 'purge_seq': 0, 'doc_count': 0, 'compact_running': False, 'db_name': 'mydb', 'doc_del_count': 0, 'instance_start_time': '1314934043214745', 'committed_update_seq': 0, 'disk_format_version': 5}
141-
142-
143-Changes
144--------
145-
146-**GET /db/_changes**
147+Traceback (most recent call last):
148+ ...
149+microfiber.NotFound: 404 Object Not Found: GET /mydb/
150+
151+Or using a :class:`Server`, when the database exists:
152+
153+>>> s.put(None, 'mydb')
154+{'ok': True}
155+>>> sorted(s.get('mydb'))
156+['committed_update_seq', 'compact_running', 'data_size', 'db_name', 'disk_format_version', 'disk_size', 'doc_count', 'doc_del_count', 'instance_start_time', 'purge_seq', 'update_seq']
157+
158+
159+``GET /db/_changes``
160+--------------------
161
162 Using a :class:`Database`:
163
164->>> db = Database('mydb')
165->>> db.get('_changes')
166-{'last_seq': 0, 'results': []}
167-
168+>>> couch = TempCouch()
169+>>> env = couch.bootstrap()
170+>>> db = Database('mydb', env)
171+>>> db.put(None)
172+{'ok': True}
173+>>> doc = {'_id': 'mydoc'}
174+>>> doc['_rev'] = db.post(doc)['rev']
175+>>> changes = db.get('_changes')
176+>>> print(dumps(changes, pretty=True))
177+{
178+ "last_seq": 1,
179+ "results": [
180+ {
181+ "changes": [
182+ {
183+ "rev": "1-967a00dff5e02add41819138abb3284d"
184+ }
185+ ],
186+ "id": "mydoc",
187+ "seq": 1
188+ }
189+ ]
190+}
191
192 Or using a :class:`Server`:
193
194->>> s = Server()
195->>> s.get('mydb', '_changes')
196-{'last_seq': 0, 'results': []}
197-
198-
199-Compact
200--------
201-
202-**POST /db/_compact**
203-
204-Using a :class:`Database`:
205-
206->>> db = Database('mydb')
207+>>> s = Server(env)
208+>>> changes = s.get('mydb', '_changes')
209+>>> print(dumps(changes, pretty=True))
210+{
211+ "last_seq": 1,
212+ "results": [
213+ {
214+ "changes": [
215+ {
216+ "rev": "1-967a00dff5e02add41819138abb3284d"
217+ }
218+ ],
219+ "id": "mydoc",
220+ "seq": 1
221+ }
222+ ]
223+}
224+
225+
226+``POST /db/_compact``
227+---------------------
228+
229+This will trigger database compaction. Note this has no effect if compaction
230+is already running (in other words, only a single compaction task will ever be
231+running per database). As setup for our examples, we'll do this:
232+
233+>>> couch = TempCouch()
234+>>> env = couch.bootstrap()
235+>>> server = Server(env)
236+>>> server.put(None, 'db1')
237+{'ok': True}
238+>>> db = Database('db2', env)
239+>>> db.put(None)
240+{'ok': True}
241+
242+To compact "db1" using our :class:`Server`:
243+
244+>>> server.post(None, 'db1', '_compact')
245+{'ok': True}
246+
247+And to compact "db2" using our :class:`Database`:
248+
249 >>> db.post(None, '_compact')
250 {'ok': True}
251
252
253-Or using a :class:`Server`:
254-
255->>> s = Server()
256->>> s.post(None, 'mydb', '_compact')
257-{'ok': True}
258-
259-
260-Delete
261-------
262-
263-**DELETE /db**
264-
265-This will delete the CouchDB database. A :exc:`NotFound` exception is raised if
266-the database does not exist.
267-
268-Using a :class:`Database`:
269-
270->>> db = Database('mydb')
271->>> db.delete()
272-{'ok': True}
273-
274-
275-Or using a :class:`Server`:
276-
277->>> s = Server()
278->>> s.delete('mydb')
279-{'ok': True}
280+``DELETE /db``
281+--------------
282+
283+This will delete the CouchDB database. As setup for our examples, we'll do
284+this:
285+
286+>>> couch = TempCouch()
287+>>> env = couch.bootstrap()
288+>>> server = Server(env)
289+>>> server.put(None, 'db1')
290+{'ok': True}
291+>>> db = Database('db2', env)
292+>>> db.put(None)
293+{'ok': True}
294+
295+For example, to delete "db1" using our :class:`Server`:
296+
297+>>> server.delete('db1')
298+{'ok': True}
299+
300+Or to delete "db2' using our :class:`Database`:
301+
302+>>> db.delete()
303+{'ok': True}
304+
305+A :exc:`NotFound` exception is raised if the database does not exist. For
306+example, if we try to delete the now non-existent "db1" using our
307+:class:`Server`:
308+
309+>>> server.delete('db1')
310+Traceback (most recent call last):
311+ ...
312+microfiber.NotFound: 404 Object Not Found: DELETE /db1
313+
314+And if we try to delete the now non-existent "db2" using our :class:`Database`:
315+
316+>>> db.delete()
317+Traceback (most recent call last):
318+ ...
319+microfiber.NotFound: 404 Object Not Found: DELETE /db2
320
321
322
323@@ -130,99 +229,298 @@
324 instance, but you can do the same using a :class:`Server` instance.
325
326
327-Create
328-------
329-
330-**POST /db**
331-
332-This will create a new document. A :exc:`Conflict` exception is raised if the
333-document already exists.
334-
335-Using a :class:`Database`:
336-
337->>> db = Database('mydb')
338->>> db.post({'_id': 'mydoc'})
339-{'rev': '1-967a00dff5e02add41819138abb3284d', 'ok': True, 'id': 'mydoc'}
340-
341-
342-Or using a :class:`Server`:
343-
344->>> s = Server()
345->>> s.post({'_id': 'mydoc'}, 'mydb')
346-{'rev': '1-967a00dff5e02add41819138abb3284d', 'ok': True, 'id': 'mydoc'}
347-
348-
349-
350-Update
351-------
352-
353-**POST /db**
354-
355-This will update an existing document. A :exc:`Conflict` exception is raised if
356-``doc['_rev']`` doesn't match the latest revision of the document in CouchDB
357-(meaning the document has been updated since you last retrieved it).
358-
359-Using a :class:`Database`:
360-
361->>> db = Database('mydb')
362->>> db.post({'_id': 'mydoc', '_rev': '1-967a00dff5e02add41819138abb3284d'})
363-{'rev': '2-7051cbe5c8faecd085a3fa619e6e6337', 'ok': True, 'id': 'mydoc'}
364-
365-
366-Or using a :class:`Server`:
367-
368->>> s = Server()
369->>> s.post({'_id': 'mydoc', '_rev': '1-967a00dff5e02add41819138abb3284d'}, 'mydb')
370-{'rev': '2-7051cbe5c8faecd085a3fa619e6e6337', 'ok': True, 'id': 'mydoc'}
371-
372-
373-
374-Retrieve
375---------
376-
377-**GET /db/doc**
378-
379-A :exc:`NotFound` exception is raised if the document does not exist.
380-
381-Using a :class:`Database`:
382-
383->>> db = Database('mydb')
384+``PUT /db/doc``
385+---------------
386+
387+This can be used to create a new document, and likewise to update an existing
388+document.
389+
390+.. note::
391+
392+ :meth:`Database.save()` is a better way to create and update documents as
393+ it will automatically update ``doc['_rev']`` in-place for you
394+
395+As setup for our examples, we'll do this:
396+
397+>>> couch = TempCouch()
398+>>> env = couch.bootstrap()
399+>>> server = Server(env)
400+>>> db = Database('mydb', env)
401+>>> db.put(None)
402+{'ok': True}
403+
404+For example, we'll create "doc1" using our :class:`Server`:
405+
406+>>> server.put({'foo': 'bar'}, 'mydb', 'doc1')['rev']
407+'1-4c6114c65e295552ab1019e2b046b10e'
408+
409+And we'll create "doc2" using our :class:`Database`:
410+
411+>>> db.put({'foo': 'bar'}, 'doc2')['rev']
412+'1-4c6114c65e295552ab1019e2b046b10e'
413+
414+
415+``POST /db``
416+------------
417+
418+This can be used to create a new document, and likewise to update an existing
419+document.
420+
421+.. note::
422+
423+ :meth:`Database.save()` is a better way to create and update documents as
424+ it will automatically update ``doc['_rev']`` in-place for you
425+
426+As setup for our examples, we'll do this:
427+
428+>>> couch = TempCouch()
429+>>> env = couch.bootstrap()
430+>>> server = Server(env)
431+>>> db = Database('mydb', env)
432+>>> db.put(None)
433+{'ok': True}
434+
435+For example, we can create "doc1" using our :class:`Server`:
436+
437+>>> doc1 = {'_id': 'doc1'}
438+>>> doc1['_rev'] = server.post(doc1, 'mydb')['rev']
439+>>> doc1['_rev']
440+'1-967a00dff5e02add41819138abb3284d'
441+
442+And we can create "doc2" using our :class:`Database`:
443+
444+>>> doc2 = {'_id': 'doc2'}
445+>>> doc2['_rev'] = db.post(doc2)['rev']
446+>>> doc2['_rev']
447+'1-967a00dff5e02add41819138abb3284d'
448+
449+When updated a document, ``doc['_rev']`` must be included, otherwise a
450+:exc:`Conflict` exception will be raised.
451+
452+Note that above we updated ``doc1`` and ``doc2`` in-place with the correct
453+revision. So now we can update "doc1" using our :class:`Server` like this:
454+
455+>>> server.post(doc1, 'mydb')['rev']
456+'2-7051cbe5c8faecd085a3fa619e6e6337'
457+
458+And update "doc2" using our :class:`Database` like this:
459+
460+>>> db.post(doc2)['rev']
461+'2-7051cbe5c8faecd085a3fa619e6e6337'
462+
463+A :exc:`Conflict` exception is raised if ``doc['_rev']`` doesn't match the
464+latest revision of the document in CouchDB (meaning the document has been
465+updated since you last retrieved it).
466+
467+Note that in the above updates, we did not update ``doc1`` and ``doc2`` with the
468+correct revision:
469+
470+>>> print(dumps(doc1))
471+{"_id":"doc1","_rev":"1-967a00dff5e02add41819138abb3284d"}
472+>>> print(dumps(doc2))
473+{"_id":"doc2","_rev":"1-967a00dff5e02add41819138abb3284d"}
474+
475+So when we try to update "doc1" this time using our :class:`Server`, a
476+:exc:`Conflict` will be raised:
477+
478+>>> server.post(doc1, 'mydb')
479+Traceback (most recent call last):
480+ ...
481+microfiber.Conflict: 409 Conflict: POST /mydb
482+
483+And likewise when we try to update "doc2" using our :class:`Database`:
484+
485+>>> db.post(doc2)
486+Traceback (most recent call last):
487+ ...
488+microfiber.Conflict: 409 Conflict: POST /
489+
490+
491+``GET /db/doc``
492+---------------
493+
494+This will retrieve a document from a database. As setup for our examples, we'll
495+do this:
496+
497+>>> couch = TempCouch()
498+>>> env = couch.bootstrap()
499+>>> server = Server(env)
500+>>> db = Database('mydb', env)
501+>>> db.put(None)
502+{'ok': True}
503+
504+A :exc:`NotFound` exception is raised if the document does not exist. For
505+example, using our :class:`Server`:
506+
507+>>> server.get('mydb', 'mydoc')
508+Traceback (most recent call last):
509+ ...
510+microfiber.NotFound: 404 Object Not Found: GET /mydb/mydoc
511+
512+Or using our :class:`Database`:
513+
514 >>> db.get('mydoc')
515-{'_rev': '2-7051cbe5c8faecd085a3fa619e6e6337', '_id': 'mydoc'}
516-
517-
518-Or using a :class:`Server`:
519-
520->>> s = Server()
521->>> s.get('mydb', 'mydoc')
522-{'_rev': '2-7051cbe5c8faecd085a3fa619e6e6337', '_id': 'mydoc'}
523-
524-
525-
526-Delete
527-------
528-
529-**DELETE /db/doc**
530-
531-This will delete a document. A :exc:`NotFound` exception is raised if the
532-document does not exist.
533-
534-A :exc:`Conflict` exception is raised if the *rev* keyword argument doesn't
535+Traceback (most recent call last):
536+ ...
537+microfiber.NotFound: 404 Object Not Found: GET /mydb/mydoc
538+
539+On the other hand, if we create "mydoc":
540+
541+>>> mydoc = {'_id': 'mydoc'}
542+>>> mydoc['_rev'] = db.post(mydoc)['rev']
543+>>> mydoc['_rev']
544+'1-967a00dff5e02add41819138abb3284d'
545+
546+We can retrieve it using our :class:`Server`:
547+
548+>>> doc = server.get('mydb', 'mydoc')
549+>>> print(dumps(doc, pretty=True))
550+{
551+ "_id": "mydoc",
552+ "_rev": "1-967a00dff5e02add41819138abb3284d"
553+}
554+
555+Or retrieve it using our :class:`Database`:
556+
557+>>> doc = db.get('mydoc')
558+>>> print(dumps(doc, pretty=True))
559+{
560+ "_id": "mydoc",
561+ "_rev": "1-967a00dff5e02add41819138abb3284d"
562+}
563+
564+If you supply the *rev* keyword argument, you can retrieve the contents of an
565+older revisions of a document (assuming the database hasn't yet been compacted).
566+
567+.. warning::
568+
569+ You should *never* assume that older document revisions will be available!
570+ When a database is compacted, only the latest revision of each document
571+ will be preserved!
572+
573+ The term "revision" is quite suggestive, but CouchDB is *not* a version
574+ control system. CouchDB uses "revisions" as a concurrency control
575+ mechanism, and nothing more.
576+
577+For example, let's create a new revision of "mydoc":
578+
579+>>> mydoc['message'] = 'hello, world'
580+>>> db.post(mydoc)['rev']
581+'2-91babf69deda1e2767615ba457c80807'
582+
583+We can explicitly retrieve ``'2-91babf69deda1e2767615ba457c80807'`` (which also
584+happens to be the latest revision):
585+
586+>>> doc = db.get('mydoc', rev='2-91babf69deda1e2767615ba457c80807')
587+>>> print(dumps(doc, pretty=True))
588+{
589+ "_id": "mydoc",
590+ "_rev": "2-91babf69deda1e2767615ba457c80807",
591+ "message": "hello, world"
592+}
593+
594+Or we can retrieve ``'1-967a00dff5e02add41819138abb3284d'``, the previous
595+revision:
596+
597+>>> doc = db.get('mydoc', rev='1-967a00dff5e02add41819138abb3284d')
598+>>> print(dumps(doc, pretty=True))
599+{
600+ "_id": "mydoc",
601+ "_rev": "1-967a00dff5e02add41819138abb3284d"
602+}
603+
604+
605+``DELETE /db/doc``
606+------------------
607+
608+This will delete a document from a database. Note that a small document
609+tombstone will still exist so that the deletion can be replicated between nodes.
610+
611+>>> couch = TempCouch()
612+>>> env = couch.bootstrap()
613+>>> server = Server(env)
614+>>> db = Database('mydb', env)
615+>>> db.put(None)
616+{'ok': True}
617+
618+For example, using a :class:`Server`:
619+
620+>>> server.post({'_id': 'doc1'}, 'mydb')['rev']
621+'1-967a00dff5e02add41819138abb3284d'
622+>>> server.delete('mydb', 'doc1', rev='1-967a00dff5e02add41819138abb3284d')['rev']
623+'2-eec205a9d413992850a6e32678485900'
624+
625+Or using a :class:`Database`:
626+
627+>>> db.post({'_id': 'doc2'})['rev']
628+'1-967a00dff5e02add41819138abb3284d'
629+>>> db.delete('doc2', rev='1-967a00dff5e02add41819138abb3284d')['rev']
630+'2-eec205a9d413992850a6e32678485900'
631+
632+A :exc:`NotFound` exception is raised if the document does not exist. For
633+example, using a :class:`Server`:
634+
635+>>> server.delete('mydb', 'mydoc')
636+Traceback (most recent call last):
637+ ...
638+microfiber.NotFound: 404 Object Not Found: DELETE /mydb/mydoc
639+
640+Or using a :class:`Database`:
641+
642+>>> db.delete('mydoc')
643+Traceback (most recent call last):
644+ ...
645+microfiber.NotFound: 404 Object Not Found: DELETE /mydb/mydoc
646+
647+When the document exists, a :exc:`Conflict` exception is raised if you don't
648+supply the *rev* keyword argument.
649+
650+For example, we'll create a document:
651+
652+>>> mydoc = {'_id': 'mydoc'}
653+>>> mydoc['_rev'] = db.post(mydoc)['rev']
654+>>> mydoc['_rev']
655+'1-967a00dff5e02add41819138abb3284d'
656+
657+And then try to delete it using a :class:`Server`:
658+
659+>>> server.delete('mydb', 'mydoc')
660+Traceback (most recent call last):
661+ ...
662+microfiber.Conflict: 409 Conflict: DELETE /mydb/mydoc
663+
664+Or try deleting it using a :class:`Database`:
665+
666+>>> db.delete('mydoc')
667+Traceback (most recent call last):
668+ ...
669+microfiber.Conflict: 409 Conflict: DELETE /mydb/mydoc
670+
671+Likewise, a :exc:`Conflict` exception is raised the *rev* you supply doesn't
672 match the latest revision of the document in CouchDB (meaning the document has
673 been updated since you last retrieved it).
674
675-Using a :class:`Database`:
676-
677->>> db = Database('mydb')
678->>> db.delete('mydoc', rev='2-7051cbe5c8faecd085a3fa619e6e6337')
679-{'rev': '3-7379b9e515b161226c6559d90c4dc49f', 'ok': True, 'id': 'mydoc'}
680-
681-
682-Or using a :class:`Server`:
683-
684->>> s = Server()
685->>> s.delete('mydb', 'mydoc', rev='2-7051cbe5c8faecd085a3fa619e6e6337')
686-{'rev': '3-7379b9e515b161226c6559d90c4dc49f', 'ok': True, 'id': 'mydoc'}
687+For example, we'll modify "mydoc":
688+
689+>>> mydoc['message'] = 'hello, world'
690+>>> db.post(mydoc)['rev']
691+'2-91babf69deda1e2767615ba457c80807'
692+
693+And then try to delete the document using the outdated revision
694+``'1-967a00dff5e02add41819138abb3284d'``, first using a :class:`Server`:
695+
696+>>> server.delete('mydb', 'mydoc', rev='1-967a00dff5e02add41819138abb3284d')
697+Traceback (most recent call last):
698+ ...
699+microfiber.Conflict: 409 Conflict: DELETE /mydb/mydoc?rev=1-967a00dff5e02add41819138abb3284d
700+
701+And second using a :class:`Database`:
702+
703+>>> db.delete('mydoc', rev='1-967a00dff5e02add41819138abb3284d')
704+Traceback (most recent call last):
705+ ...
706+microfiber.Conflict: 409 Conflict: DELETE /mydb/mydoc?rev=1-967a00dff5e02add41819138abb3284d
707+
708
709
710 Attachments
711@@ -232,95 +530,178 @@
712 instance, but you can do the same using a :class:`Server` instance.
713
714
715-Create
716-------
717-
718-**PUT /db/doc/attachment**
719+``PUT /db/doc/att``
720+-------------------
721
722 You create document attachments using the :meth:`CouchBase.put_att()` method.
723
724+For setup, we'll do this:
725+
726+>>> couch = TempCouch()
727+>>> env = couch.bootstrap()
728+>>> server = Server(env)
729+>>> db = Database('mydb', env)
730+>>> db.put(None)
731+{'ok': True}
732+
733 If you're creating an attachment for a document that does not yet exists, the
734 *rev* keyword argument isn't needed, and the document will be implicitly created
735-by CouchDB. If the document exists, you must include *rev*.
736-
737-A :exc:`Conflict` exception is raised if the *rev* keyword argument doesn't
738-match the latest revision of the document in CouchDB (meaning the document has
739-been updated since you last retrieved it).
740-
741-Using a :class:`Database` when the document does *not* exists:
742-
743->>> db = Database('mydb')
744->>> db.put_att('image/png', b'PNG Data', 'mydoc', 'myatt')
745-{'rev': '1-904eb7a25f6c4df64f49b0eeeb27dbbc', 'ok': True, 'id': 'mydoc'}
746-
747-Or using a :class:`Database` when the document does exists:
748-
749->>> db.put_att('image/png', b'PNG Data', 'mydoc', 'myatt2',
750-... rev='1-904eb7a25f6c4df64f49b0eeeb27dbbc'
751-... )
752-{'rev': '2-1e294b322cd16610bf3becb62167f7f2', 'ok': True, 'id': 'mydoc'}
753-
754-
755-Using a :class:`Server` when the document does *not* exists:
756-
757->>> s = Server()
758->>> s.put_att('image/png', b'PNG Data', 'mydb', 'mydoc', 'myatt')
759-{'rev': '1-904eb7a25f6c4df64f49b0eeeb27dbbc', 'ok': True, 'id': 'mydoc'}
760-
761-Or using a :class:`Server` when the document does exists:
762-
763->>> s.put_att('image/png', b'PNG Data', 'mydb', 'mydoc', 'myatt2',
764-... rev='1-904eb7a25f6c4df64f49b0eeeb27dbbc'
765-... )
766-{'rev': '2-1e294b322cd16610bf3becb62167f7f2', 'ok': True, 'id': 'mydoc'}
767-
768-
769-Retrieve
770---------
771-
772-**GET /db/doc/attachment**
773+by CouchDB. For example, using a :class:`Server`:
774+
775+>>> server.put_att('text/plain', b'Foo', 'mydb', 'doc1', 'foo')['rev']
776+'1-383671a0277edeb17918f714d1c5b63e'
777+
778+Or using using a :class:`Database`:
779+
780+>>> db.put_att('text/plain', b'Foo', 'mydb', 'doc2', 'foo')['rev']
781+'1-183074fa494ac6e04d360e6354057360'
782+
783+If the document exists, you must provide *rev* keyword argument. For example,
784+to add a 2nd attachment to "doc1" using a :class:`Server`:
785+
786+>>> server.put_att('text/plain', b'Bar', 'mydb', 'doc1', 'bar', rev='1-383671a0277edeb17918f714d1c5b63e')['rev']
787+'2-8654772d9053f1c949bffe3cf7ef4aa2'
788+
789+Or to add a 2nd attachment to "doc2" using using a :class:`Database`:
790+
791+>>> db.put_att('text/plain', b'Bar', 'mydb', 'doc2', 'bar', rev='1-183074fa494ac6e04d360e6354057360')['rev']
792+'2-d37c9c0cedace0a2a857deed922b330e'
793+
794+A :exc:`Conflict` exception is raised if you don't include the *rev* keyword
795+argument, of if the *rev* doesn't match the latest revision of the document in
796+CouchDB (meaning the document has been updated since you last retrieved it).
797+
798+For example, if trying to add a 3rd attachment to "doc1" using a
799+:class:`Server` and the outdated revision
800+``'1-383671a0277edeb17918f714d1c5b63e'``:
801+
802+>>> server.put_att('text/plain', b'Baz', 'mydb', 'doc1', 'baz', rev='1-383671a0277edeb17918f714d1c5b63e')['rev']
803+Traceback (most recent call last):
804+ ...
805+microfiber.Conflict: 409 Conflict: PUT /mydb/doc1/baz?rev=1-383671a0277edeb17918f714d1c5b63e
806+
807+Or if trying to add a 3rd attachment to "doc2" using a
808+:class:`Database` and the outdated revision
809+``'1-183074fa494ac6e04d360e6354057360'``:
810+
811+>>> db.put_att('text/plain', b'Baz', 'mydb', 'doc2', 'baz', rev='1-183074fa494ac6e04d360e6354057360')['rev']
812+Traceback (most recent call last):
813+ ...
814+microfiber.Conflict: 409 Conflict: PUT /mydb/doc2/baz?rev=1-183074fa494ac6e04d360e6354057360
815+
816+
817+``GET /db/doc/att``
818+-------------------
819
820 You retrieve document attachments using the :meth:`CouchBase.get_att()` method.
821
822-A :exc:`NotFound` exception is raised if the attachment does not exist.
823-
824-Using a :class:`Database`:
825-
826->>> db = Database('mydb')
827+For setup, we'll do this:
828+
829+>>> couch = TempCouch()
830+>>> env = couch.bootstrap()
831+>>> server = Server(env)
832+>>> db = Database('mydb', env)
833+>>> db.put(None)
834+{'ok': True}
835+>>> db.post({'_id': 'mydoc'})['rev']
836+'1-967a00dff5e02add41819138abb3284d'
837+
838+A :exc:`NotFound` exception is raised if the attachment does not exist. For
839+example, using a :class:`Server`:
840+
841+>>> server.get('mydb', 'mydoc', 'myatt')
842+Traceback (most recent call last):
843+ ...
844+microfiber.NotFound: 404 Object Not Found: GET /mydb/mydoc/myatt
845+
846+Or using a :class:`Database`:
847+
848+>>> db.get('mydoc', 'myatt')
849+Traceback (most recent call last):
850+ ...
851+microfiber.NotFound: 404 Object Not Found: GET /mydb/mydoc/myatt
852+
853+Finally, we'll create an attachment with this setup:
854+
855+>>> db.put_att('text/plain', b'hello, world', 'mydoc', 'myatt', rev='1-967a00dff5e02add41819138abb3284d')['rev']
856+'2-d403ee4d0528a7be93cffb89c4beb3e4'
857+
858+For example, we'll retrieve this attachment using a :class:`Server`:
859+
860+>>> server.get_att('mydb', 'mydoc', 'myatt')
861+Attachment(content_type='text/plain', data=b'hello, world')
862+
863+Or using :class:`Database`:
864+
865 >>> db.get_att('mydoc', 'myatt')
866-('image/png', b'PNG Data')
867-
868-
869-Or using a :class:`Server`:
870-
871->>> s = Server()
872->>> s.get_att('mydb', 'mydoc', 'myatt')
873-('image/png', b'PNG Data')
874-
875-
876-Delete
877-------
878-
879-**DELETE /db/doc/attachment**
880-
881-A :exc:`NotFound` exception is raised if the attachment does not exist.
882-
883-A :exc:`Conflict` exception is raised if the *rev* keyword argument doesn't
884-match the latest revision of the document in CouchDB (meaning the document has
885-been updated since you last retrieved it).
886-
887-Using a :class:`Database`:
888-
889->>> db = Database('mydb')
890->>> db.delete('mydoc', 'myatt', rev='2-1e294b322cd16610bf3becb62167f7f2')
891-{'rev': '3-6de76b26d1b5a1ca533d94039132e594', 'ok': True, 'id': 'mydoc'}
892-
893-
894-Or using a :class:`Server`:
895-
896->>> s = Server()
897->>> s.delete('mydb', 'mydoc', 'myatt', rev='2-1e294b322cd16610bf3becb62167f7f2')
898-{'rev': '3-6de76b26d1b5a1ca533d94039132e594', 'ok': True, 'id': 'mydoc'}
899+Attachment(content_type='text/plain', data=b'hello, world')
900+
901+
902+``DELETE /db/doc/att``
903+----------------------
904+
905+For setup, we'll do this:
906+
907+>>> couch = TempCouch()
908+>>> env = couch.bootstrap()
909+>>> server = Server(env)
910+>>> db = Database('mydb', env)
911+>>> db.put(None)
912+{'ok': True}
913+>>> db.post({'_id': 'mydoc'})['rev']
914+'1-967a00dff5e02add41819138abb3284d'
915+
916+And then add an attachment using a :class:`Server`:
917+
918+>>> server.put_att('text/plain', b'hello, world', 'mydb', 'mydoc', 'att1', rev='1-967a00dff5e02add41819138abb3284d')['rev']
919+'2-f2d88125f27039a1af069b76c398d21e'
920+
921+And then add an attachment using a :class:`Database`:
922+
923+>>> db.put_att('text/plain', b'hello, naughty nurse', 'mydoc', 'att2', rev='2-f2d88125f27039a1af069b76c398d21e')['rev']
924+'3-00d0d01ee1cc715522f060ea49e4df22'
925+
926+A :exc:`Conflict` exception is raised if the *rev* keyword argument isn't
927+provided, for example:
928+
929+>>> server.delete('mydb', 'mydoc', 'att1')
930+Traceback (most recent call last):
931+ ...
932+microfiber.Conflict: 409 Conflict: DELETE /mydb/mydoc/att1
933+
934+Or using :class:`Database`:
935+
936+>>> db.delete('mydoc', 'att1')
937+Traceback (most recent call last):
938+ ...
939+microfiber.Conflict: 409 Conflict: DELETE /mydb/mydoc/att1
940+
941+Likewise, a :exc:`Conflict` exception is raised if the *rev* keyword argument
942+doesn't match the latest revision of the document in CouchDB (meaning the
943+document has been updated since you last retrieved it):
944+
945+>>> server.delete('mydb', 'mydoc', 'att1', rev='2-f2d88125f27039a1af069b76c398d21e')
946+Traceback (most recent call last):
947+ ...
948+microfiber.Conflict: 409 Conflict: DELETE /mydb/mydoc/att1
949+
950+Or using :class:`Database`:
951+
952+>>> db.delete('mydoc', 'att1', rev='2-f2d88125f27039a1af069b76c398d21e')
953+Traceback (most recent call last):
954+ ...
955+microfiber.Conflict: 409 Conflict: DELETE /mydb/mydoc/att1
956+
957+Finally, two examples in which the attachment is deleted:
958+
959+>>> server.delete('mydb', 'mydoc', 'att1', rev='3-00d0d01ee1cc715522f060ea49e4df22')['rev']
960+'4-b3726f26cdcf3c7101e14ca0caf701f0'
961+
962+Or using :class:`Database`:
963+
964+>>> db.delete('mydoc', 'att2', rev='4-b3726f26cdcf3c7101e14ca0caf701f0')['rev']
965+'5-aca674de3a1607e3003e5d4e7c0337d6'
966+
967
968
969 Server
970@@ -328,33 +709,44 @@
971
972 To perform server-level actions, you must use a :class:`Server` instance.
973
974-
975-Welcome
976--------
977-
978-**GET /**
979-
980-This will retrieve the CouchDB welcome response, which includes the CouchDB
981-version.
982-
983->>> s = Server()
984->>> s.get()
985-{'couchdb': 'Welcome', 'version': '1.1.0'}
986-
987-
988-Databases
989+Setup for the examples:
990+
991+>>> couch = TempCouch()
992+>>> env = couch.bootstrap()
993+>>> s = Server(env)
994+
995+
996+``GET /``
997 ---------
998
999-**GET /_all_dbs**
1000-
1001-This will retrieve the list of databases in this CouchDB instance.
1002-
1003->>> s.get('_all_dbs')
1004-['_replicator', '_users', 'dmedia', 'mydb']
1005-
1006-
1007-
1008-
1009-
1010-
1011+This will retrieve a ``dict`` containing the CouchDB welcome response, which
1012+will include the CouchDB version and other useful info.
1013+
1014+>>> sorted(s.get())
1015+['couchdb', 'uuid', 'vendor', 'version']
1016+
1017+
1018+``GET /_all_dbs``
1019+-----------------
1020+
1021+This will retrieve the list of databases in this CouchDB instance. For example,
1022+when no user-created databases exists:
1023+
1024+>>> s.get('_all_dbs')
1025+['_replicator', '_users']
1026+
1027+And now if we create a database:
1028+
1029+>>> s.put(None, 'foo')
1030+{'ok': True}
1031+>>> s.get('_all_dbs')
1032+['_replicator', '_users', 'foo']
1033+
1034+And finally if we create another database (note the database names are returned
1035+in sorted order):
1036+
1037+>>> s.put(None, 'bar')
1038+{'ok': True}
1039+>>> s.get('_all_dbs')
1040+['_replicator', '_users', 'bar', 'foo']
1041
1042
1043=== modified file 'doc/microfiber.rst'
1044--- doc/microfiber.rst 2012-09-22 12:46:46 +0000
1045+++ doc/microfiber.rst 2014-02-23 01:42:08 +0000
1046@@ -5,21 +5,47 @@
1047 .. py:module:: microfiber
1048 :synopsis: fabric for a lightweight couch
1049
1050-In a nutshell, the Microfiber API is the CouchDB API, nothing more. For
1051-example:
1052+In a nutshell, Microfiber is generic REST adapter that allows you to make
1053+requests to an arbitrary JSON-centric REST API like CouchDB. This means that by
1054+and large the Microfiber API *is* `CouchDB REST API`_, as expressed through the
1055+Microfiber REST adapter.
1056+
1057+In all our examples, we'll create throw-away CouchDB instances using
1058+`UserCouch`_.
1059+
1060+First, well create a ``TempCouch``, and bootstrap it get the *env* we'll pass to
1061+a :class:`Server` or :class:`Database`.
1062+
1063+>>> from usercouch.misc import TempCouch
1064+>>> couch = TempCouch()
1065+>>> env = couch.bootstrap()
1066+
1067+A :class:`Server` expose the REST API relative to the root URL. For example:
1068+
1069+>>> from microfiber import Server
1070+>>> server = Server(env)
1071+>>> server.get()['couchdb'] # GET /
1072+'Welcome'
1073+>>> server.put(None, 'mydb') # PUT /mydb
1074+{'ok': True}
1075+>>> doc = {'_id': 'mydoc'}
1076+>>> doc['_rev'] = server.post(doc, 'mydb')['rev'] # POST /mydb
1077+>>> server.get('mydb', 'mydoc') == doc # GET /mydb/mydoc
1078+True
1079+
1080+A :class:`Database` expose the exact same REST API relative to the root URL.
1081+For example:
1082
1083 >>> from microfiber import Database
1084->>> db = Database('foo')
1085->>> db.put(None) # PUT /foo
1086-{'ok': True}
1087->>> db.put({}, 'bar') # PUT /foo/bar
1088-{'rev': '1-967a00dff5e02add41819138abb3284d', 'ok': True, 'id': 'bar'}
1089->>> db.get('bar') # GET /foo/bar
1090-{'_rev': '1-967a00dff5e02add41819138abb3284d', '_id': 'bar'}
1091->>> db.delete('bar', rev='1-967a00dff5e02add41819138abb3284d') # DELETE /foo/bar
1092-{'rev': '2-eec205a9d413992850a6e32678485900', 'ok': True, 'id': 'bar'}
1093->>> db.delete() # DELETE /foo
1094-{'ok': True}
1095+>>> db = Database('mydb', env)
1096+>>> db.get()['db_name'] # GET /mydb
1097+'mydb'
1098+>>> db.put(None) # PUT /mydb ('mydb' already exists!)
1099+Traceback (most recent call last):
1100+ ...
1101+microfiber.PreconditionFailed: 412 Precondition Failed: PUT /mydb/
1102+>>> db.get('mydoc') == doc # GET /mydb/mydoc
1103+True
1104
1105 Chances are you'll use the :class:`Database` class most of all.
1106
1107@@ -131,7 +157,7 @@
1108 ... 'url': 'https://example.com/',
1109 ... 'ssl': {
1110 ... 'ca_file': '/trusted/server.ca',
1111-... 'ca_path': 'trusted/ca.directory/',
1112+... 'ca_path': '/trusted/ca.directory/',
1113 ... 'check_hostname': False,
1114 ... 'cert_file': '/my/client.cert',
1115 ... 'key_file': '/my/client.key',
1116@@ -181,10 +207,18 @@
1117 * :meth:`CouchBase.delete()`
1118 * :meth:`CouchBase.put_att()`
1119 * :meth:`CouchBase.get_att()`
1120-
1121+
1122 All these methods are inherited unchanged by the :class:`Server` and
1123 :class:`Database` classes.
1124
1125+All the method examples below assume this setup:
1126+
1127+>>> from usercouch.misc import TempCouch
1128+>>> from microfiber import CouchBase, dumps
1129+>>> couch = TempCouch()
1130+>>> env = couch.bootstrap()
1131+
1132+
1133 .. class:: CouchBase(env='http://localhost:5984/')
1134
1135
1136@@ -192,50 +226,59 @@
1137
1138 PUT *obj*.
1139
1140- For example, to create the database "foo":
1141+ For example, to create the database "db1":
1142
1143- >>> cb = CouchBase()
1144- >>> cb.put(None, 'foo') #doctest: +SKIP
1145+ >>> cb = CouchBase(env)
1146+ >>> cb.put(None, 'db1')
1147 {'ok': True}
1148
1149- Or to create the doc "baz" in the database "foo":
1150+ Or to create the doc "doc1" in the database "db1":
1151
1152- >>> cb.put({'micro': 'fiber'}, 'foo', 'baz') #doctest: +SKIP
1153- {'rev': '1-fae0708c46b4a6c9c497c3a687170ad6', 'ok': True, 'id': 'bar'}
1154+ >>> cb.put({'micro': 'fiber'}, 'db1', 'doc1')['rev']
1155+ '1-fae0708c46b4a6c9c497c3a687170ad6'
1156
1157
1158 .. method:: post(obj, *parts, **options)
1159-
1160+
1161 POST *obj*.
1162
1163- For example, to create the doc "bar" in the database "foo":
1164-
1165- >>> cb = CouchBase()
1166- >>> cb.post({'_id': 'bar'}, 'foo') #doctest: +SKIP
1167- {'rev': '1-967a00dff5e02add41819138abb3284d', 'ok': True, 'id': 'bar'}
1168-
1169- Or to compact the database "foo":
1170-
1171- >>> cb.post(None, 'foo', '_compact') #doctest: +SKIP
1172- {'ok': True}
1173-
1174-
1175+ For example, to create the doc "doc2" in the database "db2", we'll first
1176+ create "db2":
1177+
1178+ >>> cb = CouchBase(env)
1179+ >>> cb.put(None, "db2")
1180+ {'ok': True}
1181+
1182+ And now we'll save the "doc2" document:
1183+
1184+ >>> cb.post({'_id': 'doc2'}, 'db2')['rev']
1185+ '1-967a00dff5e02add41819138abb3284d'
1186+
1187+ Or to compact the database "db2":
1188+
1189+ >>> cb.post(None, 'db2', '_compact')
1190+ {'ok': True}
1191+
1192+
1193 .. method:: get(*parts, **options)
1194
1195 Make a GET request.
1196
1197 For example, to get the welcome info from CouchDB:
1198
1199- >>> cb = CouchBase()
1200- >>> cb.get() #doctest: +SKIP
1201- {'couchdb': 'Welcome', 'version': '1.1.0'}
1202-
1203- Or to request the doc "bar" from the database "foo", including any
1204- attachments:
1205-
1206- >>> cb.get('foo', 'bar', attachments=True) #doctest: +SKIP
1207- {'_rev': '1-967a00dff5e02add41819138abb3284d', '_id': 'bar'}
1208-
1209+ >>> cb = CouchBase(env)
1210+ >>> cb.get()['couchdb']
1211+ 'Welcome'
1212+
1213+ Or to request the doc "db1" from the database "doc1":
1214+
1215+ >>> doc = cb.get('db1', 'doc1')
1216+ >>> print(dumps(doc, pretty=True))
1217+ {
1218+ "_id": "doc1",
1219+ "_rev": "1-fae0708c46b4a6c9c497c3a687170ad6",
1220+ "micro": "fiber"
1221+ }
1222
1223 .. method:: head(*parts, **options)
1224
1225@@ -243,28 +286,28 @@
1226
1227 Returns a ``dict`` containing the response headers from the HEAD
1228 request.
1229-
1230- For example, to make a HEAD request on the doc "bar" in the database
1231- "foo":
1232-
1233- >>> cb = CouchBase()
1234- >>> cb.head('foo', 'baz')['Etag'] #doctest: +SKIP
1235- '"1-967a00dff5e02add41819138abb3284d"'
1236+
1237+ For example, to make a HEAD request on the doc "doc1" in the database
1238+ "db1":
1239+
1240+ >>> cb = CouchBase(env)
1241+ >>> cb.head('db1', 'doc1')['etag']
1242+ '"1-fae0708c46b4a6c9c497c3a687170ad6"'
1243
1244
1245 .. method:: delete(*parts, **options)
1246
1247 Make a DELETE request.
1248
1249- For example, to delete the doc "bar" in the database "foo":
1250-
1251- >>> cb = CouchBase()
1252- >>> cb.delete('foo', 'bar', rev='1-967a00dff5e02add41819138abb3284d') #doctest: +SKIP
1253- {'rev': '1-967a00dff5e02add41819138abb3284d', 'ok': True, 'id': 'bar'}
1254-
1255- Or to delete the database "foo":
1256-
1257- >>> cb.delete('foo') #doctest: +SKIP
1258+ For example, to delete the doc "doc2" in the database "db2":
1259+
1260+ >>> cb = CouchBase(env)
1261+ >>> cb.delete('db2', 'doc2', rev='1-967a00dff5e02add41819138abb3284d')['rev']
1262+ '2-eec205a9d413992850a6e32678485900'
1263+
1264+ Or two delete the "db2" database:
1265+
1266+ >>> cb.delete('db2')
1267 {'ok': True}
1268
1269
1270@@ -272,30 +315,40 @@
1271
1272 PUT an attachment.
1273
1274- For example, to upload the attachment "baz" for the doc "bar" in the
1275- database "foo":
1276-
1277- >>> cb = CouchBase()
1278- >>> cb.put_att('image/png', b'da pic', 'foo', 'bar', 'baz') #doctest: +SKIP
1279- {'rev': '1-d536771b631a30c2ab4c0340adc72570', 'ok': True, 'id': 'bar'}
1280+ If uploading an attachment for a document that already exist, you don't
1281+ need to specify the *rev*. For example, to upload the attachment "att1"
1282+ for the doc "doc1" in the database "db1".
1283+
1284+ >>> cb = CouchBase(env)
1285+ >>> cb.put_att('text/plain', b'hello, world', 'db1', 'doc1', 'att1',
1286+ ... rev='1-fae0708c46b4a6c9c497c3a687170ad6',
1287+ ... )['rev']
1288+ '2-bd4ac0c5ca963e5b4f0f3b09ea540de2'
1289+
1290+ On the other hand, if uploading an attachment for a doc that doesn't
1291+ exist yet, you don't need to specify the *rev*. For example, to upload
1292+ the attachment "newatt" for the doc "newdoc" in "db":
1293+
1294+ >>> cb.put_att('text/plain', b'New', 'db1', 'newdoc', 'newatt')['rev']
1295+ '1-b2c33fbf19cadc92ab7b9860e116bb25'
1296
1297 Note that you don't need any attachment-specific method for DELETE.
1298 Just use :meth:`CouchBase.delete()`, like this:
1299-
1300- >>> cb.delete('foo', 'bar', 'baz', rev='1-d536771b631a30c2ab4c0340adc72570') #doctest: +SKIP
1301- {'rev': '2-082e66867f6d4d1753d7d0bf08122425', 'ok': True, 'id': 'bar'}
1302-
1303-
1304+
1305+ >>> cb.delete('db1', 'newdoc', 'newatt', rev='1-b2c33fbf19cadc92ab7b9860e116bb25')['rev']
1306+ '2-5a5ecda09b7010bc3f190d8766398cff'
1307+
1308+
1309 .. method:: get_att(*parts, **options)
1310
1311 GET an attachment.
1312
1313 Returns a ``(content_type, data)`` tuple. For example, to download the
1314- attachment "baz" for the doc "bar" in the database "foo":
1315+ attachment "att1" for the doc "doc1" in the database "db1":
1316
1317- >>> cb = CouchBase()
1318- >>> cb.get_att('foo', 'bar', 'baz') #doctest: +SKIP
1319- ('image/png', b'da pic')
1320+ >>> cb = CouchBase(env)
1321+ >>> cb.get_att('db1', 'doc1', 'att1')
1322+ Attachment(content_type='text/plain', data=b'hello, world')
1323
1324
1325
1326@@ -540,6 +593,7 @@
1327
1328 For example:
1329
1330+ >>> from microfiber import dumps
1331 >>> doc = {
1332 ... 'hello': 'мир',
1333 ... 'welcome': 'все',
1334@@ -549,8 +603,9 @@
1335
1336 Whereas if you directly call ``json.dumps()`` without *ensure_ascii=False*:
1337
1338+ >>> import json
1339 >>> json.dumps(doc, sort_keys=True)
1340- '{"hello": "\\\\u043c\\\\u0438\\\\u0440", "welcome": "\\\\u0432\\\\u0441\\\\u0435"}'
1341+ '{"hello": "\\u043c\\u0438\\u0440", "welcome": "\\u0432\\u0441\\u0435"}'
1342
1343 By default compact encoding is used, but if you supply *pretty=True*,
1344 4-space indentation will be used:
1345@@ -562,18 +617,6 @@
1346 }
1347
1348
1349-.. function:: dc3_env()
1350-
1351- Return the dc3 environment information.
1352-
1353- For example, to create a :class:`Database` with the correct per-user `dc3`_
1354- environment:
1355-
1356- >>> from microfiber import dc3_env, Database
1357- >>> db = Database('dmedia-0', dc3_env())
1358- >>> db.url
1359- 'http://localhost:41289/'
1360-
1361
1362 .. function:: dmedia_env()
1363
1364@@ -583,9 +626,7 @@
1365 `Dmedia`_ environment:
1366
1367 >>> from microfiber import dmedia_env, Database
1368- >>> db = Database('dmedia-0', dmedia_env())
1369- >>> db.url
1370- 'http://localhost:41289/'
1371+ >>> db = Database('dmedia-1', dmedia_env()) #doctest: +SKIP
1372
1373 If you're using Microfiber to work with `Dmedia`_ or `Novacut`_, please use
1374 this function instead of :func:`dc3_env()` as starting with the Dmedia 12.01
1375@@ -697,9 +738,9 @@
1376 The exact return value from the CouchDB request.
1377
1378
1379-
1380 .. _`Novacut`: https://wiki.ubuntu.com/Novacut
1381 .. _`UserCouch`: https://launchpad.net/usercouch
1382+.. _`CouchDB REST API`: http://couchdb.readthedocs.org/en/latest/api/index.html
1383 .. _`dc3`: https://launchpad.net/dc3
1384 .. _`Dmedia`: https://launchpad.net/dmedia
1385 .. _`python-couchdb`: http://packages.python.org/CouchDB/client.html#database
1386
1387=== modified file 'setup.py'
1388--- setup.py 2013-02-20 23:00:54 +0000
1389+++ setup.py 2014-02-23 01:42:08 +0000
1390@@ -36,11 +36,25 @@
1391 from distutils.core import setup
1392 from distutils.cmd import Command
1393 import os
1394+from os import path
1395+import subprocess
1396
1397 import microfiber
1398 from microfiber.tests.run import run_tests
1399
1400
1401+def run_sphinx_doctest():
1402+ sphinx_build = '/usr/share/sphinx/scripts/python3/sphinx-build'
1403+ if not os.access(sphinx_build, os.R_OK | os.X_OK):
1404+ print('warning, cannot read and execute: {!r}'.format(sphinx_build))
1405+ return
1406+ tree = path.dirname(path.abspath(__file__))
1407+ doc = path.join(tree, 'doc')
1408+ doctest = path.join(tree, 'doc', '_build', 'doctest')
1409+ cmd = [sys.executable, sphinx_build, '-EW', '-b', 'doctest', doc, doctest]
1410+ subprocess.check_call(cmd)
1411+
1412+
1413 class Test(Command):
1414 description = 'run unit tests and doc tests'
1415
1416@@ -63,6 +77,7 @@
1417 os.environ['MICROFIBER_TEST_SKIP_SLOW'] = 'true'
1418 if not run_tests():
1419 raise SystemExit('2')
1420+ run_sphinx_doctest()
1421
1422
1423 setup(

Subscribers

People subscribed via source and target branches