sw-serverless 10.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ """Write SW-Serverless adapters in Python.
2
+
3
+ import sw_serverless as sw
4
+
5
+ class Greeter:
6
+ def __init__(self):
7
+ sw.expect("Greeting", "Hello", description="What to say")
8
+
9
+ @sw.command(description="Greets someone")
10
+ def greet(self, name: str) -> str:
11
+ return f"{sw.value_of('Greeting')}, {name}"
12
+
13
+ if __name__ == "__main__":
14
+ sw.run(Greeter)
15
+
16
+ ``python adapter.py --describe`` prints what it is; the host runs it otherwise. An adapter with a
17
+ ``start`` hook is resident: it runs until stopped, with ``stop``, ``status`` and ``reset`` hooks.
18
+ """
19
+
20
+ from ._adapter import command, declared_settings, expect, implements, startup_values, value_of
21
+ from ._runner import SDK_VERSION, AdapterError, Context, context, describe, run
22
+
23
+ __version__ = SDK_VERSION
24
+
25
+ __all__ = [
26
+ "AdapterError", "Context", "SDK_VERSION", "command", "context", "declared_settings", "describe",
27
+ "expect", "implements", "run", "startup_values", "value_of",
28
+ ]
@@ -0,0 +1,180 @@
1
+ """Declaring an adapter: its settings, its commands, its kinds and contracts.
2
+
3
+ Settings are declared once, in code, with :func:`expect` — the Python form of the .NET SDK's
4
+ ``Runner.Expect`` — and reach the manifest through ``--describe`` and the host through ``Hello``.
5
+ """
6
+
7
+ import contextvars
8
+ import inspect
9
+ import typing
10
+
11
+ from . import _types
12
+
13
+ SETTING_TYPES = ("text", "multiline", "number", "boolean", "select", "json")
14
+
15
+
16
+ class Setting:
17
+ def __init__(self, name, default, required, secret, description, type):
18
+ self.name = name
19
+ self.default = default
20
+ self.required = required
21
+ self.secret = secret
22
+ self.description = description
23
+ self.type = type
24
+
25
+ def describe(self):
26
+ return {"name": self.name, "description": self.description, "type": self.type,
27
+ "required": self.required, "secret": self.secret, "default": self.default}
28
+
29
+
30
+ _settings = {}
31
+
32
+
33
+ def expect(name, default=None, *, required=None, secret=False, description=None, type="text"):
34
+ """Declares a setting the adapter reads.
35
+
36
+ Required unless it has a default or ``required=False`` says otherwise. A secret is masked
37
+ wherever a host application shows it. Declaring the same name again replaces it.
38
+ """
39
+ if not name or not isinstance(name, str):
40
+ raise ValueError("a setting needs a name")
41
+ if type not in SETTING_TYPES:
42
+ raise ValueError(f"setting type must be one of {', '.join(SETTING_TYPES)}")
43
+ if required is None:
44
+ required = default is None
45
+ _settings[name] = Setting(name, None if default is None else str(default), bool(required), bool(secret),
46
+ description, type)
47
+ return name
48
+
49
+
50
+ def declared_settings():
51
+ return list(_settings.values())
52
+
53
+
54
+ # The values a call sees: this invocation's properties over the process's startup values.
55
+ _startup_values = {}
56
+ _call_values = contextvars.ContextVar("sw_call_values", default=None)
57
+
58
+
59
+ def value_of(name, default=None):
60
+ """A setting's value for the current call: the call's own properties, then the values the
61
+ adapter was started with, then the declared default, then ``default``."""
62
+ call = _call_values.get()
63
+ if call and name in call:
64
+ return call[name]
65
+ if name in _startup_values:
66
+ return _startup_values[name]
67
+ declared = _settings.get(name)
68
+ if declared is not None and declared.default is not None:
69
+ return declared.default
70
+ return default
71
+
72
+
73
+ def startup_values():
74
+ return dict(_startup_values)
75
+
76
+
77
+ def command(name=None, *, description=None):
78
+ """Marks a method as a command the host can call, under ``name`` or the method's own name."""
79
+
80
+ def mark(fn):
81
+ fn.__sw_command__ = {"name": name or fn.__name__, "description": description}
82
+ return fn
83
+
84
+ if callable(name): # used bare: @command
85
+ fn, name = name, None
86
+ return mark(fn)
87
+ return mark
88
+
89
+
90
+ class Command:
91
+ def __init__(self, name, method, description):
92
+ self.name = name
93
+ self.method = method
94
+ self.description = description
95
+ signature = inspect.signature(method)
96
+ parameters = [p for p in signature.parameters.values()
97
+ if p.name != "self" and p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)]
98
+ if len(parameters) > 1:
99
+ raise TypeError(f"command {name} takes {len(parameters)} arguments; a command takes at most one")
100
+ try:
101
+ hints = typing.get_type_hints(method)
102
+ except Exception:
103
+ hints = {}
104
+ self.takes_argument = bool(parameters)
105
+ self.argument_type = hints.get(parameters[0].name, _types._EMPTY) if parameters else None
106
+ returns = hints.get("return", signature.return_annotation)
107
+ self.returns_value = returns not in (None, type(None))
108
+ self.result_type = returns
109
+
110
+ def info(self):
111
+ input_schema = _types.schema(self.argument_type) if self.takes_argument else None
112
+ output_schema = _types.schema(self.result_type) if self.returns_value else None
113
+ return {"name": self.name, "description": self.description, "takes_argument": self.takes_argument,
114
+ "returns_value": self.returns_value, "input_schema": input_schema, "output_schema": output_schema,
115
+ "parameter_type": _type_name(self.argument_type) if self.takes_argument else ""}
116
+
117
+
118
+ def _type_name(tp):
119
+ if tp in (_types._EMPTY, None):
120
+ return "object"
121
+ return getattr(tp, "__name__", str(tp))
122
+
123
+
124
+ def implements(contract, version, *kinds):
125
+ """Declares, on an adapter class, a contract it implements and the kinds of it it is.
126
+
127
+ @sw.implements("orders", 1, "processor")
128
+ class Orders: ...
129
+
130
+ A host application's contract package does this for its own base classes."""
131
+
132
+ if not contract or not isinstance(version, int):
133
+ raise ValueError("a contract needs a name and an integer version")
134
+
135
+ def mark(cls):
136
+ contracts = dict(vars(cls).get("__sw_contracts__", {}))
137
+ contracts[contract] = version
138
+ cls.__sw_contracts__ = contracts
139
+ cls.__sw_kinds__ = list(dict.fromkeys([*vars(cls).get("__sw_kinds__", []), *kinds]))
140
+ return cls
141
+
142
+ return mark
143
+
144
+
145
+ def commands_of(adapter):
146
+ """Every command on the adapter's class, by wire name. Kinds' base classes add their own."""
147
+ found = {}
148
+ cls = adapter if isinstance(adapter, type) else type(adapter)
149
+ for klass in reversed(cls.__mro__):
150
+ for attr, value in vars(klass).items():
151
+ marker = getattr(value, "__sw_command__", None)
152
+ if marker:
153
+ found[marker["name"]] = attr
154
+ result = {}
155
+ for wire_name, attr in found.items():
156
+ method = getattr(adapter, attr)
157
+ marker = getattr(getattr(cls, attr), "__sw_command__", None) or {}
158
+ result[wire_name] = Command(wire_name, method, marker.get("description"))
159
+ return result
160
+
161
+
162
+ def kinds_of(cls):
163
+ kinds = []
164
+ for klass in cls.__mro__:
165
+ for kind in getattr(klass, "__sw_kinds__", ()) or ():
166
+ if kind not in kinds:
167
+ kinds.append(kind)
168
+ return kinds
169
+
170
+
171
+ def contracts_of(cls):
172
+ contracts = {}
173
+ for klass in reversed(cls.__mro__):
174
+ contracts.update(getattr(klass, "__sw_contracts__", {}) or {})
175
+ return contracts
176
+
177
+
178
+ def is_resident(cls):
179
+ """Resident when it has a start hook: it runs until stopped rather than for one session."""
180
+ return callable(getattr(cls, "start", None))
@@ -0,0 +1,186 @@
1
+ """HPACK (RFC 7541): enough to talk to the host.
2
+
3
+ The adapter sends one set of request headers, encoded as literals without indexing, which every
4
+ decoder accepts. What the host sends back is decoded in full: static and dynamic tables, size
5
+ updates and Huffman-coded strings, since a server is free to use all of them.
6
+ """
7
+
8
+ from ._huffman import CODES
9
+
10
+ STATIC_TABLE = (
11
+ (":authority", ""), (":method", "GET"), (":method", "POST"), (":path", "/"),
12
+ (":path", "/index.html"), (":scheme", "http"), (":scheme", "https"), (":status", "200"),
13
+ (":status", "204"), (":status", "206"), (":status", "304"), (":status", "400"),
14
+ (":status", "404"), (":status", "500"), ("accept-charset", ""), ("accept-encoding", "gzip, deflate"),
15
+ ("accept-language", ""), ("accept-ranges", ""), ("accept", ""), ("access-control-allow-origin", ""),
16
+ ("age", ""), ("allow", ""), ("authorization", ""), ("cache-control", ""),
17
+ ("content-disposition", ""), ("content-encoding", ""), ("content-language", ""), ("content-length", ""),
18
+ ("content-location", ""), ("content-range", ""), ("content-type", ""), ("cookie", ""),
19
+ ("date", ""), ("etag", ""), ("expect", ""), ("expires", ""),
20
+ ("from", ""), ("host", ""), ("if-match", ""), ("if-modified-since", ""),
21
+ ("if-none-match", ""), ("if-range", ""), ("if-unmodified-since", ""), ("last-modified", ""),
22
+ ("link", ""), ("location", ""), ("max-forwards", ""), ("proxy-authenticate", ""),
23
+ ("proxy-authorization", ""), ("range", ""), ("referer", ""), ("refresh", ""),
24
+ ("retry-after", ""), ("server", ""), ("set-cookie", ""), ("strict-transport-security", ""),
25
+ ("transfer-encoding", ""), ("user-agent", ""), ("vary", ""), ("via", ""),
26
+ ("www-authenticate", ""),
27
+ )
28
+
29
+
30
+ class HpackError(Exception):
31
+ pass
32
+
33
+
34
+ def _huffman_tree():
35
+ # A binary trie over the codes: each node is [child0, child1, symbol].
36
+ root = [None, None, None]
37
+ for symbol, (code, length) in enumerate(CODES):
38
+ node = root
39
+ for bit in range(length - 1, -1, -1):
40
+ b = (code >> bit) & 1
41
+ if node[b] is None:
42
+ node[b] = [None, None, None]
43
+ node = node[b]
44
+ node[2] = symbol
45
+ return root
46
+
47
+
48
+ _TREE = _huffman_tree()
49
+
50
+
51
+ def huffman_decode(data):
52
+ out = bytearray()
53
+ node = _TREE
54
+ depth = 0
55
+ for byte in data:
56
+ for bit in range(7, -1, -1):
57
+ node = node[(byte >> bit) & 1]
58
+ depth += 1
59
+ if node is None:
60
+ raise HpackError("invalid Huffman code")
61
+ if node[2] is not None:
62
+ if node[2] == 256:
63
+ raise HpackError("EOS symbol in a Huffman string")
64
+ out.append(node[2])
65
+ node = _TREE
66
+ depth = 0
67
+ # Padding is the most significant bits of EOS (all ones), and shorter than a byte.
68
+ if depth > 7:
69
+ raise HpackError("Huffman padding longer than 7 bits")
70
+ return bytes(out)
71
+
72
+
73
+ def _decode_int(data, pos, prefix_bits):
74
+ mask = (1 << prefix_bits) - 1
75
+ value = data[pos] & mask
76
+ pos += 1
77
+ if value < mask:
78
+ return value, pos
79
+ shift = 0
80
+ while True:
81
+ if pos >= len(data):
82
+ raise HpackError("truncated integer")
83
+ byte = data[pos]
84
+ pos += 1
85
+ value += (byte & 0x7F) << shift
86
+ shift += 7
87
+ if not byte & 0x80:
88
+ return value, pos
89
+
90
+
91
+ def _encode_int(value, prefix_bits, first_byte_flags=0):
92
+ mask = (1 << prefix_bits) - 1
93
+ if value < mask:
94
+ return bytes([first_byte_flags | value])
95
+ out = bytearray([first_byte_flags | mask])
96
+ value -= mask
97
+ while value >= 0x80:
98
+ out.append((value & 0x7F) | 0x80)
99
+ value >>= 7
100
+ out.append(value)
101
+ return bytes(out)
102
+
103
+
104
+ def _decode_string(data, pos):
105
+ if pos >= len(data):
106
+ raise HpackError("truncated string")
107
+ huffman = data[pos] & 0x80
108
+ length, pos = _decode_int(data, pos, 7)
109
+ raw = data[pos:pos + length]
110
+ if len(raw) != length:
111
+ raise HpackError("truncated string")
112
+ pos += length
113
+ return (huffman_decode(raw) if huffman else bytes(raw)).decode("latin-1"), pos
114
+
115
+
116
+ def _encode_string(text):
117
+ raw = text.encode("latin-1")
118
+ return _encode_int(len(raw), 7) + raw
119
+
120
+
121
+ def encode(headers):
122
+ """Headers as literals without indexing: never touch either side's dynamic table."""
123
+ out = bytearray()
124
+ for name, value in headers:
125
+ out += b"\x00" + _encode_string(name) + _encode_string(value)
126
+ return bytes(out)
127
+
128
+
129
+ class Decoder:
130
+ def __init__(self, max_size=4096):
131
+ self.max_size = max_size
132
+ self.size = 0
133
+ self.dynamic = [] # newest first
134
+
135
+ @staticmethod
136
+ def _entry_size(name, value):
137
+ return len(name.encode("latin-1")) + len(value.encode("latin-1")) + 32
138
+
139
+ def _evict(self):
140
+ while self.size > self.max_size and self.dynamic:
141
+ name, value = self.dynamic.pop()
142
+ self.size -= self._entry_size(name, value)
143
+
144
+ def _add(self, name, value):
145
+ self.dynamic.insert(0, (name, value))
146
+ self.size += self._entry_size(name, value)
147
+ self._evict()
148
+
149
+ def _lookup(self, index):
150
+ if index <= 0:
151
+ raise HpackError("index 0")
152
+ if index <= len(STATIC_TABLE):
153
+ return STATIC_TABLE[index - 1]
154
+ index -= len(STATIC_TABLE) + 1
155
+ if index >= len(self.dynamic):
156
+ raise HpackError("index past the dynamic table")
157
+ return self.dynamic[index]
158
+
159
+ def decode(self, data):
160
+ headers = []
161
+ pos = 0
162
+ while pos < len(data):
163
+ byte = data[pos]
164
+ if byte & 0x80: # indexed
165
+ index, pos = _decode_int(data, pos, 7)
166
+ headers.append(self._lookup(index))
167
+ elif byte & 0x40: # literal, incremental indexing
168
+ index, pos = _decode_int(data, pos, 6)
169
+ name = self._lookup(index)[0] if index else None
170
+ if name is None:
171
+ name, pos = _decode_string(data, pos)
172
+ value, pos = _decode_string(data, pos)
173
+ self._add(name, value)
174
+ headers.append((name, value))
175
+ elif byte & 0x20: # dynamic table size update
176
+ size, pos = _decode_int(data, pos, 5)
177
+ self.max_size = size
178
+ self._evict()
179
+ else: # literal without indexing (0000) or never indexed (0001)
180
+ index, pos = _decode_int(data, pos, 4)
181
+ name = self._lookup(index)[0] if index else None
182
+ if name is None:
183
+ name, pos = _decode_string(data, pos)
184
+ value, pos = _decode_string(data, pos)
185
+ headers.append((name, value))
186
+ return headers
@@ -0,0 +1,253 @@
1
+ """One gRPC bidirectional stream over HTTP/2 cleartext (h2c, prior knowledge), on asyncio.
2
+
3
+ The host serves gRPC on a Unix domain socket and the adapter opens exactly one call on it, Attach,
4
+ for its whole life. That is all this implements: one client stream, the frames it needs, and flow
5
+ control in both directions — the part that matters, since a result can be 64 MB and the default
6
+ window is 64 KB.
7
+ """
8
+
9
+ import asyncio
10
+ import struct
11
+
12
+ from . import _hpack
13
+
14
+ PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
15
+
16
+ DATA, HEADERS, PRIORITY, RST_STREAM, SETTINGS, PUSH_PROMISE, PING, GOAWAY, WINDOW_UPDATE, CONTINUATION = range(10)
17
+ END_STREAM, ACK, END_HEADERS, PADDED, PRIORITY_FLAG = 0x1, 0x1, 0x4, 0x8, 0x20
18
+
19
+ SETTINGS_HEADER_TABLE_SIZE, SETTINGS_INITIAL_WINDOW_SIZE, SETTINGS_MAX_FRAME_SIZE = 0x1, 0x4, 0x5
20
+
21
+ STREAM_ID = 1
22
+ DEFAULT_WINDOW = 65535
23
+ # What the adapter lets the host send before it acknowledges: large, so a big payload is not
24
+ # trickled through 64 KB at a time.
25
+ RECEIVE_WINDOW = 16 * 1024 * 1024
26
+ MAX_MESSAGE = 64 * 1024 * 1024
27
+
28
+
29
+ class GrpcError(Exception):
30
+ def __init__(self, status, message):
31
+ super().__init__(f"gRPC status {status}: {message}")
32
+ self.status = status
33
+
34
+
35
+ class GrpcStream:
36
+ """Attach: write messages, read messages, and know when the host has ended the call."""
37
+
38
+ def __init__(self, reader, writer, path):
39
+ self._reader = reader
40
+ self._writer = writer
41
+ self._path = path
42
+ self._decoder = _hpack.Decoder()
43
+ # Messages go out whole and in order, under the message lock, which may wait for window.
44
+ # Every socket write takes only the write lock, briefly, so the read loop can still
45
+ # acknowledge frames — and so receive the WINDOW_UPDATE that wait is for.
46
+ self._message_lock = asyncio.Lock()
47
+ self._write_lock = asyncio.Lock()
48
+ self._window_changed = asyncio.Condition()
49
+ self._conn_send_window = DEFAULT_WINDOW
50
+ self._stream_send_window = DEFAULT_WINDOW
51
+ self._peer_initial_window = DEFAULT_WINDOW
52
+ self._peer_max_frame = 16384
53
+ self._inbound = asyncio.Queue()
54
+ self._buffer = bytearray()
55
+ self._header_block = bytearray()
56
+ self._header_stream = 0
57
+ self._header_end_stream = False
58
+ self._closed = False
59
+ self._read_task = None
60
+
61
+ @classmethod
62
+ async def open(cls, socket_path, path):
63
+ reader, writer = await asyncio.open_unix_connection(socket_path, limit=2 ** 20)
64
+ stream = cls(reader, writer, path)
65
+ await stream._start()
66
+ return stream
67
+
68
+ # ------------------------------------------------------------------ framing
69
+
70
+ def _frame(self, frame_type, flags, stream_id, payload=b""):
71
+ return struct.pack(">I", len(payload))[1:] + bytes([frame_type, flags]) + struct.pack(">I", stream_id) + payload
72
+
73
+ async def _write(self, data):
74
+ async with self._write_lock:
75
+ self._writer.write(data)
76
+ await self._writer.drain()
77
+
78
+ async def _start(self):
79
+ settings = struct.pack(">HI", SETTINGS_INITIAL_WINDOW_SIZE, RECEIVE_WINDOW)
80
+ # Raise the connection-level window too; it starts at 64 KB whatever SETTINGS says.
81
+ grow = struct.pack(">I", RECEIVE_WINDOW - DEFAULT_WINDOW)
82
+ headers = _hpack.encode([
83
+ (":method", "POST"), (":scheme", "http"), (":path", self._path), (":authority", "localhost"),
84
+ ("content-type", "application/grpc"), ("te", "trailers"),
85
+ ("user-agent", "sw-serverless-python"),
86
+ ])
87
+ await self._write(
88
+ PREFACE
89
+ + self._frame(SETTINGS, 0, 0, settings)
90
+ + self._frame(WINDOW_UPDATE, 0, 0, grow)
91
+ + self._frame(HEADERS, END_HEADERS, STREAM_ID, headers))
92
+ self._read_task = asyncio.get_running_loop().create_task(self._read_loop())
93
+
94
+ # ------------------------------------------------------------------ sending
95
+
96
+ async def send(self, message):
97
+ if len(message) > MAX_MESSAGE:
98
+ raise ValueError(f"a message of {len(message)} bytes is more than the {MAX_MESSAGE} the host accepts")
99
+ data = b"\x00" + struct.pack(">I", len(message)) + message
100
+ async with self._message_lock:
101
+ pos = 0
102
+ while pos < len(data):
103
+ async with self._window_changed:
104
+ await self._window_changed.wait_for(
105
+ lambda: self._closed or (self._conn_send_window > 0 and self._stream_send_window > 0))
106
+ if self._closed:
107
+ raise ConnectionError("the host closed the stream")
108
+ size = min(len(data) - pos, self._conn_send_window, self._stream_send_window, self._peer_max_frame)
109
+ self._conn_send_window -= size
110
+ self._stream_send_window -= size
111
+ await self._write(self._frame(DATA, 0, STREAM_ID, data[pos:pos + size]))
112
+ pos += size
113
+
114
+ async def close_send(self):
115
+ async with self._message_lock:
116
+ if not self._closed:
117
+ try:
118
+ await self._write(self._frame(DATA, END_STREAM, STREAM_ID))
119
+ except (ConnectionError, OSError):
120
+ pass
121
+
122
+ # ------------------------------------------------------------------ receiving
123
+
124
+ async def receive(self):
125
+ """The next message, or None once the host has ended the call."""
126
+ item = await self._inbound.get()
127
+ if isinstance(item, Exception):
128
+ raise item
129
+ return item
130
+
131
+ async def _read_loop(self):
132
+ try:
133
+ while True:
134
+ head = await self._reader.readexactly(9)
135
+ length = int.from_bytes(head[:3], "big")
136
+ frame_type, flags = head[3], head[4]
137
+ stream_id = int.from_bytes(head[5:9], "big") & 0x7FFFFFFF
138
+ payload = await self._reader.readexactly(length) if length else b""
139
+ if await self._on_frame(frame_type, flags, stream_id, payload):
140
+ break
141
+ await self._inbound.put(None)
142
+ except (asyncio.IncompleteReadError, ConnectionError, OSError):
143
+ await self._inbound.put(None)
144
+ except Exception as ex: # a protocol error: end the call rather than hang
145
+ await self._inbound.put(ex)
146
+ finally:
147
+ await self._mark_closed()
148
+
149
+ async def _mark_closed(self):
150
+ self._closed = True
151
+ async with self._window_changed:
152
+ self._window_changed.notify_all()
153
+
154
+ async def _on_frame(self, frame_type, flags, stream_id, payload):
155
+ """True when the call is over."""
156
+ if frame_type == SETTINGS:
157
+ if not flags & ACK:
158
+ await self._apply_settings(payload)
159
+ await self._write(self._frame(SETTINGS, ACK, 0))
160
+ elif frame_type == PING:
161
+ if not flags & ACK:
162
+ await self._write(self._frame(PING, ACK, 0, payload))
163
+ elif frame_type == WINDOW_UPDATE:
164
+ increment = int.from_bytes(payload[:4], "big") & 0x7FFFFFFF
165
+ async with self._window_changed:
166
+ if stream_id == 0:
167
+ self._conn_send_window += increment
168
+ elif stream_id == STREAM_ID:
169
+ self._stream_send_window += increment
170
+ self._window_changed.notify_all()
171
+ elif frame_type in (HEADERS, CONTINUATION):
172
+ if frame_type == HEADERS:
173
+ payload = self._strip(flags, payload, headers=True)
174
+ self._header_block = bytearray(payload)
175
+ self._header_stream = stream_id
176
+ self._header_end_stream = bool(flags & END_STREAM)
177
+ else:
178
+ self._header_block += payload
179
+ if flags & END_HEADERS:
180
+ headers = self._decoder.decode(bytes(self._header_block))
181
+ if self._header_stream == STREAM_ID:
182
+ self._check_headers(headers)
183
+ if self._header_end_stream:
184
+ return True
185
+ elif frame_type == DATA:
186
+ data = self._strip(flags, payload)
187
+ if length := len(payload):
188
+ # Hand the window straight back: messages are consumed as they are parsed.
189
+ increment = struct.pack(">I", length)
190
+ await self._write(self._frame(WINDOW_UPDATE, 0, 0, increment)
191
+ + self._frame(WINDOW_UPDATE, 0, STREAM_ID, increment))
192
+ if stream_id == STREAM_ID:
193
+ self._buffer += data
194
+ await self._drain_messages()
195
+ if flags & END_STREAM:
196
+ return True
197
+ elif frame_type == RST_STREAM and stream_id == STREAM_ID:
198
+ code = int.from_bytes(payload[:4], "big")
199
+ raise GrpcError(code, "the host reset the stream")
200
+ elif frame_type == GOAWAY:
201
+ return True
202
+ return False
203
+
204
+ async def _apply_settings(self, payload):
205
+ for i in range(0, len(payload) - 5, 6):
206
+ ident, value = struct.unpack(">HI", payload[i:i + 6])
207
+ if ident == SETTINGS_INITIAL_WINDOW_SIZE:
208
+ async with self._window_changed:
209
+ self._stream_send_window += value - self._peer_initial_window
210
+ self._peer_initial_window = value
211
+ self._window_changed.notify_all()
212
+ elif ident == SETTINGS_MAX_FRAME_SIZE:
213
+ self._peer_max_frame = value
214
+
215
+ @staticmethod
216
+ def _strip(flags, payload, headers=False):
217
+ pad = 0
218
+ if flags & PADDED:
219
+ pad = payload[0]
220
+ payload = payload[1:]
221
+ if headers and flags & PRIORITY_FLAG:
222
+ payload = payload[5:]
223
+ return payload[:len(payload) - pad] if pad else payload
224
+
225
+ def _check_headers(self, headers):
226
+ values = dict(headers)
227
+ status = values.get(":status")
228
+ if status is not None and status != "200":
229
+ raise GrpcError(-1, f"HTTP status {status}")
230
+ grpc_status = values.get("grpc-status")
231
+ if grpc_status is not None and grpc_status != "0":
232
+ raise GrpcError(int(grpc_status), values.get("grpc-message", ""))
233
+
234
+ async def _drain_messages(self):
235
+ while len(self._buffer) >= 5:
236
+ if self._buffer[0] != 0:
237
+ raise GrpcError(-1, "compressed messages are not supported")
238
+ length = int.from_bytes(self._buffer[1:5], "big")
239
+ if len(self._buffer) < 5 + length:
240
+ return
241
+ message = bytes(self._buffer[5:5 + length])
242
+ del self._buffer[:5 + length]
243
+ await self._inbound.put(message)
244
+
245
+ async def aclose(self):
246
+ await self.close_send()
247
+ try:
248
+ self._writer.close()
249
+ await self._writer.wait_closed()
250
+ except (ConnectionError, OSError):
251
+ pass
252
+ if self._read_task:
253
+ self._read_task.cancel()
@@ -0,0 +1,37 @@
1
+ """The HPACK Huffman code, RFC 7541 Appendix B: (code, bit length) for each symbol 0-256."""
2
+
3
+ CODES = (
4
+ (0x1ff8, 13), (0x7fffd8, 23), (0xfffffe2, 28), (0xfffffe3, 28), (0xfffffe4, 28), (0xfffffe5, 28), (0xfffffe6, 28), (0xfffffe7, 28),
5
+ (0xfffffe8, 28), (0xffffea, 24), (0x3ffffffc, 30), (0xfffffe9, 28), (0xfffffea, 28), (0x3ffffffd, 30), (0xfffffeb, 28), (0xfffffec, 28),
6
+ (0xfffffed, 28), (0xfffffee, 28), (0xfffffef, 28), (0xffffff0, 28), (0xffffff1, 28), (0xffffff2, 28), (0x3ffffffe, 30), (0xffffff3, 28),
7
+ (0xffffff4, 28), (0xffffff5, 28), (0xffffff6, 28), (0xffffff7, 28), (0xffffff8, 28), (0xffffff9, 28), (0xffffffa, 28), (0xffffffb, 28),
8
+ (0x14, 6), (0x3f8, 10), (0x3f9, 10), (0xffa, 12), (0x1ff9, 13), (0x15, 6), (0xf8, 8), (0x7fa, 11),
9
+ (0x3fa, 10), (0x3fb, 10), (0xf9, 8), (0x7fb, 11), (0xfa, 8), (0x16, 6), (0x17, 6), (0x18, 6),
10
+ (0x0, 5), (0x1, 5), (0x2, 5), (0x19, 6), (0x1a, 6), (0x1b, 6), (0x1c, 6), (0x1d, 6),
11
+ (0x1e, 6), (0x1f, 6), (0x5c, 7), (0xfb, 8), (0x7ffc, 15), (0x20, 6), (0xffb, 12), (0x3fc, 10),
12
+ (0x1ffa, 13), (0x21, 6), (0x5d, 7), (0x5e, 7), (0x5f, 7), (0x60, 7), (0x61, 7), (0x62, 7),
13
+ (0x63, 7), (0x64, 7), (0x65, 7), (0x66, 7), (0x67, 7), (0x68, 7), (0x69, 7), (0x6a, 7),
14
+ (0x6b, 7), (0x6c, 7), (0x6d, 7), (0x6e, 7), (0x6f, 7), (0x70, 7), (0x71, 7), (0x72, 7),
15
+ (0xfc, 8), (0x73, 7), (0xfd, 8), (0x1ffb, 13), (0x7fff0, 19), (0x1ffc, 13), (0x3ffc, 14), (0x22, 6),
16
+ (0x7ffd, 15), (0x3, 5), (0x23, 6), (0x4, 5), (0x24, 6), (0x5, 5), (0x25, 6), (0x26, 6),
17
+ (0x27, 6), (0x6, 5), (0x74, 7), (0x75, 7), (0x28, 6), (0x29, 6), (0x2a, 6), (0x7, 5),
18
+ (0x2b, 6), (0x76, 7), (0x2c, 6), (0x8, 5), (0x9, 5), (0x2d, 6), (0x77, 7), (0x78, 7),
19
+ (0x79, 7), (0x7a, 7), (0x7b, 7), (0x7ffe, 15), (0x7fc, 11), (0x3ffd, 14), (0x1ffd, 13), (0xffffffc, 28),
20
+ (0xfffe6, 20), (0x3fffd2, 22), (0xfffe7, 20), (0xfffe8, 20), (0x3fffd3, 22), (0x3fffd4, 22), (0x3fffd5, 22), (0x7fffd9, 23),
21
+ (0x3fffd6, 22), (0x7fffda, 23), (0x7fffdb, 23), (0x7fffdc, 23), (0x7fffdd, 23), (0x7fffde, 23), (0xffffeb, 24), (0x7fffdf, 23),
22
+ (0xffffec, 24), (0xffffed, 24), (0x3fffd7, 22), (0x7fffe0, 23), (0xffffee, 24), (0x7fffe1, 23), (0x7fffe2, 23), (0x7fffe3, 23),
23
+ (0x7fffe4, 23), (0x1fffdc, 21), (0x3fffd8, 22), (0x7fffe5, 23), (0x3fffd9, 22), (0x7fffe6, 23), (0x7fffe7, 23), (0xffffef, 24),
24
+ (0x3fffda, 22), (0x1fffdd, 21), (0xfffe9, 20), (0x3fffdb, 22), (0x3fffdc, 22), (0x7fffe8, 23), (0x7fffe9, 23), (0x1fffde, 21),
25
+ (0x7fffea, 23), (0x3fffdd, 22), (0x3fffde, 22), (0xfffff0, 24), (0x1fffdf, 21), (0x3fffdf, 22), (0x7fffeb, 23), (0x7fffec, 23),
26
+ (0x1fffe0, 21), (0x1fffe1, 21), (0x3fffe0, 22), (0x1fffe2, 21), (0x7fffed, 23), (0x3fffe1, 22), (0x7fffee, 23), (0x7fffef, 23),
27
+ (0xfffea, 20), (0x3fffe2, 22), (0x3fffe3, 22), (0x3fffe4, 22), (0x7ffff0, 23), (0x3fffe5, 22), (0x3fffe6, 22), (0x7ffff1, 23),
28
+ (0x3ffffe0, 26), (0x3ffffe1, 26), (0xfffeb, 20), (0x7fff1, 19), (0x3fffe7, 22), (0x7ffff2, 23), (0x3fffe8, 22), (0x1ffffec, 25),
29
+ (0x3ffffe2, 26), (0x3ffffe3, 26), (0x3ffffe4, 26), (0x7ffffde, 27), (0x7ffffdf, 27), (0x3ffffe5, 26), (0xfffff1, 24), (0x1ffffed, 25),
30
+ (0x7fff2, 19), (0x1fffe3, 21), (0x3ffffe6, 26), (0x7ffffe0, 27), (0x7ffffe1, 27), (0x3ffffe7, 26), (0x7ffffe2, 27), (0xfffff2, 24),
31
+ (0x1fffe4, 21), (0x1fffe5, 21), (0x3ffffe8, 26), (0x3ffffe9, 26), (0xffffffd, 28), (0x7ffffe3, 27), (0x7ffffe4, 27), (0x7ffffe5, 27),
32
+ (0xfffec, 20), (0xfffff3, 24), (0xfffed, 20), (0x1fffe6, 21), (0x3fffe9, 22), (0x1fffe7, 21), (0x1fffe8, 21), (0x7ffff3, 23),
33
+ (0x3fffea, 22), (0x3fffeb, 22), (0x1ffffee, 25), (0x1ffffef, 25), (0xfffff4, 24), (0xfffff5, 24), (0x3ffffea, 26), (0x7ffff4, 23),
34
+ (0x3ffffeb, 26), (0x7ffffe6, 27), (0x3ffffec, 26), (0x3ffffed, 26), (0x7ffffe7, 27), (0x7ffffe8, 27), (0x7ffffe9, 27), (0x7ffffea, 27),
35
+ (0x7ffffeb, 27), (0xffffffe, 28), (0x7ffffec, 27), (0x7ffffed, 27), (0x7ffffee, 27), (0x7ffffef, 27), (0x7fffff0, 27), (0x3ffffee, 26),
36
+ (0x3fffffff, 30),
37
+ )