tigrcorn-http 0.3.16__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.
@@ -0,0 +1,54 @@
1
+ from tigrcorn_http.conditional import ConditionalEvaluation, apply_conditional_request, parse_http_date
2
+ from tigrcorn_http.entity import EntitySemanticsResult, apply_response_entity_semantics
3
+ from tigrcorn_http.etag import EntityTag, EntityTagList, format_etag, generate_entity_tag, parse_entity_tag, parse_entity_tag_list, strong_compare, weak_compare
4
+ from tigrcorn_http.range import ByteRange, RangeEvaluation, apply_byte_ranges, parse_range_header
5
+ from tigrcorn_http.structured_fields import (
6
+ ByteSequence,
7
+ Date,
8
+ InnerList,
9
+ Item,
10
+ StructuredFieldError,
11
+ Token,
12
+ parse_dictionary,
13
+ parse_item,
14
+ parse_list,
15
+ parse_structured_field,
16
+ serialize_dictionary,
17
+ serialize_item,
18
+ serialize_list,
19
+ serialize_structured_value,
20
+ )
21
+
22
+ __all__ = [
23
+ 'ByteRange',
24
+ 'ConditionalEvaluation',
25
+ 'EntitySemanticsResult',
26
+ 'EntityTag',
27
+ 'EntityTagList',
28
+ 'RangeEvaluation',
29
+ 'ByteSequence',
30
+ 'Date',
31
+ 'InnerList',
32
+ 'Item',
33
+ 'StructuredFieldError',
34
+ 'Token',
35
+ 'apply_byte_ranges',
36
+ 'apply_conditional_request',
37
+ 'apply_response_entity_semantics',
38
+ 'format_etag',
39
+ 'generate_entity_tag',
40
+ 'parse_entity_tag',
41
+ 'parse_entity_tag_list',
42
+ 'parse_http_date',
43
+ 'parse_dictionary',
44
+ 'parse_range_header',
45
+ 'parse_item',
46
+ 'parse_list',
47
+ 'parse_structured_field',
48
+ 'serialize_dictionary',
49
+ 'serialize_item',
50
+ 'serialize_list',
51
+ 'serialize_structured_value',
52
+ 'strong_compare',
53
+ 'weak_compare',
54
+ ]
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+
6
+ HeaderList = list[tuple[bytes, bytes]]
7
+
8
+
9
+ def _to_bytes(value: bytes | str) -> bytes:
10
+ if isinstance(value, bytes):
11
+ return value
12
+ return str(value).encode('latin1')
13
+
14
+
15
+ def _dedupe(values: Iterable[bytes]) -> list[bytes]:
16
+ seen: set[bytes] = set()
17
+ result: list[bytes] = []
18
+ for item in values:
19
+ token = bytes(item).strip()
20
+ if not token or token in seen:
21
+ continue
22
+ seen.add(token)
23
+ result.append(token)
24
+ return result
25
+
26
+
27
+ def configured_alt_svc_values(config, *, request_http_version: str | None = None) -> list[bytes]:
28
+ explicit = _dedupe(_to_bytes(item) for item in getattr(config.http, 'alt_svc_headers', ()))
29
+ if explicit:
30
+ return explicit
31
+ if not getattr(config.http, 'alt_svc_auto', False):
32
+ return []
33
+ version = str(request_http_version or '').replace('HTTP/', '').strip().lower()
34
+ if version in {'3', '3.0', 'h3', 'http/3'}:
35
+ return []
36
+ values: list[bytes] = []
37
+ max_age = int(getattr(config.http, 'alt_svc_max_age', 86400))
38
+ persist = bool(getattr(config.http, 'alt_svc_persist', False))
39
+ for listener in getattr(config, 'listeners', ()): # pragma: no branch - tiny loop
40
+ if getattr(listener, 'kind', None) != 'udp':
41
+ continue
42
+ enabled = set(getattr(listener, 'enabled_protocols', ()))
43
+ if 'http3' not in enabled:
44
+ continue
45
+ port = getattr(listener, 'port', 0)
46
+ if not isinstance(port, int) or port <= 0:
47
+ continue
48
+ fragments = [f'h3=":{port}"'.encode('ascii')]
49
+ if max_age >= 0:
50
+ fragments.append(f'ma={max_age}'.encode('ascii'))
51
+ if persist:
52
+ fragments.append(b'persist=1')
53
+ values.append(b'; '.join(fragments))
54
+ return _dedupe(values)
55
+
56
+
57
+ def append_alt_svc_headers(
58
+ headers: Iterable[tuple[bytes, bytes]],
59
+ *,
60
+ config,
61
+ request_http_version: str | None = None,
62
+ ) -> HeaderList:
63
+ normalized = [(bytes(name).lower(), bytes(value)) for name, value in headers]
64
+ if any(name == b'alt-svc' for name, _value in normalized):
65
+ return normalized
66
+ for value in configured_alt_svc_values(config, request_http_version=request_http_version):
67
+ normalized.append((b'alt-svc', value))
68
+ return normalized
69
+
70
+
71
+ __all__ = ['append_alt_svc_headers', 'configured_alt_svc_values']
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime, timezone
5
+ from email.utils import parsedate_to_datetime
6
+
7
+ from tigrcorn_http.etag import EntityTag, EntityTagList, parse_entity_tag, parse_entity_tag_list, strong_compare, weak_compare
8
+ from tigrcorn_core.utils.headers import get_header
9
+
10
+
11
+ HeaderList = list[tuple[bytes, bytes]]
12
+ _PRECONDITION_FAILED_BODY = b'precondition failed'
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class ConditionalEvaluation:
17
+ status: int
18
+ headers: HeaderList
19
+ body: bytes
20
+ not_modified: bool = False
21
+ precondition_failed: bool = False
22
+
23
+
24
+ def parse_http_date(value: bytes | str | None) -> datetime | None:
25
+ if value is None:
26
+ return None
27
+ try:
28
+ dt = parsedate_to_datetime(value.decode('latin1') if isinstance(value, bytes) else value)
29
+ except (TypeError, ValueError, IndexError):
30
+ return None
31
+ if dt is None:
32
+ return None
33
+ if dt.tzinfo is None:
34
+ dt = dt.replace(tzinfo=timezone.utc)
35
+ return dt.astimezone(timezone.utc).replace(microsecond=0)
36
+
37
+
38
+ def _current_validators(headers: HeaderList) -> tuple[EntityTag | None, bytes | None, datetime | None, bytes | None]:
39
+ etag_raw = get_header(headers, b'etag')
40
+ last_modified_raw = get_header(headers, b'last-modified')
41
+ return parse_entity_tag(etag_raw), etag_raw, parse_http_date(last_modified_raw), last_modified_raw
42
+
43
+
44
+ def _build_precondition_failed_headers(current_etag_raw: bytes | None, last_modified_raw: bytes | None) -> HeaderList:
45
+ headers: HeaderList = [(b'content-type', b'text/plain; charset=utf-8')]
46
+ if current_etag_raw is not None:
47
+ headers.append((b'etag', current_etag_raw))
48
+ if last_modified_raw is not None:
49
+ headers.append((b'last-modified', last_modified_raw))
50
+ return headers
51
+
52
+
53
+ def _matches_if_match(condition: EntityTagList | None, current: EntityTag | None) -> bool:
54
+ if condition is None:
55
+ return True
56
+ if condition.any_value:
57
+ return current is not None
58
+ return any(strong_compare(candidate, current) for candidate in condition.items)
59
+
60
+
61
+ def _matches_if_none_match(condition: EntityTagList | None, current: EntityTag | None) -> bool:
62
+ if condition is None:
63
+ return False
64
+ if condition.any_value:
65
+ return current is not None
66
+ return any(weak_compare(candidate, current) for candidate in condition.items)
67
+
68
+
69
+ def apply_conditional_request(
70
+ *,
71
+ method: str,
72
+ request_headers: list[tuple[bytes, bytes]] | tuple[tuple[bytes, bytes], ...],
73
+ response_headers: HeaderList,
74
+ body: bytes,
75
+ status: int,
76
+ ) -> ConditionalEvaluation:
77
+ method_upper = method.upper()
78
+ headers = [(bytes(name).lower(), bytes(value)) for name, value in response_headers]
79
+ current_etag, current_etag_raw, last_modified, last_modified_raw = _current_validators(headers)
80
+
81
+ if_match_raw = get_header(request_headers, b'if-match')
82
+ if if_match_raw is not None:
83
+ condition = parse_entity_tag_list(if_match_raw)
84
+ if condition is not None and not _matches_if_match(condition, current_etag):
85
+ return ConditionalEvaluation(
86
+ status=412,
87
+ headers=_build_precondition_failed_headers(current_etag_raw, last_modified_raw),
88
+ body=_PRECONDITION_FAILED_BODY,
89
+ precondition_failed=True,
90
+ )
91
+
92
+ if_unmodified_since_raw = get_header(request_headers, b'if-unmodified-since')
93
+ if if_unmodified_since_raw is not None and last_modified is not None:
94
+ date_value = parse_http_date(if_unmodified_since_raw)
95
+ if date_value is not None and last_modified > date_value:
96
+ return ConditionalEvaluation(
97
+ status=412,
98
+ headers=_build_precondition_failed_headers(current_etag_raw, last_modified_raw),
99
+ body=_PRECONDITION_FAILED_BODY,
100
+ precondition_failed=True,
101
+ )
102
+
103
+ if_none_match_raw = get_header(request_headers, b'if-none-match')
104
+ if if_none_match_raw is not None:
105
+ condition = parse_entity_tag_list(if_none_match_raw)
106
+ if condition is not None and _matches_if_none_match(condition, current_etag):
107
+ if method_upper in {'GET', 'HEAD'}:
108
+ return ConditionalEvaluation(status=304, headers=headers, body=b'', not_modified=True)
109
+ return ConditionalEvaluation(
110
+ status=412,
111
+ headers=_build_precondition_failed_headers(current_etag_raw, last_modified_raw),
112
+ body=_PRECONDITION_FAILED_BODY,
113
+ precondition_failed=True,
114
+ )
115
+
116
+ if if_none_match_raw is None and method_upper in {'GET', 'HEAD'} and last_modified is not None:
117
+ if_modified_since_raw = get_header(request_headers, b'if-modified-since')
118
+ if if_modified_since_raw is not None:
119
+ date_value = parse_http_date(if_modified_since_raw)
120
+ if date_value is not None and last_modified <= date_value:
121
+ return ConditionalEvaluation(status=304, headers=headers, body=b'', not_modified=True)
122
+
123
+ return ConditionalEvaluation(status=status, headers=headers, body=body)
124
+
125
+
126
+ __all__ = ['ConditionalEvaluation', 'apply_conditional_request', 'parse_http_date']
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ from tigrcorn_core.utils.headers import strip_connection_specific_headers
6
+
7
+
8
+ HeaderList = list[tuple[bytes, bytes]]
9
+
10
+ # Keep the public support envelope intentionally narrow for checkpointability:
11
+ # 103 responses are treated as preload-hint carriers and only preserve Link fields.
12
+ _EARLY_HINTS_ALLOWED_HEADERS = {b'link'}
13
+
14
+
15
+ def _normalize(headers: Iterable[tuple[bytes, bytes]]) -> HeaderList:
16
+ normalized = [(bytes(name).lower(), bytes(value)) for name, value in headers]
17
+ return strip_connection_specific_headers(normalized)
18
+
19
+
20
+ def sanitize_informational_headers(status: int, headers: Iterable[tuple[bytes, bytes]]) -> HeaderList:
21
+ """Return a safe informational-header list.
22
+
23
+ For 103 Early Hints, restrict the surface to Link preload hints and drop
24
+ connection-specific framing metadata. Other informational responses keep
25
+ ordinary end-to-end fields except framing metadata.
26
+ """
27
+
28
+ normalized = _normalize(headers)
29
+ if status == 103:
30
+ result: HeaderList = []
31
+ seen: set[tuple[bytes, bytes]] = set()
32
+ for name, value in normalized:
33
+ if name not in _EARLY_HINTS_ALLOWED_HEADERS:
34
+ continue
35
+ if b'\r' in value or b'\n' in value:
36
+ continue
37
+ item = (name, value)
38
+ if item not in seen:
39
+ seen.add(item)
40
+ result.append(item)
41
+ return result
42
+ return [
43
+ (name, value)
44
+ for name, value in normalized
45
+ if name not in {b'content-length', b'transfer-encoding'}
46
+ ]
47
+
48
+
49
+ __all__ = ['sanitize_informational_headers']
@@ -0,0 +1,338 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from tigrcorn_asgi.send import BodySegment
6
+ from tigrcorn_http.conditional import apply_conditional_request
7
+ from tigrcorn_http.etag import generate_entity_tag
8
+ from tigrcorn_http.range import apply_byte_ranges, build_file_range_segments, plan_file_byte_ranges
9
+ from tigrcorn_protocols.content_coding import ContentCodingSelection, apply_http_content_coding
10
+ from tigrcorn_protocols.http1.serializer import response_allows_body
11
+ from tigrcorn_core.utils.headers import append_if_missing, get_header, replace_header
12
+
13
+
14
+ HeaderList = list[tuple[bytes, bytes]]
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class EntitySemanticsResult:
19
+ status: int
20
+ headers: HeaderList
21
+ body: bytes
22
+ content_coding: ContentCodingSelection
23
+ range_applied: bool = False
24
+ not_modified: bool = False
25
+ precondition_failed: bool = False
26
+ head_response: bool = False
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class FileBackedEntitySemanticsResult:
31
+ status: int
32
+ headers: HeaderList
33
+ body: bytes
34
+ body_segments: tuple[BodySegment, ...] = ()
35
+ use_body_segments: bool = False
36
+ range_applied: bool = False
37
+ not_modified: bool = False
38
+ precondition_failed: bool = False
39
+ head_response: bool = False
40
+ requires_materialization: bool = False
41
+
42
+
43
+ def _normalize_headers(headers: list[tuple[bytes, bytes]]) -> HeaderList:
44
+ return [(bytes(name).lower(), bytes(value)) for name, value in headers]
45
+
46
+
47
+ def finalize_response_content_length(*, method: str, status: int, headers: HeaderList, body_length: int, trailers_present: bool = False) -> HeaderList:
48
+ normalized = [(name.lower(), value) for name, value in headers if name.lower() != b'content-length']
49
+ method_upper = method.upper()
50
+ if status in {204} or 100 <= status < 200:
51
+ return normalized
52
+ if trailers_present:
53
+ return normalized
54
+ if status == 304:
55
+ return normalized
56
+ if not response_allows_body(status):
57
+ return normalized
58
+ normalized.append((b'content-length', str(max(int(body_length), 0)).encode('ascii')))
59
+ if method_upper == 'HEAD':
60
+ return normalized
61
+ return normalized
62
+
63
+
64
+ def _maybe_generate_etag(headers: HeaderList, body: bytes, *, enabled: bool) -> HeaderList:
65
+ if not enabled:
66
+ return headers
67
+ if get_header(headers, b'etag') is not None:
68
+ return headers
69
+ headers = list(headers)
70
+ headers.append((b'etag', generate_entity_tag(body)))
71
+ return headers
72
+
73
+
74
+ def _default_selection() -> ContentCodingSelection:
75
+ return ContentCodingSelection(coding=None, identity_acceptable=True)
76
+
77
+
78
+ def should_materialize_response_body(
79
+ *,
80
+ method: str,
81
+ request_headers: list[tuple[bytes, bytes]] | tuple[tuple[bytes, bytes], ...],
82
+ response_headers: list[tuple[bytes, bytes]],
83
+ status: int,
84
+ apply_content_coding: bool = True,
85
+ ) -> bool:
86
+ method_upper = method.upper()
87
+ if method_upper == 'HEAD' or not response_allows_body(status) or status in {304} or 100 <= status < 200:
88
+ return False
89
+ if not apply_content_coding:
90
+ return False
91
+ if get_header(response_headers, b'content-encoding') is not None:
92
+ return False
93
+ return get_header(request_headers, b'accept-encoding') is not None
94
+
95
+
96
+ def apply_header_only_response_semantics(
97
+ *,
98
+ method: str,
99
+ request_headers: list[tuple[bytes, bytes]] | tuple[tuple[bytes, bytes], ...],
100
+ response_headers: list[tuple[bytes, bytes]],
101
+ status: int,
102
+ body_length: int,
103
+ generated_etag: bytes | None = None,
104
+ trailers_present: bool = False,
105
+ advertise_accept_ranges: bool = False,
106
+ ) -> EntitySemanticsResult:
107
+ method_upper = method.upper()
108
+ headers = _normalize_headers(response_headers)
109
+ if generated_etag is not None and get_header(headers, b'etag') is None and status not in {412, 416} and not (100 <= status < 200):
110
+ headers = list(headers)
111
+ headers.append((b'etag', generated_etag))
112
+
113
+ conditional = apply_conditional_request(
114
+ method=method_upper,
115
+ request_headers=request_headers,
116
+ response_headers=headers,
117
+ body=b'',
118
+ status=status,
119
+ )
120
+ status = conditional.status
121
+ headers = conditional.headers
122
+ body = conditional.body
123
+
124
+ if advertise_accept_ranges and get_header(headers, b'accept-ranges') is None and status in {200, 206} and get_header(headers, b'content-encoding') is None:
125
+ append_if_missing(headers, b'accept-ranges', b'bytes')
126
+
127
+ content_length = len(body) if conditional.precondition_failed else int(body_length)
128
+ headers = finalize_response_content_length(
129
+ method=method_upper,
130
+ status=status,
131
+ headers=headers,
132
+ body_length=content_length,
133
+ trailers_present=trailers_present,
134
+ )
135
+
136
+ if method_upper == 'HEAD':
137
+ return EntitySemanticsResult(
138
+ status=status,
139
+ headers=headers,
140
+ body=b'',
141
+ content_coding=_default_selection(),
142
+ not_modified=conditional.not_modified,
143
+ precondition_failed=conditional.precondition_failed,
144
+ head_response=True,
145
+ )
146
+
147
+ return EntitySemanticsResult(
148
+ status=status,
149
+ headers=headers,
150
+ body=body,
151
+ content_coding=_default_selection(),
152
+ not_modified=conditional.not_modified,
153
+ precondition_failed=conditional.precondition_failed,
154
+ )
155
+
156
+
157
+ def plan_file_backed_response_entity_semantics(
158
+ *,
159
+ method: str,
160
+ request_headers: list[tuple[bytes, bytes]] | tuple[tuple[bytes, bytes], ...],
161
+ response_headers: list[tuple[bytes, bytes]],
162
+ status: int,
163
+ body_path: str,
164
+ body_length: int,
165
+ generated_etag: bytes | None = None,
166
+ apply_content_coding: bool = True,
167
+ trailers_present: bool = False,
168
+ ) -> FileBackedEntitySemanticsResult:
169
+ method_upper = method.upper()
170
+ headers = _normalize_headers(response_headers)
171
+ if generated_etag is not None and get_header(headers, b'etag') is None and status not in {412, 416} and not (100 <= status < 200):
172
+ headers = list(headers)
173
+ headers.append((b'etag', generated_etag))
174
+
175
+ if should_materialize_response_body(
176
+ method=method_upper,
177
+ request_headers=request_headers,
178
+ response_headers=headers,
179
+ status=status,
180
+ apply_content_coding=apply_content_coding,
181
+ ):
182
+ return FileBackedEntitySemanticsResult(
183
+ status=status,
184
+ headers=headers,
185
+ body=b'',
186
+ requires_materialization=True,
187
+ )
188
+
189
+ conditional = apply_conditional_request(
190
+ method=method_upper,
191
+ request_headers=request_headers,
192
+ response_headers=headers,
193
+ body=b'',
194
+ status=status,
195
+ )
196
+ if conditional.not_modified or conditional.precondition_failed:
197
+ precondition_body = conditional.body if not (method_upper == 'HEAD') else b''
198
+ precondition_headers = finalize_response_content_length(
199
+ method=method_upper,
200
+ status=conditional.status,
201
+ headers=conditional.headers,
202
+ body_length=len(conditional.body),
203
+ trailers_present=False,
204
+ )
205
+ return FileBackedEntitySemanticsResult(
206
+ status=conditional.status,
207
+ headers=precondition_headers,
208
+ body=precondition_body,
209
+ not_modified=conditional.not_modified,
210
+ precondition_failed=conditional.precondition_failed,
211
+ head_response=method_upper == 'HEAD',
212
+ )
213
+
214
+ plan = plan_file_byte_ranges(
215
+ method=method_upper,
216
+ request_headers=request_headers,
217
+ response_headers=conditional.headers,
218
+ resource_length=body_length,
219
+ status=conditional.status,
220
+ )
221
+ headers = finalize_response_content_length(
222
+ method=method_upper,
223
+ status=plan.status,
224
+ headers=plan.headers,
225
+ body_length=plan.body_length,
226
+ trailers_present=trailers_present,
227
+ )
228
+
229
+ if method_upper == 'HEAD' or not response_allows_body(plan.status) or plan.unsatisfied:
230
+ return FileBackedEntitySemanticsResult(
231
+ status=plan.status,
232
+ headers=headers,
233
+ body=b'',
234
+ range_applied=plan.applied,
235
+ head_response=method_upper == 'HEAD',
236
+ )
237
+
238
+ body_segments = build_file_range_segments(
239
+ path=body_path,
240
+ plan=plan,
241
+ total_length=body_length,
242
+ source_content_type=get_header(conditional.headers, b'content-type'),
243
+ )
244
+ return FileBackedEntitySemanticsResult(
245
+ status=plan.status,
246
+ headers=headers,
247
+ body=b'',
248
+ body_segments=body_segments,
249
+ use_body_segments=True,
250
+ range_applied=plan.applied,
251
+ )
252
+
253
+
254
+ def apply_response_entity_semantics(
255
+ *,
256
+ method: str,
257
+ request_headers: list[tuple[bytes, bytes]] | tuple[tuple[bytes, bytes], ...],
258
+ response_headers: list[tuple[bytes, bytes]],
259
+ body: bytes,
260
+ status: int,
261
+ content_coding_policy: str = 'allowlist',
262
+ supported_codings: tuple[str, ...] = ('br', 'gzip', 'deflate'),
263
+ apply_content_coding: bool = True,
264
+ generate_etag: bool = True,
265
+ trailers_present: bool = False,
266
+ ) -> EntitySemanticsResult:
267
+ method_upper = method.upper()
268
+ headers = _normalize_headers(response_headers)
269
+ range_present = get_header(request_headers, b'range') is not None and method_upper in {'GET', 'HEAD'}
270
+
271
+ if apply_content_coding and not range_present:
272
+ status, headers, body, selection = apply_http_content_coding(
273
+ request_headers=request_headers,
274
+ response_headers=headers,
275
+ body=body,
276
+ status=status,
277
+ policy=content_coding_policy,
278
+ supported=supported_codings,
279
+ )
280
+ else:
281
+ selection = _default_selection()
282
+
283
+ headers = _maybe_generate_etag(headers, body, enabled=generate_etag and status not in {412, 416} and not (100 <= status < 200))
284
+
285
+ conditional = apply_conditional_request(
286
+ method=method_upper,
287
+ request_headers=request_headers,
288
+ response_headers=headers,
289
+ body=body,
290
+ status=status,
291
+ )
292
+ status = conditional.status
293
+ headers = conditional.headers
294
+ body = conditional.body
295
+ range_applied = False
296
+
297
+ if not conditional.not_modified and not conditional.precondition_failed:
298
+ range_result = apply_byte_ranges(
299
+ method=method_upper,
300
+ request_headers=request_headers,
301
+ response_headers=headers,
302
+ body=body,
303
+ status=status,
304
+ )
305
+ status = range_result.status
306
+ headers = range_result.headers
307
+ body = range_result.body
308
+ range_applied = range_result.applied
309
+
310
+ if get_header(headers, b'accept-ranges') is None and status in {200, 206} and get_header(headers, b'content-encoding') is None:
311
+ append_if_missing(headers, b'accept-ranges', b'bytes')
312
+
313
+ headers = finalize_response_content_length(method=method_upper, status=status, headers=headers, body_length=len(body), trailers_present=trailers_present)
314
+
315
+ if method_upper == 'HEAD':
316
+ return EntitySemanticsResult(
317
+ status=status,
318
+ headers=headers,
319
+ body=b'',
320
+ content_coding=selection,
321
+ range_applied=range_applied,
322
+ not_modified=conditional.not_modified,
323
+ precondition_failed=conditional.precondition_failed,
324
+ head_response=True,
325
+ )
326
+
327
+ return EntitySemanticsResult(
328
+ status=status,
329
+ headers=headers,
330
+ body=body,
331
+ content_coding=selection,
332
+ range_applied=range_applied,
333
+ not_modified=conditional.not_modified,
334
+ precondition_failed=conditional.precondition_failed,
335
+ )
336
+
337
+
338
+ __all__ = ['EntitySemanticsResult', 'FileBackedEntitySemanticsResult', 'apply_header_only_response_semantics', 'apply_response_entity_semantics', 'finalize_response_content_length', 'plan_file_backed_response_entity_semantics', 'should_materialize_response_body']