nyaf-csvjson 1.0.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.
- nyaf_csvjson-1.0.0/PKG-INFO +80 -0
- nyaf_csvjson-1.0.0/README.md +71 -0
- nyaf_csvjson-1.0.0/pyproject.toml +22 -0
- nyaf_csvjson-1.0.0/src/csvjson/__init__.py +3 -0
- nyaf_csvjson-1.0.0/src/csvjson/cli.py +190 -0
- nyaf_csvjson-1.0.0/src/csvjson/core.py +95 -0
- nyaf_csvjson-1.0.0/src/csvjson.egg-info/PKG-INFO +80 -0
- nyaf_csvjson-1.0.0/src/csvjson.egg-info/SOURCES.txt +10 -0
- nyaf_csvjson-1.0.0/src/csvjson.egg-info/dependency_links.txt +1 -0
- nyaf_csvjson-1.0.0/src/csvjson.egg-info/entry_points.txt +2 -0
- nyaf_csvjson-1.0.0/src/csvjson.egg-info/top_level.txt +1 -0
- nyaf_csvjson-1.0.0/test.py +81 -0
- nyaf_csvjson-1.0.0/uv.lock +8 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nyaf-csvjson
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Bidirectional CSV ↔ JSON converter with nested JSON support
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: cli,converter,csv,json,nested
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# csvjson
|
|
11
|
+
|
|
12
|
+
Bidirectional CSV ↔ JSON converter with nested JSON support — zero dependencies, pure Python.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Recommended: uv tool install (isolated, globally available)
|
|
18
|
+
uv tool install csvjson
|
|
19
|
+
|
|
20
|
+
# Or: pip
|
|
21
|
+
pip install csvjson
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Interactive mode
|
|
27
|
+
```bash
|
|
28
|
+
csvjson
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### File mode
|
|
32
|
+
```bash
|
|
33
|
+
# CSV → JSON
|
|
34
|
+
csvjson csv2json input.csv
|
|
35
|
+
csvjson csv2json input.csv -o output.json
|
|
36
|
+
|
|
37
|
+
# JSON → CSV
|
|
38
|
+
csvjson json2csv input.json
|
|
39
|
+
csvjson json2csv input.json -o output.csv
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Nested JSON via dot-notation
|
|
43
|
+
|
|
44
|
+
CSV headers with dots become nested JSON — and flatten back automatically.
|
|
45
|
+
|
|
46
|
+
**Input CSV:**
|
|
47
|
+
```
|
|
48
|
+
name,address.city,address.zip,scores.math
|
|
49
|
+
Alice,NYC,10001,95
|
|
50
|
+
Bob,LA,90001,88
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Output JSON:**
|
|
54
|
+
```json
|
|
55
|
+
[
|
|
56
|
+
{ "name": "Alice", "address": { "city": "NYC", "zip": 10001 }, "scores": { "math": 95 } },
|
|
57
|
+
{ "name": "Bob", "address": { "city": "LA", "zip": 90001 }, "scores": { "math": 88 } }
|
|
58
|
+
]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Type coercion
|
|
62
|
+
|
|
63
|
+
| CSV value | Python type |
|
|
64
|
+
|------------------|-------------|
|
|
65
|
+
| `42` | `int` |
|
|
66
|
+
| `3.14` | `float` |
|
|
67
|
+
| `true` / `false` | `bool` |
|
|
68
|
+
| empty | `None` |
|
|
69
|
+
| anything else | `str` |
|
|
70
|
+
|
|
71
|
+
## Publish to PyPI
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv build # creates dist/
|
|
75
|
+
uv publish # uploads to PyPI (needs PYPI_TOKEN)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# csvjson
|
|
2
|
+
|
|
3
|
+
Bidirectional CSV ↔ JSON converter with nested JSON support — zero dependencies, pure Python.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Recommended: uv tool install (isolated, globally available)
|
|
9
|
+
uv tool install csvjson
|
|
10
|
+
|
|
11
|
+
# Or: pip
|
|
12
|
+
pip install csvjson
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### Interactive mode
|
|
18
|
+
```bash
|
|
19
|
+
csvjson
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### File mode
|
|
23
|
+
```bash
|
|
24
|
+
# CSV → JSON
|
|
25
|
+
csvjson csv2json input.csv
|
|
26
|
+
csvjson csv2json input.csv -o output.json
|
|
27
|
+
|
|
28
|
+
# JSON → CSV
|
|
29
|
+
csvjson json2csv input.json
|
|
30
|
+
csvjson json2csv input.json -o output.csv
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Nested JSON via dot-notation
|
|
34
|
+
|
|
35
|
+
CSV headers with dots become nested JSON — and flatten back automatically.
|
|
36
|
+
|
|
37
|
+
**Input CSV:**
|
|
38
|
+
```
|
|
39
|
+
name,address.city,address.zip,scores.math
|
|
40
|
+
Alice,NYC,10001,95
|
|
41
|
+
Bob,LA,90001,88
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Output JSON:**
|
|
45
|
+
```json
|
|
46
|
+
[
|
|
47
|
+
{ "name": "Alice", "address": { "city": "NYC", "zip": 10001 }, "scores": { "math": 95 } },
|
|
48
|
+
{ "name": "Bob", "address": { "city": "LA", "zip": 90001 }, "scores": { "math": 88 } }
|
|
49
|
+
]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Type coercion
|
|
53
|
+
|
|
54
|
+
| CSV value | Python type |
|
|
55
|
+
|------------------|-------------|
|
|
56
|
+
| `42` | `int` |
|
|
57
|
+
| `3.14` | `float` |
|
|
58
|
+
| `true` / `false` | `bool` |
|
|
59
|
+
| empty | `None` |
|
|
60
|
+
| anything else | `str` |
|
|
61
|
+
|
|
62
|
+
## Publish to PyPI
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv build # creates dist/
|
|
66
|
+
uv publish # uploads to PyPI (needs PYPI_TOKEN)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[tool.hatch.build.targets.wheel]
|
|
6
|
+
packages = ["src/csvjson"]
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "nyaf-csvjson"
|
|
10
|
+
version = "1.0.0"
|
|
11
|
+
description = "Bidirectional CSV ↔ JSON converter with nested JSON support"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
license = { text = "MIT" }
|
|
15
|
+
keywords = ["csv", "json", "converter", "cli", "nested"]
|
|
16
|
+
dependencies = []
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
csvjson = "csvjson.cli:main"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""CLI entrypoint for csvjson."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from csvjson.core import csv_to_json, json_to_csv
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ── Colours ───────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
RESET = "\033[0m"
|
|
16
|
+
BOLD = "\033[1m"
|
|
17
|
+
CYAN = "\033[36m"
|
|
18
|
+
GREEN = "\033[32m"
|
|
19
|
+
YELLOW = "\033[33m"
|
|
20
|
+
RED = "\033[31m"
|
|
21
|
+
DIM = "\033[2m"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def c(color: str, text: str) -> str:
|
|
25
|
+
return f"{color}{text}{RESET}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
def read_file(path: str) -> str:
|
|
31
|
+
return Path(path).read_text(encoding="utf-8")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def write_file(path: str, content: str) -> None:
|
|
35
|
+
Path(path).write_text(content, encoding="utf-8")
|
|
36
|
+
print(c(GREEN, f"✅ Saved → {path}"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def save_prompt(content: str) -> None:
|
|
40
|
+
ans = input(f"\n{YELLOW}Save to file? (Enter filename or press Enter to skip): {RESET}").strip()
|
|
41
|
+
if ans:
|
|
42
|
+
write_file(ans, content)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def read_multiline(prompt: str) -> str:
|
|
46
|
+
print(c(DIM, f"{prompt} (type END on a new line when done)"))
|
|
47
|
+
lines = []
|
|
48
|
+
while True:
|
|
49
|
+
line = input()
|
|
50
|
+
if line.strip() == "END":
|
|
51
|
+
break
|
|
52
|
+
lines.append(line)
|
|
53
|
+
return "\n".join(lines)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def show_examples() -> None:
|
|
57
|
+
print(f"""
|
|
58
|
+
{BOLD}── CSV with flat keys ──────────────────────────────{RESET}
|
|
59
|
+
name,age,city
|
|
60
|
+
Alice,30,NYC
|
|
61
|
+
Bob,25,LA
|
|
62
|
+
|
|
63
|
+
{BOLD}↓ CSV → JSON:{RESET}
|
|
64
|
+
[
|
|
65
|
+
{{"name": "Alice", "age": 30, "city": "NYC"}},
|
|
66
|
+
{{"name": "Bob", "age": 25, "city": "LA" }}
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
{BOLD}── CSV with nested (dot-notation) keys ─────────────{RESET}
|
|
70
|
+
name,address.city,address.zip,scores.math
|
|
71
|
+
Alice,NYC,10001,95
|
|
72
|
+
Bob,LA,90001,88
|
|
73
|
+
|
|
74
|
+
{BOLD}↓ CSV → JSON:{RESET}
|
|
75
|
+
[
|
|
76
|
+
{{"name":"Alice","address":{{"city":"NYC","zip":10001}},"scores":{{"math":95}}}},
|
|
77
|
+
{{"name":"Bob", "address":{{"city":"LA", "zip":90001}},"scores":{{"math":88}}}}
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
{BOLD}── JSON → CSV flattens nested keys automatically ───{RESET}
|
|
81
|
+
""")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ── Arg mode ──────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
def run_arg_mode(args: argparse.Namespace) -> None:
|
|
87
|
+
raw = read_file(args.input)
|
|
88
|
+
|
|
89
|
+
if args.command == "csv2json":
|
|
90
|
+
result = json.dumps(csv_to_json(raw), indent=2, ensure_ascii=False)
|
|
91
|
+
else:
|
|
92
|
+
result = json_to_csv(json.loads(raw))
|
|
93
|
+
|
|
94
|
+
if args.output:
|
|
95
|
+
write_file(args.output, result)
|
|
96
|
+
else:
|
|
97
|
+
print(result)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ── Interactive mode ──────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
def run_interactive() -> None:
|
|
103
|
+
print(f"""
|
|
104
|
+
{CYAN}{BOLD}╔══════════════════════════════════════╗
|
|
105
|
+
║ CSV ↔ JSON Bidirectional Tool ║
|
|
106
|
+
╚══════════════════════════════════════╝{RESET}
|
|
107
|
+
{DIM}Supports nested JSON via dot-notation keys{RESET}
|
|
108
|
+
""")
|
|
109
|
+
|
|
110
|
+
while True:
|
|
111
|
+
print(f"""{BOLD}What do you want to do?{RESET}
|
|
112
|
+
{CYAN}1{RESET} CSV → JSON (from file)
|
|
113
|
+
{CYAN}2{RESET} JSON → CSV (from file)
|
|
114
|
+
{CYAN}3{RESET} CSV → JSON (paste inline)
|
|
115
|
+
{CYAN}4{RESET} JSON → CSV (paste inline)
|
|
116
|
+
{CYAN}5{RESET} Show examples
|
|
117
|
+
{CYAN}q{RESET} Quit
|
|
118
|
+
""")
|
|
119
|
+
choice = input(f"{CYAN}> {RESET}").strip()
|
|
120
|
+
|
|
121
|
+
if choice == "q":
|
|
122
|
+
print(c(DIM, "\nBye!\n"))
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
if choice in ("1", "2"):
|
|
127
|
+
path = input(f"{YELLOW}Enter file path: {RESET}").strip()
|
|
128
|
+
raw = read_file(path)
|
|
129
|
+
if choice == "1":
|
|
130
|
+
result = json.dumps(csv_to_json(raw), indent=2, ensure_ascii=False)
|
|
131
|
+
print(f"\n{GREEN}{BOLD}── JSON Output ──{RESET}\n{result}\n")
|
|
132
|
+
else:
|
|
133
|
+
result = json_to_csv(json.loads(raw))
|
|
134
|
+
print(f"\n{GREEN}{BOLD}── CSV Output ──{RESET}\n{result}\n")
|
|
135
|
+
save_prompt(result)
|
|
136
|
+
|
|
137
|
+
elif choice == "3":
|
|
138
|
+
raw = read_multiline("Paste your CSV")
|
|
139
|
+
result = json.dumps(csv_to_json(raw), indent=2, ensure_ascii=False)
|
|
140
|
+
print(f"\n{GREEN}{BOLD}── JSON Output ──{RESET}\n{result}\n")
|
|
141
|
+
save_prompt(result)
|
|
142
|
+
|
|
143
|
+
elif choice == "4":
|
|
144
|
+
raw = read_multiline("Paste your JSON")
|
|
145
|
+
result = json_to_csv(json.loads(raw))
|
|
146
|
+
print(f"\n{GREEN}{BOLD}── CSV Output ──{RESET}\n{result}\n")
|
|
147
|
+
save_prompt(result)
|
|
148
|
+
|
|
149
|
+
elif choice == "5":
|
|
150
|
+
show_examples()
|
|
151
|
+
|
|
152
|
+
else:
|
|
153
|
+
print(c(RED, "Invalid choice.\n"))
|
|
154
|
+
|
|
155
|
+
except FileNotFoundError as e:
|
|
156
|
+
print(c(RED, f"File not found: {e}\n"))
|
|
157
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
158
|
+
print(c(RED, f"Parse error: {e}\n"))
|
|
159
|
+
except KeyboardInterrupt:
|
|
160
|
+
print(c(DIM, "\n\nBye!\n"))
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
def main() -> None:
|
|
167
|
+
if len(sys.argv) < 2:
|
|
168
|
+
run_interactive()
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
parser = argparse.ArgumentParser(
|
|
172
|
+
prog="csvjson",
|
|
173
|
+
description="Bidirectional CSV ↔ JSON converter with nested JSON support",
|
|
174
|
+
)
|
|
175
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
176
|
+
|
|
177
|
+
for cmd, help_text in [
|
|
178
|
+
("csv2json", "Convert CSV file to JSON"),
|
|
179
|
+
("json2csv", "Convert JSON file to CSV"),
|
|
180
|
+
]:
|
|
181
|
+
p = sub.add_parser(cmd, help=help_text)
|
|
182
|
+
p.add_argument("input", help="Input file path")
|
|
183
|
+
p.add_argument("-o", "--output", help="Output file path (default: stdout)")
|
|
184
|
+
|
|
185
|
+
args = parser.parse_args()
|
|
186
|
+
run_arg_mode(args)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
main()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Core bidirectional CSV ↔ JSON conversion with nested support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# ── Nested helpers ────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
def _set_nested(obj: dict, dot_key: str, value: str) -> None:
|
|
13
|
+
"""Set 'address.city' = 'NYC' as nested keys on obj."""
|
|
14
|
+
keys = dot_key.split(".")
|
|
15
|
+
cur = obj
|
|
16
|
+
for key in keys[:-1]:
|
|
17
|
+
cur = cur.setdefault(key, {})
|
|
18
|
+
cur[keys[-1]] = _coerce(value)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _coerce(value: str):
|
|
22
|
+
"""Convert CSV string to the most appropriate Python type."""
|
|
23
|
+
if value == "":
|
|
24
|
+
return None
|
|
25
|
+
if value.lower() == "true":
|
|
26
|
+
return True
|
|
27
|
+
if value.lower() == "false":
|
|
28
|
+
return False
|
|
29
|
+
try:
|
|
30
|
+
as_int = int(value)
|
|
31
|
+
# avoid coercing strings like "007" or "1e5"
|
|
32
|
+
if str(as_int) == value:
|
|
33
|
+
return as_int
|
|
34
|
+
except ValueError:
|
|
35
|
+
pass
|
|
36
|
+
try:
|
|
37
|
+
return float(value)
|
|
38
|
+
except ValueError:
|
|
39
|
+
pass
|
|
40
|
+
return value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _flatten(obj: dict, prefix: str = "", result: dict | None = None) -> dict:
|
|
44
|
+
"""Flatten {'address': {'city': 'NYC'}} → {'address.city': 'NYC'}."""
|
|
45
|
+
if result is None:
|
|
46
|
+
result = {}
|
|
47
|
+
for k, v in obj.items():
|
|
48
|
+
key = f"{prefix}.{k}" if prefix else k
|
|
49
|
+
if isinstance(v, dict):
|
|
50
|
+
_flatten(v, key, result)
|
|
51
|
+
elif isinstance(v, list):
|
|
52
|
+
result[key] = json.dumps(v)
|
|
53
|
+
else:
|
|
54
|
+
result[key] = "" if v is None else str(v)
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ── CSV → JSON ────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
def csv_to_json(text: str) -> list[dict]:
|
|
61
|
+
"""Parse CSV text into a list of (possibly nested) dicts."""
|
|
62
|
+
reader = csv.DictReader(io.StringIO(text.strip()))
|
|
63
|
+
rows = []
|
|
64
|
+
for raw_row in reader:
|
|
65
|
+
obj: dict = {}
|
|
66
|
+
for key, value in raw_row.items():
|
|
67
|
+
_set_nested(obj, key.strip(), value or "")
|
|
68
|
+
rows.append(obj)
|
|
69
|
+
return rows
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ── JSON → CSV ────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
def json_to_csv(data: list | dict) -> str:
|
|
75
|
+
"""Serialize a list of dicts (or a single dict) to CSV text."""
|
|
76
|
+
if isinstance(data, dict):
|
|
77
|
+
data = [data]
|
|
78
|
+
if not data:
|
|
79
|
+
return ""
|
|
80
|
+
|
|
81
|
+
flat_rows = [_flatten(row) for row in data]
|
|
82
|
+
|
|
83
|
+
# Union of all headers, preserving first-seen order
|
|
84
|
+
seen: dict[str, None] = {}
|
|
85
|
+
for row in flat_rows:
|
|
86
|
+
seen.update(dict.fromkeys(row))
|
|
87
|
+
headers = list(seen)
|
|
88
|
+
|
|
89
|
+
output = io.StringIO()
|
|
90
|
+
writer = csv.DictWriter(output, fieldnames=headers, extrasaction="ignore", lineterminator="\n")
|
|
91
|
+
writer.writeheader()
|
|
92
|
+
for row in flat_rows:
|
|
93
|
+
writer.writerow({h: row.get(h, "") for h in headers})
|
|
94
|
+
|
|
95
|
+
return output.getvalue()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: csvjson
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Bidirectional CSV ↔ JSON converter with nested JSON support
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: csv,json,converter,cli,nested
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# csvjson
|
|
11
|
+
|
|
12
|
+
Bidirectional CSV ↔ JSON converter with nested JSON support — zero dependencies, pure Python.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Recommended: uv tool install (isolated, globally available)
|
|
18
|
+
uv tool install csvjson
|
|
19
|
+
|
|
20
|
+
# Or: pip
|
|
21
|
+
pip install csvjson
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Interactive mode
|
|
27
|
+
```bash
|
|
28
|
+
csvjson
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### File mode
|
|
32
|
+
```bash
|
|
33
|
+
# CSV → JSON
|
|
34
|
+
csvjson csv2json input.csv
|
|
35
|
+
csvjson csv2json input.csv -o output.json
|
|
36
|
+
|
|
37
|
+
# JSON → CSV
|
|
38
|
+
csvjson json2csv input.json
|
|
39
|
+
csvjson json2csv input.json -o output.csv
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Nested JSON via dot-notation
|
|
43
|
+
|
|
44
|
+
CSV headers with dots become nested JSON — and flatten back automatically.
|
|
45
|
+
|
|
46
|
+
**Input CSV:**
|
|
47
|
+
```
|
|
48
|
+
name,address.city,address.zip,scores.math
|
|
49
|
+
Alice,NYC,10001,95
|
|
50
|
+
Bob,LA,90001,88
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Output JSON:**
|
|
54
|
+
```json
|
|
55
|
+
[
|
|
56
|
+
{ "name": "Alice", "address": { "city": "NYC", "zip": 10001 }, "scores": { "math": 95 } },
|
|
57
|
+
{ "name": "Bob", "address": { "city": "LA", "zip": 90001 }, "scores": { "math": 88 } }
|
|
58
|
+
]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Type coercion
|
|
62
|
+
|
|
63
|
+
| CSV value | Python type |
|
|
64
|
+
|------------------|-------------|
|
|
65
|
+
| `42` | `int` |
|
|
66
|
+
| `3.14` | `float` |
|
|
67
|
+
| `true` / `false` | `bool` |
|
|
68
|
+
| empty | `None` |
|
|
69
|
+
| anything else | `str` |
|
|
70
|
+
|
|
71
|
+
## Publish to PyPI
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv build # creates dist/
|
|
75
|
+
uv publish # uploads to PyPI (needs PYPI_TOKEN)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/csvjson/__init__.py
|
|
4
|
+
src/csvjson/cli.py
|
|
5
|
+
src/csvjson/core.py
|
|
6
|
+
src/csvjson.egg-info/PKG-INFO
|
|
7
|
+
src/csvjson.egg-info/SOURCES.txt
|
|
8
|
+
src/csvjson.egg-info/dependency_links.txt
|
|
9
|
+
src/csvjson.egg-info/entry_points.txt
|
|
10
|
+
src/csvjson.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
csvjson
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Tests for csvjson core logic."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
|
8
|
+
|
|
9
|
+
from csvjson.core import csv_to_json, json_to_csv
|
|
10
|
+
|
|
11
|
+
passed = 0
|
|
12
|
+
failed = 0
|
|
13
|
+
|
|
14
|
+
def test(name, fn):
|
|
15
|
+
global passed, failed
|
|
16
|
+
try:
|
|
17
|
+
fn()
|
|
18
|
+
print(f" ✅ {name}")
|
|
19
|
+
passed += 1
|
|
20
|
+
except AssertionError as e:
|
|
21
|
+
print(f" ❌ {name}: {e}")
|
|
22
|
+
failed += 1
|
|
23
|
+
|
|
24
|
+
FLAT_CSV = "name,age,city\nAlice,30,NYC\nBob,25,LA"
|
|
25
|
+
NESTED_CSV = "name,address.city,address.zip,scores.math\nAlice,NYC,10001,95\nBob,LA,90001,88"
|
|
26
|
+
|
|
27
|
+
print("\nRunning tests...\n")
|
|
28
|
+
|
|
29
|
+
def test_flat_types():
|
|
30
|
+
out = csv_to_json(FLAT_CSV)
|
|
31
|
+
assert out[0]["name"] == "Alice"
|
|
32
|
+
assert out[0]["age"] == 30, f"expected int 30, got {out[0]['age']!r}"
|
|
33
|
+
assert out[1]["city"] == "LA"
|
|
34
|
+
|
|
35
|
+
def test_nested_csv_to_json():
|
|
36
|
+
out = csv_to_json(NESTED_CSV)
|
|
37
|
+
assert out[0]["address"]["city"] == "NYC"
|
|
38
|
+
assert out[0]["address"]["zip"] == 10001
|
|
39
|
+
assert out[0]["scores"]["math"] == 95
|
|
40
|
+
|
|
41
|
+
def test_json_to_csv_flat():
|
|
42
|
+
data = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
|
|
43
|
+
out = json_to_csv(data)
|
|
44
|
+
assert "name,age" in out
|
|
45
|
+
assert "Alice,30" in out
|
|
46
|
+
|
|
47
|
+
def test_json_to_csv_nested():
|
|
48
|
+
data = [{"name": "Alice", "address": {"city": "NYC", "zip": 10001}}]
|
|
49
|
+
out = json_to_csv(data)
|
|
50
|
+
assert "address.city" in out
|
|
51
|
+
assert "NYC" in out
|
|
52
|
+
|
|
53
|
+
def test_roundtrip():
|
|
54
|
+
original = csv_to_json(NESTED_CSV)
|
|
55
|
+
csv_out = json_to_csv(original)
|
|
56
|
+
back = csv_to_json(csv_out)
|
|
57
|
+
assert back[0]["address"]["city"] == "NYC"
|
|
58
|
+
assert back[1]["scores"]["math"] == 88
|
|
59
|
+
|
|
60
|
+
def test_type_coercion():
|
|
61
|
+
csv_text = "flag,value,empty\ntrue,3.14,"
|
|
62
|
+
out = csv_to_json(csv_text)
|
|
63
|
+
assert out[0]["flag"] is True
|
|
64
|
+
assert out[0]["value"] == 3.14
|
|
65
|
+
assert out[0]["empty"] is None
|
|
66
|
+
|
|
67
|
+
def test_single_object_json():
|
|
68
|
+
out = json_to_csv({"name": "Alice", "age": 30})
|
|
69
|
+
assert "Alice" in out
|
|
70
|
+
|
|
71
|
+
test("flat CSV → JSON: type coercion", test_flat_types)
|
|
72
|
+
test("nested CSV → JSON: nesting works", test_nested_csv_to_json)
|
|
73
|
+
test("JSON → CSV: flat output", test_json_to_csv_flat)
|
|
74
|
+
test("JSON → CSV: nested flattened", test_json_to_csv_nested)
|
|
75
|
+
test("roundtrip CSV → JSON → CSV", test_roundtrip)
|
|
76
|
+
test("type coercion: bool, float, None", test_type_coercion)
|
|
77
|
+
test("single dict (not list) input", test_single_object_json)
|
|
78
|
+
|
|
79
|
+
print(f"\n{passed} passed, {failed} failed\n")
|
|
80
|
+
if failed:
|
|
81
|
+
sys.exit(1)
|