Merge lp:~jderose/microfiber/doctest into lp:microfiber
- doctest
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
David Jordan | Approve | ||
Review via email:
|
Commit message
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.
>>> 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://
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/
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/
3) I couldn't resist doing at least some stylistic improvement in doc/couchdb_
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Zygmunt Krynicki (zyga) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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!
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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
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( |
1401 +def run_sphinx_ doctest( ): sphinx/ scripts/ python3/ sphinx- build'
1402 + sphinx_build = '/usr/share/
How about executing setup.py with build_shpinx -b doctest?