Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/werkzeug/http.py: 65%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

434 statements  

1from __future__ import annotations 

2 

3import email.utils 

4import hashlib 

5import re 

6import typing as t 

7import warnings 

8from base64 import b64encode 

9from datetime import date 

10from datetime import datetime 

11from datetime import time 

12from datetime import timedelta 

13from datetime import timezone 

14from enum import Enum 

15from time import mktime 

16from time import struct_time 

17from urllib.parse import quote 

18from urllib.parse import unquote 

19 

20from ._internal import _dt_as_utc 

21from ._internal import _plain_int 

22 

23if t.TYPE_CHECKING: 

24 from _typeshed.wsgi import WSGIEnvironment 

25 

26_token_chars = frozenset( 

27 "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~" 

28) 

29_entity_headers = frozenset( 

30 [ 

31 "allow", 

32 "content-encoding", 

33 "content-language", 

34 "content-length", 

35 "content-location", 

36 "content-md5", 

37 "content-range", 

38 "content-type", 

39 "expires", 

40 "last-modified", 

41 ] 

42) 

43_hop_by_hop_headers = frozenset( 

44 [ 

45 "connection", 

46 "keep-alive", 

47 "proxy-authenticate", 

48 "proxy-authorization", 

49 "te", 

50 "trailer", 

51 "transfer-encoding", 

52 "upgrade", 

53 ] 

54) 

55_HTTP_STATUS_CODES = { 

56 100: "Continue", 

57 101: "Switching Protocols", 

58 102: "Processing", 

59 103: "Early Hints", # see RFC 8297 

60 200: "OK", 

61 201: "Created", 

62 202: "Accepted", 

63 203: "Non Authoritative Information", 

64 204: "No Content", 

65 205: "Reset Content", 

66 206: "Partial Content", 

67 207: "Multi Status", 

68 208: "Already Reported", # see RFC 5842 

69 226: "IM Used", # see RFC 3229 

70 300: "Multiple Choices", 

71 301: "Moved Permanently", 

72 302: "Found", 

73 303: "See Other", 

74 304: "Not Modified", 

75 305: "Use Proxy", 

76 306: "Switch Proxy", # unused 

77 307: "Temporary Redirect", 

78 308: "Permanent Redirect", 

79 400: "Bad Request", 

80 401: "Unauthorized", 

81 402: "Payment Required", # unused 

82 403: "Forbidden", 

83 404: "Not Found", 

84 405: "Method Not Allowed", 

85 406: "Not Acceptable", 

86 407: "Proxy Authentication Required", 

87 408: "Request Timeout", 

88 409: "Conflict", 

89 410: "Gone", 

90 411: "Length Required", 

91 412: "Precondition Failed", 

92 413: "Request Entity Too Large", 

93 414: "Request URI Too Long", 

94 415: "Unsupported Media Type", 

95 416: "Requested Range Not Satisfiable", 

96 417: "Expectation Failed", 

97 418: "I'm a teapot", # see RFC 2324 

98 421: "Misdirected Request", # see RFC 7540 

99 422: "Unprocessable Entity", 

100 423: "Locked", 

101 424: "Failed Dependency", 

102 425: "Too Early", # see RFC 8470 

103 426: "Upgrade Required", 

104 428: "Precondition Required", # see RFC 6585 

105 429: "Too Many Requests", 

106 431: "Request Header Fields Too Large", 

107 449: "Retry With", # proprietary MS extension 

108 451: "Unavailable For Legal Reasons", 

109 500: "Internal Server Error", 

110 501: "Not Implemented", 

111 502: "Bad Gateway", 

112 503: "Service Unavailable", 

113 504: "Gateway Timeout", 

114 505: "HTTP Version Not Supported", 

115 506: "Variant Also Negotiates", # see RFC 2295 

116 507: "Insufficient Storage", 

117 508: "Loop Detected", # see RFC 5842 

118 510: "Not Extended", 

119 511: "Network Authentication Failed", 

120} 

121 

122 

123class COEP(Enum): 

124 """``Cross-Origin-Embedder-Policy`` header values. Used by 

125 :attr:`.Response.cross_origin_embedder_policy`. 

126 

127 .. versionchanged:: 3.2 

128 Added the ``credentialless`` member. 

129 

130 .. versionadded:: 2.0 

131 """ 

132 

133 UNSAFE_NONE = "unsafe-none" 

134 REQUIRE_CORP = "require-corp" 

135 CREDENTIALLESS = "credentialless" 

136 

137 

138class COOP(Enum): 

139 """``Cross-Origin-Opener-Policy`` header values. Used by 

140 :attr:`.Response.cross_origin_opener_policy`. 

141 

142 .. versionchanged:: 3.2 

143 Added the ``noopener-allow-popups`` member. 

144 

145 .. versionadded:: 2.0 

146 """ 

147 

148 UNSAFE_NONE = "unsafe-none" 

149 SAME_ORIGIN_ALLOW_POPUPS = "same-origin-allow-popups" 

150 SAME_ORIGIN = "same-origin" 

151 NOOPENER_ALLOW_POPUPS = "noopener-allow-popups" 

152 

153 

154class CORP(Enum): 

155 """``Cross-Origin-Resource-Policy`` header values. Used by 

156 :attr:`.Response.cross_origin_resource_policy`. 

157 

158 .. versionadded:: 3.2 

159 """ 

160 

161 SAME_SITE = "same-site" 

162 SAME_ORIGIN = "same-origin" 

163 CROSS_ORIGIN = "cross-origin" 

164 

165 

166class SecFetchSite(Enum): 

167 """``Sec-Fetch-Site`` header values. Used by :attr:`.Request.sec_fetch_site`. 

168 

169 .. versionadded:: 3.2 

170 """ 

171 

172 CROSS_SITE = "cross-site" 

173 SAME_ORIGIN = "same-origin" 

174 SAME_SITE = "same-site" 

175 NONE = "none" 

176 

177 

178class SecFetchMode(Enum): 

179 """``Sec-Fetch-Mode`` header values. Used by :attr:`.Request.sec_fetch_mode`. 

180 

181 .. versionadded:: 3.2 

182 """ 

183 

184 CORS = "cors" 

185 NAVIGATE = "navigate" 

186 NO_CORS = "no-cors" 

187 SAME_ORIGIN = "same-origin" 

188 WEBSOCKET = "websocket" 

189 

190 

191class SecFetchDest(Enum): 

192 """``Sec-Fetch-Dest`` header values. Used by :attr:`.Request.sec_fetch_dest`. 

193 

194 .. versionadded:: 3.2 

195 """ 

196 

197 AUDIO = "audio" 

198 AUDIOWORKLET = "audioworklet" 

199 DOCUMENT = "document" 

200 EMBED = "embed" 

201 EMPTY = "empty" 

202 FENCEDFRAME = "fencedframe" 

203 FONT = "font" 

204 FRAME = "frame" 

205 IFRAME = "iframe" 

206 IMAGE = "image" 

207 JSON = "json" 

208 MANIFEST = "manifest" 

209 OBJECT = "object" 

210 PAINTWORKLET = "paintworklet" 

211 REPORT = "report" 

212 SCRIPT = "script" 

213 SERVICEWORKER = "serviceworker" 

214 SHAREDWORKER = "sharedworker" 

215 STYLE = "style" 

216 TRACK = "track" 

217 VIDEO = "video" 

218 WEBIDENTITY = "webidentity" 

219 WORKER = "worker" 

220 XSLT = "xslt" 

221 

222 

223def quote_header_value(value: t.Any, allow_token: bool = True) -> str: 

224 """Add double quotes around a header value. If the header contains only ASCII token 

225 characters, it will be returned unchanged. If the header contains ``"`` or ``\\`` 

226 characters, they will be escaped with an additional ``\\`` character. 

227 

228 This is the reverse of :func:`unquote_header_value`. 

229 

230 :param value: The value to quote. Will be converted to a string. 

231 :param allow_token: Disable to quote the value even if it only has token characters. 

232 

233 .. versionchanged:: 3.0 

234 Passing bytes is not supported. 

235 

236 .. versionchanged:: 3.0 

237 The ``extra_chars`` parameter is removed. 

238 

239 .. versionchanged:: 2.3 

240 The value is quoted if it is the empty string. 

241 

242 .. versionadded:: 0.5 

243 """ 

244 value_str = str(value) 

245 

246 if not value_str: 

247 return '""' 

248 

249 if allow_token: 

250 token_chars = _token_chars 

251 

252 if token_chars.issuperset(value_str): 

253 return value_str 

254 

255 value_str = value_str.replace("\\", "\\\\").replace('"', '\\"') 

256 return f'"{value_str}"' 

257 

258 

259_unslash_re = re.compile(r"\\(.)", re.A) 

260 

261 

262def unquote_header_value(value: str) -> str: 

263 """Remove double quotes and backslash escapes from a header value. 

264 

265 This is the reverse of :func:`quote_header_value`. 

266 

267 :param value: The header value to unquote. 

268 

269 .. versionchanged:: 3.2 

270 Removes escape preceding any character. 

271 

272 .. versionchanged:: 3.0 

273 The ``is_filename`` parameter is removed. 

274 """ 

275 if len(value) >= 2 and value[0] == value[-1] == '"': 

276 return _unslash_re.sub(r"\g<1>", value[1:-1]) 

277 

278 return value 

279 

280 

281def dump_options_header(header: str | None, options: t.Mapping[str, t.Any]) -> str: 

282 """Produce a header value and ``key=value`` parameters separated by semicolons 

283 ``;``. For example, the ``Content-Type`` header. 

284 

285 .. code-block:: python 

286 

287 dump_options_header("text/html", {"charset": "UTF-8"}) 

288 'text/html; charset=UTF-8' 

289 

290 This is the reverse of :func:`parse_options_header`. 

291 

292 If a value contains non-token characters, it will be quoted. 

293 

294 If a value is ``None``, the parameter is skipped. 

295 

296 In some keys for some headers, a UTF-8 value can be encoded using a special 

297 ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will 

298 not produce that format automatically, but if a given key ends with an asterisk 

299 ``*``, the value is assumed to have that form and will not be quoted further. 

300 

301 :param header: The primary header value. 

302 :param options: Parameters to encode as ``key=value`` pairs. 

303 

304 .. versionchanged:: 2.3 

305 Keys with ``None`` values are skipped rather than treated as a bare key. 

306 

307 .. versionchanged:: 2.2.3 

308 If a key ends with ``*``, its value will not be quoted. 

309 """ 

310 segments = [] 

311 

312 if header is not None: 

313 segments.append(header) 

314 

315 for key, value in options.items(): 

316 if value is None: 

317 continue 

318 

319 if key[-1] == "*": 

320 segments.append(f"{key}={value}") 

321 else: 

322 segments.append(f"{key}={quote_header_value(value)}") 

323 

324 return "; ".join(segments) 

325 

326 

327def dump_header(iterable: dict[str, t.Any] | t.Iterable[t.Any]) -> str: 

328 """Produce a header value from a list of items or ``key=value`` pairs, separated by 

329 commas ``,``. 

330 

331 This is the reverse of :func:`parse_list_header`, :func:`parse_dict_header`, and 

332 :func:`parse_set_header`. 

333 

334 If a value contains non-token characters, it will be quoted. 

335 

336 If a value is ``None``, the key is output alone. 

337 

338 In some keys for some headers, a UTF-8 value can be encoded using a special 

339 ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will 

340 not produce that format automatically, but if a given key ends with an asterisk 

341 ``*``, the value is assumed to have that form and will not be quoted further. 

342 

343 .. code-block:: python 

344 

345 dump_header(["foo", "bar baz"]) 

346 'foo, "bar baz"' 

347 

348 dump_header({"foo": "bar baz"}) 

349 'foo="bar baz"' 

350 

351 :param iterable: The items to create a header from. 

352 

353 .. versionchanged:: 3.0 

354 The ``allow_token`` parameter is removed. 

355 

356 .. versionchanged:: 2.2.3 

357 If a key ends with ``*``, its value will not be quoted. 

358 """ 

359 if isinstance(iterable, dict): 

360 items = [] 

361 

362 for key, value in iterable.items(): 

363 if value is None: 

364 items.append(key) 

365 elif key[-1] == "*": 

366 items.append(f"{key}={value}") 

367 else: 

368 items.append(f"{key}={quote_header_value(value)}") 

369 else: 

370 items = [quote_header_value(x) for x in iterable] 

371 

372 return ", ".join(items) 

373 

374 

375def _dump_csp_header(header: ds.ContentSecurityPolicy) -> str: 

376 """Dump a Content Security Policy header. 

377 

378 These are structured into policies such as "default-src 'self'; 

379 script-src 'self'". 

380 

381 .. deprecated:: 3.2 

382 Will be removed in Werkzeug 3.3. Use the 

383 ``ContentSecurityPolicy.to_header`` method instead. 

384 

385 .. versionadded:: 1.0.0 

386 Support for Content Security Policy headers was added. 

387 

388 """ 

389 import warnings 

390 

391 warnings.warn( 

392 "The 'dump_csp_header' function is deprecated and will be removed in" 

393 " Werkzeug 3.3. Use the 'ContentSecurityPolicy.to_header' method instead.", 

394 DeprecationWarning, 

395 stacklevel=2, 

396 ) 

397 return header.to_header() 

398 

399 

400def parse_list_header(value: str) -> list[str]: 

401 """Parse a header value that consists of a list of comma separated items according 

402 to `RFC 9110 <https://httpwg.org/specs/rfc9110.html#abnf.extension>`__. 

403 

404 Surrounding quotes are removed from items, but internal quotes are left for 

405 future parsing. Empty values are discarded. 

406 

407 .. code-block:: python 

408 

409 parse_list_header('token, "quoted value"') 

410 ['token', 'quoted value'] 

411 

412 This is the reverse of :func:`dump_header`. 

413 

414 :param value: The header value to parse. 

415 

416 .. versionchanged:: 3.2 

417 Quotes and escapes are kept if only part of an item is quoted. Empty 

418 values are omitted. An empty list is returned if the value contains an 

419 unclosed quoted string. 

420 """ 

421 items = [] 

422 item = "" 

423 escape = False 

424 quote = False 

425 

426 for char in value: 

427 if escape: 

428 escape = False 

429 item += char 

430 continue 

431 

432 if quote: 

433 if char == "\\": 

434 escape = True 

435 elif char == '"': 

436 quote = False 

437 

438 item += char 

439 continue 

440 

441 if char == ",": 

442 items.append(item) 

443 item = "" 

444 continue 

445 

446 if char == '"': 

447 quote = True 

448 

449 item += char 

450 

451 if quote: 

452 # invalid, unclosed quoted string 

453 return [] 

454 

455 items.append(item) 

456 return [ 

457 unquote_header_value(item) for item in (item.strip() for item in items) if item 

458 ] 

459 

460 

461def parse_dict_header(value: str) -> dict[str, str | None]: 

462 """Parse a list header using :func:`parse_list_header`, then parse each item as a 

463 ``key=value`` pair. 

464 

465 .. code-block:: python 

466 

467 parse_dict_header('a=b, c="d, e", f') 

468 {"a": "b", "c": "d, e", "f": None} 

469 

470 This is the reverse of :func:`dump_header`. 

471 

472 If a key does not have a value, it is ``None``. 

473 

474 This handles charsets for values as described in 

475 `RFC 2231 <https://www.rfc-editor.org/rfc/rfc2231#section-3>`__. Only ASCII, UTF-8, 

476 and ISO-8859-1 charsets are accepted, otherwise the value remains quoted. 

477 

478 :param value: The header value to parse. 

479 

480 .. versionchanged:: 3.2 

481 An empty dict is returned if the value contains an unclosed quoted 

482 string. 

483 

484 .. versionchanged:: 3.0 

485 Passing bytes is not supported. 

486 

487 .. versionchanged:: 3.0 

488 The ``cls`` argument is removed. 

489 

490 .. versionchanged:: 2.3 

491 Added support for ``key*=charset''value`` encoded items. 

492 

493 .. versionchanged:: 0.9 

494 The ``cls`` argument was added. 

495 """ 

496 result: dict[str, str | None] = {} 

497 

498 for item in parse_list_header(value): 

499 key, has_value, value = item.partition("=") 

500 key = key.strip() 

501 

502 if not key: 

503 # =value is not valid 

504 continue 

505 

506 if not has_value: 

507 result[key] = None 

508 continue 

509 

510 value = value.strip() 

511 encoding: str | None = None 

512 

513 if key[-1] == "*": 

514 # key*=charset''value becomes key=value, where value is percent encoded 

515 # adapted from parse_options_header, without the continuation handling 

516 key = key[:-1] 

517 match = _charset_value_re.match(value) 

518 

519 if match: 

520 # If there is a charset marker in the value, split it off. 

521 encoding, value = match.groups() 

522 encoding = encoding.lower() 

523 

524 # A safe list of encodings. Modern clients should only send ASCII or UTF-8. 

525 # This list will not be extended further. An invalid encoding will leave the 

526 # value quoted. 

527 if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}: 

528 # invalid bytes are replaced during unquoting 

529 value = unquote(value, encoding=encoding) 

530 

531 result[key] = unquote_header_value(value) 

532 

533 return result 

534 

535 

536# https://httpwg.org/specs/rfc9110.html#parameter 

537_parameter_key_re = re.compile(r"([\w!#$%&'*+\-.^`|~]+)=", flags=re.ASCII) 

538_parameter_token_value_re = re.compile(r"[\w!#$%&'*+\-.^`|~]+", flags=re.ASCII) 

539# https://www.rfc-editor.org/rfc/rfc2231#section-4 

540_charset_value_re = re.compile( 

541 r""" 

542 ([\w!#$%&*+\-.^`|~]*)' # charset part, could be empty 

543 [\w!#$%&*+\-.^`|~]*' # don't care about language part, usually empty 

544 ([\w!#$%&'*+\-.^`|~]+) # one or more token chars with percent encoding 

545 """, 

546 re.ASCII | re.VERBOSE, 

547) 

548# https://www.rfc-editor.org/rfc/rfc2231#section-3 

549_continuation_re = re.compile(r"\*(\d+)$", re.ASCII) 

550 

551 

552def parse_options_header(value: str | None) -> tuple[str, dict[str, str]]: 

553 """Parse a header that consists of a value with ``key=value`` parameters separated 

554 by semicolons ``;``. For example, the ``Content-Type`` header. 

555 

556 .. code-block:: python 

557 

558 parse_options_header("text/html; charset=UTF-8") 

559 ('text/html', {'charset': 'UTF-8'}) 

560 

561 parse_options_header("") 

562 ("", {}) 

563 

564 This is the reverse of :func:`dump_options_header`. 

565 

566 This parses valid parameter parts as described in 

567 `RFC 9110 <https://httpwg.org/specs/rfc9110.html#parameter>`__. Invalid parts are 

568 skipped. 

569 

570 This handles continuations and charsets as described in 

571 `RFC 2231 <https://www.rfc-editor.org/rfc/rfc2231#section-3>`__, although not as 

572 strictly as the RFC. Only ASCII, UTF-8, and ISO-8859-1 charsets are accepted, 

573 otherwise the value remains quoted. 

574 

575 Clients may not be consistent in how they handle a quote character within a quoted 

576 value. The `HTML Standard <https://html.spec.whatwg.org/#multipart-form-data>`__ 

577 replaces it with ``%22`` in multipart form data. 

578 `RFC 9110 <https://httpwg.org/specs/rfc9110.html#quoted.strings>`__ uses backslash 

579 escapes in HTTP headers. Both are decoded to the ``"`` character. 

580 

581 Clients may not be consistent in how they handle non-ASCII characters. HTML 

582 documents must declare ``<meta charset=UTF-8>``, otherwise browsers may replace with 

583 HTML character references, which can be decoded using :func:`html.unescape`. 

584 

585 :param value: The header value to parse. 

586 :return: ``(value, options)``, where ``options`` is a dict 

587 

588 .. versionchanged:: 2.3 

589 Invalid parts, such as keys with no value, quoted keys, and incorrectly quoted 

590 values, are discarded instead of treating as ``None``. 

591 

592 .. versionchanged:: 2.3 

593 Only ASCII, UTF-8, and ISO-8859-1 are accepted for charset values. 

594 

595 .. versionchanged:: 2.3 

596 Escaped quotes in quoted values, like ``%22`` and ``\\"``, are handled. 

597 

598 .. versionchanged:: 2.2 

599 Option names are always converted to lowercase. 

600 

601 .. versionchanged:: 2.2 

602 The ``multiple`` parameter was removed. 

603 

604 .. versionchanged:: 0.15 

605 :rfc:`2231` parameter continuations are handled. 

606 

607 .. versionadded:: 0.5 

608 """ 

609 if value is None: 

610 return "", {} 

611 

612 value, _, rest = value.partition(";") 

613 value = value.strip() 

614 rest = rest.strip() 

615 

616 if not value or not rest: 

617 # empty (invalid) value, or value without options 

618 return value, {} 

619 

620 # Collect all valid key=value parts without processing the value. 

621 parts: list[tuple[str, str]] = [] 

622 

623 while True: 

624 if (m := _parameter_key_re.match(rest)) is not None: 

625 pk = m.group(1).lower() 

626 rest = rest[m.end() :] 

627 

628 # Value may be a token. 

629 if (m := _parameter_token_value_re.match(rest)) is not None: 

630 parts.append((pk, m.group())) 

631 

632 # Value may be a quoted string, find the closing quote. 

633 elif rest[:1] == '"': 

634 pos = 1 

635 length = len(rest) 

636 

637 while pos < length: 

638 if rest[pos : pos + 2] in {"\\\\", '\\"'}: 

639 # Consume escaped slashes and quotes. 

640 pos += 2 

641 elif rest[pos] == '"': 

642 # Stop at an unescaped quote. 

643 parts.append((pk, rest[: pos + 1])) 

644 rest = rest[pos + 1 :] 

645 break 

646 else: 

647 # Consume any other character. 

648 pos += 1 

649 

650 # Find the next section delimited by `;`, if any. 

651 if (end := rest.find(";")) == -1: 

652 break 

653 

654 rest = rest[end + 1 :].lstrip() 

655 

656 options: dict[str, str] = {} 

657 encoding: str | None = None 

658 continued_encoding: str | None = None 

659 

660 # For each collected part, process optional charset and continuation, 

661 # unquote quoted values. 

662 for pk, pv in parts: 

663 if pk[-1] == "*": 

664 # key*=charset''value becomes key=value, where value is percent encoded 

665 pk = pk[:-1] 

666 match = _charset_value_re.match(pv) 

667 

668 if match: 

669 # If there is a valid charset marker in the value, split it off. 

670 encoding, pv = match.groups() 

671 # This might be the empty string, handled next. 

672 encoding = encoding.lower() 

673 

674 # No charset marker, or marker with empty charset value. 

675 if not encoding: 

676 encoding = continued_encoding 

677 

678 # A safe list of encodings. Modern clients should only send ASCII or UTF-8. 

679 # This list will not be extended further. An invalid encoding will leave the 

680 # value quoted. 

681 if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}: 

682 # Continuation parts don't require their own charset marker. This is 

683 # looser than the RFC, it will persist across different keys and allows 

684 # changing the charset during a continuation. But this implementation is 

685 # much simpler than tracking the full state. 

686 continued_encoding = encoding 

687 # invalid bytes are replaced during unquoting 

688 pv = unquote(pv, encoding=encoding) 

689 

690 # Remove quotes. At this point the value cannot be empty or a single quote. 

691 if pv[0] == pv[-1] == '"': 

692 # HTTP headers use slash, multipart form data uses percent 

693 pv = pv[1:-1].replace("\\\\", "\\").replace('\\"', '"').replace("%22", '"') 

694 

695 match = _continuation_re.search(pk) 

696 

697 if match: 

698 # key*0=a; key*1=b becomes key=ab 

699 pk = pk[: match.start()] 

700 options[pk] = options.get(pk, "") + pv 

701 else: 

702 options[pk] = pv 

703 

704 return value, options 

705 

706 

707_TAnyAccept = t.TypeVar("_TAnyAccept", bound="ds.Accept") 

708 

709 

710@t.overload 

711def _parse_accept_header(value: str | None) -> ds.Accept: ... 

712 

713 

714@t.overload 

715def _parse_accept_header(value: str | None, cls: type[_TAnyAccept]) -> _TAnyAccept: ... 

716 

717 

718def _parse_accept_header( 

719 value: str | None, cls: type[_TAnyAccept] | None = None 

720) -> _TAnyAccept: 

721 """Parse an ``Accept`` header according to 

722 `RFC 9110 <https://httpwg.org/specs/rfc9110.html#field.accept>`__. 

723 

724 Returns an :class:`.Accept` instance, which can sort and inspect items based on 

725 their quality parameter. When parsing ``Accept-Charset``, ``Accept-Encoding``, or 

726 ``Accept-Language``, pass the appropriate :class:`.Accept` subclass. 

727 

728 :param value: The header value to parse. 

729 :param cls: The :class:`.Accept` class to wrap the result in. 

730 :return: An instance of ``cls``. 

731 

732 .. deprecated:: 3.2 

733 Will be removed in Werkzeug 3.3. Use the ``Accept.from_header`` method 

734 instead. 

735 

736 .. versionchanged:: 2.3 

737 Parse according to RFC 9110. Items with invalid ``q`` values are skipped. 

738 """ 

739 import warnings 

740 

741 warnings.warn( 

742 "The 'parse_accept_header' function is deprecated and will be removed in" 

743 " Werkzeug 3.3. Use the 'Accept.from_header' method instead.", 

744 DeprecationWarning, 

745 stacklevel=2, 

746 ) 

747 

748 if cls is None: 

749 cls = t.cast(type[_TAnyAccept], ds.Accept) 

750 

751 return cls.from_header(value) 

752 

753 

754_TAnyCC = t.TypeVar("_TAnyCC", bound="ds.cache_control._CacheControl") 

755 

756 

757@t.overload 

758def _parse_cache_control_header( 

759 value: str | None, 

760 on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None, 

761) -> ds.RequestCacheControl: ... 

762 

763 

764@t.overload 

765def _parse_cache_control_header( 

766 value: str | None, 

767 on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None, 

768 cls: type[_TAnyCC] = ..., 

769) -> _TAnyCC: ... 

770 

771 

772def _parse_cache_control_header( 

773 value: str | None, 

774 on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None, 

775 cls: type[_TAnyCC] | None = None, 

776) -> _TAnyCC: 

777 """Parse a cache control header. The RFC differs between response and 

778 request cache control, this method does not. It's your responsibility 

779 to not use the wrong control statements. 

780 

781 .. deprecated:: 3.2 

782 Will be removed in Werkzeug 3.3. Use the ``CacheControl.from_header`` 

783 method instead. 

784 

785 .. versionadded:: 0.5 

786 The `cls` was added. If not specified an immutable 

787 :class:`~werkzeug.datastructures.RequestCacheControl` is returned. 

788 

789 :param value: a cache control header to be parsed. 

790 :param on_update: an optional callable that is called every time a value 

791 on the :class:`~werkzeug.datastructures.CacheControl` 

792 object is changed. 

793 :param cls: the class for the returned object. By default 

794 :class:`~werkzeug.datastructures.RequestCacheControl` is used. 

795 :return: a `cls` object. 

796 """ 

797 import warnings 

798 

799 warnings.warn( 

800 "The 'parse_cache_control_header' function is deprecated and will be" 

801 " removed in Werkzeug 3.3. Use the 'RequestCacheControl.from_header'" 

802 " method instead.", 

803 DeprecationWarning, 

804 stacklevel=2, 

805 ) 

806 

807 if cls is None: 

808 cls = t.cast(type[_TAnyCC], ds.RequestCacheControl) 

809 

810 obj = cls.from_header(value) 

811 obj.on_update = on_update 

812 return obj 

813 

814 

815_TAnyCSP = t.TypeVar("_TAnyCSP", bound="ds.ContentSecurityPolicy") 

816 

817 

818@t.overload 

819def _parse_csp_header( 

820 value: str | None, 

821 on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None, 

822) -> ds.ContentSecurityPolicy: ... 

823 

824 

825@t.overload 

826def _parse_csp_header( 

827 value: str | None, 

828 on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None, 

829 cls: type[_TAnyCSP] = ..., 

830) -> _TAnyCSP: ... 

831 

832 

833def _parse_csp_header( 

834 value: str | None, 

835 on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None, 

836 cls: type[_TAnyCSP] | None = None, 

837) -> _TAnyCSP: 

838 """Parse a Content Security Policy header. 

839 

840 .. deprecated:: 3.2 

841 Will be removed in Werkzeug 3.3. Use the 

842 ``ContentSecurityPolicy.from_header`` method instead. 

843 

844 .. versionadded:: 1.0 

845 Support for Content Security Policy headers was added. 

846 

847 :param value: a csp header to be parsed. 

848 :param on_update: an optional callable that is called every time a value 

849 on the object is changed. 

850 :param cls: the class for the returned object. By default 

851 :class:`~werkzeug.datastructures.ContentSecurityPolicy` is used. 

852 :return: a `cls` object. 

853 """ 

854 import warnings 

855 

856 warnings.warn( 

857 "The 'parse_csp_header' function is deprecated and will be removed in" 

858 " Werkzeug 3.3. Use the 'Response.content_security_policy' property or" 

859 " the 'ContentSecurityPolicy.from_header' method instead.", 

860 DeprecationWarning, 

861 stacklevel=2, 

862 ) 

863 

864 if cls is None: 

865 cls = t.cast(type[_TAnyCSP], ds.ContentSecurityPolicy) 

866 

867 obj = cls.from_header(value) 

868 obj.on_update = on_update 

869 return obj 

870 

871 

872def _parse_set_header( 

873 value: str | None, 

874 on_update: t.Callable[[ds.HeaderSet], None] | None = None, 

875) -> ds.HeaderSet: 

876 """Parse a set-like header and return a 

877 :class:`~werkzeug.datastructures.HeaderSet` object: 

878 

879 >>> hs = parse_set_header('token, "quoted value"') 

880 

881 The return value is an object that treats the items case-insensitively 

882 and keeps the order of the items: 

883 

884 >>> 'TOKEN' in hs 

885 True 

886 >>> hs.index('quoted value') 

887 1 

888 >>> hs 

889 HeaderSet(['token', 'quoted value']) 

890 

891 To create a header from the :class:`HeaderSet` again, use the 

892 :func:`dump_header` function. 

893 

894 :param value: a set header to be parsed. 

895 :param on_update: an optional callable that is called every time a 

896 value on the :class:`~werkzeug.datastructures.HeaderSet` 

897 object is changed. 

898 

899 .. deprecated:: 3.2 

900 Will be removed in Werkzeug 3.3. Use the ``HeaderSet.from_header`` 

901 method instead. 

902 """ 

903 import warnings 

904 

905 warnings.warn( 

906 "The 'parse_set_header' function is deprecated and will be removed in" 

907 " Werkzeug 3.3. Use the 'HeaderSet.from_header' method instead.", 

908 DeprecationWarning, 

909 stacklevel=2, 

910 ) 

911 obj = ds.HeaderSet.from_header(value) 

912 obj._on_update = on_update 

913 return obj 

914 

915 

916def _parse_if_range_header(value: str | None) -> ds.IfRange: 

917 """Parses an if-range header which can be an etag or a date. Returns 

918 a :class:`~werkzeug.datastructures.IfRange` object. 

919 

920 .. deprecated:: 3.2 

921 Will be removed in Werkzeug 3.3. Use the ``IfRange.from_header`` 

922 method instead. 

923 

924 .. versionchanged:: 2.0 

925 If the value represents a datetime, it is timezone-aware. 

926 

927 .. versionadded:: 0.7 

928 """ 

929 import warnings 

930 

931 warnings.warn( 

932 "The 'parse_if_range_header' function is deprecated and will be removed in" 

933 " Werkzeug 3.3. Use the 'IfRange.from_header' method instead.", 

934 DeprecationWarning, 

935 stacklevel=2, 

936 ) 

937 return ds.IfRange.from_header(value) 

938 

939 

940def _parse_range_header( 

941 value: str | None, make_inclusive: bool = True 

942) -> ds.Range | None: 

943 """Parses a range header into a :class:`~werkzeug.datastructures.Range` 

944 object. If the header is missing or malformed `None` is returned. 

945 `ranges` is a list of ``(start, stop)`` tuples where the ranges are 

946 non-inclusive. 

947 

948 .. deprecated:: 3.2 

949 Will be removed in Werkzeug 3.3. Use the ``Range.from_header`` 

950 method instead. 

951 

952 .. versionadded:: 0.7 

953 """ 

954 import warnings 

955 

956 warnings.warn( 

957 "The 'parse_range_header' function is deprecated and will be removed in" 

958 " Werkzeug 3.3. Use the 'Range.from_header' method instead.", 

959 DeprecationWarning, 

960 stacklevel=2, 

961 ) 

962 return ds.Range.from_header(value) 

963 

964 

965def _parse_content_range_header( 

966 value: str | None, 

967 on_update: t.Callable[[ds.ContentRange], None] | None = None, 

968) -> ds.ContentRange | None: 

969 """Parses a range header into a 

970 :class:`~werkzeug.datastructures.ContentRange` object or `None` if 

971 parsing is not possible. 

972 

973 .. deprecated:: 3.2 

974 Will be removed in Werkzeug 3.3. Use the ``ContentRange.from_header`` 

975 method instead. 

976 

977 .. versionadded:: 0.7 

978 

979 :param value: a content range header to be parsed. 

980 :param on_update: an optional callable that is called every time a value 

981 on the :class:`~werkzeug.datastructures.ContentRange` 

982 object is changed. 

983 """ 

984 import warnings 

985 

986 warnings.warn( 

987 "The 'parse_range_header' function is deprecated and will be removed in" 

988 " Werkzeug 3.3. Use the 'Range.from_header' method instead.", 

989 DeprecationWarning, 

990 stacklevel=2, 

991 ) 

992 

993 if (obj := ds.ContentRange.from_header(value)) is None: 

994 return None 

995 

996 obj._on_update = on_update 

997 return obj 

998 

999 

1000def quote_etag(etag: str, weak: bool = False) -> str: 

1001 """Quote an etag. 

1002 

1003 :param etag: the etag to quote. 

1004 :param weak: set to `True` to tag it "weak". 

1005 """ 

1006 if '"' in etag: 

1007 raise ValueError("invalid etag") 

1008 etag = f'"{etag}"' 

1009 if weak: 

1010 etag = f"W/{etag}" 

1011 return etag 

1012 

1013 

1014@t.overload 

1015def unquote_etag(etag: str) -> tuple[str, bool]: ... 

1016@t.overload 

1017def unquote_etag(etag: None) -> tuple[None, None]: ... 

1018def unquote_etag( 

1019 etag: str | None, 

1020) -> tuple[str, bool] | tuple[None, None]: 

1021 """Unquote a single etag: 

1022 

1023 >>> unquote_etag('W/"bar"') 

1024 ('bar', True) 

1025 >>> unquote_etag('"bar"') 

1026 ('bar', False) 

1027 

1028 :param etag: the etag identifier to unquote. 

1029 :return: a ``(etag, weak)`` tuple. 

1030 """ 

1031 if not etag: 

1032 return None, None 

1033 etag = etag.strip() 

1034 weak = False 

1035 if etag.startswith(("W/", "w/")): 

1036 weak = True 

1037 etag = etag[2:] 

1038 if etag[:1] == etag[-1:] == '"': 

1039 etag = etag[1:-1] 

1040 return etag, weak 

1041 

1042 

1043def _parse_etags(value: str | None) -> ds.ETags: 

1044 """Parse an etag header. 

1045 

1046 :param value: the tag header to parse 

1047 :return: an :class:`~werkzeug.datastructures.ETags` object. 

1048 

1049 .. deprecated:: 3.2 

1050 Will be removed in Werkzeug 3.3. Use the 'ETags.from_header' method instead. 

1051 """ 

1052 import warnings 

1053 

1054 warnings.warn( 

1055 "The 'parse_etags' function is deprecated and will be removed in" 

1056 " Werkzeug 3.3. Use the 'ETags.from_header' method instead.", 

1057 DeprecationWarning, 

1058 stacklevel=2, 

1059 ) 

1060 return ds.ETags.from_header(value) 

1061 

1062 

1063def generate_etag(data: bytes) -> str: 

1064 """Generate a strong ETag value by hashing the given data. 

1065 

1066 .. versionchanged:: 3.2 

1067 Use SHA3-256. SHA-1 is not allowed in FIPS-enabled systems. Use base64 

1068 instead of hex digest. This increases the length from 40 to 43 

1069 characters. 

1070 

1071 .. versionchanged:: 2.0 

1072 Use SHA-1. MD5 is not allowed in FIPS-enabled systems. This increases 

1073 the length from 32 to 40 characters. 

1074 """ 

1075 digest = hashlib.sha3_256(data, usedforsecurity=False).digest() 

1076 return b64encode(digest).decode().rstrip("=") 

1077 

1078 

1079def parse_date(value: str | None) -> datetime | None: 

1080 """Parse an :rfc:`2822` date into a timezone-aware 

1081 :class:`datetime.datetime` object, or ``None`` if parsing fails. 

1082 

1083 This is a wrapper for :func:`email.utils.parsedate_to_datetime`. It 

1084 returns ``None`` if parsing fails instead of raising an exception, 

1085 and always returns a timezone-aware datetime object. If the string 

1086 doesn't have timezone information, it is assumed to be UTC. 

1087 

1088 :param value: A string with a supported date format. 

1089 

1090 .. versionchanged:: 2.0 

1091 Return a timezone-aware datetime object. Use 

1092 ``email.utils.parsedate_to_datetime``. 

1093 """ 

1094 if value is None: 

1095 return None 

1096 

1097 try: 

1098 dt = email.utils.parsedate_to_datetime(value) 

1099 except (TypeError, ValueError): 

1100 return None 

1101 

1102 if dt.tzinfo is None: 

1103 return dt.replace(tzinfo=timezone.utc) 

1104 

1105 return dt 

1106 

1107 

1108def http_date( 

1109 timestamp: datetime | date | int | float | struct_time | None = None, 

1110) -> str: 

1111 """Format a datetime object or timestamp into an :rfc:`2822` date 

1112 string. 

1113 

1114 This is a wrapper for :func:`email.utils.format_datetime`. It 

1115 assumes naive datetime objects are in UTC instead of raising an 

1116 exception. 

1117 

1118 :param timestamp: The datetime or timestamp to format. Defaults to 

1119 the current time. 

1120 

1121 .. versionchanged:: 2.0 

1122 Use ``email.utils.format_datetime``. Accept ``date`` objects. 

1123 """ 

1124 if isinstance(timestamp, date): 

1125 if not isinstance(timestamp, datetime): 

1126 # Assume plain date is midnight UTC. 

1127 timestamp = datetime.combine(timestamp, time(), tzinfo=timezone.utc) 

1128 else: 

1129 # Ensure datetime is timezone-aware. 

1130 timestamp = _dt_as_utc(timestamp) 

1131 

1132 return email.utils.format_datetime(timestamp, usegmt=True) 

1133 

1134 if isinstance(timestamp, struct_time): 

1135 timestamp = mktime(timestamp) 

1136 

1137 return email.utils.formatdate(timestamp, usegmt=True) 

1138 

1139 

1140def parse_age(value: str | None = None) -> timedelta | None: 

1141 """Parses a base-10 integer count of seconds into a timedelta. 

1142 

1143 If parsing fails, the return value is `None`. 

1144 

1145 :param value: a string consisting of an integer represented in base-10 

1146 :return: a :class:`datetime.timedelta` object or `None`. 

1147 """ 

1148 if not value: 

1149 return None 

1150 try: 

1151 seconds = _plain_int(value) 

1152 except ValueError: 

1153 return None 

1154 if seconds < 0: 

1155 return None 

1156 try: 

1157 return timedelta(seconds=seconds) 

1158 except OverflowError: 

1159 return None 

1160 

1161 

1162def dump_age(age: timedelta | int | None = None) -> str | None: 

1163 """Formats the duration as a base-10 integer. 

1164 

1165 :param age: should be an integer number of seconds, 

1166 a :class:`datetime.timedelta` object, or, 

1167 if the age is unknown, `None` (default). 

1168 """ 

1169 if age is None: 

1170 return None 

1171 

1172 if isinstance(age, timedelta): 

1173 age = int(age.total_seconds()) 

1174 

1175 if age < 0: 

1176 raise ValueError("age cannot be negative") 

1177 

1178 return str(age) 

1179 

1180 

1181def is_resource_modified( 

1182 environ: WSGIEnvironment, 

1183 etag: str | None = None, 

1184 data: bytes | None = None, 

1185 last_modified: datetime | str | None = None, 

1186 ignore_if_range: bool = True, 

1187) -> bool: 

1188 """Convenience method for conditional requests. 

1189 

1190 :param environ: the WSGI environment of the request to be checked. 

1191 :param etag: the etag for the response for comparison. 

1192 :param data: or alternatively the data of the response to automatically 

1193 generate an etag using :func:`generate_etag`. 

1194 :param last_modified: an optional date of the last modification. 

1195 :param ignore_if_range: If `False`, `If-Range` header will be taken into 

1196 account. 

1197 :return: `True` if the resource was modified, otherwise `False`. 

1198 

1199 .. versionchanged:: 1.0 

1200 The check is run for methods other than ``GET`` and ``HEAD``. 

1201 """ 

1202 return _sansio_http.is_resource_modified( 

1203 http_range=environ.get("HTTP_RANGE"), 

1204 http_if_range=environ.get("HTTP_IF_RANGE"), 

1205 http_if_modified_since=environ.get("HTTP_IF_MODIFIED_SINCE"), 

1206 http_if_none_match=environ.get("HTTP_IF_NONE_MATCH"), 

1207 http_if_match=environ.get("HTTP_IF_MATCH"), 

1208 etag=etag, 

1209 data=data, 

1210 last_modified=last_modified, 

1211 ignore_if_range=ignore_if_range, 

1212 ) 

1213 

1214 

1215def remove_entity_headers( 

1216 headers: ds.Headers | list[tuple[str, str]], 

1217 allowed: t.Iterable[str] = ("expires", "content-location"), 

1218) -> None: 

1219 """Remove all entity headers from a list or :class:`Headers` object. This 

1220 operation works in-place. `Expires` and `Content-Location` headers are 

1221 by default not removed. The reason for this is :rfc:`2616` section 

1222 10.3.5 which specifies some entity headers that should be sent. 

1223 

1224 .. versionchanged:: 0.5 

1225 added `allowed` parameter. 

1226 

1227 :param headers: a list or :class:`Headers` object. 

1228 :param allowed: a list of headers that should still be allowed even though 

1229 they are entity headers. 

1230 """ 

1231 allowed = {x.lower() for x in allowed} 

1232 headers[:] = [ 

1233 (key, value) 

1234 for key, value in headers 

1235 if not is_entity_header(key) or key.lower() in allowed 

1236 ] 

1237 

1238 

1239def remove_hop_by_hop_headers(headers: ds.Headers | list[tuple[str, str]]) -> None: 

1240 """Remove all HTTP/1.1 "Hop-by-Hop" headers from a list or 

1241 :class:`Headers` object. This operation works in-place. 

1242 

1243 .. versionadded:: 0.5 

1244 

1245 :param headers: a list or :class:`Headers` object. 

1246 """ 

1247 headers[:] = [ 

1248 (key, value) for key, value in headers if not is_hop_by_hop_header(key) 

1249 ] 

1250 

1251 

1252def is_entity_header(header: str) -> bool: 

1253 """Check if a header is an entity header. 

1254 

1255 .. versionadded:: 0.5 

1256 

1257 :param header: the header to test. 

1258 :return: `True` if it's an entity header, `False` otherwise. 

1259 """ 

1260 return header.lower() in _entity_headers 

1261 

1262 

1263def is_hop_by_hop_header(header: str) -> bool: 

1264 """Check if a header is an HTTP/1.1 "Hop-by-Hop" header. 

1265 

1266 .. versionadded:: 0.5 

1267 

1268 :param header: the header to test. 

1269 :return: `True` if it's an HTTP/1.1 "Hop-by-Hop" header, `False` otherwise. 

1270 """ 

1271 return header.lower() in _hop_by_hop_headers 

1272 

1273 

1274def parse_cookie( 

1275 header: WSGIEnvironment | str | None, **kwargs: t.Any 

1276) -> ds.ImmutableMultiDict[str, str]: 

1277 """Parse cookies from a ``Cookie`` header as an :class:`.ImmutableMultiDict`. 

1278 

1279 :param header: The ``Cookie`` header, or a WSGI environ with an 

1280 ``HTTP_COOKIE`` key. 

1281 

1282 .. versionchanged:: 3.2 

1283 The ``cls`` parameter is deprecated and will be removed in Werkzeug 3.3. 

1284 It will always be ``ImmutableMultiDict``. 

1285 

1286 .. versionchanged:: 3.0 

1287 Passing bytes, and the ``charset`` and ``errors`` parameters, were removed. 

1288 

1289 .. versionchanged:: 1.0 

1290 Returns a :class:`MultiDict` instead of a ``TypeConversionDict``. 

1291 

1292 .. versionchanged:: 0.5 

1293 Returns a :class:`TypeConversionDict` instead of a regular dict. The ``cls`` 

1294 parameter was added. 

1295 """ 

1296 if isinstance(header, dict): 

1297 cookie = header.get("HTTP_COOKIE") 

1298 else: 

1299 cookie = header 

1300 

1301 if cookie: 

1302 cookie = cookie.encode("latin1").decode() 

1303 

1304 parse_kwargs: dict[str, t.Any] = {} 

1305 

1306 if "cls" in kwargs: 

1307 import warnings 

1308 

1309 warnings.warn( 

1310 "The 'cls' parameter is deprecated and will be removed in Werkzeug" 

1311 " 3.3. It will always be 'ImmutableMultiDict'.", 

1312 DeprecationWarning, 

1313 stacklevel=2, 

1314 ) 

1315 parse_kwargs["cls"] = kwargs["cls"] 

1316 

1317 return _sansio_http.parse_cookie(cookie=cookie, **kwargs) 

1318 

1319 

1320_cookie_no_quote_re = re.compile(r"[\w!#$%&'()*+\-./:<=>?@\[\]^`{|}~]*", re.A) 

1321_cookie_slash_re = re.compile(rb"[\x00-\x19\",;\\\x7f-\xff]", re.A) 

1322_cookie_slash_map = {b'"': b'\\"', b"\\": b"\\\\"} 

1323_cookie_slash_map.update( 

1324 (v.to_bytes(1, "big"), b"\\%03o" % v) 

1325 for v in [*range(0x20), *b",;", *range(0x7F, 256)] 

1326) 

1327 

1328 

1329def dump_cookie( 

1330 key: str, 

1331 value: str = "", 

1332 max_age: timedelta | int | None = None, 

1333 expires: str | datetime | int | float | None = None, 

1334 path: str | None = "/", 

1335 domain: str | None = None, 

1336 secure: bool = False, 

1337 httponly: bool = False, 

1338 sync_expires: bool = True, 

1339 max_size: int = 4093, 

1340 samesite: str | None = None, 

1341 partitioned: bool = False, 

1342) -> str: 

1343 """Create a Set-Cookie header without the ``Set-Cookie`` prefix. 

1344 

1345 The return value is usually restricted to ascii as the vast majority 

1346 of values are properly escaped, but that is no guarantee. It's 

1347 tunneled through latin1 as required by :pep:`3333`. 

1348 

1349 The return value is not ASCII safe if the key contains unicode 

1350 characters. This is technically against the specification but 

1351 happens in the wild. It's strongly recommended to not use 

1352 non-ASCII values for the keys. 

1353 

1354 :param max_age: should be a number of seconds, or `None` (default) if 

1355 the cookie should last only as long as the client's 

1356 browser session. Additionally `timedelta` objects 

1357 are accepted, too. 

1358 :param expires: should be a `datetime` object or unix timestamp. 

1359 :param path: limits the cookie to a given path, per default it will 

1360 span the whole domain. 

1361 :param domain: Use this if you want to set a cross-domain cookie. For 

1362 example, ``domain="example.com"`` will set a cookie 

1363 that is readable by the domain ``www.example.com``, 

1364 ``foo.example.com`` etc. Otherwise, a cookie will only 

1365 be readable by the domain that set it. 

1366 :param secure: The cookie will only be available via HTTPS 

1367 :param httponly: disallow JavaScript to access the cookie. This is an 

1368 extension to the cookie standard and probably not 

1369 supported by all browsers. 

1370 :param charset: the encoding for string values. 

1371 :param sync_expires: automatically set expires if max_age is defined 

1372 but expires not. 

1373 :param max_size: Warn if the final header value exceeds this size. The 

1374 default, 4093, should be safely `supported by most browsers 

1375 <cookie_>`_. Set to 0 to disable this check. 

1376 :param samesite: Limits the scope of the cookie such that it will 

1377 only be attached to requests if those requests are same-site. 

1378 :param partitioned: Opts the cookie into partitioned storage. This 

1379 will also set secure to True 

1380 

1381 .. _`cookie`: http://browsercookielimits.squawky.net/ 

1382 

1383 .. versionchanged:: 3.1 

1384 The ``partitioned`` parameter was added. 

1385 

1386 .. versionchanged:: 3.0 

1387 Passing bytes, and the ``charset`` parameter, were removed. 

1388 

1389 .. versionchanged:: 2.3.3 

1390 The ``path`` parameter is ``/`` by default. 

1391 

1392 .. versionchanged:: 2.3.1 

1393 The value allows more characters without quoting. 

1394 

1395 .. versionchanged:: 2.3 

1396 ``localhost`` and other names without a dot are allowed for the domain. A 

1397 leading dot is ignored. 

1398 

1399 .. versionchanged:: 2.3 

1400 The ``path`` parameter is ``None`` by default. 

1401 

1402 .. versionchanged:: 1.0.0 

1403 The string ``'None'`` is accepted for ``samesite``. 

1404 """ 

1405 if path is not None: 

1406 # safe = https://url.spec.whatwg.org/#url-path-segment-string 

1407 # as well as percent for things that are already quoted 

1408 # excluding semicolon since it's part of the header syntax 

1409 path = quote(path, safe="%!$&'()*+,/:=@") 

1410 

1411 if domain: 

1412 domain = domain.partition(":")[0].lstrip(".").encode("idna").decode("ascii") 

1413 

1414 if isinstance(max_age, timedelta): 

1415 max_age = int(max_age.total_seconds()) 

1416 

1417 if expires is not None: 

1418 if not isinstance(expires, str): 

1419 expires = http_date(expires) 

1420 elif max_age is not None and sync_expires: 

1421 expires = http_date(datetime.now(tz=timezone.utc).timestamp() + max_age) 

1422 

1423 if samesite is not None: 

1424 samesite = samesite.title() 

1425 

1426 if samesite not in {"Strict", "Lax", "None"}: 

1427 raise ValueError("SameSite must be 'Strict', 'Lax', or 'None'.") 

1428 

1429 if partitioned: 

1430 secure = True 

1431 

1432 # Quote value if it contains characters not allowed by RFC 6265. Slash-escape with 

1433 # three octal digits, which matches http.cookies, although the RFC suggests base64. 

1434 if not _cookie_no_quote_re.fullmatch(value): 

1435 # Work with bytes here, since a UTF-8 character could be multiple bytes. 

1436 value = _cookie_slash_re.sub( 

1437 lambda m: _cookie_slash_map[m.group()], value.encode() 

1438 ).decode("ascii") 

1439 value = f'"{value}"' 

1440 

1441 # Send a non-ASCII key as mojibake. Everything else should already be ASCII. 

1442 # TODO Remove encoding dance, it seems like clients accept UTF-8 keys 

1443 buf = [f"{key.encode().decode('latin1')}={value}"] 

1444 

1445 for k, v in ( 

1446 ("Domain", domain), 

1447 ("Expires", expires), 

1448 ("Max-Age", max_age), 

1449 ("Secure", secure), 

1450 ("HttpOnly", httponly), 

1451 ("Path", path), 

1452 ("SameSite", samesite), 

1453 ("Partitioned", partitioned), 

1454 ): 

1455 if v is None or v is False: 

1456 continue 

1457 

1458 if v is True: 

1459 buf.append(k) 

1460 continue 

1461 

1462 buf.append(f"{k}={v}") 

1463 

1464 rv = "; ".join(buf) 

1465 

1466 # Warn if the final value of the cookie is larger than the limit. If the cookie is 

1467 # too large, then it may be silently ignored by the browser, which can be quite hard 

1468 # to debug. 

1469 cookie_size = len(rv) 

1470 

1471 if max_size and cookie_size > max_size: 

1472 value_size = len(value) 

1473 warnings.warn( 

1474 f"The '{key}' cookie is too large: the value was {value_size} bytes but the" 

1475 f" header required {cookie_size - value_size} extra bytes. The final size" 

1476 f" was {cookie_size} bytes but the limit is {max_size} bytes. Browsers may" 

1477 " silently ignore cookies larger than this.", 

1478 stacklevel=2, 

1479 ) 

1480 

1481 return rv 

1482 

1483 

1484def is_byte_range_valid( 

1485 start: int | None, stop: int | None, length: int | None 

1486) -> bool: 

1487 """Checks if a given byte content range is valid for the given length. 

1488 

1489 .. versionadded:: 0.7 

1490 """ 

1491 if (start is None) != (stop is None): 

1492 return False 

1493 elif start is None: 

1494 return length is None or length >= 0 

1495 elif length is None: 

1496 return 0 <= start < stop # type: ignore 

1497 elif start >= stop: # type: ignore 

1498 return False 

1499 return 0 <= start < length 

1500 

1501 

1502# circular dependencies 

1503from . import datastructures as ds # noqa: E402 

1504from .sansio import http as _sansio_http # noqa: E402 

1505 

1506if not t.TYPE_CHECKING: 

1507 

1508 def __getattr__(name: str) -> t.Any: 

1509 if name == "HTTP_STATUS_CODES": 

1510 import warnings 

1511 

1512 warnings.warn( 

1513 "The 'HTTP_STATUS_CODES' data is deprecated and will be removed in" 

1514 " Werkzeug 3.3. Use Python's built-in 'http.HTTPStatus' instead.", 

1515 DeprecationWarning, 

1516 stacklevel=2, 

1517 ) 

1518 return _HTTP_STATUS_CODES 

1519 

1520 alts = { 

1521 "dump_csp_header": "ContentSecurityPolicy.to_header", 

1522 "parse_accept_header": "Accept.from_header", 

1523 "parse_cache_control_header": "CacheControl.from_header", 

1524 "parse_content_range_header": "ContentRange.from_header", 

1525 "parse_csp_header": "ContentSecurityPolicy.from_header", 

1526 "parse_etags": "ETags.from_header", 

1527 "parse_if_range_header": "IfRange.from_header", 

1528 "parse_range_header": "Range.from_header", 

1529 "parse_set_header": "HeaderSet.from_header", 

1530 } 

1531 

1532 if name in alts: 

1533 import warnings 

1534 

1535 warnings.warn( 

1536 f"The '{name}' function is deprecated and will be removed in" 

1537 f" Werkzeug 3.3. Use the '{alts[name]}' method instead.", 

1538 DeprecationWarning, 

1539 stacklevel=2, 

1540 ) 

1541 return globals()[f"_{name}"] 

1542 

1543 raise AttributeError(name)