Some resources published by Launchpad can have binary
representations. launchpadlib gives access to these resources.

    >>> from launchpadlib.testing.helpers import salgado_with_full_permissions
    >>> launchpad = salgado_with_full_permissions.login()

An example of a hosted binary file is a person's mugshot. The
"salgado" user starts off with no mugshot.

    >>> mugshot = launchpad.me.mugshot
    >>> sorted(dir(mugshot))
    [..., 'open']

    >>> mugshot.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.

    >>> import os
    >>> def load_image(filename):
    ...     image_file = os.path.join(
    ...         os.path.dirname(__file__), 'files', filename)
    ...     return open(image_file).read()
    >>> image = load_image("mugshot.png")
    >>> len(image)
    2260

    >>> file_handle = mugshot.open("w", "image/png", "a-mugshot.png")
    >>> file_handle.content_type
    'image/png'
    >>> file_handle.filename
    'a-mugshot.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 = mugshot.open()
    >>> file_handle.content_type
    'image/png'
    >>> file_handle.filename
    'a-mugshot.png'
    >>> file_handle.last_modified is None
    False
    >>> len(file_handle.read())
    2260

Modifying a file will change its 'last_modified' attribute.

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

    >>> file_handle = mugshot.open()
    >>> file_handle.filename
    'another-mugshot.png'

Once it exists, a file can be deleted.

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


== Error handling ==

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

    >>> mugshot.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.

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

    >>> mugshot.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 argument--it comes from the server.

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

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

The server may set restrictions on what kinds of documents can be
written to a particular file.

    >>> file_handle = mugshot.open("w", "image/png", "nonimage.txt")
    >>> file_handle.content_type
    'image/png'
    >>> file_handle.filename
    'nonimage.txt'
    >>> file_handle.write("Not an image.")
    >>> file_handle.close()
    Traceback (most recent call last):
    ...
    HTTPError: HTTP Error 400: Bad Request


== Caching ==

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

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

    >>> import httplib2
    >>> httplib2.debuglevel = 1
    >>> launchpad = salgado_with_full_permissions.login()
    connect: ...
    >>> mugshot = launchpad.me.mugshot
    send: ...

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

    >>> len(mugshot.open().read())
    send: ...
    reply: 'HTTP/1.1 303 See Other...
    reply: 'HTTP/1.1 200 OK...
    2260

The second request retrieves the file from the cache. After receiving
the 303 request with its Location header, no further HTTP requests are
issued because we know it is fresh due to the Cache-Control: headers
emitted by the Librarian.

    >>> len(mugshot.open().read())
    send: ...
    reply: 'HTTP/1.1 303 See Other...
    header: Location: http://localhost:58000/.../image.png
    header: Vary: Cookie, Authorization, Accept
    header: Content-Type: text/plain
    2260

Finally, some cleanup code that deletes the mugshot.

    >>> mugshot.delete()
    send: 'DELETE...
    reply: 'HTTP/1.1 200...

    >>> httplib2.debuglevel = 0
