| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 7cbceab commit fdf7c80
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -29,6 +29,7 @@ | |||
| 29 | 29 | import uritemplate | |
| 30 | 30 | import urllib | |
| 31 | 31 | import urlparse | |
| 32 | + import mimeparse | ||
| 32 | 33 | import mimetypes | |
| 33 | 34 | ||
| 34 | 35 | try: | |
@@ -41,6 +42,8 @@ | |||
| 41 | 42 | from email.mime.nonmultipart import MIMENonMultipart | |
| 42 | 43 | from errors import HttpError | |
| 43 | 44 | from errors import InvalidJsonError | |
| 45 | + from errors import MediaUploadSizeError | ||
| 46 | + from errors import UnacceptableMimeTypeError | ||
| 44 | 47 | from errors import UnknownLinkType | |
| 45 | 48 | from http import HttpRequest | |
| 46 | 49 | from model import JsonModel | |
@@ -226,6 +229,22 @@ def _cast(value, schema_type): | |||
| 226 | 229 | else: | |
| 227 | 230 | return str(value) | |
| 228 | 231 | ||
| 232 | + MULTIPLIERS = { | ||
| 233 | + "KB": 2**10, | ||
| 234 | + "MB": 2**20, | ||
| 235 | + "GB": 2**30, | ||
| 236 | + "TB": 2**40, | ||
| 237 | + } | ||
| 238 | + | ||
| 239 | + def _media_size_to_long(maxSize): | ||
| 240 | + """Convert a string media size, such as 10GB or 3TB into an integer.""" | ||
| 241 | + units = maxSize[-2:].upper() | ||
| 242 | + multiplier = MULTIPLIERS.get(units, 0) | ||
| 243 | + if multiplier: | ||
| 244 | + return int(maxSize[:-2])*multiplier | ||
| 245 | + else: | ||
| 246 | + return int(maxSize) | ||
| 247 | + | ||
| 229 | 248 | ||
| 230 | 249 | def createResource(http, baseUrl, model, requestBuilder, | |
| 231 | 250 | developerKey, resourceDesc, futureDesc): | |
@@ -245,6 +264,15 @@ def createMethod(theclass, methodName, methodDesc, futureDesc): | |||
| 245 | 264 | httpMethod = methodDesc['httpMethod'] | |
| 246 | 265 | methodId = methodDesc['id'] | |
| 247 | 266 | ||
| 267 | + mediaPathUrl = None | ||
| 268 | + accept = [] | ||
| 269 | + maxSize = 0 | ||
| 270 | + if 'mediaUpload' in methodDesc: | ||
| 271 | + mediaUpload = methodDesc['mediaUpload'] | ||
| 272 | + mediaPathUrl = mediaUpload['protocols']['simple']['path'] | ||
| 273 | + accept = mediaUpload['accept'] | ||
| 274 | + maxSize = _media_size_to_long(mediaUpload['maxSize']) | ||
| 275 | + | ||
| 248 | 276 | if 'parameters' not in methodDesc: | |
| 249 | 277 | methodDesc['parameters'] = {} | |
| 250 | 278 | for name in STACK_QUERY_PARAMETERS: | |
@@ -259,11 +287,13 @@ def createMethod(theclass, methodName, methodDesc, futureDesc): | |||
| 259 | 287 | 'type': 'object', | |
| 260 | 288 | 'required': True, | |
| 261 | 289 | } | |
| 262 | - methodDesc['parameters']['media_body'] = { | ||
| 263 | - 'description': 'The filename of the media request body.', | ||
| 264 | - 'type': 'string', | ||
| 265 | - 'required': False, | ||
| 266 | - } | ||
| 290 | + if 'mediaUpload' in methodDesc: | ||
| 291 | + methodDesc['parameters']['media_body'] = { | ||
| 292 | + 'description': 'The filename of the media request body.', | ||
| 293 | + 'type': 'string', | ||
| 294 | + 'required': False, | ||
| 295 | + } | ||
| 296 | + methodDesc['parameters']['body']['required'] = False | ||
| 267 | 297 | ||
| 268 | 298 | argmap = {} # Map from method parameter name to query parameter name | |
| 269 | 299 | required_params = [] # Required parameters | |
@@ -324,7 +354,6 @@ def method(self, **kwargs): | |||
| 324 | 354 | 'Parameter "%s" value "%s" is not an allowed value in "%s"' % | |
| 325 | 355 | (name, kwargs[name], str(enums))) | |
| 326 | 356 | ||
| 327 | - media_filename = kwargs.pop('media_body', None) | ||
| 328 | 357 | actual_query_params = {} | |
| 329 | 358 | actual_path_params = {} | |
| 330 | 359 | for key, value in kwargs.iteritems(): | |
@@ -339,6 +368,7 @@ def method(self, **kwargs): | |||
| 339 | 368 | if key in path_params: | |
| 340 | 369 | actual_path_params[argmap[key]] = cast_value | |
| 341 | 370 | body_value = kwargs.get('body', None) | |
| 371 | + media_filename = kwargs.get('media_body', None) | ||
| 342 | 372 | ||
| 343 | 373 | if self._developerKey: | |
| 344 | 374 | actual_query_params['key'] = self._developerKey | |
@@ -354,11 +384,16 @@ def method(self, **kwargs): | |||
| 354 | 384 | (media_mime_type, encoding) = mimetypes.guess_type(media_filename) | |
| 355 | 385 | if media_mime_type is None: | |
| 356 | 386 | raise UnknownFileType(media_filename) | |
| 387 | + if not mimeparse.best_match([media_mime_type], ','.join(accept)): | ||
| 388 | + raise UnacceptableMimeTypeError(media_mime_type) | ||
| 357 | 389 | ||
| 358 | - # modify the path to prepend '/upload' | ||
| 359 | - parsed = list(urlparse.urlparse(url)) | ||
| 360 | - parsed[2] = '/upload' + parsed[2] | ||
| 361 | - url = urlparse.urlunparse(parsed) | ||
| 390 | + # Check the maxSize | ||
| 391 | + if maxSize > 0 and os.path.getsize(media_filename) > maxSize: | ||
| 392 | + raise MediaUploadSizeError(media_filename) | ||
| 393 | + | ||
| 394 | + # Use the media path uri for media uploads | ||
| 395 | + expanded_url = uritemplate.expand(mediaPathUrl, params) | ||
| 396 | + url = urlparse.urljoin(self._baseUrl, expanded_url + query) | ||
| 362 | 397 | ||
| 363 | 398 | if body is None: | |
| 364 | 399 | headers['content-type'] = media_mime_type | |
@@ -406,8 +441,6 @@ def method(self, **kwargs): | |||
| 406 | 441 | for arg in argmap.iterkeys(): | |
| 407 | 442 | if arg in STACK_QUERY_PARAMETERS: | |
| 408 | 443 | continue | |
| 409 | - if arg == 'media_body': | ||
| 410 | - continue | ||
| 411 | 444 | repeated = '' | |
| 412 | 445 | if arg in repeated_params: | |
| 413 | 446 | repeated = ' (repeated)' | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -40,8 +40,7 @@ def __init__(self, resp, content, uri=None): | |||
| 40 | 40 | self.uri = uri | |
| 41 | 41 | ||
| 42 | 42 | def _get_reason(self): | |
| 43 | - """Calculate the reason for the error from the response content. | ||
| 44 | - """ | ||
| 43 | + """Calculate the reason for the error from the response content.""" | ||
| 45 | 44 | if self.resp.get('content-type', '').startswith('application/json'): | |
| 46 | 45 | try: | |
| 47 | 46 | data = simplejson.loads(self.content) | |
@@ -70,3 +69,11 @@ class InvalidJsonError(Error): | |||
| 70 | 69 | class UnknownLinkType(Error): | |
| 71 | 70 | """Link type unknown or unexpected.""" | |
| 72 | 71 | pass | |
| 72 | + | ||
| 73 | + class UnacceptableMimeTypeError(Error): | ||
| 74 | + """That is an unacceptable mimetype for this operation.""" | ||
| 75 | + pass | ||
| 76 | + | ||
| 77 | + class MediaUploadSizeError(Error): | ||
| 78 | + """Media is larger than the method can accept.""" | ||
| 79 | + pass | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,172 @@ | |||
| 1 | + # Copyright (C) 2007 Joe Gregorio | ||
| 2 | + # | ||
| 3 | + # Licensed under the MIT License | ||
| 4 | + | ||
| 5 | + """MIME-Type Parser | ||
| 6 | + | ||
| 7 | + This module provides basic functions for handling mime-types. It can handle | ||
| 8 | + matching mime-types against a list of media-ranges. See section 14.1 of the | ||
| 9 | + HTTP specification [RFC 2616] for a complete explanation. | ||
| 10 | + | ||
| 11 | + http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 | ||
| 12 | + | ||
| 13 | + Contents: | ||
| 14 | + - parse_mime_type(): Parses a mime-type into its component parts. | ||
| 15 | + - parse_media_range(): Media-ranges are mime-types with wild-cards and a 'q' | ||
| 16 | + quality parameter. | ||
| 17 | + - quality(): Determines the quality ('q') of a mime-type when | ||
| 18 | + compared against a list of media-ranges. | ||
| 19 | + - quality_parsed(): Just like quality() except the second parameter must be | ||
| 20 | + pre-parsed. | ||
| 21 | + - best_match(): Choose the mime-type with the highest quality ('q') | ||
| 22 | + from a list of candidates. | ||
| 23 | + """ | ||
| 24 | + | ||
| 25 | + __version__ = '0.1.3' | ||
| 26 | + __author__ = 'Joe Gregorio' | ||
| 27 | + __email__ = 'joe@bitworking.org' | ||
| 28 | + __license__ = 'MIT License' | ||
| 29 | + __credits__ = '' | ||
| 30 | + | ||
| 31 | + | ||
| 32 | + def parse_mime_type(mime_type): | ||
| 33 | + """Parses a mime-type into its component parts. | ||
| 34 | + | ||
| 35 | + Carves up a mime-type and returns a tuple of the (type, subtype, params) | ||
| 36 | + where 'params' is a dictionary of all the parameters for the media range. | ||
| 37 | + For example, the media range 'application/xhtml;q=0.5' would get parsed | ||
| 38 | + into: | ||
| 39 | + | ||
| 40 | + ('application', 'xhtml', {'q', '0.5'}) | ||
| 41 | + """ | ||
| 42 | + parts = mime_type.split(';') | ||
| 43 | + params = dict([tuple([s.strip() for s in param.split('=', 1)])\ | ||
| 44 | + for param in parts[1:] | ||
| 45 | + ]) | ||
| 46 | + full_type = parts[0].strip() | ||
| 47 | + # Java URLConnection class sends an Accept header that includes a | ||
| 48 | + # single '*'. Turn it into a legal wildcard. | ||
| 49 | + if full_type == '*': | ||
| 50 | + full_type = '*/*' | ||
| 51 | + (type, subtype) = full_type.split('/') | ||
| 52 | + | ||
| 53 | + return (type.strip(), subtype.strip(), params) | ||
| 54 | + | ||
| 55 | + | ||
| 56 | + def parse_media_range(range): | ||
| 57 | + """Parse a media-range into its component parts. | ||
| 58 | + | ||
| 59 | + Carves up a media range and returns a tuple of the (type, subtype, | ||
| 60 | + params) where 'params' is a dictionary of all the parameters for the media | ||
| 61 | + range. For example, the media range 'application/*;q=0.5' would get parsed | ||
| 62 | + into: | ||
| 63 | + | ||
| 64 | + ('application', '*', {'q', '0.5'}) | ||
| 65 | + | ||
| 66 | + In addition this function also guarantees that there is a value for 'q' | ||
| 67 | + in the params dictionary, filling it in with a proper default if | ||
| 68 | + necessary. | ||
| 69 | + """ | ||
| 70 | + (type, subtype, params) = parse_mime_type(range) | ||
| 71 | + if not params.has_key('q') or not params['q'] or \ | ||
| 72 | + not float(params['q']) or float(params['q']) > 1\ | ||
| 73 | + or float(params['q']) < 0: | ||
| 74 | + params['q'] = '1' | ||
| 75 | + | ||
| 76 | + return (type, subtype, params) | ||
| 77 | + | ||
| 78 | + | ||
| 79 | + def fitness_and_quality_parsed(mime_type, parsed_ranges): | ||
| 80 | + """Find the best match for a mime-type amongst parsed media-ranges. | ||
| 81 | + | ||
| 82 | + Find the best match for a given mime-type against a list of media_ranges | ||
| 83 | + that have already been parsed by parse_media_range(). Returns a tuple of | ||
| 84 | + the fitness value and the value of the 'q' quality parameter of the best | ||
| 85 | + match, or (-1, 0) if no match was found. Just as for quality_parsed(), | ||
| 86 | + 'parsed_ranges' must be a list of parsed media ranges. | ||
| 87 | + """ | ||
| 88 | + best_fitness = -1 | ||
| 89 | + best_fit_q = 0 | ||
| 90 | + (target_type, target_subtype, target_params) =\ | ||
| 91 | + parse_media_range(mime_type) | ||
| 92 | + for (type, subtype, params) in parsed_ranges: | ||
| 93 | + type_match = (type == target_type or\ | ||
| 94 | + type == '*' or\ | ||
| 95 | + target_type == '*') | ||
| 96 | + subtype_match = (subtype == target_subtype or\ | ||
| 97 | + subtype == '*' or\ | ||
| 98 | + target_subtype == '*') | ||
| 99 | + if type_match and subtype_match: | ||
| 100 | + param_matches = reduce(lambda x, y: x + y, [1 for (key, value) in \ | ||
| 101 | + target_params.iteritems() if key != 'q' and \ | ||
| 102 | + params.has_key(key) and value == params[key]], 0) | ||
| 103 | + fitness = (type == target_type) and 100 or 0 | ||
| 104 | + fitness += (subtype == target_subtype) and 10 or 0 | ||
| 105 | + fitness += param_matches | ||
| 106 | + if fitness > best_fitness: | ||
| 107 | + best_fitness = fitness | ||
| 108 | + best_fit_q = params['q'] | ||
| 109 | + | ||
| 110 | + return best_fitness, float(best_fit_q) | ||
| 111 | + | ||
| 112 | + | ||
| 113 | + def quality_parsed(mime_type, parsed_ranges): | ||
| 114 | + """Find the best match for a mime-type amongst parsed media-ranges. | ||
| 115 | + | ||
| 116 | + Find the best match for a given mime-type against a list of media_ranges | ||
| 117 | + that have already been parsed by parse_media_range(). Returns the 'q' | ||
| 118 | + quality parameter of the best match, 0 if no match was found. This function | ||
| 119 | + bahaves the same as quality() except that 'parsed_ranges' must be a list of | ||
| 120 | + parsed media ranges. | ||
| 121 | + """ | ||
| 122 | + | ||
| 123 | + return fitness_and_quality_parsed(mime_type, parsed_ranges)[1] | ||
| 124 | + | ||
| 125 | + | ||
| 126 | + def quality(mime_type, ranges): | ||
| 127 | + """Return the quality ('q') of a mime-type against a list of media-ranges. | ||
| 128 | + | ||
| 129 | + Returns the quality 'q' of a mime-type when compared against the | ||
| 130 | + media-ranges in ranges. For example: | ||
| 131 | + | ||
| 132 | + >>> quality('text/html','text/*;q=0.3, text/html;q=0.7, | ||
| 133 | + text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5') | ||
| 134 | + 0.7 | ||
| 135 | + | ||
| 136 | + """ | ||
| 137 | + parsed_ranges = [parse_media_range(r) for r in ranges.split(',')] | ||
| 138 | + | ||
| 139 | + return quality_parsed(mime_type, parsed_ranges) | ||
| 140 | + | ||
| 141 | + | ||
| 142 | + def best_match(supported, header): | ||
| 143 | + """Return mime-type with the highest quality ('q') from list of candidates. | ||
| 144 | + | ||
| 145 | + Takes a list of supported mime-types and finds the best match for all the | ||
| 146 | + media-ranges listed in header. The value of header must be a string that | ||
| 147 | + conforms to the format of the HTTP Accept: header. The value of 'supported' | ||
| 148 | + is a list of mime-types. The list of supported mime-types should be sorted | ||
| 149 | + in order of increasing desirability, in case of a situation where there is | ||
| 150 | + a tie. | ||
| 151 | + | ||
| 152 | + >>> best_match(['application/xbel+xml', 'text/xml'], | ||
| 153 | + 'text/*;q=0.5,*/*; q=0.1') | ||
| 154 | + 'text/xml' | ||
| 155 | + """ | ||
| 156 | + split_header = _filter_blank(header.split(',')) | ||
| 157 | + parsed_header = [parse_media_range(r) for r in split_header] | ||
| 158 | + weighted_matches = [] | ||
| 159 | + pos = 0 | ||
| 160 | + for mime_type in supported: | ||
| 161 | + weighted_matches.append((fitness_and_quality_parsed(mime_type, | ||
| 162 | + parsed_header), pos, mime_type)) | ||
| 163 | + pos += 1 | ||
| 164 | + weighted_matches.sort() | ||
| 165 | + | ||
| 166 | + return weighted_matches[-1][0][1] and weighted_matches[-1][2] or '' | ||
| 167 | + | ||
| 168 | + | ||
| 169 | + def _filter_blank(i): | ||
| 170 | + for s in i: | ||
| 171 | + if s.strip(): | ||
| 172 | + yield s | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -258,6 +258,22 @@ | |||
| 258 | 258 | }, | |
| 259 | 259 | "response": { | |
| 260 | 260 | "$ref": "Animal" | |
| 261 | + }, | ||
| 262 | + "mediaUpload": { | ||
| 263 | + "accept": [ | ||
| 264 | + "image/png" | ||
| 265 | + ], | ||
| 266 | + "maxSize": "1KB", | ||
| 267 | + "protocols": { | ||
| 268 | + "simple": { | ||
| 269 | + "multipart": true, | ||
| 270 | + "path": "upload/activities/{userId}/@self" | ||
| 271 | + }, | ||
| 272 | + "resumable": { | ||
| 273 | + "multipart": true, | ||
| 274 | + "path": "upload/activities/{userId}/@self" | ||
| 275 | + } | ||
| 276 | + } | ||
| 261 | 277 | } | |
| 262 | 278 | }, | |
| 263 | 279 | "list": { | |
| Back | FazBrowse Home | New Git URL |
0 commit comments