├── mimeparse ├── py.typed └── __init__.py ├── MANIFEST.in ├── .gitignore ├── tox.ini ├── LICENSE ├── .github └── workflows │ └── run-test.yaml ├── README.rst ├── pyproject.toml ├── mimeparse_test.py └── testdata.json /mimeparse/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE mimeparse_test.py testdata.json pyproject.toml 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .*.sw[po] 3 | .tox 4 | *.egg-info 5 | dist/ 6 | build/ 7 | .venv/ 8 | .vscode 9 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py, flake8 3 | 4 | [testenv] 5 | deps = pytest 6 | commands = pytest 7 | 8 | [testenv:flake8] 9 | deps = flake8 10 | flake8-import-order 11 | commands = flake8 --statistics --show-source --ignore=E501 --exclude=.venv,.tox,*egg,*.egg-info,build,dist . 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 17 | THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /.github/workflows/run-test.yaml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | run-tox: 9 | name: test-${{ matrix.python-version }} 10 | runs-on: "ubuntu-latest" 11 | strategy: 12 | matrix: 13 | python-version: 14 | - "3.8" 15 | - "3.9" 16 | - "3.10" 17 | - "3.11" 18 | - "3.12" 19 | - "3.13.0-rc.1 - 3.13" 20 | - "pypy-3.8" 21 | - "pypy-3.9" 22 | - "pypy-3.10" 23 | 24 | fail-fast: false 25 | 26 | steps: 27 | - name: Checkout repo 28 | uses: actions/checkout@v4 29 | 30 | - name: Set up python 31 | uses: actions/setup-python@v5 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | 35 | - name: Install dependencies 36 | run: | 37 | pip install --upgrade pip 38 | pip install --upgrade tox 39 | 40 | - name: Run tests 41 | run: | 42 | tox -e py 43 | 44 | run-lint: 45 | name: lint 46 | runs-on: "ubuntu-latest" 47 | strategy: 48 | matrix: 49 | python-version: 50 | - "3.12" 51 | 52 | fail-fast: false 53 | 54 | steps: 55 | - name: Checkout repo 56 | uses: actions/checkout@v4 57 | 58 | - name: Set up python 59 | uses: actions/setup-python@v5 60 | with: 61 | python-version: ${{ matrix.python-version }} 62 | 63 | - name: Install dependencies 64 | run: | 65 | pip install --upgrade pip 66 | pip install --upgrade tox 67 | 68 | - name: Run lint 69 | run: | 70 | tox -e flake8 71 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Python-MimeParse 2 | ================ 3 | 4 | This module provides basic functions for handling mime-types. It can 5 | handle matching mime-types against a list of media-ranges. See section 6 | 5.3.2 of the HTTP 1.1 Semantics and Content specification [RFC 7231] for 7 | a complete explanation: https://tools.ietf.org/html/rfc7231#section-5.3.2 8 | 9 | Installation 10 | ------------ 11 | 12 | Use **pip**: 13 | 14 | .. code-block:: sh 15 | 16 | $ pip install python-mimeparse 17 | 18 | It supports Python 3.8+ and PyPy. 19 | 20 | Functions 21 | --------- 22 | 23 | **parse_mime_type()** 24 | 25 | Parses a mime-type into its component parts. 26 | 27 | **parse_media_range()** 28 | 29 | Media-ranges are mime-types with wild-cards and a "q" quality parameter. 30 | 31 | **quality()** 32 | 33 | Determines the quality ("q") of a mime-type when compared against a list of 34 | media-ranges. 35 | 36 | **quality_parsed()** 37 | 38 | Just like ``quality()`` except the second parameter must be pre-parsed. 39 | 40 | **best_match()** 41 | 42 | Choose the mime-type with the highest quality ("q") from a list of candidates. 43 | 44 | Testing 45 | ------- 46 | 47 | Run the tests by typing: ``python mimeparse_test.py``. 48 | 49 | To make sure that the package works in all the supported environments, you can 50 | run **tox** tests: 51 | 52 | .. code-block:: sh 53 | 54 | $ pip install tox 55 | $ tox 56 | 57 | The format of the JSON test data file is as follows: A top-level JSON object 58 | which has a key for each of the functions to be tested. The value corresponding 59 | to that key is a list of tests. Each test contains: the argument or arguments 60 | to the function being tested, the expected results and an optional description. 61 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "setuptools.build_meta" 3 | requires = [ 4 | "setuptools>=47", 5 | "wheel>=0.34", 6 | ] 7 | 8 | [project] 9 | name = "python-mimeparse" 10 | readme = "README.rst" 11 | dynamic = ["version"] 12 | dependencies = [] 13 | requires-python = ">=3.8" 14 | description = "A module provides basic functions for parsing mime-type names and matching them against a list of media-ranges." 15 | license = {text = "MIT"} 16 | authors = [ 17 | {name = "Joe Gregorio", email = "joe@bitworking.org"}, 18 | {name = "DB Tsai", email = "dbtsai@dbtsai.com"}, 19 | ] 20 | maintainers = [ 21 | {name = "Falconry team", email = "mail@kgriffs.com"}, 22 | ] 23 | classifiers = [ 24 | "Programming Language :: Python", 25 | "Programming Language :: Python :: 3", 26 | "Programming Language :: Python :: 3.8", 27 | "Programming Language :: Python :: 3.9", 28 | "Programming Language :: Python :: 3.10", 29 | "Programming Language :: Python :: 3.11", 30 | "Programming Language :: Python :: 3.12", 31 | "Programming Language :: Python :: 3.13", 32 | "Programming Language :: Python :: Implementation :: CPython", 33 | "Programming Language :: Python :: Implementation :: PyPy", 34 | "License :: OSI Approved :: MIT License", 35 | "Operating System :: OS Independent", 36 | "Development Status :: 6 - Mature", 37 | "Intended Audience :: Developers", 38 | "Topic :: Internet :: WWW/HTTP", 39 | "Topic :: Software Development :: Libraries :: Python Modules", 40 | ] 41 | keywords = ["mime-type", "media-type"] 42 | 43 | [project.optional-dependencies] 44 | test = ["pytest"] 45 | 46 | [project.urls] 47 | Homepage = "https://github.com/falconry/python-mimeparse" 48 | "Issue Tracker" = "https://github.com/falconry/python-mimeparse/issues" 49 | Funding = "https://opencollective.com/falcon" 50 | Chat = "https://gitter.im/falconry/user" 51 | 52 | [tool.setuptools] 53 | license-files = ["LICENSE"] 54 | packages = ["mimeparse"] 55 | 56 | [tool.setuptools.dynamic] 57 | version = {attr = "mimeparse.__version__"} 58 | -------------------------------------------------------------------------------- /mimeparse_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Python tests for Mime-Type Parser. 4 | 5 | This module loads a json file and converts the tests specified therein to a set 6 | of PyUnitTestCases. Then it uses PyUnit to run them and report their status. 7 | """ 8 | import json 9 | import unittest 10 | 11 | import mimeparse 12 | 13 | 14 | __version__ = "0.1" 15 | __author__ = 'Ade Oshineye' 16 | __email__ = "ade@oshineye.com" 17 | __credits__ = "" 18 | 19 | 20 | class MimeParseTestCase(unittest.TestCase): 21 | 22 | def setUp(self): 23 | super().setUp() 24 | with open("testdata.json") as f: 25 | self.test_data = json.load(f) 26 | 27 | def _test_parse_media_range(self, args, expected): 28 | expected = tuple(expected) 29 | result = mimeparse.parse_media_range(args) 30 | message = f"Expected: '{expected}' but got {result}" 31 | self.assertEqual(expected, result, message) 32 | 33 | def _test_quality(self, args, expected): 34 | result = mimeparse.quality(args[0], args[1]) 35 | message = f"Expected: '{expected}' but got {result}" 36 | self.assertEqual(expected, result, message) 37 | 38 | def _test_best_match(self, args, expected, description): 39 | if expected is None: 40 | self.assertRaises(mimeparse.MimeTypeParseException, 41 | mimeparse.best_match, args[0], args[1]) 42 | else: 43 | result = mimeparse.best_match(args[0], args[1]) 44 | message = \ 45 | "Expected: '%s' but got %s. Description for this test: %s" % \ 46 | (expected, result, description) 47 | self.assertEqual(expected, result, message) 48 | 49 | def _test_parse_mime_type(self, args, expected): 50 | if expected is None: 51 | self.assertRaises(mimeparse.MimeTypeParseException, 52 | mimeparse.parse_mime_type, args) 53 | else: 54 | expected = tuple(expected) 55 | result = mimeparse.parse_mime_type(args) 56 | message = f"Expected: '{expected}' but got {result}" 57 | self.assertEqual(expected, result, message) 58 | 59 | def test_parse_media_range(self): 60 | for args, expected in self.test_data['parse_media_range']: 61 | self._test_parse_media_range(args, expected) 62 | 63 | def test_quality(self): 64 | for args, expected in self.test_data['quality']: 65 | self._test_quality(args, expected) 66 | 67 | def test_best_match(self): 68 | for args, expected, description in self.test_data['best_match']: 69 | self._test_best_match(args, expected, description) 70 | 71 | def test_parse_mime_type(self): 72 | for args, expected in self.test_data['parse_mime_type']: 73 | self._test_parse_mime_type(args, expected) 74 | 75 | 76 | if __name__ == '__main__': 77 | unittest.main() 78 | -------------------------------------------------------------------------------- /testdata.json: -------------------------------------------------------------------------------- 1 | { 2 | "parse_media_range": [ 3 | ["application/xml;q=1", ["application", "xml", {"q": "1"}]], 4 | ["application/xml", ["application", "xml", {"q": "1"}]], 5 | ["application/xml;q=",["application", "xml", {"q": "1"}]], 6 | ["application/xml ;q=",["application", "xml", {"q": "1"}]], 7 | ["application/xml ;q=-1",["application", "xml", {"q": "1"}]], 8 | ["application/xml ; q=1;b=other",["application", "xml", {"q": "1", "b":"other"}]], 9 | ["application/xml ; q=2;b=other",["application", "xml", {"q": "1", "b":"other"}]], 10 | ["application/xml ; q=0",["application", "xml", {"q": "0"}]], 11 | ["application/xml ; q=foo", ["application", "xml", {"q": "1"}]], 12 | ["application/xml ; Q=0.6", ["application", "xml", {"q": "0.6"}]], 13 | [" *; q=.2", ["*", "*", {"q": ".2"}]] 14 | ], 15 | 16 | "quality": [ 17 | [ 18 | [ 19 | "text/html;level=1", 20 | "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5" 21 | ], 22 | 1 23 | ], 24 | [ 25 | [ 26 | "text/html", 27 | "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5" 28 | ], 29 | 0.7 30 | ], 31 | [ 32 | [ 33 | "text/plain", 34 | "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5" 35 | ], 36 | 0.3 37 | ], 38 | [ 39 | [ 40 | "image/jpeg", 41 | "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5" 42 | ], 43 | 0.5 44 | ], 45 | [ 46 | [ 47 | "text/html;level=2", 48 | "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5" 49 | ], 50 | 0.4 51 | ], 52 | [ 53 | [ 54 | "text/html;level=3", 55 | "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5" 56 | ], 57 | 0.7 58 | ], 59 | [ 60 | [ 61 | "text/plain", 62 | "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2" 63 | ], 64 | 0.2 65 | ] 66 | ], 67 | 68 | "best_match": [ 69 | [ 70 | [ 71 | ["application/json", "text/html"], 72 | "application/json, text/javascript, */*" 73 | ], 74 | "application/json", 75 | "common AJAX scenario" 76 | ], 77 | [ 78 | [ 79 | ["application/xbel+xml", "application/xml"], 80 | "application/xbel+xml" 81 | ], 82 | "application/xbel+xml", 83 | "direct match" 84 | ], 85 | [ 86 | [ 87 | ["application/xbel+xml", "application/xml"], 88 | "application/xbel+xml; q=1" 89 | ], 90 | "application/xbel+xml", 91 | "direct match with a q parameter" 92 | ], 93 | [ 94 | [ 95 | ["application/xbel+xml", "application/xml"], 96 | "application/xml; q=1" 97 | ], 98 | "application/xml", 99 | "direct match of our second choice with a q parameter" 100 | ], 101 | [ 102 | [ 103 | ["application/xbel+xml", "application/xml"], 104 | "application/*; q=1" 105 | ], 106 | "application/xml", 107 | "match using a subtype wildcard" 108 | ], 109 | [ 110 | [ 111 | ["application/xbel+xml", "application/xml"], 112 | "*/*" 113 | ], 114 | "application/xml", 115 | "match using a type wildcard" 116 | ], 117 | [ 118 | [ 119 | ["application/xbel+xml", "text/xml"], 120 | "text/*;q=0.5,*/*; q=0.1" 121 | ], 122 | "text/xml", 123 | "match using a type versus a lower weighted subtype" 124 | ], 125 | [ 126 | [ 127 | ["application/xbel+xml", "text/xml"], 128 | "text/html,application/atom+xml; q=0.9" 129 | ], 130 | "", 131 | "fail to match anything" 132 | ], 133 | [ 134 | [ 135 | ["application/json", "text/html"], 136 | "application/json, text/html;q=0.9" 137 | ], 138 | "application/json", 139 | "verify fitness ordering" 140 | ], 141 | [ 142 | [ 143 | ["image/*", "application/xml"], 144 | "image/png" 145 | ], 146 | "image/*", 147 | "match using a type wildcard" 148 | ], 149 | [ 150 | [ 151 | ["image/*", "application/xml"], 152 | "image/*" 153 | ], 154 | "image/*", 155 | "match using a wildcard for both requested and supported" 156 | ], 157 | [ 158 | [ 159 | ["image/jpeg", "text/plain"], 160 | "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5" 161 | ], 162 | "image/jpeg", 163 | "media type with highest associated quality factor should win, not necessarily most specific" 164 | ], 165 | [ 166 | [ 167 | ["text/html", "application/rdf+xml"], 168 | "text/html, application/rdf+xml" 169 | ], 170 | "application/rdf+xml", 171 | "match should use highest order of supported when there is a tie" 172 | ], 173 | [ 174 | [ 175 | ["text/plain", "text/plain;format=flowed", "text/html"], 176 | "text/*, text/plain, text/plain;format=flowed, */*" 177 | ], 178 | "text/plain;format=flowed", 179 | "most specific reference has precedence" 180 | ], 181 | [ 182 | [ 183 | ["application/rdf+xml", "text/html"], 184 | "text/html, application/rdf+xml" 185 | ], 186 | "text/html", 187 | "match should use highest order of supported when there is a tie" 188 | ], 189 | [ 190 | [ 191 | ["application/json;q=1.0", "text/html;q=0.9", "text/plain;q=0.1"], 192 | "*/*" 193 | ], 194 | "application/json;q=1.0", 195 | "*/* match should pick an acceptable type with the highest quality" 196 | ], 197 | [ 198 | [ 199 | ["text/html;q=0.9", "application/json", "text/plain;q=0.1"], 200 | "*/*" 201 | ], 202 | "application/json", 203 | "*/* match should pick an acceptable type with the highest quality, even if it's implicit" 204 | ], 205 | [ 206 | [ 207 | ["application/json", "text/html"], 208 | "text" 209 | ], 210 | null, 211 | "match should use the default if an invalid Accept header is passed" 212 | ] 213 | ], 214 | 215 | "parse_mime_type": [ 216 | [ 217 | "application/xhtml;q=0.5", 218 | ["application", "xhtml", {"q": "0.5"}] 219 | ], 220 | [ 221 | "application/xhtml;q=0.5;ver=1.2", 222 | ["application", "xhtml", {"q": "0.5", "ver": "1.2"}] 223 | ], 224 | [ 225 | "application/xhtml;q=0.5;foo=\"bar quux\"", 226 | ["application", "xhtml", {"q": "0.5", "foo": "bar quux"}]], 227 | [ 228 | "text", null 229 | ], 230 | [ 231 | "text/something/invalid", null 232 | ] 233 | ] 234 | } 235 | -------------------------------------------------------------------------------- /mimeparse/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | __version__ = '2.1.0.dev1' 4 | __author__ = 'Joe Gregorio' 5 | __email__ = 'joe@bitworking.org' 6 | __license__ = 'MIT License' 7 | __credits__ = '' 8 | 9 | from typing import Dict, Generator, Iterable, Tuple, Union 10 | 11 | 12 | class MimeTypeParseException(ValueError): 13 | pass 14 | 15 | 16 | # Vendored version of cgi._parseparam from Python 3.11 (deprecated and slated 17 | # for removal in 3.13) 18 | def _parseparam(s: str) -> Generator[str, None, None]: 19 | while s[:1] == ';': 20 | s = s[1:] 21 | end = s.find(';') 22 | while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2: 23 | end = s.find(';', end + 1) 24 | if end < 0: 25 | end = len(s) 26 | f = s[:end] 27 | yield f.strip() 28 | s = s[end:] 29 | 30 | 31 | # Vendored version of cgi.parse_header from Python 3.11 (deprecated and slated 32 | # for removal in 3.13) 33 | def _parse_header(line: str) -> Tuple[str, Dict[str, str]]: 34 | """Parse a Content-type like header. 35 | 36 | Return the main content-type and a dictionary of options. 37 | 38 | """ 39 | parts = _parseparam(';' + line) 40 | key = parts.__next__() 41 | pdict = {} 42 | for p in parts: 43 | i = p.find('=') 44 | if i >= 0: 45 | name = p[:i].strip().lower() 46 | value = p[i + 1:].strip() 47 | if len(value) >= 2 and value[0] == value[-1] == '"': 48 | value = value[1:-1] 49 | value = value.replace('\\\\', '\\').replace('\\"', '"') 50 | pdict[name] = value 51 | return key, pdict 52 | 53 | 54 | def parse_mime_type(mime_type: str) -> Tuple[str, str, Dict[str, str]]: 55 | """Parses a mime-type into its component parts. 56 | 57 | Carves up a mime-type and returns a tuple of the (type, subtype, params) 58 | where 'params' is a dictionary of all the parameters for the media range. 59 | For example, the media range 'application/xhtml;q=0.5' would get parsed 60 | into: 61 | 62 | ('application', 'xhtml', {'q', '0.5'}) 63 | """ 64 | full_type, params = _parse_header(mime_type) 65 | # Java URLConnection class sends an Accept header that includes a 66 | # single '*'. Turn it into a legal wildcard. 67 | if full_type == '*': 68 | full_type = '*/*' 69 | 70 | type_parts = full_type.split('/') if '/' in full_type else None 71 | if not type_parts or len(type_parts) > 2: 72 | raise MimeTypeParseException( 73 | f"Can't parse type \"{full_type}\"") 74 | 75 | (type, subtype) = type_parts 76 | 77 | return (type.strip(), subtype.strip(), params) 78 | 79 | 80 | def parse_media_range(range: str) -> Tuple[str, str, Dict[str, str]]: 81 | """Parse a media-range into its component parts. 82 | 83 | Carves up a media range and returns a tuple of the (type, subtype, 84 | params) where 'params' is a dictionary of all the parameters for the media 85 | range. For example, the media range 'application/*;q=0.5' would get parsed 86 | into: 87 | 88 | ('application', '*', {'q', '0.5'}) 89 | 90 | In addition this function also guarantees that there is a value for 'q' 91 | in the params dictionary, filling it in with a proper default if 92 | necessary. 93 | """ 94 | (type, subtype, params) = parse_mime_type(range) 95 | params.setdefault('q', params.pop('Q', '1')) # q is case insensitive 96 | try: 97 | if not params['q'] or not 0 <= float(params['q']) <= 1: 98 | params['q'] = '1' 99 | except ValueError: # from float() 100 | params['q'] = '1' 101 | 102 | return (type, subtype, params) 103 | 104 | 105 | def quality_and_fitness_parsed( 106 | mime_type: str, 107 | parsed_ranges: Iterable[Tuple[str, str, Dict[str, str]]], 108 | ) -> Tuple[float, float]: 109 | """Find the best match for a mime-type amongst parsed media-ranges. 110 | 111 | Find the best match for a given mime-type against a list of media_ranges 112 | that have already been parsed by parse_media_range(). Returns a tuple of 113 | the fitness value and the value of the 'q' quality parameter of the best 114 | match, or (-1, 0) if no match was found. Just as for quality_parsed(), 115 | 'parsed_ranges' must be a list of parsed media ranges. 116 | """ 117 | best_fitness = -1. 118 | best_fit_q: Union[float, str] = 0. 119 | (target_type, target_subtype, target_params) = \ 120 | parse_media_range(mime_type) 121 | 122 | for (type, subtype, params) in parsed_ranges: 123 | 124 | # check if the type and the subtype match 125 | type_match = type in (target_type, '*') or target_type == '*' 126 | 127 | subtype_match = subtype in (target_subtype, '*') or target_subtype == '*' 128 | 129 | # if they do, assess the "fitness" of this mime_type 130 | if type_match and subtype_match: 131 | 132 | # 100 points if the type matches w/o a wildcard 133 | fitness = type == target_type and 100. or 0. 134 | 135 | # 10 points if the subtype matches w/o a wildcard 136 | fitness += subtype == target_subtype and 10. or 0. 137 | 138 | # 1 bonus point for each matching param besides "q" 139 | param_matches = sum([ 140 | 1 for (key, value) in target_params.items() 141 | if key != 'q' and key in params and value == params[key] 142 | ]) 143 | fitness += param_matches 144 | 145 | # finally, add the target's "q" param (between 0 and 1) 146 | fitness += float(target_params.get('q', 1)) 147 | 148 | if fitness > best_fitness: 149 | best_fitness = fitness 150 | best_fit_q = params['q'] 151 | 152 | return float(best_fit_q), best_fitness 153 | 154 | 155 | def quality_parsed(mime_type: str, parsed_ranges: Iterable[Tuple[str, str, Dict[str, str]]]) -> float: 156 | """Find the best match for a mime-type amongst parsed media-ranges. 157 | 158 | Find the best match for a given mime-type against a list of media_ranges 159 | that have already been parsed by parse_media_range(). Returns the 'q' 160 | quality parameter of the best match, 0 if no match was found. This function 161 | behaves the same as quality() except that 'parsed_ranges' must be a list of 162 | parsed media ranges. 163 | """ 164 | 165 | return quality_and_fitness_parsed(mime_type, parsed_ranges)[0] 166 | 167 | 168 | def quality(mime_type: str, ranges: str) -> float: 169 | """Return the quality ('q') of a mime-type against a list of media-ranges. 170 | 171 | Returns the quality 'q' of a mime-type when compared against the 172 | media-ranges in ranges. For example: 173 | 174 | >>> quality('text/html','text/*;q=0.3, text/html;q=0.7', 175 | text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5') 176 | 0.7 177 | """ 178 | parsed_ranges = [parse_media_range(r) for r in ranges.split(',')] 179 | 180 | return quality_parsed(mime_type, parsed_ranges) 181 | 182 | 183 | def best_match(supported: Iterable[str], header: str) -> str: 184 | """Return mime-type with the highest quality ('q') from list of candidates. 185 | 186 | Takes a list of supported mime-types and finds the best match for all the 187 | media-ranges listed in header. The value of header must be a string that 188 | conforms to the format of the HTTP Accept: header. The value of 'supported' 189 | is a list of mime-types. The list of supported mime-types should be sorted 190 | in order of increasing desirability, in case of a situation where there is 191 | a tie. 192 | 193 | >>> best_match(['application/xbel+xml', 'text/xml'], 194 | 'text/*;q=0.5,*/*; q=0.1') 195 | 'text/xml' 196 | """ 197 | split_header = _filter_blank(header.split(',')) 198 | parsed_header = [parse_media_range(r) for r in split_header] 199 | weighted_matches = [] 200 | pos = 0 201 | for mime_type in supported: 202 | weighted_matches.append(( 203 | quality_and_fitness_parsed(mime_type, parsed_header), 204 | pos, 205 | mime_type 206 | )) 207 | pos += 1 208 | weighted_matches.sort() 209 | 210 | return weighted_matches[-1][0][0] and weighted_matches[-1][2] or '' 211 | 212 | 213 | def _filter_blank(i: Iterable[str]) -> Generator[str, None, None]: 214 | """Return all non-empty items in the list.""" 215 | for s in i: 216 | if s.strip(): 217 | yield s 218 | --------------------------------------------------------------------------------