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.
Files changed (80) hide show
  1. tigrcorn_protocols/__init__.py +1 -0
  2. tigrcorn_protocols/_compression.py +219 -0
  3. tigrcorn_protocols/connect.py +107 -0
  4. tigrcorn_protocols/content_coding.py +179 -0
  5. tigrcorn_protocols/custom/__init__.py +3 -0
  6. tigrcorn_protocols/custom/adapters.py +18 -0
  7. tigrcorn_protocols/custom/registry.py +15 -0
  8. tigrcorn_protocols/flow/__init__.py +1 -0
  9. tigrcorn_protocols/flow/backpressure.py +17 -0
  10. tigrcorn_protocols/flow/buffers.py +29 -0
  11. tigrcorn_protocols/flow/credits.py +21 -0
  12. tigrcorn_protocols/flow/keepalive.py +85 -0
  13. tigrcorn_protocols/flow/timeouts.py +17 -0
  14. tigrcorn_protocols/flow/watermarks.py +16 -0
  15. tigrcorn_protocols/http1/__init__.py +16 -0
  16. tigrcorn_protocols/http1/keepalive.py +21 -0
  17. tigrcorn_protocols/http1/parser.py +481 -0
  18. tigrcorn_protocols/http1/serializer.py +198 -0
  19. tigrcorn_protocols/http1/state.py +9 -0
  20. tigrcorn_protocols/http2/__init__.py +16 -0
  21. tigrcorn_protocols/http2/codec.py +266 -0
  22. tigrcorn_protocols/http2/flow.py +35 -0
  23. tigrcorn_protocols/http2/handler.py +1303 -0
  24. tigrcorn_protocols/http2/hpack.py +393 -0
  25. tigrcorn_protocols/http2/state.py +226 -0
  26. tigrcorn_protocols/http2/streams.py +76 -0
  27. tigrcorn_protocols/http2/websocket.py +360 -0
  28. tigrcorn_protocols/http3/__init__.py +82 -0
  29. tigrcorn_protocols/http3/codec.py +148 -0
  30. tigrcorn_protocols/http3/handler/__init__.py +3 -0
  31. tigrcorn_protocols/http3/handler/core.py +1823 -0
  32. tigrcorn_protocols/http3/handler/webtransport.py +184 -0
  33. tigrcorn_protocols/http3/handler.py +3 -0
  34. tigrcorn_protocols/http3/qpack.py +843 -0
  35. tigrcorn_protocols/http3/state.py +129 -0
  36. tigrcorn_protocols/http3/streams.py +657 -0
  37. tigrcorn_protocols/http3/websocket.py +360 -0
  38. tigrcorn_protocols/lifespan/__init__.py +3 -0
  39. tigrcorn_protocols/lifespan/driver.py +83 -0
  40. tigrcorn_protocols/py.typed +1 -0
  41. tigrcorn_protocols/rawframed/__init__.py +5 -0
  42. tigrcorn_protocols/rawframed/codec.py +18 -0
  43. tigrcorn_protocols/rawframed/frames.py +28 -0
  44. tigrcorn_protocols/rawframed/handler.py +72 -0
  45. tigrcorn_protocols/rawframed/state.py +9 -0
  46. tigrcorn_protocols/registry.py +22 -0
  47. tigrcorn_protocols/scheduler/__init__.py +17 -0
  48. tigrcorn_protocols/scheduler/cancellation.py +40 -0
  49. tigrcorn_protocols/scheduler/dispatch.py +27 -0
  50. tigrcorn_protocols/scheduler/fairness.py +21 -0
  51. tigrcorn_protocols/scheduler/policy.py +12 -0
  52. tigrcorn_protocols/scheduler/priorities.py +8 -0
  53. tigrcorn_protocols/scheduler/quotas.py +19 -0
  54. tigrcorn_protocols/scheduler/runtime.py +156 -0
  55. tigrcorn_protocols/scheduler/tasks.py +31 -0
  56. tigrcorn_protocols/sessions/__init__.py +1 -0
  57. tigrcorn_protocols/sessions/base.py +16 -0
  58. tigrcorn_protocols/sessions/connection.py +12 -0
  59. tigrcorn_protocols/sessions/limits.py +12 -0
  60. tigrcorn_protocols/sessions/manager.py +31 -0
  61. tigrcorn_protocols/sessions/metadata.py +10 -0
  62. tigrcorn_protocols/sessions/quic.py +14 -0
  63. tigrcorn_protocols/streams/__init__.py +1 -0
  64. tigrcorn_protocols/streams/base.py +13 -0
  65. tigrcorn_protocols/streams/ids.py +5 -0
  66. tigrcorn_protocols/streams/multiplex.py +6 -0
  67. tigrcorn_protocols/streams/registry.py +22 -0
  68. tigrcorn_protocols/streams/singleplex.py +6 -0
  69. tigrcorn_protocols/websocket/__init__.py +1 -0
  70. tigrcorn_protocols/websocket/codec.py +31 -0
  71. tigrcorn_protocols/websocket/extensions.py +324 -0
  72. tigrcorn_protocols/websocket/frames.py +174 -0
  73. tigrcorn_protocols/websocket/handler.py +462 -0
  74. tigrcorn_protocols/websocket/handshake.py +66 -0
  75. tigrcorn_protocols/websocket/state.py +10 -0
  76. tigrcorn_protocols-0.3.16.dev5.dist-info/METADATA +240 -0
  77. tigrcorn_protocols-0.3.16.dev5.dist-info/RECORD +80 -0
  78. tigrcorn_protocols-0.3.16.dev5.dist-info/WHEEL +5 -0
  79. tigrcorn_protocols-0.3.16.dev5.dist-info/licenses/LICENSE +163 -0
  80. 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,8 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True, order=True)
7
+ class Priority:
8
+ value: int = 0
@@ -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,10 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(slots=True)
7
+ class SessionMetadata:
8
+ listener_name: str = 'default'
9
+ transport: str = 'tcp'
10
+ label: str = ''
@@ -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,13 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(slots=True)
7
+ class LogicalStream:
8
+ stream_id: int
9
+ multiplexed: bool = False
10
+ open: bool = True
11
+
12
+ def close(self) -> None:
13
+ self.open = False
@@ -0,0 +1,5 @@
1
+ from tigrcorn_core.utils.ids import next_id
2
+
3
+
4
+ def next_stream_id() -> int:
5
+ return next_id()
@@ -0,0 +1,6 @@
1
+ from .base import LogicalStream
2
+
3
+
4
+ class MultiplexStream(LogicalStream):
5
+ def __init__(self, stream_id: int) -> None:
6
+ super().__init__(stream_id=stream_id, multiplexed=True)
@@ -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,6 @@
1
+ from .base import LogicalStream
2
+
3
+
4
+ class SingleplexStream(LogicalStream):
5
+ def __init__(self, stream_id: int = 1) -> None:
6
+ super().__init__(stream_id=stream_id, multiplexed=False)
@@ -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))