httpquoted 0.2.0__tar.gz → 1.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.
- httpquoted-1.0.0/CHANGELOG.md +83 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/CONTRIBUTING.md +19 -19
- httpquoted-1.0.0/GUIDE.md +149 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/LICENSE +21 -21
- {httpquoted-0.2.0 → httpquoted-1.0.0}/PKG-INFO +35 -17
- {httpquoted-0.2.0 → httpquoted-1.0.0}/README.md +29 -14
- {httpquoted-0.2.0 → httpquoted-1.0.0}/pyproject.toml +18 -8
- httpquoted-1.0.0/scripts/oracle_fixture.py +125 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/__init__.py +19 -3
- {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/_chars.py +29 -10
- {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/_cli.py +81 -16
- {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/_comment.py +15 -3
- {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/_quote.py +25 -4
- {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/_scan.py +25 -7
- {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/_split.py +21 -1
- {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/_token.py +8 -0
- httpquoted-1.0.0/tests/fixtures/oracle_runs.json +9436 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_contract.py +5 -0
- httpquoted-1.0.0/tests/test_contract_api.py +140 -0
- httpquoted-1.0.0/tests/test_contract_blind.py +66 -0
- httpquoted-1.0.0/tests/test_contract_cli.py +194 -0
- httpquoted-1.0.0/tests/test_contract_hostile.py +166 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_docs.py +22 -0
- httpquoted-1.0.0/tests/test_e2e_upgrade.py +108 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_e2e_wheel.py +19 -1
- httpquoted-1.0.0/tests/test_examples.py +115 -0
- httpquoted-1.0.0/tests/test_oracle.py +132 -0
- httpquoted-1.0.0/tests/test_whatwg.py +120 -0
- httpquoted-1.0.0/tests/upstream/MANIFEST.json +83 -0
- httpquoted-1.0.0/tests/whatwg_reference.py +64 -0
- httpquoted-0.2.0/.gitattributes +0 -1
- httpquoted-0.2.0/CHANGELOG.md +0 -37
- httpquoted-0.2.0/scripts/check.ps1 +0 -16
- httpquoted-0.2.0/scripts/release.ps1 +0 -28
- httpquoted-0.2.0/tests/upstream/MANIFEST.json +0 -29
- {httpquoted-0.2.0 → httpquoted-1.0.0}/.gitignore +0 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/requirements-dev.txt +0 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/scripts/fetch_upstream.py +0 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/__main__.py +0 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/py.typed +0 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/conftest.py +0 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_bytes.py +0 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_cli.py +0 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_comment.py +0 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_hostile.py +0 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_properties.py +0 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_quote.py +0 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_review_fixes.py +0 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_scan.py +0 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_sources.py +0 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_split.py +0 -0
- {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_token.py +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [1.0.0] - 2026-10-17
|
|
10
|
+
|
|
11
|
+
The public API is now stable: the eleven names in `httpquoted.__all__`, their
|
|
12
|
+
signatures, defaults, results and error types, and the `httpquoted` command with its
|
|
13
|
+
options and exit statuses. Results on valid input are identical to 0.2.0.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- The `strict`, `ascii_only` and `comments` options must be real `bool` values; anything
|
|
18
|
+
else raises `TypeError` (0.2.0 used the truth value, so `strict="no"` meant strict).
|
|
19
|
+
- `QuotedStringError` survives pickling (0.2.0 raised `TypeError` when unpickled).
|
|
20
|
+
- The package no longer exposes a stray `annotations` attribute.
|
|
21
|
+
- Public names report `httpquoted` as their module instead of a private module. One
|
|
22
|
+
side effect: `inspect.getsource(httpquoted.QuotedStringError)` can no longer find the
|
|
23
|
+
class source (functions are not affected).
|
|
24
|
+
- A `TypeError` message names at most 40 characters of the offending type name.
|
|
25
|
+
- A `str` or `bytes` subclass is read as its plain value without calling any of its
|
|
26
|
+
methods, and an `int` subclass given as `start` is read as its plain value.
|
|
27
|
+
- A scanner `start` outside the text is reported at offset 0 or at the length of the
|
|
28
|
+
text instead of at the given value (0.2.0 could not even format a `start` with more
|
|
29
|
+
than 4,300 digits).
|
|
30
|
+
- Command line: on POSIX systems each argument is read back as the octets it was given
|
|
31
|
+
(ISO-8859-1, like standard input) instead of as file system text, so a UTF-8 argument
|
|
32
|
+
is written out unchanged; an interrupt exits with status 130 and a closed output
|
|
33
|
+
(`| head`, reported as EPIPE on POSIX and EINVAL on Windows) with status 1, both
|
|
34
|
+
without a traceback. A closed standard input reads as no values, a closed standard
|
|
35
|
+
output exits with status 1, another write error (a full disk) is reported with its
|
|
36
|
+
errno and exits with status 1, and with standard error closed the reasons are dropped
|
|
37
|
+
instead of being written into the output. The help names the values `VALUE`
|
|
38
|
+
and the separator `SEP`.
|
|
39
|
+
- The sdist is built from an explicit file list, so a local virtual environment or
|
|
40
|
+
scratch file next to the sources is never packed.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- `GUIDE.md`: the grammar, lenient and strict modes, every error reason, the command
|
|
45
|
+
line and how the results compare with Werkzeug, h11, the standard library and the
|
|
46
|
+
WHATWG Fetch Standard.
|
|
47
|
+
- A runnable example in the docstring of every public name.
|
|
48
|
+
|
|
49
|
+
### Upgrading from 0.2.0
|
|
50
|
+
|
|
51
|
+
- Pass `True` or `False` to `strict`, `ascii_only` and `comments`.
|
|
52
|
+
- Code that compared `QuotedStringError.__module__` or a function's `__module__` with
|
|
53
|
+
a private module name now sees `httpquoted`.
|
|
54
|
+
|
|
55
|
+
## [0.2.0] - 2026-10-16
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- `QuotedStringError`, a `ValueError` with `reason` and `offset`.
|
|
60
|
+
- `strict=True` on `unquote` (exactly a token or a quoted-string) and `split_list`
|
|
61
|
+
(field content only, every quoted-string and comment well formed and closed).
|
|
62
|
+
- `scan_quoted_string` and `scan_comment`: read one quoted-string or (nested) comment
|
|
63
|
+
inside a larger value and return its value and end offset.
|
|
64
|
+
- `quote_comment` and `unquote_comment` for the comment syntax of section 5.6.5.
|
|
65
|
+
- `quote_if_needed` and `needs_quoting` for parameter values (section 5.6.6).
|
|
66
|
+
- `split_list` options: `separator` (";" for parameters, `str` or `bytes`) and `comments=True` for Via,
|
|
67
|
+
User-Agent and Server, where a comment may hold commas and a lone DQUOTE.
|
|
68
|
+
- `bytes` support everywhere: bytes in, bytes out, read as ISO-8859-1.
|
|
69
|
+
- `ascii_only=True` on the quoting functions to refuse obs-text.
|
|
70
|
+
- The `httpquoted` command (`quote`, `unquote`, `split`) and `python -m httpquoted`;
|
|
71
|
+
standard input and output are read and written as ISO-8859-1.
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
|
|
75
|
+
- `quote` raises `QuotedStringError` for CR, LF, NUL, the other controls, DEL and
|
|
76
|
+
characters above U+00FF instead of writing them into the header.
|
|
77
|
+
- Every function raises `TypeError` for input that is neither `str` nor `bytes`;
|
|
78
|
+
`is_token(None)` returned `False` before.
|
|
79
|
+
|
|
80
|
+
## [0.1.0] - 2026-10-12
|
|
81
|
+
|
|
82
|
+
- First release: `quote`, `unquote`, `is_token` and `split_list` for the HTTP
|
|
83
|
+
quoted-string primitive (RFC 9110).
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
# Contributing
|
|
2
|
-
|
|
3
|
-
Thanks for taking the time. This project keeps a small, predictable shape.
|
|
4
|
-
|
|
5
|
-
## Setup
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
pip install -r requirements-dev.txt
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Before you open a pull request
|
|
12
|
-
|
|
13
|
-
- Keep public API in `src/httpquoted/__init__.py`; internals live in `_`-prefixed modules.
|
|
14
|
-
- Add a test for the behaviour you change. Tests inject time, disk and network; they
|
|
15
|
-
never touch the real thing.
|
|
16
|
-
- Run `scripts/check.ps1` (format, lint, types, tests) and make sure it passes.
|
|
17
|
-
- One logical change per commit, imperative subject line.
|
|
18
|
-
|
|
19
|
-
Open an issue first if the change is large or shifts the public API.
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for taking the time. This project keeps a small, predictable shape.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip install -r requirements-dev.txt
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Before you open a pull request
|
|
12
|
+
|
|
13
|
+
- Keep public API in `src/httpquoted/__init__.py`; internals live in `_`-prefixed modules.
|
|
14
|
+
- Add a test for the behaviour you change. Tests inject time, disk and network; they
|
|
15
|
+
never touch the real thing.
|
|
16
|
+
- Run `scripts/check.ps1` (format, lint, types, tests) and make sure it passes.
|
|
17
|
+
- One logical change per commit, imperative subject line.
|
|
18
|
+
|
|
19
|
+
Open an issue first if the change is large or shifts the public API.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# httpquoted guide
|
|
2
|
+
|
|
3
|
+
This guide describes every behaviour of httpquoted 1.0 in detail. The
|
|
4
|
+
[README](https://github.com/JaviLianes8/httpquoted/blob/main/README.md) has the quick start.
|
|
5
|
+
|
|
6
|
+
## The grammar
|
|
7
|
+
|
|
8
|
+
RFC 9110 section 5.6 defines the pieces this library reads and writes:
|
|
9
|
+
|
|
10
|
+
| Rule | Characters |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `tchar` (token) | letters, digits and ``!#$%&'*+-.^_`|~`` |
|
|
13
|
+
| `qdtext` | HTAB, SP, `!`, `#` to `[`, `]` to `~`, obs-text |
|
|
14
|
+
| `ctext` | HTAB, SP, `!` to `'`, `*` to `[`, `]` to `~`, obs-text |
|
|
15
|
+
| `quoted-pair` | backslash followed by HTAB, SP, a visible ASCII character or obs-text |
|
|
16
|
+
| `obs-text` | the octets 0x80 to 0xFF |
|
|
17
|
+
|
|
18
|
+
A quoted-string is `DQUOTE *( qdtext / quoted-pair ) DQUOTE`. A comment is
|
|
19
|
+
`"(" *( ctext / quoted-pair / comment ) ")"`, so comments nest, and inside a comment
|
|
20
|
+
DQUOTE is plain text. The test suite evaluates these rules from the fetched RFC and
|
|
21
|
+
compares them with the library for every code point up to U+010F.
|
|
22
|
+
|
|
23
|
+
## Writing values
|
|
24
|
+
|
|
25
|
+
`quote` escapes only DQUOTE and backslash, `quote_comment` only `(`, `)` and backslash,
|
|
26
|
+
as section 5.6.4 asks of senders. Both refuse anything a quoted-pair cannot carry: NUL,
|
|
27
|
+
CR, LF and the other controls, DEL, and every character above U+00FF. That is what stops
|
|
28
|
+
a value from smuggling a line break into a header. With `ascii_only=True` they also
|
|
29
|
+
refuse obs-text, which section 5.5 discourages in new fields.
|
|
30
|
+
|
|
31
|
+
`quote_if_needed` returns a token unchanged and quotes anything else, including the
|
|
32
|
+
empty value; `needs_quoting` and `is_token` answer the same question as booleans.
|
|
33
|
+
|
|
34
|
+
## Reading values: lenient and strict
|
|
35
|
+
|
|
36
|
+
Every reader has two modes.
|
|
37
|
+
|
|
38
|
+
- Lenient (the default) never raises on malformed input. `unquote` and
|
|
39
|
+
`unquote_comment` strip the first and last character when they are the quotes (or the
|
|
40
|
+
parentheses) and resolve every backslash pair in between; anything else comes back
|
|
41
|
+
unchanged. The scanners stop at the first unescaped closing quote or matching
|
|
42
|
+
parenthesis and let an unterminated value run to the end of the text; a backslash at
|
|
43
|
+
the very end is kept. This is the "collect an HTTP quoted string" algorithm of the
|
|
44
|
+
WHATWG Fetch Standard, which the tests compare character by character.
|
|
45
|
+
- Strict (`strict=True`) checks every character against the grammar and raises
|
|
46
|
+
`QuotedStringError` at the first one that breaks it. `unquote` accepts exactly a token
|
|
47
|
+
or one quoted-string; `unquote_comment` exactly one comment.
|
|
48
|
+
|
|
49
|
+
`scan_quoted_string(text, start)` and `scan_comment(text, start)` return the value and
|
|
50
|
+
the offset just past the closing character, so a hand-written parser can continue from
|
|
51
|
+
there. The value of a comment keeps the parentheses of nested comments and has every
|
|
52
|
+
quoted-pair resolved, so `\)` and a nesting `)` look alike in it; slice the raw comment
|
|
53
|
+
with the returned offset to parse it again.
|
|
54
|
+
|
|
55
|
+
## Splitting lists
|
|
56
|
+
|
|
57
|
+
`split_list` splits on a separator outside quoted-strings (and outside comments with
|
|
58
|
+
`comments=True`, for Via, User-Agent and Server), trims spaces and tabs around each
|
|
59
|
+
element and drops empty elements, as section 5.6.1.2 asks of recipients. Elements keep
|
|
60
|
+
their quotes; pass them to `unquote` to read a value. `separator=";"` splits parameters.
|
|
61
|
+
The separator must be one field character other than DQUOTE, backslash, `(` and `)`.
|
|
62
|
+
|
|
63
|
+
Strict mode refuses a character that is not field content and any quoted-string or
|
|
64
|
+
comment that breaks its grammar or never ends, and, with `comments=True`, a stray `)`.
|
|
65
|
+
It does not check the grammar of each element, which belongs to the field: `"a"b` is
|
|
66
|
+
one element.
|
|
67
|
+
|
|
68
|
+
## str and bytes
|
|
69
|
+
|
|
70
|
+
Every function takes `str` or `bytes` and returns the same type. Bytes are read as
|
|
71
|
+
ISO-8859-1, where each octet is the code point of the same value, so obs-text
|
|
72
|
+
round-trips unchanged and every offset is an octet offset. `bytearray`, `memoryview` and
|
|
73
|
+
other types raise `TypeError`. The signatures use the `AnyStr` type variable, so a type
|
|
74
|
+
checker wants a value known to be `str` or known to be `bytes`; narrow a `str | bytes`
|
|
75
|
+
value with `isinstance` before the call. A subclass of `str` or `bytes` is read as its
|
|
76
|
+
plain value: none of its overridden methods are called. Lenient `unquote`,
|
|
77
|
+
`unquote_comment` and `quote_if_needed` return the very object they were given when they
|
|
78
|
+
have nothing to change.
|
|
79
|
+
|
|
80
|
+
## Errors
|
|
81
|
+
|
|
82
|
+
| Error | When |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `QuotedStringError` | a value breaks the grammar; a subclass of `ValueError` |
|
|
85
|
+
| `TypeError` | the value or the separator is not `str` or `bytes`; `strict`, `ascii_only` or `comments` is not a `bool`; `start` is not an `int` |
|
|
86
|
+
| `ValueError` | `split_list` was given a separator that cannot split a field value |
|
|
87
|
+
|
|
88
|
+
`QuotedStringError` carries `reason` and `offset`, and its message is
|
|
89
|
+
`"<reason> at offset <offset>"`. It never quotes the input: characters are named by
|
|
90
|
+
code point (`U+000A`) and type names are cut to 40 characters, so a huge or secret value
|
|
91
|
+
never reaches a log through an error. It survives pickling. The reasons are:
|
|
92
|
+
|
|
93
|
+
| Reason | Offset points at |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `U+XXXX is not allowed` | a character `quote`, `quote_comment` or strict `split_list` refuses |
|
|
96
|
+
| `U+XXXX is obs-text` | an obs-text character refused by `ascii_only=True` |
|
|
97
|
+
| `U+XXXX is not allowed in a quoted-string` (or `comment`) | a character outside qdtext or ctext |
|
|
98
|
+
| `U+XXXX cannot be quoted` | the character after a backslash |
|
|
99
|
+
| `a quoted-pair needs a character` | a backslash at the end of the text |
|
|
100
|
+
| `unterminated quoted-string` (or `comment`) | the opening quote, or the outermost opening parenthesis |
|
|
101
|
+
| `text after the closing quote` (or `parenthesis`) | the first character after it |
|
|
102
|
+
| `not a token or a quoted-string` | the first character that is not a token character |
|
|
103
|
+
| `a quoted-string must start with "` (or `comment`, `(`) | the `start` given to a scanner, moved to 0 or to the length of the text when it lies outside, or 0 for strict `unquote_comment` |
|
|
104
|
+
| `unbalanced )` | a stray `)` in `split_list(..., comments=True, strict=True)` |
|
|
105
|
+
|
|
106
|
+
## Command line
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
httpquoted quote [-h] [--comment] [--ascii-only] [VALUE ...]
|
|
110
|
+
httpquoted unquote [-h] [--comment] [--strict] [VALUE ...]
|
|
111
|
+
httpquoted split [-h] [--separator SEP] [--comments] [--strict] [VALUE ...]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Values come from the arguments, or one per line from standard input. Standard input and
|
|
115
|
+
output are read and written as ISO-8859-1, so octets pass through unchanged. On POSIX
|
|
116
|
+
systems each argument is read back as the octets it was given; on Windows arguments are
|
|
117
|
+
text and each character up to U+00FF is written as that octet. A closed standard input
|
|
118
|
+
reads as no values; with standard error closed the reasons are dropped rather than
|
|
119
|
+
mixed into the output. `python -m httpquoted` works the same way.
|
|
120
|
+
|
|
121
|
+
| Exit status | Meaning |
|
|
122
|
+
|---|---|
|
|
123
|
+
| 0 | every value was written |
|
|
124
|
+
| 1 | a value was rejected (the reason goes to standard error), the output closed early, or it could not be written |
|
|
125
|
+
| 2 | usage error |
|
|
126
|
+
| 130 | interrupted |
|
|
127
|
+
|
|
128
|
+
## Compared with other parsers
|
|
129
|
+
|
|
130
|
+
The tests run these tools on the same seeded inputs and check each statement below.
|
|
131
|
+
|
|
132
|
+
- Werkzeug 3.1.9: `quote_header_value` gives the same result as `quote_if_needed`, and
|
|
133
|
+
`unquote_header_value` the same as lenient `unquote` (except after a backslash
|
|
134
|
+
followed by LF). `parse_list_header` gives `[unquote(e) for e in split_list(v)]`,
|
|
135
|
+
but returns an empty list when a quoted-string never closes.
|
|
136
|
+
- The standard library: `urllib.request.parse_http_list` splits a well-formed value at the same commas but
|
|
137
|
+
keeps empty items and drops the backslash of each quoted-pair; `email.utils.quote`
|
|
138
|
+
escapes the same two characters as `quote`.
|
|
139
|
+
- h11 0.16.0 recognises the same tokens; it accepts the controls 0x01 to 0x08, 0x0E to
|
|
140
|
+
0x1F and 0x7F in a field value, which RFC 9110 does not allow and httpquoted refuses.
|
|
141
|
+
- The WHATWG Fetch Standard: lenient `scan_quoted_string` is its "collect an HTTP quoted
|
|
142
|
+
string" algorithm, and `split_list` is its "get, decode, and split" without the empty
|
|
143
|
+
values. Node's `util.MIMEType` reads parameter values the same way.
|
|
144
|
+
|
|
145
|
+
## Performance and threads
|
|
146
|
+
|
|
147
|
+
Every function is linear in the length of its input, including a megabyte of
|
|
148
|
+
backslashes, quotes or nested parentheses; nesting depth uses no recursion. The functions
|
|
149
|
+
keep no state and are safe to call from any number of threads.
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Javier Lianes
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Javier Lianes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: httpquoted
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Strict, typed and zero-dependency codec for HTTP quoted-strings, comments and tokens (RFC 9110): safe quoting, strict parsing, scanners and quote-aware list splitting
|
|
5
5
|
Project-URL: Homepage, https://lianes8studio.com
|
|
6
|
-
Project-URL:
|
|
6
|
+
Project-URL: Source, https://github.com/JaviLianes8/httpquoted
|
|
7
7
|
Project-URL: Issues, https://github.com/JaviLianes8/httpquoted/issues
|
|
8
8
|
Project-URL: Changelog, https://github.com/JaviLianes8/httpquoted/blob/main/CHANGELOG.md
|
|
9
9
|
Project-URL: LinkedIn, https://www.linkedin.com/in/jlianes/
|
|
@@ -12,7 +12,7 @@ Author-email: Javier Lianes <javier@lianes8studio.com>
|
|
|
12
12
|
License-Expression: MIT
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Keywords: comment,field-value,header,http,list,parser,quote,quoted-string,rfc7230,rfc9110,token,unquote
|
|
15
|
-
Classifier: Development Status ::
|
|
15
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
16
16
|
Classifier: Environment :: Console
|
|
17
17
|
Classifier: Intended Audience :: Developers
|
|
18
18
|
Classifier: Operating System :: OS Independent
|
|
@@ -29,14 +29,19 @@ Classifier: Typing :: Typed
|
|
|
29
29
|
Requires-Python: >=3.10
|
|
30
30
|
Provides-Extra: dev
|
|
31
31
|
Requires-Dist: build; extra == 'dev'
|
|
32
|
+
Requires-Dist: h11==0.16.0; extra == 'dev'
|
|
32
33
|
Requires-Dist: mypy; extra == 'dev'
|
|
33
34
|
Requires-Dist: pytest; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest-timeout; extra == 'dev'
|
|
34
36
|
Requires-Dist: ruff; extra == 'dev'
|
|
35
37
|
Requires-Dist: twine; extra == 'dev'
|
|
38
|
+
Requires-Dist: werkzeug==3.1.9; extra == 'dev'
|
|
36
39
|
Description-Content-Type: text/markdown
|
|
37
40
|
|
|
38
41
|
# httpquoted
|
|
39
42
|
|
|
43
|
+
[](https://lianes8studio.com/certified)
|
|
44
|
+
|
|
40
45
|
Strict, typed and zero-dependency codec for the HTTP quoted-string, comment and token
|
|
41
46
|
primitives of RFC 9110 section 5.6: safe quoting, strict or lenient parsing, scanners for
|
|
42
47
|
hand-written parsers and list splitting that respects quotes and comments.
|
|
@@ -143,8 +148,8 @@ $ httpquoted split --comments '1.0 fred (proxy, eu), 1.1 example.net'
|
|
|
143
148
|
Values come from the arguments, or one per line from standard input. `quote` takes
|
|
144
149
|
`--comment` and `--ascii-only`; `unquote` takes `--comment` and `--strict`; `split`
|
|
145
150
|
takes `--separator`, `--comments` and `--strict`. Standard input and output are read
|
|
146
|
-
and written as ISO-8859-1, so octets pass through unchanged.
|
|
147
|
-
|
|
151
|
+
and written as ISO-8859-1, so octets pass through unchanged. The exit status is 0 on
|
|
152
|
+
success, 1 when a value is rejected, 2 for a usage error and 130 when interrupted.
|
|
148
153
|
|
|
149
154
|
## API
|
|
150
155
|
|
|
@@ -184,24 +189,37 @@ parentheses.
|
|
|
184
189
|
|
|
185
190
|
## Why
|
|
186
191
|
|
|
187
|
-
The quoted-string codec of RFC 9110 sits under almost every HTTP header parser, yet
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
The quoted-string codec of RFC 9110 sits under almost every HTTP header parser, yet in
|
|
193
|
+
Python it lives in pieces inside larger packages, each with its own rules:
|
|
194
|
+
`urllib.request.parse_http_list` keeps empty items and drops the backslash of every
|
|
195
|
+
quoted-pair, `email.utils.unquote` follows the mail rules and also strips `<...>`, and
|
|
196
|
+
Werkzeug's `quote_header_value` writes a CR LF into the quoted value instead of refusing
|
|
197
|
+
it. Other ecosystems expose the primitive directly (Ruby has
|
|
198
|
+
`Protocol::HTTP::QuotedString`). This is that primitive, zero-dependency and typed, with
|
|
199
|
+
strict parsing and quoting that cannot inject a header line, for the times you parse or
|
|
200
|
+
build a header by hand. The [guide](https://github.com/JaviLianes8/httpquoted/blob/main/GUIDE.md)
|
|
201
|
+
lists how its results compare with those tools, as checked by the tests.
|
|
193
202
|
|
|
194
203
|
## Development
|
|
195
204
|
|
|
196
|
-
`scripts/check.ps1` runs format, lint, `mypy --strict` and the tests. The
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
`tests/upstream/MANIFEST.json`;
|
|
200
|
-
|
|
205
|
+
`scripts/check.ps1` runs format, lint, `mypy --strict` and the tests. The checks against
|
|
206
|
+
the RFCs, the WHATWG Fetch Standard and the CPython and Werkzeug test vectors read those
|
|
207
|
+
files from `tests/upstream/`, which is not committed: `python scripts/fetch_upstream.py`
|
|
208
|
+
downloads them and verifies the sha256 of each against `tests/upstream/MANIFEST.json`;
|
|
209
|
+
until then those tests skip. `tests/fixtures/oracle_runs.json` holds the answers of
|
|
210
|
+
Werkzeug, h11, the standard library and Node recorded by `scripts/oracle_fixture.py`.
|
|
211
|
+
Set `HTTPQUOTED_E2E=1` to build the wheel and drive it from a fresh virtual environment,
|
|
212
|
+
and `HTTPQUOTED_E2E_PYPI=1` to upgrade the PyPI release in place and compare results.
|
|
213
|
+
|
|
214
|
+
## Documentation
|
|
215
|
+
|
|
216
|
+
- [Guide](https://github.com/JaviLianes8/httpquoted/blob/main/GUIDE.md): grammar, lenient
|
|
217
|
+
and strict modes, errors, command line, comparison with other parsers
|
|
218
|
+
- [Changelog](https://github.com/JaviLianes8/httpquoted/blob/main/CHANGELOG.md)
|
|
201
219
|
|
|
202
220
|
## License
|
|
203
221
|
|
|
204
|
-
MIT. See [LICENSE](LICENSE).
|
|
222
|
+
MIT. See [LICENSE](https://github.com/JaviLianes8/httpquoted/blob/main/LICENSE).
|
|
205
223
|
|
|
206
224
|
## Links
|
|
207
225
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# httpquoted
|
|
2
2
|
|
|
3
|
+
[](https://lianes8studio.com/certified)
|
|
4
|
+
|
|
3
5
|
Strict, typed and zero-dependency codec for the HTTP quoted-string, comment and token
|
|
4
6
|
primitives of RFC 9110 section 5.6: safe quoting, strict or lenient parsing, scanners for
|
|
5
7
|
hand-written parsers and list splitting that respects quotes and comments.
|
|
@@ -106,8 +108,8 @@ $ httpquoted split --comments '1.0 fred (proxy, eu), 1.1 example.net'
|
|
|
106
108
|
Values come from the arguments, or one per line from standard input. `quote` takes
|
|
107
109
|
`--comment` and `--ascii-only`; `unquote` takes `--comment` and `--strict`; `split`
|
|
108
110
|
takes `--separator`, `--comments` and `--strict`. Standard input and output are read
|
|
109
|
-
and written as ISO-8859-1, so octets pass through unchanged.
|
|
110
|
-
|
|
111
|
+
and written as ISO-8859-1, so octets pass through unchanged. The exit status is 0 on
|
|
112
|
+
success, 1 when a value is rejected, 2 for a usage error and 130 when interrupted.
|
|
111
113
|
|
|
112
114
|
## API
|
|
113
115
|
|
|
@@ -147,24 +149,37 @@ parentheses.
|
|
|
147
149
|
|
|
148
150
|
## Why
|
|
149
151
|
|
|
150
|
-
The quoted-string codec of RFC 9110 sits under almost every HTTP header parser, yet
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
152
|
+
The quoted-string codec of RFC 9110 sits under almost every HTTP header parser, yet in
|
|
153
|
+
Python it lives in pieces inside larger packages, each with its own rules:
|
|
154
|
+
`urllib.request.parse_http_list` keeps empty items and drops the backslash of every
|
|
155
|
+
quoted-pair, `email.utils.unquote` follows the mail rules and also strips `<...>`, and
|
|
156
|
+
Werkzeug's `quote_header_value` writes a CR LF into the quoted value instead of refusing
|
|
157
|
+
it. Other ecosystems expose the primitive directly (Ruby has
|
|
158
|
+
`Protocol::HTTP::QuotedString`). This is that primitive, zero-dependency and typed, with
|
|
159
|
+
strict parsing and quoting that cannot inject a header line, for the times you parse or
|
|
160
|
+
build a header by hand. The [guide](https://github.com/JaviLianes8/httpquoted/blob/main/GUIDE.md)
|
|
161
|
+
lists how its results compare with those tools, as checked by the tests.
|
|
156
162
|
|
|
157
163
|
## Development
|
|
158
164
|
|
|
159
|
-
`scripts/check.ps1` runs format, lint, `mypy --strict` and the tests. The
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
`tests/upstream/MANIFEST.json`;
|
|
163
|
-
|
|
165
|
+
`scripts/check.ps1` runs format, lint, `mypy --strict` and the tests. The checks against
|
|
166
|
+
the RFCs, the WHATWG Fetch Standard and the CPython and Werkzeug test vectors read those
|
|
167
|
+
files from `tests/upstream/`, which is not committed: `python scripts/fetch_upstream.py`
|
|
168
|
+
downloads them and verifies the sha256 of each against `tests/upstream/MANIFEST.json`;
|
|
169
|
+
until then those tests skip. `tests/fixtures/oracle_runs.json` holds the answers of
|
|
170
|
+
Werkzeug, h11, the standard library and Node recorded by `scripts/oracle_fixture.py`.
|
|
171
|
+
Set `HTTPQUOTED_E2E=1` to build the wheel and drive it from a fresh virtual environment,
|
|
172
|
+
and `HTTPQUOTED_E2E_PYPI=1` to upgrade the PyPI release in place and compare results.
|
|
173
|
+
|
|
174
|
+
## Documentation
|
|
175
|
+
|
|
176
|
+
- [Guide](https://github.com/JaviLianes8/httpquoted/blob/main/GUIDE.md): grammar, lenient
|
|
177
|
+
and strict modes, errors, command line, comparison with other parsers
|
|
178
|
+
- [Changelog](https://github.com/JaviLianes8/httpquoted/blob/main/CHANGELOG.md)
|
|
164
179
|
|
|
165
180
|
## License
|
|
166
181
|
|
|
167
|
-
MIT. See [LICENSE](LICENSE).
|
|
182
|
+
MIT. See [LICENSE](https://github.com/JaviLianes8/httpquoted/blob/main/LICENSE).
|
|
168
183
|
|
|
169
184
|
## Links
|
|
170
185
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "httpquoted"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "1.0.0"
|
|
8
8
|
description = "Strict, typed and zero-dependency codec for HTTP quoted-strings, comments and tokens (RFC 9110): safe quoting, strict parsing, scanners and quote-aware list splitting"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -13,7 +13,7 @@ license-files = ["LICENSE"]
|
|
|
13
13
|
authors = [{ name = "Javier Lianes", email = "javier@lianes8studio.com" }]
|
|
14
14
|
keywords = ["http", "header", "quoted-string", "quote", "unquote", "comment", "token", "field-value", "list", "parser", "rfc9110", "rfc7230"]
|
|
15
15
|
classifiers = [
|
|
16
|
-
"Development Status ::
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
17
|
"Intended Audience :: Developers",
|
|
18
18
|
"Programming Language :: Python :: 3",
|
|
19
19
|
"Programming Language :: Python :: 3 :: Only",
|
|
@@ -31,14 +31,14 @@ classifiers = [
|
|
|
31
31
|
dependencies = []
|
|
32
32
|
|
|
33
33
|
[project.optional-dependencies]
|
|
34
|
-
dev = ["ruff", "mypy", "pytest", "build", "twine"]
|
|
34
|
+
dev = ["ruff", "mypy", "pytest", "pytest-timeout", "build", "twine", "werkzeug==3.1.9", "h11==0.16.0"]
|
|
35
35
|
|
|
36
36
|
[project.scripts]
|
|
37
37
|
httpquoted = "httpquoted._cli:main"
|
|
38
38
|
|
|
39
39
|
[project.urls]
|
|
40
40
|
Homepage = "https://lianes8studio.com"
|
|
41
|
-
|
|
41
|
+
Source = "https://github.com/JaviLianes8/httpquoted"
|
|
42
42
|
Issues = "https://github.com/JaviLianes8/httpquoted/issues"
|
|
43
43
|
Changelog = "https://github.com/JaviLianes8/httpquoted/blob/main/CHANGELOG.md"
|
|
44
44
|
LinkedIn = "https://www.linkedin.com/in/jlianes/"
|
|
@@ -48,12 +48,21 @@ Funding = "https://buymeacoffee.com/jlianesglrs"
|
|
|
48
48
|
packages = ["src/httpquoted"]
|
|
49
49
|
|
|
50
50
|
[tool.hatch.build.targets.sdist]
|
|
51
|
+
# An explicit list, so a local virtual environment or scratch file is never packed.
|
|
51
52
|
# Third-party reference files are fetched locally and never shipped; only their manifest is.
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
53
|
+
only-include = [
|
|
54
|
+
"src",
|
|
55
|
+
"tests",
|
|
56
|
+
"scripts/fetch_upstream.py",
|
|
57
|
+
"scripts/oracle_fixture.py",
|
|
58
|
+
"README.md",
|
|
59
|
+
"GUIDE.md",
|
|
60
|
+
"CHANGELOG.md",
|
|
61
|
+
"CONTRIBUTING.md",
|
|
62
|
+
"LICENSE",
|
|
63
|
+
"requirements-dev.txt",
|
|
56
64
|
]
|
|
65
|
+
exclude = ["tests/upstream/*.txt", "__pycache__"]
|
|
57
66
|
|
|
58
67
|
[tool.ruff]
|
|
59
68
|
line-length = 100
|
|
@@ -70,3 +79,4 @@ strict = true
|
|
|
70
79
|
testpaths = ["tests"]
|
|
71
80
|
pythonpath = ["src"]
|
|
72
81
|
addopts = "-q"
|
|
82
|
+
timeout = 300
|