subagent-cli 0.1.1__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,202 @@
1
+ """Very small YAML subset parser for offline bootstrap.
2
+
3
+ This intentionally supports only the subset used by subagent v1 config:
4
+ - mappings via `key: value` and nested indentation
5
+ - lists via `- item`
6
+ - block scalar literal via `|` for multi-line strings
7
+ - scalar values: string, int, float, bool, null, `{}`, `[]`
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+
15
+
16
+ @dataclass(slots=True)
17
+ class Line:
18
+ indent: int
19
+ content: str
20
+
21
+
22
+ class ParseError(ValueError):
23
+ pass
24
+
25
+
26
+ def parse_yaml_subset(text: str) -> Any:
27
+ lines = _tokenize(text)
28
+ if not lines:
29
+ return {}
30
+ parser = _Parser(lines)
31
+ value = parser.parse_block(0)
32
+ if parser.has_next():
33
+ raise ParseError("Unexpected trailing content")
34
+ return value if value is not None else {}
35
+
36
+
37
+ def _tokenize(text: str) -> list[Line]:
38
+ out: list[Line] = []
39
+ for raw in text.splitlines():
40
+ if "\t" in raw:
41
+ raise ParseError("Tab indentation is not supported")
42
+ stripped_comments = _strip_inline_comment(raw)
43
+ if not stripped_comments.strip():
44
+ continue
45
+ indent = len(stripped_comments) - len(stripped_comments.lstrip(" "))
46
+ out.append(Line(indent=indent, content=stripped_comments.strip()))
47
+ return out
48
+
49
+
50
+ def _strip_inline_comment(line: str) -> str:
51
+ in_single = False
52
+ in_double = False
53
+ for idx, char in enumerate(line):
54
+ if char == "'" and not in_double:
55
+ in_single = not in_single
56
+ continue
57
+ if char == '"' and not in_single:
58
+ in_double = not in_double
59
+ continue
60
+ if char == "#" and not in_single and not in_double:
61
+ if idx == 0 or line[idx - 1].isspace():
62
+ return line[:idx].rstrip()
63
+ return line.rstrip()
64
+
65
+
66
+ class _Parser:
67
+ def __init__(self, lines: list[Line]) -> None:
68
+ self.lines = lines
69
+ self.index = 0
70
+
71
+ def has_next(self) -> bool:
72
+ return self.index < len(self.lines)
73
+
74
+ def peek(self) -> Line | None:
75
+ if not self.has_next():
76
+ return None
77
+ return self.lines[self.index]
78
+
79
+ def next_line(self) -> Line:
80
+ line = self.lines[self.index]
81
+ self.index += 1
82
+ return line
83
+
84
+ def parse_block(self, indent: int) -> Any:
85
+ line = self.peek()
86
+ if line is None or line.indent < indent:
87
+ return None
88
+ if line.indent > indent:
89
+ raise ParseError(f"Unexpected indentation at line {self.index + 1}")
90
+ if line.content.startswith("- "):
91
+ return self.parse_list(indent)
92
+ return self.parse_mapping(indent)
93
+
94
+ def parse_mapping(self, indent: int) -> dict[str, Any]:
95
+ mapping: dict[str, Any] = {}
96
+ while self.has_next():
97
+ line = self.peek()
98
+ assert line is not None
99
+ if line.indent < indent:
100
+ break
101
+ if line.indent > indent:
102
+ raise ParseError(f"Unexpected indentation at line {self.index + 1}")
103
+ if line.content.startswith("- "):
104
+ break
105
+
106
+ current = self.next_line()
107
+ if ":" not in current.content:
108
+ raise ParseError(f"Expected key:value mapping at line {self.index}")
109
+ key, rest = current.content.split(":", 1)
110
+ key = key.strip()
111
+ rest = rest.strip()
112
+ if not key:
113
+ raise ParseError(f"Empty key at line {self.index}")
114
+
115
+ if rest == "":
116
+ nested = self.parse_block(indent + 2)
117
+ mapping[key] = {} if nested is None else nested
118
+ elif rest == "|":
119
+ mapping[key] = self.parse_literal(indent + 2)
120
+ else:
121
+ mapping[key] = _parse_scalar(rest)
122
+ return mapping
123
+
124
+ def parse_list(self, indent: int) -> list[Any]:
125
+ items: list[Any] = []
126
+ while self.has_next():
127
+ line = self.peek()
128
+ assert line is not None
129
+ if line.indent < indent:
130
+ break
131
+ if line.indent > indent:
132
+ raise ParseError(f"Unexpected indentation at line {self.index + 1}")
133
+ if not line.content.startswith("- "):
134
+ break
135
+
136
+ current = self.next_line()
137
+ body = current.content[2:].strip()
138
+ if body == "":
139
+ nested = self.parse_block(indent + 2)
140
+ items.append({} if nested is None else nested)
141
+ elif ":" in body and not body.startswith(("'", '"')):
142
+ key, rest = body.split(":", 1)
143
+ key = key.strip()
144
+ rest = rest.strip()
145
+ value: Any
146
+ if rest == "":
147
+ nested = self.parse_block(indent + 2)
148
+ value = {} if nested is None else nested
149
+ elif rest == "|":
150
+ value = self.parse_literal(indent + 2)
151
+ else:
152
+ value = _parse_scalar(rest)
153
+ items.append({key: value})
154
+ else:
155
+ items.append(_parse_scalar(body))
156
+ return items
157
+
158
+ def parse_literal(self, indent: int) -> str:
159
+ fragments: list[str] = []
160
+ while self.has_next():
161
+ line = self.peek()
162
+ assert line is not None
163
+ if line.indent < indent:
164
+ break
165
+ current = self.next_line()
166
+ # Preserve relative indentation in block scalar.
167
+ slice_start = min(indent, current.indent)
168
+ reconstructed = (" " * (current.indent - slice_start)) + current.content
169
+ fragments.append(reconstructed)
170
+ return "\n".join(fragments).rstrip()
171
+
172
+
173
+ def _parse_scalar(value: str) -> Any:
174
+ lowered = value.lower()
175
+ if lowered in {"null", "~"}:
176
+ return None
177
+ if lowered == "true":
178
+ return True
179
+ if lowered == "false":
180
+ return False
181
+ if value == "{}":
182
+ return {}
183
+ if value == "[]":
184
+ return []
185
+ if value.startswith('"') and value.endswith('"') and len(value) >= 2:
186
+ return value[1:-1]
187
+ if value.startswith("'") and value.endswith("'") and len(value) >= 2:
188
+ return value[1:-1]
189
+
190
+ try:
191
+ if value.startswith("0") and value != "0":
192
+ raise ValueError
193
+ return int(value)
194
+ except ValueError:
195
+ pass
196
+
197
+ try:
198
+ return float(value)
199
+ except ValueError:
200
+ pass
201
+
202
+ return value