Coverage for aiocoap/numbers/contentformat.py: 64%

90 statements  

« prev     ^ index     » next       coverage.py v7.6.3, created at 2024-10-15 22:10 +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', encoding='identity'> 

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', encoding='identity'> 

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 return "<%s %d%s>" % ( 

246 type(self).__name__, 

247 self, 

248 ", media_type=%r, encoding=%r" % (self.media_type, self.encoding) 

249 if self.is_known() 

250 else "", 

251 ) 

252 

253 def __bool__(self): 

254 return True 

255 

256 def _repr_html_(self): 

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

258 if self.is_known(): 

259 import html 

260 

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

262 else: 

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

264 

265 TEXT = 0 

266 LINKFORMAT = 40 

267 OCTETSTREAM = 42 

268 JSON = 50 

269 CBOR = 60 

270 SENML = 112 

271 

272 

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

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

275 continue 

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

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

278 

279 

280class _MediaTypes: 

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

282 0.4.2""" 

283 

284 def __getitem__(self, content_format): 

285 warnings.warn( 

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

287 DeprecationWarning, 

288 stacklevel=2, 

289 ) 

290 if content_format is None: 

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

292 # fail through the constructor 

293 raise KeyError(None) 

294 

295 cf = ContentFormat(content_format) 

296 if cf.is_known(): 

297 return _normalize_media_type(cf.media_type) 

298 else: 

299 raise KeyError(content_format) 

300 

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

302 warnings.warn( 

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

304 DeprecationWarning, 

305 stacklevel=2, 

306 ) 

307 try: 

308 return self[content_format] 

309 except KeyError: 

310 return default 

311 

312 

313class _MediaTypesRev: 

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

315 to 0.4.2""" 

316 

317 def __getitem__(self, name): 

318 warnings.warn( 

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

320 DeprecationWarning, 

321 stacklevel=2, 

322 ) 

323 if name == "text/plain": 

324 # deprecated alias. Kept alive for scripts like 

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

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

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

328 return int(ContentFormat.by_media_type(name)) 

329 

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

331 warnings.warn( 

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

333 DeprecationWarning, 

334 stacklevel=2, 

335 ) 

336 try: 

337 return self[name] 

338 except KeyError: 

339 return default