Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/werkzeug/sansio/response.py: 42%

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

278 statements  

1from __future__ import annotations 

2 

3import typing as t 

4from datetime import datetime 

5from datetime import timedelta 

6from datetime import timezone 

7from http import HTTPStatus 

8 

9from ..datastructures import CallbackDict 

10from ..datastructures import ContentRange 

11from ..datastructures import ContentSecurityPolicy 

12from ..datastructures import Headers 

13from ..datastructures import HeaderSet 

14from ..datastructures import ResponseCacheControl 

15from ..datastructures import WWWAuthenticate 

16from ..datastructures.cache_control import _CacheControl 

17from ..http import COEP 

18from ..http import COOP 

19from ..http import CORP 

20from ..http import dump_age 

21from ..http import dump_cookie 

22from ..http import dump_header 

23from ..http import dump_options_header 

24from ..http import http_date 

25from ..http import parse_age 

26from ..http import parse_date 

27from ..http import parse_options_header 

28from ..http import quote_etag 

29from ..http import unquote_etag 

30from ..utils import get_content_type 

31from ..utils import header_property 

32 

33 

34def _set_property(name: str, doc: str | None = None) -> property: 

35 def fget(self: Response) -> HeaderSet: 

36 def on_update(header_set: HeaderSet) -> None: 

37 if not header_set and name in self.headers: 

38 del self.headers[name] 

39 elif header_set: 

40 self.headers[name] = header_set.to_header() 

41 

42 obj = HeaderSet.from_header(self.headers.get(name)) 

43 obj._on_update = on_update 

44 return obj 

45 

46 def fset( 

47 self: Response, 

48 value: None | (str | dict[str, str | int] | t.Iterable[str]), 

49 ) -> None: 

50 if not value: 

51 del self.headers[name] 

52 elif isinstance(value, str): 

53 self.headers[name] = value 

54 else: 

55 self.headers[name] = dump_header(value) 

56 

57 return property(fget, fset, doc=doc) 

58 

59 

60class Response: 

61 """Represents the non-IO parts of an HTTP response, specifically the 

62 status and headers but not the body. 

63 

64 This class is not meant for general use. It should only be used when 

65 implementing WSGI, ASGI, or another HTTP application spec. Werkzeug 

66 provides a WSGI implementation at :cls:`werkzeug.wrappers.Response`. 

67 

68 :param status: The status code for the response. Either an int, in 

69 which case the default status message is added, or a string in 

70 the form ``{code} {message}``, like ``404 Not Found``. Defaults 

71 to 200. 

72 :param headers: A :class:`~werkzeug.datastructures.Headers` object, 

73 or a list of ``(key, value)`` tuples that will be converted to a 

74 ``Headers`` object. 

75 :param mimetype: The mime type (content type without charset or 

76 other parameters) of the response. If the value starts with 

77 ``text/`` (or matches some other special cases), the charset 

78 will be added to create the ``content_type``. 

79 :param content_type: The full content type of the response. 

80 Overrides building the value from ``mimetype``. 

81 

82 .. versionchanged:: 3.0 

83 The ``charset`` attribute was removed. 

84 

85 .. versionadded:: 2.0 

86 """ 

87 

88 #: the default status if none is provided. 

89 default_status = 200 

90 

91 #: the default mimetype if none is provided. 

92 default_mimetype: str | None = "text/plain" 

93 

94 #: Warn if a cookie header exceeds this size. The default, 4093, should be 

95 #: safely `supported by most browsers <cookie_>`_. A cookie larger than 

96 #: this size will still be sent, but it may be ignored or handled 

97 #: incorrectly by some browsers. Set to 0 to disable this check. 

98 #: 

99 #: .. versionadded:: 0.13 

100 #: 

101 #: .. _`cookie`: http://browsercookielimits.squawky.net/ 

102 max_cookie_size = 4093 

103 

104 # A :class:`Headers` object representing the response headers. 

105 headers: Headers 

106 

107 def __init__( 

108 self, 

109 status: int | str | HTTPStatus | None = None, 

110 headers: t.Mapping[str, str | t.Iterable[str]] 

111 | t.Iterable[tuple[str, str]] 

112 | None = None, 

113 mimetype: str | None = None, 

114 content_type: str | None = None, 

115 ) -> None: 

116 if isinstance(headers, Headers): 

117 self.headers = headers 

118 elif not headers: 

119 self.headers = Headers() 

120 else: 

121 self.headers = Headers(headers) 

122 

123 if content_type is None: 

124 if mimetype is None and "Content-Type" not in self.headers: 

125 mimetype = self.default_mimetype 

126 if mimetype is not None: 

127 mimetype = get_content_type(mimetype, "utf-8") 

128 content_type = mimetype 

129 if content_type is not None: 

130 self.headers["Content-Type"] = content_type 

131 if status is None: 

132 status = self.default_status 

133 self.status = status 

134 

135 def __repr__(self) -> str: 

136 return f"<{type(self).__name__} [{self.status}]>" 

137 

138 @property 

139 def status_code(self) -> int: 

140 """The HTTP status code as a number.""" 

141 return self._status_code 

142 

143 @status_code.setter 

144 def status_code(self, code: int) -> None: 

145 self.status = code 

146 

147 @property 

148 def status(self) -> str: 

149 """The HTTP status code as a string.""" 

150 return self._status 

151 

152 @status.setter 

153 def status(self, value: str | int | HTTPStatus) -> None: 

154 self._status, self._status_code = self._clean_status(value) 

155 

156 def _clean_status(self, value: str | int | HTTPStatus) -> tuple[str, int]: 

157 if isinstance(value, (int, HTTPStatus)): 

158 status_code = int(value) 

159 else: 

160 value = value.strip() 

161 

162 if not value: 

163 raise ValueError("Empty status argument") 

164 

165 code_str, sep, _ = value.partition(" ") 

166 

167 try: 

168 status_code = int(code_str) 

169 except ValueError: 

170 # only message 

171 return f"0 {value}", 0 

172 

173 if sep: 

174 # code and message 

175 return value, status_code 

176 

177 # only code, look up message 

178 try: 

179 status = f"{status_code} {HTTPStatus(status_code).phrase}" 

180 except ValueError: 

181 status = f"{status_code} Unknown" 

182 

183 return status, status_code 

184 

185 def set_cookie( 

186 self, 

187 key: str, 

188 value: str = "", 

189 max_age: timedelta | int | None = None, 

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

191 path: str | None = "/", 

192 domain: str | None = None, 

193 secure: bool = False, 

194 httponly: bool = False, 

195 samesite: str | None = None, 

196 partitioned: bool = False, 

197 ) -> None: 

198 """Sets a cookie. 

199 

200 A warning is raised if the size of the cookie header exceeds 

201 :attr:`max_cookie_size`, but the header will still be set. 

202 

203 :param key: the key (name) of the cookie to be set. 

204 :param value: the value of the cookie. 

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

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

207 browser session. 

208 :param expires: should be a `datetime` object or UNIX timestamp. 

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

210 span the whole domain. 

211 :param domain: if you want to set a cross-domain cookie. For example, 

212 ``domain="example.com"`` will set a cookie that is 

213 readable by the domain ``www.example.com``, 

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

215 be readable by the domain that set it. 

216 :param secure: If ``True``, the cookie will only be available 

217 via HTTPS. 

218 :param httponly: Disallow JavaScript access to the cookie. 

219 :param samesite: Limit the scope of the cookie to only be 

220 attached to requests that are "same-site". 

221 :param partitioned: If ``True``, the cookie will be partitioned. 

222 

223 .. versionchanged:: 3.1 

224 The ``partitioned`` parameter was added. 

225 """ 

226 self.headers.add( 

227 "Set-Cookie", 

228 dump_cookie( 

229 key, 

230 value=value, 

231 max_age=max_age, 

232 expires=expires, 

233 path=path, 

234 domain=domain, 

235 secure=secure, 

236 httponly=httponly, 

237 max_size=self.max_cookie_size, 

238 samesite=samesite, 

239 partitioned=partitioned, 

240 ), 

241 ) 

242 

243 def delete_cookie( 

244 self, 

245 key: str, 

246 path: str | None = "/", 

247 domain: str | None = None, 

248 secure: bool = False, 

249 httponly: bool = False, 

250 samesite: str | None = None, 

251 partitioned: bool = False, 

252 ) -> None: 

253 """Delete a cookie. Fails silently if key doesn't exist. 

254 

255 :param key: the key (name) of the cookie to be deleted. 

256 :param path: if the cookie that should be deleted was limited to a 

257 path, the path has to be defined here. 

258 :param domain: if the cookie that should be deleted was limited to a 

259 domain, that domain has to be defined here. 

260 :param secure: If ``True``, the cookie will only be available 

261 via HTTPS. 

262 :param httponly: Disallow JavaScript access to the cookie. 

263 :param samesite: Limit the scope of the cookie to only be 

264 attached to requests that are "same-site". 

265 :param partitioned: If ``True``, the cookie will be partitioned. 

266 """ 

267 self.set_cookie( 

268 key, 

269 expires=0, 

270 max_age=0, 

271 path=path, 

272 domain=domain, 

273 secure=secure, 

274 httponly=httponly, 

275 samesite=samesite, 

276 partitioned=partitioned, 

277 ) 

278 

279 @property 

280 def is_json(self) -> bool: 

281 """Check if the mimetype indicates JSON data, either 

282 :mimetype:`application/json` or :mimetype:`application/*+json`. 

283 """ 

284 mt = self.mimetype 

285 return mt is not None and ( 

286 mt == "application/json" 

287 or mt.startswith("application/") 

288 and mt.endswith("+json") 

289 ) 

290 

291 # Common Descriptors 

292 

293 @property 

294 def mimetype(self) -> str | None: 

295 """The mimetype (content type without charset etc.)""" 

296 ct = self.headers.get("Content-Type") 

297 

298 if ct: 

299 return ct.split(";")[0].strip() 

300 else: 

301 return None 

302 

303 @mimetype.setter 

304 def mimetype(self, value: str) -> None: 

305 self.headers["Content-Type"] = get_content_type(value, "utf-8") 

306 

307 @property 

308 def mimetype_params(self) -> dict[str, str]: 

309 """The mimetype parameters as dict. For example if the 

310 content type is ``text/html; charset=utf-8`` the params would be 

311 ``{'charset': 'utf-8'}``. 

312 

313 .. versionadded:: 0.5 

314 """ 

315 

316 def on_update(d: CallbackDict[str, str]) -> None: 

317 self.headers["Content-Type"] = dump_options_header(self.mimetype, d) 

318 

319 d = parse_options_header(self.headers.get("Content-Type", ""))[1] 

320 return CallbackDict(d, on_update) 

321 

322 location = header_property[str]( 

323 "Location", 

324 doc="""The Location response-header field is used to redirect 

325 the recipient to a location other than the Request-URI for 

326 completion of the request or identification of a new 

327 resource.""", 

328 ) 

329 age = header_property( 

330 "Age", 

331 None, 

332 parse_age, 

333 dump_age, # type: ignore 

334 doc="""The Age response-header field conveys the sender's 

335 estimate of the amount of time since the response (or its 

336 revalidation) was generated at the origin server. 

337 

338 Age values are non-negative decimal integers, representing time 

339 in seconds.""", 

340 ) 

341 content_type = header_property[str]( 

342 "Content-Type", 

343 doc="""The Content-Type entity-header field indicates the media 

344 type of the entity-body sent to the recipient or, in the case of 

345 the HEAD method, the media type that would have been sent had 

346 the request been a GET.""", 

347 ) 

348 content_length = header_property( 

349 "Content-Length", 

350 None, 

351 int, 

352 str, 

353 doc="""The Content-Length entity-header field indicates the size 

354 of the entity-body, in decimal number of OCTETs, sent to the 

355 recipient or, in the case of the HEAD method, the size of the 

356 entity-body that would have been sent had the request been a 

357 GET.""", 

358 ) 

359 content_location = header_property[str]( 

360 "Content-Location", 

361 doc="""The Content-Location entity-header field MAY be used to 

362 supply the resource location for the entity enclosed in the 

363 message when that entity is accessible from a location separate 

364 from the requested resource's URI.""", 

365 ) 

366 content_encoding = header_property[str]( 

367 "Content-Encoding", 

368 doc="""The Content-Encoding entity-header field is used as a 

369 modifier to the media-type. When present, its value indicates 

370 what additional content codings have been applied to the 

371 entity-body, and thus what decoding mechanisms must be applied 

372 in order to obtain the media-type referenced by the Content-Type 

373 header field.""", 

374 ) 

375 

376 @property 

377 def content_md5(self) -> str | None: 

378 """The ``Content-MD5`` header, an MD5 digest of the response body. 

379 

380 .. deprecated:: 3.2 

381 The header has not been used for a long time. Will be removed 

382 in Werkzeug 3.3. 

383 """ 

384 import warnings 

385 

386 warnings.warn( 

387 "The 'content_md5' attribute is deprecated and will be removed in" 

388 " Werkzeug 3.3. The header has not been used for a long time.", 

389 DeprecationWarning, 

390 stacklevel=2, 

391 ) 

392 return self.headers.get("Content-MD5") 

393 

394 @content_md5.setter 

395 def content_md5(self, value: str | None) -> None: 

396 import warnings 

397 

398 warnings.warn( 

399 "The 'content_md5' attribute is deprecated and will be removed in" 

400 " Werkzeug 3.3. The header has not been used for a long time.", 

401 DeprecationWarning, 

402 stacklevel=2, 

403 ) 

404 

405 if value is None: 

406 del self.headers["Content-MD5"] 

407 else: 

408 self.headers["Content-MD5"] = value 

409 

410 @content_md5.deleter 

411 def content_md5(self) -> None: 

412 import warnings 

413 

414 warnings.warn( 

415 "The 'content_md5' attribute is deprecated and will be removed in" 

416 " Werkzeug 3.3. The header has not been used for a long time.", 

417 DeprecationWarning, 

418 stacklevel=2, 

419 ) 

420 del self.headers["Content-MD5"] 

421 

422 date = header_property( 

423 "Date", 

424 None, 

425 parse_date, 

426 http_date, 

427 doc="""The Date general-header field represents the date and 

428 time at which the message was originated, having the same 

429 semantics as orig-date in RFC 822. 

430 

431 .. versionchanged:: 2.0 

432 The datetime object is timezone-aware. 

433 """, 

434 ) 

435 expires = header_property( 

436 "Expires", 

437 None, 

438 parse_date, 

439 http_date, 

440 doc="""The Expires entity-header field gives the date/time after 

441 which the response is considered stale. A stale cache entry may 

442 not normally be returned by a cache. 

443 

444 .. versionchanged:: 2.0 

445 The datetime object is timezone-aware. 

446 """, 

447 ) 

448 last_modified = header_property( 

449 "Last-Modified", 

450 None, 

451 parse_date, 

452 http_date, 

453 doc="""The Last-Modified entity-header field indicates the date 

454 and time at which the origin server believes the variant was 

455 last modified. 

456 

457 .. versionchanged:: 2.0 

458 The datetime object is timezone-aware. 

459 """, 

460 ) 

461 

462 @property 

463 def retry_after(self) -> datetime | None: 

464 """The Retry-After response-header field can be used with a 

465 503 (Service Unavailable) response to indicate how long the 

466 service is expected to be unavailable to the requesting client. 

467 

468 Time in seconds until expiration or date. 

469 

470 .. versionchanged:: 2.0 

471 The datetime object is timezone-aware. 

472 """ 

473 value = self.headers.get("Retry-After") 

474 if value is None: 

475 return None 

476 

477 try: 

478 seconds = int(value) 

479 except ValueError: 

480 return parse_date(value) 

481 

482 return datetime.now(timezone.utc) + timedelta(seconds=seconds) 

483 

484 @retry_after.setter 

485 def retry_after(self, value: datetime | int | str | None) -> None: 

486 if value is None: 

487 if "Retry-After" in self.headers: 

488 del self.headers["Retry-After"] 

489 return 

490 elif isinstance(value, datetime): 

491 value = http_date(value) 

492 else: 

493 value = str(value) 

494 self.headers["Retry-After"] = value 

495 

496 vary = _set_property( 

497 "Vary", 

498 doc="""The Vary field value indicates the set of request-header 

499 fields that fully determines, while the response is fresh, 

500 whether a cache is permitted to use the response to reply to a 

501 subsequent request without revalidation.""", 

502 ) 

503 content_language = _set_property( 

504 "Content-Language", 

505 doc="""The Content-Language entity-header field describes the 

506 natural language(s) of the intended audience for the enclosed 

507 entity. Note that this might not be equivalent to all the 

508 languages used within the entity-body.""", 

509 ) 

510 allow = _set_property( 

511 "Allow", 

512 doc="""The Allow entity-header field lists the set of methods 

513 supported by the resource identified by the Request-URI. The 

514 purpose of this field is strictly to inform the recipient of 

515 valid methods associated with the resource. An Allow header 

516 field MUST be present in a 405 (Method Not Allowed) 

517 response.""", 

518 ) 

519 

520 # ETag 

521 

522 @property 

523 def cache_control(self) -> ResponseCacheControl: 

524 """The Cache-Control general-header field is used to specify 

525 directives that MUST be obeyed by all caching mechanisms along the 

526 request/response chain. 

527 """ 

528 

529 def on_update(cache_control: _CacheControl) -> None: 

530 if not cache_control and "Cache-Control" in self.headers: 

531 del self.headers["Cache-Control"] 

532 elif cache_control: 

533 self.headers["Cache-Control"] = cache_control.to_header() 

534 

535 obj = ResponseCacheControl.from_header(self.headers.get("Cache-Control")) 

536 obj.on_update = on_update 

537 return obj 

538 

539 def set_etag(self, etag: str, weak: bool = False) -> None: 

540 """Set the etag, and override the old one if there was one.""" 

541 self.headers["ETag"] = quote_etag(etag, weak) 

542 

543 def get_etag(self) -> tuple[str, bool] | tuple[None, None]: 

544 """Return a tuple in the form ``(etag, is_weak)``. If there is no 

545 ETag the return value is ``(None, None)``. 

546 """ 

547 return unquote_etag(self.headers.get("ETag")) 

548 

549 accept_ranges = header_property[str]( 

550 "Accept-Ranges", 

551 doc="""The `Accept-Ranges` header. Even though the name would 

552 indicate that multiple values are supported, it must be one 

553 string token only. 

554 

555 The values ``'bytes'`` and ``'none'`` are common. 

556 

557 .. versionadded:: 0.7""", 

558 ) 

559 

560 @property 

561 def content_range(self) -> ContentRange: 

562 """The ``Content-Range`` header as a 

563 :class:`~werkzeug.datastructures.ContentRange` object. Available 

564 even if the header is not set. 

565 

566 .. versionadded:: 0.7 

567 """ 

568 

569 def on_update(rng: ContentRange) -> None: 

570 if not rng: 

571 del self.headers["Content-Range"] 

572 else: 

573 self.headers["Content-Range"] = rng.to_header() 

574 

575 obj = ContentRange.from_header(self.headers.get("Content-Range")) 

576 # always provide a content range object to make the descriptor 

577 # more user friendly. It provides an unset() method that can be 

578 # used to remove the header quickly. 

579 if obj is None: 

580 obj = ContentRange(None, None, None) 

581 

582 obj._on_update = on_update 

583 return obj 

584 

585 @content_range.setter 

586 def content_range(self, value: ContentRange | str | None) -> None: 

587 if not value: 

588 del self.headers["Content-Range"] 

589 elif isinstance(value, str): 

590 self.headers["Content-Range"] = value 

591 else: 

592 self.headers["Content-Range"] = value.to_header() 

593 

594 # Authorization 

595 

596 @property 

597 def www_authenticate(self) -> WWWAuthenticate: 

598 """The ``WWW-Authenticate`` header parsed into a :class:`.WWWAuthenticate` 

599 object. Modifying the object will modify the header value. 

600 

601 This header is not set by default. To set this header, assign an instance of 

602 :class:`.WWWAuthenticate` to this attribute. 

603 

604 .. code-block:: python 

605 

606 response.www_authenticate = WWWAuthenticate( 

607 "basic", {"realm": "Authentication Required"} 

608 ) 

609 

610 Multiple values for this header can be sent to give the client multiple options. 

611 Assign a list to set multiple headers. However, modifying the items in the list 

612 will not automatically update the header values, and accessing this attribute 

613 will only ever return the first value. 

614 

615 To unset this header, assign ``None`` or use ``del``. 

616 

617 .. versionchanged:: 2.3 

618 This attribute can be assigned to set the header. A list can be assigned 

619 to set multiple header values. Use ``del`` to unset the header. 

620 

621 .. versionchanged:: 2.3 

622 :class:`WWWAuthenticate` is no longer a ``dict``. The ``token`` attribute 

623 was added for auth challenges that use a token instead of parameters. 

624 """ 

625 value = WWWAuthenticate.from_header(self.headers.get("WWW-Authenticate")) 

626 

627 if value is None: 

628 value = WWWAuthenticate("basic") 

629 

630 def on_update(value: WWWAuthenticate) -> None: 

631 self.www_authenticate = value 

632 

633 value._on_update = on_update 

634 return value 

635 

636 @www_authenticate.setter 

637 def www_authenticate( 

638 self, value: WWWAuthenticate | list[WWWAuthenticate] | None 

639 ) -> None: 

640 if not value: # None or empty list 

641 del self.www_authenticate 

642 elif isinstance(value, list): 

643 # Clear any existing header by setting the first item. 

644 self.headers.set("WWW-Authenticate", value[0].to_header()) 

645 

646 for item in value[1:]: 

647 # Add additional header lines for additional items. 

648 self.headers.add("WWW-Authenticate", item.to_header()) 

649 else: 

650 self.headers.set("WWW-Authenticate", value.to_header()) 

651 

652 def on_update(value: WWWAuthenticate) -> None: 

653 self.www_authenticate = value 

654 

655 # When setting a single value, allow updating it directly. 

656 value._on_update = on_update 

657 

658 @www_authenticate.deleter 

659 def www_authenticate(self) -> None: 

660 if "WWW-Authenticate" in self.headers: 

661 del self.headers["WWW-Authenticate"] 

662 

663 # CSP 

664 

665 @property 

666 def content_security_policy(self) -> ContentSecurityPolicy: 

667 """The ``Content-Security-Policy`` header as a 

668 :class:`~werkzeug.datastructures.ContentSecurityPolicy` object. Available 

669 even if the header is not set. 

670 

671 The Content-Security-Policy header adds an additional layer of 

672 security to help detect and mitigate certain types of attacks. 

673 """ 

674 

675 def on_update(csp: ContentSecurityPolicy) -> None: 

676 if not csp: 

677 del self.headers["Content-Security-Policy"] 

678 else: 

679 self.headers["Content-Security-Policy"] = csp.to_header() 

680 

681 obj = ContentSecurityPolicy.from_header( 

682 self.headers.get("Content-Security-Policy") 

683 ) 

684 obj.on_update = on_update 

685 return obj 

686 

687 @content_security_policy.setter 

688 def content_security_policy( 

689 self, value: ContentSecurityPolicy | str | None 

690 ) -> None: 

691 if not value: 

692 del self.headers["Content-Security-Policy"] 

693 elif isinstance(value, str): 

694 self.headers["Content-Security-Policy"] = value 

695 else: 

696 self.headers["Content-Security-Policy"] = value.to_header() 

697 

698 @property 

699 def content_security_policy_report_only(self) -> ContentSecurityPolicy: 

700 """The ``Content-Security-Policy-Report-Only`` header as a 

701 :class:`~werkzeug.datastructures.ContentSecurityPolicy` object. Available 

702 even if the header is not set. 

703 

704 The Content-Security-Policy-Report-Only header adds a csp policy 

705 that is not enforced but is reported thereby helping detect 

706 certain types of attacks. 

707 """ 

708 

709 def on_update(csp: ContentSecurityPolicy) -> None: 

710 if not csp: 

711 del self.headers["Content-Security-Policy-Report-Only"] 

712 else: 

713 self.headers["Content-Security-Policy-Report-Only"] = csp.to_header() 

714 

715 obj = ContentSecurityPolicy.from_header( 

716 self.headers.get("Content-Security-Policy-Report-Only") 

717 ) 

718 obj.on_update = on_update 

719 return obj 

720 

721 @content_security_policy_report_only.setter 

722 def content_security_policy_report_only( 

723 self, value: ContentSecurityPolicy | str | None 

724 ) -> None: 

725 if not value: 

726 del self.headers["Content-Security-Policy-Report-Only"] 

727 elif isinstance(value, str): 

728 self.headers["Content-Security-Policy-Report-Only"] = value 

729 else: 

730 self.headers["Content-Security-Policy-Report-Only"] = value.to_header() 

731 

732 # CORS 

733 

734 @property 

735 def access_control_allow_credentials(self) -> bool: 

736 """Whether credentials can be shared by the browser to 

737 JavaScript code. As part of the preflight request it indicates 

738 whether credentials can be used on the cross origin request. 

739 """ 

740 return "Access-Control-Allow-Credentials" in self.headers 

741 

742 @access_control_allow_credentials.setter 

743 def access_control_allow_credentials(self, value: bool | None) -> None: 

744 if value is True: 

745 self.headers["Access-Control-Allow-Credentials"] = "true" 

746 else: 

747 self.headers.pop("Access-Control-Allow-Credentials", None) 

748 

749 access_control_allow_headers = header_property[HeaderSet]( 

750 "Access-Control-Allow-Headers", 

751 load_func=HeaderSet.from_header, 

752 dump_func=dump_header, 

753 doc="Which headers can be sent with the cross origin request.", 

754 ) 

755 

756 access_control_allow_methods = header_property[HeaderSet]( 

757 "Access-Control-Allow-Methods", 

758 load_func=HeaderSet.from_header, 

759 dump_func=dump_header, 

760 doc="Which methods can be used for the cross origin request.", 

761 ) 

762 

763 access_control_allow_origin = header_property[str]( 

764 "Access-Control-Allow-Origin", 

765 doc="The origin or '*' for any origin that may make cross origin requests.", 

766 ) 

767 

768 access_control_expose_headers = header_property[HeaderSet]( 

769 "Access-Control-Expose-Headers", 

770 load_func=HeaderSet.from_header, 

771 dump_func=dump_header, 

772 doc="Which headers can be shared by the browser to JavaScript code.", 

773 ) 

774 

775 access_control_max_age = header_property( 

776 "Access-Control-Max-Age", 

777 load_func=int, 

778 dump_func=str, 

779 doc="The maximum age in seconds the access control settings can be cached for.", 

780 ) 

781 

782 cross_origin_opener_policy = header_property[COOP]( 

783 "Cross-Origin-Opener-Policy", 

784 load_func=COOP, 

785 dump_func=lambda value: value.value, 

786 default=COOP.UNSAFE_NONE, 

787 doc="""Allows control over sharing of browsing context group with cross-origin 

788 documents. 

789 

790 Values are members of the :class:`.COOP` enum. 

791 

792 .. versionadded:: 2.0 

793 """, 

794 ) 

795 

796 cross_origin_embedder_policy = header_property[COEP]( 

797 "Cross-Origin-Embedder-Policy", 

798 load_func=COEP, 

799 dump_func=lambda value: value.value, 

800 default=COEP.UNSAFE_NONE, 

801 doc="""Prevents a document from loading any cross-origin resources that do not 

802 explicitly grant the document permission. 

803 

804 Values are members of the :class:`.COEP` enum. 

805 

806 .. versionadded:: 2.0 

807 """, 

808 ) 

809 

810 cross_origin_resource_policy = header_property[CORP]( 

811 "Cross-Origin-Resource-Policy", 

812 load_func=CORP, 

813 dump_func=lambda value: value.value, 

814 doc="""specifies the policy for what sites/origins should be allowed to load 

815 this resource. 

816 

817 Values are members of the :class:`.CORP` enum. 

818 

819 .. versionadded:: 3.2 

820 """, 

821 )