This file is indexed.

/usr/share/pyshared/lazr/restful/example/base/tests/entry.txt is in python-lazr.restful 0.19.3-0ubuntu2.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
Entries
*******

Most objects published by a lazr.restful web service are entries:
self-contained data structures with an independent existence from any
other entry. Entries are distinguished from collections, which are
groupings of entries.

All entries in a web service work pretty much the same way. This
document illustrates the general features of entries, using the
example web service's dishes and recipes as examples.

    >>> from lazr.restful.testing.webservice import WebServiceCaller
    >>> webservice = WebServiceCaller(domain='cookbooks.dev')

=======
Reading
=======

It's possible to get a JSON 'representation' of an entry by sending a
GET request to the entry's URL.

Here we see that the cookbook 'Everyday Greens' is a vegetarian cookbook.

    >>> from urllib import quote
    >>> greens_url = quote("/cookbooks/Everyday Greens")
    >>> webservice.get(greens_url).jsonBody()['cuisine']
    u'Vegetarian'

Data is served encoded in UTF-8, and a good client will automatically
convert it into Unicode.

    >>> construsions_url = quote("/cookbooks/Construsions un repas")
    >>> webservice.get(construsions_url).jsonBody()['cuisine']
    u'Fran\xe7aise'

Content negotiation
===================

By varying the 'Accept' header, the client can request either a JSON
or XHTML representation of an entry, or a WADL description of the
entry's capabilities.

    >>> def negotiated_type(accept_header,
    ...                     uri='/cookbooks/Everyday%20Greens'):
    ...     return webservice.get(
    ...         uri, accept_header).getheader('Content-Type')

    >>> negotiated_type('application/json')
    'application/json'

    >>> negotiated_type('application/xhtml+xml')
    'application/xhtml+xml'

    >>> negotiated_type('application/vnd.sun.wadl+xml')
    'application/vnd.sun.wadl+xml'

    >>> negotiated_type('')
    'application/json'

    >>> negotiated_type('text/html')
    'application/json'

    >>> negotiated_type('application/json, application/vnd.sun.wadl+xml')
    'application/json'

    >>> negotiated_type('application/json, application/xhtml+xml')
    'application/json'

    >>> negotiated_type('application/vnd.sun.wadl+xml, text/html, '
    ...                 'application/json')
    'application/vnd.sun.wadl+xml'

    >>> negotiated_type('application/json;q=0.5, application/vnd.sun.wadl+xml')
    'application/vnd.sun.wadl+xml'

    >>> negotiated_type('application/json;q=0, application/xhtml+xml;q=0.05,'
    ...                 'application/vd.sun.wadl+xml;q=0.1')
    'application/vd.sun.wadl+xml'

The client can also set the 'ws.accept' query string variable, which
will take precedence over any value set for the Accept header.

    >>> def qs_negotiated_type(query_string, header):
    ...     uri = '/cookbooks/Everyday%20Greens?ws.accept=' + query_string
    ...     return negotiated_type(header, uri)

    >>> qs_negotiated_type('application/json', '')
    'application/json'

    >>> qs_negotiated_type('application/json', 'application/xhtml+xml')
    'application/json'

    >>> negotiated_type('application/json;q=0, application/xhtml+xml;q=0.5,'
    ...                 'application/json;q=0.5, application/xhtml+xml;q=0,')
    'application/xhtml+xml'

Earlier versions of lazr.restful served a misspelling of the WADL
media type. For purposes of backwards compatibility, lazr.restful
will still serve this media type if it's requested.

    >>> negotiated_type('application/vd.sun.wadl+xml')
    'application/vd.sun.wadl+xml'

XHTML representations
=====================

Every entry has an XHTML representation. The default representation is
a simple definition list.

    >>> print webservice.get(greens_url, 'application/xhtml+xml')
    HTTP/1.1 200 Ok
    ...
    <dl ...>
    ...
    </dl>

Getting the XHTML representation works correctly even when some of the fields
have non-ascii values.

    >>> print webservice.get(construsions_url, 'application/xhtml+xml')
    HTTP/1.1 200 Ok
    ...
    <dl ...>
    ...
    <BLANKLINE>
     <dt>cuisine</dt>
     <dd>Française</dd>
    <BLANKLINE>
    ...
    </dl>

But it's possible to define a custom HTML view for a particular object
type. Here's a simple view that serves some hard-coded HTML.

    >>> class DummyView:
    ...
    ...     def __init__(*args):
    ...         pass
    ...
    ...     def __call__(*args):
    ...         return "<html>foo</html>"

Register the view as the IWebServiceClientRequest view for an
ICookbook entry...

    >>> from lazr.restful.interfaces import IWebServiceClientRequest
    >>> from lazr.restful.example.base.interfaces import ICookbook
    >>> from zope.interface.interfaces import IInterface
    >>> view_name = "lazr.restful.EntryResource"
    >>> from zope.component import getGlobalSiteManager
    >>> manager = getGlobalSiteManager()
    >>> manager.registerAdapter(
    ...      factory=DummyView,
    ...      required=[ICookbook, IWebServiceClientRequest],
    ...      provided=IInterface, name=view_name)

...and the XHTML representation of an ICookbook will be the result of
calling a DummyView object.

    >>> print webservice.get(greens_url, 'application/xhtml+xml')
    HTTP/1.1 200 Ok
    ...
    <html>foo</html>

Before we continue, here's some cleanup code to remove the custom view
we just defined.

    >>> from zope.component import getGlobalSiteManager
    >>> ignored = getGlobalSiteManager().unregisterAdapter(
    ...      factory=DummyView,
    ...      required=[ICookbook, IWebServiceClientRequest],
    ...      provided=IInterface, name=view_name)

    >>> print webservice.get(greens_url, 'application/xhtml+xml')
    HTTP/1.1 200 Ok
    ...
    <dl ...>
    ...
    </dl>

Visibility
==========

There are two recipes in "James Beard's American Cookery", but one of
them has been marked private. The private one cannot be retrieved.

    >>> print webservice.get('/recipes/3')
    HTTP/1.1 200 Ok
    ...

    >>> print webservice.get('/recipes/5')
    HTTP/1.1 401 Unauthorized
    ...

If a resource itself is visible to the client, but it contains
information that's not visible, the information will be
redacted. Here, a cookbook resource is visible to our client, but its
value for the 'confirmed' field is not visible.

    >>> cookbook = webservice.get(greens_url).jsonBody()
    >>> print cookbook['name']
    Everyday Greens
    >>> print cookbook['confirmed']
    tag:launchpad.net:2008:redacted

Named operations
================

Some entries support custom operations through GET. The custom
operation to be invoked is named in the query string's 'ws.op'
argument. You can search a cookbook's recipes by specifying
the 'find_recipes' operation.

    >>> joy_url = quote("/cookbooks/The Joy of Cooking")
    >>> recipes = webservice.get(
    ...     "%s?ws.op=find_recipes&search=e" % joy_url).jsonBody()
    >>> sorted([r['self_link'] for r in recipes['entries']])
    [u'.../recipes/2', u'.../recipes/4']

A named operation can take as an argument the URL to another
object. Here the 'dish' argument is the URL to a dish, and the named
operation finds a recipe for making that dish.

    >>> def find_recipe_in_joy(dish_url):
    ...     """Look up a dish in 'The Joy of Cooking'."""
    ...     return webservice.get("%s?ws.op=find_recipe_for&dish=%s" %
    ...         (joy_url, quote(dish_url))).jsonBody()

    >>> dish_url = webservice.get("/recipes/2").jsonBody()['dish_link']
    >>> find_recipe_in_joy(dish_url)['instructions']
    u'Draw, singe, stuff, and truss...'

The URL passed in to a named operation may be an absolute URL, or it
may be relative to the versioned service root. This is for developer
convenience only, as lazr.restful never serves relative URLs.

    >>> print dish_url
    http://cookbooks.dev/devel/dishes/Roast%20chicken
    >>> relative_url = quote("/dishes/Roast chicken")
    >>> find_recipe_in_joy(relative_url)['instructions']
    u'Draw, singe, stuff, and truss...'

A URL relative to the unversioned service root will not work.

    >>> relative_url = quote("/devel/dishes/Roast chicken")
    >>> find_recipe_in_joy(relative_url)
    Traceback (most recent call last):
    ...
    ValueError: dish: No such object "/devel/dishes/Roast%20chicken".


Some entries support custom operations through POST. You can invoke a
custom operation to modify a cookbook's name, making it seem more
interesting.

    >>> print webservice.get(joy_url).jsonBody()['cuisine']
    General

    >>> print webservice.named_post(joy_url, 'make_more_interesting', {})
    HTTP/1.1 200 Ok
    ...

    >>> new_joy_url = quote("/cookbooks/The New The Joy of Cooking")
    >>> print webservice.get(new_joy_url).jsonBody()['name']
    The New The Joy of Cooking

Custom operations may have error handling.

    >>> print webservice.named_post(new_joy_url, 'make_more_interesting', {})
    HTTP/1.1 400 Bad Request
    ...
    The 'New' trick can't be used on this cookbook because its
    name already starts with 'The New'.

    >>> import simplejson
    >>> ignore = webservice.patch(
    ...     new_joy_url, 'application/json',
    ...     simplejson.dumps({"name": "The Joy of Cooking"}))

Trying to invoke a nonexistent custom operation yields an error.

    >>> print webservice.get("%s?ws.op=no_such_operation" % joy_url)
    HTTP/1.1 400 Bad Request
    ...
    No such operation: no_such_operation

============
Modification
============

It's possible to modify an entry by sending to the server a document
asserting what the entry should look like. The document may only
describe part of the entry's new state, in which case the client
should use the PATCH HTTP method. Or it may completely describe the
entry's state, in which case the client should use PUT.

    >>> def modify_entry(url, representation, method, headers=None):
    ...     "A helper function to PUT or PATCH an entry."
    ...     new_headers = {'Content-type': 'application/json'}
    ...     if headers is not None:
    ...         new_headers.update(headers)
    ...     return webservice(
    ...         url, method, simplejson.dumps(representation), headers)

    >>> def modify_cookbook(cookbook, representation, method, headers=None):
    ...     "A helper function to PUT or PATCH a cookbook."
    ...     return modify_entry(
    ...         '/cookbooks/' + quote(cookbook), representation,
    ...         method, headers)

Here we use the web service to change the cuisine of the "Everyday
Greens" cookbook. The data returned is the new JSON representation of
the object.

    >>> print webservice.get(greens_url).jsonBody()['revision_number']
    0

    >>> result = modify_cookbook('Everyday Greens', {'cuisine' : 'American'},
    ...                          'PATCH')
    >>> print result
    HTTP/1.1 209 Content Returned
    ...
    Content-Type: application/json
    ...
    {...}

    >>> greens = result.jsonBody()
    >>> print greens['cuisine']
    American

Whenever a client modifies a cookbook, the revision_number is
incremented behind the scenes.

    >>> print greens['revision_number']
    1

A modification may cause one of en entry's links to point to another
object. Here, we change the 'dish_link' field of a roast chicken
recipe, turning it into a recipe for baked beans.

    >>> old_dish = webservice.get("/recipes/1").jsonBody()['dish_link']
    >>> print old_dish
    http://.../dishes/Roast%20chicken

    >>> new_dish = webservice.get("/recipes/4").jsonBody()['dish_link']
    >>> print new_dish
    http://.../dishes/Baked%20beans

    >>> new_entry = modify_entry(
    ...     "/recipes/2", {'dish_link' : new_dish}, 'PATCH').jsonBody()
    >>> print new_entry['dish_link']
    http://.../dishes/Baked%20beans

When changing one of an entry's links, you can use an absolute URL (as
seen above) or a URL relative to the versioned service root. Let's use
a relative URL to change the baked beans recipe back to a roast
chicken recipe.

    >>> relative_old_dish = quote('/dishes/Roast chicken')
    >>> new_entry = modify_entry(
    ...     "/recipes/2", {'dish_link' : relative_old_dish},
    ...     'PATCH').jsonBody()
    >>> print new_entry['dish_link']
    http://.../dishes/Roast%20chicken

A modification might cause an entry's address to change. Here we use
the web service to change the cookbook's name to 'Everyday Greens 2'.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'name' : 'Everyday Greens 2'}, 'PATCH')
    HTTP/1.1 301 Moved Permanently
    ...
    Location: http://.../Everyday%20Greens%202
    ...

At this point we can no longer manipulate this cookbook by sending
HTTP requests to http://cookbooks.dev/1.0/cookbooks/Everyday%20Greens,
because that cookbook now 'lives' at
http://cookbooks.dev/1.0/cookbooks/Everyday%20Greens%202. To change
the cookbook name back, we need to send a PATCH request to the new
address.

    >>> print modify_cookbook('Everyday Greens 2',
    ...                       {'name' : 'Everyday Greens'}, 'PATCH')
    HTTP/1.1 301 Moved Permanently
    ...
    Location: http://.../cookbooks/Everyday%20Greens
    ...

The PATCH HTTP method is useful for simple changes, but not all HTTP
clients support PATCH. It's possible to fake a PATCH request with
POST, by setting the X-HTTP-Method-Override header to "PATCH". Because
Firefox 3 mangles the Content-Type header for POST requests, you may
also set the X-Content-Type-Override header, which will override the
value of Content-Type.

    >>> print modify_cookbook('Everyday Greens',
    ...     {'cuisine' : 'General'}, 'POST',
    ...     {'X-HTTP-Method-Override' : 'PATCH',
    ...      'Content-Type': 'not-a-valid-content/type',
    ...      'X-Content-Type-Override': 'application/json'})
    HTTP/1.1 209 Content Returned
    ...

Here, the use of a nonexistent HTTP method causes an error.

    >>> print modify_cookbook('Everyday Greens',
    ...     {'cuisine' : 'General'}, 'POST',
    ...     {'X-HTTP-Method-Override' : 'NOSUCHMETHOD'})
    HTTP/1.1 405 Method Not Allowed
    ...

X-HTTP-Method-Override is only respected when the underlying HTTP
method is POST. If you use X-HTTP-Method-Override with any other HTTP
method, your value is ignored. Here, a nonexistent HTTP method is
ignored in favor of HTTP GET.

    >>> print webservice('/cookbooks/Everyday%20Greens', 'GET',
    ...     headers={'X-HTTP-Method-Override' : 'NOSUCHMETHOD'})
    HTTP/1.1 200 Ok
    ...
    Content-Type: application/json
    ...

Even if a client supports PATCH, sometimes it's easier to GET a
document, modify it, and send it back. If you have the full document
at hand, you can use the PUT method.

We happen to have a full document from when we sent a GET request to
the 'Everday Greens' cookbook. Modifying that document and PUTting it
back is less work than constructing a new document and sending it with
PATCH. As with PATCH, a successful PUT serve the new representation of
the object that was modified.

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> print greens['cuisine']
    General

    >>> greens['cuisine'] = 'Vegetarian'
    >>> print modify_cookbook('Everyday Greens', greens, 'PUT')
    HTTP/1.1 209 Content Returned
    ...
    {...}

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> print greens['cuisine']
    Vegetarian

Because our patch format is the same as our representation format (a
JSON hash), any document that works with a PUT request will also work
with a PATCH request.

    >>> print modify_cookbook('Everyday Greens', greens, 'PATCH')
    HTTP/1.1 209 Content Returned
    ...

Content negotiation during modification
=======================================

When making a PATCH, you don't have to get a JSON representation
back. You can also get an HTML representation.

    >>> print modify_cookbook('Everyday Greens', {}, 'PATCH',
    ...                       headers={'Accept': 'application/xhtml+xml'})
    HTTP/1.1 209 Content Returned
    ...
    Content-Type: application/xhtml+xml
    ...
    <?xml version="1.0"?>
    ...

You can even get a WADL representation, though that's pretty useless.

    >>> headers = {'Accept':'application/vd.sun.wadl+xml'}
    >>> print modify_cookbook('Everyday Greens', {}, 'PATCH',
    ...                       headers=headers)
    HTTP/1.1 209 Content Returned
    ...
    Content-Type: application/vd.sun.wadl+xml
    ...

Server-side modification
========================

Sometimes the server will transparently modify a value sent by the
client, to clean it up or put it into a canonical form. For this
purpose, the response to a PUT or PATCH request includes a brand new
JSON representation of the object, so that the client can know whether
and which changes were made.

Here's an example. If a cookbook's description contains leading or trailing
whitespace, the whitespace will be stripped.

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> greens['description']
    u''
    >>> first_etag = greens['http_etag']

Send in a name with leading or trailing whitespace and it'll be
transparently trimmed. The document returned from the POST request
will be the new representation, modified by both client and server.

    >>> greens = webservice(
    ...     greens_url, "PATCH",
    ...     simplejson.dumps({'description' : '  A description '}),
    ...     {'Content-type': 'application/json'}).jsonBody()
    >>> greens['description']
    u'A description'
    >>> greens['http_etag'] == first_etag
    False

The canonicalization works for PUT requests as well.

    >>> greens['description'] = "    Another description  "
    >>> greens = webservice(greens_url, "PUT", simplejson.dumps(greens),
    ...                     {'Content-type': 'application/json'}).jsonBody()
    >>> greens['description']
    u'Another description'

Conditional GET, PUT and PATCH
==============================

When you GET an entry you're given an ETag; an opaque string that
changes whenever the entry changes.

    >>> response = webservice.get(greens_url)
    >>> greens_etag = response.getheader('ETag')
    >>> greens = response.jsonBody()

The ETag is present in the HTTP response headers when you GET an
entry, but it's also present in the representation of the entry
itself.

    >>> greens['http_etag'] == greens_etag
    True

This is so you can get the ETags for all the entries in a collection
at once, without making a separate HTTP request for each.

    >>> cookbooks = webservice.get('/cookbooks').jsonBody()
    >>> etags = [book['http_etag'] for book in cookbooks['entries']]

The ETag provided with an entry of a collection is the same as the
ETag you'd get if you got that entry individually.

    >>> first_book = cookbooks['entries'][0]
    >>> first_book_2 = webservice.get(first_book['self_link']).jsonBody()
    >>> first_book['http_etag'] == first_book_2['http_etag']
    True

When you make a GET request, you can provide the ETag as the
If-None-Match header. This lets you save time when the resource hasn't
changed.

    >>> print webservice.get(greens_url,
    ...                      headers={'If-None-Match': greens_etag})
    HTTP/1.1 304 Not Modified
    ...

Conditional GET works the same way whether the request goes through
the web service's virtual host or through the website-level interface
designed for Ajax.

    >>> from lazr.restful.testing.webservice import WebServiceAjaxCaller
    >>> ajax = WebServiceAjaxCaller(domain='cookbooks.dev')
    >>> etag = 'dummy-etag'
    >>> response = ajax.get(greens_url, headers={'If-None-Match' : etag})
    >>> etag = response.getheader("Etag")
    >>> print ajax.get(greens_url, headers={'If-None-Match' : etag})
    HTTP/1.1 304 Not Modified
    ...

When you make a PUT or PATCH request, you can provide the ETag as the
If-Match header. This lets you detect changes that other people made
to the entry, so your changes don't overwrite theirs.

If the ETag you provide in If-Match matches the entry's current ETag,
your request goes through.

    >>> print modify_cookbook('Everyday Greens', greens, 'PATCH',
    ...                     {'If-Match' : greens_etag})
    HTTP/1.1 209 Content Returned
    ...

If the ETags don't match, it's because somebody modified the entry
after you got your copy of it. Your request will fail with status code
412.

    >>> print modify_cookbook('Everyday Greens', greens, 'PATCH',
    ...                       {'If-Match' : '"an-old-etag"'})
    HTTP/1.1 412 Precondition Failed
    ...

If you specify a number of ETags, and any of them match, your request
will go through.

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> match = '"an-old-etag", %s' % greens['http_etag']
    >>> print modify_cookbook('Everyday Greens', greens, 'PATCH',
    ...                       {'If-Match' : match})
    HTTP/1.1 209 Content Returned
    ...

Both PUT and PATCH requests work this way.

    >>> print modify_cookbook('Everyday Greens', greens, 'PUT',
    ...                       {'If-Match' : 'an-old-etag'})
    HTTP/1.1 412 Precondition Failed
    ...

Conditional writes are a little more complicated
************************************************

OK, but consider the 'copyright_date' field of a cookbook. This is
published as a read-only field; the client can't change it. But it's
not read-only on the server side. What if this value changes on the
server-side? What happens to the ETag then? Does it change, causing a
conditional PATCH to fail, even if the PATCH doesn't touch that
read-only field?

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> print greens['copyright_date']
    2003-01-01
    >>> etag_before_server_modification = greens['http_etag']

    >>> from lazr.restful.example.base.root import C4
    >>> greens_object = C4
    >>> old_date = greens_object.copyright_date
    >>> old_date
    datetime.date(2003, 1, 1)

Let's change the server-side value and find out.

    >>> import datetime
    >>> greens_object.copyright_date = datetime.date(2005, 12, 12)

    >>> new_greens = webservice.get(greens_url).jsonBody()
    >>> print new_greens['copyright_date']
    2005-12-12
    >>> etag_after_server_modification = new_greens['http_etag']

The ETag has indeed changed.

    >>> etag_before_server_modification == etag_after_server_modification
    False

So if we try to modify the cookbook using the old ETag, it should
fail, right?

    >>> body = {'description' : 'New description.'}
    >>> print modify_cookbook('Everyday Greens', body, 'PATCH',
    ...                       {'If-Match' : etag_before_server_modification})
    HTTP/1.1 209 Content Returned
    ...

Actually, it succeeds! How does that work? Well, the ETag consists of
two parts, separated by a dash.

    >>> read_before, write_before = etag_before_server_modification.split('-')
    >>> read_after, write_after = etag_after_server_modification.split('-')

The first part of the ETag only changes when a field the client can't
modify changes. This is the part of the ETag that changed when
copyright_date changed.

    >>> read_before == read_after
    False

The second part only changes when a field changes that the client can
modify. This part of the ETag hasn't changed.

    >>> write_before == write_after
    True

When you make a conditional write, the second part of the ETag you
provide is checked against the second part of the ETag generated on
the server. The first part of the ETag is ignored.

The point of checking the ETag is to avoid conflicts where two clients
modify the same resource. But no client can modify any of those
read-only fields, so changes to them don't matter for purposes of
avoiding conflicts.

If you hack the ETag to something that's not "two parts, separated by
a dash", lazr.restful will still handle it. (Of course, since that
ETag will never match anything, you'll always get a 412 error.)

    >>> body = {'description' : 'New description.'}
    >>> print modify_cookbook('Everyday Greens', body, 'PATCH',
    ...                       {'If-Match' : "Weird etag"})
    HTTP/1.1 412 Precondition Failed
    ...

Conditional PUT fails where a PATCH would succeed, but not because
lazr.restful rejects an old ETag. To verify this, let's change the
cookbook's copyright_date again, behind the scenes.

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> old_etag = greens['http_etag']
    >>> greens_object.copyright_date = datetime.date(2005, 11, 11)

A totally bogus ETag fails with a 412 error.

    >>> greens['description'] = 'Another new description'
    >>> print modify_cookbook('Everyday Greens', greens, 'PUT',
    ...                       {'If-Match' : "Not the old ETag"})
    HTTP/1.1 412 Precondition Failed
    ...

When we use the original ETag, we don't cause a 412 error, but the PUT
request fails anyway.

    >>> print modify_cookbook('Everyday Greens', greens, 'PUT',
    ...                       {'If-Match' : old_etag})
    HTTP/1.1 400 Bad Request
    ...
    http_etag: You tried to modify a read-only attribute.
    copyright_date: You tried to modify a read-only attribute.

Rather, it's because a PUT request includes a representation of the
entire resource, and lazr.restful thinks the client is trying to
modify the fields that changed behind the scenes--in this case,
copyright_date and http_etag.

Conditional reads are *not* more complicated
********************************************

The point of the two-part ETag is to avoid spurious 412 errors when
doing conditional writes. When making a conditional _read_ request,
the condition will fail if _any_ part of the ETag is different.

    >>> new_etag = webservice.get(greens_url).jsonBody()['http_etag']

    >>> print webservice.get(
    ...     greens_url,
    ...     headers={'If-None-Match': new_etag})
    HTTP/1.1 304 Not Modified
    ...

    >>> print webservice.get(
    ...     greens_url,
    ...     headers={'If-None-Match': "changed" + new_etag})
    HTTP/1.1 200 Ok
    ...

lazr.restful checks the entire ETag on conditional GET because the
purpose of a conditional read is to avoid getting data that hasn't
changed. A server-side change to a read-only field like copyright_date
doesn't affect future client writes, but it _is_ a change to the
representation.

A bit of cleanup: restore the old value for the cookbook's
copyright_date.

    >>> greens_object.copyright_date = old_date

Changing object relationships
=============================

In addition to changing an object's data fields, you can change its
relationships to other objects. Here we change which dish a recipe is
for.

    >>> recipe_url = '/recipes/3'
    >>> recipe = webservice.get(recipe_url).jsonBody()
    >>> print recipe['dish_link']
    http://.../dishes/Roast%20chicken

    >>> def modify_dish(url, recipe, new_dish_url):
    ...     recipe['dish_link'] = new_dish_url
    ...     return webservice.put(
    ...         url, 'application/json', simplejson.dumps(recipe))

    >>> new_dish = webservice.get(quote('/dishes/Baked beans')).jsonBody()
    >>> new_dish_url = new_dish['self_link']
    >>> recipe['dish_link'] = new_dish_url
    >>> print modify_dish(recipe_url, recipe, new_dish_url)
    HTTP/1.1 209 Content Returned
    ...

    >>> recipe = webservice.get(recipe_url).jsonBody()
    >>> print recipe['dish_link']
    http://.../dishes/Baked%20beans

Identification of the dish is done by specifying a URL; a random
string won't work.

    >>> print modify_dish(recipe_url, recipe, 'A random string')
    HTTP/1.1 400 Bad Request
    ...
    dish_link: "A random string" is not a valid URI.

But not just any URL will do. It has to identify an object in the web
service.

    >>> print modify_dish(recipe_url, recipe, 'http://www.canonical.com')
    HTTP/1.1 400 Bad Request
    ...
    dish_link: No such object "http://www.canonical.com".

    >>> print modify_dish(
    ...     recipe_url, recipe,
    ...     'http://www.canonical.com/dishes/Baked%20beans')
    HTTP/1.1 400 Bad Request
    ...
    dish_link: No such object "http://www.canonical.com/dishes/Baked%20beans".

This URL would be valid, but it uses the wrong protocol (HTTPS instead
of HTTP).

    >>> https_link = recipe['dish_link'].replace('http:', 'https:')
    >>> print modify_dish(recipe_url, recipe, https_link)
    HTTP/1.1 400 Bad Request
    ...
    dish_link: No such object "https://.../Baked%20beans".

Even a URL that identifies an object in the web service won't work, if
the object isn't the right kind of object. A recipe must be for a
dish, not a cookbook:

    >>> print modify_dish(recipe_url, recipe, recipe['cookbook_link'])
    HTTP/1.1 400 Bad Request
    ...
    dish_link: Your value points to the wrong kind of object

Date formats
============

lazr.restful web services serve and parse dates in ISO 8601
format. Only UTC dates are allowed.

The tests that follow make a number of PATCH requests that include
values for a cookbook's 'copyright_date' attribute.

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> greens['copyright_date']
    u'2003-01-01'

    >>> def patch_greens_copyright_date(date):
    ...     "A helper method to try and change a date field."
    ...     return modify_cookbook(
    ...         'Everyday Greens', {'copyright_date' : date}, 'PATCH')

These requests aren't actually trying to modify 'copyright_date', which
is read-only. They're asserting that 'copyright_date' is a certain
value. If the assertion succeeds (because 'copyright_date' does in fact
have that value), the response code is 200. If the assertion could not
be understood (because the date is in the wrong format), the response
code is 400, and the body is an error message about the date
format. If the assertion _fails_ (because 'copyright_date' happens to be
read-only), the response code is also 400, but the error message talks
about an attempt to modify a read-only attribute.

The two 400 error codes below are caused by a failure to understand
the assertion. The string used in the assertion might not be a date.

    >>> print patch_greens_copyright_date('dummy')
    HTTP/1.1 400 Bad Request
    ...
    copyright_date: Value doesn't look like a date.

Or it might be a date that's not in UTC.

    >>> print patch_greens_copyright_date(u'2005-06-06T00:00:00.000000+05:00')
    HTTP/1.1 400 Bad Request
    ...
    copyright_date: Time not in UTC.

There are five ways to specify UTC:

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000Z')
    HTTP/1.1 209 Content Returned
    ...

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000+00:00')
    HTTP/1.1 209 Content Returned
    ...

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000+0000')
    HTTP/1.1 209 Content Returned
    ...

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000-00:00')
    HTTP/1.1 209 Content Returned
    ...

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000-0000')
    HTTP/1.1 209 Content Returned
    ...

A value with a missing timezone is treated as UTC.

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000')
    HTTP/1.1 209 Content Returned
    ...

Less precise time measurements may also be acceptable.

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00Z')
    HTTP/1.1 209 Content Returned
    ...

    >>> print patch_greens_copyright_date(u'2003-01-01')
    HTTP/1.1 209 Content Returned
    ...

What you can't do
=================

A document that would be acceptable as the payload of a PATCH request
might not be acceptable as the payload of a PUT request.

    >>> print modify_cookbook('Everyday Greens', {'name' : 'Greens'}, 'PUT')
    HTTP/1.1 400 Bad Request
    ...
    You didn't specify a value for the attribute 'cuisine'.

A document that's not a valid JSON document is also unacceptable.

    >>> print webservice.patch(greens_url, "application/json", "{")
    HTTP/1.1 400 Bad Request
    ...
    Entity-body was not a well-formed JSON document.

A document that's valid JSON but is not a JSON hash is unacceptable.

    >>> print modify_cookbook('Everyday Greens', 'name=Greens', 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    Expected a JSON hash.

An entry's read-only attributes can't be modified.

    >>> print modify_cookbook(
    ...     'Everyday Greens',
    ...     {'copyright_date' : u'2001-01-01T01:01:01+00:00Z'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    copyright_date: You tried to modify a read-only attribute.

You can send a document that includes a value for a read-only
attribute, but it has to be the same as the current value.

    >>> print modify_cookbook(
    ...     'Everyday Greens',
    ...     {'copyright_date' : greens['copyright_date']}, 'PATCH')
    HTTP/1.1 209 Content Returned
    ...

You can't change the link to an entry's associated collection.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'recipes_collection_link' : 'dummy'},
    ...                       'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    recipes_collection_link: You tried to modify a collection...

Again, you can send a document that includes a value for an associated
collection link; you just can't _change_ the value.

    >>> print modify_cookbook(
    ...     'Everyday Greens',
    ...     {'recipes_collection_link' : greens['recipes_collection_link']},
    ...     'PATCH')
    HTTP/1.1 209 Content Returned
    ...

You can't directly change an entry's URL address.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'self_link' : 'dummy'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    self_link: You tried to modify a read-only attribute.

You can't directly change an entry's ETag.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'http_etag' : 'dummy'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    http_etag: You tried to modify a read-only attribute.

You can't change an entry's resource type.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'resource_type_link' : 'dummy'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    resource_type_link: You tried to modify a read-only attribute.

You can't refer to a link to an associated object or collection as
though it were the actual object. A cookbook has a
'recipes_collection_link', but it doesn't have 'recipes' directly.

    >>> print modify_cookbook(
    ...     'Everyday Greens', {'recipes' : 'dummy'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    recipes: You tried to modify a nonexistent attribute.

A recipe has a 'dish_link', but it doesn't have a 'dish' directly.

    >>> url = quote('/cookbooks/The Joy of Cooking/Roast chicken')
    >>> print webservice.patch(url, 'application/json',
    ...     simplejson.dumps({'dish' : 'dummy'}))
    HTTP/1.1 400 Bad Request
    ...
    dish: You tried to modify a nonexistent attribute.

You can't set values that violate data integrity rules. For instance,
you can't set a required value to None.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'name' : None}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    name: Missing required value.

And of course you can't modify attributes that don't exist.

    >>> print modify_cookbook(
    ...     'Everyday Greens', {'nonesuch' : 'dummy'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    nonesuch: You tried to modify a nonexistent attribute.

Deletion
========

Some entries may be deleted with the HTTP DELETE method. In the
example web service, recipes can be deleted.

    >>> recipe_url = "/recipes/6"
    >>> print webservice.get(recipe_url)
    HTTP/1.1 200 Ok
    ...

    >>> print webservice.delete(recipe_url)
    HTTP/1.1 200 Ok
    ...

    >>> print webservice.get(recipe_url)
    HTTP/1.1 404 Not Found
    ...