Coverage for aiocoap/message.py: 84%
285 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
5from __future__ import annotations
7import ipaddress
8import urllib.parse
9import struct
10import copy
11import string
12from collections import namedtuple
14from . import error, optiontypes
15from .numbers.codes import Code, CHANGED
16from .numbers.types import Type
17from .numbers.constants import TransportTuning, MAX_REGULAR_BLOCK_SIZE_EXP
18from .options import Options
19from .util import hostportjoin, hostportsplit, Sentinel, quote_nonascii
20from .util.uri import quote_factory, unreserved, sub_delims
21from . import interfaces
23__all__ = ["Message", "NoResponse"]
25# FIXME there should be a proper inteface for this that does all the urllib
26# patching possibly required and works with pluggable transports. urls qualify
27# if they can be parsed into the Proxy-Scheme / Uri-* structure.
28coap_schemes = ["coap", "coaps", "coap+tcp", "coaps+tcp", "coap+ws", "coaps+ws"]
30# Monkey patch urllib to make URL joining available in CoAP
31# This is a workaround for <http://bugs.python.org/issue23759>.
32urllib.parse.uses_relative.extend(coap_schemes)
33urllib.parse.uses_netloc.extend(coap_schemes)
36class Message(object):
37 """CoAP Message with some handling metadata
39 This object's attributes provide access to the fields in a CoAP message and
40 can be directly manipulated.
42 * Some attributes are additional data that do not round-trip through
43 serialization and deserialization. They are marked as "non-roundtrippable".
44 * Some attributes that need to be filled for submission of the message can
45 be left empty by most applications, and will be taken care of by the
46 library. Those are marked as "managed".
48 The attributes are:
50 * :attr:`payload`: The payload (body) of the message as bytes.
51 * :attr:`mtype`: Message type (CON, ACK etc, see :mod:`.numbers.types`).
52 Managed unless set by the application.
53 * :attr:`code`: The code (either request or response code), see
54 :mod:`.numbers.codes`.
55 * :attr:`opt`: A container for the options, see :class:`.options.Options`.
57 * :attr:`mid`: The message ID. Managed by the :class:`.Context`.
58 * :attr:`token`: The message's token as bytes. Managed by the :class:`.Context`.
59 * :attr:`remote`: The socket address of the other side, managed by the
60 :class:`.protocol.Request` by resolving the ``.opt.uri_host`` or
61 ``unresolved_remote``, or by the stack by echoing the incoming
62 request's. Follows the :class:`.interfaces.EndpointAddress` interface.
63 Non-roundtrippable.
65 While a message has not been transmitted, the property is managed by the
66 :class:`.Message` itself using the :meth:`.set_request_uri()` or the
67 constructor `uri` argument.
68 * :attr:`transport_tuning`: Parameters used by one or more transports to
69 guide transmission. These are purely advisory hints; unknown properties
70 of that object are ignored, and transports consider them over built-in
71 constants on a best-effort basis.
73 Note that many attributes are mandatory if this is not None; it is
74 recommended that any objects passed in here are based on the
75 :class:`aiocoap.numbers.constants.TransportTuning` class.
77 * :attr:`request`: The request to which an incoming response message
78 belongs; only available at the client. Managed by the
79 :class:`.interfaces.RequestProvider` (typically a :class:`.Context`).
81 These properties are still available but deprecated:
83 * requested_*: Managed by the :class:`.protocol.Request` a response results
84 from, and filled with the request's URL data. Non-roundtrippable.
86 * unresolved_remote: ``host[:port]`` (strictly speaking; hostinfo as in a
87 URI) formatted string. If this attribute is set, it overrides
88 ``.RequestManageropt.uri_host`` (and ``-_port``) when it comes to filling the
89 ``remote`` in an outgoing request.
91 Use this when you want to send a request with a host name that would not
92 normally resolve to the destination address. (Typically, this is used for
93 proxying.)
95 Options can be given as further keyword arguments at message construction
96 time. This feature is experimental, as future message parameters could
97 collide with options.
100 The four messages involved in an exchange
101 -----------------------------------------
103 ::
105 Requester Responder
107 +-------------+ +-------------+
108 | request msg | ---- send request ---> | request msg |
109 +-------------+ +-------------+
110 |
111 processed into
112 |
113 v
114 +-------------+ +-------------+
115 | response m. | <--- send response --- | response m. |
116 +-------------+ +-------------+
119 The above shows the four message instances involved in communication
120 between an aiocoap client and server process. Boxes represent instances of
121 Message, and the messages on the same line represent a single CoAP as
122 passed around on the network. Still, they differ in some aspects:
124 * The requested URI will look different between requester and responder
125 if the requester uses a host name and does not send it in the message.
126 * If the request was sent via multicast, the response's requested URI
127 differs from the request URI because it has the responder's address
128 filled in. That address is not known at the responder's side yet, as
129 it is typically filled out by the network stack.
130 * It is yet unclear whether the response's URI should contain an IP
131 literal or a host name in the unicast case if the Uri-Host option was
132 not sent.
133 * Properties like Message ID and token will differ if a proxy was
134 involved.
135 * Some options or even the payload may differ if a proxy was involved.
136 """
138 def __init__(
139 self,
140 *,
141 mtype=None,
142 mid=None,
143 code=None,
144 payload=b"",
145 token=b"",
146 uri=None,
147 transport_tuning=None,
148 **kwargs,
149 ):
150 self.version = 1
151 if mtype is None:
152 # leave it unspecified for convenience, sending functions will know what to do
153 self.mtype = None
154 else:
155 self.mtype = Type(mtype)
156 self.mid = mid
157 if code is None:
158 # as above with mtype
159 self.code = None
160 else:
161 self.code = Code(code)
162 self.token = token
163 self.payload = payload
164 self.opt = Options()
166 self.remote = None
168 self.transport_tuning = transport_tuning or TransportTuning()
170 # deprecation error, should go away roughly after 0.2 release
171 if self.payload is None:
172 raise TypeError("Payload must not be None. Use empty string instead.")
174 if uri:
175 self.set_request_uri(uri)
177 for k, v in kwargs.items():
178 setattr(self.opt, k, v)
180 def __repr__(self):
181 return "<aiocoap.Message at %#x: %s %s (%s, %s) remote %s%s%s>" % (
182 id(self),
183 self.mtype if self.mtype is not None else "no mtype,",
184 self.code,
185 "MID %s" % self.mid if self.mid is not None else "no MID",
186 "token %s" % self.token.hex() if self.token else "empty token",
187 self.remote,
188 ", %s option(s)" % len(self.opt._options) if self.opt._options else "",
189 ", %s byte(s) payload" % len(self.payload) if self.payload else "",
190 )
192 def _repr_html_(self):
193 """An HTML representation for Jupyter and similar environments
195 While the precise format is not guaranteed, it will be usable through
196 tooltips and possibly fold-outs:
198 >>> from aiocoap import *
199 >>> msg = Message(code=GET, uri="coap://localhost/other/separate")
200 >>> html = msg._repr_html_()
201 >>> 'Message with code <abbr title="Request Code 0.01">GET</abbr>' in html
202 True
203 >>> '3 options</summary>' in html
204 True
205 """
206 import html
208 if not self.payload:
209 payload_rendered = "<p>No payload</p>"
210 else:
211 from . import defaults
213 if defaults.prettyprint_missing_modules():
214 payload_rendered = f"<code>{html.escape(repr(self.payload))}</code>"
215 else:
216 from .util.prettyprint import pretty_print, lexer_for_mime
218 (notes, mediatype, text) = pretty_print(self)
219 import pygments
220 from pygments.formatters import HtmlFormatter
222 try:
223 lexer = lexer_for_mime(mediatype)
224 text = pygments.highlight(text, lexer, HtmlFormatter())
225 except pygments.util.ClassNotFound:
226 text = html.escape(text)
227 payload_rendered = (
228 "<div>"
229 + "".join(
230 f'<p style="color:gray;font-size:small;">{html.escape(n)}</p>'
231 for n in notes
232 )
233 + f"<pre>{text}</pre>"
234 + "</div>"
235 )
236 return f"""<details style="padding-left:1em"><summary style="margin-left:-1em;display:list-item;">Message with code {self.code._repr_html_() if self.code is not None else 'None'}, remote {html.escape(str(self.remote))}</summary>
237 {self.opt._repr_html_()}{payload_rendered}"""
239 def copy(self, **kwargs):
240 """Create a copy of the Message. kwargs are treated like the named
241 arguments in the constructor, and update the copy."""
242 # This is part of moving messages in an "immutable" direction; not
243 # necessarily hard immutable. Let's see where this goes.
245 new = type(self)(
246 mtype=kwargs.pop("mtype", self.mtype),
247 mid=kwargs.pop("mid", self.mid),
248 code=kwargs.pop("code", self.code),
249 payload=kwargs.pop("payload", self.payload),
250 token=kwargs.pop("token", self.token),
251 # Assuming these are not readily mutated, but rather passed
252 # around in a class-like fashion
253 transport_tuning=kwargs.pop("transport_tuning", self.transport_tuning),
254 )
255 new.remote = kwargs.pop("remote", self.remote)
256 new.opt = copy.deepcopy(self.opt)
258 if "uri" in kwargs:
259 new.set_request_uri(kwargs.pop("uri"))
261 for k, v in kwargs.items():
262 setattr(new.opt, k, v)
264 return new
266 @classmethod
267 def decode(cls, rawdata, remote=None):
268 """Create Message object from binary representation of message."""
269 try:
270 (vttkl, code, mid) = struct.unpack("!BBH", rawdata[:4])
271 except struct.error:
272 raise error.UnparsableMessage("Incoming message too short for CoAP")
273 version = (vttkl & 0xC0) >> 6
274 if version != 1:
275 raise error.UnparsableMessage("Fatal Error: Protocol Version must be 1")
276 mtype = (vttkl & 0x30) >> 4
277 token_length = vttkl & 0x0F
278 msg = Message(mtype=mtype, mid=mid, code=code)
279 msg.token = rawdata[4 : 4 + token_length]
280 msg.payload = msg.opt.decode(rawdata[4 + token_length :])
281 msg.remote = remote
282 return msg
284 def encode(self):
285 """Create binary representation of message from Message object."""
286 if self.code is None or self.mtype is None or self.mid is None:
287 raise TypeError(
288 "Fatal Error: Code, Message Type and Message ID must not be None."
289 )
290 rawdata = bytes(
291 [
292 (self.version << 6)
293 + ((self.mtype & 0x03) << 4)
294 + (len(self.token) & 0x0F)
295 ]
296 )
297 rawdata += struct.pack("!BH", self.code, self.mid)
298 rawdata += self.token
299 rawdata += self.opt.encode()
300 if len(self.payload) > 0:
301 rawdata += bytes([0xFF])
302 rawdata += self.payload
303 return rawdata
305 def get_cache_key(self, ignore_options=()):
306 """Generate a hashable and comparable object (currently a tuple) from
307 the message's code and all option values that are part of the cache key
308 and not in the optional list of ignore_options (which is the list of
309 option numbers that are not technically NoCacheKey but handled by the
310 application using this method).
312 >>> from aiocoap.numbers import GET
313 >>> m1 = Message(code=GET)
314 >>> m2 = Message(code=GET)
315 >>> m1.opt.uri_path = ('s', '1')
316 >>> m2.opt.uri_path = ('s', '1')
317 >>> m1.opt.size1 = 10 # the only no-cache-key option in the base spec
318 >>> m2.opt.size1 = 20
319 >>> m1.get_cache_key() == m2.get_cache_key()
320 True
321 >>> m2.opt.etag = b'000'
322 >>> m1.get_cache_key() == m2.get_cache_key()
323 False
324 >>> from aiocoap.numbers.optionnumbers import OptionNumber
325 >>> ignore = [OptionNumber.ETAG]
326 >>> m1.get_cache_key(ignore) == m2.get_cache_key(ignore)
327 True
328 """
330 options = []
332 for option in self.opt.option_list():
333 if option.number in ignore_options or (
334 option.number.is_safetoforward() and option.number.is_nocachekey()
335 ):
336 continue
337 options.append((option.number, option.value))
339 return (self.code, tuple(options))
341 #
342 # splitting and merging messages into and from message blocks
343 #
345 def _extract_block(self, number, size_exp, max_bert_size):
346 """Extract block from current message."""
347 if size_exp == 7:
348 start = number * 1024
349 size = 1024 * (max_bert_size // 1024)
350 else:
351 size = 2 ** (size_exp + 4)
352 start = number * size
354 if start >= len(self.payload):
355 raise error.BadRequest("Block request out of bounds")
357 end = start + size if start + size < len(self.payload) else len(self.payload)
358 more = True if end < len(self.payload) else False
360 payload = self.payload[start:end]
361 blockopt = (number, more, size_exp)
363 if self.code.is_request():
364 return self.copy(payload=payload, mid=None, block1=blockopt)
365 else:
366 return self.copy(payload=payload, mid=None, block2=blockopt)
368 def _append_request_block(self, next_block):
369 """Modify message by appending another block"""
370 if not self.code.is_request():
371 raise ValueError("_append_request_block only works on requests.")
373 block1 = next_block.opt.block1
374 if block1.more:
375 if len(next_block.payload) == block1.size:
376 pass
377 elif (
378 block1.size_exponent == 7 and len(next_block.payload) % block1.size == 0
379 ):
380 pass
381 else:
382 raise error.BadRequest("Payload size does not match Block1")
383 if block1.start == len(self.payload):
384 self.payload += next_block.payload
385 self.opt.block1 = block1
386 self.token = next_block.token
387 self.mid = next_block.mid
388 if not block1.more and next_block.opt.block2 is not None:
389 self.opt.block2 = next_block.opt.block2
390 else:
391 # possible extension point: allow messages with "gaps"; then
392 # ValueError would only be raised when trying to overwrite an
393 # existing part; it is doubtful though that the blockwise
394 # specification even condones such behavior.
395 raise ValueError()
397 def _append_response_block(self, next_block):
398 """Append next block to current response message.
399 Used when assembling incoming blockwise responses."""
400 if not self.code.is_response():
401 raise ValueError("_append_response_block only works on responses.")
403 block2 = next_block.opt.block2
404 if not block2.is_valid_for_payload_size(len(next_block.payload)):
405 raise error.UnexpectedBlock2("Payload size does not match Block2")
406 if block2.start != len(self.payload):
407 # Does not need to be implemented as long as the requesting code
408 # sequentially clocks out data
409 raise error.NotImplemented()
411 if next_block.opt.etag != self.opt.etag:
412 raise error.ResourceChanged()
414 self.payload += next_block.payload
415 self.opt.block2 = block2
416 self.token = next_block.token
417 self.mid = next_block.mid
419 def _generate_next_block2_request(self, response):
420 """Generate a sub-request for next response block.
422 This method is used by client after receiving blockwise response from
423 server with "more" flag set."""
425 # Note: response here is the assembled response, but (due to
426 # _append_response_block's workings) it carries the Block2 option of
427 # the last received block.
429 next_after_received = len(response.payload) // response.opt.block2.size
430 blockopt = optiontypes.BlockOption.BlockwiseTuple(
431 next_after_received, False, response.opt.block2.size_exponent
432 )
434 # has been checked in assembly, just making sure
435 assert blockopt.start == len(
436 response.payload
437 ), "Unexpected state of preassembled message"
439 blockopt = blockopt.reduced_to(response.remote.maximum_block_size_exp)
441 return self.copy(
442 payload=b"",
443 mid=None,
444 token=None,
445 block2=blockopt,
446 block1=None,
447 observe=None,
448 )
450 def _generate_next_block1_response(self):
451 """Generate a response to acknowledge incoming request block.
453 This method is used by server after receiving blockwise request from
454 client with "more" flag set."""
455 response = Message(code=CHANGED, token=self.token)
456 response.remote = self.remote
457 if (
458 self.opt.block1.block_number == 0
459 and self.opt.block1.size_exponent
460 > self.transport_tuning.DEFAULT_BLOCK_SIZE_EXP
461 ):
462 new_size_exponent = self.transport_tuning.DEFAULT_BLOCK_SIZE_EXP
463 response.opt.block1 = (0, True, new_size_exponent)
464 else:
465 response.opt.block1 = (
466 self.opt.block1.block_number,
467 True,
468 self.opt.block1.size_exponent,
469 )
470 return response
472 #
473 # the message in the context of network and addresses
474 #
476 def get_request_uri(self, *, local_is_server=False):
477 """The absolute URI this message belongs to.
479 For requests, this is composed from the options (falling back to the
480 remote). For responses, this is largely taken from the original request
481 message (so far, that could have been trackecd by the requesting
482 application as well), but -- in case of a multicast request -- with the
483 host replaced by the responder's endpoint details.
485 This implements Section 6.5 of RFC7252.
487 By default, these values are only valid on the client. To determine a
488 message's request URI on the server, set the local_is_server argument
489 to True. Note that determining the request URI on the server is brittle
490 when behind a reverse proxy, may not be possible on all platforms, and
491 can only be applied to a request message in a renderer (for the
492 response message created by the renderer will only be populated when it
493 gets transmitted; simple manual copying of the request's remote to the
494 response will not magically make this work, for in the very case where
495 the request and response's URIs differ, that would not catch the
496 difference and still report the multicast address, while the actual
497 sending address will only be populated by the operating system later).
498 """
500 # maybe this function does not belong exactly *here*, but it belongs to
501 # the results of .request(message), which is currently a message itself.
503 if hasattr(self, "_original_request_uri"):
504 # During server-side processing, a message's options may be altered
505 # to the point where its options don't accurately reflect its URI
506 # any more. In that case, this is stored.
507 return self._original_request_uri
509 if self.code.is_response():
510 refmsg = self.request
512 if refmsg.remote.is_multicast:
513 if local_is_server:
514 multicast_netloc_override = self.remote.hostinfo_local
515 else:
516 multicast_netloc_override = self.remote.hostinfo
517 else:
518 multicast_netloc_override = None
519 else:
520 refmsg = self
521 multicast_netloc_override = None
523 proxyuri = refmsg.opt.proxy_uri
524 if proxyuri is not None:
525 return proxyuri
527 scheme = refmsg.opt.proxy_scheme or refmsg.remote.scheme
528 query = refmsg.opt.uri_query or ()
529 path = refmsg.opt.uri_path
531 if multicast_netloc_override is not None:
532 netloc = multicast_netloc_override
533 else:
534 if local_is_server:
535 netloc = refmsg.remote.hostinfo_local
536 else:
537 netloc = refmsg.remote.hostinfo
539 if refmsg.opt.uri_host is not None or refmsg.opt.uri_port is not None:
540 host, port = hostportsplit(netloc)
542 host = refmsg.opt.uri_host or host
543 port = refmsg.opt.uri_port or port
545 # FIXME: This sounds like it should be part of
546 # hpostportjoin/-split
547 escaped_host = quote_nonascii(host)
549 # FIXME: "If host is not valid reg-name / IP-literal / IPv4address,
550 # fail"
552 netloc = hostportjoin(escaped_host, port)
554 # FIXME this should follow coap section 6.5 more closely
555 query = "&".join(_quote_for_query(q) for q in query)
556 path = "".join("/" + _quote_for_path(p) for p in path) or "/"
558 fragment = None
559 params = "" # are they not there at all?
561 # Eases debugging, for when they raise from urunparse you won't know
562 # which of them it was
563 assert scheme is not None, "Remote has no scheme set"
564 assert netloc is not None, "Remote has no netloc set"
565 return urllib.parse.urlunparse((scheme, netloc, path, params, query, fragment))
567 def set_request_uri(self, uri, *, set_uri_host=True):
568 """Parse a given URI into the uri_* fields of the options.
570 The remote does not get set automatically; instead, the remote data is
571 stored in the uri_host and uri_port options. That is because name resolution
572 is coupled with network specifics the protocol will know better by the
573 time the message is sent. Whatever sends the message, be it the
574 protocol itself, a proxy wrapper or an alternative transport, will know
575 how to handle the information correctly.
577 When ``set_uri_host=False`` is passed, the host/port is stored in the
578 ``unresolved_remote`` message property instead of the uri_host option;
579 as a result, the unresolved host name is not sent on the wire, which
580 breaks virtual hosts but makes message sizes smaller.
582 This implements Section 6.4 of RFC7252.
584 This raises IncompleteUrlError if URI references are passed in (instead
585 of a full URI), and MalformedUrlError if the URI specification or the
586 library's expectations of URI shapes (eg. 'coap+tcp:no-slashes') are
587 violated.
588 """
590 try:
591 parsed = urllib.parse.urlparse(uri)
592 except ValueError as e:
593 raise error.MalformedUrlError from e
595 if parsed.fragment:
596 raise error.MalformedUrlError(
597 "Fragment identifiers can not be set on a request URI"
598 )
600 if not parsed.scheme:
601 raise error.IncompleteUrlError()
603 if parsed.scheme not in coap_schemes:
604 self.opt.proxy_uri = uri
605 return
607 if not parsed.hostname:
608 raise error.MalformedUrlError("CoAP URIs need a hostname")
610 if parsed.username or parsed.password:
611 raise error.MalformedUrlError("User name and password not supported.")
613 try:
614 if parsed.path not in ("", "/"):
615 # FIXME: This tolerates incomplete % sequences.
616 self.opt.uri_path = [
617 urllib.parse.unquote(x, errors="strict")
618 for x in parsed.path.split("/")[1:]
619 ]
620 else:
621 self.opt.uri_path = []
622 if parsed.query:
623 # FIXME: This tolerates incomplete % sequences.
624 self.opt.uri_query = [
625 urllib.parse.unquote(x, errors="strict")
626 for x in parsed.query.split("&")
627 ]
628 else:
629 self.opt.uri_query = []
630 except UnicodeError as e:
631 raise error.MalformedUrlError(
632 "Percent encoded strings in CoAP URIs need to be UTF-8 encoded"
633 ) from e
635 self.remote = UndecidedRemote(parsed.scheme, parsed.netloc)
637 try:
638 _ = parsed.port
639 except ValueError as e:
640 raise error.MalformedUrlError("Port must be numeric") from e
642 is_ip_literal = parsed.netloc.startswith("[") or (
643 parsed.hostname.count(".") == 3
644 and all(c in "0123456789." for c in parsed.hostname)
645 and all(int(x) <= 255 for x in parsed.hostname.split("."))
646 )
648 if set_uri_host and not is_ip_literal:
649 try:
650 self.opt.uri_host = urllib.parse.unquote(
651 parsed.hostname, errors="strict"
652 ).translate(_ascii_lowercase)
653 except UnicodeError as e:
654 raise error.MalformedUrlError(
655 "Percent encoded strings in CoAP URI hosts need to be UTF-8 encoded"
656 ) from e
658 # Deprecated accessors to moved functionality
660 @property
661 def unresolved_remote(self):
662 return self.remote.hostinfo
664 @unresolved_remote.setter
665 def unresolved_remote(self, value):
666 # should get a big fat deprecation warning
667 if value is None:
668 self.remote = UndecidedRemote("coap", None)
669 else:
670 self.remote = UndecidedRemote("coap", value)
672 @property
673 def requested_scheme(self):
674 if self.code.is_request():
675 return self.remote.scheme
676 else:
677 return self.request.requested_scheme
679 @requested_scheme.setter
680 def requested_scheme(self, value):
681 self.remote = UndecidedRemote(value, self.remote.hostinfo)
683 @property
684 def requested_proxy_uri(self):
685 return self.request.opt.proxy_uri
687 @property
688 def requested_hostinfo(self):
689 return self.request.opt.uri_host or self.request.unresolved_remote
691 @property
692 def requested_path(self):
693 return self.request.opt.uri_path
695 @property
696 def requested_query(self):
697 return self.request.opt.uri_query
700class UndecidedRemote(
701 namedtuple("_UndecidedRemote", ("scheme", "hostinfo")), interfaces.EndpointAddress
702):
703 """Remote that is set on messages that have not been sent through any any
704 transport.
706 It describes scheme, hostname and port that were set in
707 :meth:`.set_request_uri()` or when setting a URI per Message constructor.
709 * :attr:`scheme`: The scheme string
710 * :attr:`hostinfo`: The authority component of the URI, as it would occur
711 in the URI.
713 In order to produce URIs identical to those received in responses, and
714 because the underlying types should really be binary anyway, IP addresses
715 in the hostinfo are normalized:
717 >>> UndecidedRemote("coap+tcp", "[::0001]:1234")
718 UndecidedRemote(scheme='coap+tcp', hostinfo='[::1]:1234')
719 """
721 # This is settable per instance, for other transports to pick it up.
722 maximum_block_size_exp = MAX_REGULAR_BLOCK_SIZE_EXP
724 def __new__(cls, scheme, hostinfo):
725 if "[" in hostinfo:
726 (host, port) = hostportsplit(hostinfo)
727 ip = ipaddress.ip_address(host)
728 host = str(ip)
729 hostinfo = hostportjoin(host, port)
731 return super().__new__(cls, scheme, hostinfo)
733 @classmethod
734 def from_pathless_uri(cls, uri: str) -> UndecidedRemote:
735 """Create an UndecidedRemote for a given URI that has no query, path,
736 fragment or other components not expressed in an UndecidedRemote
738 >>> from aiocoap.message import UndecidedRemote
739 >>> UndecidedRemote.from_pathless_uri("coap://localhost")
740 UndecidedRemote(scheme='coap', hostinfo='localhost')
741 """
743 parsed = urllib.parse.urlparse(uri)
745 if parsed.username or parsed.password:
746 raise ValueError("User name and password not supported.")
748 if parsed.path not in ("", "/") or parsed.query or parsed.fragment:
749 raise ValueError(
750 "Paths and query and fragment can not be set on an UndecidedRemote"
751 )
753 return cls(parsed.scheme, parsed.netloc)
756_ascii_lowercase = str.maketrans(string.ascii_uppercase, string.ascii_lowercase)
758_quote_for_path = quote_factory(unreserved + sub_delims + ":@")
759_quote_for_query = quote_factory(
760 unreserved + "".join(c for c in sub_delims if c != "&") + ":@/?"
761)
763#: Result that can be returned from a render method instead of a Message when
764#: due to defaults (eg. multicast link-format queries) or explicit
765#: configuration (eg. the No-Response option), no response should be sent at
766#: all. Note that per RFC7967 section 2, an ACK is still sent to a CON
767#: request.
768#:
769#: Depercated; set the no_response option on a regular response instead (see
770#: :meth:`.interfaces.Resource.render` for details).
771NoResponse = Sentinel("NoResponse")