fresco 3.5.0__py3-none-any.whl → 3.6.0__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.

Potentially problematic release.


This version of fresco might be problematic. Click here for more details.

fresco/options.py CHANGED
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
  #
15
15
  import contextlib
16
+ import dataclasses
16
17
  import inspect
17
18
  import itertools
18
19
  import json
@@ -20,6 +21,7 @@ import logging
20
21
  import typing as t
21
22
  import os
22
23
  import re
24
+ from collections.abc import Mapping
23
25
  from decimal import Decimal
24
26
  from pathlib import Path
25
27
  from socket import gethostname
@@ -28,7 +30,6 @@ from typing import Callable
28
30
  from typing import Dict
29
31
  from typing import Iterable
30
32
  from typing import List
31
- from typing import Mapping
32
33
  from typing import Sequence
33
34
  from typing import Union
34
35
 
@@ -37,6 +38,8 @@ from fresco.exceptions import OptionsLoadedException
37
38
  __all__ = ["Options"]
38
39
 
39
40
  logger = logging.getLogger(__name__)
41
+ known_suffixes = {"py", "sh", "rc", "txt", "cfg", "ini", "json", "toml", "conf"}
42
+ priority_pattern = re.compile(r"([0-9]+)(?:-(.*))?$")
40
43
 
41
44
 
42
45
  class Options(dict):
@@ -98,7 +101,7 @@ class Options(dict):
98
101
  strict=True,
99
102
  dir=None,
100
103
  trigger_onload=True,
101
- ):
104
+ ) -> "Options":
102
105
  """
103
106
  Find all files matching glob pattern ``sources`` and populates the
104
107
  options object from those with matching filenames containing ``tags``.
@@ -121,20 +124,43 @@ class Options(dict):
121
124
  format. Any other files will be interpreted as simple lists of
122
125
  ```key=value`` pairs.
123
126
 
124
- Tags in filenames are delimited with periods,
125
- for example ".env.production.py".
127
+ Filename format
128
+ ---------------
129
+
130
+ The general format of filenames is:
131
+
132
+ .. code-block:: shell
133
+
134
+ <base>(.<priority number>-)(<tags>)(<suffix>)
135
+
136
+ Example filenames:
137
+
138
+ .. code-block:: shell
139
+
140
+ # Just <base>
141
+ .env
142
+
143
+ # <base>.<suffix>
144
+ .settings.toml
145
+
146
+ # <base>.<tags>.<suffix>
147
+ .env.dev.local..py
148
+
149
+ # <base>.<priority>-<tags>.<suffix>
150
+ .env.100-dev.py
151
+
152
+ Priority number, if specified, is used to determine loading order.
153
+ Lower numbers are loaded first. Priority numbers must be positive
154
+ integers.
155
+
156
+ Tags are delimited with periods,
157
+ for example ``.env.production.py``.
126
158
  The filename ``setttings.dev.local.ini`` would be
127
159
  considered to have the tags ``('dev', 'local')``
128
160
 
129
161
  Where filename contain multiple tags, all tags must match for the file
130
162
  to be loaded.
131
163
 
132
- Files are processed in the order that tags are specified in the
133
- ``tags`` parameter, and then in lexicographical order.
134
- For example, calling ``options.load(..., tags=["dev", "local"])`` would
135
- cause a file named "settings.dev" to be loaded before one named
136
- "settings.local".
137
-
138
164
  Tag names may contain the names of environment variable surrounded by
139
165
  braces, for example ``{USER}``. These will be substituted for the
140
166
  environment variable's value, with any dots or path separators replaced
@@ -145,8 +171,24 @@ class Options(dict):
145
171
 
146
172
  Files with the suffix ".sample" are unconditionally excluded.
147
173
 
148
- Files are loaded in the order specified by ``tags``, then in filename
149
- order. Environment variables, if requested, are loaded last.
174
+ Loading order
175
+ -------------
176
+
177
+ Files are loaded in the following order:
178
+
179
+ 1. On priority number, from low to high. If the priority number is not
180
+ given, a priority of zero is assumed
181
+
182
+ 2. Then in tag order, based on the ordering given in the
183
+ ``tags`` parameter
184
+
185
+ For example, calling ``options.load(..., tags=["dev", "local"])`` would
186
+ cause a file named "settings.dev" to be loaded before one named
187
+ "settings.local".
188
+
189
+ 3. Finally in lexicographical order.
190
+
191
+ Environment variables, if requested, are loaded last.
150
192
 
151
193
  Example::
152
194
 
@@ -154,19 +196,12 @@ class Options(dict):
154
196
  opts.load(".env*", ["dev", "host-{hostname}", "local"])
155
197
 
156
198
  Would load options from files named ``.env``, ``.env.json``, ``.env.dev.py``
157
- and ``.env.local.py.``
199
+ and ``.env.local.py``, in that order.
158
200
 
159
201
  """
160
202
  if self._is_loaded:
161
203
  raise OptionsLoadedException("Options have already been loaded")
162
204
 
163
- tag_substitutions = os.environ.copy()
164
- tag_substitutions["hostname"] = gethostname()
165
- tag_substitutions = {
166
- k: v.replace(".", "_").replace(os.pathsep, "_")
167
- for k, v in tag_substitutions.items()
168
- }
169
-
170
205
  candidates: List[Path] = []
171
206
  if dir is None:
172
207
  dir = Path(".")
@@ -183,52 +218,45 @@ class Options(dict):
183
218
  if p.suffix.lower() != ".sample"
184
219
  )
185
220
 
186
- if tags:
187
- subbed_tags = []
188
- for tag in tags:
189
- try:
190
- subbed_tags.append(tag.format(**tag_substitutions))
191
- except KeyError:
192
- pass
193
- tags = subbed_tags
194
- tagged_sources = []
195
- for p in candidates:
196
- candidate_tags = [t for t in str(p.name).split(".") if t][1:]
197
- if len(candidate_tags) == 0:
198
- tagged_sources.append((candidate_tags, p))
199
- else:
200
- # Ignore the final tag if it matches a common config file
201
- # extension
202
- if candidate_tags[-1].lower() in {
203
- "py",
204
- "sh",
205
- "rc",
206
- "txt",
207
- "cfg",
208
- "ini",
209
- "json",
210
- "toml",
211
- "conf",
212
- }:
213
- candidate_tags.pop()
214
-
215
- if all(t in tags for t in candidate_tags):
216
- tagged_sources.append((candidate_tags, p))
217
- matched = [
218
- ts[1]
219
- for ts in sorted(
220
- tagged_sources,
221
- key=(
222
- lambda ts: (
223
- ([], ts[1])
224
- if len(ts[0]) == 0 else
225
- (sorted(tags.index(t) for t in ts[0]), ts[1])
226
- )
227
- )
221
+ tag_substitutions = make_tag_substitutions()
222
+ subbed_tags: list[str] = []
223
+ for tag in tags:
224
+ try:
225
+ subbed_tags.append(tag.format(**tag_substitutions))
226
+ except KeyError:
227
+ pass
228
+ tags = subbed_tags
229
+ tagged_sources = []
230
+ for path in candidates:
231
+ priority = 0
232
+ filename = path.name
233
+ path_tags = []
234
+ path_tags = [t for t in str(filename).split(".") if t][1:]
235
+ if len(path_tags) > 0:
236
+ # Ignore the final tag if it matches a common config file
237
+ # extension
238
+ if path_tags[-1].lower() in known_suffixes:
239
+ path_tags.pop()
240
+
241
+ if path_tags:
242
+ if m := priority_pattern.match(path_tags[0]):
243
+ priority = int(m.group(1), 10)
244
+ if m.group(2):
245
+ path_tags[0] = m.group(2)
246
+ else:
247
+ path_tags = path_tags[1:]
248
+
249
+ if all(t in tags for t in path_tags):
250
+ tagged_sources.append(TaggedSource(priority, path_tags, path))
251
+ else:
252
+ excluded = [t for t in path_tags if t not in tags]
253
+ logger.debug(
254
+ f"Ignoring {path} as one or more tag does not match: {excluded=}"
228
255
  )
229
- ]
230
- else:
231
- matched = candidates
256
+
257
+ matched = [
258
+ ts.path for ts in sorted(tagged_sources, key=tagged_source_sort_key(tags))
259
+ ]
232
260
 
233
261
  for path in matched:
234
262
  existing_keys = set(self.keys())
@@ -236,7 +264,7 @@ class Options(dict):
236
264
  if path.suffix == ".py":
237
265
  self.update_from_file(str(path))
238
266
  elif path.suffix == ".toml":
239
- import toml
267
+ import toml # type: ignore
240
268
 
241
269
  with path.open("r") as f:
242
270
  self.update(toml.load(f))
@@ -264,9 +292,9 @@ class Options(dict):
264
292
  )
265
293
 
266
294
  if use_environ:
267
- for k in self:
268
- if k in os.environ:
269
- self[k] = parse_value(self, os.environ[k])
295
+ self |= {
296
+ k: parse_value(self, os.environ[k]) for k in self if k in os.environ
297
+ }
270
298
 
271
299
  if trigger_onload:
272
300
  self.do_loaded_callbacks()
@@ -282,9 +310,11 @@ class Options(dict):
282
310
  :param load_all: If true private symbols will also be loaded into the
283
311
  options object.
284
312
  """
285
- ns: Dict[str, Any] = {"__file__": path}
313
+ ns: Dict[str, Any] = {"__file__": path, "options": self}
286
314
  with open(path) as f:
287
315
  exec(f.read(), ns)
316
+ if ns.get("options") is self:
317
+ del ns["options"]
288
318
  self.update_from_dict(ns, load_all)
289
319
 
290
320
  def update_from_dict(self, d, load_all=False):
@@ -330,11 +360,8 @@ def override_options(options, other=None, **kwargs):
330
360
  """
331
361
  saved: list[tuple[str, t.Any]] = []
332
362
  items: t.Iterable[tuple[str, t.Any]] = []
333
- if other is not None:
334
-
335
- keys = getattr(other, "keys", None)
336
- if keys and callable(keys):
337
- items = ((k, other[k]) for k in keys())
363
+ if isinstance(other, Mapping):
364
+ items = ((k, other[k]) for k in other.keys())
338
365
 
339
366
  if kwargs:
340
367
  items = itertools.chain(items, kwargs.items())
@@ -459,3 +486,47 @@ def dict_from_options(
459
486
  if recursive:
460
487
  return recursive_split(d, prefix[-1])
461
488
  return d
489
+
490
+
491
+ @dataclasses.dataclass
492
+ class TaggedSource:
493
+ priority: int
494
+ tags: list[str]
495
+ path: Path
496
+
497
+
498
+ def tagged_source_sort_key(
499
+ tags: list[str],
500
+ ) -> Callable[[TaggedSource], tuple[int, list[int], str]]:
501
+ """
502
+ Return a function that can be used as the ``key`` argument of``sorted``,
503
+ that sorts TaggedSource objects based on
504
+ priority, tag order, then filename.
505
+ """
506
+
507
+ def _sort_key(ts: TaggedSource) -> tuple[int, list[int], str]:
508
+ sorted_tags = [tags.index(t) for t in ts.tags]
509
+ return (ts.priority, sorted_tags, str(ts.path))
510
+
511
+ return _sort_key
512
+
513
+
514
+ def make_tag_substitutions():
515
+ """
516
+ Return a dict of substitutions to make in tag names, based on the current
517
+ hostname and environment variables
518
+
519
+ This permits tags to be specified as, for example,
520
+ '{hostname}' or '{FRESCO_PROFILE}'
521
+ and the associated value will be substituted in.
522
+
523
+ Path separators and dots (which separate tags) are replaced in substitution
524
+ values by an underscore.
525
+ """
526
+ substitutions = os.environ.copy()
527
+ substitutions["hostname"] = gethostname()
528
+ substitutions = {
529
+ k: v.replace(".", "_").replace(os.pathsep, "_")
530
+ for k, v in substitutions.items()
531
+ }
532
+ return substitutions
fresco/request.py CHANGED
@@ -23,6 +23,7 @@ from urllib.parse import ParseResult
23
23
  from urllib.parse import quote
24
24
  from urllib.parse import urlparse
25
25
  from urllib.parse import urlunparse
26
+ from decimal import Decimal
26
27
  import typing as t
27
28
  import datetime
28
29
  import json
@@ -32,6 +33,7 @@ import re
32
33
  from fresco import exceptions
33
34
  from fresco.cookie import parse_cookie_header
34
35
  from fresco.multidict import MultiDict
36
+ from fresco.defaults import DEFAULT_CHARSET
35
37
  from fresco.types import QuerySpec
36
38
  from fresco.util.http import FileUpload
37
39
  from fresco.util.http import get_body_bytes
@@ -48,7 +50,15 @@ __all__ = "Request", "currentrequest"
48
50
  KB = 1024
49
51
  MB = 1024 * KB
50
52
 
51
- _marker = object()
53
+
54
+ class _Marker:
55
+ ...
56
+
57
+
58
+ _marker = _Marker()
59
+
60
+ T1 = t.TypeVar("T1")
61
+ T2 = t.TypeVar("T2")
52
62
 
53
63
 
54
64
  class Request(object):
@@ -86,7 +96,7 @@ class Request(object):
86
96
  environ: t.Dict
87
97
 
88
98
  #: Encoding used to decode WSGI parameters, notably PATH_INFO and form data
89
- default_charset = fresco.DEFAULT_CHARSET
99
+ default_charset = DEFAULT_CHARSET
90
100
 
91
101
  #: The decoder class to use for JSON request payloads
92
102
  json_decoder_class = json.JSONDecoder
@@ -196,7 +206,9 @@ class Request(object):
196
206
  by the client.
197
207
  """
198
208
  try:
199
- return self.json_decoder_class(*args, **kwargs).decode(self.body)
209
+ return self.json_decoder_class(*args, **kwargs).decode(
210
+ self.body # type: ignore
211
+ )
200
212
  except ValueError:
201
213
  raise exceptions.RequestParseError("Payload is not valid JSON")
202
214
 
@@ -235,35 +247,64 @@ class Request(object):
235
247
 
236
248
  return self._query
237
249
 
238
- def __getitem__(self, key, _marker=_marker):
250
+ def __getitem__(self, key: str) -> str:
239
251
  """
240
252
  Return the value of ``key`` from submitted form values.
241
253
  """
242
- v = self.get(key, _marker)
243
- if v is _marker:
254
+ v = self.get(key, default=None)
255
+ if v is None:
244
256
  raise KeyError(key)
245
257
  return v
246
258
 
247
- def get(self, key, default=_marker, type=None):
259
+ @t.overload
260
+ def get(
261
+ self, key: str, default: T1, type: t.Callable[[str], T2] = str
262
+ ) -> t.Union[T1, T2, None]:
263
+ ...
264
+
265
+ @t.overload
266
+ def get(self, key: str, *, type: t.Callable[[str], T2]) -> t.Union[T2, None]:
267
+ ...
268
+
269
+ @t.overload
270
+ def get(
271
+ self, key: str, *, type: t.Callable[[str], T2] = str, required: t.Literal[True]
272
+ ) -> T2:
273
+ ...
274
+
275
+ def get(
276
+ self,
277
+ key: str,
278
+ default=_marker,
279
+ type: t.Callable[[str], T2] = str,
280
+ required: bool = False,
281
+ ):
248
282
  """
249
283
  Look up ``key`` in submitted form values.
250
284
 
251
- :param type: The type to which the returned result should be converted
252
- :param default: The value produced if the argument is missing
285
+ :param type:
286
+ The type to which the returned result should be converted
287
+
288
+ A :class:`fresco.exceptions.BadRequest` error is raised if ``type`` is
289
+ specified and the value cannot be converted to the given type
290
+
291
+ :param default:
292
+ The value to return if the key not present.
293
+
294
+ :param required:
295
+ If True, a missing key will cause a
296
+ :class:`fresco.exceptions.BadRequest` to be raised.
253
297
 
254
- If ``type`` is specified and the value cannot be converted to the given
255
- type, or the value is not present and and no default value has been
256
- specified, then a :class:`fresco.exceptions.BadRequest` error is raised.
257
298
  """
258
- default_specified = default is not _marker
259
- value = self.form.get(key, default)
260
- if value is default:
261
- if default_specified:
262
- return default
263
- if type is None:
299
+ value = self.form.get(key, _marker)
300
+ if value is _marker:
301
+ if required:
302
+ raise exceptions.BadRequest()
303
+ if default is _marker:
264
304
  return None
305
+ return default
265
306
 
266
- if type is None:
307
+ if type is str:
267
308
  return value
268
309
  try:
269
310
  return type(value)
@@ -276,15 +317,97 @@ class Request(object):
276
317
  """
277
318
  return self.form.getlist(key)
278
319
 
279
- def getint(self, key, default=_marker) -> int:
320
+ @t.overload
321
+ def getbool(self, key: str) -> t.Union[bool, None]:
322
+ ...
323
+
324
+ @t.overload
325
+ def getbool(self, key: str, default: T1) -> t.Union[bool, T1, None]:
326
+ ...
327
+
328
+ @t.overload
329
+ def getbool(self, key: str, *, required: t.Literal[True]) -> bool:
330
+ ...
331
+
332
+ def getbool(
333
+ self, key: str, default: T1 = _marker, required: bool = False
334
+ ) -> t.Union[bool, T1, None]:
280
335
  """
281
- Return the named key, converted to an integer.
336
+ Return the named key, converted to a bool.
337
+ """
338
+ if required:
339
+ return self.get(key, type=bool, required=True)
340
+ else:
341
+ return self.get(key, default=default, type=bool)
342
+
343
+ @t.overload
344
+ def getdecimal(self, key: str) -> t.Union[Decimal, None]:
345
+ ...
346
+
347
+ @t.overload
348
+ def getdecimal(self, key: str, default: T1) -> t.Union[Decimal, T1, None]:
349
+ ...
282
350
 
283
- If the key is not present or could not be converted to an int and no
284
- default is specified, :class:`~fresco.exceptions.BadRequest` will be
285
- raised.
351
+ @t.overload
352
+ def getdecimal(self, key: str, *, required: t.Literal[True]) -> Decimal:
353
+ ...
354
+
355
+ def getdecimal(
356
+ self, key: str, default: T1 = _marker, required: bool = False
357
+ ) -> t.Union[Decimal, T1, None]:
358
+ """
359
+ Return the named key, converted to a decimal.Decimal
286
360
  """
287
- return self.get(key, default, type=int)
361
+ if required:
362
+ return self.get(key, type=Decimal, required=True)
363
+ else:
364
+ return self.get(key, default=default, type=Decimal)
365
+
366
+ @t.overload
367
+ def getfloat(self, key: str) -> t.Union[float, None]:
368
+ ...
369
+
370
+ @t.overload
371
+ def getfloat(self, key: str, default: T1) -> t.Union[float, T1, None]:
372
+ ...
373
+
374
+ @t.overload
375
+ def getfloat(self, key: str, *, required: t.Literal[True]) -> float:
376
+ ...
377
+
378
+ def getfloat(
379
+ self, key: str, default: T1 = _marker, required: bool = False
380
+ ) -> t.Union[float, T1, None]:
381
+ """
382
+ Return the named key, converted to a float.
383
+ """
384
+ if required:
385
+ return self.get(key, type=float, required=True)
386
+ else:
387
+ return self.get(key, default=default, type=float)
388
+
389
+ @t.overload
390
+ def getint(self, key: str) -> t.Union[int, None]:
391
+ ...
392
+
393
+ @t.overload
394
+ def getint(self, key: str, default: T1) -> t.Union[int, T1, None]:
395
+ ...
396
+
397
+ @t.overload
398
+ def getint(self, key: str, *, required: t.Literal[True]) -> int:
399
+ ...
400
+
401
+ def getint(
402
+ self, key: str, default: T1 = _marker, required: bool = False
403
+ ) -> t.Union[int, T1, None]:
404
+ """
405
+ Return the named key, converted to an integer.
406
+ """
407
+ if required:
408
+ return self.get(key, type=int, required=True)
409
+ else:
410
+ return self.get(key, default=default, type=int)
288
411
 
289
412
  def __contains__(self, key):
290
413
  """
@@ -293,9 +416,7 @@ class Request(object):
293
416
  return key in self.form
294
417
 
295
418
  @property
296
- def now( # type: ignore
297
- self, now=datetime.datetime.now, utc=datetime.timezone.utc
298
- ):
419
+ def now(self, now=datetime.datetime.now, utc=datetime.timezone.utc): # type: ignore
299
420
  """
300
421
  Return a timezone-aware UTC datetime instance. The value returned is
301
422
  guaranteed to be constant throughout the lifetime of the request.
@@ -451,7 +572,7 @@ class Request(object):
451
572
  return self._parsed_url
452
573
 
453
574
  @property
454
- def path_info(self, environ_to_str=environ_to_str) -> str: # type: ignore
575
+ def path_info(self, environ_to_str=environ_to_str) -> str:
455
576
  """
456
577
  The PATH_INFO value as a string
457
578
 
@@ -463,9 +584,9 @@ class Request(object):
463
584
  raise exceptions.BadRequest
464
585
 
465
586
  @property
466
- def script_name(self):
467
- """\
468
- The SCRIPT_NAME value as a unicode string
587
+ def script_name(self) -> str:
588
+ """
589
+ The SCRIPT_NAME value as a string
469
590
 
470
591
  Note that SCRIPT_NAME is already unquoted by the server
471
592
  """
@@ -477,12 +598,12 @@ class Request(object):
477
598
  @property
478
599
  def query_string(self) -> t.Optional[str]:
479
600
  """
480
- The QUERY_STRING value as a unicode string
601
+ The QUERY_STRING value as a string
481
602
  """
482
603
  return self.environ.get("QUERY_STRING")
483
604
 
484
605
  @property
485
- def referrer(self):
606
+ def referrer(self) -> t.Optional[str]:
486
607
  """
487
608
  Return the HTTP referer header, or ``None`` if this is not available.
488
609
  """
fresco/requestcontext.py CHANGED
@@ -68,6 +68,9 @@ class RequestContext(object):
68
68
  def currentcontext(self) -> ContextDict:
69
69
  return self._contexts[self._ident_func()][-1]
70
70
 
71
+ def __contains__(self, item):
72
+ return item in self._contexts[self._ident_func()][-1]
73
+
71
74
  def __getattr__(self, item):
72
75
  try:
73
76
  return self._contexts[self._ident_func()][-1][item]
fresco/response.py CHANGED
@@ -16,12 +16,14 @@
16
16
  The :class:`Response` class models the response from your application to a
17
17
  single request.
18
18
  """
19
+ from collections.abc import Iterable
19
20
  from datetime import datetime
20
21
  from itertools import chain
21
22
  from typing import Callable
22
23
  from typing import List
23
24
  from typing import Tuple
24
25
  from typing import Set
26
+ import typing as t
25
27
  import re
26
28
  import json as stdlib_json
27
29
 
@@ -331,6 +333,7 @@ class Response(object):
331
333
  """
332
334
 
333
335
  default_content_type = "text/html; charset=UTF-8"
336
+ content_iterator: Iterable[bytes]
334
337
 
335
338
  def __init__(
336
339
  self,
@@ -386,7 +389,7 @@ class Response(object):
386
389
  self.status = "200 OK"
387
390
  else:
388
391
  try:
389
- self.status = f"{status} {HTTP_STATUS_CODES[status]}"
392
+ self.status = f"{status} {HTTP_STATUS_CODES[status]}" # type: ignore
390
393
  except KeyError:
391
394
  self.status = str(status)
392
395
 
@@ -441,7 +444,7 @@ class Response(object):
441
444
  else:
442
445
  self.content_iterator = encoder(content, self.charset)
443
446
 
444
- def __call__(self, environ, start_response, exc_info=None):
447
+ def __call__(self, environ, start_response, exc_info=None) -> Iterable[bytes]:
445
448
  """
446
449
  WSGI callable. Calls ``start_response`` with assigned headers and
447
450
  returns an iterator over ``content``.
@@ -449,7 +452,7 @@ class Response(object):
449
452
  start_response(self.status, self.headers, exc_info)
450
453
  result = self.content_iterator
451
454
  if self.onclose:
452
- result = ClosingIterator(result, *self.onclose)
455
+ result = ClosingIterator[bytes](result, *self.onclose)
453
456
  return result
454
457
 
455
458
  def add_onclose(self, *funcs):
@@ -610,12 +613,12 @@ class Response(object):
610
613
  value,
611
614
  max_age=None,
612
615
  expires=None,
613
- path="/",
614
- secure=None,
615
- domain=None,
616
- comment=None,
617
- httponly=False,
618
- samesite="Lax",
616
+ path: str = "/",
617
+ secure: bool = False,
618
+ domain: t.Optional[str] = None,
619
+ comment: t.Optional[str] = None,
620
+ httponly: bool = False,
621
+ samesite: str = "Lax",
619
622
  ):
620
623
  """
621
624
  Return a new response object with the given cookie added.