1919from fastapi .testclient import TestClient
2020
2121from app .categories import COLLECTIONS as CATEGORY_COLLECTIONS
22+ from app .validate import changed_paths
2223
2324OUTPUT_DIR = Path (__file__ ).resolve ().parent .parent / "dump"
2425
2930PAGE_LIMIT = 100 # API max page size (§7.3)
3031
3132
32- def resolve_collections (exclude : list [str ] | None = None ) -> list [str ]:
33- """Return the collections to dump, minus ``exclude``.
33+ def resolve_collections (
34+ exclude : list [str ] | None = None , only : list [str ] | None = None
35+ ) -> list [str ]:
36+ """Return the collections to dump: just ``only`` if given, minus ``exclude``.
3437
3538 Unknown names raise instead of being ignored, so a typo in a workflow fails
3639 loudly rather than silently dumping everything.
3740 """
41+ if only :
42+ unknown = sorted (set (only ) - set (COLLECTIONS ))
43+ if unknown :
44+ raise ValueError (
45+ f"unknown collection(s) { unknown } ; valid names: { ', ' .join (COLLECTIONS )} "
46+ )
47+ return [r for r in COLLECTIONS if r in set (only ) and r not in set (exclude or [])]
3848 if not exclude :
3949 return list (COLLECTIONS )
4050 unknown = sorted (set (exclude ) - set (COLLECTIONS ))
@@ -45,6 +55,31 @@ def resolve_collections(exclude: list[str] | None = None) -> list[str]:
4555 return [resource for resource in COLLECTIONS if resource not in set (exclude )]
4656
4757
58+ # Extra collections whose pages embed another category's data. ``None`` = everything.
59+ # Scored collections (smartphones/cpus/gpus/socs) are always re-dumped whole: scores
60+ # are partly relative to the population, so one record can move its neighbours.
61+ DEPENDENTS : dict [str , list [str ] | None ] = {
62+ "brand" : None ,
63+ "soc" : ["socs" , "smartphones" , "tablets" , "watches" , "pdas" ],
64+ "cpu" : ["cpus" , "laptops" ],
65+ "gpu" : ["gpus" , "laptops" ],
66+ }
67+
68+
69+ def collections_for_changes (paths : set [str ]) -> list [str ]:
70+ """Collections a set of changed seed paths (``data/`` stripped) can affect."""
71+ affected : set [str ] = set ()
72+ for path in paths :
73+ category = path .split ("/" , 1 )[0 ]
74+ if category not in CATEGORY_COLLECTIONS :
75+ continue # e.g. _verify/: not part of the dump
76+ deps = DEPENDENTS .get (category , [CATEGORY_COLLECTIONS [category ]])
77+ if deps is None :
78+ return list (COLLECTIONS )
79+ affected .update (deps )
80+ return [resource for resource in COLLECTIONS if resource in affected ]
81+
82+
4883def _write_json (path : Path , data : object ) -> None :
4984 text = json .dumps (data , indent = 2 , ensure_ascii = False ) + "\n "
5085 # Leave identical pages untouched: rewriting ~1M unchanged files resets
@@ -105,6 +140,14 @@ def generate(
105140 """Write the full static dump. Returns the number of detail files per collection."""
106141 counts : dict [str , int ] = {}
107142 manifest : dict [str , object ] = {"version" : "v1" , "collections" : {}}
143+ if collections is not None and set (collections ) != set (COLLECTIONS ):
144+ # Partial run: keep the entries of collections we are not touching.
145+ try :
146+ previous = json .loads ((output_dir / "v1" / "index.json" ).read_text (encoding = "utf-8" ))
147+ if isinstance (previous .get ("collections" ), dict ):
148+ manifest ["collections" ] = previous ["collections" ]
149+ except (FileNotFoundError , json .JSONDecodeError ):
150+ pass
108151
109152 for resource in collections or COLLECTIONS :
110153 count , items = _fetch_all (client , resource )
@@ -142,14 +185,21 @@ def generate(
142185 return counts
143186
144187
145- def run (output_dir : Path = OUTPUT_DIR , exclude : list [str ] | None = None ) -> None :
188+ def run (
189+ output_dir : Path = OUTPUT_DIR ,
190+ exclude : list [str ] | None = None ,
191+ only : list [str ] | None = None ,
192+ ) -> None :
146193 from sqlmodel import Session
147194
148195 from app .database import create_db_and_tables , engine
149196 from app .main import app
150197 from app .seed import seed
151198
152- collections = resolve_collections (exclude )
199+ collections = resolve_collections (exclude , only )
200+ if not collections :
201+ print ("Nothing to dump." )
202+ return
153203
154204 create_db_and_tables ()
155205 with Session (engine ) as session :
@@ -176,5 +226,23 @@ def run(output_dir: Path = OUTPUT_DIR, exclude: list[str] | None = None) -> None
176226 "writing hundreds of thousands of files that are discarded anyway."
177227 ),
178228 )
229+ parser .add_argument (
230+ "--only" ,
231+ action = "append" ,
232+ default = [],
233+ metavar = "COLLECTION" ,
234+ help = "dump just this collection, repeatable; other manifest entries are kept" ,
235+ )
236+ parser .add_argument (
237+ "--changed-since" ,
238+ metavar = "BASE" ,
239+ help = "dump only the collections affected by data changed since BASE" ,
240+ )
179241 args = parser .parse_args ()
180- run (args .output , args .exclude )
242+ only = args .only
243+ if args .changed_since :
244+ only = only + collections_for_changes (changed_paths (args .changed_since ))
245+ if not only :
246+ print ("No dump-relevant data changed." )
247+ raise SystemExit (0 )
248+ run (args .output , args .exclude , only )
0 commit comments