jep-cli 0.7.1__tar.gz → 0.7.2__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.
- {jep_cli-0.7.1 → jep_cli-0.7.2}/NOTICE.md +1 -1
- {jep_cli-0.7.1/jep_cli.egg-info → jep_cli-0.7.2}/PKG-INFO +2 -1
- jep_cli-0.7.2/jep_cli/__init__.py +3 -0
- {jep_cli-0.7.1 → jep_cli-0.7.2}/jep_cli/client.py +5 -3
- {jep_cli-0.7.1 → jep_cli-0.7.2}/jep_cli/main.py +14 -5
- jep_cli-0.7.2/jep_cli/strict_json.py +78 -0
- {jep_cli-0.7.1 → jep_cli-0.7.2/jep_cli.egg-info}/PKG-INFO +2 -1
- {jep_cli-0.7.1 → jep_cli-0.7.2}/jep_cli.egg-info/SOURCES.txt +3 -1
- {jep_cli-0.7.1 → jep_cli-0.7.2}/jep_cli.egg-info/requires.txt +1 -0
- {jep_cli-0.7.1 → jep_cli-0.7.2}/pyproject.toml +2 -2
- jep_cli-0.7.2/tests/test_strict_json.py +87 -0
- jep_cli-0.7.1/jep_cli/__init__.py +0 -3
- {jep_cli-0.7.1 → jep_cli-0.7.2}/LICENSE +0 -0
- {jep_cli-0.7.1 → jep_cli-0.7.2}/README.md +0 -0
- {jep_cli-0.7.1 → jep_cli-0.7.2}/jep_cli.egg-info/dependency_links.txt +0 -0
- {jep_cli-0.7.1 → jep_cli-0.7.2}/jep_cli.egg-info/entry_points.txt +0 -0
- {jep_cli-0.7.1 → jep_cli-0.7.2}/jep_cli.egg-info/top_level.txt +0 -0
- {jep_cli-0.7.1 → jep_cli-0.7.2}/setup.cfg +0 -0
- {jep_cli-0.7.1 → jep_cli-0.7.2}/tests/test_acceptance.py +0 -0
- {jep_cli-0.7.1 → jep_cli-0.7.2}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Notice
|
|
2
2
|
|
|
3
|
-
This repository contains an experimental CLI seed for the JEP
|
|
3
|
+
This repository contains an experimental CLI seed for the JEP reference API. Supported protocol revisions are documented in README.md.
|
|
4
4
|
|
|
5
5
|
Internet-Draft text and excerpts are governed by the IETF Trust Legal Provisions and BCP 78 / BCP 79.
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jep-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Command-line client for JEP Core 0.7 with explicit legacy compatibility
|
|
5
5
|
Author-email: HJS Spec <signal@humanjudgment.org>
|
|
6
6
|
License: MIT
|
|
@@ -15,6 +15,7 @@ Requires-Python: >=3.9
|
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
License-File: NOTICE.md
|
|
18
|
+
Requires-Dist: rfc8785==0.1.4
|
|
18
19
|
Provides-Extra: dev
|
|
19
20
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
20
21
|
Dynamic: license-file
|
|
@@ -6,6 +6,8 @@ import json
|
|
|
6
6
|
from typing import Any, Dict, Optional
|
|
7
7
|
from urllib import request, error
|
|
8
8
|
|
|
9
|
+
from . import strict_json
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
class JEPCLIError(Exception):
|
|
11
13
|
pass
|
|
@@ -44,19 +46,19 @@ class JEPClient:
|
|
|
44
46
|
data = None
|
|
45
47
|
headers = {
|
|
46
48
|
"content-type": "application/json",
|
|
47
|
-
"user-agent": "JEP-CLI/0.7.
|
|
49
|
+
"user-agent": "JEP-CLI/0.7.2",
|
|
48
50
|
}
|
|
49
51
|
if self.api_key:
|
|
50
52
|
headers["authorization"] = f"Bearer {self.api_key}"
|
|
51
53
|
headers["x-api-key"] = self.api_key
|
|
52
54
|
if payload is not None:
|
|
53
|
-
data =
|
|
55
|
+
data = strict_json.dumps(payload).encode("utf-8")
|
|
54
56
|
|
|
55
57
|
req = request.Request(self.base_url + path, data=data, headers=headers, method=method)
|
|
56
58
|
try:
|
|
57
59
|
with request.urlopen(req, timeout=self.timeout) as resp:
|
|
58
60
|
body = resp.read().decode("utf-8")
|
|
59
|
-
return
|
|
61
|
+
return strict_json.loads(body) if body else {}
|
|
60
62
|
except error.HTTPError as exc:
|
|
61
63
|
body = exc.read().decode("utf-8")
|
|
62
64
|
try:
|
|
@@ -10,6 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
from typing import Any, Dict
|
|
11
11
|
|
|
12
12
|
from .client import JEPClient, JEPCLIError
|
|
13
|
+
from . import strict_json
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
VALID_VERBS = {"J", "D", "T", "V"}
|
|
@@ -17,12 +18,16 @@ VALID_VERBS = {"J", "D", "T", "V"}
|
|
|
17
18
|
|
|
18
19
|
def parse_json_value(value: str) -> Any:
|
|
19
20
|
if value == "-":
|
|
20
|
-
|
|
21
|
+
# Read bytes when available: a text stream may replace malformed UTF-8.
|
|
22
|
+
stream = getattr(sys.stdin, "buffer", sys.stdin)
|
|
23
|
+
return strict_json.loads(stream.read())
|
|
21
24
|
|
|
22
25
|
try:
|
|
23
|
-
return
|
|
26
|
+
return strict_json.loads(value)
|
|
24
27
|
except json.JSONDecodeError:
|
|
25
|
-
|
|
28
|
+
# JSON-looking input must not become a plain string after a parse error.
|
|
29
|
+
if value.lstrip().startswith(("{", "[", '"')):
|
|
30
|
+
raise
|
|
26
31
|
# Inline event JSON may be longer than the filesystem filename limit.
|
|
27
32
|
path = Path(value)
|
|
28
33
|
try:
|
|
@@ -30,7 +35,8 @@ def parse_json_value(value: str) -> Any:
|
|
|
30
35
|
except OSError:
|
|
31
36
|
is_file = False
|
|
32
37
|
if is_file:
|
|
33
|
-
return
|
|
38
|
+
return strict_json.loads(path.read_bytes())
|
|
39
|
+
strict_json.validate(value)
|
|
34
40
|
return value
|
|
35
41
|
|
|
36
42
|
|
|
@@ -40,12 +46,15 @@ def parse_ext_items(items: list[str]) -> Dict[str, Any]:
|
|
|
40
46
|
if "=" not in item:
|
|
41
47
|
raise ValueError(f"extension must be key=json_or_string, got: {item}")
|
|
42
48
|
key, raw = item.split("=", 1)
|
|
49
|
+
if key in ext:
|
|
50
|
+
raise ValueError(f"duplicate extension: {key!r}")
|
|
51
|
+
strict_json.validate(key)
|
|
43
52
|
ext[key] = parse_json_value(raw)
|
|
44
53
|
return ext
|
|
45
54
|
|
|
46
55
|
|
|
47
56
|
def print_json(data: Any) -> None:
|
|
48
|
-
print(
|
|
57
|
+
print(strict_json.dumps(data))
|
|
49
58
|
|
|
50
59
|
|
|
51
60
|
def build_client(args: argparse.Namespace) -> JEPClient:
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Strict JSON input boundary; signature/profile validation stays with the API."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import rfc8785
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _pairs(pairs):
|
|
12
|
+
result = {}
|
|
13
|
+
for key, value in pairs:
|
|
14
|
+
if key in result:
|
|
15
|
+
raise ValueError(f"duplicate JSON member: {key!r}")
|
|
16
|
+
result[key] = value
|
|
17
|
+
return result
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _constant(token):
|
|
21
|
+
raise ValueError(f"non-JSON numeric constant: {token}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate(value: Any) -> None:
|
|
25
|
+
"""Reject lossy/invalid input without rewriting values or signed members.
|
|
26
|
+
|
|
27
|
+
Large integer policy matches Core 0.7.4: exact binary64 integers and their
|
|
28
|
+
JCS shortest-decimal spellings are supported. This is not signature checking.
|
|
29
|
+
"""
|
|
30
|
+
if value is None or isinstance(value, bool):
|
|
31
|
+
return
|
|
32
|
+
if isinstance(value, str):
|
|
33
|
+
value.encode("utf-8", errors="strict")
|
|
34
|
+
elif isinstance(value, int):
|
|
35
|
+
if abs(value) > 2**53 - 1:
|
|
36
|
+
try:
|
|
37
|
+
number = float(value)
|
|
38
|
+
if not math.isfinite(number):
|
|
39
|
+
raise ValueError("integer exceeds binary64 range")
|
|
40
|
+
if int(number) != value and rfc8785.dumps(number) != str(value).encode("ascii"):
|
|
41
|
+
raise ValueError("integer is neither exact binary64 nor its JCS spelling")
|
|
42
|
+
except OverflowError as exc:
|
|
43
|
+
raise ValueError("integer exceeds binary64 range") from exc
|
|
44
|
+
elif isinstance(value, float):
|
|
45
|
+
if not math.isfinite(value):
|
|
46
|
+
raise ValueError("non-finite number")
|
|
47
|
+
elif isinstance(value, list):
|
|
48
|
+
for item in value:
|
|
49
|
+
validate(item)
|
|
50
|
+
elif isinstance(value, dict):
|
|
51
|
+
for key, item in value.items():
|
|
52
|
+
if not isinstance(key, str):
|
|
53
|
+
raise ValueError("JSON member names must be strings")
|
|
54
|
+
validate(key)
|
|
55
|
+
validate(item)
|
|
56
|
+
else:
|
|
57
|
+
raise ValueError("unsupported JSON value")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def loads(text: str | bytes) -> Any:
|
|
61
|
+
if isinstance(text, bytes):
|
|
62
|
+
text = text.decode("utf-8", errors="strict")
|
|
63
|
+
# Reject raw surrogate code points even before JSON parsing.
|
|
64
|
+
text.encode("utf-8", errors="strict")
|
|
65
|
+
try:
|
|
66
|
+
value = json.loads(text, object_pairs_hook=_pairs, parse_constant=_constant)
|
|
67
|
+
validate(value)
|
|
68
|
+
return value
|
|
69
|
+
except RecursionError as exc:
|
|
70
|
+
raise ValueError("JSON nesting exceeds parser capacity") from exc
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def dumps(value: Any) -> str:
|
|
74
|
+
try:
|
|
75
|
+
validate(value)
|
|
76
|
+
return json.dumps(value, ensure_ascii=False, allow_nan=False)
|
|
77
|
+
except RecursionError as exc:
|
|
78
|
+
raise ValueError("JSON nesting exceeds parser capacity") from exc
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jep-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Command-line client for JEP Core 0.7 with explicit legacy compatibility
|
|
5
5
|
Author-email: HJS Spec <signal@humanjudgment.org>
|
|
6
6
|
License: MIT
|
|
@@ -15,6 +15,7 @@ Requires-Python: >=3.9
|
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
License-File: NOTICE.md
|
|
18
|
+
Requires-Dist: rfc8785==0.1.4
|
|
18
19
|
Provides-Extra: dev
|
|
19
20
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
20
21
|
Dynamic: license-file
|
|
@@ -5,6 +5,7 @@ pyproject.toml
|
|
|
5
5
|
jep_cli/__init__.py
|
|
6
6
|
jep_cli/client.py
|
|
7
7
|
jep_cli/main.py
|
|
8
|
+
jep_cli/strict_json.py
|
|
8
9
|
jep_cli.egg-info/PKG-INFO
|
|
9
10
|
jep_cli.egg-info/SOURCES.txt
|
|
10
11
|
jep_cli.egg-info/dependency_links.txt
|
|
@@ -12,4 +13,5 @@ jep_cli.egg-info/entry_points.txt
|
|
|
12
13
|
jep_cli.egg-info/requires.txt
|
|
13
14
|
jep_cli.egg-info/top_level.txt
|
|
14
15
|
tests/test_acceptance.py
|
|
15
|
-
tests/test_cli.py
|
|
16
|
+
tests/test_cli.py
|
|
17
|
+
tests/test_strict_json.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "jep-cli"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.2"
|
|
8
8
|
description = "Command-line client for JEP Core 0.7 with explicit legacy compatibility"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -12,7 +12,7 @@ authors = [
|
|
|
12
12
|
{ name = "HJS Spec", email = "signal@humanjudgment.org" }
|
|
13
13
|
]
|
|
14
14
|
requires-python = ">=3.9"
|
|
15
|
-
dependencies = []
|
|
15
|
+
dependencies = ["rfc8785==0.1.4"]
|
|
16
16
|
classifiers = [
|
|
17
17
|
"Development Status :: 3 - Alpha",
|
|
18
18
|
"Intended Audience :: Developers",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from jep_cli import main as cli
|
|
7
|
+
from jep_cli import strict_json
|
|
8
|
+
|
|
9
|
+
BAD = [
|
|
10
|
+
'{"x":1,"x":2}',
|
|
11
|
+
'{"x":{"a":1,"\\u0061":2}}',
|
|
12
|
+
'{"x":NaN}', '{"x":Infinity}', '{"x":-Infinity}',
|
|
13
|
+
'{"x":1e400}', '{"x":9007199254740993}',
|
|
14
|
+
'{"x":"\\ud800"}', '{"\\udfff":1}',
|
|
15
|
+
'{"x":1,}', '{"x":1} {}',
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.parametrize('raw', BAD)
|
|
20
|
+
@pytest.mark.parametrize('source', ['inline', 'file', 'stdin'])
|
|
21
|
+
def test_all_input_sources_reject(raw, source, tmp_path, monkeypatch):
|
|
22
|
+
argument = raw
|
|
23
|
+
if source == 'file':
|
|
24
|
+
path = tmp_path / 'event.json'
|
|
25
|
+
path.write_bytes(raw.encode('utf-8'))
|
|
26
|
+
argument = str(path)
|
|
27
|
+
elif source == 'stdin':
|
|
28
|
+
monkeypatch.setattr(cli.sys, 'stdin', io.StringIO(raw))
|
|
29
|
+
argument = '-'
|
|
30
|
+
with pytest.raises(ValueError):
|
|
31
|
+
cli.parse_json_value(argument)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.mark.parametrize('source', ['file', 'stdin'])
|
|
35
|
+
def test_bad_utf8_rejected_before_text_replacement(source, tmp_path, monkeypatch):
|
|
36
|
+
raw = b'{"x":"\xff"}'
|
|
37
|
+
if source == 'file':
|
|
38
|
+
path = tmp_path / 'event.json'
|
|
39
|
+
path.write_bytes(raw)
|
|
40
|
+
argument = str(path)
|
|
41
|
+
else:
|
|
42
|
+
stream = io.TextIOWrapper(io.BytesIO(raw), encoding='utf-8', errors='replace')
|
|
43
|
+
monkeypatch.setattr(cli.sys, 'stdin', stream)
|
|
44
|
+
argument = '-'
|
|
45
|
+
with pytest.raises(ValueError):
|
|
46
|
+
cli.parse_json_value(argument)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.parametrize('raw', [
|
|
50
|
+
'{"x":1000000000000000100}', '{"x":1000000000000000128}',
|
|
51
|
+
'{"x":-1000000000000000100}', '{"x":100000000000000000000}',
|
|
52
|
+
'{"x":"\\ud83d\\ude00","ext":{},"ext_crit":[],"sig":"keep..me"}',
|
|
53
|
+
'{"x":"\\\\ud800","__proto__":{"ok":true}}',
|
|
54
|
+
])
|
|
55
|
+
def test_supported_values_preserve_members(raw):
|
|
56
|
+
value = strict_json.loads(raw)
|
|
57
|
+
assert value == json.loads(raw)
|
|
58
|
+
assert strict_json.loads(strict_json.dumps(value)) == value
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_duplicate_extension_is_not_silently_overwritten():
|
|
62
|
+
with pytest.raises(ValueError):
|
|
63
|
+
cli.parse_ext_items(['urn:test=1', 'urn:test=2'])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.mark.parametrize('legacy', [False, True])
|
|
67
|
+
def test_rejection_happens_before_network_and_never_falls_back(monkeypatch, legacy):
|
|
68
|
+
def no_client(*args):
|
|
69
|
+
pytest.fail('invalid input reached the client')
|
|
70
|
+
monkeypatch.setattr(cli, 'build_client', no_client)
|
|
71
|
+
args = ['verify', '{"who":"a","who":"b"}']
|
|
72
|
+
if legacy:
|
|
73
|
+
args.append('--legacy')
|
|
74
|
+
assert cli.main(args) == 1
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_outgoing_values_are_checked():
|
|
78
|
+
with pytest.raises(ValueError):
|
|
79
|
+
strict_json.dumps({'x': float('nan')})
|
|
80
|
+
with pytest.raises(ValueError):
|
|
81
|
+
strict_json.dumps({'x': '\ud800'})
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_plain_text_and_long_inline_json_still_work():
|
|
85
|
+
assert cli.parse_json_value('approve') == 'approve'
|
|
86
|
+
raw = json.dumps({'x': 'a' * 10000})
|
|
87
|
+
assert cli.parse_json_value(raw) == json.loads(raw)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|