88
99The output is intentionally compatible with the existing conveyor layout:
1010
11- outputs/reindexed_pr/{1024,2048,4096,...}/pr_discussions_<offset>.parquet
11+ outputs/reindexed_pr/{1024,2048,4096,...}/pr_discussions_<repo>_< offset>.parquet
1212
1313RULE #1: every stage uses the existing materializer/packer path and raises on
1414missing input, empty output, or malformed store rows. There is no fallback path.
2121import sqlite3
2222import sys
2323import tempfile
24+ import time
2425from pathlib import Path
2526
2627REPO_ROOT = Path (__file__ ).resolve ().parents [2 ]
3738
3839DEFAULT_STORE = REPO_ROOT / "outputs" / "pr_ingest" / "prs.sqlite"
3940DEFAULT_OUTPUT_ROOT = REPO_ROOT / "outputs" / "reindexed_pr"
41+ DEFAULT_MANIFEST = DEFAULT_OUTPUT_ROOT / "_done.json"
4042ZSTD_LEVELS = (1024 , 2048 , 4096 , 8192 , 16384 )
4143
4244
@@ -79,6 +81,24 @@ def _iter_pr_keys(
7981 yield from conn .execute (sql , params )
8082
8183
84+ def _count_pr_keys (
85+ conn : sqlite3 .Connection ,
86+ * ,
87+ repo : str | None ,
88+ offset : int ,
89+ limit : int | None ,
90+ ) -> int :
91+ sql = "SELECT COUNT(*) AS n FROM (SELECT 1 FROM prs"
92+ params : list [object ] = []
93+ if repo :
94+ sql += " WHERE repo=?"
95+ params .append (repo )
96+ sql += " ORDER BY repo, pr_number LIMIT ? OFFSET ?)"
97+ params .append (- 1 if limit is None else int (limit ))
98+ params .append (int (offset ))
99+ return int (conn .execute (sql , params ).fetchone ()["n" ])
100+
101+
82102def _write_pr_jsonl (
83103 conn : sqlite3 .Connection ,
84104 out_jsonl : Path ,
@@ -136,6 +156,29 @@ def _append_output(shard_name: str, packed: Path, target_length: int, output_roo
136156 return sr ._parquet_stats (dest , target_length )
137157
138158
159+ def _repo_slug (repo : str | None ) -> str :
160+ if not repo :
161+ return "all"
162+ return repo .replace ("/" , "__" ).replace (":" , "_" )
163+
164+
165+ def _shard_name (repo : str | None , offset : int ) -> str :
166+ return f"pr_discussions_{ _repo_slug (repo )} _{ int (offset ):08d} "
167+
168+
169+ def _load_manifest (path : Path ) -> dict :
170+ if not path .exists ():
171+ return {"done" : {}, "failed" : {}}
172+ return json .loads (path .read_text (encoding = "utf-8" ))
173+
174+
175+ def _save_manifest (path : Path , manifest : dict ) -> None :
176+ path .parent .mkdir (parents = True , exist_ok = True )
177+ tmp = path .with_suffix (".json.tmp" )
178+ tmp .write_text (json .dumps (manifest , indent = 2 , sort_keys = True ), encoding = "utf-8" )
179+ tmp .replace (path )
180+
181+
139182def export_pr_parquet (args : argparse .Namespace ) -> dict :
140183 store = Path (args .store )
141184 if not store .exists ():
@@ -146,32 +189,35 @@ def export_pr_parquet(args: argparse.Namespace) -> dict:
146189 raise ValueError ("--target-lengths produced no lengths" )
147190
148191 conn = connect (str (store ), create = False )
149- shard_name = f"pr_discussions_{ int (args .offset ):08d} "
150- with tempfile .TemporaryDirectory (prefix = "pr_parquet_export_" ) as tmp :
151- work = Path (tmp )
152- jsonl = work / f"{ shard_name } .jsonl"
153- n_docs = _write_pr_jsonl (
154- conn ,
155- jsonl ,
156- repo = args .repo ,
157- offset = int (args .offset ),
158- limit = args .limit ,
159- )
160- tok = sr .stage_materialize (
161- shard_name ,
162- jsonl ,
163- work ,
164- memory_limit_gb = float (args .memory_limit_gb ),
165- )
166- routed = route_by_fit (tok , lengths , work / "routed" )
167- if not routed :
168- raise RuntimeError (f"no PR docs routed for { jsonl } " )
169- per_length : dict [str , dict ] = {}
170- for length , route_parquet in sorted (routed .items ()):
171- packed = sr .stage_pack (shard_name , route_parquet , length , work )
172- per_length [str (length )] = _append_output (
173- shard_name , packed , length , output_root ,
192+ try :
193+ shard_name = _shard_name (args .repo , int (args .offset ))
194+ with tempfile .TemporaryDirectory (prefix = "pr_parquet_export_" ) as tmp :
195+ work = Path (tmp )
196+ jsonl = work / f"{ shard_name } .jsonl"
197+ n_docs = _write_pr_jsonl (
198+ conn ,
199+ jsonl ,
200+ repo = args .repo ,
201+ offset = int (args .offset ),
202+ limit = args .limit ,
174203 )
204+ tok = sr .stage_materialize (
205+ shard_name ,
206+ jsonl ,
207+ work ,
208+ memory_limit_gb = float (args .memory_limit_gb ),
209+ )
210+ routed = route_by_fit (tok , lengths , work / "routed" )
211+ if not routed :
212+ raise RuntimeError (f"no PR docs routed for { jsonl } " )
213+ per_length : dict [str , dict ] = {}
214+ for length , route_parquet in sorted (routed .items ()):
215+ packed = sr .stage_pack (shard_name , route_parquet , length , work )
216+ per_length [str (length )] = _append_output (
217+ shard_name , packed , length , output_root ,
218+ )
219+ finally :
220+ conn .close ()
175221 return {
176222 "source" : "pr" ,
177223 "store" : str (store ),
@@ -182,6 +228,88 @@ def export_pr_parquet(args: argparse.Namespace) -> dict:
182228 }
183229
184230
231+ def export_pr_parquet_batches (args : argparse .Namespace ) -> dict :
232+ store = Path (args .store )
233+ if not store .exists ():
234+ raise FileNotFoundError (f"--store does not exist: { store } " )
235+ manifest_path = Path (args .manifest )
236+ manifest = _load_manifest (manifest_path )
237+ resume = not args .no_resume
238+ batch_size = int (args .batch_size )
239+ if batch_size <= 0 :
240+ raise ValueError ("--batch-size must be > 0" )
241+
242+ conn = connect (str (store ), create = False )
243+ try :
244+ offset = int (args .offset )
245+ max_shards = args .max_shards
246+ shards : list [dict ] = []
247+ totals_by_length : dict [str , dict [str , int ]] = {}
248+ while True :
249+ shard_name = _shard_name (args .repo , offset )
250+ done_key = f"{ _repo_slug (args .repo )} :{ offset } "
251+ n_keys = _count_pr_keys (
252+ conn ,
253+ repo = args .repo ,
254+ offset = offset ,
255+ limit = batch_size ,
256+ )
257+ if n_keys == 0 :
258+ break
259+ if resume and done_key in manifest .get ("done" , {}):
260+ info = manifest ["done" ][done_key ]
261+ shards .append ({"shard" : shard_name , "skipped" : True , ** info })
262+ else :
263+ shard_args = argparse .Namespace (** vars (args ))
264+ shard_args .limit = batch_size
265+ shard_args .offset = offset
266+ try :
267+ info = export_pr_parquet (shard_args )
268+ except Exception as exc :
269+ manifest .setdefault ("failed" , {})[done_key ] = {
270+ "offset" : offset ,
271+ "limit" : batch_size ,
272+ "repo" : args .repo ,
273+ "stage" : "export_pr_parquet" ,
274+ "detail" : str (exc )[:2000 ],
275+ "ts" : time .strftime ("%Y-%m-%dT%H:%M:%S" ),
276+ }
277+ _save_manifest (manifest_path , manifest )
278+ raise
279+ manifest .setdefault ("done" , {})[done_key ] = info
280+ manifest .get ("failed" , {}).pop (done_key , None )
281+ _save_manifest (manifest_path , manifest )
282+ shards .append (info )
283+
284+ last = shards [- 1 ]
285+ for length , st in last .get ("lengths" , {}).items ():
286+ agg = totals_by_length .setdefault (
287+ length ,
288+ {"rows" : 0 , "valid_tokens" : 0 , "pad_tokens" : 0 , "capacity_tokens" : 0 },
289+ )
290+ for key in ("rows" , "valid_tokens" , "pad_tokens" , "capacity_tokens" ):
291+ agg [key ] += int (st .get (key , 0 ))
292+
293+ offset += batch_size
294+ if max_shards is not None and len (shards ) >= int (max_shards ):
295+ break
296+ return {
297+ "source" : "pr" ,
298+ "store" : str (store ),
299+ "output_root" : str (Path (args .output_root )),
300+ "manifest" : str (manifest_path ),
301+ "repo" : args .repo ,
302+ "start_offset" : int (args .offset ),
303+ "batch_size" : batch_size ,
304+ "next_offset" : offset ,
305+ "shards" : shards ,
306+ "n_shards" : len (shards ),
307+ "lengths" : totals_by_length ,
308+ }
309+ finally :
310+ conn .close ()
311+
312+
185313def parse_args (argv : list [str ]) -> argparse .Namespace :
186314 p = argparse .ArgumentParser (description = __doc__ )
187315 p .add_argument ("--store" , default = str (DEFAULT_STORE ))
@@ -190,13 +318,24 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
190318 p .add_argument ("--repo" , default = None , help = "Optional owner/repo filter." )
191319 p .add_argument ("--offset" , type = int , default = 0 )
192320 p .add_argument ("--limit" , type = int , default = 10_000 )
321+ p .add_argument ("--all" , action = "store_true" ,
322+ help = "Export every PR row from --offset in resumable batches." )
323+ p .add_argument ("--batch-size" , type = int , default = 10_000 ,
324+ help = "Rows per shard when --all is set." )
325+ p .add_argument ("--max-shards" , type = int , default = None ,
326+ help = "Optional cap on number of shards exported in this run." )
327+ p .add_argument ("--manifest" , default = str (DEFAULT_MANIFEST ),
328+ help = "Resume manifest for --all batched export." )
329+ p .add_argument ("--no-resume" , action = "store_true" ,
330+ help = "Ignore completed PR export shards in --manifest." )
193331 p .add_argument ("--memory-limit-gb" , type = float , default = 10.0 )
194332 return p .parse_args (argv )
195333
196334
197335def main (argv : list [str ]) -> int :
198336 args = parse_args (argv )
199- print (json .dumps (export_pr_parquet (args ), indent = 2 , sort_keys = True ))
337+ result = export_pr_parquet_batches (args ) if args .all else export_pr_parquet (args )
338+ print (json .dumps (result , indent = 2 , sort_keys = True ))
200339 return 0
201340
202341
0 commit comments