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.
- sw_serverless/__init__.py +28 -0
- sw_serverless/_adapter.py +180 -0
- sw_serverless/_hpack.py +186 -0
- sw_serverless/_http2.py +253 -0
- sw_serverless/_huffman.py +37 -0
- sw_serverless/_runner.py +565 -0
- sw_serverless/_types.py +124 -0
- sw_serverless/_wire.py +242 -0
- sw_serverless-10.2.3.dist-info/METADATA +58 -0
- sw_serverless-10.2.3.dist-info/RECORD +12 -0
- sw_serverless-10.2.3.dist-info/WHEEL +5 -0
- sw_serverless-10.2.3.dist-info/top_level.txt +1 -0
|
@@ -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))
|
sw_serverless/_hpack.py
ADDED
|
@@ -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
|
sw_serverless/_http2.py
ADDED
|
@@ -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
|
+
)
|