bareutils 4.0.2__tar.gz → 5.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bareutils-4.0.2 → bareutils-5.0.0}/LICENSE +1 -1
- bareutils-5.0.0/PKG-INFO +52 -0
- bareutils-5.0.0/README.md +15 -0
- bareutils-5.0.0/pyproject.toml +97 -0
- bareutils-5.0.0/setup.cfg +4 -0
- {bareutils-4.0.2 → bareutils-5.0.0/src}/bareutils/__init__.py +2 -0
- {bareutils-4.0.2 → bareutils-5.0.0/src}/bareutils/compression.py +89 -11
- {bareutils-4.0.2 → bareutils-5.0.0/src}/bareutils/cookies.py +25 -30
- bareutils-5.0.0/src/bareutils/dates/constants.py +7 -0
- {bareutils-4.0.2 → bareutils-5.0.0/src}/bareutils/dates/helpers.py +1 -2
- {bareutils-4.0.2 → bareutils-5.0.0/src}/bareutils/dates/rfc_7231.py +4 -6
- {bareutils-4.0.2 → bareutils-5.0.0/src}/bareutils/dates/rfc_850.py +4 -6
- {bareutils-4.0.2 → bareutils-5.0.0/src}/bareutils/header.py +488 -340
- {bareutils-4.0.2 → bareutils-5.0.0/src}/bareutils/unpacking.py +6 -6
- bareutils-5.0.0/src/bareutils.egg-info/PKG-INFO +52 -0
- bareutils-5.0.0/src/bareutils.egg-info/SOURCES.txt +26 -0
- bareutils-5.0.0/src/bareutils.egg-info/dependency_links.txt +1 -0
- bareutils-5.0.0/src/bareutils.egg-info/requires.txt +15 -0
- bareutils-5.0.0/src/bareutils.egg-info/top_level.txt +1 -0
- bareutils-5.0.0/tests/test_compression.py +35 -0
- bareutils-5.0.0/tests/test_cookies.py +147 -0
- bareutils-5.0.0/tests/test_header.py +895 -0
- bareutils-5.0.0/tests/test_response_code.py +21 -0
- bareutils-5.0.0/tests/test_streaming.py +34 -0
- bareutils-4.0.2/PKG-INFO +0 -34
- bareutils-4.0.2/README.md +0 -15
- bareutils-4.0.2/pyproject.toml +0 -30
- bareutils-4.0.2/setup.py +0 -26
- {bareutils-4.0.2 → bareutils-5.0.0/src}/bareutils/dates/__init__.py +0 -0
- {bareutils-4.0.2 → bareutils-5.0.0/src}/bareutils/py.typed +0 -0
- {bareutils-4.0.2 → bareutils-5.0.0/src}/bareutils/response_code.py +0 -0
- {bareutils-4.0.2 → bareutils-5.0.0/src}/bareutils/streaming.py +0 -0
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
same "printed page" as the copyright notice for easier
|
|
187
187
|
identification within third-party archives.
|
|
188
188
|
|
|
189
|
-
Copyright
|
|
189
|
+
Copyright 2026 Rob Blackbourn
|
|
190
190
|
|
|
191
191
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
192
|
you may not use this file except in compliance with the License.
|
bareutils-5.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bareutils
|
|
3
|
+
Version: 5.0.0
|
|
4
|
+
Summary: Utilities for bareASGI and bareClient
|
|
5
|
+
Author-email: Rob Blackbourn <rob.blackbourn@gmail.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://bareASGI.github.io/bareUtils
|
|
8
|
+
Project-URL: Repository, https://github.com/bareASGI/bareUtils
|
|
9
|
+
Project-URL: Issues, https://github.com/bareASGI/bareUtils/issues
|
|
10
|
+
Keywords: asgi,web,framework,asyncio,http
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Environment :: Web Environment
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: autopep8; extra == "dev"
|
|
25
|
+
Requires-Dist: coverage; extra == "dev"
|
|
26
|
+
Requires-Dist: mypy; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
29
|
+
Requires-Dist: pylint; extra == "dev"
|
|
30
|
+
Requires-Dist: types-setuptools; extra == "dev"
|
|
31
|
+
Provides-Extra: docs
|
|
32
|
+
Requires-Dist: mkdocs; extra == "docs"
|
|
33
|
+
Requires-Dist: mkdocs-material; extra == "docs"
|
|
34
|
+
Requires-Dist: jetblack-markdown; extra == "docs"
|
|
35
|
+
Requires-Dist: mike; extra == "docs"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# bareutils
|
|
39
|
+
|
|
40
|
+
Utilities for [bareASGI](https://github.com/bareASGI/bareASGI)
|
|
41
|
+
and [bareClient](https://github.com/bareASGI/bareClient)
|
|
42
|
+
(read the [docs](https://bareASGI.github.io/bareUtils/)).
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
The package can be installed with pip.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install bareutils
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This is a Python 3.12 and later package.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# bareutils
|
|
2
|
+
|
|
3
|
+
Utilities for [bareASGI](https://github.com/bareASGI/bareASGI)
|
|
4
|
+
and [bareClient](https://github.com/bareASGI/bareClient)
|
|
5
|
+
(read the [docs](https://bareASGI.github.io/bareUtils/)).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
The package can be installed with pip.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install bareutils
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This is a Python 3.12 and later package.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bareutils"
|
|
3
|
+
version = "5.0.0"
|
|
4
|
+
description = "Utilities for bareASGI and bareClient"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Rob Blackbourn", email = "rob.blackbourn@gmail.com" }
|
|
7
|
+
]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 5 - Production/Stable",
|
|
11
|
+
"Environment :: Web Environment",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Programming Language :: Python :: 3.14",
|
|
18
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
19
|
+
]
|
|
20
|
+
readme = "README.md"
|
|
21
|
+
license = "Apache-2.0"
|
|
22
|
+
license-files = [
|
|
23
|
+
"LICENSE"
|
|
24
|
+
]
|
|
25
|
+
keywords = [
|
|
26
|
+
"asgi",
|
|
27
|
+
"web",
|
|
28
|
+
"framework",
|
|
29
|
+
"asyncio",
|
|
30
|
+
"http",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"autopep8",
|
|
36
|
+
"coverage",
|
|
37
|
+
"mypy",
|
|
38
|
+
"pytest",
|
|
39
|
+
"pytest-asyncio",
|
|
40
|
+
"pylint",
|
|
41
|
+
"types-setuptools",
|
|
42
|
+
]
|
|
43
|
+
docs = [
|
|
44
|
+
"mkdocs",
|
|
45
|
+
"mkdocs-material",
|
|
46
|
+
"jetblack-markdown",
|
|
47
|
+
"mike",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.urls]
|
|
51
|
+
Homepage = "https://bareASGI.github.io/bareUtils"
|
|
52
|
+
Repository = "https://github.com/bareASGI/bareUtils"
|
|
53
|
+
Issues = "https://github.com/bareASGI/bareUtils/issues"
|
|
54
|
+
|
|
55
|
+
[build-system]
|
|
56
|
+
requires = ["setuptools>=61.0"]
|
|
57
|
+
build-backend = "setuptools.build_meta"
|
|
58
|
+
|
|
59
|
+
# pylint
|
|
60
|
+
[tool.pylint.main]
|
|
61
|
+
py-version = "3.12"
|
|
62
|
+
jobs = 4
|
|
63
|
+
|
|
64
|
+
[tool.pylint.basic]
|
|
65
|
+
good-names = ["i", "j", "k", "ex", "Run", "_"]
|
|
66
|
+
bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"]
|
|
67
|
+
|
|
68
|
+
[tool.pylint."messages control"]
|
|
69
|
+
disable = [
|
|
70
|
+
"raw-checker-failed",
|
|
71
|
+
"bad-inline-option",
|
|
72
|
+
"locally-disabled",
|
|
73
|
+
"file-ignored",
|
|
74
|
+
"suppressed-message",
|
|
75
|
+
"useless-suppression",
|
|
76
|
+
"deprecated-pragma",
|
|
77
|
+
"use-symbolic-message-instead",
|
|
78
|
+
"use-implicit-booleaness-not-comparison-to-string",
|
|
79
|
+
"use-implicit-booleaness-not-comparison-to-zero",
|
|
80
|
+
"missing-module-docstring",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
# pytest
|
|
84
|
+
[tool.pytest.ini_options]
|
|
85
|
+
testpaths = [
|
|
86
|
+
"tests",
|
|
87
|
+
]
|
|
88
|
+
asyncio_default_fixture_loop_scope = "session"
|
|
89
|
+
asyncio_default_test_loop_scope = "function"
|
|
90
|
+
|
|
91
|
+
# mypy
|
|
92
|
+
[tool.mypy]
|
|
93
|
+
files = [ "src", "tests" ]
|
|
94
|
+
|
|
95
|
+
[[tool.mypy.overrides]]
|
|
96
|
+
module = "pytest"
|
|
97
|
+
ignore_missing_imports = true
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
1
|
+
"""Compression utilities
|
|
2
|
+
|
|
3
|
+
Only compression direclty supported by standard library functions are provided
|
|
4
|
+
here to avoid the need for additional dependencies. Other compression methods
|
|
5
|
+
should be implemented in a separate module.
|
|
3
6
|
"""
|
|
4
7
|
|
|
5
8
|
from __future__ import annotations
|
|
6
9
|
from abc import ABCMeta, abstractmethod
|
|
7
|
-
from typing import AsyncIterable, Callable,
|
|
10
|
+
from typing import AsyncIterable, Callable, cast
|
|
8
11
|
import zlib
|
|
9
12
|
|
|
10
13
|
from .streaming import bytes_writer, bytes_reader
|
|
@@ -24,6 +27,7 @@ class Compressor(metaclass=ABCMeta):
|
|
|
24
27
|
bytes: The compressed buffer.
|
|
25
28
|
"""
|
|
26
29
|
|
|
30
|
+
@abstractmethod
|
|
27
31
|
def flush(self) -> bytes:
|
|
28
32
|
"""Flush the compressor
|
|
29
33
|
|
|
@@ -116,29 +120,82 @@ class Decompressor(metaclass=ABCMeta):
|
|
|
116
120
|
@property
|
|
117
121
|
@abstractmethod
|
|
118
122
|
def unused_data(self) -> bytes:
|
|
119
|
-
|
|
123
|
+
"""A bytes object which contains any bytes past the end of the
|
|
124
|
+
compressed data.
|
|
125
|
+
|
|
126
|
+
That is, this remains b"" until the last byte that contains compression
|
|
127
|
+
data is available. If the whole bytestring turned out to contain
|
|
128
|
+
compressed data, this is b"", an empty bytes object.
|
|
129
|
+
"""
|
|
120
130
|
|
|
121
131
|
@property
|
|
122
132
|
@abstractmethod
|
|
123
133
|
def unconsumed_tail(self) -> bytes:
|
|
124
|
-
|
|
134
|
+
"""A bytes object that contains any data that was not consumed by the
|
|
135
|
+
last decompress() call because it exceeded the limit for the
|
|
136
|
+
uncompressed data buffer.
|
|
137
|
+
"""
|
|
125
138
|
|
|
126
139
|
@property
|
|
127
140
|
@abstractmethod
|
|
128
141
|
def eof(self) -> bool:
|
|
129
|
-
|
|
142
|
+
"""A boolean indicating whether the end of the compressed data stream has been reached.
|
|
143
|
+
|
|
144
|
+
This makes it possible to distinguish between a properly formed compressed
|
|
145
|
+
stream, and an incomplete or truncated one.
|
|
146
|
+
"""
|
|
130
147
|
|
|
131
148
|
@abstractmethod
|
|
132
149
|
def decompress(self, buf: bytes, max_length: int = 0) -> bytes:
|
|
133
|
-
|
|
150
|
+
"""Decompress data, returning a bytes object containing the uncompressed
|
|
151
|
+
data corresponding to at least part of the data in string.
|
|
152
|
+
|
|
153
|
+
This data should be concatenated to the output produced by any preceding
|
|
154
|
+
calls to the decompress() method. Some of the input data may be
|
|
155
|
+
preserved in internal buffers for later processing.
|
|
156
|
+
|
|
157
|
+
If the optional parameter max_length is non-zero then the return value
|
|
158
|
+
will be no longer than max_length. This may mean that not all of the
|
|
159
|
+
compressed input can be processed; and unconsumed data will be stored in
|
|
160
|
+
the attribute unconsumed_tail. This bytestring must be passed to a
|
|
161
|
+
subsequent call to decompress() if decompression is to continue. If
|
|
162
|
+
max_length is zero then the whole input is decompressed, and
|
|
163
|
+
unconsumed_tail is empty.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
buf (bytes): The data to decompress.
|
|
167
|
+
max_length (int, optional): Max length of output. Defaults to 0.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
bytes: The decompressed data.
|
|
171
|
+
"""
|
|
134
172
|
|
|
135
173
|
@abstractmethod
|
|
136
|
-
def flush(self, length:
|
|
137
|
-
|
|
174
|
+
def flush(self, length: int | None = None) -> bytes:
|
|
175
|
+
"""All pending input is processed, and a bytes object containing the
|
|
176
|
+
remaining uncompressed output is returned. After calling flush(), the
|
|
177
|
+
decompress() method cannot be called again; the only realistic action is
|
|
178
|
+
to delete the object.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
length (int | None, optional): The initial size of the output
|
|
182
|
+
buffer. Defaults to None.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
bytes: The remaining uncompressed output.
|
|
186
|
+
"""
|
|
138
187
|
|
|
139
188
|
@abstractmethod
|
|
140
189
|
def copy(self) -> Decompressor:
|
|
141
|
-
|
|
190
|
+
"""Returns a copy of the decompression object.
|
|
191
|
+
|
|
192
|
+
This can be used to save the state of the decompressor midway through
|
|
193
|
+
the data stream in order to speed up random seeks into the stream at a
|
|
194
|
+
future point.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Decompressor: A copy of the decompressor.
|
|
198
|
+
"""
|
|
142
199
|
|
|
143
200
|
|
|
144
201
|
DecompressorFactory = Callable[[], Decompressor]
|
|
@@ -178,7 +235,7 @@ def make_compress_decompressobj() -> Decompressor:
|
|
|
178
235
|
"""
|
|
179
236
|
return cast(
|
|
180
237
|
Decompressor,
|
|
181
|
-
zlib.decompressobj(
|
|
238
|
+
zlib.decompressobj(zlib.MAX_WBITS)
|
|
182
239
|
)
|
|
183
240
|
|
|
184
241
|
|
|
@@ -186,6 +243,18 @@ async def compression_reader_adapter(
|
|
|
186
243
|
reader: AsyncIterable[bytes],
|
|
187
244
|
decompressobj: Decompressor
|
|
188
245
|
) -> AsyncIterable[bytes]:
|
|
246
|
+
"""Adapters a reader to decompress the data.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
reader (AsyncIterable[bytes]): The reader.
|
|
250
|
+
decompressobj (Decompressor): The decompressor.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
AsyncIterable[bytes]: An async iterable of decompressed bytes.
|
|
254
|
+
|
|
255
|
+
Yields:
|
|
256
|
+
bytes: Decompressed bytes.
|
|
257
|
+
"""
|
|
189
258
|
async for item in reader:
|
|
190
259
|
yield decompressobj.decompress(item)
|
|
191
260
|
yield decompressobj.flush()
|
|
@@ -195,4 +264,13 @@ async def compression_reader(
|
|
|
195
264
|
source: AsyncIterable[bytes],
|
|
196
265
|
decompressobj: Decompressor
|
|
197
266
|
) -> bytes:
|
|
267
|
+
"""Reads a compressed stream and returns the decompressed bytes.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
source (AsyncIterable[bytes]): The input stream.
|
|
271
|
+
decompressobj (Decompressor): The decompressor.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
bytes: The decompressed bytes.
|
|
275
|
+
"""
|
|
198
276
|
return await bytes_reader(compression_reader_adapter(source, decompressobj))
|
|
@@ -3,12 +3,8 @@
|
|
|
3
3
|
from datetime import datetime, timedelta
|
|
4
4
|
from typing import (
|
|
5
5
|
Any,
|
|
6
|
-
Dict,
|
|
7
|
-
List,
|
|
8
6
|
Mapping,
|
|
9
|
-
MutableMapping
|
|
10
|
-
Optional,
|
|
11
|
-
Union
|
|
7
|
+
MutableMapping
|
|
12
8
|
)
|
|
13
9
|
|
|
14
10
|
from .dates import parse_date
|
|
@@ -19,13 +15,13 @@ def encode_set_cookie(
|
|
|
19
15
|
name: bytes,
|
|
20
16
|
value: bytes,
|
|
21
17
|
*,
|
|
22
|
-
expires:
|
|
23
|
-
max_age:
|
|
24
|
-
path:
|
|
25
|
-
domain:
|
|
18
|
+
expires: datetime | None = None,
|
|
19
|
+
max_age: int | timedelta | None = None,
|
|
20
|
+
path: bytes | None = None,
|
|
21
|
+
domain: bytes | None = None,
|
|
26
22
|
secure: bool = False,
|
|
27
23
|
http_only: bool = False,
|
|
28
|
-
same_site:
|
|
24
|
+
same_site: bytes | None = None
|
|
29
25
|
) -> bytes:
|
|
30
26
|
"""Encode set-cookie
|
|
31
27
|
|
|
@@ -45,14 +41,13 @@ def encode_set_cookie(
|
|
|
45
41
|
same_site (Optional[bytes], optional): CORS directive. Defaults to None.
|
|
46
42
|
|
|
47
43
|
Raises:
|
|
48
|
-
|
|
49
|
-
ValueError: If a date could not be parsed.
|
|
44
|
+
ValueError: Raised if the __Secure- or __Host- was used without secure
|
|
50
45
|
|
|
51
46
|
Returns:
|
|
52
47
|
bytes: The set-cookie header
|
|
53
48
|
"""
|
|
54
49
|
if not secure and (name.startswith(b'__Secure-') or name.startswith(b'__Host-')):
|
|
55
|
-
raise
|
|
50
|
+
raise ValueError(
|
|
56
51
|
'Keys starting __Secure- or __Host- require the secure directive'
|
|
57
52
|
)
|
|
58
53
|
|
|
@@ -101,26 +96,26 @@ def decode_set_cookie(set_cookie: bytes) -> Mapping[str, Any]:
|
|
|
101
96
|
"""
|
|
102
97
|
i = iter(set_cookie.split(b';'))
|
|
103
98
|
key, _, value = next(i).partition(b'=')
|
|
104
|
-
result:
|
|
99
|
+
result: dict[str, Any] = {'name': key, 'value': value}
|
|
105
100
|
for item in i:
|
|
106
101
|
key, _, value = item.partition(b'=')
|
|
107
|
-
name = key.lower().strip()
|
|
108
|
-
if name == 'secure':
|
|
102
|
+
name = key.lower().strip()
|
|
103
|
+
if name == b'secure':
|
|
109
104
|
result['secure'] = True
|
|
110
|
-
elif name == 'httponly':
|
|
105
|
+
elif name == b'httponly':
|
|
111
106
|
result['http_only'] = True
|
|
112
|
-
elif name == 'expires':
|
|
107
|
+
elif name == b'expires':
|
|
113
108
|
result['expires'] = parse_date(value.decode('ascii'))
|
|
114
|
-
elif name == 'max-age':
|
|
109
|
+
elif name == b'max-age':
|
|
115
110
|
result['max_age'] = timedelta(seconds=int(value))
|
|
116
|
-
elif name == 'samesite':
|
|
117
|
-
result['same_site'] = value
|
|
111
|
+
elif name == b'samesite':
|
|
112
|
+
result['same_site'] = value
|
|
118
113
|
else:
|
|
119
|
-
result[name] = value
|
|
114
|
+
result[name.decode('ascii')] = value
|
|
120
115
|
return result
|
|
121
116
|
|
|
122
117
|
|
|
123
|
-
def encode_cookies(cookies: Mapping[bytes,
|
|
118
|
+
def encode_cookies(cookies: Mapping[bytes, list[bytes]]) -> bytes:
|
|
124
119
|
"""Encode the cookie header
|
|
125
120
|
|
|
126
121
|
Args:
|
|
@@ -136,16 +131,16 @@ def encode_cookies(cookies: Mapping[bytes, List[bytes]]) -> bytes:
|
|
|
136
131
|
)
|
|
137
132
|
|
|
138
133
|
|
|
139
|
-
def decode_cookies(cookies: bytes) -> Mapping[bytes,
|
|
134
|
+
def decode_cookies(cookies: bytes) -> Mapping[bytes, list[bytes]]:
|
|
140
135
|
"""Decode a cookie header
|
|
141
136
|
|
|
142
137
|
Args:
|
|
143
138
|
cookies (bytes): The header
|
|
144
139
|
|
|
145
140
|
Returns:
|
|
146
|
-
Mapping[bytes,
|
|
141
|
+
Mapping[bytes, list[bytes]]: The cookies
|
|
147
142
|
"""
|
|
148
|
-
result: MutableMapping[bytes,
|
|
143
|
+
result: MutableMapping[bytes, list[bytes]] = {}
|
|
149
144
|
for morsel in cookies.rstrip(b'; ').split(b'; '):
|
|
150
145
|
name, _, value = morsel.partition(b'=')
|
|
151
146
|
result.setdefault(name, []).append(value)
|
|
@@ -156,12 +151,12 @@ def make_cookie(
|
|
|
156
151
|
key: bytes,
|
|
157
152
|
value: bytes,
|
|
158
153
|
*,
|
|
159
|
-
expires:
|
|
160
|
-
path:
|
|
161
|
-
domain:
|
|
154
|
+
expires: datetime | timedelta | None = None,
|
|
155
|
+
path: bytes | None = None,
|
|
156
|
+
domain: bytes | None = None,
|
|
162
157
|
secure: bool = False,
|
|
163
158
|
http_only: bool = False,
|
|
164
|
-
same_site:
|
|
159
|
+
same_site: bytes | None = None
|
|
165
160
|
) -> bytes:
|
|
166
161
|
"""Make a set-cookie header
|
|
167
162
|
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"""Date helpers"""
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
from . import rfc_7231
|
|
7
6
|
from . import rfc_850
|
|
8
7
|
|
|
9
8
|
|
|
10
|
-
def try_parse_date(value: str) ->
|
|
9
|
+
def try_parse_date(value: str) -> datetime | None:
|
|
11
10
|
"""Try to parse a date
|
|
12
11
|
|
|
13
12
|
Args:
|
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""RFC 7231"""
|
|
2
2
|
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
4
|
import re
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
from .constants import DAY_NAMES, MONTH_NAMES
|
|
6
7
|
|
|
7
8
|
# pylint: disable=line-too-long
|
|
8
9
|
# Date: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
|
|
9
10
|
DATE_PATTERN = re.compile(
|
|
10
11
|
r'^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d{2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d{2}):(\d{2}):(\d{2}) GMT$'
|
|
11
12
|
)
|
|
12
|
-
DAY_NAMES = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
|
|
13
|
-
MONTH_NAMES = ("Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
14
|
-
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
|
|
15
13
|
|
|
16
14
|
|
|
17
|
-
def try_parse_date(value: str) ->
|
|
15
|
+
def try_parse_date(value: str) -> datetime | None:
|
|
18
16
|
"""Parse a date according to RFC 7231, section 7.1.1.2: Date
|
|
19
17
|
|
|
20
18
|
Args:
|
|
@@ -2,18 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
4
|
import re
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
from .constants import DAY_NAMES, MONTH_NAMES
|
|
6
7
|
|
|
7
8
|
# 'Mon, 23-Mar-20 07:36:36 GMT'
|
|
8
9
|
DATE_PATTERN = re.compile(
|
|
9
10
|
r'^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d{2})-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d{2}) (\d{2}):(\d{2}):(\d{2}) GMT$'
|
|
10
11
|
)
|
|
11
|
-
DAY_NAMES = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
|
|
12
|
-
MONTH_NAMES = ("Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
13
|
-
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
|
|
14
12
|
|
|
15
13
|
|
|
16
|
-
def try_parse_date(value: str) ->
|
|
14
|
+
def try_parse_date(value: str) -> datetime | None:
|
|
17
15
|
"""Parse a date according to RFC 850.
|
|
18
16
|
|
|
19
17
|
e.g. 'Mon, 23-Mar-20 07:36:36 GMT'
|
|
@@ -58,7 +56,7 @@ def format_date(value: datetime) -> str:
|
|
|
58
56
|
day=DAY_NAMES[time_tuple.tm_wday],
|
|
59
57
|
mday=time_tuple.tm_mday,
|
|
60
58
|
mon=MONTH_NAMES[time_tuple.tm_mon - 1],
|
|
61
|
-
year=
|
|
59
|
+
year=time_tuple.tm_year % 100,
|
|
62
60
|
hour=time_tuple.tm_hour,
|
|
63
61
|
min=time_tuple.tm_min,
|
|
64
62
|
sec=time_tuple.tm_sec
|