jsonc-document 2026.10.15__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.
@@ -0,0 +1,27 @@
1
+ """Typed JSONC parsing with comments and original source spans."""
2
+
3
+ from jsonc_document._lexer import tokenize
4
+ from jsonc_document._model import (
5
+ Document,
6
+ ErrorKind,
7
+ JSONCDecodeError,
8
+ Node,
9
+ Token,
10
+ TokenKind,
11
+ )
12
+ from jsonc_document._parser import loads, parse
13
+ from jsonc_document.types import Scalar, Value
14
+
15
+ __all__ = [
16
+ "Document",
17
+ "ErrorKind",
18
+ "JSONCDecodeError",
19
+ "Node",
20
+ "Scalar",
21
+ "Token",
22
+ "TokenKind",
23
+ "Value",
24
+ "loads",
25
+ "parse",
26
+ "tokenize",
27
+ ]
@@ -0,0 +1,201 @@
1
+ """JSONC lexical grammar with original source spans."""
2
+
3
+ import math
4
+ import re
5
+ from collections.abc import Iterator
6
+
7
+ from jsonc_document._model import (
8
+ ErrorKind,
9
+ JSONCDecodeError,
10
+ Token,
11
+ TokenKind,
12
+ )
13
+
14
+ _CONTROL_BOUNDARY = 0x20
15
+ _UNICODE_ESCAPE_DIGITS = 4
16
+ _WHITE_SPACE = " \t\r\n"
17
+ _NUMBER = re.compile(
18
+ pattern=r"-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?"
19
+ )
20
+ _LITERAL = re.compile(pattern=r"true|false|null")
21
+ _ESCAPES = {
22
+ '"': '"',
23
+ "\\": "\\",
24
+ "/": "/",
25
+ "b": "\b",
26
+ "f": "\f",
27
+ "n": "\n",
28
+ "r": "\r",
29
+ "t": "\t",
30
+ }
31
+
32
+
33
+ def _error(*, source: str, position: int, message: str) -> JSONCDecodeError:
34
+ """Build a position-bearing lexical error."""
35
+ return JSONCDecodeError(
36
+ message=message,
37
+ source=source,
38
+ position=position,
39
+ reason=ErrorKind.SYNTAX,
40
+ )
41
+
42
+
43
+ def _string(*, source: str, start: int) -> tuple[int, str]:
44
+ """Read a double-quoted string and decode JSON escape sequences."""
45
+ index = start + 1
46
+ characters: list[str] = []
47
+ while index < len(source):
48
+ char = source[index]
49
+ index += 1
50
+ if char == '"':
51
+ decoded = "".join(characters)
52
+ return index, decoded.encode(
53
+ encoding="utf-16", errors="surrogatepass"
54
+ ).decode(encoding="utf-16", errors="surrogatepass")
55
+ if ord(char) < _CONTROL_BOUNDARY:
56
+ raise _error(
57
+ source=source,
58
+ position=index - 1,
59
+ message="Unescaped control character in string",
60
+ )
61
+ if char != "\\":
62
+ characters.append(char)
63
+ continue
64
+ if index == len(source):
65
+ break
66
+ char = source[index]
67
+ index += 1
68
+ if char == "u":
69
+ digits = source[index : index + _UNICODE_ESCAPE_DIGITS]
70
+ if len(digits) != _UNICODE_ESCAPE_DIGITS or any(
71
+ digit not in "0123456789abcdefABCDEF" for digit in digits
72
+ ):
73
+ raise _error(
74
+ source=source,
75
+ position=index,
76
+ message="Invalid Unicode escape",
77
+ )
78
+ characters.append(chr(int(digits, base=16)))
79
+ index += _UNICODE_ESCAPE_DIGITS
80
+ elif char in _ESCAPES:
81
+ characters.append(_ESCAPES[char])
82
+ else:
83
+ raise _error(
84
+ source=source,
85
+ position=index - 1,
86
+ message="Invalid string escape",
87
+ )
88
+ raise _error(source=source, position=start, message="Unterminated string")
89
+
90
+
91
+ def _number_value(*, source: str, start: int, raw: str) -> int | float:
92
+ """Decode a number while rejecting finite overflow and underflow."""
93
+ if raw == "-0":
94
+ return -0.0
95
+ try:
96
+ if not any(char in raw for char in ".eE"):
97
+ return int(raw, base=10)
98
+ value = float(raw)
99
+ except ValueError as exc:
100
+ raise JSONCDecodeError(
101
+ source=source,
102
+ position=start,
103
+ reason=ErrorKind.INTEGER_DIGITS,
104
+ message="Integer exceeds the interpreter's supported digit limit",
105
+ ) from exc
106
+ significant = (
107
+ raw.lstrip("-").lower().split(sep="e")[0].replace(".", "").strip("0")
108
+ )
109
+ if math.isinf(value) or (value == 0 and bool(significant)):
110
+ raise JSONCDecodeError(
111
+ source=source,
112
+ position=start,
113
+ reason=ErrorKind.NUMBER_RANGE,
114
+ message="Finite numeric token is outside binary64 range",
115
+ )
116
+ return value
117
+
118
+
119
+ def _block_comment_end(*, source: str, index: int) -> int:
120
+ """Find a block comment's terminator or report its original start."""
121
+ close = source.find("*/", index + 2)
122
+ if close == -1:
123
+ raise _error(
124
+ source=source, position=index, message="Unterminated block comment"
125
+ )
126
+ return close + 2
127
+
128
+
129
+ def _whitespace_end(*, source: str, index: int) -> int:
130
+ """Find the end of consecutive JSON whitespace."""
131
+ index += 1
132
+ while index < len(source) and source[index] in _WHITE_SPACE:
133
+ index += 1
134
+ return index
135
+
136
+
137
+ def _line_comment_end(*, source: str, index: int) -> int:
138
+ """Find a CR or LF terminating a line comment."""
139
+ index += 2
140
+ while index < len(source) and source[index] not in "\r\n":
141
+ index += 1
142
+ return index
143
+
144
+
145
+ def tokenize(*, source: str) -> Iterator[Token]:
146
+ """
147
+ Yield every source token, including comments and whitespace.
148
+
149
+ Lexical validity is checked; use ``parse`` to validate the complete
150
+ document grammar.
151
+ """
152
+ index = 0
153
+ while index < len(source):
154
+ start = index
155
+ char = source[index]
156
+ value: str | int | float | bool | None = None
157
+ if char in _WHITE_SPACE:
158
+ kind = TokenKind.WHITESPACE
159
+ index = _whitespace_end(source=source, index=index)
160
+ elif source.startswith("//", index):
161
+ kind = TokenKind.COMMENT
162
+ index = _line_comment_end(source=source, index=index)
163
+ elif source.startswith("/*", index):
164
+ kind = TokenKind.COMMENT
165
+ index = _block_comment_end(source=source, index=index)
166
+ elif char in "{}[]:,":
167
+ kind = TokenKind.PUNCTUATION
168
+ index += 1
169
+ elif char == '"':
170
+ kind = TokenKind.STRING
171
+ index, value = _string(source=source, start=start)
172
+ elif char in "-0123456789":
173
+ kind = TokenKind.NUMBER
174
+ match = _NUMBER.match(string=source, pos=index)
175
+ if match is None:
176
+ raise _error(
177
+ source=source, position=start, message="Invalid number"
178
+ )
179
+ index = match.end()
180
+ value = _number_value(
181
+ source=source, start=start, raw=match.group()
182
+ )
183
+ else:
184
+ kind = TokenKind.LITERAL
185
+ match = _LITERAL.match(string=source, pos=index)
186
+ if match is None:
187
+ raise _error(
188
+ source=source,
189
+ position=start,
190
+ message="Unexpected character",
191
+ )
192
+ index = match.end()
193
+ literals = {"true": True, "false": False, "null": None}
194
+ value = literals[match.group()]
195
+ yield Token(
196
+ kind=kind,
197
+ text=source[start:index],
198
+ start=start,
199
+ end=index,
200
+ value=value,
201
+ )
@@ -0,0 +1,142 @@
1
+ """Public document, token, and syntax tree models."""
2
+
3
+ import dataclasses
4
+ import enum
5
+ import json
6
+ from collections.abc import Callable
7
+ from typing import override
8
+
9
+ from jsonc_document.types import Scalar, Value
10
+
11
+
12
+ class TokenKind(enum.Enum):
13
+ """The lexical categories retained in a document."""
14
+
15
+ PUNCTUATION = enum.auto()
16
+ STRING = enum.auto()
17
+ LITERAL = enum.auto()
18
+ NUMBER = enum.auto()
19
+ COMMENT = enum.auto()
20
+ WHITESPACE = enum.auto()
21
+
22
+
23
+ @dataclasses.dataclass(frozen=True, slots=True)
24
+ class Token:
25
+ """A token's original spelling and half-open Unicode source span."""
26
+
27
+ kind: TokenKind
28
+ text: str
29
+ start: int
30
+ end: int
31
+ value: Scalar
32
+
33
+
34
+ @dataclasses.dataclass(frozen=True, slots=True)
35
+ class Node:
36
+ """
37
+ A value and its syntax tree, retaining object keys in source order.
38
+
39
+ ``children`` contains array elements or object member values.
40
+ ``keys`` is empty for arrays and scalars. Collection delimiters are
41
+ included in the node's half-open source span.
42
+ """
43
+
44
+ value: Value
45
+ start: int
46
+ end: int
47
+ children: tuple["Node", ...]
48
+ keys: tuple[Token, ...]
49
+ commas: tuple[Token, ...]
50
+
51
+
52
+ @dataclasses.dataclass(frozen=True, slots=True)
53
+ class Document:
54
+ """
55
+ A parsed JSONC source snapshot with every lexical token retained.
56
+
57
+ Mutating a returned Python collection does not rewrite source text.
58
+ Source editing is outside the document API.
59
+ """
60
+
61
+ source: str
62
+ root: Node
63
+ tokens: tuple[Token, ...]
64
+
65
+ @property
66
+ def value(self) -> Value:
67
+ """Return the decoded Python value."""
68
+ return self.root.value
69
+
70
+ @property
71
+ def comments(self) -> tuple[Token, ...]:
72
+ """Return line and block comments in source order."""
73
+ return tuple(
74
+ token for token in self.tokens if token.kind == TokenKind.COMMENT
75
+ )
76
+
77
+ def to_source(self) -> str:
78
+ """Return the original source, preserving all spelling and spacing."""
79
+ return self.source
80
+
81
+
82
+ class ErrorKind(enum.Enum):
83
+ """Structured error categories for applications adapting
84
+ diagnostics.
85
+ """
86
+
87
+ SYNTAX = enum.auto()
88
+ DUPLICATE_KEY = enum.auto()
89
+ NUMBER_RANGE = enum.auto()
90
+ INTEGER_DIGITS = enum.auto()
91
+ DEPTH = enum.auto()
92
+
93
+
94
+ class JSONCDecodeError(json.JSONDecodeError):
95
+ """
96
+ A syntax or data-policy error with original source coordinates.
97
+
98
+ ``pos`` is a zero-based character offset. ``lineno`` and ``colno``
99
+ are one-based and recognize CR, LF, and CRLF.
100
+ """
101
+
102
+ reason: ErrorKind
103
+ lineno: int
104
+ colno: int
105
+ args: tuple[object, ...]
106
+
107
+ def __init__(
108
+ self, *, message: str, source: str, position: int, reason: ErrorKind
109
+ ) -> None:
110
+ """Record a structured failure at its original source location."""
111
+ self.reason = reason
112
+ super().__init__(msg=message, doc=source, pos=position)
113
+ prefix = source[:position].replace("\r\n", "\n")
114
+ for ending in ("\r",):
115
+ prefix = prefix.replace(ending, "\n")
116
+ self.lineno = prefix.count("\n") + 1
117
+ self.colno = len(prefix.rsplit(sep="\n", maxsplit=1)[-1]) + 1
118
+ formatted = (
119
+ f"{message}: line {self.lineno} column {self.colno} "
120
+ f"(char {position})"
121
+ )
122
+ self.args = (formatted,)
123
+
124
+ @override
125
+ def __reduce__(
126
+ self,
127
+ ) -> tuple[
128
+ Callable[..., "JSONCDecodeError"], tuple[str, str, int, ErrorKind]
129
+ ]:
130
+ """Retain source coordinates and reason when copying or
131
+ serializing.
132
+ """
133
+ return _restore_error, (self.msg, self.doc, self.pos, self.reason)
134
+
135
+
136
+ def _restore_error(
137
+ message: str, source: str, position: int, reason: ErrorKind
138
+ ) -> JSONCDecodeError:
139
+ """Restore a serialized exception through its keyword argument API."""
140
+ return JSONCDecodeError(
141
+ message=message, source=source, position=position, reason=reason
142
+ )
@@ -0,0 +1,264 @@
1
+ """An iterative JSONC parser, independent of Python's recursion limit."""
2
+
3
+ import dataclasses
4
+ import enum
5
+
6
+ from jsonc_document._lexer import tokenize
7
+ from jsonc_document._model import (
8
+ Document,
9
+ ErrorKind,
10
+ JSONCDecodeError,
11
+ Node,
12
+ Token,
13
+ TokenKind,
14
+ )
15
+ from jsonc_document.types import Value
16
+
17
+ _MAX_DEPTH = 512
18
+
19
+
20
+ class _State(enum.Enum):
21
+ """The next grammar production expected inside a collection."""
22
+
23
+ KEY = enum.auto()
24
+ COLON = enum.auto()
25
+ VALUE = enum.auto()
26
+ ARRAY_VALUE = enum.auto()
27
+ SEPARATOR = enum.auto()
28
+
29
+
30
+ @dataclasses.dataclass
31
+ class _Frame:
32
+ """A collection currently being assembled."""
33
+
34
+ opening: Token
35
+ children: list[Node]
36
+ keys: list[Token]
37
+ commas: list[Token]
38
+ values: dict[str, Value]
39
+ pending_name: str
40
+ seen_keys: set[str]
41
+ pending_key: Token | None
42
+ state: _State
43
+
44
+ def expect(self, *, state: _State) -> None:
45
+ """Record the next production expected by this frame."""
46
+ self.state = state
47
+
48
+
49
+ @dataclasses.dataclass
50
+ class _Parser:
51
+ """Consume syntax tokens while retaining their original positions."""
52
+
53
+ source: str
54
+ tokens: tuple[Token, ...]
55
+ frames: list[_Frame]
56
+ root: Node | None
57
+
58
+ def error(
59
+ self, *, token: Token, message: str, reason: ErrorKind
60
+ ) -> JSONCDecodeError:
61
+ """Create an error at the offending syntax token."""
62
+ return JSONCDecodeError(
63
+ message=message,
64
+ source=self.source,
65
+ position=token.start,
66
+ reason=reason,
67
+ )
68
+
69
+ def accept(self, *, node: Node) -> None:
70
+ """Attach a completed node to its parent or the document root."""
71
+ if not bool(self.frames):
72
+ self.root = node
73
+ return
74
+ frame = self.frames[-1]
75
+ frame.children.append(node)
76
+ if frame.pending_key is not None:
77
+ frame.keys.append(frame.pending_key)
78
+ frame.values[frame.pending_name] = node.value
79
+ frame.pending_key = None
80
+ frame.expect(state=_State.SEPARATOR)
81
+
82
+ def close(self, *, token: Token) -> None:
83
+ """Finish the current collection and attach its complete tree."""
84
+ frame = self.frames.pop()
85
+ value: Value
86
+ if frame.opening.text == "[":
87
+ value = [child.value for child in frame.children]
88
+ else:
89
+ value = frame.values
90
+ self.accept(
91
+ node=Node(
92
+ value=value,
93
+ start=frame.opening.start,
94
+ end=token.end,
95
+ children=tuple(frame.children),
96
+ keys=tuple(frame.keys),
97
+ commas=tuple(frame.commas),
98
+ )
99
+ )
100
+
101
+ def advance(self, *, token: Token) -> bool:
102
+ """Consume collection punctuation and keys before reading a
103
+ value.
104
+ """
105
+ frame = self.frames[-1]
106
+ closing = "]" if frame.opening.text == "[" else "}"
107
+ if frame.state == _State.SEPARATOR:
108
+ if token.text == closing:
109
+ self.close(token=token)
110
+ elif token.text == ",":
111
+ frame.commas.append(token)
112
+ frame.expect(
113
+ state=_State.ARRAY_VALUE if closing == "]" else _State.KEY
114
+ )
115
+ else:
116
+ raise self.error(
117
+ token=token,
118
+ message="Expected comma or closing delimiter",
119
+ reason=ErrorKind.SYNTAX,
120
+ )
121
+ return True
122
+ if (
123
+ frame.state in {_State.KEY, _State.ARRAY_VALUE}
124
+ and token.text == closing
125
+ ):
126
+ if bool(frame.commas) and len(frame.commas) == len(frame.children):
127
+ raise self.error(
128
+ token=token,
129
+ message="Trailing commas are not allowed",
130
+ reason=ErrorKind.SYNTAX,
131
+ )
132
+ self.close(token=token)
133
+ return True
134
+ if frame.state == _State.KEY:
135
+ self.read_key(token=token)
136
+ return True
137
+ if frame.state == _State.COLON:
138
+ if token.text != ":":
139
+ raise self.error(
140
+ token=token,
141
+ message="Expected colon",
142
+ reason=ErrorKind.SYNTAX,
143
+ )
144
+ frame.expect(state=_State.VALUE)
145
+ return True
146
+ return False
147
+
148
+ def read_key(self, *, token: Token) -> None:
149
+ """Decode a member name and reject repeated keys."""
150
+ frame = self.frames[-1]
151
+ if token.kind != TokenKind.STRING or not isinstance(token.value, str):
152
+ raise self.error(
153
+ token=token,
154
+ message="Expected an object member name",
155
+ reason=ErrorKind.SYNTAX,
156
+ )
157
+ if token.value in frame.seen_keys:
158
+ raise self.error(
159
+ token=token,
160
+ message=f"Duplicate key {token.value!r}",
161
+ reason=ErrorKind.DUPLICATE_KEY,
162
+ )
163
+ frame.seen_keys.add(token.value)
164
+ frame.pending_key = token
165
+ frame.pending_name = token.value
166
+ frame.expect(state=_State.COLON)
167
+
168
+ def read_value(self, *, token: Token) -> None:
169
+ """Read a scalar or open another collection."""
170
+ if token.text in {"[", "{"}:
171
+ if len(self.frames) == _MAX_DEPTH:
172
+ raise self.error(
173
+ token=token,
174
+ message="Input exceeds the supported nesting depth",
175
+ reason=ErrorKind.DEPTH,
176
+ )
177
+ self.frames.append(
178
+ _Frame(
179
+ opening=token,
180
+ children=[],
181
+ keys=[],
182
+ commas=[],
183
+ values={},
184
+ pending_name="",
185
+ seen_keys=set(),
186
+ pending_key=None,
187
+ state=_State.ARRAY_VALUE
188
+ if token.text == "["
189
+ else _State.KEY,
190
+ )
191
+ )
192
+ return
193
+ value: Value = token.value
194
+ if token.kind not in {
195
+ TokenKind.NUMBER,
196
+ TokenKind.STRING,
197
+ TokenKind.LITERAL,
198
+ }:
199
+ raise self.error(
200
+ token=token,
201
+ message="Expected a JSONC value",
202
+ reason=ErrorKind.SYNTAX,
203
+ )
204
+ self.accept(
205
+ node=Node(
206
+ value=value,
207
+ start=token.start,
208
+ end=token.end,
209
+ children=(),
210
+ keys=(),
211
+ commas=(),
212
+ )
213
+ )
214
+
215
+ def parse(self) -> Node:
216
+ """Build a complete syntax tree or report the first invalid
217
+ token.
218
+ """
219
+ for token in self.tokens:
220
+ if self.root is not None and not bool(self.frames):
221
+ raise self.error(
222
+ token=token,
223
+ message="Unexpected data after the root value",
224
+ reason=ErrorKind.SYNTAX,
225
+ )
226
+ if bool(self.frames) and self.advance(token=token):
227
+ continue
228
+ self.read_value(token=token)
229
+ if self.root is None:
230
+ message = (
231
+ "Unterminated collection"
232
+ if bool(self.frames)
233
+ else "Expected a JSONC value"
234
+ )
235
+ raise JSONCDecodeError(
236
+ message=message,
237
+ source=self.source,
238
+ position=len(self.source),
239
+ reason=ErrorKind.SYNTAX,
240
+ )
241
+ return self.root
242
+
243
+
244
+ def parse(*, source: str) -> Document:
245
+ """
246
+ Parse a JSONC document, retaining tokens, comments, and source
247
+ spans.
248
+
249
+ Duplicate keys and finite numbers outside binary64 range are rejected.
250
+ Up to 512 nested collections are accepted. No source is executed.
251
+ """
252
+ tokens = tuple(tokenize(source=source))
253
+ syntax = tuple(
254
+ token
255
+ for token in tokens
256
+ if token.kind not in {TokenKind.COMMENT, TokenKind.WHITESPACE}
257
+ )
258
+ root = _Parser(source=source, tokens=syntax, frames=[], root=None).parse()
259
+ return Document(source=source, root=root, tokens=tokens)
260
+
261
+
262
+ def loads(*, source: str) -> Value:
263
+ """Decode a JSONC string to ordinary Python values."""
264
+ return parse(source=source).value
File without changes
@@ -0,0 +1,7 @@
1
+ """Public recursive types for decoded JSONC values."""
2
+
3
+ type Scalar = str | int | float | bool | None
4
+ """A decoded string, number, Boolean, or null value."""
5
+
6
+ type Value = Scalar | list[Value] | dict[str, Value]
7
+ """A decoded scalar or recursively nested JSONC collection."""
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: jsonc-document
3
+ Version: 2026.10.15
4
+ Summary: Typed JSONC parsing with preserved comments, tokens, and source spans.
5
+ Author-email: Adam Dangoor <adamdangoor@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Documentation, https://adamtheturtle.github.io/jsonc-document/
8
+ Project-URL: Source, https://github.com/adamtheturtle/jsonc-document
9
+ Keywords: comments,jsonc,parser
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: 3.15
16
+ Requires-Python: >=3.12
17
+ Description-Content-Type: text/x-rst
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ jsonc-document
22
+ ==============
23
+
24
+ A typed JSONC parser that preserves comments, tokens, and source spans.
25
+ It requires Python 3.12 or newer and has no runtime dependencies.
26
+
27
+ Installation
28
+ ------------
29
+
30
+ .. code-block:: console
31
+
32
+ pip install jsonc-document
33
+
34
+ Usage
35
+ -----
36
+
37
+ .. code-block:: python
38
+
39
+ """Parse JSONC while preserving source comments."""
40
+
41
+ import jsonc_document
42
+
43
+ source = '// heading\n{"count": 2}'
44
+ document = jsonc_document.parse(source=source)
45
+ assert document.value == {"count": 2}
46
+ assert document.comments[0].text == "// heading"
47
+ assert document.to_source() == source
48
+
49
+ Use ``loads(source=...)`` when only Python values are needed.
50
+ See the `documentation <https://adamtheturtle.github.io/jsonc-document/>`_ for the full API and parsing policies.
@@ -0,0 +1,11 @@
1
+ jsonc_document/__init__.py,sha256=DQhCgABvaLCAjJcaX3ZvHE2iNGDJc2uB6I74W0gu_Yg,514
2
+ jsonc_document/_lexer.py,sha256=8rPMMLj3yfnzw2bzi5b7iCJet9FZF73awZfvRKTUevo,6411
3
+ jsonc_document/_model.py,sha256=vIGk2B95mgEi3hnkaXAe4evakQ-J0z7st6dDVbqUnrY,3888
4
+ jsonc_document/_parser.py,sha256=Q1wrrAqgeKlOJPXpZ22VUEBNFdPxbby9TxGHz07hiOc,8321
5
+ jsonc_document/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ jsonc_document/types.py,sha256=6AniLLgjI4U-4UEtXDyt1UoFd6hLGEkUzetHIqh42Xw,275
7
+ jsonc_document-2026.10.15.dist-info/licenses/LICENSE,sha256=cnfgEzv8YMudbgPysNfgAdqeOo4zf0y2C3Ctw4RgEE4,1069
8
+ jsonc_document-2026.10.15.dist-info/METADATA,sha256=jB_US4O61NSDzKBx7W_-mfWjGK9wq3uWHGuD3-3Lna4,1584
9
+ jsonc_document-2026.10.15.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
10
+ jsonc_document-2026.10.15.dist-info/top_level.txt,sha256=s1329lphqq1OrAM94ils1Sd5LRf6a72s9ZgjhFaiNSQ,15
11
+ jsonc_document-2026.10.15.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam Dangoor
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.
@@ -0,0 +1 @@
1
+ jsonc_document