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

1"""MIME types. 

2 

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. 

5 

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. 

13 

14Example:: 

15 

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""" 

23 

24import mimetypes 

25 

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' 

66 

67 

68_common = {globals()[k] for k in list(globals()) if k.isupper()} 

69 

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} 

104 

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} 

132 

133 

134def get(s: str) -> str: 

135 """Return the normalized MIME type, keeping its parameters. 

136 

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``. 

139 

140 Args: 

141 s: MIME type in any spelling, content type, alias or file extension. 

142 

143 Returns: 

144 The normalized MIME type, or an empty string if it is unknown. 

145 """ 

146 

147 if not s: 

148 return '' 

149 

150 mime = _get_quick(s) 

151 if mime: 

152 return mime 

153 

154 s = _normalize(s) 

155 

156 mime = _get_quick(s) 

157 if mime: 

158 return mime 

159 

160 base, sep, params = s.partition('; ') 

161 

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 '' 

169 

170 if '/' in s and mimetypes.guess_extension(s): 

171 return s 

172 

173 mime, _ = mimetypes.guess_type('x.' + s) 

174 if mime: 

175 return mime 

176 

177 return '' 

178 

179 

180def get_base(s: str) -> str: 

181 """Return the normalized MIME type without parameters. 

182 

183 Parameters are removed before and after the lookup, so that unknown parameters 

184 do not make a known type unknown. 

185 

186 Example:: 

187 

188 get_base('image/png8') # 'image/png' 

189 get_base('application/vnd.ogc.gml') # 'application/gml+xml' 

190 

191 Args: 

192 s: MIME type in any spelling, content type, alias or file extension. 

193 

194 Returns: 

195 The normalized MIME type without parameters, or an empty string if it is unknown. 

196 """ 

197 

198 mime = get(s.split(';')[0].strip()) 

199 return mime.split(';')[0] 

200 

201 

202def _normalize(s: str) -> str: 

203 """Spell a MIME type canonically (RFC 9110, 8.3.1): lower-case names, no quotes, ``"; "`` between parameters.""" 

204 

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) 

217 

218 

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] 

227 

228 

229def for_path(path: str) -> str: 

230 """Return the MIME type for a file path, based on its extension. 

231 

232 Args: 

233 path: File path or name. 

234 

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 

243 

244 

245def extension_for(mime: str, default: str = 'bin') -> str: 

246 """Return the file extension for a MIME type. 

247 

248 Args: 

249 mime: Normalized MIME type. 

250 default: Extension to return if the MIME type is unknown. 

251 

252 Returns: 

253 The extension, without a dot. 

254 """ 

255 

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