Coverage for aiocoap/numbers/contentformat.py: 64%
90 statements
« prev ^ index » next coverage.py v7.6.8, created at 2024-11-28 12:34 +0000
« prev ^ index » next coverage.py v7.6.8, created at 2024-11-28 12:34 +0000
1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors
2#
3# SPDX-License-Identifier: MIT
5"""Module containing the CoRE parameters / CoAP Content-Formats registry"""
7from __future__ import annotations
9from typing import Dict, Tuple
11from ..util import ExtensibleIntEnum, ExtensibleEnumMeta
12import warnings
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:])'`
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
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("; ", ";")
122class ContentFormatMeta(ExtensibleEnumMeta):
123 def __init__(self, name, bases, dict) -> None:
124 super().__init__(name, bases, dict)
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"] = {}
132class ContentFormat(ExtensibleIntEnum, metaclass=ContentFormatMeta):
133 """Entry in the `CoAP Content-Formats registry`__ of the IANA Constrained
134 RESTful Environments (Core) Parameters group
136 .. __: https://www.iana.org/assignments/core-parameters/core-parameters.xhtml#content-formats
138 Known entries have ``.media_type`` and ``.encoding`` attributes:
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'
149 Unknown entries do not have these properties:
151 >>> ContentFormat(12345).is_known()
152 False
153 >>> ContentFormat(12345).media_type # doctest: +ELLIPSIS
154 Traceback (most recent call last):
155 ...
156 AttributeError: ...
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'>
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:
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 """
175 @classmethod
176 def define(cls, number, media_type: str, encoding: str = "identity"):
177 s = cls(number)
179 if hasattr(s, "media_type"):
180 warnings.warn(
181 "Redefining media type is a compatibility hazard, but allowed for experimental purposes"
182 )
184 s._media_type = media_type
185 s._encoding = encoding
187 cls._by_mt_encoding[(_normalize_media_type(media_type), encoding)] = s
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)]
197 def is_known(self):
198 return hasattr(self, "media_type")
200 @property
201 def media_type(self) -> str:
202 return self._media_type
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
213 @property
214 def encoding(self) -> str:
215 return self._encoding
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
226 @classmethod
227 def _rehash(cls):
228 """Update the class's cache of known media types
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 }
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 )
253 def __bool__(self):
254 return True
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
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>"""
265 TEXT = 0
266 LINKFORMAT = 40
267 OCTETSTREAM = 42
268 JSON = 50
269 CBOR = 60
270 SENML = 112
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")
280class _MediaTypes:
281 """Wrapper to provide a media_types indexable object as was present up to
282 0.4.2"""
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)
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)
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
313class _MediaTypesRev:
314 """Wrapper to provide a media_types_rev indexable object as was present up
315 to 0.4.2"""
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))
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