99from typing import Optional
1010
1111import typer
12+ from click import Choice
1213from rich import print
1314from rich .table import Table
1415from typing_extensions import Annotated
1516
1617from codesectools .datasets import DATASETS_ALL
17- from codesectools .datasets .core .cli import cli as dataset_cli
18+ from codesectools .datasets .core .dataset import Dataset
1819from codesectools .sasts import SASTS_ALL
20+ from codesectools .sasts .core .sast .requirements import DownloadableRequirement
1921
2022cli = typer .Typer (name = "cstools" , no_args_is_help = True )
2123
@@ -63,19 +65,21 @@ def status(
6365 for sast_name , sast_data in SASTS_ALL .items ():
6466 if sast_data ["status" ] == "full" :
6567 table .add_row (
66- sast_name , "Full ✅" , f"See subcommand [b]{ sast_name .lower ()} [/b]"
68+ sast_name ,
69+ "Full ✅" ,
70+ "[b]Analysis[/b] and [b]result parsing[/b] are available" ,
6771 )
6872 elif sast_data ["status" ] == "partial" :
6973 table .add_row (
7074 sast_name ,
7175 "Partial ⚠️" ,
72- f"See subcommand [b]{ sast_name . lower () } [/b]\n Missing: [b ]{ sast_data ['missing' ]} [/b ]" ,
76+ f"Only [b]result parsing [/b] is available \n Missing: [red ]{ sast_data ['missing' ]} [/red ]" ,
7377 )
7478 else :
7579 table .add_row (
7680 sast_name ,
7781 "None ❌" ,
78- f"Missing : [b ]{ sast_data ['missing' ]} [/b ]" ,
82+ f"[b]Nothing[/b] is available \n Missing : [red ]{ sast_data ['missing' ]} [/red ]" ,
7983 )
8084 print (table )
8185
@@ -98,12 +102,62 @@ def status(
98102 dataset_name ,
99103 dataset .__bases__ [0 ].__name__ ,
100104 "❌" ,
101- f"[ red]cstools dataset download { dataset_name } [/red]" ,
105+ f"Download with: [i red]cstools download { dataset_name } [/i red]" ,
102106 )
103107 print (table )
104108
105109
106- cli .add_typer (dataset_cli )
110+ def get_downloadable () -> dict [str , DownloadableRequirement | Dataset ]:
111+ """Identify and collect all missing downloadable resources.
112+
113+ Collects unfulfilled `DownloadableRequirement` instances from all SASTs
114+ and un-cached `Dataset` instances.
115+
116+ Returns:
117+ A dictionary mapping the resource name to its downloadable object.
118+
119+ """
120+ downloadable = {}
121+
122+ for _ , sast_data in SASTS_ALL .items ():
123+ sast = sast_data ["sast" ]
124+ for req in sast .requirements .all :
125+ if isinstance (req , DownloadableRequirement ):
126+ if not req .is_fulfilled ():
127+ downloadable [req .name ] = req
128+
129+ for dataset_name , dataset in DATASETS_ALL .items ():
130+ dataset_instance = dataset ()
131+ if not dataset .is_cached ():
132+ downloadable [dataset_name ] = dataset_instance
133+
134+ return downloadable
135+
136+
137+ if DOWNLOADABLE := get_downloadable ():
138+
139+ @cli .command ()
140+ def download (
141+ name : Annotated [
142+ str ,
143+ typer .Argument (
144+ click_type = Choice (["all" ] + list (DOWNLOADABLE )),
145+ metavar = "NAME" ,
146+ ),
147+ ],
148+ ) -> None :
149+ """Download missing resources."""
150+ if name == "all" :
151+ targets = DOWNLOADABLE .values ()
152+ else :
153+ targets = [DOWNLOADABLE [name ]]
154+
155+ for downloadable in targets :
156+ if isinstance (downloadable , DownloadableRequirement ):
157+ downloadable .download ()
158+ else :
159+ downloadable .download_dataset ()
160+
107161
108162for _ , sast_data in SASTS_ALL .items ():
109163 cli .add_typer (sast_data ["cli_factory" ].build_cli ())
0 commit comments