Some resources published by lazr.restful are externally hosted files
that can have binary representations. lazr.restfulclient gives you
access to these resources.

    >>> from lazr.restfulclient.tests.example import CookbookWebServiceClient
    >>> service = CookbookWebServiceClient()

An example of a hosted binary file is the cover of a
cookbook. "Everyday Greens" starts off with no cover.

    >>> greens = service.cookbooks['Everyday Greens']
    >>> cover = greens.cover
    >>> sorted(dir(cover))
    [..., 'open']

    >>> cover.open()
    Traceback (most recent call last):
    ...
    HTTPError: HTTP Error 404: Not Found

You can open a hosted file for write access and write to it as though
it were a file on disk.

    >>> image = "Pretend this is an image."
    >>> len(image)
    25

    >>> file_handle = cover.open("w", "image/png", "a-cover.png")
    >>> file_handle.content_type
    'image/png'
    >>> file_handle.filename
    'a-cover.png'
    >>> print file_handle.last_modified
    None
    >>> file_handle.write(image)
    >>> file_handle.close()

Once it exists on the server, you can open a hosted file for read
access and read it.

    >>> file_handle = cover.open()
    >>> file_handle.content_type
    'image/png'
    >>> file_handle.filename
    '0'
    >>> last_modified = file_handle.last_modified
    >>> last_modified is None
    False
    >>> len(file_handle.read())
    25

Note that the filename is '0', not 'a-cover.png'. The filename from
the server is implementation-dependent and may not have anything to do
with the filename the client sent. If the server implementation uses
lazr.librarian, it will serve files with the originally uploaded
filename, but the example web service uses its own, simpler
implementation which serves the file's ID as the filename.

Modifying a file will change its 'last_modified' attribute.

    >>> file_handle = cover.open("w", "image/png", "another-cover.png")
    >>> file_handle.write(image)
    >>> file_handle.close()

    >>> file_handle = cover.open()
    >>> file_handle.filename
    '1'
    >>> last_modified_2 = file_handle.last_modified
    >>> last_modified == last_modified_2
    False

Once it exists, a file can be deleted.

    >>> cover.delete()
    >>> cover.open()
    Traceback (most recent call last):
    ...
    HTTPError: HTTP Error 404: Not Found


== Error handling ==

The only access modes supported are 'r' and 'w'.

    >>> cover.open("r+")
    Traceback (most recent call last):
    ...
    ValueError: Invalid mode. Supported modes are: r, w

When opening a file for write access, you must specify the
content_type argument.

    >>> cover.open("w")
    Traceback (most recent call last):
    ...
    ValueError: Files opened for write access must specify content_type.

    >>> cover.open("w", "image/png")
    Traceback (most recent call last):
    ...
    ValueError: Files opened for write access must specify filename.

When opening a file for read access, you must *not* specify the
content_type or filename arguments--they come from the server.

    >>> cover.open("r", "image/png")
    Traceback (most recent call last):
    ...
    ValueError: Files opened for read access can't specify content_type.

    >>> cover.open("r", filename="foo.png")
    Traceback (most recent call last):
    ...
    ValueError: Files opened for read access can't specify filename.


== Caching ==

Hosted file resources implement the normal server-side caching
mechanism.

    >>> file_handle = cover.open("w", "image/png", "image.png")
    >>> file_handle.write(image)
    >>> file_handle.close()

    >>> import httplib2
    >>> httplib2.debuglevel = 1
    >>> service = CookbookWebServiceClient()
    send: ...
    >>> cover = service.cookbooks['Everyday Greens'].cover
    send: ...

The first request for a file retrieves the file from the server.

    >>> len(cover.open().read())
    send: ...
    reply: '...303 See Other...
    reply: '...200 Ok...
    25

The second request retrieves the file from the cache.

    >>> len(cover.open().read())
    send: ...
    reply: '...303 See Other...
    reply: '...304 Not Modified...
    25

Finally, some cleanup code that deletes the cover.

    >>> cover.delete()
    send: 'DELETE...
    reply: '...200...

    >>> httplib2.debuglevel = 0
