flow-toon-format 0.9.0b2__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.
toon_format/cli.py ADDED
@@ -0,0 +1,217 @@
1
+ # Copyright (c) 2025 TOON Format Organization
2
+ # SPDX-License-Identifier: MIT
3
+ """Command-line interface for TOON encoding/decoding.
4
+
5
+ Provides the `toon` command-line tool for converting between JSON and TOON formats.
6
+ Supports auto-detection based on file extensions and content, with options for
7
+ delimiters, indentation, and validation modes.
8
+ """
9
+
10
+ import argparse
11
+ import json
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ from . import decode, encode
16
+ from .types import DecodeOptions, EncodeOptions
17
+
18
+
19
+ def main() -> int:
20
+ """Main CLI entry point."""
21
+ parser = argparse.ArgumentParser(
22
+ prog="toon",
23
+ description="Convert between JSON and TOON formats",
24
+ )
25
+
26
+ parser.add_argument(
27
+ "input",
28
+ type=str,
29
+ help="Input file path (or - for stdin)",
30
+ )
31
+
32
+ parser.add_argument(
33
+ "-o",
34
+ "--output",
35
+ type=str,
36
+ help="Output file path (prints to stdout if omitted)",
37
+ )
38
+
39
+ parser.add_argument(
40
+ "-e",
41
+ "--encode",
42
+ action="store_true",
43
+ help="Force encode mode (overrides auto-detection)",
44
+ )
45
+
46
+ parser.add_argument(
47
+ "-d",
48
+ "--decode",
49
+ action="store_true",
50
+ help="Force decode mode (overrides auto-detection)",
51
+ )
52
+
53
+ parser.add_argument(
54
+ "--delimiter",
55
+ type=str,
56
+ choices=[",", "\t", "|"],
57
+ default=",",
58
+ help='Array delimiter: , (comma), \\t (tab), | (pipe) (default: ",")',
59
+ )
60
+
61
+ parser.add_argument(
62
+ "--indent",
63
+ type=int,
64
+ default=2,
65
+ help="Indentation size (default: 2)",
66
+ )
67
+
68
+ parser.add_argument(
69
+ "--length-marker",
70
+ action="store_true",
71
+ help="Add # prefix to array lengths (e.g., items[#3])",
72
+ )
73
+
74
+ parser.add_argument(
75
+ "--no-strict",
76
+ action="store_true",
77
+ help="Disable strict validation when decoding",
78
+ )
79
+
80
+ args = parser.parse_args()
81
+
82
+ # Read input
83
+ try:
84
+ if args.input == "-":
85
+ input_text = sys.stdin.read()
86
+ input_path = None
87
+ else:
88
+ input_path = Path(args.input)
89
+ if not input_path.exists():
90
+ print(f"Error: Input file not found: {args.input}", file=sys.stderr)
91
+ return 1
92
+ input_text = input_path.read_text(encoding="utf-8")
93
+ except Exception as e:
94
+ print(f"Error reading input: {e}", file=sys.stderr)
95
+ return 1
96
+
97
+ # Determine operation mode
98
+ if args.encode and args.decode:
99
+ print("Error: Cannot specify both --encode and --decode", file=sys.stderr)
100
+ return 1
101
+
102
+ if args.encode:
103
+ mode = "encode"
104
+ elif args.decode:
105
+ mode = "decode"
106
+ else:
107
+ # Auto-detect based on file extension
108
+ if input_path:
109
+ if input_path.suffix.lower() == ".json":
110
+ mode = "encode"
111
+ elif input_path.suffix.lower() == ".toon":
112
+ mode = "decode"
113
+ else:
114
+ # Try to detect by content
115
+ try:
116
+ json.loads(input_text)
117
+ mode = "encode"
118
+ except json.JSONDecodeError:
119
+ mode = "decode"
120
+ else:
121
+ # No file path, try to detect by content
122
+ try:
123
+ json.loads(input_text)
124
+ mode = "encode"
125
+ except json.JSONDecodeError:
126
+ mode = "decode"
127
+
128
+ # Process
129
+ try:
130
+ if mode == "encode":
131
+ output_text = encode_json_to_toon(
132
+ input_text,
133
+ delimiter=args.delimiter,
134
+ indent=args.indent,
135
+ length_marker=args.length_marker,
136
+ )
137
+ else:
138
+ output_text = decode_toon_to_json(
139
+ input_text,
140
+ indent=args.indent,
141
+ strict=not args.no_strict,
142
+ )
143
+ except Exception as e:
144
+ print(f"Error during {mode}: {e}", file=sys.stderr)
145
+ return 1
146
+
147
+ # Write output
148
+ try:
149
+ if args.output:
150
+ output_path = Path(args.output)
151
+ output_path.write_text(output_text, encoding="utf-8")
152
+ else:
153
+ print(output_text)
154
+ except Exception as e:
155
+ print(f"Error writing output: {e}", file=sys.stderr)
156
+ return 1
157
+
158
+ return 0
159
+
160
+
161
+ def encode_json_to_toon(
162
+ json_text: str,
163
+ delimiter: str = ",",
164
+ indent: int = 2,
165
+ length_marker: bool = False,
166
+ ) -> str:
167
+ """Encode JSON text to TOON format.
168
+
169
+ Args:
170
+ json_text: JSON input string
171
+ delimiter: Delimiter character
172
+ indent: Indentation size
173
+ length_marker: Whether to add # prefix
174
+
175
+ Returns:
176
+ TOON-formatted string
177
+
178
+ Raises:
179
+ json.JSONDecodeError: If JSON is invalid
180
+ """
181
+ data = json.loads(json_text)
182
+
183
+ options: EncodeOptions = {
184
+ "indent": indent,
185
+ "delimiter": delimiter,
186
+ "lengthMarker": "#" if length_marker else False,
187
+ }
188
+
189
+ return encode(data, options)
190
+
191
+
192
+ def decode_toon_to_json(
193
+ toon_text: str,
194
+ indent: int = 2,
195
+ strict: bool = True,
196
+ ) -> str:
197
+ """Decode TOON text to JSON format.
198
+
199
+ Args:
200
+ toon_text: TOON input string
201
+ indent: Indentation size
202
+ strict: Whether to use strict validation
203
+
204
+ Returns:
205
+ JSON-formatted string
206
+
207
+ Raises:
208
+ ToonDecodeError: If TOON is invalid
209
+ """
210
+ options = DecodeOptions(indent=indent, strict=strict)
211
+ data = decode(toon_text, options)
212
+
213
+ return json.dumps(data, indent=2, ensure_ascii=False)
214
+
215
+
216
+ if __name__ == "__main__":
217
+ sys.exit(main())
@@ -0,0 +1,84 @@
1
+ # Copyright (c) 2025 TOON Format Organization
2
+ # SPDX-License-Identifier: MIT
3
+ """Constants for TOON format encoding and decoding.
4
+
5
+ Defines all string literals, characters, and configuration values used throughout
6
+ the TOON implementation. Centralizes magic values for maintainability.
7
+ """
8
+
9
+ from typing import TYPE_CHECKING, Dict
10
+
11
+ if TYPE_CHECKING:
12
+ from .types import Delimiter
13
+
14
+ # region List markers
15
+ LIST_ITEM_MARKER = "-"
16
+ LIST_ITEM_PREFIX = "- "
17
+ # endregion
18
+
19
+ # region Structural characters
20
+ COMMA: "Delimiter" = ","
21
+ COLON = ":"
22
+ SPACE = " "
23
+ PIPE: "Delimiter" = "|"
24
+ # endregion
25
+
26
+ # region Brackets and braces
27
+ OPEN_BRACKET = "["
28
+ CLOSE_BRACKET = "]"
29
+ OPEN_BRACE = "{"
30
+ CLOSE_BRACE = "}"
31
+ # endregion
32
+
33
+ # region Literals
34
+ NULL_LITERAL = "null"
35
+ TRUE_LITERAL = "true"
36
+ FALSE_LITERAL = "false"
37
+ # endregion
38
+
39
+ # region Escape characters
40
+ BACKSLASH = "\\"
41
+ DOUBLE_QUOTE = '"'
42
+ NEWLINE = "\n"
43
+ CARRIAGE_RETURN = "\r"
44
+ TAB: "Delimiter" = "\t"
45
+ # endregion
46
+
47
+ # region Delimiters
48
+ DELIMITERS: Dict[str, "Delimiter"] = {
49
+ "comma": COMMA,
50
+ "tab": TAB,
51
+ "pipe": PIPE,
52
+ }
53
+
54
+ DEFAULT_DELIMITER: "Delimiter" = DELIMITERS["comma"]
55
+ # endregion
56
+
57
+ # region Regex patterns
58
+ # Pattern strings are compiled in modules that use them
59
+ STRUCTURAL_CHARS_REGEX = r"[\[\]{}]"
60
+ CONTROL_CHARS_REGEX = r"[\n\r\t]"
61
+ NUMERIC_REGEX = r"^-?\d+(?:\.\d+)?(?:e[+-]?\d+)?$"
62
+ OCTAL_REGEX = r"^0\d+$"
63
+ VALID_KEY_REGEX = r"^[A-Z_][\w.]*$"
64
+ HEADER_LENGTH_REGEX = r"^#?(\d+)([\|\t])?$"
65
+ INTEGER_REGEX = r"^-?\d+$"
66
+ # endregion
67
+
68
+ # region Escape sequence maps
69
+ ESCAPE_SEQUENCES = {
70
+ BACKSLASH: "\\\\",
71
+ DOUBLE_QUOTE: '\\"',
72
+ NEWLINE: "\\n",
73
+ CARRIAGE_RETURN: "\\r",
74
+ TAB: "\\t",
75
+ }
76
+
77
+ UNESCAPE_SEQUENCES = {
78
+ "n": NEWLINE,
79
+ "r": CARRIAGE_RETURN,
80
+ "t": TAB,
81
+ "\\": BACKSLASH,
82
+ '"': DOUBLE_QUOTE,
83
+ }
84
+ # endregion