33from __future__ import annotations
44
55import csv
6+ import io
67import json
78import sys
89from pathlib import Path
9- from typing import Any , List
10+ from typing import Any , List , Optional
1011
1112import typer
1213from moss import DocumentInfo
@@ -23,7 +24,7 @@ def load_documents(file_path: str) -> List[DocumentInfo]:
2324 raise typer .BadParameter (f"File not found: { file_path } " )
2425
2526 suffix = path .suffix .lower ()
26- content = path .read_text ()
27+ content = path .read_text (encoding = "utf-8-sig" )
2728
2829 if suffix == ".csv" :
2930 return _parse_csv_docs (content )
@@ -67,42 +68,70 @@ def _parse_jsonl_docs(raw: str, source: str = "input") -> List[DocumentInfo]:
6768
6869
6970def _parse_csv_docs (content : str ) -> List [DocumentInfo ]:
70- reader = csv .DictReader (content .splitlines ())
71+ reader = csv .DictReader (io .StringIO (content , newline = "" ))
72+ if reader .fieldnames is None :
73+ raise typer .BadParameter ("CSV is empty: expected a header row" )
74+
75+ reader .fieldnames = [(name or "" ).strip () for name in reader .fieldnames ]
76+
77+ seen = set ()
78+ dupes = set ()
79+ for name in reader .fieldnames :
80+ if not name :
81+ continue
82+ if name in seen :
83+ dupes .add (name )
84+ else :
85+ seen .add (name )
86+ if dupes :
87+ raise typer .BadParameter (
88+ f"CSV header has duplicate column name(s): { ', ' .join (sorted (dupes ))} "
89+ )
90+
91+ missing = [name for name in ("id" , "text" ) if name not in reader .fieldnames ]
92+ if missing :
93+ raise typer .BadParameter (
94+ f"CSV header is missing required column(s): { ', ' .join (missing )} "
95+ )
96+
7197 docs = []
72- for i , row in enumerate (reader ):
73- if "id" not in row or "text" not in row :
98+ for row in reader :
99+ line_no = reader .line_num
100+ doc_id = row .get ("id" )
101+ text = row .get ("text" )
102+ if not doc_id or text is None :
74103 raise typer .BadParameter (
75- f"CSV row { i + 1 } : missing required 'id' or 'text' column"
104+ f"CSV line { line_no } : empty value in required 'id' or 'text' column"
76105 )
77- metadata = None
78- if "metadata" in row and row ["metadata" ]:
79- try :
80- metadata = json .loads (row ["metadata" ])
81- except json .JSONDecodeError :
82- raise typer .BadParameter (
83- f"CSV row { i + 1 } : invalid JSON in 'metadata' column"
84- )
85-
86- embedding = None
87- if "embedding" in row and row ["embedding" ]:
88- try :
89- embedding = json .loads (row ["embedding" ])
90- except json .JSONDecodeError :
91- raise typer .BadParameter (
92- f"CSV row { i + 1 } : invalid JSON in 'embedding' column"
93- )
106+
107+ metadata = _parse_csv_json (row .get ("metadata" ), "metadata" , line_no )
108+ embedding = _parse_csv_json (row .get ("embedding" ), "embedding" , line_no )
94109
95110 docs .append (
96111 DocumentInfo (
97- id = row [ "id" ] ,
98- text = row [ " text" ] ,
112+ id = doc_id ,
113+ text = text ,
99114 metadata = metadata ,
100115 embedding = embedding ,
101116 )
102117 )
103118 return docs
104119
105120
121+ def _parse_csv_json (value : Optional [str ], column : str , line_no : int ) -> Any :
122+ if value is None :
123+ return None
124+ value = value .strip ()
125+ if not value :
126+ return None
127+ try :
128+ return json .loads (value )
129+ except json .JSONDecodeError :
130+ raise typer .BadParameter (
131+ f"CSV line { line_no } : invalid JSON in '{ column } ' column"
132+ )
133+
134+
106135def _dict_to_doc (d : Any , index : int ) -> DocumentInfo :
107136 if not isinstance (d , dict ):
108137 raise typer .BadParameter (f"Document at index { index } : expected object, got { type (d ).__name__ } " )
0 commit comments