jsonology 0.1.0__py3-none-any.whl
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.
- jsonology/__init__.py +6 -0
- jsonology/core.py +232 -0
- jsonology/py.typed +0 -0
- jsonology-0.1.0.dist-info/METADATA +185 -0
- jsonology-0.1.0.dist-info/RECORD +7 -0
- jsonology-0.1.0.dist-info/WHEEL +4 -0
- jsonology-0.1.0.dist-info/licenses/LICENSE +21 -0
jsonology/__init__.py
ADDED
jsonology/core.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Core evaluator for the jsonology expression DSL.
|
|
2
|
+
|
|
3
|
+
Implements the confirmed spec (see SPEC.md): a strict, tiny JSON DSL with
|
|
4
|
+
``$``-rooted paths, six comparisons, a lazy ``if``, and short-circuiting
|
|
5
|
+
``and``/``or``/``not``. Two exceptions, and only two: ``ExpressionSyntaxError``
|
|
6
|
+
for malformed expressions, ``EvaluationError`` for undefined operations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
__all__ = ["EvaluationError", "ExpressionSyntaxError", "evaluate"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExpressionSyntaxError(Exception):
|
|
20
|
+
"""The expression is not well-formed: structure, arity, unknown operator, bad JSON text."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EvaluationError(Exception):
|
|
24
|
+
"""The expression is well-formed but an operation is undefined (e.g. cross-type ordering)."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_COMPARISONS = {"==", "!=", "<", "<=", ">", ">="}
|
|
28
|
+
_LOGICAL = {"if", "and", "or", "not"}
|
|
29
|
+
_OPERATORS = _COMPARISONS | _LOGICAL
|
|
30
|
+
|
|
31
|
+
# operator -> (minimum arity, maximum arity); math.inf means "no maximum"
|
|
32
|
+
_ARITY: dict[str, tuple[int, float]] = {op: (2, 2) for op in _COMPARISONS}
|
|
33
|
+
_ARITY.update({"if": (3, 3), "not": (1, 1), "and": (1, math.inf), "or": (1, math.inf)})
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def evaluate(expression: Any, data: Any) -> Any:
|
|
37
|
+
"""Evaluate a jsonology expression against ``data``.
|
|
38
|
+
|
|
39
|
+
``expression`` may be an already-parsed Python object or a raw JSON string.
|
|
40
|
+
A string that is ``"$"`` or starts with ``"$."`` is a path, not JSON text;
|
|
41
|
+
any other string is parsed as JSON, and a parse failure raises
|
|
42
|
+
:class:`ExpressionSyntaxError`.
|
|
43
|
+
|
|
44
|
+
Neither ``expression`` nor ``data`` is mutated; the function is stateless
|
|
45
|
+
and thread-safe. Raises :class:`ExpressionSyntaxError` for malformed
|
|
46
|
+
expressions and :class:`EvaluationError` for undefined operations
|
|
47
|
+
(ordering across types, with null, or between booleans).
|
|
48
|
+
"""
|
|
49
|
+
if isinstance(expression, str) and expression != "$" and not expression.startswith("$."):
|
|
50
|
+
try:
|
|
51
|
+
expression = json.loads(expression)
|
|
52
|
+
except ValueError as exc:
|
|
53
|
+
raise ExpressionSyntaxError(f"expression string is not valid JSON: {exc}") from exc
|
|
54
|
+
try:
|
|
55
|
+
return _eval(expression, data, "$")
|
|
56
|
+
except RecursionError:
|
|
57
|
+
raise ExpressionSyntaxError("expression too deeply nested") from None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _eval(node: Any, data: Any, path: str) -> Any:
|
|
61
|
+
"""Recursively evaluate one expression node, located at ``path`` in the tree."""
|
|
62
|
+
if node is None or isinstance(node, (bool, int, float)):
|
|
63
|
+
return node
|
|
64
|
+
if isinstance(node, str):
|
|
65
|
+
return _resolve_path(node, data) if node == "$" or node.startswith("$.") else node
|
|
66
|
+
if isinstance(node, Mapping):
|
|
67
|
+
if len(node) != 1:
|
|
68
|
+
raise ExpressionSyntaxError(
|
|
69
|
+
f"operator object must have exactly one key, got {len(node)}: "
|
|
70
|
+
f"{_snippet(node)} (at {path})"
|
|
71
|
+
)
|
|
72
|
+
op, args = next(iter(node.items()))
|
|
73
|
+
if op not in _OPERATORS:
|
|
74
|
+
raise ExpressionSyntaxError(f"unknown operator {op!r} (at {path})")
|
|
75
|
+
if not isinstance(args, (list, tuple)):
|
|
76
|
+
raise ExpressionSyntaxError(
|
|
77
|
+
f"arguments of {op!r} must be a list, got {_snippet(args)} (at {path})"
|
|
78
|
+
)
|
|
79
|
+
low, high = _ARITY[op]
|
|
80
|
+
if not low <= len(args) <= high:
|
|
81
|
+
expectation = f"exactly {low}" if low == high else f"at least {low}"
|
|
82
|
+
raise ExpressionSyntaxError(
|
|
83
|
+
f"operator {op!r} takes {expectation} argument(s), got {len(args)} (at {path})"
|
|
84
|
+
)
|
|
85
|
+
if op in _COMPARISONS:
|
|
86
|
+
left = _eval(args[0], data, f"{path}.{op}[0]")
|
|
87
|
+
right = _eval(args[1], data, f"{path}.{op}[1]")
|
|
88
|
+
return _compare(op, left, right, f"{path}.{op}")
|
|
89
|
+
if op == "if":
|
|
90
|
+
branch = 1 if _truthy(_eval(args[0], data, f"{path}.if[0]")) else 2
|
|
91
|
+
return _eval(args[branch], data, f"{path}.if[{branch}]")
|
|
92
|
+
if op in ("and", "or"):
|
|
93
|
+
want_truthy = op == "or"
|
|
94
|
+
for i, arg in enumerate(args):
|
|
95
|
+
if _truthy(_eval(arg, data, f"{path}.{op}[{i}]")) is want_truthy:
|
|
96
|
+
return want_truthy
|
|
97
|
+
return not want_truthy
|
|
98
|
+
return not _truthy(_eval(args[0], data, f"{path}.not[0]"))
|
|
99
|
+
raise ExpressionSyntaxError(
|
|
100
|
+
f"expression must be a JSON scalar, a '$.'-prefixed path string, or a "
|
|
101
|
+
f"single-key operator object, got {_snippet(node)} (at {path})"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _resolve_path(path: str, data: Any) -> Any:
|
|
106
|
+
"""Resolve a ``$``-rooted path; any missing segment evaluates to None."""
|
|
107
|
+
if path == "$":
|
|
108
|
+
return data
|
|
109
|
+
current = data
|
|
110
|
+
for segment in path[2:].split("."):
|
|
111
|
+
if isinstance(current, Mapping):
|
|
112
|
+
if segment in current:
|
|
113
|
+
current = current[segment]
|
|
114
|
+
else:
|
|
115
|
+
return None
|
|
116
|
+
elif isinstance(current, (list, tuple)):
|
|
117
|
+
# isdecimal, not isdigit: isdigit() admits characters int() rejects
|
|
118
|
+
# (e.g. superscript '²'), which would crash. Decimal digits are
|
|
119
|
+
# exactly the segments int() can parse.
|
|
120
|
+
if segment.isdecimal() and int(segment) < len(current):
|
|
121
|
+
current = current[int(segment)]
|
|
122
|
+
else:
|
|
123
|
+
return None
|
|
124
|
+
else:
|
|
125
|
+
return None
|
|
126
|
+
return current
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _truthy(value: Any) -> bool:
|
|
130
|
+
"""Python-style truthiness: false, null, 0, "", [], {} are falsy; else truthy."""
|
|
131
|
+
if value is None or isinstance(value, bool):
|
|
132
|
+
return bool(value)
|
|
133
|
+
if isinstance(value, (int, float)):
|
|
134
|
+
return value != 0
|
|
135
|
+
if isinstance(value, (str, list, tuple, Mapping)):
|
|
136
|
+
return len(value) > 0
|
|
137
|
+
return bool(value)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _compare(op: str, left: Any, right: Any, path: str) -> bool:
|
|
141
|
+
if op == "==":
|
|
142
|
+
return _equal(left, right)
|
|
143
|
+
if op == "!=":
|
|
144
|
+
return not _equal(left, right)
|
|
145
|
+
order = _ordered(left, right, op, path)
|
|
146
|
+
if op == "<":
|
|
147
|
+
return order < 0
|
|
148
|
+
if op == "<=":
|
|
149
|
+
return order <= 0
|
|
150
|
+
if op == ">":
|
|
151
|
+
return order > 0
|
|
152
|
+
return order >= 0
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _ordered(left: Any, right: Any, op: str, path: str) -> int:
|
|
156
|
+
"""Return -1/0/1 for two numbers or two strings; raise for anything else."""
|
|
157
|
+
if left is None or right is None or isinstance(left, bool) or isinstance(right, bool):
|
|
158
|
+
raise EvaluationError(
|
|
159
|
+
f"operator {op!r} is undefined for {_snippet(left)} and {_snippet(right)}: "
|
|
160
|
+
f"ordering is undefined for booleans and null (at {path})"
|
|
161
|
+
)
|
|
162
|
+
both_numbers = isinstance(left, (int, float)) and isinstance(right, (int, float))
|
|
163
|
+
both_strings = isinstance(left, str) and isinstance(right, str)
|
|
164
|
+
# Out-of-contract, same-type pairs (e.g. two datetimes) fall through to
|
|
165
|
+
# Python semantics. In-contract families that Python would happily order
|
|
166
|
+
# (lists compare lexicographically!) must NOT slip through: arrays,
|
|
167
|
+
# objects, booleans and null always raise.
|
|
168
|
+
both_other = (
|
|
169
|
+
_family(left) == "other" and _family(right) == "other" and type(left) is type(right)
|
|
170
|
+
)
|
|
171
|
+
if not (both_numbers or both_strings or both_other):
|
|
172
|
+
raise EvaluationError(
|
|
173
|
+
f"operator {op!r} is undefined for {_snippet(left)} and {_snippet(right)}: "
|
|
174
|
+
f"ordering requires two numbers or two strings (at {path})"
|
|
175
|
+
)
|
|
176
|
+
try:
|
|
177
|
+
if left < right:
|
|
178
|
+
return -1
|
|
179
|
+
if right < left:
|
|
180
|
+
return 1
|
|
181
|
+
return 0
|
|
182
|
+
except TypeError as exc:
|
|
183
|
+
raise EvaluationError(
|
|
184
|
+
f"operator {op!r} is undefined for {_snippet(left)} and {_snippet(right)}: "
|
|
185
|
+
f"{exc} (at {path})"
|
|
186
|
+
) from exc
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _family(value: Any) -> str:
|
|
190
|
+
"""The JSON type family of a value; ``bool`` is deliberately not ``number``."""
|
|
191
|
+
if value is None:
|
|
192
|
+
return "null"
|
|
193
|
+
if isinstance(value, bool):
|
|
194
|
+
return "boolean"
|
|
195
|
+
if isinstance(value, (int, float)):
|
|
196
|
+
return "number"
|
|
197
|
+
if isinstance(value, str):
|
|
198
|
+
return "string"
|
|
199
|
+
if isinstance(value, (list, tuple)):
|
|
200
|
+
return "array"
|
|
201
|
+
if isinstance(value, Mapping):
|
|
202
|
+
return "object"
|
|
203
|
+
return "other"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _equal(left: Any, right: Any) -> bool:
|
|
207
|
+
"""Strict, deep, structural equality; cross-type (incl. bool vs number) is False."""
|
|
208
|
+
left_family, right_family = _family(left), _family(right)
|
|
209
|
+
if left_family != right_family:
|
|
210
|
+
return False
|
|
211
|
+
if left_family == "array":
|
|
212
|
+
return len(left) == len(right) and all(
|
|
213
|
+
_equal(a, b) for a, b in zip(left, right, strict=True)
|
|
214
|
+
)
|
|
215
|
+
if left_family == "object":
|
|
216
|
+
return left.keys() == right.keys() and all(_equal(v, right[k]) for k, v in left.items())
|
|
217
|
+
if left_family == "other":
|
|
218
|
+
return type(left) is type(right) and bool(left == right)
|
|
219
|
+
return bool(left == right)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _snippet(value: Any, limit: int = 40) -> str:
|
|
223
|
+
"""A short, crash-proof rendering of a value for error messages."""
|
|
224
|
+
try:
|
|
225
|
+
text = json.dumps(value, default=repr)
|
|
226
|
+
except RecursionError:
|
|
227
|
+
return "<deeply nested value>"
|
|
228
|
+
except (TypeError, ValueError):
|
|
229
|
+
text = repr(value)
|
|
230
|
+
if len(text) > limit:
|
|
231
|
+
text = text[: limit - 1] + "…"
|
|
232
|
+
return text
|
jsonology/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: jsonology
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Evaluate a strict, tiny JSON DSL (comparisons, if, and/or/not) against a $-rooted context object
|
|
5
|
+
Author: The jsonology authors
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: dsl,evaluator,expression,json,jsonlogic,rules
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: hypothesis>=6.100; extra == 'dev'
|
|
23
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# jsonology
|
|
29
|
+
|
|
30
|
+
Evaluate a strict, tiny JSON DSL against a context object.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
>>> from jsonology import evaluate
|
|
34
|
+
>>> evaluate({"==": ["$.operation", "edit"]}, {"operation": "edit"})
|
|
35
|
+
True
|
|
36
|
+
>>> evaluate({"if": [{"==": ["$.operation", "edit"]}, "an edit!", "not an edit"]},
|
|
37
|
+
... {"operation": "delete"})
|
|
38
|
+
'not an edit'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Expressions are JSON values, authored by trusted teammates, stored in rules or
|
|
42
|
+
config. `jsonology` interprets them strictly and errors loudly. Zero runtime
|
|
43
|
+
dependencies. Python ≥ 3.10, fully typed.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install jsonology
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## The language
|
|
52
|
+
|
|
53
|
+
An **expression** is one of:
|
|
54
|
+
|
|
55
|
+
- a JSON scalar — a literal (`5`, `5.5`, `"edit"`, `true`, `false`, `null`)
|
|
56
|
+
- a string starting with `$.` — a path into the context
|
|
57
|
+
- an operator object `{op: [args...]}` — where every argument is itself an expression
|
|
58
|
+
|
|
59
|
+
### Operators
|
|
60
|
+
|
|
61
|
+
| Operator | Arity | Semantics |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| `==`, `!=` | exactly 2 | strict equality, no coercion |
|
|
64
|
+
| `<`, `<=`, `>`, `>=` | exactly 2 | ordering: two numbers or two strings only |
|
|
65
|
+
| `if` | exactly 3 | `[condition, if_true, if_false]`, **lazy** |
|
|
66
|
+
| `and`, `or` | 1 or more | short-circuit, return strict booleans |
|
|
67
|
+
| `not` | exactly 1 | negation of truthiness |
|
|
68
|
+
|
|
69
|
+
Operator objects must have exactly one key. Bare lists are not expressions.
|
|
70
|
+
Unknown operators are rejected with an error naming them.
|
|
71
|
+
|
|
72
|
+
### Paths
|
|
73
|
+
|
|
74
|
+
- `$.user.address.city` — dot-nested object access
|
|
75
|
+
- `$.items.0` — numeric segments index lists (no negative indices)
|
|
76
|
+
- a missing path evaluates to `null`, so `{"==": ["$.nickname", null]}` is the "is unset" idiom
|
|
77
|
+
- `$` alone is the entire context (which may itself be a list)
|
|
78
|
+
|
|
79
|
+
### Truthiness
|
|
80
|
+
|
|
81
|
+
`if` conditions and `and`/`or`/`not` operands are interpreted Python-style:
|
|
82
|
+
|
|
83
|
+
| Value | Truthy? |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| `false`, `null` | no |
|
|
86
|
+
| `0` | no |
|
|
87
|
+
| `""` | no |
|
|
88
|
+
| `[]`, `{}` | no |
|
|
89
|
+
| everything else | yes |
|
|
90
|
+
|
|
91
|
+
### Strict typing
|
|
92
|
+
|
|
93
|
+
- No coercion: `{"==": ["5", 5]}` is `false`.
|
|
94
|
+
- `int` and `float` unify: `5 == 5.0` is `true`.
|
|
95
|
+
- `bool` is its own type: `true == 1` is `false`, and ordering booleans is an error.
|
|
96
|
+
- Cross-type equality is always decided, never an error: `{"==": [1, "1"]}` → `false`.
|
|
97
|
+
- Ordering is only defined for two numbers or two strings; anything else —
|
|
98
|
+
cross-type, `null` on either side, booleans, lists — raises `EvaluationError`.
|
|
99
|
+
Note `{"<": ["$.missing", 5]}` raises, because a missing path is `null`.
|
|
100
|
+
- Strings order lexicographically: `{"<": ["10", "9"]}` is `true`.
|
|
101
|
+
- Equality is deep and structural over arrays and objects.
|
|
102
|
+
|
|
103
|
+
### Laziness and short-circuiting
|
|
104
|
+
|
|
105
|
+
`if` evaluates only the taken branch. `and` stops at the first falsy operand,
|
|
106
|
+
`or` at the first truthy one — later operands are never evaluated (or errored):
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
evaluate({"and": [False, {"<": [None, 1]}]}, {}) # False — the broken branch never runs
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Because of this laziness, syntax errors inside dead branches stay silent —
|
|
113
|
+
`evaluate({"if": [True, 1, {"frobnicate": [1]}]}, {})` is `1`.
|
|
114
|
+
|
|
115
|
+
## API
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
evaluate(expression, data) -> Any
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- `expression`: an already-parsed Python object **or** a raw JSON string.
|
|
122
|
+
A string that is `"$"` or starts with `"$."` is treated as a path, not JSON
|
|
123
|
+
text; any other string is parsed as JSON and failures raise
|
|
124
|
+
`ExpressionSyntaxError`. There is **no escape hatch** for literal strings
|
|
125
|
+
that start with `$.` — they are always paths.
|
|
126
|
+
- `data`: the context object (a mapping, or a list if your paths start `$.0` …).
|
|
127
|
+
- Pure, stateless, thread-safe: neither `expression` nor `data` is mutated.
|
|
128
|
+
|
|
129
|
+
Two exceptions, and only two:
|
|
130
|
+
|
|
131
|
+
| Exception | Raised for |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| `ExpressionSyntaxError` | malformed DSL: structure, arity, unknown operator, invalid JSON text, nesting too deep |
|
|
134
|
+
| `EvaluationError` | undefined operation during evaluation (ordering rules above) |
|
|
135
|
+
|
|
136
|
+
Error messages name the offending operator and locate the failing
|
|
137
|
+
subexpression within the expression tree, e.g. `$.if[0].==[1]` — "argument 1
|
|
138
|
+
of the `==` at argument 0 of the `if`".
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
>>> from jsonology import evaluate, ExpressionSyntaxError
|
|
142
|
+
>>> try:
|
|
143
|
+
... evaluate('{"if": [{"==": ["$.a", {"x": 1, "y": 2}]}, 1, 2]}', {})
|
|
144
|
+
... except ExpressionSyntaxError as e:
|
|
145
|
+
... print(e)
|
|
146
|
+
operator object must have exactly one key, got 2: {"x": 1, "y": 2} (at $.if[0].==[1])
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Limitations (deliberate, v1)
|
|
150
|
+
|
|
151
|
+
- No literal escape hatch for strings starting with `$.`.
|
|
152
|
+
- No array or object literals: every operand is a scalar, a path, or a nested
|
|
153
|
+
operator. `{"==": ["$.tags", ["a", "b"]]}` is a syntax error (compare two
|
|
154
|
+
paths instead: `{"==": ["$.tags", "$.want"]}`), and an object literal like
|
|
155
|
+
`{"x": 1}` in operand position fails as an unknown operator `'x'`.
|
|
156
|
+
- Dead `if` branches and short-circuited `and`/`or` operands are never
|
|
157
|
+
evaluated, so their syntax is not validated.
|
|
158
|
+
- Context keys containing dots cannot be addressed by paths.
|
|
159
|
+
- The contract is JSON types. Non-JSON Python values in `data` (datetime,
|
|
160
|
+
Decimal, …) are out of contract — same-type pairs fall through to Python
|
|
161
|
+
semantics, everything else is unspecified. For time comparisons store
|
|
162
|
+
ISO-8601 strings (lexicographic `<` does the right thing) or epoch integers.
|
|
163
|
+
- No custom operators, no depth or size limits (trusted-input model).
|
|
164
|
+
- No `compile()` step — expressions are interpreted directly; reach for one
|
|
165
|
+
only if profiling demands it.
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
uv venv && uv pip install -e '.[dev]'
|
|
171
|
+
python -m pytest # 149 cases: table-driven goldens + property-based invariants
|
|
172
|
+
python -m ruff check .
|
|
173
|
+
python -m mypy
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The property suite (Hypothesis) checks the contract as invariants: only the
|
|
177
|
+
two documented exceptions ever escape, `==`/ordering obey the algebraic laws
|
|
178
|
+
(reflexivity, symmetry, transitivity, connexity), truthiness matches the
|
|
179
|
+
reference table, path resolution matches an independent reference walk, dead
|
|
180
|
+
branches never evaluate, inputs are never mutated, and JSON-text input agrees
|
|
181
|
+
with object input.
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
jsonology/__init__.py,sha256=k9z_2XqnN-0zn23TWux9JSJElAcFMXbFmWrZVkLP4Ww,259
|
|
2
|
+
jsonology/core.py,sha256=T2LEdqJYXecg46BVuliuM2x5MFSz2nzsNRF1cj98LTU,9147
|
|
3
|
+
jsonology/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
jsonology-0.1.0.dist-info/METADATA,sha256=OpLt5JSkzDruGapDO7jIRIraWDEODGLjRw4ug9RZ-co,7059
|
|
5
|
+
jsonology-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
6
|
+
jsonology-0.1.0.dist-info/licenses/LICENSE,sha256=84ed0FiUtfdroKBIs9yusK2POq4hk_FqkZS2OyqDi6U,1078
|
|
7
|
+
jsonology-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The jsonology authors
|
|
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.
|