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

66 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-16 16:09 +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 ..util import ExtensibleIntEnum 

10import warnings 

11 

12# _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:])'` 

13 

14_raw = [ 

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

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

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

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

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

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

21 ['Unassigned', '', '20', ''], 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

79 ['Unassigned', '', '321', ''], 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

108 ] 

109 

110def _normalize_media_type(s): 

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

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

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

114 return s.replace('; ', ';') 

115 

116class ContentFormat(ExtensibleIntEnum): 

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

118 RESTful Environments (Core) Parameters group 

119 

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

121 

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

123 

124 >>> ContentFormat(0).media_type 

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

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

127 0 

128 >>> ContentFormat(60) 

129 <ContentFormat 60, media_type='application/cbor', encoding='identity'> 

130 >>> ContentFormat(11060).encoding 

131 'deflate' 

132 

133 Unknown entries do not have these properties: 

134 

135 >>> ContentFormat(12345).is_known() 

136 False 

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

138 Traceback (most recent call last): 

139 ... 

140 AttributeError: ... 

141 

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

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

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

145 >>> ContentFormat.TEXT 

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

147 

148 A convenient property of ContentFormat is that any known content format is 

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

150 be assigned defaults easily: 

151 

152 >>> requested_by_client = ContentFormat.TEXT 

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

154 0 

155 >>> used = requested_by_client or ContentFormat.LINKFORMAT 

156 >>> assert used == ContentFormat.TEXT 

157 """ 

158 

159 @classmethod 

160 def by_media_type(cls, media_type: str, encoding: str = 'identity') -> ContentFormat: 

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

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

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

164 

165 def is_known(self): 

166 return hasattr(self, "media_type") 

167 

168 @classmethod 

169 def _rehash(cls): 

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

171 

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

173 should be found later on.""" 

174 cls._by_mt_encoding = {(_normalize_media_type(c.media_type), c.encoding): c for c in cls._value2member_map_.values()} 

175 

176 def __repr__(self): 

177 return "<%s %d%s>" % (type(self).__name__, self, ', media_type=%r, encoding=%r' % (self.media_type, self.encoding) if self.is_known() else "") 

178 

179 def __bool__(self): 

180 return True 

181 

182 def _repr_html_(self): 

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

184 if self.is_known(): 

185 import html 

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

187 else: 

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

189 

190 TEXT = 0 

191 LINKFORMAT = 40 

192 OCTETSTREAM = 42 

193 JSON = 50 

194 CBOR = 60 

195 SENML = 112 

196 

197for (_mt, _enc, _i, _source) in _raw: 

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

199 continue 

200 _mt, _, _ = _mt.partition(' (TEMPORARY') 

201 _cf = ContentFormat(int(_i)) 

202 _cf.media_type = _mt 

203 _cf.encoding = _enc or "identity" 

204ContentFormat._rehash() 

205 

206 

207class _MediaTypes: 

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

209 0.4.2""" 

210 def __getitem__(self, content_format): 

211 warnings.warn("media_types is deprecated, please use aiocoap.numbers.ContentFormat", DeprecationWarning, stacklevel=2) 

212 if content_format is None: 

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

214 # fail through the constructor 

215 raise KeyError(None) 

216 

217 cf = ContentFormat(content_format) 

218 if cf.is_known(): 

219 return _normalize_media_type(cf.media_type) 

220 else: 

221 raise KeyError(content_format) 

222 

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

224 warnings.warn("media_types is deprecated, please use aiocoap.numbers.ContentFormat", DeprecationWarning, stacklevel=2) 

225 try: 

226 return self[content_format] 

227 except KeyError: 

228 return default 

229 

230class _MediaTypesRev: 

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

232 to 0.4.2""" 

233 def __getitem__(self, name): 

234 warnings.warn("media_types_rev is deprecated, please use aiocoap.numbers.ContentFormat", DeprecationWarning, stacklevel=2) 

235 if name == 'text/plain': 

236 # deprecated alias. Kept alive for scripts like 

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

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

239 name = 'text/plain;charset=utf-8' 

240 return int(ContentFormat.by_media_type(name)) 

241 

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

243 warnings.warn("media_types_rev is deprecated, please use aiocoap.numbers.ContentFormat", DeprecationWarning, stacklevel=2) 

244 try: 

245 return self[name] 

246 except KeyError: 

247 return default