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.

    >>> len(mugshot.open().read())
    send: ...
    reply: 'HTTP/1.1 303 See Other...
    reply: 'HTTP/1.1 304 Not Modified...
    2260

Finally, some cleanup code that deletes the mugshot.

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

    >>> httplib2.debuglevel = 0
