Coverage for gws-app/gws/lib/mime/__init__.py: 98%
105 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
1"""MIME types.
3Defines constants for the MIME types used in the application (``PNG``, ``JSON``, ``GML3``
4and so on) and functions to normalize MIME types and to map them to and from file extensions.
6All MIME types in the application go through ``get``. It accepts a MIME type in any spelling
7(case, spaces, quoted parameter values), a content type with parameters, a known alias (e.g.
8``application/vnd.ogc.gml`` or ``image/png8``) or a file extension, and returns the normalized
9MIME type with its parameters: lower case, ``"; "`` between parameters, known types and aliases
10mapped to the constants of this module (``application/gml+xml; version=3``). ``get_base`` returns
11it without parameters. Lookups that are not covered by the built-in tables fall back to the
12standard ``mimetypes`` module; unknown MIME types give an empty string.
14Example::
16 gws.lib.mime.get('image/jpg') # 'image/jpeg'
17 gws.lib.mime.get('png') # 'image/png'
18 gws.lib.mime.get('image/png8') # 'image/png; mode=8bit'
19 gws.lib.mime.get_base('image/png8') # 'image/png'
20 gws.lib.mime.for_path('/data/report.pdf') # 'application/pdf'
21 gws.lib.mime.extension_for(gws.lib.mime.JPEG) # 'jpeg'
22"""
24import mimetypes
26BIN = 'application/octet-stream'
27CSS = 'text/css'
28CSV = 'text/csv'
29DOC = 'application/msword'
30DOCX = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
31FORM = 'application/x-www-form-urlencoded'
32GEOJSON = 'application/geo+json'
33GIF = 'image/gif'
34GML = 'application/gml+xml'
35GML2 = 'application/gml+xml; version=2'
36GML3 = 'application/gml+xml; version=3'
37GML32 = 'application/gml+xml; version=3.2'
38GPKG = 'application/geopackage+sqlite3'
39GZIP = 'application/gzip'
40HTML = 'text/html'
41JPEG = 'image/jpeg'
42JPEG2000 = 'image/jp2'
43JS = 'text/javascript'
44JSON = 'application/json'
45KML = 'application/vnd.google-earth.kml+xml'
46KMZ = 'application/vnd.google-earth.kmz'
47MSGPACK = 'application/msgpack'
48MULTIPART = 'multipart/form-data'
49MVT = 'application/vnd.mapbox-vector-tile'
50PDF = 'application/pdf'
51PNG = 'image/png'
52PNG8 = 'image/png; mode=8bit'
53PNG24 = 'image/png; mode=32bit'
54PPT = 'application/vnd.ms-powerpoint'
55PPTX = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
56SVG = 'image/svg+xml'
57TIFF = 'image/tiff'
58TTF = 'font/ttf'
59TXT = 'text/plain'
60WEBP = 'image/webp'
61WOFF2 = 'font/woff2'
62XLS = 'application/vnd.ms-excel'
63XLSX = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
64XML = 'text/xml'
65ZIP = 'application/zip'
68_common = {globals()[k] for k in list(globals()) if k.isupper()}
70_common_extensions = {
71 'css': CSS,
72 'csv': CSV,
73 'doc': DOC,
74 'docx': DOCX,
75 'geojson': GEOJSON,
76 'gif': GIF,
77 'gml': GML,
78 'gml2': GML2,
79 'gpkg': GPKG,
80 'html': HTML,
81 'jpeg': JPEG,
82 'jpg': JPEG,
83 'js': JS,
84 'json': JSON,
85 'kml': KML,
86 'kmz': KMZ,
87 'mvt': MVT,
88 'pdf': PDF,
89 'png': PNG,
90 'ppt': PPT,
91 'pptx': PPTX,
92 'svg': SVG,
93 'tif': TIFF,
94 'tiff': TIFF,
95 'ttf': TTF,
96 'txt': TXT,
97 'webp': WEBP,
98 'woff2': WOFF2,
99 'xls': XLS,
100 'xlsx': XLSX,
101 'xml': XML,
102 'zip': ZIP,
103}
105_aliases = {
106 'application/gml:3': GML,
107 'application/html': HTML,
108 'application/javascript': JS,
109 'application/json; subtype=geojson': GEOJSON,
110 'application/vnd.geo+json': GEOJSON,
111 'application/vnd.ogc.csw_xml': XML,
112 'application/vnd.ogc.gml': GML2,
113 'application/vnd.ogc.gml/3.1.1': GML3,
114 'application/vnd.ogc.se_xml': XML,
115 'application/vnd.ogc.wms_xml': XML,
116 'application/x-font-ttf': TTF,
117 'application/x-gzip': GZIP,
118 'application/x-pdf': PDF,
119 'application/x-protobuf': MVT,
120 'application/xml': XML,
121 'application/xml; subtype=gml/2': GML2,
122 'application/xml; subtype=gml/3': GML,
123 'application/xml; subtype=gml/3.1.1': GML3,
124 'image/geotiff': TIFF,
125 'image/jpg': JPEG,
126 'image/png32': PNG24,
127 'image/png8': PNG8,
128 'image/svg': SVG,
129 'text/xhtml': HTML,
130 'text/xml; subtype=gml/3.1.1': GML3,
131}
134def get(s: str) -> str:
135 """Return the normalized MIME type, keeping its parameters.
137 Known types and aliases are mapped to the constants of this module (``image/png8`` to ``PNG8``).
138 Other parameters are kept: ``text/html;Charset=utf-8`` gives ``text/html; charset=utf-8``.
140 Args:
141 s: MIME type in any spelling, content type, alias or file extension.
143 Returns:
144 The normalized MIME type, or an empty string if it is unknown.
145 """
147 if not s:
148 return ''
150 mime = _get_quick(s)
151 if mime:
152 return mime
154 s = _normalize(s)
156 mime = _get_quick(s)
157 if mime:
158 return mime
160 base, sep, params = s.partition('; ')
162 if sep:
163 mime = _get_quick(base)
164 if mime:
165 return mime if ';' in mime else mime + '; ' + params
166 if mimetypes.guess_extension(base):
167 return s
168 return ''
170 if '/' in s and mimetypes.guess_extension(s):
171 return s
173 mime, _ = mimetypes.guess_type('x.' + s)
174 if mime:
175 return mime
177 return ''
180def get_base(s: str) -> str:
181 """Return the normalized MIME type without parameters.
183 Parameters are removed before and after the lookup, so that unknown parameters
184 do not make a known type unknown.
186 Example::
188 get_base('image/png8') # 'image/png'
189 get_base('application/vnd.ogc.gml') # 'application/gml+xml'
191 Args:
192 s: MIME type in any spelling, content type, alias or file extension.
194 Returns:
195 The normalized MIME type without parameters, or an empty string if it is unknown.
196 """
198 mime = get(s.split(';')[0].strip())
199 return mime.split(';')[0]
202def _normalize(s: str) -> str:
203 """Spell a MIME type canonically (RFC 9110, 8.3.1): lower-case names, no quotes, ``"; "`` between parameters."""
205 parts = s.split(';')
206 res = [parts[0].replace(' ', '').lower()]
207 for p in parts[1:]:
208 name, _, value = p.partition('=')
209 name = name.strip().lower()
210 if not name:
211 continue
212 value = value.strip()
213 if len(value) >= 2 and value[0] == '"' and value[-1] == '"':
214 value = value[1:-1]
215 res.append(f'{name}={value}')
216 return '; '.join(res)
219def _get_quick(s):
220 """Look up a MIME type, an extension or an alias in the built-in tables."""
221 if s in _common:
222 return s
223 if s in _common_extensions:
224 return _common_extensions[s]
225 if s in _aliases:
226 return _aliases[s]
229def for_path(path: str) -> str:
230 """Return the MIME type for a file path, based on its extension.
232 Args:
233 path: File path or name.
235 Returns:
236 The MIME type, or ``BIN`` if it is unknown.
237 """
238 _, _, e = path.rpartition('.')
239 if e in _common_extensions:
240 return _common_extensions[e]
241 t, _ = mimetypes.guess_type(path)
242 return t or BIN
245def extension_for(mime: str, default: str = 'bin') -> str:
246 """Return the file extension for a MIME type.
248 Args:
249 mime: Normalized MIME type.
250 default: Extension to return if the MIME type is unknown.
252 Returns:
253 The extension, without a dot.
254 """
256 for ext, rt in _common_extensions.items():
257 if rt == mime:
258 return ext
259 s = mimetypes.guess_extension(mime)
260 if s:
261 return s[1:]
262 return default