Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(465)

Side by Side Diff: gslib/third_party/protorpc/util.py

Issue 698893003: Update checked in version of gsutil to version 4.6 (Closed) Base URL: http://dart.googlecode.com/svn/third_party/gsutil/
Patch Set: Created 6 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « gslib/third_party/protorpc/protojson.py ('k') | gslib/third_party/storage_apitools/__init__.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Property Changes:
Added: svn:eol-style
+ LF
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 #
3 # Copyright 2010 Google Inc.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 #
17
18 """Common utility library."""
19
20 from __future__ import with_statement
21
22 __author__ = ['rafek@google.com (Rafe Kaplan)',
23 'guido@google.com (Guido van Rossum)',
24 ]
25
26 import cgi
27 import datetime
28 import inspect
29 import os
30 import re
31 import sys
32
33 __all__ = ['AcceptItem',
34 'AcceptError',
35 'Error',
36 'choose_content_type',
37 'decode_datetime',
38 'get_package_for_module',
39 'pad_string',
40 'parse_accept_header',
41 'positional',
42 'PROTORPC_PROJECT_URL',
43 'TimeZoneOffset',
44 ]
45
46
47 class Error(Exception):
48 """Base class for protorpc exceptions."""
49
50
51 class AcceptError(Error):
52 """Raised when there is an error parsing the accept header."""
53
54
55 PROTORPC_PROJECT_URL = 'http://code.google.com/p/google-protorpc'
56
57 _TIME_ZONE_RE_STRING = r"""
58 # Examples:
59 # +01:00
60 # -05:30
61 # Z12:00
62 ((?P<z>Z) | (?P<sign>[-+])
63 (?P<hours>\d\d) :
64 (?P<minutes>\d\d))$
65 """
66 _TIME_ZONE_RE = re.compile(_TIME_ZONE_RE_STRING, re.IGNORECASE | re.VERBOSE)
67
68
69 def pad_string(string):
70 """Pad a string for safe HTTP error responses.
71
72 Prevents Internet Explorer from displaying their own error messages
73 when sent as the content of error responses.
74
75 Args:
76 string: A string.
77
78 Returns:
79 Formatted string left justified within a 512 byte field.
80 """
81 return string.ljust(512)
82
83
84 def positional(max_positional_args):
85 """A decorator to declare that only the first N arguments may be positional.
86
87 This decorator makes it easy to support Python 3 style keyword-only
88 parameters. For example, in Python 3 it is possible to write:
89
90 def fn(pos1, *, kwonly1=None, kwonly1=None):
91 ...
92
93 All named parameters after * must be a keyword:
94
95 fn(10, 'kw1', 'kw2') # Raises exception.
96 fn(10, kwonly1='kw1') # Ok.
97
98 Example:
99 To define a function like above, do:
100
101 @positional(1)
102 def fn(pos1, kwonly1=None, kwonly2=None):
103 ...
104
105 If no default value is provided to a keyword argument, it becomes a required
106 keyword argument:
107
108 @positional(0)
109 def fn(required_kw):
110 ...
111
112 This must be called with the keyword parameter:
113
114 fn() # Raises exception.
115 fn(10) # Raises exception.
116 fn(required_kw=10) # Ok.
117
118 When defining instance or class methods always remember to account for
119 'self' and 'cls':
120
121 class MyClass(object):
122
123 @positional(2)
124 def my_method(self, pos1, kwonly1=None):
125 ...
126
127 @classmethod
128 @positional(2)
129 def my_method(cls, pos1, kwonly1=None):
130 ...
131
132 One can omit the argument to 'positional' altogether, and then no
133 arguments with default values may be passed positionally. This
134 would be equivalent to placing a '*' before the first argument
135 with a default value in Python 3. If there are no arguments with
136 default values, and no argument is given to 'positional', an error
137 is raised.
138
139 @positional
140 def fn(arg1, arg2, required_kw1=None, required_kw2=0):
141 ...
142
143 fn(1, 3, 5) # Raises exception.
144 fn(1, 3) # Ok.
145 fn(1, 3, required_kw1=5) # Ok.
146
147 Args:
148 max_positional_arguments: Maximum number of positional arguments. All
149 parameters after the this index must be keyword only.
150
151 Returns:
152 A decorator that prevents using arguments after max_positional_args from
153 being used as positional parameters.
154
155 Raises:
156 TypeError if a keyword-only argument is provided as a positional parameter.
157 ValueError if no maximum number of arguments is provided and the function
158 has no arguments with default values.
159 """
160 def positional_decorator(wrapped):
161 def positional_wrapper(*args, **kwargs):
162 if len(args) > max_positional_args:
163 plural_s = ''
164 if max_positional_args != 1:
165 plural_s = 's'
166 raise TypeError('%s() takes at most %d positional argument%s '
167 '(%d given)' % (wrapped.__name__,
168 max_positional_args,
169 plural_s, len(args)))
170 return wrapped(*args, **kwargs)
171 return positional_wrapper
172
173 if isinstance(max_positional_args, (int, long)):
174 return positional_decorator
175 else:
176 args, _, _, defaults = inspect.getargspec(max_positional_args)
177 if defaults is None:
178 raise ValueError(
179 'Functions with no keyword arguments must specify '
180 'max_positional_args')
181 return positional(len(args) - len(defaults))(max_positional_args)
182
183
184 # TODO(rafek): Support 'level' from the Accept header standard.
185 class AcceptItem(object):
186 """Encapsulate a single entry of an Accept header.
187
188 Parses and extracts relevent values from an Accept header and implements
189 a sort order based on the priority of each requested type as defined
190 here:
191
192 http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
193
194 Accept headers are normally a list of comma separated items. Each item
195 has the format of a normal HTTP header. For example:
196
197 Accept: text/plain, text/html, text/*, */*
198
199 This header means to prefer plain text over HTML, HTML over any other
200 kind of text and text over any other kind of supported format.
201
202 This class does not attempt to parse the list of items from the Accept header.
203 The constructor expects the unparsed sub header and the index within the
204 Accept header that the fragment was found.
205
206 Properties:
207 index: The index that this accept item was found in the Accept header.
208 main_type: The main type of the content type.
209 sub_type: The sub type of the content type.
210 q: The q value extracted from the header as a float. If there is no q
211 value, defaults to 1.0.
212 values: All header attributes parsed form the sub-header.
213 sort_key: A tuple (no_main_type, no_sub_type, q, no_values, index):
214 no_main_type: */* has the least priority.
215 no_sub_type: Items with no sub-type have less priority.
216 q: Items with lower q value have less priority.
217 no_values: Items with no values have less priority.
218 index: Index of item in accept header is the last priority.
219 """
220
221 __CONTENT_TYPE_REGEX = re.compile(r'^([^/]+)/([^/]+)$')
222
223 def __init__(self, accept_header, index):
224 """Parse component of an Accept header.
225
226 Args:
227 accept_header: Unparsed sub-expression of accept header.
228 index: The index that this accept item was found in the Accept header.
229 """
230 accept_header = accept_header.lower()
231 content_type, values = cgi.parse_header(accept_header)
232 match = self.__CONTENT_TYPE_REGEX.match(content_type)
233 if not match:
234 raise AcceptError('Not valid Accept header: %s' % accept_header)
235 self.__index = index
236 self.__main_type = match.group(1)
237 self.__sub_type = match.group(2)
238 self.__q = float(values.get('q', 1))
239 self.__values = values
240
241 if self.__main_type == '*':
242 self.__main_type = None
243
244 if self.__sub_type == '*':
245 self.__sub_type = None
246
247 self.__sort_key = (not self.__main_type,
248 not self.__sub_type,
249 -self.__q,
250 not self.__values,
251 self.__index)
252
253 @property
254 def index(self):
255 return self.__index
256
257 @property
258 def main_type(self):
259 return self.__main_type
260
261 @property
262 def sub_type(self):
263 return self.__sub_type
264
265 @property
266 def q(self):
267 return self.__q
268
269 @property
270 def values(self):
271 """Copy the dictionary of values parsed from the header fragment."""
272 return dict(self.__values)
273
274 @property
275 def sort_key(self):
276 return self.__sort_key
277
278 def match(self, content_type):
279 """Determine if the given accept header matches content type.
280
281 Args:
282 content_type: Unparsed content type string.
283
284 Returns:
285 True if accept header matches content type, else False.
286 """
287 content_type, _ = cgi.parse_header(content_type)
288 match = self.__CONTENT_TYPE_REGEX.match(content_type.lower())
289 if not match:
290 return False
291
292 main_type, sub_type = match.group(1), match.group(2)
293 if not(main_type and sub_type):
294 return False
295
296 return ((self.__main_type is None or self.__main_type == main_type) and
297 (self.__sub_type is None or self.__sub_type == sub_type))
298
299
300 def __cmp__(self, other):
301 """Comparison operator based on sort keys."""
302 if not isinstance(other, AcceptItem):
303 return NotImplemented
304 return cmp(self.sort_key, other.sort_key)
305
306 def __str__(self):
307 """Rebuilds Accept header."""
308 content_type = '%s/%s' % (self.__main_type or '*', self.__sub_type or '*')
309 values = self.values
310
311 if values:
312 value_strings = ['%s=%s' % (i, v) for i, v in values.iteritems()]
313 return '%s; %s' % (content_type, '; '.join(value_strings))
314 else:
315 return content_type
316
317 def __repr__(self):
318 return 'AcceptItem(%r, %d)' % (str(self), self.__index)
319
320
321 def parse_accept_header(accept_header):
322 """Parse accept header.
323
324 Args:
325 accept_header: Unparsed accept header. Does not include name of header.
326
327 Returns:
328 List of AcceptItem instances sorted according to their priority.
329 """
330 accept_items = []
331 for index, header in enumerate(accept_header.split(',')):
332 accept_items.append(AcceptItem(header, index))
333 return sorted(accept_items)
334
335
336 def choose_content_type(accept_header, supported_types):
337 """Choose most appropriate supported type based on what client accepts.
338
339 Args:
340 accept_header: Unparsed accept header. Does not include name of header.
341 supported_types: List of content-types supported by the server. The index
342 of the supported types determines which supported type is prefered by
343 the server should the accept header match more than one at the same
344 priority.
345
346 Returns:
347 The preferred supported type if the accept header matches any, else None.
348 """
349 for accept_item in parse_accept_header(accept_header):
350 for supported_type in supported_types:
351 if accept_item.match(supported_type):
352 return supported_type
353 return None
354
355
356 @positional(1)
357 def get_package_for_module(module):
358 """Get package name for a module.
359
360 Helper calculates the package name of a module.
361
362 Args:
363 module: Module to get name for. If module is a string, try to find
364 module in sys.modules.
365
366 Returns:
367 If module contains 'package' attribute, uses that as package name.
368 Else, if module is not the '__main__' module, the module __name__.
369 Else, the base name of the module file name. Else None.
370 """
371 if isinstance(module, basestring):
372 try:
373 module = sys.modules[module]
374 except KeyError:
375 return None
376
377 try:
378 return unicode(module.package)
379 except AttributeError:
380 if module.__name__ == '__main__':
381 try:
382 file_name = module.__file__
383 except AttributeError:
384 pass
385 else:
386 base_name = os.path.basename(file_name)
387 split_name = os.path.splitext(base_name)
388 if len(split_name) == 1:
389 return unicode(base_name)
390 else:
391 return u'.'.join(split_name[:-1])
392
393 return unicode(module.__name__)
394
395
396 class TimeZoneOffset(datetime.tzinfo):
397 """Time zone information as encoded/decoded for DateTimeFields."""
398
399 def __init__(self, offset):
400 """Initialize a time zone offset.
401
402 Args:
403 offset: Integer or timedelta time zone offset, in minutes from UTC. This
404 can be negative.
405 """
406 super(TimeZoneOffset, self).__init__()
407 if isinstance(offset, datetime.timedelta):
408 offset = timedelta_totalseconds(offset)
409 self.__offset = offset
410
411 def utcoffset(self, dt):
412 """Get the a timedelta with the time zone's offset from UTC.
413
414 Returns:
415 The time zone offset from UTC, as a timedelta.
416 """
417 return datetime.timedelta(minutes=self.__offset)
418
419 def dst(self, dt):
420 """Get the daylight savings time offset.
421
422 The formats that ProtoRPC uses to encode/decode time zone information don't
423 contain any information about daylight savings time. So this always
424 returns a timedelta of 0.
425
426 Returns:
427 A timedelta of 0.
428 """
429 return datetime.timedelta(0)
430
431
432 def decode_datetime(encoded_datetime):
433 """Decode a DateTimeField parameter from a string to a python datetime.
434
435 Args:
436 encoded_datetime: A string in RFC 3339 format.
437
438 Returns:
439 A datetime object with the date and time specified in encoded_datetime.
440
441 Raises:
442 ValueError: If the string is not in a recognized format.
443 """
444 # Check if the string includes a time zone offset. Break out the
445 # part that doesn't include time zone info. Convert to uppercase
446 # because all our comparisons should be case-insensitive.
447 time_zone_match = _TIME_ZONE_RE.search(encoded_datetime)
448 if time_zone_match:
449 time_string = encoded_datetime[:time_zone_match.start(1)].upper()
450 else:
451 time_string = encoded_datetime.upper()
452
453 if '.' in time_string:
454 format_string = '%Y-%m-%dT%H:%M:%S.%f'
455 else:
456 format_string = '%Y-%m-%dT%H:%M:%S'
457
458 decoded_datetime = datetime.datetime.strptime(time_string, format_string)
459
460 if not time_zone_match:
461 return decoded_datetime
462
463 # Time zone info was included in the parameter. Add a tzinfo
464 # object to the datetime. Datetimes can't be changed after they're
465 # created, so we'll need to create a new one.
466 if time_zone_match.group('z'):
467 offset_minutes = 0
468 else:
469 sign = time_zone_match.group('sign')
470 hours, minutes = [int(value) for value in
471 time_zone_match.group('hours', 'minutes')]
472 offset_minutes = hours * 60 + minutes
473 if sign == '-':
474 offset_minutes *= -1
475
476 return datetime.datetime(decoded_datetime.year,
477 decoded_datetime.month,
478 decoded_datetime.day,
479 decoded_datetime.hour,
480 decoded_datetime.minute,
481 decoded_datetime.second,
482 decoded_datetime.microsecond,
483 TimeZoneOffset(offset_minutes))
484
485 # TODO: This function was added to the existing library
486 # (which is otherwise not python 2.6-compatible). If we move this to a
487 # submodule we will need to fix it to include this change.
488 def timedelta_totalseconds(delta):
489 # python2.6 does not have timedelta.total_seconds() so we have
490 # to calculate this ourselves. This is straight from the
491 # datetime docs.
492 return (
493 (delta.microseconds + (delta.seconds + delta.days * 24 * 3600)
494 * 10**6) / 10**6)
OLDNEW
« no previous file with comments | « gslib/third_party/protorpc/protojson.py ('k') | gslib/third_party/storage_apitools/__init__.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698