http-sf 1.2.0__tar.gz → 1.2.1__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 (37) hide show
  1. {http_sf-1.2.0 → http_sf-1.2.1}/PKG-INFO +2 -1
  2. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/__init__.py +11 -12
  3. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/__main__.py +0 -1
  4. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/bare_item.py +6 -6
  5. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/byteseq.py +2 -1
  6. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/date.py +9 -3
  7. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/dictionary.py +2 -3
  8. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/innerlist.py +3 -4
  9. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/integer.py +3 -1
  10. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/item.py +2 -2
  11. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/list.py +0 -1
  12. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/parameters.py +1 -1
  13. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/token.py +2 -1
  14. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/types.py +2 -1
  15. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/util.py +8 -5
  16. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf.egg-info/PKG-INFO +2 -1
  17. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf.egg-info/SOURCES.txt +1 -0
  18. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf.egg-info/requires.txt +1 -0
  19. {http_sf-1.2.0 → http_sf-1.2.1}/pyproject.toml +5 -1
  20. {http_sf-1.2.0 → http_sf-1.2.1}/test/test.py +5 -1
  21. http_sf-1.2.1/test/test_error_contract.py +69 -0
  22. {http_sf-1.2.0 → http_sf-1.2.1}/MANIFEST.in +0 -0
  23. {http_sf-1.2.0 → http_sf-1.2.1}/README.md +0 -0
  24. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/boolean.py +1 -1
  25. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/decimal.py +0 -0
  26. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/display_string.py +1 -1
  27. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/errors.py +0 -0
  28. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/py.typed +0 -0
  29. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/retrofit.py +0 -0
  30. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/state.py +0 -0
  31. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf/string.py +1 -1
  32. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf.egg-info/dependency_links.txt +0 -0
  33. {http_sf-1.2.0 → http_sf-1.2.1}/http_sf.egg-info/top_level.txt +0 -0
  34. {http_sf-1.2.0 → http_sf-1.2.1}/setup.cfg +0 -0
  35. {http_sf-1.2.0 → http_sf-1.2.1}/test/test_callback.py +0 -0
  36. {http_sf-1.2.0 → http_sf-1.2.1}/test/test_perf.py +0 -0
  37. {http_sf-1.2.0 → http_sf-1.2.1}/test/test_position.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: http-sf
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Parse and serialise HTTP Structured Fields
5
5
  Author-email: Mark Nottingham <mnot@mnot.net>
6
6
  License-Expression: MIT
@@ -13,6 +13,7 @@ Requires-Dist: typing_extensions
13
13
  Provides-Extra: dev
14
14
  Requires-Dist: mypy; extra == "dev"
15
15
  Requires-Dist: black; extra == "dev"
16
+ Requires-Dist: isort; extra == "dev"
16
17
  Requires-Dist: pylint; extra == "dev"
17
18
  Requires-Dist: pytest; extra == "dev"
18
19
  Requires-Dist: pytest-md; extra == "dev"
@@ -25,28 +25,27 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25
25
  THE SOFTWARE.
26
26
  """
27
27
 
28
- __version__ = "1.2.0"
28
+ __version__ = "1.2.1"
29
29
 
30
- from typing import Tuple, List, Dict, Optional, Callable
30
+ from typing import Callable, Dict, List, Optional, Tuple
31
31
 
32
32
  from http_sf.dictionary import parse_dictionary, ser_dictionary
33
+ from http_sf.errors import StructuredFieldError
33
34
  from http_sf.item import parse_item, ser_item
34
35
  from http_sf.list import parse_list, ser_list
35
36
  from http_sf.retrofit import retrofit
37
+ from http_sf.state import ParserState
36
38
  from http_sf.types import (
37
- StructuredType,
38
- Token,
39
- DisplayString,
40
- OnDuplicateKeyType,
41
39
  DictionaryType,
42
- ListType,
43
- ItemType,
40
+ DisplayString,
44
41
  InnerListType,
42
+ ItemType,
43
+ ListType,
44
+ OnDuplicateKeyType,
45
+ StructuredType,
46
+ Token,
45
47
  )
46
- from http_sf.util import discard_ows, to_json, from_json
47
- from http_sf.state import ParserState
48
- from http_sf.errors import StructuredFieldError
49
-
48
+ from http_sf.util import discard_ows, from_json, to_json
50
49
 
51
50
  __all__ = [
52
51
  "parse",
@@ -5,7 +5,6 @@ import sys
5
5
 
6
6
  from . import parse, to_json
7
7
 
8
-
9
8
  parser = argparse.ArgumentParser(
10
9
  description="Validate and show data model of a Structured Field Value."
11
10
  )
@@ -1,16 +1,16 @@
1
1
  from datetime import datetime
2
2
  from decimal import Decimal
3
- from typing import Dict, Callable, Any, Type
3
+ from typing import Any, Callable, Dict, Type
4
4
 
5
5
  from http_sf.boolean import parse_boolean, ser_boolean
6
- from http_sf.byteseq import parse_byteseq, ser_byteseq, BYTE_DELIMIT
6
+ from http_sf.byteseq import BYTE_DELIMIT, parse_byteseq, ser_byteseq
7
7
  from http_sf.date import parse_date, ser_date
8
8
  from http_sf.decimal import ser_decimal
9
9
  from http_sf.display_string import parse_display_string, ser_display_string
10
- from http_sf.integer import parse_number, ser_integer, NUMBER_START_CHARS
11
- from http_sf.string import parse_string, ser_string, DQUOTE
12
- from http_sf.token import parse_token, ser_token, TOKEN_START_CHARS
13
- from http_sf.types import BareItemType, Token, DisplayString
10
+ from http_sf.integer import NUMBER_START_CHARS, parse_number, ser_integer
11
+ from http_sf.string import DQUOTE, parse_string, ser_string
12
+ from http_sf.token import TOKEN_START_CHARS, parse_token, ser_token
13
+ from http_sf.types import BareItemType, DisplayString, Token
14
14
 
15
15
  from .errors import StructuredFieldError
16
16
  from .state import ParserState
@@ -1,8 +1,9 @@
1
1
  import base64
2
2
  import binascii
3
3
  from string import ascii_letters, digits
4
- from .state import ParserState
4
+
5
5
  from .errors import StructuredFieldError
6
+ from .state import ParserState
6
7
 
7
8
  BYTE_DELIMIT = ord(b":")
8
9
  B64CONTENT = set((ascii_letters + digits + "+/=").encode("ascii"))
@@ -1,7 +1,8 @@
1
- from datetime import datetime
2
- from .state import ParserState
1
+ from datetime import datetime, timezone
2
+
3
3
  from .errors import StructuredFieldError
4
4
  from .integer import parse_integer
5
+ from .state import ParserState
5
6
 
6
7
 
7
8
  def parse_date(state: ParserState) -> datetime:
@@ -11,7 +12,12 @@ def parse_date(state: ParserState) -> datetime:
11
12
  raise StructuredFieldError(
12
13
  "Non-integer Date", position=state.cursor, offending_char=None
13
14
  )
14
- return datetime.fromtimestamp(value)
15
+ try:
16
+ return datetime.fromtimestamp(value, tz=timezone.utc)
17
+ except (ValueError, OSError) as why:
18
+ raise StructuredFieldError(
19
+ "Date value out of range", position=state.cursor, offending_char=None
20
+ ) from why
15
21
 
16
22
 
17
23
  def ser_date(inval: datetime) -> str:
@@ -1,12 +1,11 @@
1
- from typing import cast, Optional
1
+ from typing import Optional, cast
2
2
 
3
3
  from .errors import StructuredFieldError
4
4
  from .innerlist import parse_item_or_inner_list, ser_item_or_inner_list
5
5
  from .parameters import parse_params, ser_params
6
6
  from .state import ParserState
7
7
  from .types import DictionaryType, ItemType, OnDuplicateKeyType
8
- from .util import discard_http_ows, ser_key, parse_key
9
-
8
+ from .util import discard_http_ows, parse_key, ser_key
10
9
 
11
10
  EQUALS = ord(b"=")
12
11
  COMMA = ord(b",")
@@ -1,4 +1,4 @@
1
- from typing import List, cast, Union, Optional
1
+ from typing import List, Optional, Union, cast
2
2
 
3
3
  from .errors import StructuredFieldError
4
4
  from .item import parse_item, ser_item
@@ -6,14 +6,13 @@ from .parameters import parse_params, ser_params
6
6
  from .state import ParserState
7
7
  from .types import (
8
8
  InnerListType,
9
- ItemType,
10
- ParamsType,
11
9
  ItemOrInnerListType,
10
+ ItemType,
12
11
  OnDuplicateKeyType,
12
+ ParamsType,
13
13
  )
14
14
  from .util import discard_ows
15
15
 
16
-
17
16
  PAREN_OPEN = ord(b"(")
18
17
  PAREN_CLOSE = ord(b")")
19
18
  INNERLIST_DELIMS = set(b" )")
@@ -61,7 +61,9 @@ def parse_number(state: ParserState) -> Union[int, Decimal]:
61
61
  elif _type is INTEGER and char == PERIOD:
62
62
  # spec says 12, but character is appended afterwards
63
63
  if num_length > 13:
64
- raise ValueError("Decimal too long.")
64
+ raise StructuredFieldError(
65
+ "Decimal too long.", position=state.cursor, offending_char=None
66
+ )
65
67
  state.cursor += 1
66
68
  num_length = state.cursor - num_start - 1
67
69
  if char in DIGITS:
@@ -1,9 +1,9 @@
1
- from typing import Tuple, Optional
1
+ from typing import Optional, Tuple
2
2
 
3
3
  from .bare_item import parse_bare_item, ser_bare_item
4
4
  from .parameters import parse_params, ser_params
5
5
  from .state import ParserState
6
- from .types import BareItemType, ItemType, ParamsType, OnDuplicateKeyType
6
+ from .types import BareItemType, ItemType, OnDuplicateKeyType, ParamsType
7
7
 
8
8
  PAREN_OPEN = ord(b"(")
9
9
  SEMICOLON = ord(b";")
@@ -6,7 +6,6 @@ from .state import ParserState
6
6
  from .types import ListType, OnDuplicateKeyType
7
7
  from .util import discard_http_ows
8
8
 
9
-
10
9
  COMMA = ord(b",")
11
10
 
12
11
 
@@ -3,7 +3,7 @@ from typing import Optional
3
3
  from .bare_item import parse_bare_item, ser_bare_item
4
4
  from .errors import StructuredFieldError
5
5
  from .state import ParserState
6
- from .types import BareItemType, ParamsType, OnDuplicateKeyType
6
+ from .types import BareItemType, OnDuplicateKeyType, ParamsType
7
7
  from .util import discard_ows, parse_key, ser_key
8
8
 
9
9
  PAREN_OPEN = ord(b"(")
@@ -1,6 +1,7 @@
1
1
  from string import ascii_letters, digits
2
- from http_sf.types import Token
2
+
3
3
  from http_sf.state import ParserState
4
+ from http_sf.types import Token
4
5
 
5
6
  TOKEN_START_CHARS = set((ascii_letters + "*").encode("ascii"))
6
7
  TOKEN_CHARS = set((ascii_letters + digits + ":/!#$%&'*+-.^_`|~").encode("ascii"))
@@ -1,7 +1,8 @@
1
1
  from collections import UserString
2
2
  from datetime import datetime
3
3
  from decimal import Decimal
4
- from typing import Union, Dict, List, Tuple, Callable, Any
4
+ from typing import Any, Callable, Dict, List, Tuple, Union
5
+
5
6
  from typing_extensions import TypeAlias
6
7
 
7
8
 
@@ -1,14 +1,14 @@
1
1
  import base64
2
- from datetime import datetime
3
- from decimal import Decimal
4
2
  import json
5
3
  import re
4
+ from datetime import datetime, timezone
5
+ from decimal import Decimal
6
6
  from string import ascii_lowercase, ascii_uppercase, digits
7
7
  from typing import Any
8
8
 
9
- from .types import StructuredType, Token, DisplayString, JsonDict
10
- from .state import ParserState
11
9
  from .errors import StructuredFieldError
10
+ from .state import ParserState
11
+ from .types import DisplayString, JsonDict, StructuredType, Token
12
12
 
13
13
  SPACE = ord(b" ")
14
14
  HTTP_OWS = set(b" \t")
@@ -115,7 +115,10 @@ def json_load(inobj: JsonDict) -> Any:
115
115
  if objtype == "binary":
116
116
  return base64.b32decode(inobj["value"])
117
117
  if objtype == "date":
118
- return datetime.fromtimestamp(inobj["value"])
118
+ try:
119
+ return datetime.fromtimestamp(inobj["value"], tz=timezone.utc)
120
+ except (ValueError, OSError):
121
+ return int(inobj["value"])
119
122
  if objtype == "displaystring":
120
123
  return DisplayString(inobj["value"])
121
124
  raise ValueError(f"Unknown object type - {inobj}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: http-sf
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Parse and serialise HTTP Structured Fields
5
5
  Author-email: Mark Nottingham <mnot@mnot.net>
6
6
  License-Expression: MIT
@@ -13,6 +13,7 @@ Requires-Dist: typing_extensions
13
13
  Provides-Extra: dev
14
14
  Requires-Dist: mypy; extra == "dev"
15
15
  Requires-Dist: black; extra == "dev"
16
+ Requires-Dist: isort; extra == "dev"
16
17
  Requires-Dist: pylint; extra == "dev"
17
18
  Requires-Dist: pytest; extra == "dev"
18
19
  Requires-Dist: pytest-md; extra == "dev"
@@ -30,5 +30,6 @@ http_sf.egg-info/requires.txt
30
30
  http_sf.egg-info/top_level.txt
31
31
  test/test.py
32
32
  test/test_callback.py
33
+ test/test_error_contract.py
33
34
  test/test_perf.py
34
35
  test/test_position.py
@@ -3,6 +3,7 @@ typing_extensions
3
3
  [dev]
4
4
  mypy
5
5
  black
6
+ isort
6
7
  pylint
7
8
  pytest
8
9
  pytest-md
@@ -19,7 +19,7 @@ dependencies = [
19
19
  ]
20
20
 
21
21
  [project.optional-dependencies]
22
- dev = ["mypy", "black", "pylint", "pytest", "pytest-md", "validate-pyproject", "build"]
22
+ dev = ["mypy", "black", "isort", "pylint", "pytest", "pytest-md", "validate-pyproject", "build"]
23
23
 
24
24
  [project.urls]
25
25
  homepage = "https://github.com/mnot/http-sf"
@@ -40,6 +40,7 @@ version = {attr = "http_sf.__version__"}
40
40
  [tool.setuptools.packages.find]
41
41
  where = ["."]
42
42
  include = ["http_sf", "http_sf.*"]
43
+ exclude = ["build*", "dist*"]
43
44
  namespaces = false
44
45
 
45
46
  [tool.setuptools]
@@ -80,3 +81,6 @@ min-public-methods=1
80
81
 
81
82
  [tool.pylint.similarities]
82
83
  min-similarity-lines = 10
84
+
85
+ [tool.isort]
86
+ profile = "black"
@@ -54,7 +54,11 @@ def run_suite(suite_name: str, suite: List) -> None:
54
54
  if ser_success:
55
55
  suite_passed += 1
56
56
  else:
57
- print(f"{FAIL} * {test['name']}: SERIALISE FAIL{ENDC}")
57
+ if test.get("can_fail", False):
58
+ print(f"{WARN} * {test['name']}: SERIALISE FAIL (non-critical){ENDC}")
59
+ suite_passed += 1
60
+ else:
61
+ print(f"{FAIL} * {test['name']}: SERIALISE FAIL{ENDC}")
58
62
  print(f" - expected: {ser_expected}")
59
63
  print(f" - got: ['{serialised}']")
60
64
  if ser_fail_reason:
@@ -0,0 +1,69 @@
1
+ """
2
+ Tests that all parse errors raise StructuredFieldError, never bare ValueError
3
+ or OSError, regardless of input. Covers cases where the SF integer range
4
+ permits values that downstream Python APIs reject.
5
+ """
6
+ import sys
7
+ from http_sf import parse, StructuredFieldError
8
+
9
+
10
+ def test_date_out_of_range():
11
+ print("Testing out-of-range Date values raise StructuredFieldError...")
12
+
13
+ # Max SF integer — year ~31 million, far outside datetime's year 1-9999 range
14
+ try:
15
+ parse(b"@999999999999999", tltype="item")
16
+ print(" Test 1: FAIL (no exception raised)")
17
+ sys.exit(1)
18
+ except StructuredFieldError:
19
+ print(" Test 1: OK (large positive timestamp)")
20
+ except (ValueError, OSError) as e:
21
+ print(f" Test 1: FAIL (wrong exception type {type(e).__name__}: {e})")
22
+ sys.exit(1)
23
+
24
+ # Min SF integer — far before year 1
25
+ try:
26
+ parse(b"@-999999999999999", tltype="item")
27
+ print(" Test 2: FAIL (no exception raised)")
28
+ sys.exit(1)
29
+ except StructuredFieldError:
30
+ print(" Test 2: OK (large negative timestamp)")
31
+ except (ValueError, OSError) as e:
32
+ print(f" Test 2: FAIL (wrong exception type {type(e).__name__}: {e})")
33
+ sys.exit(1)
34
+
35
+ print("All Date out-of-range tests passed.")
36
+
37
+
38
+ def test_decimal_too_long():
39
+ print("Testing overlong decimal integer part raises StructuredFieldError...")
40
+
41
+ # 14 digits before decimal point — exceeds the 12-digit SF limit
42
+ try:
43
+ parse(b"12345678901234.1", tltype="item")
44
+ print(" Test 1: FAIL (no exception raised)")
45
+ sys.exit(1)
46
+ except StructuredFieldError:
47
+ print(" Test 1: OK (14-digit integer part)")
48
+ except ValueError as e:
49
+ print(f" Test 1: FAIL (wrong exception type ValueError: {e})")
50
+ sys.exit(1)
51
+
52
+ # Exactly at the boundary — 13 digits before decimal (also invalid per spec)
53
+ try:
54
+ parse(b"1234567890123.1", tltype="item")
55
+ print(" Test 2: FAIL (no exception raised)")
56
+ sys.exit(1)
57
+ except StructuredFieldError:
58
+ print(" Test 2: OK (13-digit integer part)")
59
+ except ValueError as e:
60
+ print(f" Test 2: FAIL (wrong exception type ValueError: {e})")
61
+ sys.exit(1)
62
+
63
+ print("All Decimal too-long tests passed.")
64
+
65
+
66
+ if __name__ == "__main__":
67
+ test_date_out_of_range()
68
+ print()
69
+ test_decimal_too_long()
File without changes
File without changes
@@ -1,5 +1,5 @@
1
- from .state import ParserState
2
1
  from .errors import StructuredFieldError
2
+ from .state import ParserState
3
3
 
4
4
  QUESTION = ord(b"?")
5
5
  ONE = ord(b"1")
File without changes
@@ -1,5 +1,5 @@
1
- from .state import ParserState
2
1
  from .errors import StructuredFieldError
2
+ from .state import ParserState
3
3
  from .types import DisplayString
4
4
 
5
5
  PERCENT = ord("%")
File without changes
File without changes
File without changes
File without changes
@@ -1,5 +1,5 @@
1
- from .state import ParserState
2
1
  from .errors import StructuredFieldError
2
+ from .state import ParserState
3
3
 
4
4
  DQUOTE = ord('"')
5
5
  BACKSLASH = ord("\\")
File without changes
File without changes
File without changes
File without changes