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.
@@ -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,3 @@
1
+ """csvjson — bidirectional CSV ↔ JSON converter."""
2
+
3
+ __version__ = "1.0.0"
@@ -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,2 @@
1
+ [console_scripts]
2
+ csvjson = csvjson.cli:main
@@ -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)
@@ -0,0 +1,8 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.9"
4
+
5
+ [[package]]
6
+ name = "csvjson"
7
+ version = "1.0.0"
8
+ source = { editable = "." }