tigrcorn-config 0.3.18.dev2__tar.gz → 0.3.18.dev3__tar.gz

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 (44) hide show
  1. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/PKG-INFO +3 -3
  2. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/README.md +1 -1
  3. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/pyproject.toml +2 -2
  4. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/env.py +6 -0
  5. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/governance_surface.py +3 -3
  6. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/load/listeners.py +4 -0
  7. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/load/mapping.py +5 -1
  8. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/load/namespace.py +8 -0
  9. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/load/public_api.py +20 -0
  10. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/load/public_helpers.py +24 -0
  11. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/model/__init__.py +2 -1
  12. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/model/transports.py +17 -1
  13. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/normalize.py +63 -1
  14. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/observability_surface.py +17 -0
  15. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/quic_surface.py +2 -0
  16. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/validate.py +26 -0
  17. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config.egg-info/PKG-INFO +3 -3
  18. tigrcorn_config-0.3.18.dev3/src/tigrcorn_config.egg-info/requires.txt +4 -0
  19. tigrcorn_config-0.3.18.dev2/src/tigrcorn_config.egg-info/requires.txt +0 -4
  20. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/LICENSE +0 -0
  21. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/setup.cfg +0 -0
  22. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/__init__.py +0 -0
  23. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/audit.py +0 -0
  24. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/defaults.py +0 -0
  25. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/files.py +0 -0
  26. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/load/__init__.py +0 -0
  27. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/load/helpers.py +0 -0
  28. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/load/sources.py +0 -0
  29. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/merge.py +0 -0
  30. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/model/app.py +0 -0
  31. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/model/http.py +0 -0
  32. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/model/observability.py +0 -0
  33. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/model/scheduler.py +0 -0
  34. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/model/security.py +0 -0
  35. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/model/server.py +0 -0
  36. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/model/types.py +0 -0
  37. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/negative_surface.py +0 -0
  38. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/origin_surface.py +0 -0
  39. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/policy_surface.py +0 -0
  40. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/profiles.py +0 -0
  41. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config/py.typed +0 -0
  42. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config.egg-info/SOURCES.txt +0 -0
  43. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config.egg-info/dependency_links.txt +0 -0
  44. {tigrcorn_config-0.3.18.dev2 → tigrcorn_config-0.3.18.dev3}/src/tigrcorn_config.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tigrcorn-config
3
- Version: 0.3.18.dev2
3
+ Version: 0.3.18.dev3
4
4
  Summary: Typed configuration models, profiles, environment loading, and validation for the Tigrcorn ASGI/HTTP server stack.
5
5
  Author-email: Jacob Stewart <jacob@swarmauri.com>
6
6
  License: Apache License
@@ -186,7 +186,7 @@ Classifier: Typing :: Typed
186
186
  Requires-Python: <3.15,>=3.10
187
187
  Description-Content-Type: text/markdown
188
188
  License-File: LICENSE
189
- Requires-Dist: tigrcorn-core==0.3.18.dev2
189
+ Requires-Dist: tigrcorn-core==0.3.18.dev3
190
190
  Provides-Extra: yaml
191
191
  Requires-Dist: PyYAML>=6.0; extra == "yaml"
192
192
  Dynamic: license-file
@@ -280,7 +280,7 @@ The package exposes its supported public surface through the <code>tigrcorn_conf
280
280
 
281
281
  ## Package Graph
282
282
 
283
- [tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
283
+ [tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-quic-cc](https://pypi.org/project/tigrcorn-quic-cc/) | [tigrcorn-quic-cc-reno](https://pypi.org/project/tigrcorn-quic-cc-reno/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
284
284
 
285
285
  ## Best Practices
286
286
 
@@ -87,7 +87,7 @@ The package exposes its supported public surface through the <code>tigrcorn_conf
87
87
 
88
88
  ## Package Graph
89
89
 
90
- [tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
90
+ [tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-quic-cc](https://pypi.org/project/tigrcorn-quic-cc/) | [tigrcorn-quic-cc-reno](https://pypi.org/project/tigrcorn-quic-cc-reno/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
91
91
 
92
92
  ## Best Practices
93
93
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tigrcorn-config"
7
- version = "0.3.18.dev2"
7
+ version = "0.3.18.dev3"
8
8
  description = "Typed configuration models, profiles, environment loading, and validation for the Tigrcorn ASGI/HTTP server stack."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10,<3.15"
@@ -28,7 +28,7 @@ classifiers = [
28
28
  "Topic :: Software Development :: Libraries :: Python Modules",
29
29
  "Typing :: Typed",
30
30
  ]
31
- dependencies = ["tigrcorn-core==0.3.18.dev2"]
31
+ dependencies = ["tigrcorn-core==0.3.18.dev3"]
32
32
 
33
33
  [project.optional-dependencies]
34
34
  yaml = ["PyYAML>=6.0"]
@@ -76,11 +76,17 @@ _FLAT_ENV_MAP = {
76
76
  "QUIC_MAX_DATAGRAM_SIZE": ("quic", "max_datagram_size"),
77
77
  "QUIC_IDLE_TIMEOUT": ("quic", "idle_timeout"),
78
78
  "QUIC_EARLY_DATA_POLICY": ("quic", "early_data_policy"),
79
+ "QUIC_CONGESTION_CONTROL": ("quic", "congestion_control", "algorithm"),
80
+ "QUIC_CONGESTION_CONTROL_OPTIONS": ("quic", "congestion_control", "options"),
79
81
  "WEBTRANSPORT_MAX_SESSIONS": ("webtransport", "max_sessions"),
80
82
  "WEBTRANSPORT_MAX_STREAMS": ("webtransport", "max_streams"),
81
83
  "WEBTRANSPORT_MAX_DATAGRAM_SIZE": ("webtransport", "max_datagram_size"),
84
+ "WEBTRANSPORT_STREAM_RECEIVE_COALESCE_BYTES": ("webtransport", "stream_receive_coalesce_bytes"),
85
+ "WEBTRANSPORT_STREAM_RECEIVE_MAX_DELAY_MS": ("webtransport", "stream_receive_max_delay_ms"),
82
86
  "WEBTRANSPORT_ORIGIN": ("webtransport", "origins"),
83
87
  "WEBTRANSPORT_PATH": ("webtransport", "path"),
88
+ "WEBTRANSPORT_PROFILES": ("webtransport", "profiles"),
89
+ "WEBTRANSPORT_PREFERRED_PROFILE": ("webtransport", "preferred_profile"),
84
90
  "TIMEOUT_KEEP_ALIVE": ("http", "keep_alive_timeout"),
85
91
  "READ_TIMEOUT": ("http", "read_timeout"),
86
92
  "WRITE_TIMEOUT": ("http", "write_timeout"),
@@ -223,8 +223,6 @@ APPROVED_LEGACY_UNITTEST_FILES = (
223
223
  'tests/test_stream_probe_fixtures.py',
224
224
  'tests/test_websocket_uix_demo.py',
225
225
  'tests/test_webtransport_bidi_stream_context.py',
226
- 'tests/test_webtransport_feature_demo_app.py',
227
- 'tests/test_webtransport_feature_coverage.py',
228
226
  'tests/test_webtransport_mtls_demo.py',
229
227
  'tests/test_webtransport_mtls_demo_cleanup.py',
230
228
  'tests/test_webtransport_mtls_demo_runtime.py',
@@ -241,7 +239,9 @@ def scan_legacy_unittest_files(root: Path) -> list[str]:
241
239
  detected: list[str] = []
242
240
  for path in sorted(tests_root.glob('test_*.py')):
243
241
  text = path.read_text(encoding='utf-8')
244
- if 'import unittest' in text or 'from unittest' in text:
242
+ # unittest.mock is a standalone mock utility and does not make a
243
+ # pytest module a legacy unittest test module.
244
+ if 'import unittest' in text or 'from unittest import' in text:
245
245
  detected.append(path.as_posix().split(root.as_posix() + '/', 1)[-1])
246
246
  return detected
247
247
 
@@ -120,3 +120,7 @@ def _apply_listener_quic_fields(ns: Namespace, listener: dict[str, Any]) -> None
120
120
  listener["max_datagram_size"] = ns.quic_max_datagram_size
121
121
  if ns.quic_secret is not None:
122
122
  listener["quic_secret"] = ns.quic_secret.encode("utf-8") if isinstance(ns.quic_secret, str) else ns.quic_secret
123
+ if getattr(ns, "quic_congestion_control", None) is not None:
124
+ listener.setdefault("congestion_control", {})["algorithm"] = ns.quic_congestion_control
125
+ if getattr(ns, "quic_congestion_control_options", None) is not None:
126
+ listener.setdefault("congestion_control", {})["options"] = dict(ns.quic_congestion_control_options)
@@ -7,7 +7,7 @@ from typing import Any, Mapping
7
7
  from tigrcorn_config.defaults import default_config
8
8
  from tigrcorn_config.files import load_config_source
9
9
  from tigrcorn_config.merge import merge_config_dicts
10
- from tigrcorn_config.model import ListenerConfig, ServerConfig
10
+ from tigrcorn_config.model import CongestionControlConfig, ListenerConfig, ServerConfig
11
11
  from tigrcorn_config.normalize import normalize_config
12
12
  from tigrcorn_config.profiles import resolve_effective_profile_mapping, resolve_requested_profile
13
13
  from tigrcorn_config.validate import validate_config
@@ -40,6 +40,10 @@ def _apply_mapping(target: Any, data: Mapping[str, Any]) -> None:
40
40
  _apply_mapping(listener, entry)
41
41
  listeners.append(listener)
42
42
  setattr(target, key, listeners)
43
+ elif key == "congestion_control" and isinstance(value, Mapping):
44
+ congestion_control = CongestionControlConfig()
45
+ _apply_mapping(congestion_control, value)
46
+ setattr(target, key, congestion_control)
43
47
  elif dataclasses.is_dataclass(current) and isinstance(value, Mapping):
44
48
  _apply_mapping(current, value)
45
49
  else:
@@ -116,6 +116,10 @@ def _populate_special_blocks(
116
116
  blocks["websocket"]["enabled"] = not ns.disable_websocket
117
117
  if ns.quic_secret is not None:
118
118
  blocks["quic"]["quic_secret"] = ns.quic_secret.encode("utf-8") if isinstance(ns.quic_secret, str) else ns.quic_secret
119
+ if getattr(ns, "quic_congestion_control", None) is not None:
120
+ blocks["quic"].setdefault("congestion_control", {})["algorithm"] = ns.quic_congestion_control
121
+ if getattr(ns, "quic_congestion_control_options", None) is not None:
122
+ blocks["quic"].setdefault("congestion_control", {})["options"] = dict(ns.quic_congestion_control_options)
119
123
  if getattr(ns, "webtransport_origin", None):
120
124
  blocks["webtransport"]["origins"] = listify(ns.webtransport_origin)
121
125
 
@@ -195,7 +199,11 @@ _NAMESPACE_MAPPING: dict[str, tuple[str, str]] = {
195
199
  "webtransport_max_sessions": ("webtransport", "max_sessions"),
196
200
  "webtransport_max_streams": ("webtransport", "max_streams"),
197
201
  "webtransport_max_datagram_size": ("webtransport", "max_datagram_size"),
202
+ "webtransport_stream_receive_coalesce_bytes": ("webtransport", "stream_receive_coalesce_bytes"),
203
+ "webtransport_stream_receive_max_delay_ms": ("webtransport", "stream_receive_max_delay_ms"),
198
204
  "webtransport_path": ("webtransport", "path"),
205
+ "webtransport_profiles": ("webtransport", "profiles"),
206
+ "webtransport_preferred_profile": ("webtransport", "preferred_profile"),
199
207
  "log_level": ("logging", "level"),
200
208
  "access_log": ("logging", "access_log"),
201
209
  "access_log_file": ("logging", "access_log_file"),
@@ -62,8 +62,12 @@ def build_config(
62
62
  webtransport_max_sessions: int | None = None,
63
63
  webtransport_max_streams: int | None = None,
64
64
  webtransport_max_datagram_size: int | None = None,
65
+ webtransport_stream_receive_coalesce_bytes: int | None = None,
66
+ webtransport_stream_receive_max_delay_ms: int | None = None,
65
67
  webtransport_origins: list[str] | None = None,
66
68
  webtransport_path: str | None = None,
69
+ webtransport_profiles: list[str] | None = None,
70
+ webtransport_preferred_profile: str | None = None,
67
71
  pipe_mode: str = "rawframed",
68
72
  config: Mapping[str, Any] | None = None,
69
73
  default_headers: list[str] | list[tuple[str, str]] | None = None,
@@ -99,8 +103,12 @@ def build_config(
99
103
  webtransport_max_sessions=webtransport_max_sessions,
100
104
  webtransport_max_streams=webtransport_max_streams,
101
105
  webtransport_max_datagram_size=webtransport_max_datagram_size,
106
+ webtransport_stream_receive_coalesce_bytes=webtransport_stream_receive_coalesce_bytes,
107
+ webtransport_stream_receive_max_delay_ms=webtransport_stream_receive_max_delay_ms,
102
108
  webtransport_origins=webtransport_origins,
103
109
  webtransport_path=webtransport_path,
110
+ webtransport_profiles=webtransport_profiles,
111
+ webtransport_preferred_profile=webtransport_preferred_profile,
104
112
  )
105
113
  effective_websocket_enabled = True if websocket is None and direct_runtime_customized else bool(websocket)
106
114
  effective_h2c_enabled = _effective_h2c_enabled(enable_h2c, requested_http_versions)
@@ -156,8 +164,12 @@ def build_config(
156
164
  webtransport_max_sessions=webtransport_max_sessions,
157
165
  webtransport_max_streams=webtransport_max_streams,
158
166
  webtransport_max_datagram_size=webtransport_max_datagram_size,
167
+ webtransport_stream_receive_coalesce_bytes=webtransport_stream_receive_coalesce_bytes,
168
+ webtransport_stream_receive_max_delay_ms=webtransport_stream_receive_max_delay_ms,
159
169
  webtransport_origins=webtransport_origins,
160
170
  webtransport_path=webtransport_path,
171
+ webtransport_profiles=webtransport_profiles,
172
+ webtransport_preferred_profile=webtransport_preferred_profile,
161
173
  )
162
174
  _apply_listener_override_if_needed(
163
175
  overrides,
@@ -184,8 +196,12 @@ def build_config(
184
196
  webtransport_max_sessions=webtransport_max_sessions,
185
197
  webtransport_max_streams=webtransport_max_streams,
186
198
  webtransport_max_datagram_size=webtransport_max_datagram_size,
199
+ webtransport_stream_receive_coalesce_bytes=webtransport_stream_receive_coalesce_bytes,
200
+ webtransport_stream_receive_max_delay_ms=webtransport_stream_receive_max_delay_ms,
187
201
  webtransport_origins=webtransport_origins,
188
202
  webtransport_path=webtransport_path,
203
+ webtransport_profiles=webtransport_profiles,
204
+ webtransport_preferred_profile=webtransport_preferred_profile,
189
205
  )
190
206
  _apply_optional_http_overrides(
191
207
  overrides,
@@ -227,8 +243,12 @@ def _direct_runtime_customized(**values: Any) -> bool:
227
243
  or values["webtransport_max_sessions"] is not None
228
244
  or values["webtransport_max_streams"] is not None
229
245
  or values["webtransport_max_datagram_size"] is not None
246
+ or values["webtransport_stream_receive_coalesce_bytes"] is not None
247
+ or values["webtransport_stream_receive_max_delay_ms"] is not None
230
248
  or values["webtransport_origins"] is not None
231
249
  or values["webtransport_path"] is not None
250
+ or values["webtransport_profiles"] is not None
251
+ or values["webtransport_preferred_profile"] is not None
232
252
  )
233
253
 
234
254
 
@@ -56,8 +56,12 @@ def apply_webtransport_overrides(
56
56
  webtransport_max_sessions: int | None,
57
57
  webtransport_max_streams: int | None,
58
58
  webtransport_max_datagram_size: int | None,
59
+ webtransport_stream_receive_coalesce_bytes: int | None,
60
+ webtransport_stream_receive_max_delay_ms: int | None,
59
61
  webtransport_origins: list[str] | None,
60
62
  webtransport_path: str | None,
63
+ webtransport_profiles: list[str] | None,
64
+ webtransport_preferred_profile: str | None,
61
65
  ) -> None:
62
66
  if any(
63
67
  value is not None
@@ -65,16 +69,32 @@ def apply_webtransport_overrides(
65
69
  webtransport_max_sessions,
66
70
  webtransport_max_streams,
67
71
  webtransport_max_datagram_size,
72
+ webtransport_stream_receive_coalesce_bytes,
73
+ webtransport_stream_receive_max_delay_ms,
68
74
  webtransport_origins,
69
75
  webtransport_path,
76
+ webtransport_profiles,
77
+ webtransport_preferred_profile,
70
78
  )
71
79
  ):
72
80
  overrides["webtransport"] = {
73
81
  "max_sessions": webtransport_max_sessions,
74
82
  "max_streams": webtransport_max_streams,
75
83
  "max_datagram_size": webtransport_max_datagram_size,
84
+ **(
85
+ {"stream_receive_coalesce_bytes": webtransport_stream_receive_coalesce_bytes}
86
+ if webtransport_stream_receive_coalesce_bytes is not None
87
+ else {}
88
+ ),
89
+ **(
90
+ {"stream_receive_max_delay_ms": webtransport_stream_receive_max_delay_ms}
91
+ if webtransport_stream_receive_max_delay_ms is not None
92
+ else {}
93
+ ),
76
94
  "origins": webtransport_origins or [],
77
95
  "path": webtransport_path,
96
+ "profiles": webtransport_profiles or [],
97
+ "preferred_profile": webtransport_preferred_profile,
78
98
  }
79
99
 
80
100
 
@@ -94,8 +114,12 @@ def apply_listener_override_if_needed(overrides: dict[str, Any], **values: Any)
94
114
  or values["webtransport_max_sessions"] is not None
95
115
  or values["webtransport_max_streams"] is not None
96
116
  or values["webtransport_max_datagram_size"] is not None
117
+ or values["webtransport_stream_receive_coalesce_bytes"] is not None
118
+ or values["webtransport_stream_receive_max_delay_ms"] is not None
97
119
  or values["webtransport_origins"] is not None
98
120
  or values["webtransport_path"] is not None
121
+ or values["webtransport_profiles"] is not None
122
+ or values["webtransport_preferred_profile"] is not None
99
123
  )
100
124
  if not listener_customized:
101
125
  return
@@ -6,13 +6,14 @@ from .observability import LoggingConfig, MetricsConfig
6
6
  from .scheduler import SchedulerConfig
7
7
  from .security import ProxyConfig, TLSConfig
8
8
  from .server import ServerConfig
9
- from .transports import ListenerConfig, QUICConfig, WebTransportConfig
9
+ from .transports import CongestionControlConfig, ListenerConfig, QUICConfig, WebTransportConfig
10
10
  from .types import AppInterface, ClaimClass, ListenerKind, ProtocolName
11
11
 
12
12
  __all__ = [
13
13
  "AppConfig",
14
14
  "AppInterface",
15
15
  "ClaimClass",
16
+ "CongestionControlConfig",
16
17
  "HTTPConfig",
17
18
  "HooksConfig",
18
19
  "ListenerConfig",
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass, field
4
- from typing import Literal
4
+ from typing import Any, Literal
5
5
 
6
6
  from tigrcorn_core.constants import (
7
7
  DEFAULT_BACKLOG,
@@ -15,6 +15,12 @@ from tigrcorn_core.constants import (
15
15
  from .types import ListenerKind
16
16
 
17
17
 
18
+ @dataclass(slots=True)
19
+ class CongestionControlConfig:
20
+ algorithm: str | None = None
21
+ options: dict[str, Any] = field(default_factory=dict)
22
+
23
+
18
24
  @dataclass(slots=True)
19
25
  class QUICConfig:
20
26
  quic_secret: bytes | None = None
@@ -22,13 +28,22 @@ class QUICConfig:
22
28
  max_datagram_size: int = DEFAULT_MAX_DATAGRAM_SIZE
23
29
  idle_timeout: float = DEFAULT_IDLE_TIMEOUT
24
30
  early_data_policy: Literal["allow", "deny", "require"] = "deny"
31
+ congestion_control: CongestionControlConfig = field(
32
+ default_factory=lambda: CongestionControlConfig(algorithm="reno")
33
+ )
25
34
 
26
35
 
27
36
  @dataclass(slots=True)
28
37
  class WebTransportConfig:
38
+ enabled: bool = False
39
+ compatibility: Literal["current", "draft13"] = "current"
40
+ profiles: list[str] = field(default_factory=list)
41
+ preferred_profile: str | None = None
29
42
  max_sessions: int | None = None
30
43
  max_streams: int | None = None
31
44
  max_datagram_size: int | None = None
45
+ stream_receive_coalesce_bytes: int = 16 * 1024
46
+ stream_receive_max_delay_ms: int = 5
32
47
  origins: list[str] = field(default_factory=list)
33
48
  path: str | None = None
34
49
 
@@ -69,6 +84,7 @@ class ListenerConfig:
69
84
  quic_secret: bytes | None = None
70
85
  quic_require_retry: bool = False
71
86
  max_datagram_size: int = DEFAULT_MAX_DATAGRAM_SIZE
87
+ congestion_control: CongestionControlConfig | None = None
72
88
  pipe_mode: Literal["rawframed", "stream"] = DEFAULT_PIPE_MODE
73
89
  user: str | int | None = None
74
90
  group: str | int | None = None
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import secrets
4
4
 
5
- from tigrcorn_config.model import ListenerConfig, ServerConfig
5
+ from tigrcorn_config.model import CongestionControlConfig, ListenerConfig, ServerConfig
6
6
  from tigrcorn_core.constants import (
7
7
  DEFAULT_HTTP2_INITIAL_CONNECTION_WINDOW_SIZE,
8
8
  DEFAULT_HTTP2_INITIAL_STREAM_WINDOW_SIZE,
@@ -90,6 +90,55 @@ def normalize_config(config: ServerConfig) -> None:
90
90
  config.webtransport.max_streams = int(config.webtransport.max_streams)
91
91
  if config.webtransport.max_datagram_size is not None:
92
92
  config.webtransport.max_datagram_size = int(config.webtransport.max_datagram_size)
93
+ config.webtransport.stream_receive_coalesce_bytes = int(
94
+ config.webtransport.stream_receive_coalesce_bytes
95
+ )
96
+ config.webtransport.stream_receive_max_delay_ms = int(
97
+ config.webtransport.stream_receive_max_delay_ms
98
+ )
99
+ config.webtransport.enabled = bool(config.webtransport.enabled)
100
+ config.quic.congestion_control.algorithm = str(
101
+ config.quic.congestion_control.algorithm or "reno"
102
+ ).strip().lower()
103
+ config.quic.congestion_control.options = dict(
104
+ config.quic.congestion_control.options or {}
105
+ )
106
+ config.webtransport.compatibility = str(config.webtransport.compatibility or 'current').strip().lower()
107
+ aliases = {'current': 'ietf-current', 'chromium': 'draft02'}
108
+ configured_profiles = [
109
+ aliases.get(str(value).strip().lower(), str(value).strip().lower())
110
+ for value in _ensure_list(config.webtransport.profiles)
111
+ if str(value).strip()
112
+ ]
113
+ if (
114
+ config.webtransport.compatibility == 'draft13'
115
+ and configured_profiles in (
116
+ ['ietf-current'],
117
+ ['draft02', 'draft13', 'ietf-current'],
118
+ )
119
+ and config.webtransport.preferred_profile in {None, 'ietf-current', 'current'}
120
+ ):
121
+ configured_profiles = ['draft13']
122
+ config.webtransport.preferred_profile = 'draft13'
123
+ if not configured_profiles:
124
+ configured_profiles = (
125
+ ['draft13']
126
+ if config.webtransport.compatibility == 'draft13'
127
+ else ['draft02', 'draft13', 'ietf-current']
128
+ )
129
+ config.webtransport.profiles = list(dict.fromkeys(configured_profiles))
130
+ preferred = str(config.webtransport.preferred_profile or '').strip().lower()
131
+ if preferred:
132
+ preferred = aliases.get(preferred, preferred)
133
+ else:
134
+ preferred = (
135
+ 'ietf-current'
136
+ if 'ietf-current' in config.webtransport.profiles
137
+ else config.webtransport.profiles[0]
138
+ )
139
+ config.webtransport.preferred_profile = preferred
140
+ if preferred in {'ietf-current', 'draft13'}:
141
+ config.webtransport.compatibility = 'draft13' if preferred == 'draft13' else 'current'
93
142
  config.webtransport.origins = [str(v).strip() for v in _ensure_list(config.webtransport.origins) if str(v).strip()]
94
143
  if config.webtransport.path is not None:
95
144
  path = str(config.webtransport.path).strip()
@@ -218,7 +267,10 @@ def normalize_config(config: ServerConfig) -> None:
218
267
  listener.protocols.append("websocket")
219
268
  listener.websocket = config.websocket.enabled if listener.websocket else listener.websocket
220
269
  if listener.kind == "udp":
270
+ if config.webtransport.enabled and "webtransport" not in listener.protocols:
271
+ listener.protocols.append("webtransport")
221
272
  if "webtransport" in listener.protocols:
273
+ config.webtransport.enabled = True
222
274
  if "quic" not in listener.protocols:
223
275
  listener.protocols.insert(0, "quic")
224
276
  if "http3" not in listener.protocols:
@@ -229,6 +281,16 @@ def normalize_config(config: ServerConfig) -> None:
229
281
  if "http3" in listener.protocols and "quic" not in listener.protocols:
230
282
  listener.protocols.insert(0, "quic")
231
283
  listener.max_datagram_size = int(config.quic.max_datagram_size or listener.max_datagram_size)
284
+ listener_cc = listener.congestion_control or CongestionControlConfig()
285
+ listener.congestion_control = CongestionControlConfig(
286
+ algorithm=str(
287
+ listener_cc.algorithm or config.quic.congestion_control.algorithm
288
+ ).strip().lower(),
289
+ options={
290
+ **config.quic.congestion_control.options,
291
+ **dict(listener_cc.options or {}),
292
+ },
293
+ )
232
294
  listener.quic_require_retry = bool(config.quic.require_retry or listener.quic_require_retry)
233
295
  listener.quic_secret = listener.quic_secret or config.quic.quic_secret or secrets.token_bytes(32)
234
296
  if not listener.scheme:
@@ -28,10 +28,22 @@ METRICS_SCHEMA = {
28
28
  'loss': [
29
29
  'quic_packets_lost',
30
30
  'quic_pto_expirations',
31
+ 'quic_pto_probes',
32
+ 'quic_stream_bytes_retransmitted',
33
+ 'quic_crypto_bytes_retransmitted',
34
+ 'quic_datagram_frames_abandoned',
35
+ 'quic_frames_regenerated',
31
36
  'quic_path_challenges',
32
37
  'quic_path_responses',
33
38
  'quic_path_migrations',
34
39
  ],
40
+ 'congestion': [
41
+ 'quic_bytes_in_flight',
42
+ 'quic_congestion_window_bytes',
43
+ 'quic_smoothed_rtt_seconds',
44
+ 'quic_pacing_rate_bytes_per_second',
45
+ 'quic_pending_outbound_datagrams',
46
+ ],
35
47
  'http3': [
36
48
  'http3_requests_served',
37
49
  'http3_stream_resets',
@@ -45,6 +57,11 @@ METRICS_SCHEMA = {
45
57
  'active_connections',
46
58
  'active_websocket_connections',
47
59
  'active_quic_sessions',
60
+ 'quic_bytes_in_flight',
61
+ 'quic_congestion_window_bytes',
62
+ 'quic_smoothed_rtt_seconds',
63
+ 'quic_pacing_rate_bytes_per_second',
64
+ 'quic_pending_outbound_datagrams',
48
65
  ],
49
66
  'notes': {
50
67
  'aggregation': 'Counters are monotonic totals; gauges report current in-process state.',
@@ -94,6 +94,8 @@ QUIC_FLAG_HELP: dict[str, str] = {
94
94
  '--quic-max-datagram-size': 'Maximum QUIC UDP payload size advertised and accepted by package-owned QUIC listeners',
95
95
  '--quic-idle-timeout': 'QUIC idle timeout in seconds for package-owned UDP listeners',
96
96
  '--quic-early-data-policy': 'QUIC early-data policy: deny, allow, or require with 425 downgrade handling',
97
+ '--quic-congestion-control': 'Installed QUIC congestion-control provider id (default: reno)',
98
+ '--quic-congestion-control-options': 'JSON object passed to the selected QUIC congestion-control provider at listener startup',
97
99
  }
98
100
 
99
101
 
@@ -11,6 +11,7 @@ from tigrcorn_observability.metrics import parse_statsd_host
11
11
  from tigrcorn_observability.tracing import validate_otel_endpoint
12
12
  from tigrcorn_core.errors import ConfigError
13
13
  from tigrcorn_config.profiles import list_blessed_profiles
14
+ from tigrcorn_protocols.webtransport.profiles import profile_registry
14
15
 
15
16
  _ALLOWED_PROTOCOLS = {"http1", "http2", "http3", "quic", "websocket", "webtransport", "rawframed", "custom"}
16
17
  _ALLOWED_WORKER_CLASSES = {"local", "process", *SUPPORTED_WORKER_CLASS_ALIASES}
@@ -23,7 +24,20 @@ def _require_positive(name: str, value: int | float | None) -> None:
23
24
 
24
25
 
25
26
  def validate_config(config: ServerConfig) -> None:
27
+ if config.webtransport.compatibility not in {'current', 'draft13'}:
28
+ raise ConfigError("webtransport.compatibility must be 'current' or 'draft13'")
26
29
  normalize_config(config)
30
+ registry = profile_registry()
31
+ if config.webtransport.enabled and not config.webtransport.profiles:
32
+ raise ConfigError('webtransport.profiles must not be empty when WebTransport is enabled')
33
+ unknown_profiles = [name for name in config.webtransport.profiles if name not in registry]
34
+ if unknown_profiles:
35
+ raise ConfigError(f"unsupported WebTransport profiles: {', '.join(unknown_profiles)}")
36
+ if config.webtransport.preferred_profile not in config.webtransport.profiles:
37
+ raise ConfigError('webtransport.preferred_profile must be included in webtransport.profiles')
38
+ unavailable = [name for name in config.webtransport.profiles if not registry[name].implemented]
39
+ if unavailable:
40
+ raise ConfigError(f"WebTransport profiles are not implemented: {', '.join(unavailable)}")
27
41
  if config.app.profile is not None and config.app.profile not in set(list_blessed_profiles()):
28
42
  raise ConfigError(f"unsupported app.profile: {config.app.profile!r}")
29
43
  if config.app.interface not in {"auto", "tigr-asgi-contract", "asgi3"}:
@@ -88,6 +102,7 @@ def validate_config(config: ServerConfig) -> None:
88
102
  "webtransport.max_sessions": config.webtransport.max_sessions,
89
103
  "webtransport.max_streams": config.webtransport.max_streams,
90
104
  "webtransport.max_datagram_size": config.webtransport.max_datagram_size,
105
+ "webtransport.stream_receive_coalesce_bytes": config.webtransport.stream_receive_coalesce_bytes,
91
106
  "tls.ocsp_cache_size": config.tls.ocsp_cache_size,
92
107
  "scheduler.limit_concurrency": config.scheduler.limit_concurrency,
93
108
  "scheduler.max_connections": config.scheduler.max_connections,
@@ -97,6 +112,11 @@ def validate_config(config: ServerConfig) -> None:
97
112
  }.items():
98
113
  _require_positive(field_name, value)
99
114
 
115
+ if config.webtransport.stream_receive_max_delay_ms < 0:
116
+ raise ConfigError(
117
+ "webtransport.stream_receive_max_delay_ms must be nonnegative"
118
+ )
119
+
100
120
  if config.http.alt_svc_max_age < 0:
101
121
  raise ConfigError('http.alt_svc_max_age must be non-negative')
102
122
  if not (16_384 <= config.http.http2_max_frame_size <= 16_777_215):
@@ -119,6 +139,8 @@ def validate_config(config: ServerConfig) -> None:
119
139
  raise ConfigError(f"unsupported websocket compression mode: {config.websocket.compression!r}")
120
140
  if config.quic.early_data_policy not in {"allow", "deny", "require"}:
121
141
  raise ConfigError(f"unsupported quic early data policy: {config.quic.early_data_policy!r}")
142
+ if not config.quic.congestion_control.algorithm:
143
+ raise ConfigError("quic.congestion_control.algorithm must not be empty")
122
144
  if config.quic.quic_secret is not None and len(config.quic.quic_secret) == 0:
123
145
  raise ConfigError('quic.quic_secret must not be empty when provided')
124
146
  if config.webtransport.path is not None and not config.webtransport.path.startswith('/'):
@@ -213,6 +235,8 @@ def validate_config(config: ServerConfig) -> None:
213
235
  raise ConfigError("max_datagram_size must be positive for udp listeners")
214
236
  if listener.quic_secret is not None and len(listener.quic_secret) == 0:
215
237
  raise ConfigError('udp listener quic_secret must not be empty when provided')
238
+ if listener.congestion_control is None or not listener.congestion_control.algorithm:
239
+ raise ConfigError("udp listener congestion_control.algorithm must not be empty")
216
240
  if "http3" in listener.enabled_protocols and "quic" not in listener.enabled_protocols:
217
241
  raise ConfigError("http3 requires quic on udp listeners")
218
242
  if "webtransport" in listener.enabled_protocols:
@@ -220,6 +244,8 @@ def validate_config(config: ServerConfig) -> None:
220
244
  raise ConfigError("webtransport requires quic and http3 on udp listeners")
221
245
  if "3" not in listener.http_versions:
222
246
  raise ConfigError("webtransport requires HTTP/3 on udp listeners")
247
+ if listener.alpn_protocols and 'h3' not in listener.alpn_protocols:
248
+ raise ConfigError("webtransport requires h3 ALPN on udp listeners")
223
249
  if listener.ssl_ca_certs and not listener.ssl_enabled:
224
250
  raise ConfigError("ssl_ca_certs requires ssl_certfile and ssl_keyfile on udp listeners")
225
251
  if listener.ssl_require_client_cert:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tigrcorn-config
3
- Version: 0.3.18.dev2
3
+ Version: 0.3.18.dev3
4
4
  Summary: Typed configuration models, profiles, environment loading, and validation for the Tigrcorn ASGI/HTTP server stack.
5
5
  Author-email: Jacob Stewart <jacob@swarmauri.com>
6
6
  License: Apache License
@@ -186,7 +186,7 @@ Classifier: Typing :: Typed
186
186
  Requires-Python: <3.15,>=3.10
187
187
  Description-Content-Type: text/markdown
188
188
  License-File: LICENSE
189
- Requires-Dist: tigrcorn-core==0.3.18.dev2
189
+ Requires-Dist: tigrcorn-core==0.3.18.dev3
190
190
  Provides-Extra: yaml
191
191
  Requires-Dist: PyYAML>=6.0; extra == "yaml"
192
192
  Dynamic: license-file
@@ -280,7 +280,7 @@ The package exposes its supported public surface through the <code>tigrcorn_conf
280
280
 
281
281
  ## Package Graph
282
282
 
283
- [tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
283
+ [tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-quic-cc](https://pypi.org/project/tigrcorn-quic-cc/) | [tigrcorn-quic-cc-reno](https://pypi.org/project/tigrcorn-quic-cc-reno/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
284
284
 
285
285
  ## Best Practices
286
286
 
@@ -0,0 +1,4 @@
1
+ tigrcorn-core==0.3.18.dev3
2
+
3
+ [yaml]
4
+ PyYAML>=6.0
@@ -1,4 +0,0 @@
1
- tigrcorn-core==0.3.18.dev2
2
-
3
- [yaml]
4
- PyYAML>=6.0