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.
Files changed (52) hide show
  1. httpquoted-1.0.0/CHANGELOG.md +83 -0
  2. {httpquoted-0.2.0 → httpquoted-1.0.0}/CONTRIBUTING.md +19 -19
  3. httpquoted-1.0.0/GUIDE.md +149 -0
  4. {httpquoted-0.2.0 → httpquoted-1.0.0}/LICENSE +21 -21
  5. {httpquoted-0.2.0 → httpquoted-1.0.0}/PKG-INFO +35 -17
  6. {httpquoted-0.2.0 → httpquoted-1.0.0}/README.md +29 -14
  7. {httpquoted-0.2.0 → httpquoted-1.0.0}/pyproject.toml +18 -8
  8. httpquoted-1.0.0/scripts/oracle_fixture.py +125 -0
  9. {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/__init__.py +19 -3
  10. {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/_chars.py +29 -10
  11. {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/_cli.py +81 -16
  12. {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/_comment.py +15 -3
  13. {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/_quote.py +25 -4
  14. {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/_scan.py +25 -7
  15. {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/_split.py +21 -1
  16. {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/_token.py +8 -0
  17. httpquoted-1.0.0/tests/fixtures/oracle_runs.json +9436 -0
  18. {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_contract.py +5 -0
  19. httpquoted-1.0.0/tests/test_contract_api.py +140 -0
  20. httpquoted-1.0.0/tests/test_contract_blind.py +66 -0
  21. httpquoted-1.0.0/tests/test_contract_cli.py +194 -0
  22. httpquoted-1.0.0/tests/test_contract_hostile.py +166 -0
  23. {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_docs.py +22 -0
  24. httpquoted-1.0.0/tests/test_e2e_upgrade.py +108 -0
  25. {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_e2e_wheel.py +19 -1
  26. httpquoted-1.0.0/tests/test_examples.py +115 -0
  27. httpquoted-1.0.0/tests/test_oracle.py +132 -0
  28. httpquoted-1.0.0/tests/test_whatwg.py +120 -0
  29. httpquoted-1.0.0/tests/upstream/MANIFEST.json +83 -0
  30. httpquoted-1.0.0/tests/whatwg_reference.py +64 -0
  31. httpquoted-0.2.0/.gitattributes +0 -1
  32. httpquoted-0.2.0/CHANGELOG.md +0 -37
  33. httpquoted-0.2.0/scripts/check.ps1 +0 -16
  34. httpquoted-0.2.0/scripts/release.ps1 +0 -28
  35. httpquoted-0.2.0/tests/upstream/MANIFEST.json +0 -29
  36. {httpquoted-0.2.0 → httpquoted-1.0.0}/.gitignore +0 -0
  37. {httpquoted-0.2.0 → httpquoted-1.0.0}/requirements-dev.txt +0 -0
  38. {httpquoted-0.2.0 → httpquoted-1.0.0}/scripts/fetch_upstream.py +0 -0
  39. {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/__main__.py +0 -0
  40. {httpquoted-0.2.0 → httpquoted-1.0.0}/src/httpquoted/py.typed +0 -0
  41. {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/conftest.py +0 -0
  42. {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_bytes.py +0 -0
  43. {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_cli.py +0 -0
  44. {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_comment.py +0 -0
  45. {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_hostile.py +0 -0
  46. {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_properties.py +0 -0
  47. {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_quote.py +0 -0
  48. {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_review_fixes.py +0 -0
  49. {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_scan.py +0 -0
  50. {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_sources.py +0 -0
  51. {httpquoted-0.2.0 → httpquoted-1.0.0}/tests/test_split.py +0 -0
  52. {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.2.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: Repository, https://github.com/JaviLianes8/httpquoted
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 :: 4 - Beta
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
+ [![Lianes8 Certified](https://lianes8studio.com/certified/lianes8-certified-badge.svg)](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. A rejected value exits with
147
- status 1.
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 no
188
- small typed library ships it on its own: the stdlib `quopri` handles MIME
189
- quoted-printable (a different thing), and the logic is vendored privately into
190
- frameworks and SDKs, often without the checks that stop header injection. Other
191
- ecosystems expose it directly (Ruby has `Protocol::HTTP::QuotedString`). This is that
192
- primitive, zero-dependency and typed, for the times you parse or build a header by hand.
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 RFC checks
197
- read the RFCs from `tests/upstream/`, which is not committed: `python
198
- scripts/fetch_upstream.py` downloads them and verifies the sha256 of each against
199
- `tests/upstream/MANIFEST.json`; until then those tests skip. Set `HTTPQUOTED_E2E=1` to
200
- build the wheel and drive it from a fresh virtual environment.
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
+ [![Lianes8 Certified](https://lianes8studio.com/certified/lianes8-certified-badge.svg)](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. A rejected value exits with
110
- status 1.
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 no
151
- small typed library ships it on its own: the stdlib `quopri` handles MIME
152
- quoted-printable (a different thing), and the logic is vendored privately into
153
- frameworks and SDKs, often without the checks that stop header injection. Other
154
- ecosystems expose it directly (Ruby has `Protocol::HTTP::QuotedString`). This is that
155
- primitive, zero-dependency and typed, for the times you parse or build a header by hand.
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 RFC checks
160
- read the RFCs from `tests/upstream/`, which is not committed: `python
161
- scripts/fetch_upstream.py` downloads them and verifies the sha256 of each against
162
- `tests/upstream/MANIFEST.json`; until then those tests skip. Set `HTTPQUOTED_E2E=1` to
163
- build the wheel and drive it from a fresh virtual environment.
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.2.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 :: 4 - Beta",
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
- Repository = "https://github.com/JaviLianes8/httpquoted"
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
- exclude = [
53
- ".github",
54
- "IMPROVEMENTS-round*.md",
55
- "tests/upstream/*.txt",
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