| OLD | NEW |
| (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) |
| OLD | NEW |