├── .gitignore ├── LICENSE ├── README.md ├── content_types ├── __init__.py └── py.typed ├── pyproject.toml ├── ruff.toml └── samples └── compare_to_builtin.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | /.idea/ 173 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Michael Kennedy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # content-types 🗃️🔎 3 | 4 | A Python library to map file extensions to MIME types. 5 | It also provides a CLI for quick lookups right from your terminal. 6 | If no known mapping is found, the tool returns `application/octet-stream`. 7 | 8 | Unlike other libraries, this one does **not** try to access the file 9 | or parse the bytes of the file or stream. It just looks at the extension 10 | which is valuable when you don't have access to the file directly. 11 | For example, you know the filename but it is stored in s3 and you don't want 12 | to download it just to fully inspect the file. 13 | 14 | Why not just use Python's built-in `mimetypes`? Or the excellent `python-magic` package? 15 | [See below](#more-correct-than-pythons-mimetypes). 16 | 17 | ## Installation 18 | 19 | ```bash 20 | uv pip install content-types 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```python 26 | import content_types 27 | 28 | # Forward lookup: filename -> MIME type 29 | the_type = content_types.get_content_type("example.jpg") 30 | print(the_type) # "image/jpeg" 31 | 32 | # For very common files, you have shortcuts: 33 | print(f'Content-Type for webp is {content_types.webp}.') 34 | # Content-Type for webp is image/webp. 35 | ``` 36 | 37 | ## CLI 38 | 39 | To use the library as a CLI tool, just install it with **uv** or **pipx**. 40 | 41 | ```bash 42 | uv tool install content-types 43 | ``` 44 | 45 | Now it will be available machine-wide. 46 | 47 | ```bash 48 | content-types example.jpg 49 | 50 | # Outputs image/jpeg 51 | ``` 52 | 53 | ## More correct than Python's `mimetypes` 54 | 55 | When I first learned about Python's mimetypes module, I thought it was exactly what I need. However, 56 | it doesn't have all the MIME types. And, it recommends deprecated, out-of-date answers for very obvious types. 57 | 58 | For example, mimetypes has `.xml` as text/xml where it should be `application/xml` 59 | (see [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types/Common_types)). 60 | 61 | And mimetypes is missing important types such as: 62 | 63 | - .m4v -> video/mp4 64 | - .tgz -> application/gzip 65 | - .flac -> audio/flac 66 | - .epub -> application/epub+zip 67 | - ... 68 | 69 | Here is a full comparison found by running `samples/compare_to_builtin.py`: 70 | 71 | ```text 72 | There are 5 types where mimetypes and content-types disagree 73 | 74 | mimetypes: .wav audio/x-wav, content-types: .wav audio/wav 75 | mimetypes: .obj application/octet-stream, content-types: .obj model/obj 76 | mimetypes: .xml text/xml, content-types: .xml application/xml 77 | mimetypes: .exe application/octet-stream, content-types: .exe application/x-msdownload 78 | mimetypes: .dll application/octet-stream, content-types: .dll application/x-msdownload 79 | 80 | There are 0 types in mimetypes that are not in content-types 81 | 82 | There are 31 types in content-types that are not in mimetypes 83 | 84 | .docx -> application/vnd.openxmlformats-officedocument.wordprocessingml.document 85 | .m4v -> video/mp4 86 | .odp -> application/vnd.oasis.opendocument.presentation 87 | .deb -> application/x-debian-package 88 | .glb -> model/gltf-binary 89 | .php -> application/x-httpd-php 90 | .xlsx -> application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 91 | .woff -> font/woff 92 | .tgz -> application/gzip 93 | .ogg -> audio/ogg 94 | .odt -> application/vnd.oasis.opendocument.text 95 | .wmv -> video/x-ms-wmv 96 | .stl -> model/stl 97 | .ttf -> font/ttf 98 | .flac -> audio/flac 99 | .rar -> application/vnd.rar 100 | .odg -> application/vnd.oasis.opendocument.graphics 101 | .ods -> application/vnd.oasis.opendocument.spreadsheet 102 | .weba -> audio/webm 103 | .gltf -> model/gltf+json 104 | .epub -> application/epub+zip 105 | .m4a -> audio/mp4 106 | .map -> application/json 107 | .pptx -> application/vnd.openxmlformats-officedocument.presentationml.presentation 108 | .woff2 -> font/woff2 109 | .otf -> font/otf 110 | .gz -> application/gzip 111 | .rpm -> application/x-rpm 112 | .7z -> application/x-7z-compressed 113 | .ogv -> video/ogg 114 | .apk -> application/vnd.android.package-archive 115 | ``` 116 | 117 | ## Works when python-magic package doesn't 118 | 119 | Why not the excellent python-magic package? That one works by reading the header bytes of 120 | binary files which requires access to the file data. The whole goal of this project is 121 | to avoid accessing or needing the file data. They are for different use-cases. 122 | 123 | ## Contributing 124 | 125 | Contributions are welcome! Check out [the GitHub repo](https://github.com/mikeckennedy/content-types) 126 | for more details on how to get involved. 127 | -------------------------------------------------------------------------------- /content_types/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | from typing import Dict 4 | 5 | __VERSION__ = '0.2.3' 6 | 7 | # This dictionary maps file extensions (no dot) to the most specific content type. 8 | 9 | # noinspection SpellCheckingInspection 10 | EXTENSION_TO_CONTENT_TYPE: Dict[str, str] = { 11 | # Text 12 | 'txt': 'text/plain', 13 | 'htm': 'text/html', 14 | 'html': 'text/html', 15 | 'css': 'text/css', 16 | 'csv': 'text/csv', 17 | 'tsv': 'text/tab-separated-values', 18 | # JavaScript 19 | 'js': 'text/javascript', 20 | # MJS for ES modules 21 | 'mjs': 'text/javascript', 22 | # JSON 23 | 'json': 'application/json', 24 | 'map': 'application/json', 25 | # XML (keep application/xml) 26 | 'xml': 'application/xml', 27 | # Images 28 | 'jpg': 'image/jpeg', 29 | 'jpeg': 'image/jpeg', 30 | 'png': 'image/png', 31 | 'gif': 'image/gif', 32 | 'bmp': 'image/bmp', 33 | 'webp': 'image/webp', 34 | 'avif': 'image/avif', 35 | # Some new ones: 36 | 'ico': 'image/vnd.microsoft.icon', 37 | 'svg': 'image/svg+xml', 38 | 'tif': 'image/tiff', 39 | 'tiff': 'image/tiff', 40 | 'heic': 'image/heic', # new 41 | 'heif': 'image/heif', # new 42 | 'jpe': 'image/jpeg', # new alias 43 | 'ief': 'image/ief', # new 44 | 'ras': 'image/x-cmu-raster', # new 45 | 'pnm': 'image/x-portable-anymap', 46 | 'pbm': 'image/x-portable-bitmap', 47 | 'pgm': 'image/x-portable-graymap', 48 | 'ppm': 'image/x-portable-pixmap', 49 | 'rgb': 'image/x-rgb', 50 | 'xbm': 'image/x-xbitmap', 51 | 'xpm': 'image/x-xpixmap', 52 | 'xwd': 'image/x-xwindowdump', 53 | # Audio 54 | 'mp3': 'audio/mpeg', 55 | 'ogg': 'audio/ogg', 56 | 'wav': 'audio/wav', 57 | 'aac': 'audio/aac', 58 | 'flac': 'audio/flac', 59 | 'm4a': 'audio/mp4', 60 | 'weba': 'audio/webm', 61 | 'ass': 'audio/aac', 62 | 'adts': 'audio/aac', 63 | 'rst': 'text/x-rst', 64 | 'loas': 'audio/aac', 65 | # New ones: 66 | 'mp2': 'audio/mpeg', # new 67 | 'opus': 'audio/opus', # new 68 | 'aif': 'audio/x-aiff', 69 | 'aifc': 'audio/x-aiff', 70 | 'aiff': 'audio/x-aiff', 71 | 'au': 'audio/basic', 72 | 'snd': 'audio/basic', 73 | 'ra': 'audio/x-pn-realaudio', 74 | # Video 75 | 'mp4': 'video/mp4', 76 | 'm4v': 'video/mp4', 77 | 'mov': 'video/quicktime', 78 | 'avi': 'video/x-msvideo', 79 | 'wmv': 'video/x-ms-wmv', 80 | 'mpg': 'video/mpeg', 81 | 'mpeg': 'video/mpeg', 82 | 'ogv': 'video/ogg', 83 | 'webm': 'video/webm', 84 | # New aliases: 85 | 'm1v': 'video/mpeg', 86 | 'mpa': 'video/mpeg', 87 | 'mpe': 'video/mpeg', 88 | 'qt': 'video/quicktime', 89 | 'movie': 'video/x-sgi-movie', 90 | # 3GP family (prefer official video/*): 91 | '3gp': 'audio/3gpp', 92 | '3gpp': 'audio/3gpp', 93 | '3g2': 'audio/3gpp2', 94 | '3gpp2': 'audio/3gpp2', 95 | # Archives / Packages 96 | 'pdf': 'application/pdf', 97 | 'zip': 'application/zip', 98 | 'gz': 'application/gzip', 99 | 'tgz': 'application/gzip', 100 | 'tar': 'application/x-tar', 101 | '7z': 'application/x-7z-compressed', 102 | 'rar': 'application/vnd.rar', 103 | # Additional 104 | 'bin': 'application/octet-stream', # new explicit 105 | 'a': 'application/octet-stream', 106 | 'so': 'application/octet-stream', 107 | 'o': 'application/octet-stream', 108 | 'obj': 'model/obj', # keep from original (not octet-stream) 109 | 'dll': 'application/x-msdownload', 110 | 'exe': 'application/x-msdownload', 111 | # Some additional archiving/compression tools 112 | 'bcpio': 'application/x-bcpio', 113 | 'cpio': 'application/x-cpio', 114 | 'shar': 'application/x-shar', 115 | 'sv4cpio': 'application/x-sv4cpio', 116 | 'sv4crc': 'application/x-sv4crc', 117 | 'ustar': 'application/x-ustar', 118 | 'src': 'application/x-wais-source', 119 | # Application / Office 120 | 'doc': 'application/msword', 121 | 'xls': 'application/vnd.ms-excel', 122 | 'ppt': 'application/vnd.ms-powerpoint', 123 | 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 124 | 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 125 | 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 126 | # New ones: 127 | 'dot': 'application/msword', 128 | 'wiz': 'application/msword', 129 | 'xlb': 'application/vnd.ms-excel', 130 | 'pot': 'application/vnd.ms-powerpoint', 131 | 'ppa': 'application/vnd.ms-powerpoint', 132 | 'pps': 'application/vnd.ms-powerpoint', 133 | 'pwz': 'application/vnd.ms-powerpoint', 134 | # Additional special apps 135 | 'webmanifest': 'application/manifest+json', 136 | 'nq': 'application/n-quads', 137 | 'nt': 'application/n-triples', 138 | 'oda': 'application/oda', 139 | 'p7c': 'application/pkcs7-mime', 140 | 'ps': 'application/postscript', 141 | 'ai': 'application/postscript', 142 | 'eps': 'application/postscript', 143 | 'trig': 'application/trig', 144 | 'm3u': 'application/vnd.apple.mpegurl', 145 | 'm3u8': 'application/vnd.apple.mpegurl', 146 | 'wasm': 'application/wasm', 147 | 'csh': 'application/x-csh', 148 | 'dvi': 'application/x-dvi', 149 | 'gtar': 'application/x-gtar', 150 | 'hdf': 'application/x-hdf', 151 | 'h5': 'application/x-hdf5', # not in older standard lists but sometimes used 152 | 'latex': 'application/x-latex', 153 | 'mif': 'application/x-mif', 154 | 'cdf': 'application/x-netcdf', 155 | 'nc': 'application/x-netcdf', 156 | 'p12': 'application/x-pkcs12', 157 | 'pfx': 'application/x-pkcs12', 158 | 'ram': 'application/x-pn-realaudio', 159 | 'pyc': 'application/x-python-code', 160 | 'pyo': 'application/x-python-code', 161 | 'swf': 'application/x-shockwave-flash', 162 | 'tcl': 'application/x-tcl', 163 | 'tex': 'application/x-tex', 164 | 'texi': 'application/x-texinfo', 165 | 'texinfo': 'application/x-texinfo', 166 | 'roff': 'application/x-troff', 167 | 't': 'application/x-troff', 168 | 'tr': 'application/x-troff', 169 | 'man': 'application/x-troff-man', 170 | 'me': 'application/x-troff-me', 171 | 'ms': 'application/x-troff-ms', 172 | # More XML-based 173 | 'xsl': 'application/xml', 174 | 'rdf': 'application/xml', 175 | 'wsdl': 'application/xml', 176 | 'xpdl': 'application/xml', 177 | # ODF 178 | 'odt': 'application/vnd.oasis.opendocument.text', 179 | 'ods': 'application/vnd.oasis.opendocument.spreadsheet', 180 | 'odp': 'application/vnd.oasis.opendocument.presentation', 181 | 'odg': 'application/vnd.oasis.opendocument.graphics', 182 | # Fonts 183 | 'otf': 'font/otf', 184 | 'ttf': 'font/ttf', 185 | 'woff': 'font/woff', 186 | 'woff2': 'font/woff2', 187 | # 3D 188 | 'gltf': 'model/gltf+json', 189 | 'glb': 'model/gltf-binary', 190 | 'stl': 'model/stl', 191 | # Scripts / Misc 192 | 'sh': 'application/x-sh', 193 | 'php': 'application/x-httpd-php', 194 | # Code files 195 | 'py': 'text/x-python', # new (rather than text/plain) 196 | 'c': 'text/plain', # some prefer text/x-c; we’ll keep text/plain 197 | 'h': 'text/plain', 198 | 'ksh': 'text/plain', 199 | 'pl': 'text/plain', 200 | 'bat': 'text/plain', 201 | # Packages etc. 202 | 'apk': 'application/vnd.android.package-archive', 203 | 'deb': 'application/x-debian-package', 204 | 'rpm': 'application/x-rpm', 205 | # Messages 206 | 'eml': 'message/rfc822', 207 | 'mht': 'message/rfc822', 208 | 'mhtml': 'message/rfc822', 209 | 'nws': 'message/rfc822', 210 | # Markdown / Markup 211 | 'md': 'text/markdown', 212 | 'markdown': 'text/markdown', 213 | # RDF-ish / text-ish 214 | 'n3': 'text/n3', 215 | 'rtx': 'text/richtext', 216 | 'rtf': 'text/rtf', 217 | 'srt': 'text/plain', 218 | 'vtt': 'text/vtt', 219 | 'etx': 'text/x-setext', 220 | 'sgm': 'text/x-sgml', 221 | 'sgml': 'text/x-sgml', 222 | 'vcf': 'text/x-vcard', 223 | # Books 224 | 'epub': 'application/epub+zip', 225 | } 226 | 227 | 228 | def get_content_type(filename_or_extension: str | Path, treat_as_binary: bool = True) -> str: 229 | """ 230 | Given a filename (or just an extension), return the most specific, 231 | commonly accepted MIME type based on extension. 232 | 233 | Falls back to 'application/octet-stream' if `treat_as_binary` is True (default) and 'text/plain' if it is 234 | False when the extension is not known. 235 | 236 | Example: 237 | >>> get_content_type("picture.jpg") 238 | 'image/jpeg' 239 | >>> get_content_type(".webp") 240 | 'image/webp' 241 | >>> get_content_type("script.js") 242 | 'application/javascript' 243 | >>> get_content_type("unknown.xyz") 244 | 'application/octet-stream' 245 | >>> get_content_type("unknown.xyz", treat_as_binary=False) 246 | 'text/plain' 247 | """ 248 | 249 | if filename_or_extension is None: 250 | raise Exception('filename cannot be None.') 251 | 252 | if isinstance(filename_or_extension, Path): 253 | filename_or_extension = filename_or_extension.suffix 254 | 255 | if '.' not in filename_or_extension: 256 | filename_or_extension = f'.{filename_or_extension}' 257 | 258 | # Split by dot, take the last part as extension 259 | # e.g., "archive.tar.gz" => "gz" 260 | # Also handle cases like ".webp" => "webp" 261 | dot_parts = filename_or_extension.lower().split('.') 262 | ext = dot_parts[-1] if len(dot_parts) > 1 else '' 263 | 264 | if treat_as_binary: 265 | return EXTENSION_TO_CONTENT_TYPE.get(ext, 'application/octet-stream') 266 | 267 | return EXTENSION_TO_CONTENT_TYPE.get(ext, 'text/plain') 268 | 269 | 270 | webp: str = get_content_type('.webp') 271 | png: str = get_content_type('.png') 272 | jpg: str = get_content_type('.jpg') 273 | mp3: str = get_content_type('.mp3') 274 | json: str = get_content_type('.json') 275 | pdf: str = get_content_type('.pdf') 276 | zip: str = get_content_type('.zip') # noqa == it's fine to overwrite zip() in this module only. 277 | xml: str = get_content_type('.xml') 278 | csv: str = get_content_type('.csv') 279 | md: str = get_content_type('.md') 280 | 281 | 282 | def cli(): 283 | """ 284 | A simple CLI to look up the MIME type for a given filename or extension. 285 | Install via uv tool install content-types 286 | Usage example : 287 | content-types my_file.jpg 288 | """ 289 | if len(sys.argv) < 2: 290 | print('Usage: contenttypes [FILENAME_OR_EXTENSION]\nExample: contenttypes .jpg') 291 | sys.exit(1) 292 | 293 | filename = sys.argv[1] 294 | mime_type = get_content_type(filename) 295 | print(mime_type) 296 | -------------------------------------------------------------------------------- /content_types/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/content-types/57e79e7fc23ae0c14d59629183c52e42362a039f/content_types/py.typed -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling>=1.0"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "content-types" 7 | version = "0.2.3" 8 | description = "A library to map file extensions to content types and vice versa." 9 | readme = "README.md" 10 | license = "MIT" 11 | authors = [ 12 | { name = "Michael Kennedy", email = "mikeckennedy@gmail.com" } 13 | ] 14 | requires-python = ">=3.10, <=3.14" 15 | keywords = ["mime", "content-type", "mapping", "file extensions"] 16 | homepage = "https://github.com/mikeckennedy/content-types" 17 | 18 | # If you have no runtime dependencies, this can be an empty array or omitted 19 | dependencies = [] 20 | 21 | classifiers = [ 22 | "License :: OSI Approved :: MIT License", 23 | "Intended Audience :: Developers", 24 | "Operating System :: OS Independent", 25 | "Programming Language :: Python :: 3.10", 26 | "Programming Language :: Python :: 3.11", 27 | "Programming Language :: Python :: 3.12", 28 | "Programming Language :: Python :: 3.13", 29 | "Programming Language :: Python :: 3.14", 30 | "Topic :: Internet :: WWW/HTTP", 31 | "Topic :: Software Development :: Libraries :: Python Modules" 32 | ] 33 | 34 | [project.urls] 35 | "Homepage" = "https://github.com/mikeckennedy/content-types" 36 | "Bug Reports" = "https://github.com/mikeckennedy/content-types/issues" 37 | "Source" = "https://github.com/mikeckennedy/content-types" 38 | 39 | [project.scripts] 40 | content-types = "content_types:cli" 41 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | # [ruff] 2 | line-length = 120 3 | format.quote-style = "single" 4 | 5 | # Enable Pyflakes `E` and `F` codes by default. 6 | lint.select = ["E", "F"] 7 | lint.ignore = [] 8 | 9 | # Exclude a variety of commonly ignored directories. 10 | exclude = [ 11 | ".bzr", 12 | ".direnv", 13 | ".eggs", 14 | ".git", 15 | ".hg", 16 | ".mypy_cache", 17 | ".nox", 18 | ".pants.d", 19 | ".ruff_cache", 20 | ".svn", 21 | ".tox", 22 | "__pypackages__", 23 | "_build", 24 | "buck-out", 25 | "build", 26 | "dist", 27 | "node_modules", 28 | ".env", 29 | ".venv", 30 | "venv", 31 | ] 32 | lint.per-file-ignores = {} 33 | 34 | # Allow unused variables when underscore-prefixed. 35 | # dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 36 | 37 | # Assume Python 3.13. 38 | target-version = "py313" 39 | 40 | #[tool.ruff.mccabe] 41 | ## Unlike Flake8, default to a complexity level of 10. 42 | lint.mccabe.max-complexity = 10 43 | 44 | -------------------------------------------------------------------------------- /samples/compare_to_builtin.py: -------------------------------------------------------------------------------- 1 | import mimetypes 2 | 3 | import content_types 4 | 5 | 6 | def main(): 7 | print('Compare types in mimetypes vs content-types.') 8 | in_mime_only = set() 9 | differ = set() 10 | for k, v in mimetypes.types_map.items(): 11 | cv_v = content_types.EXTENSION_TO_CONTENT_TYPE.get(k.lower().strip('.')) 12 | if not cv_v: 13 | in_mime_only.add((k, v)) 14 | continue 15 | 16 | if cv_v != v: 17 | differ.add(((k, v), (k, cv_v))) 18 | continue 19 | 20 | only_ct = set() 21 | for k, v in content_types.EXTENSION_TO_CONTENT_TYPE.items(): 22 | mv = mimetypes.types_map.get('.' + k) 23 | if not mv: 24 | only_ct.add((k, v)) 25 | continue 26 | 27 | print(f'There are {len(differ):,} types where mimetypes and content-types disagree') 28 | for (mk, mv), (ct_k, ct_v) in sorted(differ): 29 | print(f'mimetypes: {mk} {mv}, content-types: {ct_k} {ct_v}') 30 | print() 31 | 32 | print(f'There are {len(in_mime_only):,} types in mimetypes that are not in content-types') 33 | for k, v in sorted(in_mime_only): 34 | print(f'{k.ljust(5)}: {v}') 35 | print() 36 | 37 | print(f'There are {len(only_ct):,} types in content-types that are not in mimetypes') 38 | for k, v in sorted(only_ct): 39 | print(f'.{k.ljust(5)} -> {v}') 40 | print() 41 | 42 | 43 | if __name__ == '__main__': 44 | main() 45 | --------------------------------------------------------------------------------