Coverage for aiocoap/blockwise.py: 92%
52 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"""Helpers for the implementation of RFC7959 blockwise transfers"""
7from typing import Awaitable, Callable
9from . import numbers
10from .numbers.optionnumbers import OptionNumber
11from .numbers import codes
12from .error import ConstructionRenderableError
13from .message import Message
14from .optiontypes import BlockOption
15from .util.asyncio.timeoutdict import TimeoutDict
18def _extract_block_key(message):
19 """Extract a key that hashes equally for all blocks of a blockwise
20 operation from a request message.
22 See discussion at <https://mailarchive.ietf.org/arch/msg/core/I-6LzAL6lIUVDA6_g9YM3Zjhg8E>.
23 """
25 return (
26 message.remote.blockwise_key,
27 message.code,
28 message.get_cache_key(
29 [
30 OptionNumber.BLOCK1,
31 OptionNumber.BLOCK2,
32 OptionNumber.OBSERVE,
33 ]
34 ),
35 )
38class ContinueException(ConstructionRenderableError):
39 """Not an error in the CoAP sense, but an error in the processing sense,
40 indicating that no complete request message is available for processing.
42 It reflects back the request's block1 option when rendered.
43 """
45 def __init__(self, block1):
46 self.block1 = block1
48 def to_message(self):
49 m = super().to_message()
50 m.opt.block1 = self.block1
51 return m
53 code = codes.CONTINUE
56class IncompleteException(ConstructionRenderableError):
57 code = codes.REQUEST_ENTITY_INCOMPLETE
60class Block1Spool:
61 def __init__(self):
62 # FIXME: introduce an actual parameter here
63 self._assemblies = TimeoutDict(numbers.TransportTuning().MAX_TRANSMIT_WAIT)
65 def feed_and_take(self, req: Message) -> Message:
66 """Assemble the request into the spool. This either produces a
67 reassembled request message, or raises either a Continue or a Request
68 Entity Incomplete exception.
70 Requests without block1 are simply passed through."""
72 if req.opt.block1 is None:
73 return req
75 block_key = _extract_block_key(req)
77 if req.opt.block1.block_number == 0:
78 # silently discarding any old incomplete operation
79 self._assemblies[block_key] = req
80 else:
81 try:
82 self._assemblies[block_key]._append_request_block(req)
83 except KeyError:
84 # KeyError: Received unmatched blockwise response
85 # ValueError: Failed to assemble -- gaps or overlaps in data
86 raise IncompleteException from None
88 if req.opt.block1.more:
89 raise ContinueException(req.opt.block1)
90 else:
91 return self._assemblies[block_key]
92 # which happens to carry the last block's block1 option
95class Block2Cache:
96 """A cache of responses to a give block key.
98 Use this when result rendering is expensive, not idempotent or has varying
99 output -- otherwise it's often better to calculate the full response again
100 and serve chunks.
101 """
103 def __init__(self):
104 # FIXME: introduce an actual parameter here
105 self._completes = TimeoutDict(numbers.TransportTuning().MAX_TRANSMIT_WAIT)
107 async def extract_or_insert(
108 self, req: Message, response_builder: Callable[[], Awaitable[Message]]
109 ):
110 """Given a request message,
112 * if it is querying a particular block, look it up in the cache or
113 raise Request Entity Incomplete.
114 * otherwise,
115 * await the response builder
116 * return the response if it doesn't need chunking, or
117 * return the first chunk and store it for later use
119 """
120 block_key = _extract_block_key(req)
122 if req.opt.block2 is None or req.opt.block2.block_number == 0:
123 assembled = await response_builder()
124 else:
125 try:
126 assembled = self._completes[block_key]
127 except KeyError:
128 raise IncompleteException from None
130 if (
131 len(assembled.payload) > req.remote.maximum_payload_size
132 or req.opt.block2 is not None
133 and len(assembled.payload) > req.opt.block2.size
134 ):
135 self._completes[block_key] = assembled
137 block2 = req.opt.block2 or BlockOption.BlockwiseTuple(
138 0, 0, req.remote.maximum_block_size_exp
139 )
140 return assembled._extract_block(
141 block2.block_number,
142 block2.size_exponent,
143 req.remote.maximum_payload_size,
144 )
145 else:
146 return assembled