tigrcorn-protocols 0.3.16.dev5__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.
- tigrcorn_protocols/__init__.py +1 -0
- tigrcorn_protocols/_compression.py +219 -0
- tigrcorn_protocols/connect.py +107 -0
- tigrcorn_protocols/content_coding.py +179 -0
- tigrcorn_protocols/custom/__init__.py +3 -0
- tigrcorn_protocols/custom/adapters.py +18 -0
- tigrcorn_protocols/custom/registry.py +15 -0
- tigrcorn_protocols/flow/__init__.py +1 -0
- tigrcorn_protocols/flow/backpressure.py +17 -0
- tigrcorn_protocols/flow/buffers.py +29 -0
- tigrcorn_protocols/flow/credits.py +21 -0
- tigrcorn_protocols/flow/keepalive.py +85 -0
- tigrcorn_protocols/flow/timeouts.py +17 -0
- tigrcorn_protocols/flow/watermarks.py +16 -0
- tigrcorn_protocols/http1/__init__.py +16 -0
- tigrcorn_protocols/http1/keepalive.py +21 -0
- tigrcorn_protocols/http1/parser.py +481 -0
- tigrcorn_protocols/http1/serializer.py +198 -0
- tigrcorn_protocols/http1/state.py +9 -0
- tigrcorn_protocols/http2/__init__.py +16 -0
- tigrcorn_protocols/http2/codec.py +266 -0
- tigrcorn_protocols/http2/flow.py +35 -0
- tigrcorn_protocols/http2/handler.py +1303 -0
- tigrcorn_protocols/http2/hpack.py +393 -0
- tigrcorn_protocols/http2/state.py +226 -0
- tigrcorn_protocols/http2/streams.py +76 -0
- tigrcorn_protocols/http2/websocket.py +360 -0
- tigrcorn_protocols/http3/__init__.py +82 -0
- tigrcorn_protocols/http3/codec.py +148 -0
- tigrcorn_protocols/http3/handler/__init__.py +3 -0
- tigrcorn_protocols/http3/handler/core.py +1823 -0
- tigrcorn_protocols/http3/handler/webtransport.py +184 -0
- tigrcorn_protocols/http3/handler.py +3 -0
- tigrcorn_protocols/http3/qpack.py +843 -0
- tigrcorn_protocols/http3/state.py +129 -0
- tigrcorn_protocols/http3/streams.py +657 -0
- tigrcorn_protocols/http3/websocket.py +360 -0
- tigrcorn_protocols/lifespan/__init__.py +3 -0
- tigrcorn_protocols/lifespan/driver.py +83 -0
- tigrcorn_protocols/py.typed +1 -0
- tigrcorn_protocols/rawframed/__init__.py +5 -0
- tigrcorn_protocols/rawframed/codec.py +18 -0
- tigrcorn_protocols/rawframed/frames.py +28 -0
- tigrcorn_protocols/rawframed/handler.py +72 -0
- tigrcorn_protocols/rawframed/state.py +9 -0
- tigrcorn_protocols/registry.py +22 -0
- tigrcorn_protocols/scheduler/__init__.py +17 -0
- tigrcorn_protocols/scheduler/cancellation.py +40 -0
- tigrcorn_protocols/scheduler/dispatch.py +27 -0
- tigrcorn_protocols/scheduler/fairness.py +21 -0
- tigrcorn_protocols/scheduler/policy.py +12 -0
- tigrcorn_protocols/scheduler/priorities.py +8 -0
- tigrcorn_protocols/scheduler/quotas.py +19 -0
- tigrcorn_protocols/scheduler/runtime.py +156 -0
- tigrcorn_protocols/scheduler/tasks.py +31 -0
- tigrcorn_protocols/sessions/__init__.py +1 -0
- tigrcorn_protocols/sessions/base.py +16 -0
- tigrcorn_protocols/sessions/connection.py +12 -0
- tigrcorn_protocols/sessions/limits.py +12 -0
- tigrcorn_protocols/sessions/manager.py +31 -0
- tigrcorn_protocols/sessions/metadata.py +10 -0
- tigrcorn_protocols/sessions/quic.py +14 -0
- tigrcorn_protocols/streams/__init__.py +1 -0
- tigrcorn_protocols/streams/base.py +13 -0
- tigrcorn_protocols/streams/ids.py +5 -0
- tigrcorn_protocols/streams/multiplex.py +6 -0
- tigrcorn_protocols/streams/registry.py +22 -0
- tigrcorn_protocols/streams/singleplex.py +6 -0
- tigrcorn_protocols/websocket/__init__.py +1 -0
- tigrcorn_protocols/websocket/codec.py +31 -0
- tigrcorn_protocols/websocket/extensions.py +324 -0
- tigrcorn_protocols/websocket/frames.py +174 -0
- tigrcorn_protocols/websocket/handler.py +462 -0
- tigrcorn_protocols/websocket/handshake.py +66 -0
- tigrcorn_protocols/websocket/state.py +10 -0
- tigrcorn_protocols-0.3.16.dev5.dist-info/METADATA +240 -0
- tigrcorn_protocols-0.3.16.dev5.dist-info/RECORD +80 -0
- tigrcorn_protocols-0.3.16.dev5.dist-info/WHEEL +5 -0
- tigrcorn_protocols-0.3.16.dev5.dist-info/licenses/LICENSE +163 -0
- tigrcorn_protocols-0.3.16.dev5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Awaitable
|
|
5
|
+
|
|
6
|
+
from .policy import SchedulerPolicy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TaskDispatcher:
|
|
10
|
+
def __init__(self, policy: SchedulerPolicy | None = None) -> None:
|
|
11
|
+
self.policy = policy or SchedulerPolicy()
|
|
12
|
+
self.tasks: set[asyncio.Task] = set()
|
|
13
|
+
|
|
14
|
+
def spawn(self, coro: Awaitable):
|
|
15
|
+
if len(self.tasks) >= self.policy.max_tasks:
|
|
16
|
+
close = getattr(coro, 'close', None)
|
|
17
|
+
if callable(close):
|
|
18
|
+
close()
|
|
19
|
+
raise RuntimeError('task quota exceeded')
|
|
20
|
+
task = asyncio.create_task(coro)
|
|
21
|
+
self.tasks.add(task)
|
|
22
|
+
task.add_done_callback(self.tasks.discard)
|
|
23
|
+
return task
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def spawn(coro: Awaitable):
|
|
27
|
+
return asyncio.create_task(coro)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
T = TypeVar('T')
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class FairnessPolicy(Generic[T]):
|
|
12
|
+
round_robin: bool = True
|
|
13
|
+
_queue: deque[T] = field(default_factory=deque)
|
|
14
|
+
|
|
15
|
+
def push(self, item: T) -> None:
|
|
16
|
+
self._queue.append(item)
|
|
17
|
+
|
|
18
|
+
def pop(self) -> T | None:
|
|
19
|
+
if not self._queue:
|
|
20
|
+
return None
|
|
21
|
+
return self._queue.popleft()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(slots=True)
|
|
7
|
+
class SchedulerPolicy:
|
|
8
|
+
max_connections: int = 10_000
|
|
9
|
+
max_tasks: int = 50_000
|
|
10
|
+
max_streams_per_session: int = 128
|
|
11
|
+
limit_concurrency: int | None = None
|
|
12
|
+
drain_on_shutdown: bool = True
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(slots=True)
|
|
7
|
+
class Quotas:
|
|
8
|
+
max_connections: int = 10_000
|
|
9
|
+
max_streams_per_connection: int = 128
|
|
10
|
+
current_connections: int = 0
|
|
11
|
+
|
|
12
|
+
def acquire_connection(self) -> bool:
|
|
13
|
+
if self.current_connections >= self.max_connections:
|
|
14
|
+
return False
|
|
15
|
+
self.current_connections += 1
|
|
16
|
+
return True
|
|
17
|
+
|
|
18
|
+
def release_connection(self) -> None:
|
|
19
|
+
self.current_connections = max(0, self.current_connections - 1)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Awaitable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .dispatch import TaskDispatcher
|
|
9
|
+
from .policy import SchedulerPolicy
|
|
10
|
+
from .quotas import Quotas
|
|
11
|
+
from .tasks import TaskSet
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(slots=True)
|
|
15
|
+
class ConnectionLease:
|
|
16
|
+
scheduler: "ProductionScheduler"
|
|
17
|
+
released: bool = False
|
|
18
|
+
|
|
19
|
+
def release(self) -> None:
|
|
20
|
+
if self.released:
|
|
21
|
+
return
|
|
22
|
+
self.scheduler.release_connection()
|
|
23
|
+
self.released = True
|
|
24
|
+
|
|
25
|
+
def __enter__(self) -> "ConnectionLease":
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
29
|
+
self.release()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class WorkLease:
|
|
34
|
+
scheduler: "ProductionScheduler"
|
|
35
|
+
released: bool = False
|
|
36
|
+
|
|
37
|
+
def release(self) -> None:
|
|
38
|
+
if self.released:
|
|
39
|
+
return
|
|
40
|
+
self.scheduler.release_work()
|
|
41
|
+
self.released = True
|
|
42
|
+
|
|
43
|
+
def __enter__(self) -> "WorkLease":
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
47
|
+
self.release()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ProductionScheduler:
|
|
51
|
+
"""Package-owned runtime scheduler for connection admission and task draining.
|
|
52
|
+
|
|
53
|
+
The scheduler keeps protocol code out of ad-hoc concurrency decisions. It owns:
|
|
54
|
+
- connection quotas
|
|
55
|
+
- task quotas
|
|
56
|
+
- global in-flight work admission (`limit_concurrency`)
|
|
57
|
+
- graceful shutdown / drain behavior
|
|
58
|
+
- task tracking for server-internal relay work
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, policy: SchedulerPolicy | None = None) -> None:
|
|
62
|
+
self.policy = policy or SchedulerPolicy()
|
|
63
|
+
self.dispatcher = TaskDispatcher(self.policy)
|
|
64
|
+
self.quotas = Quotas(
|
|
65
|
+
max_connections=self.policy.max_connections,
|
|
66
|
+
max_streams_per_connection=self.policy.max_streams_per_session,
|
|
67
|
+
)
|
|
68
|
+
self.tasks = TaskSet()
|
|
69
|
+
self._closed = False
|
|
70
|
+
self._draining = False
|
|
71
|
+
self._owners: dict[asyncio.Task[Any], str | None] = {}
|
|
72
|
+
self._inflight = 0
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def open_connections(self) -> int:
|
|
76
|
+
return self.quotas.current_connections
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def active_tasks(self) -> int:
|
|
80
|
+
return len(self.dispatcher.tasks)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def current_inflight(self) -> int:
|
|
84
|
+
return self._inflight
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def closed(self) -> bool:
|
|
88
|
+
return self._closed
|
|
89
|
+
|
|
90
|
+
def _can_admit_work(self) -> bool:
|
|
91
|
+
if self._closed or self._draining:
|
|
92
|
+
return False
|
|
93
|
+
limit = self.policy.limit_concurrency
|
|
94
|
+
return limit is None or self._inflight < limit
|
|
95
|
+
|
|
96
|
+
def acquire_connection(self) -> ConnectionLease | None:
|
|
97
|
+
if self._closed or self._draining:
|
|
98
|
+
return None
|
|
99
|
+
if not self.quotas.acquire_connection():
|
|
100
|
+
return None
|
|
101
|
+
return ConnectionLease(self)
|
|
102
|
+
|
|
103
|
+
def release_connection(self) -> None:
|
|
104
|
+
self.quotas.release_connection()
|
|
105
|
+
|
|
106
|
+
def acquire_work(self) -> WorkLease | None:
|
|
107
|
+
if not self._can_admit_work():
|
|
108
|
+
return None
|
|
109
|
+
self._inflight += 1
|
|
110
|
+
return WorkLease(self)
|
|
111
|
+
|
|
112
|
+
def release_work(self) -> None:
|
|
113
|
+
self._inflight = max(0, self._inflight - 1)
|
|
114
|
+
|
|
115
|
+
def spawn(self, coro: Awaitable[Any], *, owner: str | None = None) -> asyncio.Task[Any]:
|
|
116
|
+
if self._closed or self._draining:
|
|
117
|
+
close = getattr(coro, 'close', None)
|
|
118
|
+
if callable(close):
|
|
119
|
+
close()
|
|
120
|
+
raise RuntimeError('scheduler is closed')
|
|
121
|
+
lease = self.acquire_work()
|
|
122
|
+
if lease is None:
|
|
123
|
+
close = getattr(coro, 'close', None)
|
|
124
|
+
if callable(close):
|
|
125
|
+
close()
|
|
126
|
+
raise RuntimeError('concurrency limit exceeded')
|
|
127
|
+
try:
|
|
128
|
+
task = self.dispatcher.spawn(coro)
|
|
129
|
+
except Exception:
|
|
130
|
+
lease.release()
|
|
131
|
+
raise
|
|
132
|
+
self.tasks.add(task)
|
|
133
|
+
self._owners[task] = owner
|
|
134
|
+
task.add_done_callback(self._owners.pop)
|
|
135
|
+
task.add_done_callback(lambda _task: lease.release())
|
|
136
|
+
return task
|
|
137
|
+
|
|
138
|
+
async def wait(self) -> None:
|
|
139
|
+
if not self.dispatcher.tasks:
|
|
140
|
+
return
|
|
141
|
+
await asyncio.gather(*list(self.dispatcher.tasks), return_exceptions=True)
|
|
142
|
+
|
|
143
|
+
async def drain(self, *, cancel_running: bool | None = None) -> None:
|
|
144
|
+
if self._closed:
|
|
145
|
+
return
|
|
146
|
+
self._draining = True
|
|
147
|
+
if cancel_running is None:
|
|
148
|
+
cancel_running = not self.policy.drain_on_shutdown
|
|
149
|
+
if cancel_running:
|
|
150
|
+
await self.tasks.cancel_all()
|
|
151
|
+
else:
|
|
152
|
+
await self.wait()
|
|
153
|
+
self._closed = True
|
|
154
|
+
|
|
155
|
+
async def close(self) -> None:
|
|
156
|
+
await self.drain()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from tigrcorn_protocols.scheduler.cancellation import CancellationResult, cancel_many_bounded
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class TaskSet:
|
|
12
|
+
tasks: set[asyncio.Task] = field(default_factory=set)
|
|
13
|
+
|
|
14
|
+
def add(self, task: asyncio.Task) -> None:
|
|
15
|
+
self.tasks.add(task)
|
|
16
|
+
task.add_done_callback(self.tasks.discard)
|
|
17
|
+
|
|
18
|
+
async def cancel_all(self) -> None:
|
|
19
|
+
for task in list(self.tasks):
|
|
20
|
+
task.cancel()
|
|
21
|
+
for task in list(self.tasks):
|
|
22
|
+
with suppress(asyncio.CancelledError):
|
|
23
|
+
await task
|
|
24
|
+
|
|
25
|
+
async def cancel_all_bounded(self, *, timeout: float) -> CancellationResult:
|
|
26
|
+
return await cancel_many_bounded(list(self.tasks), timeout=timeout)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def cancel_tasks(tasks: list[asyncio.Task]) -> None:
|
|
30
|
+
taskset = TaskSet(set(tasks))
|
|
31
|
+
await taskset.cancel_all()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Session models."""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from time import monotonic
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(slots=True)
|
|
8
|
+
class BaseSession:
|
|
9
|
+
session_id: int
|
|
10
|
+
opened_at: float = field(default_factory=monotonic)
|
|
11
|
+
protocol: str = 'unknown'
|
|
12
|
+
closed_at: float | None = None
|
|
13
|
+
|
|
14
|
+
def close(self) -> None:
|
|
15
|
+
if self.closed_at is None:
|
|
16
|
+
self.closed_at = monotonic()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .base import BaseSession
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(slots=True)
|
|
9
|
+
class ConnectionSession(BaseSession):
|
|
10
|
+
protocol: str = 'tcp'
|
|
11
|
+
peer: tuple[str | None, int | None] | None = None
|
|
12
|
+
server: tuple[str | None, int | None] | None = None
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(slots=True)
|
|
7
|
+
class SessionLimits:
|
|
8
|
+
max_streams: int = 128
|
|
9
|
+
max_inflight_bytes: int = 1_048_576
|
|
10
|
+
|
|
11
|
+
def allow_stream(self, current: int) -> bool:
|
|
12
|
+
return current < self.max_streams
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
from tigrcorn_core.utils.ids import next_id
|
|
7
|
+
|
|
8
|
+
from .base import BaseSession
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(slots=True)
|
|
12
|
+
class SessionManager:
|
|
13
|
+
sessions: dict[int, BaseSession] = field(default_factory=dict)
|
|
14
|
+
counts: dict[str, int] = field(default_factory=lambda: defaultdict(int))
|
|
15
|
+
|
|
16
|
+
def open(self, session: BaseSession | None = None, *, protocol: str = 'unknown') -> BaseSession:
|
|
17
|
+
if session is None:
|
|
18
|
+
session = BaseSession(session_id=next_id(), protocol=protocol)
|
|
19
|
+
self.sessions[session.session_id] = session
|
|
20
|
+
self.counts[session.protocol] += 1
|
|
21
|
+
return session
|
|
22
|
+
|
|
23
|
+
def close(self, session_id: int) -> None:
|
|
24
|
+
session = self.sessions.pop(session_id, None)
|
|
25
|
+
if session is None:
|
|
26
|
+
return
|
|
27
|
+
session.close()
|
|
28
|
+
self.counts[session.protocol] = max(0, self.counts[session.protocol] - 1)
|
|
29
|
+
|
|
30
|
+
def snapshot(self) -> dict[str, int]:
|
|
31
|
+
return dict(self.counts)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .base import BaseSession
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(slots=True)
|
|
9
|
+
class QuicSession(BaseSession):
|
|
10
|
+
protocol: str = 'quic'
|
|
11
|
+
stream_count: int = 0
|
|
12
|
+
|
|
13
|
+
def opened_stream(self) -> None:
|
|
14
|
+
self.stream_count += 1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Logical stream models."""
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from .base import LogicalStream
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(slots=True)
|
|
9
|
+
class StreamRegistry:
|
|
10
|
+
streams: dict[int, LogicalStream] = field(default_factory=dict)
|
|
11
|
+
|
|
12
|
+
def add(self, stream: LogicalStream) -> LogicalStream:
|
|
13
|
+
self.streams[stream.stream_id] = stream
|
|
14
|
+
return stream
|
|
15
|
+
|
|
16
|
+
def get(self, stream_id: int) -> LogicalStream | None:
|
|
17
|
+
return self.streams.get(stream_id)
|
|
18
|
+
|
|
19
|
+
def close(self, stream_id: int) -> None:
|
|
20
|
+
stream = self.streams.pop(stream_id, None)
|
|
21
|
+
if stream is not None:
|
|
22
|
+
stream.close()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = []
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from tigrcorn_protocols.websocket.frames import (
|
|
4
|
+
OP_BINARY,
|
|
5
|
+
OP_CLOSE,
|
|
6
|
+
OP_PING,
|
|
7
|
+
OP_PONG,
|
|
8
|
+
OP_TEXT,
|
|
9
|
+
encode_close_payload,
|
|
10
|
+
serialize_frame,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def text_frame(text: str, *, rsv1: bool = False) -> bytes:
|
|
15
|
+
return serialize_frame(OP_TEXT, text.encode('utf-8'), rsv1=rsv1)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def binary_frame(data: bytes, *, rsv1: bool = False) -> bytes:
|
|
19
|
+
return serialize_frame(OP_BINARY, data, rsv1=rsv1)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def ping_frame(data: bytes = b'') -> bytes:
|
|
23
|
+
return serialize_frame(OP_PING, data)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def pong_frame(data: bytes = b'') -> bytes:
|
|
27
|
+
return serialize_frame(OP_PONG, data)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def close_frame(code: int = 1000, reason: str = '') -> bytes:
|
|
31
|
+
return serialize_frame(OP_CLOSE, encode_close_payload(code, reason))
|