dan-annotation 0.1.0__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.
- dan_annotation-0.1.0/LICENSE +21 -0
- dan_annotation-0.1.0/PKG-INFO +178 -0
- dan_annotation-0.1.0/README.md +149 -0
- dan_annotation-0.1.0/dan.py +221 -0
- dan_annotation-0.1.0/dan_annotation.egg-info/PKG-INFO +178 -0
- dan_annotation-0.1.0/dan_annotation.egg-info/SOURCES.txt +10 -0
- dan_annotation-0.1.0/dan_annotation.egg-info/dependency_links.txt +1 -0
- dan_annotation-0.1.0/dan_annotation.egg-info/requires.txt +3 -0
- dan_annotation-0.1.0/dan_annotation.egg-info/top_level.txt +1 -0
- dan_annotation-0.1.0/pyproject.toml +43 -0
- dan_annotation-0.1.0/setup.cfg +4 -0
- dan_annotation-0.1.0/tests/test_dan.py +667 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 DAN Python Library Contributors
|
|
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,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dan-annotation
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DAN (Data Advanced Notation) Parser and Encoder for Python
|
|
5
|
+
Author: DAN Python Library
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://github.com/marcuwynu23/danpy#readme
|
|
8
|
+
Project-URL: Issue Tracker, https://github.com/marcuwynu23/danpy/issues
|
|
9
|
+
Project-URL: Source Code, https://github.com/marcuwynu23/danpy
|
|
10
|
+
Keywords: dan,parser,encoder,data-format,configuration,config
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.7
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# DAN (Data Advanced Notation) Python Library
|
|
31
|
+
|
|
32
|
+
[](https://www.python.org/downloads/)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
|
|
35
|
+
A Python implementation of the DAN (Data Advanced Notation) parser and encoder. DAN is a human-readable data format that combines the simplicity of key-value pairs with the power of nested structures and tables.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **Decode**: Parse DAN text into Python dictionaries
|
|
40
|
+
- **Encode**: Convert Python dictionaries into DAN text
|
|
41
|
+
- Support for nested blocks, tables, arrays, and various data types
|
|
42
|
+
- Comment support (# and //)
|
|
43
|
+
- Type inference (strings, numbers, booleans, arrays)
|
|
44
|
+
- Comprehensive test suite with 40+ test cases
|
|
45
|
+
- Multiple real-world examples
|
|
46
|
+
|
|
47
|
+
See [FEATURES.md](FEATURES.md) for a complete feature list.
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install -e .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or install from source:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/yourusername/dan-py.git
|
|
59
|
+
cd dan-py
|
|
60
|
+
pip install -e .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
### Decoding DAN
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from dan import decode
|
|
69
|
+
|
|
70
|
+
text = """
|
|
71
|
+
app {
|
|
72
|
+
name: "MyApp"
|
|
73
|
+
version: 1.0
|
|
74
|
+
server {
|
|
75
|
+
host: localhost
|
|
76
|
+
port: 3000
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
data = decode(text)
|
|
82
|
+
print(data)
|
|
83
|
+
# {'app': {'name': 'MyApp', 'version': 1.0, 'server': {'host': 'localhost', 'port': 3000}}}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Encoding to DAN
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from dan import encode
|
|
90
|
+
|
|
91
|
+
data = {
|
|
92
|
+
"app": {
|
|
93
|
+
"name": "MyApp",
|
|
94
|
+
"version": 1.0,
|
|
95
|
+
"server": {
|
|
96
|
+
"host": "localhost",
|
|
97
|
+
"port": 3000
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
text = encode(data)
|
|
103
|
+
print(text)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Working with Tables
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
text = """
|
|
110
|
+
users: table(id, username, email) [
|
|
111
|
+
1, alice, "alice@example.com"
|
|
112
|
+
2, bob, "bob@example.com"
|
|
113
|
+
]
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
data = decode(text)
|
|
117
|
+
# {'users': [{'id': 1, 'username': 'alice', 'email': 'alice@example.com'}, ...]}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Reading from Files
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from dan import decode
|
|
124
|
+
|
|
125
|
+
with open('config.dan', 'r') as f:
|
|
126
|
+
config = decode(f.read())
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Examples
|
|
130
|
+
|
|
131
|
+
Check out the [examples/](examples/) directory for real-world usage scenarios:
|
|
132
|
+
|
|
133
|
+
- Application configuration (`config.dan`)
|
|
134
|
+
- Database schemas (`database_schema.dan`)
|
|
135
|
+
- API routes (`api_routes.dan`)
|
|
136
|
+
- Microservices configuration (`microservices.dan`)
|
|
137
|
+
- Docker Compose (`docker_compose.dan`)
|
|
138
|
+
- Kubernetes resources (`kubernetes.dan`)
|
|
139
|
+
- And many more!
|
|
140
|
+
|
|
141
|
+
## Running Tests
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# Run all tests
|
|
145
|
+
python -m unittest discover tests -v
|
|
146
|
+
|
|
147
|
+
# Run specific test file
|
|
148
|
+
python -m unittest tests.test_dan -v
|
|
149
|
+
|
|
150
|
+
# Using pytest (if installed)
|
|
151
|
+
pytest tests/
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Documentation
|
|
155
|
+
|
|
156
|
+
- [FEATURES.md](FEATURES.md) - Complete feature documentation
|
|
157
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md) - Guidelines for contributing
|
|
158
|
+
- [CHANGELOG.md](CHANGELOG.md) - Version history and changes
|
|
159
|
+
- [examples/README.md](examples/README.md) - Example files documentation
|
|
160
|
+
|
|
161
|
+
## Contributing
|
|
162
|
+
|
|
163
|
+
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
168
|
+
|
|
169
|
+
## Security
|
|
170
|
+
|
|
171
|
+
Please report security vulnerabilities to the maintainers. See [SECURITY.md](SECURITY.md) for more information.
|
|
172
|
+
|
|
173
|
+
## Support
|
|
174
|
+
|
|
175
|
+
- 📖 [Documentation](https://github.com/yourusername/dan-py#readme)
|
|
176
|
+
- 🐛 [Issue Tracker](https://github.com/yourusername/dan-py/issues)
|
|
177
|
+
- 💬 [Discussions](https://github.com/yourusername/dan-py/discussions)
|
|
178
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# DAN (Data Advanced Notation) Python Library
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/downloads/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
A Python implementation of the DAN (Data Advanced Notation) parser and encoder. DAN is a human-readable data format that combines the simplicity of key-value pairs with the power of nested structures and tables.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Decode**: Parse DAN text into Python dictionaries
|
|
11
|
+
- **Encode**: Convert Python dictionaries into DAN text
|
|
12
|
+
- Support for nested blocks, tables, arrays, and various data types
|
|
13
|
+
- Comment support (# and //)
|
|
14
|
+
- Type inference (strings, numbers, booleans, arrays)
|
|
15
|
+
- Comprehensive test suite with 40+ test cases
|
|
16
|
+
- Multiple real-world examples
|
|
17
|
+
|
|
18
|
+
See [FEATURES.md](FEATURES.md) for a complete feature list.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install -e .
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or install from source:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/yourusername/dan-py.git
|
|
30
|
+
cd dan-py
|
|
31
|
+
pip install -e .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
### Decoding DAN
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from dan import decode
|
|
40
|
+
|
|
41
|
+
text = """
|
|
42
|
+
app {
|
|
43
|
+
name: "MyApp"
|
|
44
|
+
version: 1.0
|
|
45
|
+
server {
|
|
46
|
+
host: localhost
|
|
47
|
+
port: 3000
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
data = decode(text)
|
|
53
|
+
print(data)
|
|
54
|
+
# {'app': {'name': 'MyApp', 'version': 1.0, 'server': {'host': 'localhost', 'port': 3000}}}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Encoding to DAN
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from dan import encode
|
|
61
|
+
|
|
62
|
+
data = {
|
|
63
|
+
"app": {
|
|
64
|
+
"name": "MyApp",
|
|
65
|
+
"version": 1.0,
|
|
66
|
+
"server": {
|
|
67
|
+
"host": "localhost",
|
|
68
|
+
"port": 3000
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
text = encode(data)
|
|
74
|
+
print(text)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Working with Tables
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
text = """
|
|
81
|
+
users: table(id, username, email) [
|
|
82
|
+
1, alice, "alice@example.com"
|
|
83
|
+
2, bob, "bob@example.com"
|
|
84
|
+
]
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
data = decode(text)
|
|
88
|
+
# {'users': [{'id': 1, 'username': 'alice', 'email': 'alice@example.com'}, ...]}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Reading from Files
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from dan import decode
|
|
95
|
+
|
|
96
|
+
with open('config.dan', 'r') as f:
|
|
97
|
+
config = decode(f.read())
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Examples
|
|
101
|
+
|
|
102
|
+
Check out the [examples/](examples/) directory for real-world usage scenarios:
|
|
103
|
+
|
|
104
|
+
- Application configuration (`config.dan`)
|
|
105
|
+
- Database schemas (`database_schema.dan`)
|
|
106
|
+
- API routes (`api_routes.dan`)
|
|
107
|
+
- Microservices configuration (`microservices.dan`)
|
|
108
|
+
- Docker Compose (`docker_compose.dan`)
|
|
109
|
+
- Kubernetes resources (`kubernetes.dan`)
|
|
110
|
+
- And many more!
|
|
111
|
+
|
|
112
|
+
## Running Tests
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Run all tests
|
|
116
|
+
python -m unittest discover tests -v
|
|
117
|
+
|
|
118
|
+
# Run specific test file
|
|
119
|
+
python -m unittest tests.test_dan -v
|
|
120
|
+
|
|
121
|
+
# Using pytest (if installed)
|
|
122
|
+
pytest tests/
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Documentation
|
|
126
|
+
|
|
127
|
+
- [FEATURES.md](FEATURES.md) - Complete feature documentation
|
|
128
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md) - Guidelines for contributing
|
|
129
|
+
- [CHANGELOG.md](CHANGELOG.md) - Version history and changes
|
|
130
|
+
- [examples/README.md](examples/README.md) - Example files documentation
|
|
131
|
+
|
|
132
|
+
## Contributing
|
|
133
|
+
|
|
134
|
+
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
139
|
+
|
|
140
|
+
## Security
|
|
141
|
+
|
|
142
|
+
Please report security vulnerabilities to the maintainers. See [SECURITY.md](SECURITY.md) for more information.
|
|
143
|
+
|
|
144
|
+
## Support
|
|
145
|
+
|
|
146
|
+
- 📖 [Documentation](https://github.com/yourusername/dan-py#readme)
|
|
147
|
+
- 🐛 [Issue Tracker](https://github.com/yourusername/dan-py/issues)
|
|
148
|
+
- 💬 [Discussions](https://github.com/yourusername/dan-py/discussions)
|
|
149
|
+
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DAN (Data Advanced Notation) Parser and Encoder for Python
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Dict, List, Union
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def decode(text: Union[str, bytes]) -> Dict[str, Any]:
|
|
10
|
+
"""
|
|
11
|
+
Decode DAN text into a Python dictionary.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
text: DAN text as string or bytes
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Dictionary representation of the DAN data
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
TypeError: If input is not string or bytes
|
|
21
|
+
"""
|
|
22
|
+
# Handle bytes inputs
|
|
23
|
+
if isinstance(text, bytes):
|
|
24
|
+
text = text.decode('utf-8')
|
|
25
|
+
|
|
26
|
+
# Ensure text is a string
|
|
27
|
+
if not isinstance(text, str):
|
|
28
|
+
raise TypeError(f"Expected string or bytes, got {type(text).__name__}")
|
|
29
|
+
|
|
30
|
+
# Handle empty input - return empty object
|
|
31
|
+
if not text or not text.strip():
|
|
32
|
+
return {}
|
|
33
|
+
|
|
34
|
+
lines = text.splitlines()
|
|
35
|
+
stack = [{"obj": {}, "type": "root"}]
|
|
36
|
+
current_table = None
|
|
37
|
+
|
|
38
|
+
table_re = re.compile(r'^(\w+):\s*table\(([^)]+)\)\s*\[$')
|
|
39
|
+
kv_re = re.compile(r'^(\w+):\s*(.+)$')
|
|
40
|
+
|
|
41
|
+
for i, line in enumerate(lines):
|
|
42
|
+
# Remove comments and trim
|
|
43
|
+
comment_index1 = line.find("#")
|
|
44
|
+
comment_index2 = line.find("//")
|
|
45
|
+
cut_index = -1
|
|
46
|
+
if comment_index1 >= 0 and comment_index2 >= 0:
|
|
47
|
+
cut_index = min(comment_index1, comment_index2)
|
|
48
|
+
else:
|
|
49
|
+
cut_index = max(comment_index1, comment_index2)
|
|
50
|
+
|
|
51
|
+
if cut_index >= 0:
|
|
52
|
+
line = line[:cut_index]
|
|
53
|
+
line = line.strip()
|
|
54
|
+
if not line:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
top = stack[-1]
|
|
58
|
+
|
|
59
|
+
# Block start
|
|
60
|
+
if line.endswith("{"):
|
|
61
|
+
key = line[:-1].strip()
|
|
62
|
+
new_obj = {}
|
|
63
|
+
top["obj"][key] = new_obj
|
|
64
|
+
stack.append({"obj": new_obj, "type": "block"})
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
# Block end
|
|
68
|
+
if line == "}":
|
|
69
|
+
stack.pop()
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
# Table start
|
|
73
|
+
table_match = table_re.match(line)
|
|
74
|
+
if table_match:
|
|
75
|
+
key = table_match.group(1)
|
|
76
|
+
columns = [c.strip() for c in table_match.group(2).split(",")]
|
|
77
|
+
table = []
|
|
78
|
+
top["obj"][key] = table
|
|
79
|
+
current_table = {"obj": table, "columns": columns}
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# Table end
|
|
83
|
+
if line == "]":
|
|
84
|
+
current_table = None
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
# Table row
|
|
88
|
+
if current_table:
|
|
89
|
+
row = {}
|
|
90
|
+
# Split by comma and trim each value
|
|
91
|
+
values = [v.strip() for v in line.split(",")]
|
|
92
|
+
# Only process up to the number of columns defined
|
|
93
|
+
for col_idx, val in enumerate(values):
|
|
94
|
+
if col_idx < len(current_table["columns"]):
|
|
95
|
+
row[current_table["columns"][col_idx]] = parse_value(val)
|
|
96
|
+
# Ensure all columns are present (fill missing with empty string)
|
|
97
|
+
for col_idx, col_name in enumerate(current_table["columns"]):
|
|
98
|
+
if col_name not in row:
|
|
99
|
+
row[col_name] = ""
|
|
100
|
+
current_table["obj"].append(row)
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
# Key-value
|
|
104
|
+
kv_match = kv_re.match(line)
|
|
105
|
+
if kv_match:
|
|
106
|
+
key = kv_match.group(1)
|
|
107
|
+
val = kv_match.group(2)
|
|
108
|
+
top["obj"][key] = parse_value(val)
|
|
109
|
+
|
|
110
|
+
return stack[0]["obj"]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def encode(obj: Dict[str, Any], indent: int = 0) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Encode a Python dictionary into DAN text.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
obj: Dictionary to encode
|
|
119
|
+
indent: Current indentation level (for recursion)
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
DAN text representation
|
|
123
|
+
"""
|
|
124
|
+
lines = []
|
|
125
|
+
pad = " " * indent
|
|
126
|
+
|
|
127
|
+
for key, val in obj.items():
|
|
128
|
+
if isinstance(val, list):
|
|
129
|
+
if len(val) > 0 and isinstance(val[0], dict) and not isinstance(val[0], list):
|
|
130
|
+
# Table
|
|
131
|
+
columns = list(val[0].keys())
|
|
132
|
+
lines.append(f"{pad}{key}: table({', '.join(columns)}) [")
|
|
133
|
+
for row in val:
|
|
134
|
+
row_values = [serialize_value(row.get(c)) for c in columns]
|
|
135
|
+
lines.append(f"{pad} {', '.join(row_values)}")
|
|
136
|
+
lines.append(f"{pad}]")
|
|
137
|
+
else:
|
|
138
|
+
lines.append(f"{pad}{key}: {serialize_value(val)}")
|
|
139
|
+
elif isinstance(val, dict):
|
|
140
|
+
lines.append(f"{pad}{key} {{")
|
|
141
|
+
nested_lines = encode(val, indent + 1)
|
|
142
|
+
if nested_lines:
|
|
143
|
+
# Split nested lines and add them individually for proper formatting
|
|
144
|
+
nested_lines_array = nested_lines.split("\n")
|
|
145
|
+
lines.extend(nested_lines_array)
|
|
146
|
+
lines.append(f"{pad}}}")
|
|
147
|
+
else:
|
|
148
|
+
lines.append(f"{pad}{key}: {serialize_value(val)}")
|
|
149
|
+
|
|
150
|
+
return "\n".join(lines)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# --- Internal helpers ---
|
|
154
|
+
|
|
155
|
+
def parse_value(val: str) -> Any:
|
|
156
|
+
"""
|
|
157
|
+
Parse a string value into appropriate Python type.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
val: String value to parse
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Parsed value (bool, int, float, str, list, or original string)
|
|
164
|
+
"""
|
|
165
|
+
if not isinstance(val, str):
|
|
166
|
+
return val
|
|
167
|
+
|
|
168
|
+
val = val.strip()
|
|
169
|
+
|
|
170
|
+
if val == "true":
|
|
171
|
+
return True
|
|
172
|
+
if val == "false":
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
# String (quoted)
|
|
176
|
+
if len(val) >= 2 and val[0] == '"' and val[-1] == '"':
|
|
177
|
+
return val[1:-1]
|
|
178
|
+
|
|
179
|
+
# Number
|
|
180
|
+
if val and not val.startswith("["):
|
|
181
|
+
try:
|
|
182
|
+
# Try integer first
|
|
183
|
+
if '.' not in val:
|
|
184
|
+
return int(val)
|
|
185
|
+
else:
|
|
186
|
+
return float(val)
|
|
187
|
+
except ValueError:
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
# Array
|
|
191
|
+
if len(val) >= 2 and val[0] == "[" and val[-1] == "]":
|
|
192
|
+
content = val[1:-1].strip()
|
|
193
|
+
if content == "":
|
|
194
|
+
return []
|
|
195
|
+
# Split by comma, but preserve empty strings for explicit empty values
|
|
196
|
+
parts = [v.strip() for v in content.split(",")]
|
|
197
|
+
return [parse_value(v) for v in parts]
|
|
198
|
+
|
|
199
|
+
return val
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def serialize_value(val: Any) -> str:
|
|
203
|
+
"""
|
|
204
|
+
Serialize a Python value to DAN string representation.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
val: Value to serialize
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
String representation
|
|
211
|
+
"""
|
|
212
|
+
if isinstance(val, bool):
|
|
213
|
+
return "true" if val else "false"
|
|
214
|
+
if isinstance(val, str):
|
|
215
|
+
return f'"{val}"'
|
|
216
|
+
if isinstance(val, (int, float)):
|
|
217
|
+
return str(val)
|
|
218
|
+
if isinstance(val, list):
|
|
219
|
+
return f"[{', '.join(serialize_value(v) for v in val)}]"
|
|
220
|
+
return str(val)
|
|
221
|
+
|