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.
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/PKG-INFO +2 -2
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/pyproject.toml +2 -2
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/__init__.py +28 -0
- tigrcorn_http-0.3.17.dev1/src/tigrcorn_http/certification.py +269 -0
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http.egg-info/PKG-INFO +2 -2
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http.egg-info/SOURCES.txt +1 -0
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http.egg-info/requires.txt +1 -1
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/LICENSE +0 -0
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/README.md +0 -0
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/setup.cfg +0 -0
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/alt_svc.py +0 -0
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/conditional.py +0 -0
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/early_hints.py +0 -0
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/entity.py +0 -0
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/etag.py +0 -0
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/py.typed +0 -0
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/range.py +0 -0
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/structured_fields.py +0 -0
- {tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http.egg-info/dependency_links.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
]
|
{tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tigrcorn-http
|
|
3
|
-
Version: 0.3.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http/structured_fields.py
RENAMED
|
File without changes
|
|
File without changes
|
{tigrcorn_http-0.3.16.dev13 → tigrcorn_http-0.3.17.dev1}/src/tigrcorn_http.egg-info/top_level.txt
RENAMED
|
File without changes
|