Coverage for src/aiocoap/numbers/contentformat.py: 0%

95 statements  

« prev     ^ index     » next       coverage.py v7.7.0, created at 2025-03-20 17:26 +0000

1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors 

2# 

3# SPDX-License-Identifier: MIT 

4 

5"""Module containing the CoRE parameters / CoAP Content-Formats registry""" 

6 

7from __future__ import annotations 

8 

9from typing import Dict, Tuple 

10 

11from ..util import ExtensibleIntEnum, ExtensibleEnumMeta 

12import warnings 

13 

14# _raw can be updated from: `curl https://www.iana.org/assignments/core-parameters/content-formats.csv | python3 -c 'import csv, sys; print(list(csv.reader(sys.stdin))[1:])'` 

15 

16# fmt: off 

17_raw = [ 

18 ['text/plain; charset=utf-8', '', '0', '[RFC2046][RFC3676][RFC5147]'], 

19 ['Unassigned', '', '1-15', ''], 

20 ['application/cose; cose-type="cose-encrypt0"', '', '16', '[RFC-ietf-cose-rfc8152bis-struct-15]'], 

21 ['application/cose; cose-type="cose-mac0"', '', '17', '[RFC-ietf-cose-rfc8152bis-struct-15]'], 

22 ['application/cose; cose-type="cose-sign1"', '', '18', '[RFC-ietf-cose-rfc8152bis-struct-15]'], 

23 ['application/ace+cbor', '', '19', '[RFC-ietf-ace-oauth-authz-46]'], 

24 ['Unassigned', '', '20', ''], 

25 ['image/gif', '', '21', '[https://www.w3.org/Graphics/GIF/spec-gif89a.txt]'], 

26 ['image/jpeg', '', '22', '[ISO/IEC 10918-5]'], 

27 ['image/png', '', '23', '[RFC2083]'], 

28 ['Unassigned', '', '24-39', ''], 

29 ['application/link-format', '', '40', '[RFC6690]'], 

30 ['application/xml', '', '41', '[RFC3023]'], 

31 ['application/octet-stream', '', '42', '[RFC2045][RFC2046]'], 

32 ['Unassigned', '', '43-46', ''], 

33 ['application/exi', '', '47', '["Efficient XML Interchange (EXI) Format 1.0 (Second Edition)", February 2014]'], 

34 ['Unassigned', '', '48-49', ''], 

35 ['application/json', '', '50', '[RFC8259]'], 

36 ['application/json-patch+json', '', '51', '[RFC6902]'], 

37 ['application/merge-patch+json', '', '52', '[RFC7396]'], 

38 ['Unassigned', '', '53-59', ''], 

39 ['application/cbor', '', '60', '[RFC8949]'], 

40 ['application/cwt', '', '61', '[RFC8392]'], 

41 ['application/multipart-core', '', '62', '[RFC8710]'], 

42 ['application/cbor-seq', '', '63', '[RFC8742]'], 

43 ['Unassigned', '', '64-95', ''], 

44 ['application/cose; cose-type="cose-encrypt"', '', '96', '[RFC-ietf-cose-rfc8152bis-struct-15]'], 

45 ['application/cose; cose-type="cose-mac"', '', '97', '[RFC-ietf-cose-rfc8152bis-struct-15]'], 

46 ['application/cose; cose-type="cose-sign"', '', '98', '[RFC-ietf-cose-rfc8152bis-struct-15]'], 

47 ['Unassigned', '', '99-100', ''], 

48 ['application/cose-key', '', '101', '[RFC-ietf-cose-rfc8152bis-struct-15]'], 

49 ['application/cose-key-set', '', '102', '[RFC-ietf-cose-rfc8152bis-struct-15]'], 

50 ['Unassigned', '', '103-109', ''], 

51 ['application/senml+json', '', '110', '[RFC8428]'], 

52 ['application/sensml+json', '', '111', '[RFC8428]'], 

53 ['application/senml+cbor', '', '112', '[RFC8428]'], 

54 ['application/sensml+cbor', '', '113', '[RFC8428]'], 

55 ['application/senml-exi', '', '114', '[RFC8428]'], 

56 ['application/sensml-exi', '', '115', '[RFC8428]'], 

57 ['Unassigned', '', '116-139', ''], 

58 ['application/yang-data+cbor; id=sid', '', '140', '[RFC9254]'], 

59 ['Unassigned', '', '141-255', ''], 

60 ['application/coap-group+json', '', '256', '[RFC7390]'], 

61 ['application/concise-problem-details+cbor', '', '257', '[RFC-ietf-core-problem-details-08]'], 

62 ['application/swid+cbor', '', '258', '[RFC-ietf-sacm-coswid-22]'], 

63 ['Unassigned', '', '259-270', ''], 

64 ['application/dots+cbor', '', '271', '[RFC9132]'], 

65 ['application/missing-blocks+cbor-seq', '', '272', '[RFC9177]'], 

66 ['Unassigned', '', '273-279', ''], 

67 ['application/pkcs7-mime; smime-type=server-generated-key', '', '280', '[RFC7030][RFC8551][RFC9148]'], 

68 ['application/pkcs7-mime; smime-type=certs-only', '', '281', '[RFC8551][RFC9148]'], 

69 ['Unassigned', '', '282-283', ''], 

70 ['application/pkcs8', '', '284', '[RFC5958][RFC8551][RFC9148]'], 

71 ['application/csrattrs', '', '285', '[RFC7030][RFC9148]'], 

72 ['application/pkcs10', '', '286', '[RFC5967][RFC8551][RFC9148]'], 

73 ['application/pkix-cert', '', '287', '[RFC2585][RFC9148]'], 

74 ['Unassigned', '', '288-289', ''], 

75 ['application/aif+cbor', '', '290', '[RFC-ietf-ace-aif-07]'], 

76 ['application/aif+json', '', '291', '[RFC-ietf-ace-aif-07]'], 

77 ['Unassigned', '', '292-309', ''], 

78 ['application/senml+xml', '', '310', '[RFC8428]'], 

79 ['application/sensml+xml', '', '311', '[RFC8428]'], 

80 ['Unassigned', '', '312-319', ''], 

81 ['application/senml-etch+json', '', '320', '[RFC8790]'], 

82 ['Unassigned', '', '321', ''], 

83 ['application/senml-etch+cbor', '', '322', '[RFC8790]'], 

84 ['Unassigned', '', '323-339', ''], 

85 ['application/yang-data+cbor', '', '340', '[RFC9254]'], 

86 ['application/yang-data+cbor; id=name', '', '341', '[RFC9254]'], 

87 ['Unassigned', '', '342-431', ''], 

88 ['application/td+json', '', '432', '["Web of Things (WoT) Thing Description", May 2019]'], 

89 ['Unassigned', '', '433-835', ''], 

90 ['application/voucher-cose+cbor (TEMPORARY - registered 2022-04-12, expires 2023-04-12)', '', '836', '[draft-ietf-anima-constrained-voucher-17]'], 

91 ['Unassigned', '', '837-1541', ''], 

92 ['Reserved, do not use', '', '1542-1543', '[OMA-TS-LightweightM2M-V1_0]'], 

93 ['Unassigned', '', '1544-9999', ''], 

94 ['application/vnd.ocf+cbor', '', '10000', '[Michael_Koster]'], 

95 ['application/oscore', '', '10001', '[RFC8613]'], 

96 ['application/javascript', '', '10002', '[RFC4329]'], 

97 ['Unassigned', '', '10003-11049', ''], 

98 ['application/json', 'deflate', '11050', '[RFC8259]'], 

99 ['Unassigned', '', '11051-11059', ''], 

100 ['application/cbor', 'deflate', '11060', '[RFC8949]'], 

101 ['Unassigned', '', '11061-11541', ''], 

102 ['application/vnd.oma.lwm2m+tlv', '', '11542', '[OMA-TS-LightweightM2M-V1_0]'], 

103 ['application/vnd.oma.lwm2m+json', '', '11543', '[OMA-TS-LightweightM2M-V1_0]'], 

104 ['application/vnd.oma.lwm2m+cbor', '', '11544', '[OMA-TS-LightweightM2M-V1_2]'], 

105 ['Unassigned', '', '11545-19999', ''], 

106 ['text/css', '', '20000', '[RFC2318]'], 

107 ['Unassigned', '', '20001-29999', ''], 

108 ['image/svg+xml', '', '30000', '[https://www.w3.org/TR/SVG/mimereg.html]'], 

109 ['Unassigned', '', '30001-64999', ''], 

110 ['Reserved for Experimental Use', '', '65000-65535', '[RFC7252]'], 

111 ] 

112# fmt: on 

113 

114 

115def _normalize_media_type(s): 

116 """Strip out the white space between parameters; doesn't need to fully 

117 parse the types because it's applied to values of _raw (or to input that'll 

118 eventually be compared to them and fail)""" 

119 return s.replace("; ", ";") 

120 

121 

122class ContentFormatMeta(ExtensibleEnumMeta): 

123 def __init__(self, name, bases, dict) -> None: 

124 super().__init__(name, bases, dict) 

125 

126 # If this were part of the class definition, it would be taken up as an 

127 # enum instance; hoisting it to the metaclass avoids that special 

128 # treatment. 

129 self._by_mt_encoding: Dict[Tuple[str, str], "ContentFormat"] = {} 

130 

131 

132class ContentFormat(ExtensibleIntEnum, metaclass=ContentFormatMeta): 

133 """Entry in the `CoAP Content-Formats registry`__ of the IANA Constrained 

134 RESTful Environments (Core) Parameters group 

135 

136 .. __: https://www.iana.org/assignments/core-parameters/core-parameters.xhtml#content-formats 

137 

138 Known entries have ``.media_type`` and ``.encoding`` attributes: 

139 

140 >>> ContentFormat(0).media_type 

141 'text/plain; charset=utf-8' 

142 >>> int(ContentFormat.by_media_type('text/plain;charset=utf-8')) 

143 0 

144 >>> ContentFormat(60) 

145 <ContentFormat 60, media_type='application/cbor'> 

146 >>> ContentFormat(11060).encoding 

147 'deflate' 

148 

149 Unknown entries do not have these properties: 

150 

151 >>> ContentFormat(12345).is_known() 

152 False 

153 >>> ContentFormat(12345).media_type # doctest: +ELLIPSIS 

154 Traceback (most recent call last): 

155 ... 

156 AttributeError: ... 

157 

158 Only a few formats are available as attributes for easy access. Their 

159 selection and naming are arbitrary and biased. The remaining known types 

160 are available through the :meth:`by_media_type` class method. 

161 >>> ContentFormat.TEXT 

162 <ContentFormat 0, media_type='text/plain; charset=utf-8'> 

163 

164 A convenient property of ContentFormat is that any content format is 

165 true in a boolean context, and thus when used in alternation with None, can 

166 be assigned defaults easily: 

167 

168 >>> requested_by_client = ContentFormat.TEXT 

169 >>> int(requested_by_client) # Usually, this would always pick the default 

170 0 

171 >>> used = requested_by_client or ContentFormat.LINKFORMAT 

172 >>> assert used == ContentFormat.TEXT 

173 """ 

174 

175 @classmethod 

176 def define(cls, number, media_type: str, encoding: str = "identity"): 

177 s = cls(number) 

178 

179 if hasattr(s, "media_type"): 

180 warnings.warn( 

181 "Redefining media type is a compatibility hazard, but allowed for experimental purposes" 

182 ) 

183 

184 s._media_type = media_type 

185 s._encoding = encoding 

186 

187 cls._by_mt_encoding[(_normalize_media_type(media_type), encoding)] = s 

188 

189 @classmethod 

190 def by_media_type( 

191 cls, media_type: str, encoding: str = "identity" 

192 ) -> ContentFormat: 

193 """Produce known entry for a known media type (and encoding, though 

194 'identity' is default due to its prevalence), or raise KeyError.""" 

195 return cls._by_mt_encoding[(_normalize_media_type(media_type), encoding)] 

196 

197 def is_known(self): 

198 return hasattr(self, "media_type") 

199 

200 @property 

201 def media_type(self) -> str: 

202 return self._media_type 

203 

204 @media_type.setter 

205 def media_type(self, media_type: str) -> None: 

206 warnings.warn( 

207 "Setting media_type or encoding is deprecated, use ContentFormat.define(media_type, encoding) instead.", 

208 DeprecationWarning, 

209 stacklevel=1, 

210 ) 

211 self._media_type = media_type 

212 

213 @property 

214 def encoding(self) -> str: 

215 return self._encoding 

216 

217 @encoding.setter 

218 def encoding(self, encoding: str) -> None: 

219 warnings.warn( 

220 "Setting media_type or encoding is deprecated, use ContentFormat(number, media_type, encoding) instead.", 

221 DeprecationWarning, 

222 stacklevel=1, 

223 ) 

224 self._encoding = encoding 

225 

226 @classmethod 

227 def _rehash(cls): 

228 """Update the class's cache of known media types 

229 

230 Run this after having created entries with media type and encoding that 

231 should be found later on.""" 

232 # showing as a deprecation even though it is a private function because 

233 # altering media_type/encoding required users to call this. 

234 warnings.warn( 

235 "This function is not needed when defining a content type through `.define()` rather than by setting media_type and encoding.", 

236 DeprecationWarning, 

237 stacklevel=1, 

238 ) 

239 cls._by_mt_encoding = { 

240 (_normalize_media_type(c.media_type), c.encoding): c 

241 for c in cls._value2member_map_.values() 

242 } 

243 

244 def __repr__(self): 

245 parts = [] 

246 if self.is_known(): 

247 parts.append(f", media_type={self.media_type!r}") 

248 if self.encoding != "identity": 

249 parts.append(f", encoding={self.encoding!r}") 

250 return "<%s %d%s>" % ( 

251 type(self).__name__, 

252 self, 

253 "".join(parts), 

254 ) 

255 

256 def __bool__(self): 

257 return True 

258 

259 def _repr_html_(self): 

260 # The name with title thing isn't too pretty for these ones 

261 if self.is_known(): 

262 import html 

263 

264 return f"""<abbr title="Content format {int(self)}{", named ContentFormat." + html.escape(self.name) if hasattr(self, "name") else ""}">{html.escape(self.media_type)}{"@" + self.encoding if self.encoding != "identity" else ""}</abbr>""" 

265 else: 

266 return f"""<abbr title="Unknown content format">{int(self)}</abbr>""" 

267 

268 TEXT = 0 

269 LINKFORMAT = 40 

270 OCTETSTREAM = 42 

271 JSON = 50 

272 CBOR = 60 

273 SENML = 112 

274 

275 

276for _mt, _enc, _i, _source in _raw: 

277 if _mt in ["Reserved for Experimental Use", "Reserved, do not use", "Unassigned"]: 

278 continue 

279 _mt, _, _ = _mt.partition(" (TEMPORARY") 

280 ContentFormat.define(int(_i), _mt, _enc or "identity") 

281 

282 

283class _MediaTypes: 

284 """Wrapper to provide a media_types indexable object as was present up to 

285 0.4.2""" 

286 

287 def __getitem__(self, content_format): 

288 warnings.warn( 

289 "media_types is deprecated, please use aiocoap.numbers.ContentFormat", 

290 DeprecationWarning, 

291 stacklevel=2, 

292 ) 

293 if content_format is None: 

294 # That was a convenient idiom to short-circuit through, but would 

295 # fail through the constructor 

296 raise KeyError(None) 

297 

298 cf = ContentFormat(content_format) 

299 if cf.is_known(): 

300 return _normalize_media_type(cf.media_type) 

301 else: 

302 raise KeyError(content_format) 

303 

304 def get(self, content_format, default=None): 

305 warnings.warn( 

306 "media_types is deprecated, please use aiocoap.numbers.ContentFormat", 

307 DeprecationWarning, 

308 stacklevel=2, 

309 ) 

310 try: 

311 return self[content_format] 

312 except KeyError: 

313 return default 

314 

315 

316class _MediaTypesRev: 

317 """Wrapper to provide a media_types_rev indexable object as was present up 

318 to 0.4.2""" 

319 

320 def __getitem__(self, name): 

321 warnings.warn( 

322 "media_types_rev is deprecated, please use aiocoap.numbers.ContentFormat", 

323 DeprecationWarning, 

324 stacklevel=2, 

325 ) 

326 if name == "text/plain": 

327 # deprecated alias. Kept alive for scripts like 

328 # https://gitlab.f-interop.eu/f-interop-contributors/ioppytest/blob/develop/automation/coap_client_aiocoap/automated_iut.py 

329 # that run aiocoap-client with text/plain as an argument. 

330 name = "text/plain;charset=utf-8" 

331 return int(ContentFormat.by_media_type(name)) 

332 

333 def get(self, name, default=None): 

334 warnings.warn( 

335 "media_types_rev is deprecated, please use aiocoap.numbers.ContentFormat", 

336 DeprecationWarning, 

337 stacklevel=2, 

338 ) 

339 try: 

340 return self[name] 

341 except KeyError: 

342 return default