~ubuntu-branches/ubuntu/lucid/lazr.restfulclient/lucid-updates

« back to all changes in this revision

Viewing changes to .pc/uppercase-method.patch/src/lazr/restfulclient/docs/entries.txt

  • Committer: Package Import Robot
  • Author(s): Colin Watson
  • Date: 2014-12-11 16:30:02 UTC
  • mfrom: (10.2.4 lucid-proposed)
  • Revision ID: package-import@ubuntu.com-20141211163002-7bumdmxfi0eqy23r
Tags: 0.9.11-1ubuntu1.4
Always uppercase HTTP methods to match httplib2 expectations
(LP: #1401544).

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
******************
 
2
Entry manipulation
 
3
******************
 
4
 
 
5
Objects available through the web interface, such as cookbooks, have a
 
6
readable interface which is available through direct attribute access.
 
7
 
 
8
    >>> from lazr.restfulclient.tests.example import CookbookWebServiceClient
 
9
    >>> service = CookbookWebServiceClient()
 
10
 
 
11
    >>> recipe = service.recipes[1]
 
12
    >>> print recipe.instructions
 
13
    You can always judge...
 
14
 
 
15
These objects may have a number of attributes, as well as associated
 
16
entries and collections.
 
17
 
 
18
    >>> cookbook = recipe.cookbook
 
19
    >>> print cookbook.name
 
20
    Mastering the Art of French Cooking
 
21
 
 
22
    >>> len(cookbook.recipes)
 
23
    2
 
24
 
 
25
The lp_* introspection methods let you know what you can do with an
 
26
object. You can also use dir(), but it'll be cluttered with all sorts
 
27
of other stuff.
 
28
 
 
29
    >>> sorted(dir(cookbook))
 
30
    [..., 'confirmed', 'copyright_date', 'cover', ... 'find_recipes',
 
31
     ..., 'recipes', ...]
 
32
    >>> sorted(cookbook.lp_attributes)
 
33
    ['confirmed', 'copyright_date', ..., 'self_link']
 
34
 
 
35
    >>> sorted(cookbook.lp_entries)
 
36
    ['cover']
 
37
    >>> sorted(cookbook.lp_collections)
 
38
    ['recipes']
 
39
    >>> sorted(cookbook.lp_operations)
 
40
    ['find_recipe_for', 'find_recipes', 'make_more_interesting',
 
41
     'replace_cover']
 
42
 
 
43
Some attributes can only take on certain values. The lp_values_for
 
44
method will show you these values.
 
45
 
 
46
    >>> sorted(cookbook.lp_values_for('cuisine'))
 
47
    ['American', 'Dessert', u'Fran\xe7aise', 'General', 'Vegetarian']
 
48
 
 
49
Some attributes don't have a predefined list of acceptable values. For
 
50
them, lp_values_for() returns None.
 
51
 
 
52
    >>> print cookbook.lp_values_for('copyright_date')
 
53
    None
 
54
 
 
55
Some of these attributes can be changed.  For example, a client can
 
56
change a recipe's preparation instructions. When changing attribute values
 
57
though, the changes are not pushed to the web service until the entry
 
58
is explicitly saved.  This allows the client to batch the changes over
 
59
the wire for efficiency.
 
60
 
 
61
    >>> recipe.instructions = 'Modified instructions'
 
62
    >>> print service.recipes[1].instructions
 
63
    You can always judge...
 
64
 
 
65
Once the changes are saved though, they are propagated to the web
 
66
service.
 
67
 
 
68
    >>> recipe.lp_save()
 
69
    >>> print service.recipes[1].instructions
 
70
    Modified instructions
 
71
 
 
72
An entry object is a normal Python object like any other. Attributes
 
73
of an entry, like 'cuisine' or 'cookbook', are available as attributes
 
74
on the resource, and may be set. Random strings that are not
 
75
attributes of the entry cannot be set or read as Python attributes.
 
76
 
 
77
    >>> recipe.instructions = 'Different instructions'
 
78
    >>> recipe.is_great = True
 
79
    Traceback (most recent call last):
 
80
    ...
 
81
    AttributeError: 'Entry' object has no attribute 'is_great'
 
82
 
 
83
    >>> recipe.is_great
 
84
    Traceback (most recent call last):
 
85
    ...
 
86
    AttributeError: 'Entry' object has no attribute 'is_great'
 
87
 
 
88
The client can set more than one attribute on an entry at a time:
 
89
they'll all be changed when the entry is saved.
 
90
 
 
91
    >>> cookbook.cuisine
 
92
    u'Fran\xe7aise'
 
93
    >>> cookbook.description
 
94
    u''
 
95
 
 
96
    >>> cookbook.cuisine = 'Dessert'
 
97
    >>> cookbook.description = "A new description"
 
98
    >>> cookbook.lp_save()
 
99
 
 
100
    >>> cookbook = service.recipes[1].cookbook
 
101
 
 
102
    >>> print cookbook.cuisine
 
103
    Dessert
 
104
    >>> print cookbook.description
 
105
    A new description
 
106
 
 
107
Some of an entry's attributes may take other resources as values.
 
108
 
 
109
    >>> old_cookbook = recipe.cookbook
 
110
    >>> other_cookbook = service.cookbooks['Everyday Greens']
 
111
    >>> print other_cookbook.name
 
112
    Everyday Greens
 
113
    >>> recipe.cookbook = other_cookbook
 
114
    >>> recipe.lp_save()
 
115
    >>> print recipe.cookbook.name
 
116
    Everyday Greens
 
117
 
 
118
    >>> recipe.cookbook = old_cookbook
 
119
    >>> recipe.lp_save()
 
120
 
 
121
 
 
122
Refreshing data
 
123
---------------
 
124
 
 
125
Here are two objects representing recipe #1. A representation of an
 
126
entry object is not fetched until the data is needed. We'll fetch a
 
127
representation for the first object right away...
 
128
 
 
129
    >>> recipe_copy = service.recipes[1]
 
130
    >>> print recipe_copy.instructions
 
131
    Different instructions
 
132
 
 
133
...but leave the second object alone.
 
134
 
 
135
    >>> recipe_copy_2 = service.recipes[1]
 
136
 
 
137
An entry is automatically refreshed after saving.
 
138
 
 
139
    >>> recipe.instructions = 'Even newer instructions'
 
140
    >>> recipe.lp_save()
 
141
    >>> print recipe.instructions
 
142
    Even newer instructions
 
143
 
 
144
If an old object representing that entry already has a representation,
 
145
it will still show the old data.
 
146
 
 
147
    >>> print recipe_copy.instructions
 
148
    Different instructions
 
149
 
 
150
If an old object representing that entry doesn't have a representation
 
151
yet, it will show the new data.
 
152
 
 
153
    >>> print recipe_copy_2.instructions
 
154
    Even newer instructions
 
155
 
 
156
You can also refresh a resource object manually.
 
157
 
 
158
    >>> recipe_copy.lp_refresh()
 
159
    >>> print recipe_copy.instructions
 
160
    Even newer instructions
 
161
 
 
162
Bookmarking an entry
 
163
--------------------
 
164
 
 
165
You can get an entry's URL from the 'self_link' attribute, save the
 
166
URL for a while, and retrieve the entry later using the load()
 
167
function.
 
168
 
 
169
    >>> bookmark = recipe.self_link
 
170
    >>> new_recipe = service.load(bookmark)
 
171
    >>> print new_recipe.dish.name
 
172
    Roast chicken
 
173
 
 
174
You can bookmark a URI relative to the version of the web service
 
175
currently in use.
 
176
 
 
177
    >>> cookbooks = service.load("cookbooks")
 
178
    >>> print cookbooks['The Joy of Cooking'].self_link
 
179
    http://cookbooks.dev/1.0/cookbooks/The%20Joy%20of%20Cooking
 
180
 
 
181
    >>> cookbook = service.load("/cookbooks/The%20Joy%20of%20Cooking")
 
182
    >>> print cookbook.self_link
 
183
    http://cookbooks.dev/1.0/cookbooks/The%20Joy%20of%20Cooking
 
184
 
 
185
    >>> service_root = service.load("")
 
186
    >>> print service_root.cookbooks['The Joy of Cooking'].name
 
187
    The Joy of Cooking
 
188
 
 
189
But you can't provide the web service version and bookmark a URI
 
190
relative to the service root.
 
191
 
 
192
    >>> cookbooks = service.load("/1.0/cookbooks")
 
193
    Traceback (most recent call last):
 
194
    ...
 
195
    HTTPError: HTTP Error 404: Not Found
 
196
    ...
 
197
 
 
198
(That code attempts to load http://cookbooks.dev/1.0/1.0/cookbooks,
 
199
which doesn't exist.)
 
200
 
 
201
You can't bookmark an absolute or relative URI that has nothing to do
 
202
with the web service.
 
203
 
 
204
    >>> bookmark = 'http://cookbooks.dev/'
 
205
    >>> service.load(bookmark)
 
206
    Traceback (most recent call last):
 
207
    ...
 
208
    HTTPError: HTTP Error 404: Not Found
 
209
    ...
 
210
 
 
211
    >>> service.load("/no-such-url")
 
212
    Traceback (most recent call last):
 
213
    ...
 
214
    HTTPError: HTTP Error 404: Not Found
 
215
    ...
 
216
 
 
217
You can't bookmark the return value of a named operation. This is not
 
218
really desirable, but that's how things work right now.
 
219
 
 
220
    >>> url_without_type = ('http://cookbooks.dev/1.0/cookbooks' +
 
221
    ...                     '?ws.op=find_recipes&search=a')
 
222
    >>> service.load(url_without_type)
 
223
    Traceback (most recent call last):
 
224
    ...
 
225
    ValueError: Couldn't determine the resource type of...
 
226
 
 
227
Moving an entry
 
228
---------------
 
229
 
 
230
Some entries will move to different URLs when a client changes their
 
231
data attributes. For instance, a cookbook's URL is determined by its
 
232
name.
 
233
 
 
234
    >>> cookbook = service.cookbooks['The Joy of Cooking']
 
235
    >>> print cookbook.name
 
236
    The Joy of Cooking
 
237
    >>> old_link = cookbook.self_link
 
238
    >>> print old_link
 
239
    http://cookbooks.dev/1.0/cookbooks/The%20Joy%20of%20Cooking
 
240
    >>> cookbook.name = "Another Name"
 
241
    >>> cookbook.lp_save()
 
242
 
 
243
Change the name, and you change the URL.
 
244
 
 
245
    >>> new_link = cookbook.self_link
 
246
    >>> print new_link
 
247
    http://cookbooks.dev/1.0/cookbooks/Another%20Name
 
248
 
 
249
Old bookmarks won't work anymore.
 
250
 
 
251
    >>> print service.load(old_link)
 
252
    Traceback (most recent call last):
 
253
    ...
 
254
    HTTPError: HTTP Error 404: Not Found
 
255
    ...
 
256
 
 
257
    >>> print service.load(new_link).name
 
258
    Another Name
 
259
 
 
260
Under the covers though, a refresh of the original object has been
 
261
retrieved from the web service, so it's safe to continue using, and
 
262
changing it.
 
263
 
 
264
    >>> cookbook.description = u'This cookbook was renamed'
 
265
    >>> cookbook.lp_save()
 
266
    >>> print service.load(new_link).description
 
267
    This cookbook was renamed
 
268
 
 
269
It's just as easy to move this cookbook back to the old name.
 
270
 
 
271
    >>> cookbook.name = 'The Joy of Cooking'
 
272
    >>> cookbook.lp_save()
 
273
 
 
274
Now the old bookmark works again, and the new bookmark no longer works.
 
275
 
 
276
    >>> print service.load(old_link).name
 
277
    The Joy of Cooking
 
278
 
 
279
    >>> print service.load(new_link)
 
280
    Traceback (most recent call last):
 
281
    ...
 
282
    HTTPError: HTTP Error 404: Not Found
 
283
    ...
 
284
 
 
285
Validation
 
286
----------
 
287
 
 
288
Some attributes are subject to validation. For instance, a cookbook's
 
289
cuisine is limited to one of a few selections.
 
290
 
 
291
    >>> from lazr.restfulclient.errors import HTTPError
 
292
    >>> def print_error_on_save(entry):
 
293
    ...     try:
 
294
    ...         entry.lp_save()
 
295
    ...     except HTTPError, error:
 
296
    ...         for line in sorted(error.content.splitlines()):
 
297
    ...             print line.decode("utf-8")
 
298
    ...     else:
 
299
    ...         print 'Did not get expected HTTPError!'
 
300
 
 
301
    >>> cookbook.cuisine = 'No such cuisine'
 
302
    >>> print_error_on_save(cookbook)
 
303
    cuisine: Invalid value "No such cuisine". Acceptable values are: ...
 
304
    >>> cookbook.cuisine = 'General'
 
305
 
 
306
Some attributes can't be modified at all.
 
307
 
 
308
    >>> cookbook.copyright_date = None
 
309
    >>> print_error_on_save(cookbook)
 
310
    copyright_date: You tried to modify a read-only attribute.
 
311
 
 
312
If the client tries to save an entry that has more than one problem,
 
313
it will get back an error message listing all the problems.
 
314
 
 
315
    >>> cookbook.cuisine = 'No such cuisine'
 
316
    >>> print_error_on_save(cookbook)
 
317
    copyright_date: You tried to modify a read-only attribute.
 
318
    cuisine: Invalid value "No such cuisine". Acceptable values are: ...
 
319
 
 
320
 
 
321
Server-side data massage
 
322
------------------------
 
323
 
 
324
Send bad data and your request will be rejected. But if you send data
 
325
that's not quite what the server is expecting, the server may accept
 
326
it while tweaking it. This means that the state of your object after
 
327
you call lp_save() may be slightly different from the object before
 
328
you called lp_save().
 
329
 
 
330
    >>> cookbook.lp_refresh()
 
331
    >>> cookbook.description = "   Some extraneous whitespace  "
 
332
    >>> cookbook.lp_save()
 
333
    >>> cookbook.description
 
334
    u'Some extraneous whitespace'
 
335
 
 
336
Data types
 
337
----------
 
338
 
 
339
Incoming data is serialized from JSON, and all the JSON data types
 
340
appear to the end-user as native Python data types. But there's no
 
341
standard serialization for JSON dates, so those are handled
 
342
separately. From the perspective of the end-user, date and date-time
 
343
fields always look like Python datetime objects or None.
 
344
 
 
345
    >>> cookbook.copyright_date
 
346
    datetime.datetime(1995, 1, 1,...)
 
347
 
 
348
    >>> from datetime import datetime
 
349
    >>> cookbook.last_printing = datetime(2009, 1, 1)
 
350
    >>> cookbook.lp_save()
 
351
 
 
352
 
 
353
Avoiding conflicts
 
354
==================
 
355
 
 
356
lazr.restful and lazr.restfulclient work together to try to avoid
 
357
situations where one person unknowingly overwrites another's
 
358
work. Here, two different clients are interested in the same
 
359
lazr.restful object.
 
360
 
 
361
    >>> first_client = CookbookWebServiceClient()
 
362
    >>> first_cookbook = first_client.load(cookbook.self_link)
 
363
    >>> first_description = first_cookbook.description
 
364
 
 
365
    >>> second_client = CookbookWebServiceClient()
 
366
    >>> second_cookbook = second_client.load(cookbook.self_link)
 
367
    >>> second_cookbook.description == first_description
 
368
    True
 
369
 
 
370
The first client decides to change the description.
 
371
 
 
372
    >>> first_cookbook.description = 'A description.'
 
373
    >>> first_cookbook.lp_save()
 
374
 
 
375
The second client tries to make a conflicting change, but the server
 
376
detects that the second client doesn't have the latest information,
 
377
and rejects the request.
 
378
 
 
379
    >>> second_cookbook.description = 'A conflicting description.'
 
380
    >>> second_cookbook.lp_save()
 
381
    Traceback (most recent call last):
 
382
    ...
 
383
    HTTPError: HTTP Error 412: Precondition Failed
 
384
    ...
 
385
 
 
386
Now the second client has a chance to look at the changes that were
 
387
made, before making their own changes.
 
388
 
 
389
    >>> second_cookbook.lp_refresh()
 
390
    >>> print second_cookbook.description
 
391
    A description.
 
392
 
 
393
    >>> second_cookbook.description = 'A conflicting description.'
 
394
    >>> second_cookbook.lp_save()
 
395
 
 
396
Conflict detection works even when you operate on an object you
 
397
retrieved from a collection.
 
398
 
 
399
    >>> first_cookbook = first_client.cookbooks[:10][0]
 
400
    >>> second_cookbook = second_client.cookbooks[:10][0]
 
401
    >>> first_cookbook.name == second_cookbook.name
 
402
    True
 
403
 
 
404
    >>> first_cookbook.description = "A description"
 
405
    >>> first_cookbook.lp_save()
 
406
 
 
407
    >>> second_cookbook.description = "A conflicting description"
 
408
    >>> second_cookbook.lp_save()
 
409
    Traceback (most recent call last):
 
410
    ...
 
411
    HTTPError: HTTP Error 412: Precondition Failed
 
412
    ...
 
413
 
 
414
    >>> second_cookbook.lp_refresh()
 
415
    >>> print second_cookbook.description
 
416
    A description
 
417
 
 
418
    >>> second_cookbook.description = "A conflicting description"
 
419
    >>> second_cookbook.lp_save()
 
420
 
 
421
    >>> first_cookbook.lp_refresh()
 
422
    >>> print first_cookbook.description
 
423
    A conflicting description
 
424
 
 
425
 
 
426
Comparing entries
 
427
-----------------
 
428
 
 
429
Two entries are equal if they represent the same state of the same
 
430
server-side resource.
 
431
 
 
432
    >>> from lazr.restfulclient.tests.example import CookbookWebServiceClient
 
433
    >>> service = CookbookWebServiceClient()
 
434
 
 
435
What does this mean? Well, two distinct objects that represent the
 
436
same resource are equal.
 
437
 
 
438
    >>> recipe = service.recipes[1]
 
439
    >>> recipe_2 = service.load(recipe.self_link)
 
440
    >>> recipe is recipe_2
 
441
    False
 
442
 
 
443
    >>> recipe == recipe_2
 
444
    True
 
445
    >>> recipe != recipe_2
 
446
    False
 
447
 
 
448
Two totally different entries are not equal.
 
449
 
 
450
    >>> another_recipe = service.recipes[2]
 
451
    >>> recipe == another_recipe
 
452
    False
 
453
 
 
454
An entry can be compared to None, but the comparison never succeeds.
 
455
 
 
456
    >>> recipe == None
 
457
    False
 
458
 
 
459
If one entry represents the current state of the server, and the other
 
460
is out of date or has client-side modifications, they will not be
 
461
considered equal.
 
462
 
 
463
Here, 'recipe' has been modified and 'recipe_2' represents the current
 
464
state of the server.
 
465
 
 
466
    >>> recipe.instructions = "Modified for equality testing."
 
467
    >>> recipe == recipe_2
 
468
    False
 
469
 
 
470
After a save, 'recipe' is up to date, and 'recipe_2' is out of date.
 
471
 
 
472
    >>> recipe.lp_save()
 
473
    >>> recipe == recipe_2
 
474
    False
 
475
 
 
476
Refreshing 'recipe_2' brings it up to date, and equality succeeds again.
 
477
 
 
478
    >>> recipe_2.lp_refresh()
 
479
    >>> recipe == recipe_2
 
480
    True
 
481
 
 
482
If you make the _exact same_ client-side modifications to two objects
 
483
representing the same resource, the objects will be considered equal.
 
484
 
 
485
    >>> recipe.instructions = "Modified again."
 
486
    >>> recipe_2.instructions = recipe.instructions
 
487
    >>> recipe == recipe_2
 
488
    True
 
489
 
 
490
If you then save one of the objects, they will stop being equal,
 
491
because the saved object has a new ETag.
 
492
 
 
493
    >>> recipe.lp_save()
 
494
    >>> recipe == recipe_2
 
495
    False
 
496
 
 
497
When are representations fetched?
 
498
=================================
 
499
 
 
500
To avoid unnecessary HTTP requests, a representation of an entry is
 
501
fetched at the last possible moment. Let's see what that means.
 
502
 
 
503
    >>> import httplib2
 
504
    >>> httplib2.debuglevel = 1
 
505
 
 
506
    >>> service = CookbookWebServiceClient()
 
507
    send: ...
 
508
    ...
 
509
 
 
510
Here's an entry we got from a lookup operation on a top-level
 
511
collection. Just doing the lookup operation doesn't trigger an HTTP
 
512
request, because CookbookWebServiceClient happens to know that the
 
513
'recipes' collection contains recipe objects.
 
514
 
 
515
    >>> recipe1 = service.recipes[1]
 
516
 
 
517
Here's the dish associated with that original entry. Traversing from
 
518
one entry to another causes an HTTP request for the first
 
519
entry. Without this HTTP request, there's no way to know the URL of
 
520
the second entry.
 
521
 
 
522
    >>> dish = recipe1.dish
 
523
    send: 'GET /1.0/recipes/1 ...'
 
524
    ...
 
525
 
 
526
Note that this request is a request for the _recipe_, not the dish. We
 
527
don't need to know anything about the dish yet. And now that we have a
 
528
representation of the recipe, we can traverse from the recipe to its
 
529
cookbook without making another request.
 
530
 
 
531
    >>> cookbook = recipe1.cookbook
 
532
 
 
533
Accessing any information about an entry we've traversed to _will_
 
534
cause an HTTP request.
 
535
 
 
536
    >>> print dish.name
 
537
    send: 'GET /1.0/dishes/Roast%20chicken ...'
 
538
    ...
 
539
    Roast chicken
 
540
 
 
541
Invoking a named operation also causes one (and only one) HTTP
 
542
request.
 
543
 
 
544
    >>> recipes = cookbook.find_recipes(search="foo")
 
545
    send: 'get /1.0/cookbooks/...ws.op=find_recipes...'
 
546
    ...
 
547
 
 
548
Even dereferencing an entry from another entry and then invoking a
 
549
named operation causes only one HTTP request.
 
550
 
 
551
    >>> recipes = recipe1.cookbook.find_recipes(search="bar")
 
552
    send: 'get /1.0/cookbooks/...ws.op=find_recipes...'
 
553
    ...
 
554
 
 
555
In all cases we are able to delay HTTP requests until the moment we
 
556
need data that can only be found by making those HTTP requests. If it
 
557
turns out we never need that data, we've eliminated a request
 
558
entirely.
 
559
 
 
560
If CookbookWebServiceClient didn't know that the 'recipes' collection
 
561
contained recipe objects, then doing a lookup on that collection *would*
 
562
trigger an HTTP request. There'd simply be no other way to know what
 
563
kind of object was at the other end of the URL.
 
564
 
 
565
    >>> from lazr.restfulclient.tests.example import RecipeSet
 
566
    >>> old_collection_of = RecipeSet.collection_of
 
567
    >>> RecipeSet.collection_of = None
 
568
 
 
569
    >>> recipe1 = service.recipes[1]
 
570
    send: 'GET /1.0/recipes/1 ...'
 
571
    ...
 
572
 
 
573
On the plus side, at least accessing this object's properties doesn't
 
574
require _another_ HTTP request.
 
575
 
 
576
    >>> print recipe1.instructions
 
577
    Modified again.
 
578
 
 
579
Cleanup.
 
580
 
 
581
    >>> RecipeSet.collection_of = old_collection_of
 
582
    >>> httplib2.debuglevel = 0