tigrcorn-http 0.3.16.dev13__tar.gz → 0.3.17.dev1__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 (20) hide show
  1. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/PKG-INFO +2 -2
  2. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/pyproject.toml +2 -2
  3. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/__init__.py +28 -0
  4. tigrcorn_http-0.3.17.dev1/src/tigrcorn_http/certification.py +269 -0
  5. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http.egg-info/PKG-INFO +2 -2
  6. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http.egg-info/SOURCES.txt +1 -0
  7. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http.egg-info/requires.txt +1 -1
  8. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/LICENSE +0 -0
  9. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/README.md +0 -0
  10. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/setup.cfg +0 -0
  11. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/alt_svc.py +0 -0
  12. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/conditional.py +0 -0
  13. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/early_hints.py +0 -0
  14. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/entity.py +0 -0
  15. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/etag.py +0 -0
  16. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/py.typed +0 -0
  17. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/range.py +0 -0
  18. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/structured_fields.py +0 -0
  19. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http.egg-info/dependency_links.txt +0 -0
  20. {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tigrcorn-http
3
- Version: 0.3.16.dev13
3
+ Version: 0.3.17.dev1
4
4
  Summary: HTTP utilities for Tigrcorn headers, entity tags, content coding, range requests, and Python web server responses.
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.16.dev13
189
+ Requires-Dist: tigrcorn-core==0.3.17.dev1
190
190
  Provides-Extra: compression
191
191
  Requires-Dist: brotli>=1.1.0; extra == "compression"
192
192
  Dynamic: license-file
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tigrcorn-http"
7
- version = "0.3.16.dev13"
7
+ version = "0.3.17.dev1"
8
8
  description = "HTTP utilities for Tigrcorn headers, entity tags, content coding, range requests, and Python web server responses."
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.16.dev13"]
31
+ dependencies = ["tigrcorn-core==0.3.17.dev1"]
32
32
 
33
33
  [project.optional-dependencies]
34
34
  compression = ["brotli>=1.1.0"]
@@ -1,4 +1,19 @@
1
1
  from tigrcorn_http.conditional import ConditionalEvaluation, apply_conditional_request, parse_http_date
2
+ from tigrcorn_http.certification import (
3
+ HttpCertificationError,
4
+ HttpCertificationPolicy,
5
+ certify_http_entity_range_content_coding,
6
+ conditional_request_result,
7
+ content_coding_policy_result,
8
+ decode_content_coding,
9
+ http_certification_artifact,
10
+ http_certification_surface,
11
+ if_range_result,
12
+ reject_malformed_range,
13
+ strong_validator_result,
14
+ validate_range_amplification,
15
+ weak_validator_result,
16
+ )
2
17
  from tigrcorn_http.entity import EntitySemanticsResult, apply_response_entity_semantics
3
18
  from tigrcorn_http.etag import EntityTag, EntityTagList, format_etag, generate_entity_tag, parse_entity_tag, parse_entity_tag_list, strong_compare, weak_compare
4
19
  from tigrcorn_http.range import ByteRange, RangeEvaluation, apply_byte_ranges, parse_range_header
@@ -25,6 +40,8 @@ __all__ = [
25
40
  'EntitySemanticsResult',
26
41
  'EntityTag',
27
42
  'EntityTagList',
43
+ 'HttpCertificationError',
44
+ 'HttpCertificationPolicy',
28
45
  'RangeEvaluation',
29
46
  'ByteSequence',
30
47
  'Date',
@@ -35,13 +52,21 @@ __all__ = [
35
52
  'apply_byte_ranges',
36
53
  'apply_conditional_request',
37
54
  'apply_response_entity_semantics',
55
+ 'certify_http_entity_range_content_coding',
56
+ 'conditional_request_result',
57
+ 'content_coding_policy_result',
58
+ 'decode_content_coding',
38
59
  'format_etag',
39
60
  'generate_entity_tag',
61
+ 'http_certification_artifact',
62
+ 'http_certification_surface',
63
+ 'if_range_result',
40
64
  'parse_entity_tag',
41
65
  'parse_entity_tag_list',
42
66
  'parse_http_date',
43
67
  'parse_dictionary',
44
68
  'parse_range_header',
69
+ 'reject_malformed_range',
45
70
  'parse_item',
46
71
  'parse_list',
47
72
  'parse_structured_field',
@@ -50,5 +75,8 @@ __all__ = [
50
75
  'serialize_list',
51
76
  'serialize_structured_value',
52
77
  'strong_compare',
78
+ 'strong_validator_result',
79
+ 'validate_range_amplification',
53
80
  'weak_compare',
81
+ 'weak_validator_result',
54
82
  ]
@@ -0,0 +1,269 @@
1
+ from __future__ import annotations
2
+
3
+ import gzip
4
+ import zlib
5
+ from dataclasses import dataclass
6
+ from typing import Any, Iterable, Mapping
7
+
8
+ from tigrcorn_core.utils.headers import get_header
9
+ from tigrcorn_http.conditional import apply_conditional_request
10
+ from tigrcorn_http.etag import format_etag, parse_entity_tag, strong_compare, weak_compare
11
+ from tigrcorn_http.range import ByteRange, apply_byte_ranges, parse_range_header
12
+ from tigrcorn_protocols.content_coding import apply_http_content_coding
13
+
14
+
15
+ class HttpCertificationError(ValueError):
16
+ """Raised when HTTP entity/range/content-coding certification fails closed."""
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class HttpCertificationPolicy:
21
+ max_ranges: int = 8
22
+ max_range_amplification: float = 2.0
23
+ max_decompression_ratio: float = 16.0
24
+ max_decompressed_bytes: int = 1_048_576
25
+ supported_codings: tuple[str, ...] = ("gzip", "deflate", "br")
26
+
27
+ def as_dict(self) -> dict[str, Any]:
28
+ return {
29
+ "max_decompressed_bytes": self.max_decompressed_bytes,
30
+ "max_decompression_ratio": self.max_decompression_ratio,
31
+ "max_range_amplification": self.max_range_amplification,
32
+ "max_ranges": self.max_ranges,
33
+ "supported_codings": self.supported_codings,
34
+ }
35
+
36
+
37
+ def http_certification_surface() -> dict[str, Any]:
38
+ return {
39
+ "certification_id": "tigrcorn.http.entity-range-content-coding",
40
+ "sections": ("etag", "conditional", "range", "content_coding", "negative_corpus"),
41
+ "schema_version": 1,
42
+ "surface": "tigrcorn.http.certification",
43
+ }
44
+
45
+
46
+ def http_certification_artifact(policy: HttpCertificationPolicy | None = None) -> dict[str, Any]:
47
+ selected = HttpCertificationPolicy() if policy is None else policy
48
+ return {
49
+ "artifact_id": "tigrcorn.http.entity-range-content-coding",
50
+ "content_coding": {
51
+ "malformed_streams": "reject",
52
+ "policy": selected.as_dict(),
53
+ },
54
+ "etag": {
55
+ "strong_validator": "byte-identical-only",
56
+ "weak_validator": "semantic-match",
57
+ },
58
+ "negative_corpus_required": (
59
+ "malformed_range",
60
+ "overlap_range",
61
+ "malformed_coding",
62
+ "decompression_amplification",
63
+ "stale_if_range",
64
+ ),
65
+ "range": {
66
+ "malformed": "reject",
67
+ "overlap_amplification": "bounded",
68
+ },
69
+ "schema_version": 1,
70
+ }
71
+
72
+
73
+ def weak_validator_result(current: bytes | str, candidate: bytes | str) -> dict[str, Any]:
74
+ left = parse_entity_tag(current)
75
+ right = parse_entity_tag(candidate)
76
+ return {
77
+ "candidate": _safe_etag(candidate),
78
+ "current": _safe_etag(current),
79
+ "strong_match": strong_compare(left, right),
80
+ "weak_match": weak_compare(left, right),
81
+ }
82
+
83
+
84
+ def strong_validator_result(current: bytes | str, candidate: bytes | str) -> dict[str, Any]:
85
+ left = parse_entity_tag(current)
86
+ right = parse_entity_tag(candidate)
87
+ return {
88
+ "candidate": _safe_etag(candidate),
89
+ "current": _safe_etag(current),
90
+ "strong_match": strong_compare(left, right),
91
+ "weak_match": weak_compare(left, right),
92
+ }
93
+
94
+
95
+ def conditional_request_result(
96
+ *,
97
+ method: str,
98
+ request_headers: Iterable[tuple[bytes, bytes]],
99
+ etag: bytes = b'"abc"',
100
+ body: bytes = b"payload",
101
+ ) -> dict[str, Any]:
102
+ result = apply_conditional_request(
103
+ method=method,
104
+ request_headers=tuple(request_headers),
105
+ response_headers=[(b"etag", etag), (b"last-modified", b"Sun, 01 Jan 2023 00:00:00 GMT")],
106
+ body=body,
107
+ status=200,
108
+ )
109
+ return {
110
+ "body_length": len(result.body),
111
+ "not_modified": result.not_modified,
112
+ "precondition_failed": result.precondition_failed,
113
+ "status": result.status,
114
+ }
115
+
116
+
117
+ def content_coding_policy_result(
118
+ *,
119
+ accept_encoding: bytes,
120
+ policy: str,
121
+ supported: tuple[str, ...] = ("gzip", "deflate"),
122
+ ) -> dict[str, Any]:
123
+ status, headers, body, selection = apply_http_content_coding(
124
+ request_headers=[(b"accept-encoding", accept_encoding)],
125
+ response_headers=[(b"content-type", b"text/plain")],
126
+ body=b"compress-me",
127
+ status=200,
128
+ policy=policy,
129
+ supported=supported,
130
+ )
131
+ return {
132
+ "coding": selection.coding,
133
+ "content_encoding": _decode_header(get_header(headers, b"content-encoding")),
134
+ "identity_acceptable": selection.identity_acceptable,
135
+ "status": status,
136
+ "body_length": len(body),
137
+ }
138
+
139
+
140
+ def reject_malformed_range(range_header: bytes | str, *, resource_length: int = 100) -> dict[str, Any]:
141
+ parsed = parse_range_header(range_header, resource_length=resource_length)
142
+ if parsed is not None:
143
+ raise HttpCertificationError("malformed range was accepted")
144
+ return {"accepted": False, "reason": "malformed_range"}
145
+
146
+
147
+ def validate_range_amplification(
148
+ range_header: bytes | str,
149
+ *,
150
+ resource_length: int,
151
+ policy: HttpCertificationPolicy | None = None,
152
+ ) -> dict[str, Any]:
153
+ selected = HttpCertificationPolicy() if policy is None else policy
154
+ ranges = parse_range_header(range_header, resource_length=resource_length)
155
+ if ranges is None or ranges == []:
156
+ raise HttpCertificationError("range set is malformed or unsatisfied")
157
+ requested = sum(item.end - item.start + 1 for item in ranges)
158
+ unique = _unique_range_bytes(ranges)
159
+ amplification = float(requested) / float(max(unique, 1))
160
+ if len(ranges) > selected.max_ranges or amplification > selected.max_range_amplification:
161
+ raise HttpCertificationError("range overlap/amplification rejected")
162
+ return {
163
+ "accepted": True,
164
+ "amplification": amplification,
165
+ "range_count": len(ranges),
166
+ "requested_bytes": requested,
167
+ "unique_bytes": unique,
168
+ }
169
+
170
+
171
+ def decode_content_coding(
172
+ coding: str,
173
+ payload: bytes,
174
+ *,
175
+ policy: HttpCertificationPolicy | None = None,
176
+ ) -> dict[str, Any]:
177
+ selected = HttpCertificationPolicy() if policy is None else policy
178
+ try:
179
+ if coding == "gzip":
180
+ decoded = gzip.decompress(payload)
181
+ elif coding == "deflate":
182
+ decoded = zlib.decompress(payload)
183
+ else:
184
+ raise HttpCertificationError(f"unsupported content coding: {coding}")
185
+ except (OSError, zlib.error) as exc:
186
+ raise HttpCertificationError("malformed compressed stream rejected") from exc
187
+ ratio = float(len(decoded)) / float(max(len(payload), 1))
188
+ if len(decoded) > selected.max_decompressed_bytes or ratio > selected.max_decompression_ratio:
189
+ raise HttpCertificationError("decompression amplification rejected")
190
+ return {
191
+ "accepted": True,
192
+ "decoded_bytes": len(decoded),
193
+ "encoded_bytes": len(payload),
194
+ "ratio": ratio,
195
+ }
196
+
197
+
198
+ def if_range_result(
199
+ *,
200
+ if_range: bytes,
201
+ current_etag: bytes = b'"fresh"',
202
+ body: bytes = b"0123456789",
203
+ ) -> dict[str, Any]:
204
+ result = apply_byte_ranges(
205
+ method="GET",
206
+ request_headers=[(b"range", b"bytes=0-1"), (b"if-range", if_range)],
207
+ response_headers=[(b"etag", current_etag)],
208
+ body=body,
209
+ status=200,
210
+ )
211
+ return {
212
+ "applied": result.applied,
213
+ "body": result.body,
214
+ "status": result.status,
215
+ }
216
+
217
+
218
+ def certify_http_entity_range_content_coding(evidence: Mapping[str, Any]) -> dict[str, Any]:
219
+ corpus = evidence.get("negative_corpus") or {}
220
+ required = (
221
+ "malformed_range",
222
+ "overlap_range",
223
+ "malformed_coding",
224
+ "decompression_amplification",
225
+ "stale_if_range",
226
+ )
227
+ missing = tuple(key for key in required if not corpus.get(key))
228
+ if missing:
229
+ raise HttpCertificationError(
230
+ "missing HTTP negative range/coding corpus: " + ", ".join(missing)
231
+ )
232
+ return {
233
+ "certification_state": "certified",
234
+ "evidence_keys": tuple(sorted(evidence)),
235
+ "negative_corpus": required,
236
+ }
237
+
238
+
239
+ def _safe_etag(value: bytes | str) -> str:
240
+ raw = value.decode("latin1") if isinstance(value, bytes) else str(value)
241
+ return raw
242
+
243
+
244
+ def _decode_header(value: bytes | None) -> str | None:
245
+ return None if value is None else value.decode("latin1")
246
+
247
+
248
+ def _unique_range_bytes(ranges: list[ByteRange]) -> int:
249
+ covered: set[int] = set()
250
+ for item in ranges:
251
+ covered.update(range(item.start, item.end + 1))
252
+ return len(covered)
253
+
254
+
255
+ __all__ = [
256
+ "HttpCertificationError",
257
+ "HttpCertificationPolicy",
258
+ "certify_http_entity_range_content_coding",
259
+ "conditional_request_result",
260
+ "content_coding_policy_result",
261
+ "decode_content_coding",
262
+ "http_certification_artifact",
263
+ "http_certification_surface",
264
+ "if_range_result",
265
+ "reject_malformed_range",
266
+ "strong_validator_result",
267
+ "validate_range_amplification",
268
+ "weak_validator_result",
269
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tigrcorn-http
3
- Version: 0.3.16.dev13
3
+ Version: 0.3.17.dev1
4
4
  Summary: HTTP utilities for Tigrcorn headers, entity tags, content coding, range requests, and Python web server responses.
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.16.dev13
189
+ Requires-Dist: tigrcorn-core==0.3.17.dev1
190
190
  Provides-Extra: compression
191
191
  Requires-Dist: brotli>=1.1.0; extra == "compression"
192
192
  Dynamic: license-file
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  src/tigrcorn_http/__init__.py
5
5
  src/tigrcorn_http/alt_svc.py
6
+ src/tigrcorn_http/certification.py
6
7
  src/tigrcorn_http/conditional.py
7
8
  src/tigrcorn_http/early_hints.py
8
9
  src/tigrcorn_http/entity.py
@@ -1,4 +1,4 @@
1
- tigrcorn-core==0.3.16.dev13
1
+ tigrcorn-core==0.3.17.dev1
2
2
 
3
3
  [compression]
4
4
  brotli>=1.1.0