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

« back to all changes in this revision

Viewing changes to .pc/uppercase-method.patch/src/lazr/restfulclient/resource.py

  • Committer: Package Import Robot
  • Author(s): Colin Watson
  • Date: 2014-12-11 16:30:02 UTC
  • Revision ID: package-import@ubuntu.com-20141211163002-9esry51vksl1z3hd
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
# Copyright 2008 Canonical Ltd.
 
2
 
 
3
# This file is part of lazr.restfulclient.
 
4
#
 
5
# lazr.restfulclient is free software: you can redistribute it and/or
 
6
# modify it under the terms of the GNU Lesser General Public License
 
7
# as published by the Free Software Foundation, either version 3 of
 
8
# the License, or (at your option) any later version.
 
9
#
 
10
# lazr.restfulclient is distributed in the hope that it will be
 
11
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty
 
12
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
13
# Lesser General Public License for more details.
 
14
#
 
15
# You should have received a copy of the GNU Lesser General Public
 
16
# License along with lazr.restfulclient.  If not, see
 
17
# <http://www.gnu.org/licenses/>.
 
18
 
 
19
"""Common support for web service resources."""
 
20
 
 
21
__metaclass__ = type
 
22
__all__ = [
 
23
    'Collection',
 
24
    'CollectionWithKeyBasedLookup',
 
25
    'Entry',
 
26
    'NamedOperation',
 
27
    'Resource',
 
28
    'ServiceRoot',
 
29
    ]
 
30
 
 
31
 
 
32
import cgi
 
33
from email.message import Message
 
34
import simplejson
 
35
from StringIO import StringIO
 
36
import urllib
 
37
from urlparse import urlparse, urljoin
 
38
 
 
39
from lazr.uri import URI
 
40
from wadllib.application import Resource as WadlResource
 
41
from _browser import Browser, RestfulHttp
 
42
from _json import DatetimeJSONEncoder
 
43
from errors import HTTPError
 
44
 
 
45
from lazr.restfulclient import __version__
 
46
 
 
47
missing = object()
 
48
 
 
49
class HeaderDictionary:
 
50
    """A dictionary that bridges httplib2's and wadllib's expectations.
 
51
 
 
52
    httplib2 expects all header dictionary access to give lowercase
 
53
    header names. wadllib expects to access the header exactly as it's
 
54
    specified in the WADL file, which means the official HTTP header name.
 
55
 
 
56
    This class transforms keys to lowercase before doing a lookup on
 
57
    the underlying dictionary. That way wadllib can pass in the
 
58
    official header name and httplib2 will get the lowercased name.
 
59
    """
 
60
    def __init__(self, wrapped_dictionary):
 
61
        self.wrapped_dictionary = wrapped_dictionary
 
62
 
 
63
    def get(self, key, default=None):
 
64
        """Retrieve a value, converting the key to lowercase."""
 
65
        return self.wrapped_dictionary.get(key.lower())
 
66
 
 
67
    def __getitem__(self, key):
 
68
        """Retrieve a value, converting the key to lowercase."""
 
69
        value = self.get(key, missing)
 
70
        if value is missing:
 
71
            raise KeyError(key)
 
72
        return value
 
73
 
 
74
 
 
75
class RestfulBase:
 
76
    """Base class for classes that know about lazr.restful services."""
 
77
 
 
78
    JSON_MEDIA_TYPE = 'application/json'
 
79
 
 
80
    def _transform_resources_to_links(self, dictionary):
 
81
        new_dictionary = {}
 
82
        for key, value in dictionary.items():
 
83
            if isinstance(value, Resource):
 
84
                value = value.self_link
 
85
            new_dictionary[self._get_external_param_name(key)] = value
 
86
        return new_dictionary
 
87
 
 
88
    def _get_external_param_name(self, param_name):
 
89
        """Turn a lazr.restful name into something to be sent over HTTP.
 
90
 
 
91
        For resources this may involve sticking '_link' or
 
92
        '_collection_link' on the end of the parameter name. For
 
93
        arguments to named operations, the parameter name is returned
 
94
        as is.
 
95
        """
 
96
        return param_name
 
97
 
 
98
 
 
99
class Resource(RestfulBase):
 
100
    """Base class for lazr.restful HTTP resources."""
 
101
 
 
102
    def __init__(self, root, wadl_resource):
 
103
        """Initialize with respect to a wadllib Resource object."""
 
104
        if root is None:
 
105
            # This _is_ the root.
 
106
            root = self
 
107
        # These values need to be put directly into __dict__ to avoid
 
108
        # calling __setattr__, which would cause an infinite recursion.
 
109
        self.__dict__['_root'] = root
 
110
        self.__dict__['_wadl_resource'] = wadl_resource
 
111
 
 
112
    FIND_COLLECTIONS = object()
 
113
    FIND_ENTRIES = object()
 
114
    FIND_ATTRIBUTES = object()
 
115
 
 
116
    @property
 
117
    def lp_collections(self):
 
118
        """Name the collections this resource links to."""
 
119
        return self._get_parameter_names(self.FIND_COLLECTIONS)
 
120
 
 
121
    @property
 
122
    def lp_entries(self):
 
123
        """Name the entries this resource links to."""
 
124
        return self._get_parameter_names(self.FIND_ENTRIES)
 
125
 
 
126
    @property
 
127
    def lp_attributes(self):
 
128
        """Name this resource's scalar attributes."""
 
129
        return self._get_parameter_names(self.FIND_ATTRIBUTES)
 
130
 
 
131
    @property
 
132
    def lp_operations(self):
 
133
        """Name all of this resource's custom operations."""
 
134
        # This library distinguishes between named operations by the
 
135
        # value they give for ws.op, not by their WADL names or IDs.
 
136
        names = []
 
137
        for method in self._wadl_resource.method_iter:
 
138
            name = method.name.lower()
 
139
            if name == 'get':
 
140
                params = method.request.params(['query', 'plain'])
 
141
            elif name == 'post':
 
142
                for media_type in ['application/x-www-form-urlencoded',
 
143
                                   'multipart/form-data']:
 
144
                    definition = method.request.get_representation_definition(
 
145
                        media_type)
 
146
                    if definition is not None:
 
147
                        definition = definition.resolve_definition()
 
148
                        break
 
149
                params = definition.params(self._wadl_resource)
 
150
            for param in params:
 
151
                if param.name == 'ws.op':
 
152
                    names.append(param.fixed_value)
 
153
                    break
 
154
        return names
 
155
 
 
156
    @property
 
157
    def __members__(self):
 
158
        """A hook into dir() that returns web service-derived members."""
 
159
        return self._get_parameter_names(
 
160
            self.FIND_COLLECTIONS, self.FIND_ENTRIES, self.FIND_ATTRIBUTES)
 
161
 
 
162
    __methods__ = lp_operations
 
163
 
 
164
    def _get_parameter_names(self, *kinds):
 
165
        """Retrieve some subset of the resource's parameters."""
 
166
        names = []
 
167
        for name in self._wadl_resource.parameter_names(
 
168
            self.JSON_MEDIA_TYPE):
 
169
            if name.endswith('_collection_link'):
 
170
                if self.FIND_COLLECTIONS in kinds:
 
171
                    names.append(name[:-16])
 
172
            elif (name.endswith('_link')
 
173
                  and name not in ('self_link', 'resource_type_link')):
 
174
                # lazr.restful_obj.self will work, but is never
 
175
                # necessary. lazr.restful_obj.resource_type is also
 
176
                # unneccessary, and won't work anyway because
 
177
                # resource_type_link points to a WADL description,
 
178
                # not a normal lazr.restful resource.
 
179
                if self.FIND_ENTRIES in kinds:
 
180
                    names.append(name[:-5])
 
181
            elif self.FIND_ATTRIBUTES in kinds:
 
182
                names.append(name)
 
183
        return names
 
184
 
 
185
    def lp_has_parameter(self, param_name):
 
186
        """Does this resource have a parameter with the given name?"""
 
187
        return self._get_external_param_name(param_name) is not None
 
188
 
 
189
    def lp_get_parameter(self, param_name):
 
190
        """Get the value of one of the resource's parameters.
 
191
 
 
192
        :return: A scalar value if the parameter is not a link. A new
 
193
                 Resource object, whose resource is bound to a
 
194
                 representation, if the parameter is a link.
 
195
        """
 
196
        self._ensure_representation()
 
197
        for suffix in ['_link', '_collection_link']:
 
198
            param = self._wadl_resource.get_parameter(
 
199
                param_name + suffix, self.JSON_MEDIA_TYPE)
 
200
            if param is not None:
 
201
                if param.get_value() is None:
 
202
                    # This parameter is a link to another object, but
 
203
                    # there's no other object. Return None rather than
 
204
                    # chasing down the nonexistent other object.
 
205
                    return None
 
206
                linked_resource = param.linked_resource
 
207
                return self._create_bound_resource(
 
208
                    self._root, linked_resource, param_name=param.name)
 
209
        param = self._wadl_resource.get_parameter(param_name)
 
210
        if param is None:
 
211
            raise KeyError("No such parameter: %s" % param_name)
 
212
        return param.get_value()
 
213
 
 
214
    def lp_get_named_operation(self, operation_name):
 
215
        """Get a custom operation with the given name.
 
216
 
 
217
        :return: A NamedOperation instance that can be called with
 
218
                 appropriate arguments to invoke the operation.
 
219
        """
 
220
        params = { 'ws.op' : operation_name }
 
221
        method = self._wadl_resource.get_method('get', query_params=params)
 
222
        if method is None:
 
223
            method = self._wadl_resource.get_method(
 
224
                'post', representation_params=params)
 
225
        if method is None:
 
226
            raise KeyError("No operation with name: %s" % operation_name)
 
227
        return NamedOperation(self._root, self, method)
 
228
 
 
229
    @classmethod
 
230
    def _create_bound_resource(
 
231
        cls, root, resource, representation=None,
 
232
        representation_media_type='application/json',
 
233
        representation_needs_processing=True, representation_definition=None,
 
234
        param_name=None):
 
235
        """Create a lazr.restful Resource subclass from a wadllib Resource.
 
236
 
 
237
        :param resource: The wadllib Resource to wrap.
 
238
        :param representation: A previously fetched representation of
 
239
            this resource, to be reused. If not provided, this method
 
240
            will act just like the Resource constructor.
 
241
        :param representation_media_type: The media type of any previously
 
242
            fetched representation.
 
243
        :param representation_needs_processing: Set to False if the
 
244
            'representation' parameter should be used as
 
245
            is.
 
246
        :param representation_definition: A wadllib
 
247
            RepresentationDefinition object describing the structure
 
248
            of this representation. Used in cases when the representation
 
249
            isn't the result of sending a standard GET to the resource.
 
250
        :param param_name: The name of the link that was followed to get
 
251
            to this resource.
 
252
        :return: An instance of the appropriate lazr.restful Resource
 
253
            subclass.
 
254
        """
 
255
        # We happen to know that all lazr.restful resource types are
 
256
        # defined in a single document. Turn the resource's type_url
 
257
        # into an anchor into that document: this is its resource
 
258
        # type. Then look up a client-side class that corresponds to
 
259
        # the resource type.
 
260
        type_url = resource.type_url
 
261
        resource_type = urlparse(type_url)[-1]
 
262
        default = Entry
 
263
        if (type_url.endswith('-page')
 
264
            or (param_name is not None
 
265
                and param_name.endswith('_collection_link'))):
 
266
            default = Collection
 
267
        r_class = root.RESOURCE_TYPE_CLASSES.get(resource_type, default)
 
268
        if representation is not None:
 
269
            # We've been given a representation. Bind the resource
 
270
            # immediately.
 
271
            resource = resource.bind(
 
272
                representation, representation_media_type,
 
273
                representation_needs_processing,
 
274
                representation_definition=representation_definition)
 
275
        else:
 
276
            # We'll fetch a representation and bind the resource when
 
277
            # necessary.
 
278
            pass
 
279
        return r_class(root, resource)
 
280
 
 
281
    def lp_refresh(self, new_url=None, etag=None):
 
282
        """Update this resource's representation."""
 
283
        if new_url is not None:
 
284
            self._wadl_resource._url = new_url
 
285
        headers = {}
 
286
        if etag is not None:
 
287
            headers['If-None-Match'] = etag
 
288
        representation = self._root._browser.get(
 
289
            self._wadl_resource, headers=headers)
 
290
        if representation == self._root._browser.NOT_MODIFIED:
 
291
            # The entry wasn't modified. No need to do anything.
 
292
            return
 
293
        # __setattr__ assumes we're setting an attribute of the resource,
 
294
        # so we manipulate __dict__ directly.
 
295
        self.__dict__['_wadl_resource'] = self._wadl_resource.bind(
 
296
            representation, self.JSON_MEDIA_TYPE)
 
297
 
 
298
    def __getattr__(self, attr):
 
299
        """Try to retrive a named operation or parameter of the given name."""
 
300
        try:
 
301
            return self.lp_get_parameter(attr)
 
302
        except KeyError:
 
303
            pass
 
304
        try:
 
305
            return self.lp_get_named_operation(attr)
 
306
        except KeyError:
 
307
            raise AttributeError("'%s' object has no attribute '%s'"
 
308
                                 % (self.__class__.__name__, attr))
 
309
 
 
310
    def lp_values_for(self, param_name):
 
311
        """Find the set of possible values for a parameter."""
 
312
        parameter = self._wadl_resource.get_parameter(
 
313
            param_name, self.JSON_MEDIA_TYPE)
 
314
        options = parameter.options
 
315
        if len(options) > 0:
 
316
            return [option.value for option in options]
 
317
        return None
 
318
 
 
319
    def _get_external_param_name(self, param_name):
 
320
        """What's this parameter's name in the underlying representation?"""
 
321
        for suffix in ['_link', '_collection_link', '']:
 
322
            name = param_name + suffix
 
323
            if self._wadl_resource.get_parameter(name):
 
324
                return name
 
325
        return None
 
326
 
 
327
    def _ensure_representation(self):
 
328
        """Make sure this resource has a representation fetched."""
 
329
        if self._wadl_resource.representation is None:
 
330
            # Get a representation of the linked resource.
 
331
            representation = self._root._browser.get(self._wadl_resource)
 
332
            representation = simplejson.loads(representation)
 
333
            type_link = representation['resource_type_link']
 
334
            if (type_link is not None
 
335
                and type_link != self._wadl_resource.type_url):
 
336
                # In rare cases, the resource type served by the
 
337
                # server conflicts with the type the client thought
 
338
                # this resource had. When this happens, the server
 
339
                # value takes precedence.
 
340
                resource_type = self._root._wadl.get_resource_type(type_link)
 
341
                self._wadl_resource.tag = resource_type.tag
 
342
            self.__dict__['_wadl_resource'] = self._wadl_resource.bind(
 
343
                representation, self.JSON_MEDIA_TYPE,
 
344
                representation_needs_processing=False)
 
345
 
 
346
    def __ne__(self, other):
 
347
        """Inequality operator."""
 
348
        return not self == other
 
349
 
 
350
 
 
351
class HostedFile(Resource):
 
352
    """A resource representing a file managed by a lazr.restful service."""
 
353
 
 
354
    def open(self, mode='r', content_type=None, filename=None):
 
355
        """Open the file on the server for read or write access."""
 
356
        if mode in ('r', 'w'):
 
357
            return HostedFileBuffer(self, mode, content_type, filename)
 
358
        else:
 
359
            raise ValueError("Invalid mode. Supported modes are: r, w")
 
360
 
 
361
    def delete(self):
 
362
        """Delete the file from the server."""
 
363
        self._root._browser.delete(self._wadl_resource.url)
 
364
 
 
365
    def _get_parameter_names(self, *kinds):
 
366
        """HostedFile objects define no web service parameters."""
 
367
        return []
 
368
 
 
369
    def __eq__(self, other):
 
370
        """Equality comparison.
 
371
 
 
372
        Two hosted files are the same if they have the same URL.
 
373
 
 
374
        There is no need to check the contents because the only way to
 
375
        retrieve or modify the hosted file contents is to open a
 
376
        filehandle, which goes direct to the server.
 
377
        """
 
378
        return (other is not None and
 
379
                self._wadl_resource.url == other._wadl_resource.url)
 
380
 
 
381
 
 
382
class ServiceRoot(Resource):
 
383
    """Entry point to the service. Subclass this for a service-specific client.
 
384
 
 
385
    :ivar credentials: The credentials instance used to access Launchpad.
 
386
    """
 
387
 
 
388
    # Custom subclasses of Entry or Collection to use when
 
389
    # instantiating resources of a certain WADL type.
 
390
    RESOURCE_TYPE_CLASSES = {'HostedFile': HostedFile}
 
391
 
 
392
    def __init__(self, authorizer, service_root, cache=None,
 
393
                 timeout=None, proxy_info=None, version=None,
 
394
                 base_client_name=''):
 
395
        """Root access to a lazr.restful API.
 
396
 
 
397
        :param credentials: The credentials used to access the service.
 
398
        :param service_root: The URL to the root of the web service.
 
399
        :type service_root: string
 
400
        """
 
401
        if version is not None:
 
402
            if service_root[-1] != '/':
 
403
                service_root += '/'
 
404
            service_root += str(version)
 
405
            if service_root[-1] != '/':
 
406
                service_root += '/'
 
407
        self._root_uri = URI(service_root)
 
408
 
 
409
        # Set up data necessary to calculate the User-Agent header.
 
410
        self._base_client_name = base_client_name
 
411
 
 
412
        # Get the WADL definition.
 
413
        self.credentials = authorizer
 
414
        self._browser = Browser(
 
415
            self, authorizer, cache, timeout, proxy_info, self._user_agent)
 
416
        self._wadl = self._browser.get_wadl_application(self._root_uri)
 
417
 
 
418
        # Get the root resource.
 
419
        root_resource = self._wadl.get_resource_by_path('')
 
420
        bound_root = root_resource.bind(
 
421
            self._browser.get(root_resource), 'application/json')
 
422
        super(ServiceRoot, self).__init__(None, bound_root)
 
423
 
 
424
    @property
 
425
    def _user_agent(self):
 
426
        """The value for the User-Agent header.
 
427
 
 
428
        This will be something like:
 
429
        launchpadlib 1.6.1, lazr.restfulclient 1.0.0; oauth_consumer=apport
 
430
 
 
431
        That is, a string describing lazr.restfulclient and an
 
432
        optional custom client built on top, and parameters
 
433
        containing any authorization-specific information that
 
434
        identifies the user agent (such as the OAuth consumer key).
 
435
        """
 
436
        base_portion = "lazr.restfulclient %s" % __version__
 
437
        if self._base_client_name != '':
 
438
            base_portion = self._base_client_name + ' (' + base_portion + ')'
 
439
 
 
440
        message = Message()
 
441
        message['User-Agent'] = base_portion
 
442
        if self.credentials is not None:
 
443
            for key, value in self.credentials.user_agent_params.items():
 
444
                message.set_param(key, value, 'User-Agent')
 
445
        return message['User-Agent']
 
446
 
 
447
    def httpFactory(self, authorizer, cache, timeout, proxy_info):
 
448
        return RestfulHttp(authorizer, cache, timeout, proxy_info)
 
449
 
 
450
    def load(self, url):
 
451
        """Load a resource given its URL."""
 
452
        parsed = urlparse(url)
 
453
        if parsed.scheme == '':
 
454
            # This is a relative URL. Make it absolute by joining
 
455
            # it with the service root resource.
 
456
            if url[:1] == '/':
 
457
                url = url[1:]
 
458
            url = self._root_uri.append(url)
 
459
        document = self._browser.get(url)
 
460
        try:
 
461
            representation = simplejson.loads(unicode(document))
 
462
        except ValueError:
 
463
            raise ValueError("%s doesn't serve a JSON document." % url)
 
464
        type_link = representation.get("resource_type_link")
 
465
        if type_link is None:
 
466
            raise ValueError("Couldn't determine the resource type of %s."
 
467
                             % url)
 
468
        resource_type = self._root._wadl.get_resource_type(type_link)
 
469
        wadl_resource = WadlResource(self._root._wadl, url, resource_type.tag)
 
470
        return self._create_bound_resource(
 
471
            self._root, wadl_resource, representation, 'application/json',
 
472
            representation_needs_processing=False)
 
473
 
 
474
 
 
475
class NamedOperation(RestfulBase):
 
476
    """A class for a named operation to be invoked with GET or POST."""
 
477
 
 
478
    def __init__(self, root, resource, wadl_method):
 
479
        """Initialize with respect to a WADL Method object"""
 
480
        self.root = root
 
481
        self.resource = resource
 
482
        self.wadl_method = wadl_method
 
483
 
 
484
    def __call__(self, *args, **kwargs):
 
485
        """Invoke the method and process the result."""
 
486
        if len(args) > 0:
 
487
            raise TypeError('Method must be called with keyword args.')
 
488
        http_method = self.wadl_method.name
 
489
        args = self._transform_resources_to_links(kwargs)
 
490
        request = self.wadl_method.request
 
491
 
 
492
        if http_method in ('get', 'head', 'delete'):
 
493
            params = request.query_params
 
494
        else:
 
495
            definition = request.get_representation_definition(
 
496
                'multipart/form-data')
 
497
            if definition is None:
 
498
                definition = request.get_representation_definition(
 
499
                    'application/x-www-form-urlencoded')
 
500
            assert definition is not None, (
 
501
                "A POST named operation must define a multipart or "
 
502
                "form-urlencoded request representation."
 
503
                )
 
504
            params = definition.params(self.resource._wadl_resource)
 
505
        send_as_is_params = set([param.name for param in params
 
506
                                 if param.type == 'binary'
 
507
                                 or len(param.options) > 0])
 
508
        for key, value in args.items():
 
509
            # Certain parameter values should not be JSON-encoded:
 
510
            # binary parameters (because they can't be JSON-encoded)
 
511
            # and option values (because JSON-encoding them will screw
 
512
            # up wadllib's parameter validation). The option value thing
 
513
            # is a little hacky, but it's the best solution for now.
 
514
            if not key in send_as_is_params:
 
515
                args[key] = simplejson.dumps(value, cls=DatetimeJSONEncoder)
 
516
        if http_method in ('get', 'head', 'delete'):
 
517
            url = self.wadl_method.build_request_url(**args)
 
518
            in_representation = ''
 
519
            extra_headers = {}
 
520
        else:
 
521
            url = self.wadl_method.build_request_url()
 
522
            (media_type,
 
523
             in_representation) = self.wadl_method.build_representation(
 
524
                **args)
 
525
            extra_headers = { 'Content-type' : media_type }
 
526
        response, content = self.root._browser._request(
 
527
            url, in_representation, http_method, extra_headers=extra_headers)
 
528
 
 
529
        if response.status == 201:
 
530
            return self._handle_201_response(url, response, content)
 
531
        else:
 
532
            if http_method == 'post':
 
533
                # The method call probably modified this resource in
 
534
                # an unknown way. Refresh its representation.
 
535
                self.resource.lp_refresh()
 
536
            return self._handle_200_response(url, response, content)
 
537
 
 
538
    def _handle_201_response(self, url, response, content):
 
539
        """Handle the creation of a new resource by fetching it."""
 
540
        wadl_response = self.wadl_method.response.bind(
 
541
            HeaderDictionary(response))
 
542
        wadl_parameter = wadl_response.get_parameter('Location')
 
543
        wadl_resource = wadl_parameter.linked_resource
 
544
            # Fetch a representation of the new resource.
 
545
        response, content = self.root._browser._request(
 
546
            wadl_resource.url)
 
547
        # Return an instance of the appropriate lazr.restful
 
548
        # Resource subclass.
 
549
        return Resource._create_bound_resource(
 
550
            self.root, wadl_resource, content, response['content-type'])
 
551
 
 
552
    def _handle_200_response(self, url, response, content):
 
553
        """Process the return value of an operation."""
 
554
        content_type = response['content-type']
 
555
        # Process the returned content, assuming we know how.
 
556
        response_definition = self.wadl_method.response
 
557
        representation_definition = (
 
558
            response_definition.get_representation_definition(
 
559
                content_type))
 
560
 
 
561
        if representation_definition is None:
 
562
            # The operation returned a document with nothing
 
563
            # special about it.
 
564
            if content_type == self.JSON_MEDIA_TYPE:
 
565
                return simplejson.loads(unicode(content))
 
566
            # We don't know how to process the content.
 
567
            return content
 
568
 
 
569
        # The operation returned a representation of some
 
570
        # resource. Instantiate a Resource object for it.
 
571
        document = simplejson.loads(unicode(content))
 
572
        if document is None:
 
573
            # The operation returned a null value.
 
574
            return document
 
575
        if "self_link" in document and "resource_type_link" in document:
 
576
            # The operation returned an entry. Use the self_link and
 
577
            # resource_type_link of the entry representation to build
 
578
            # a Resource object of the appropriate type. That way this
 
579
            # object will support all of the right named operations.
 
580
            url = document["self_link"]
 
581
            resource_type = self.root._wadl.get_resource_type(
 
582
                document["resource_type_link"])
 
583
            wadl_resource = WadlResource(self.root._wadl, url,
 
584
                                         resource_type.tag)
 
585
        else:
 
586
            # The operation returned a collection. It's probably an ad
 
587
            # hoc collection that doesn't correspond to any resource
 
588
            # type.  Instantiate it as a resource backed by the
 
589
            # representation type defined in the return value, instead
 
590
            # of a resource type tag.
 
591
            representation_definition = (
 
592
                representation_definition.resolve_definition())
 
593
            wadl_resource = WadlResource(
 
594
                self.root._wadl, url, representation_definition.tag)
 
595
 
 
596
        return Resource._create_bound_resource(
 
597
            self.root, wadl_resource, document, content_type,
 
598
            representation_needs_processing=False,
 
599
            representation_definition=representation_definition)
 
600
 
 
601
    def _get_external_param_name(self, param_name):
 
602
        """Named operation parameter names are sent as is."""
 
603
        return param_name
 
604
 
 
605
 
 
606
class Entry(Resource):
 
607
    """A class for an entry-type resource that can be updated with PATCH."""
 
608
 
 
609
    def __init__(self, root, wadl_resource):
 
610
        super(Entry, self).__init__(root, wadl_resource)
 
611
        # Initialize this here in a semi-magical way so as to stop a
 
612
        # particular infinite loop that would follow.  Setting
 
613
        # self._dirty_attributes would call __setattr__(), which would
 
614
        # turn around immediately and get self._dirty_attributes.  If
 
615
        # this latter was not in the instance dictionary, that would
 
616
        # end up calling __getattr__(), which would again reference
 
617
        # self._dirty_attributes.  This is where the infloop would
 
618
        # occur.  Poking this directly into self.__dict__ means that
 
619
        # the check for self._dirty_attributes won't call __getattr__(),
 
620
        # breaking the cycle.
 
621
        self.__dict__['_dirty_attributes'] = {}
 
622
        super(Entry, self).__init__(root, wadl_resource)
 
623
 
 
624
    def __repr__(self):
 
625
        """Return the WADL resource type and the URL to the resource."""
 
626
        return '<%s at %s>' % (
 
627
            URI(self.resource_type_link).fragment, self.self_link)
 
628
 
 
629
    def __str__(self):
 
630
        """Return the URL to the resource."""
 
631
        return self.self_link
 
632
 
 
633
    def __getattr__(self, name):
 
634
        """Try to retrive a parameter of the given name."""
 
635
        if name != '_dirty_attributes':
 
636
            if name in self._dirty_attributes:
 
637
                return self._dirty_attributes[name]
 
638
        return super(Entry, self).__getattr__(name)
 
639
 
 
640
    def __setattr__(self, name, value):
 
641
        """Set the parameter of the given name."""
 
642
        if not self.lp_has_parameter(name):
 
643
            raise AttributeError("'%s' object has no attribute '%s'" %
 
644
                                 (self.__class__.__name__, name))
 
645
        self._dirty_attributes[name] = value
 
646
 
 
647
    def __eq__(self, other):
 
648
        """Equality operator.
 
649
 
 
650
        Two entries are the same if their self_link and http_etag
 
651
        attributes are the same, and if their dirty attribute dicts
 
652
        contain the same values.
 
653
        """
 
654
        return (
 
655
            other is not None and
 
656
            self.self_link == other.self_link and
 
657
            self.http_etag == other.http_etag and
 
658
            self._dirty_attributes == other._dirty_attributes)
 
659
 
 
660
    def lp_refresh(self, new_url=None):
 
661
        """Update this resource's representation."""
 
662
        etag = getattr(self, 'http_etag', None)
 
663
        super(Entry, self).lp_refresh(new_url, etag)
 
664
        self._dirty_attributes.clear()
 
665
 
 
666
    def lp_save(self):
 
667
        """Save changes to the entry."""
 
668
        representation = self._transform_resources_to_links(
 
669
            self._dirty_attributes)
 
670
 
 
671
        # If the entry contains an ETag, set the If-Match header
 
672
        # to that value.
 
673
        headers = {}
 
674
        etag = getattr(self, 'http_etag', None)
 
675
        if etag is not None:
 
676
            headers['If-Match'] = etag
 
677
 
 
678
        # PATCH the new representation to the 'self' link.  It's possible that
 
679
        # this will cause the object to be permanently moved.  Catch that
 
680
        # exception and refresh our representation.
 
681
        try:
 
682
            response, content = self._root._browser.patch(
 
683
                URI(self.self_link), representation, headers)
 
684
        except HTTPError, error:
 
685
            if error.response.status == 301:
 
686
                response = error.response
 
687
                self.lp_refresh(error.response['location'])
 
688
            else:
 
689
                raise
 
690
        self._dirty_attributes.clear()
 
691
 
 
692
        content_type = response['content-type']
 
693
        if response.status == 209 and content_type == self.JSON_MEDIA_TYPE:
 
694
            # The server sent back a new representation of the object.
 
695
            # Use it in preference to the existing representation.
 
696
            new_representation = simplejson.loads(unicode(content))
 
697
            self._wadl_resource.representation = new_representation
 
698
            self._wadl_resource.media_type = content_type
 
699
 
 
700
 
 
701
class Collection(Resource):
 
702
    """A collection-type resource that supports pagination."""
 
703
 
 
704
    def __init__(self, root, wadl_resource):
 
705
        """Create a collection object."""
 
706
        super(Collection, self).__init__(root, wadl_resource)
 
707
 
 
708
    def __len__(self):
 
709
        """The number of items in the collection.
 
710
 
 
711
        :return: length of the collection
 
712
        :rtype: int
 
713
        """
 
714
        try:
 
715
            return int(self.total_size)
 
716
        except AttributeError:
 
717
            raise TypeError('collection size is not available')
 
718
 
 
719
    def __iter__(self):
 
720
        """Iterate over the items in the collection.
 
721
 
 
722
        :return: iterator
 
723
        :rtype: sequence of `Entry`
 
724
        """
 
725
        self._ensure_representation()
 
726
        current_page = self._wadl_resource.representation
 
727
        while True:
 
728
            for resource in self._convert_dicts_to_entries(
 
729
                current_page.get('entries', {})):
 
730
                yield resource
 
731
            next_link = current_page.get('next_collection_link')
 
732
            if next_link is None:
 
733
                break
 
734
            current_page = simplejson.loads(
 
735
                unicode(self._root._browser.get(URI(next_link))))
 
736
 
 
737
    def __getitem__(self, key):
 
738
        """Look up a slice, or a subordinate resource by index.
 
739
 
 
740
        To discourage situations where a lazr.restful client fetches
 
741
        all of an enormous list, all collection slices must have a
 
742
        definitive end point. For performance reasons, all collection
 
743
        slices must be indexed from the start of the list rather than
 
744
        the end.
 
745
        """
 
746
        if isinstance(key, slice):
 
747
            return self._get_slice(key)
 
748
        else:
 
749
            # Look up a single item by its position in the list.
 
750
            found_slice = self._get_slice(slice(key, key+1))
 
751
            if len(found_slice) != 1:
 
752
                raise IndexError("list index out of range")
 
753
            return found_slice[0]
 
754
 
 
755
    def _get_slice(self, slice):
 
756
        """Retrieve a slice of a collection."""
 
757
        start = slice.start or 0
 
758
        stop = slice.stop
 
759
 
 
760
        if start < 0:
 
761
            raise ValueError("Collection slices must have a nonnegative "
 
762
                             "start point.")
 
763
        if stop < 0:
 
764
            raise ValueError("Collection slices must have a definite, "
 
765
                             "nonnegative end point.")
 
766
 
 
767
        existing_representation = self._wadl_resource.representation
 
768
        if (existing_representation is not None
 
769
            and start < len(existing_representation['entries'])):
 
770
            # An optimization: the first page of entries has already
 
771
            # been loaded. This can happen if this collection is the
 
772
            # return value of a named operation, or if the client did
 
773
            # something like check the length of the collection.
 
774
            #
 
775
            # Either way, we've already made an HTTP request and
 
776
            # gotten some entries back. The client has requested a
 
777
            # slice that includes some of the entries we already have.
 
778
            # In the best case, we can fulfil the slice immediately,
 
779
            # without making another HTTP request.
 
780
            #
 
781
            # Even if we can't fulfil the entire slice, we can get one
 
782
            # or more objects from the first page and then have fewer
 
783
            # objects to retrieve from the server later. This saves us
 
784
            # time and bandwidth, and it might let us save a whole
 
785
            # HTTP request.
 
786
            entry_page = existing_representation['entries']
 
787
 
 
788
            first_page_size = len(entry_page)
 
789
            entry_dicts = entry_page[start:stop]
 
790
            page_url = existing_representation.get('next_collection_link')
 
791
        else:
 
792
            # No part of this collection has been loaded yet, or the
 
793
            # slice starts beyond the part that has been loaded. We'll
 
794
            # use our secret knowledge of lazr.restful to set a value for
 
795
            # the ws.start variable. That way we start reading entries
 
796
            # from the first one we want.
 
797
            first_page_size = None
 
798
            entry_dicts = []
 
799
            page_url = self._with_url_query_variable_set(
 
800
                self._wadl_resource.url, 'ws.start', start)
 
801
 
 
802
        desired_size = stop-start
 
803
        more_needed = desired_size - len(entry_dicts)
 
804
 
 
805
        # Iterate over pages until we have the correct number of entries.
 
806
        while more_needed > 0 and page_url is not None:
 
807
            representation = simplejson.loads(
 
808
                unicode(self._root._browser.get(page_url)))
 
809
            current_page_entries = representation['entries']
 
810
            entry_dicts += current_page_entries[:more_needed]
 
811
            more_needed = desired_size - len(entry_dicts)
 
812
 
 
813
            page_url = representation.get('next_collection_link')
 
814
            if page_url is None:
 
815
                # We've gotten the entire collection; there are no
 
816
                # more entries.
 
817
                break
 
818
            if first_page_size is None:
 
819
                first_page_size = len(current_page_entries)
 
820
            if more_needed > 0 and more_needed < first_page_size:
 
821
                # An optimization: it's likely that we need less than
 
822
                # a full page of entries, because the number we need
 
823
                # is less than the size of the first page we got.
 
824
                # Instead of requesting a full-sized page, we'll
 
825
                # request only the number of entries we think we'll
 
826
                # need. If we're wrong, there's no problem; we'll just
 
827
                # keep looping.
 
828
                page_url = self._with_url_query_variable_set(
 
829
                    page_url, 'ws.size', more_needed)
 
830
 
 
831
        if slice.step is not None:
 
832
            entry_dicts = entry_dicts[::slice.step]
 
833
 
 
834
        # Convert entry_dicts into a list of Entry objects.
 
835
        return [resource for resource
 
836
                in self._convert_dicts_to_entries(entry_dicts)]
 
837
 
 
838
    def _convert_dicts_to_entries(self, entries):
 
839
        """Convert dictionaries describing entries to Entry objects.
 
840
 
 
841
        The dictionaries come from the 'entries' field of the JSON
 
842
        dictionary you get when you GET a page of a collection. Each
 
843
        dictionary is the same as you'd get if you sent a GET request
 
844
        to the corresponding entry resource. So each of these
 
845
        dictionaries can be treated as a preprocessed representation
 
846
        of an entry resource, and turned into an Entry instance.
 
847
 
 
848
        :yield: A sequence of Entry instances.
 
849
        """
 
850
        for entry_dict in entries:
 
851
            resource_url = entry_dict['self_link']
 
852
            resource_type_link = entry_dict['resource_type_link']
 
853
            wadl_application = self._wadl_resource.application
 
854
            resource_type = wadl_application.get_resource_type(
 
855
                resource_type_link)
 
856
            resource = WadlResource(
 
857
                self._wadl_resource.application, resource_url,
 
858
                resource_type.tag)
 
859
            yield Resource._create_bound_resource(
 
860
                self._root, resource, entry_dict, self.JSON_MEDIA_TYPE,
 
861
                False)
 
862
 
 
863
    def _with_url_query_variable_set(self, url, variable, new_value):
 
864
        """A helper method to set a query variable in a URL."""
 
865
        uri = URI(url)
 
866
        if uri.query is None:
 
867
            params = {}
 
868
        else:
 
869
            params = cgi.parse_qs(uri.query)
 
870
        params[variable] = str(new_value)
 
871
        uri.query = urllib.urlencode(params, True)
 
872
        return str(uri)
 
873
 
 
874
 
 
875
class CollectionWithKeyBasedLookup(Collection):
 
876
    """A collection-type resource that supports key-based lookup.
 
877
 
 
878
    This collection can be sliced, but any single index passed into
 
879
    __getitem__ will be treated as a custom lookup key.
 
880
    """
 
881
 
 
882
    def __getitem__(self, key):
 
883
        """Look up a slice, or a subordinate resource by unique ID."""
 
884
        if isinstance(key, slice):
 
885
            return super(CollectionWithKeyBasedLookup, self).__getitem__(key)
 
886
        try:
 
887
            url = self._get_url_from_id(key)
 
888
        except NotImplementedError:
 
889
            raise TypeError("unsubscriptable object")
 
890
        if url is None:
 
891
            raise KeyError(key)
 
892
 
 
893
        if self.collection_of is not None:
 
894
            # We know what kind of resource is at the other end of the
 
895
            # URL. There's no need to actually fetch that URL until
 
896
            # the user demands it. If the user is invoking a named
 
897
            # operation on this object rather than fetching its data,
 
898
            # this will save us one round trip.
 
899
            representation = None
 
900
            resource_type_link = urljoin(
 
901
                self._root._wadl.markup_url, '#' + self.collection_of)
 
902
        else:
 
903
            # We don't know what kind of resource this is. Either the
 
904
            # subclass wasn't programmed with this knowledge, or
 
905
            # there's simply no way to tell without going to the
 
906
            # server, because the collection contains more than one
 
907
            # kind of resource. The only way to know for sure is to
 
908
            # retrieve a representation of the resource and see how
 
909
            # the resource describes itself.
 
910
            try:
 
911
                representation = simplejson.loads(
 
912
                    unicode(self._root._browser.get(url)))
 
913
            except HTTPError, error:
 
914
                # There's no resource corresponding to the given ID.
 
915
                if error.response.status == 404:
 
916
                    raise KeyError(key)
 
917
                raise
 
918
            # We know that every lazr.restful resource has a
 
919
            # 'resource_type_link' in its representation.
 
920
            resource_type_link = representation['resource_type_link']
 
921
 
 
922
        resource = WadlResource(self._root._wadl, url, resource_type_link)
 
923
        return self._create_bound_resource(
 
924
            self._root, resource, representation=representation,
 
925
            representation_needs_processing=False)
 
926
 
 
927
    # If provided, this should be a string designating the ID of a
 
928
    # resource_type from a specific service's WADL file.
 
929
    collection_of = None
 
930
 
 
931
    def _get_url_from_id(self, key):
 
932
        """Transform the unique ID of an object into its URL."""
 
933
        raise NotImplementedError()
 
934
 
 
935
 
 
936
class HostedFileBuffer(StringIO):
 
937
    """The contents of a file hosted by a lazr.restful service."""
 
938
    def __init__(self, hosted_file, mode, content_type=None, filename=None):
 
939
        self.url = hosted_file._wadl_resource.url
 
940
        if mode == 'r':
 
941
            if content_type is not None:
 
942
                raise ValueError("Files opened for read access can't "
 
943
                                 "specify content_type.")
 
944
            if filename is not None:
 
945
                raise ValueError("Files opened for read access can't "
 
946
                                 "specify filename.")
 
947
            response, value = hosted_file._root._browser.get(
 
948
                self.url, return_response=True)
 
949
            content_type = response['content-type']
 
950
            last_modified = response.get('last-modified')
 
951
 
 
952
            # The Content-Location header contains the URL of the file
 
953
            # hosted by the web service. We happen to know that the
 
954
            # final component of the URL is the name of the uploaded
 
955
            # file.
 
956
            content_location = response['content-location']
 
957
            path = urlparse(content_location)[2]
 
958
            filename = urllib.unquote(path.split("/")[-1])
 
959
        elif mode == 'w':
 
960
            value = ''
 
961
            if content_type is None:
 
962
                raise ValueError("Files opened for write access must "
 
963
                                 "specify content_type.")
 
964
            if filename is None:
 
965
                raise ValueError("Files opened for write access must "
 
966
                                 "specify filename.")
 
967
            last_modified = None
 
968
        else:
 
969
            raise ValueError("Invalid mode. Supported modes are: r, w")
 
970
 
 
971
        self.hosted_file = hosted_file
 
972
        self.mode = mode
 
973
        self.content_type = content_type
 
974
        self.filename = filename
 
975
        self.last_modified = last_modified
 
976
        StringIO.__init__(self, value)
 
977
 
 
978
    def close(self):
 
979
        if self.mode == 'w':
 
980
            disposition = 'attachment; filename="%s"' % self.filename
 
981
            self.hosted_file._root._browser.put(
 
982
                self.url, self.getvalue(),
 
983
                self.content_type, {'Content-Disposition' : disposition})
 
984
        StringIO.close(self)