1+ import os
2+ from pathlib import Path
3+ from typing import Dict , List , Union , Tuple
4+
15from . import report as r
26from .utils import assert_enum_value , get_logger
37
@@ -17,6 +21,261 @@ def __init__(self, logger=None):
1721 """
1822 self .logger = logger or get_logger ("report" )
1923
24+ def _create_title_fromdir (self , file_dirname : str ) -> str :
25+ """
26+ Infers title from a file or directory, removing leading numeric prefixes.
27+
28+ Parameters
29+ ----------
30+ file_dirname : str
31+ The file or directory name to infer the title from.
32+
33+ Returns
34+ -------
35+ str
36+ A title generated from the file or directory name.
37+ """
38+ # Remove leading numbers and underscores if they exist
39+ name = os .path .splitext (file_dirname )[0 ]
40+ parts = name .split ("_" , 1 )
41+ title = parts [1 ] if parts [0 ].isdigit () and len (parts ) > 1 else name
42+ return title .replace ("_" , " " ).title ()
43+
44+ def _create_component_config_fromfile (self , file_path : Path ) -> Dict [str , str ]:
45+ """
46+ Infers a component config from a file, including component type, plot type, and additional fields.
47+
48+ Parameters
49+ ----------
50+ file_path : Path
51+ The file path to analyze.
52+
53+ Returns
54+ -------
55+ component_config : Dict[str, str]
56+ A dictionary containing inferred component configuration.
57+ """
58+ file_ext = file_path .suffix .lower ()
59+ component_config = {}
60+
61+ # Infer component config
62+ if file_ext in [r .DataFrameFormat .CSV .value_with_dot , r .DataFrameFormat .TXT .value_with_dot ]:
63+ # Check for CSVNetworkFormat keywords
64+ if "edgelist" in file_path .stem .lower ():
65+ component_config ["component_type" ] = r .ComponentType .PLOT .value
66+ component_config ["plot_type" ] = r .PlotType .INTERACTIVE_NETWORK .value
67+ component_config ["csv_network_format" ] = r .CSVNetworkFormat .EDGELIST .value
68+ elif "adjlist" in file_path .stem .lower ():
69+ component_config ["component_type" ] = r .ComponentType .PLOT .value
70+ component_config ["plot_type" ] = r .PlotType .INTERACTIVE_NETWORK .value
71+ component_config ["csv_network_format" ] = r .CSVNetworkFormat .ADJLIST .value
72+ # Fill the config with dataframe content
73+ else :
74+ component_config ["component_type" ] = r .ComponentType .DATAFRAME .value
75+ component_config ["file_format" ] = r .DataFrameFormat .CSV .value if file_ext == r .DataFrameFormat .CSV .value_with_dot else r .DataFrameFormat .TXT .value
76+ component_config ["delimiter" ] = "," if file_ext == r .DataFrameFormat .CSV .value_with_dot else "\\ t"
77+ # Check other DataframeFormats than csv and txt
78+ elif file_ext in [fmt .value_with_dot for fmt in r .DataFrameFormat if fmt not in [r .DataFrameFormat .CSV , r .DataFrameFormat .TXT ]]:
79+ component_config ["component_type" ] = r .ComponentType .DATAFRAME .value
80+ component_config ["file_format" ] = next (fmt .value for fmt in r .DataFrameFormat if fmt .value_with_dot == file_ext )
81+ # Check for network formats
82+ elif file_ext in [fmt .value_with_dot for fmt in r .NetworkFormat ]:
83+ component_config ["component_type" ] = r .ComponentType .PLOT .value
84+ if file_ext in [
85+ r .NetworkFormat .PNG .value_with_dot ,
86+ r .NetworkFormat .JPG .value_with_dot ,
87+ r .NetworkFormat .JPEG .value_with_dot ,
88+ r .NetworkFormat .SVG .value_with_dot ,
89+ ]:
90+ component_config ["plot_type" ] = r .PlotType .STATIC .value
91+ else :
92+ component_config ["plot_type" ] = r .PlotType .INTERACTIVE_NETWORK .value
93+ # Check for interactive plots
94+ elif file_ext == ".json" :
95+ component_config ["component_type" ] = r .ComponentType .PLOT .value
96+ if "plotly" in file_path .stem .lower ():
97+ component_config ["plot_type" ] = r .PlotType .PLOTLY .value
98+ elif "altair" in file_path .stem .lower ():
99+ component_config ["plot_type" ] = r .PlotType .ALTAIR .value
100+ else :
101+ component_config ["plot_type" ] = "unknown"
102+ elif file_ext == ".md" :
103+ component_config ["component_type" ] = r .ComponentType .MARKDOWN .value
104+ else :
105+ error_msg = (
106+ f"Unsupported file extension: { file_ext } . "
107+ f"Supported extensions include:\n "
108+ f" - Network formats: { ', ' .join (fmt .value_with_dot for fmt in r .NetworkFormat )} \n "
109+ f" - DataFrame formats: { ', ' .join (fmt .value_with_dot for fmt in r .DataFrameFormat )} "
110+ )
111+ #self.logger.error(error_msg)
112+ raise ValueError (error_msg )
113+
114+ return component_config
115+
116+ def _sort_paths_by_numprefix (self , paths : List [Path ]) -> List [Path ]:
117+ """
118+ Sorts a list of Paths by numeric prefixes in their names, placing non-numeric items at the end.
119+
120+ Parameters
121+ ----------
122+ paths : List[Path]
123+ The list of Path objects to sort.
124+
125+ Returns
126+ -------
127+ List[Path]
128+ The sorted list of Path objects.
129+ """
130+ def get_sort_key (path : Path ) -> tuple :
131+ parts = path .name .split ("_" , 1 )
132+ if parts [0 ].isdigit ():
133+ numeric_prefix = int (parts [0 ])
134+ else :
135+ # Non-numeric prefixes go to the end
136+ numeric_prefix = float ('inf' )
137+ return numeric_prefix , path .name .lower ()
138+
139+ return sorted (paths , key = get_sort_key )
140+
141+ def _create_subsect_config_fromdir (self , subsection_dir_path : Path ) -> Dict [str , Union [str , List [Dict ]]]:
142+ """
143+ Creates subsection config from a directory.
144+
145+ Parameters
146+ ----------
147+ subsection_dir_path : Path
148+ Path to the subsection directory.
149+
150+ Returns
151+ -------
152+ Dict[str, Union[str, List[Dict]]]
153+ The subsection config.
154+ """
155+ subsection_config = {
156+ "title" : self ._create_title_fromdir (subsection_dir_path .name ),
157+ "description" : "" ,
158+ "components" : [],
159+ }
160+
161+ # Sort files by number prefix
162+ sorted_files = self ._sort_paths_by_numprefix (list (subsection_dir_path .iterdir ()))
163+
164+ for file in sorted_files :
165+ if file .is_file ():
166+ component_config = self ._create_component_config_fromfile (file )
167+
168+ # Ensure the file path is absolute
169+ file_path = file .resolve ()
170+
171+ component_config_updt = {
172+ "title" : self ._create_title_fromdir (file .name ),
173+ "file_path" : str (file_path ),
174+ "description" : "" ,
175+ }
176+
177+ # Update inferred config information
178+ component_config .update (component_config_updt )
179+
180+ subsection_config ["components" ].append (component_config )
181+
182+ return subsection_config
183+
184+ def _create_sect_config_fromdir (self , section_dir_path : Path ) -> Dict [str , Union [str , List [Dict ]]]:
185+ """
186+ Creates section config from a directory.
187+
188+ Parameters
189+ ----------
190+ section_dir_path : Path
191+ Path to the section directory.
192+
193+ Returns
194+ -------
195+ Dict[str, Union[str, List[Dict]]]
196+ The section config.
197+ """
198+ section_config = {
199+ "title" : self ._create_title_fromdir (section_dir_path .name ),
200+ "description" : "" ,
201+ "subsections" : [],
202+ }
203+
204+ # Sort subsections by number prefix
205+ sorted_subsections = self ._sort_paths_by_numprefix (list (section_dir_path .iterdir ()))
206+
207+ for subsection_dir in sorted_subsections :
208+ if subsection_dir .is_dir ():
209+ section_config ["subsections" ].append (self ._create_subsect_config_fromdir (subsection_dir ))
210+
211+ return section_config
212+
213+
214+ def _resolve_base_dir (self , base_dir : str ) -> Path :
215+ """
216+ Resolves the provided base directory to an absolute path from the root, accounting for relative paths.
217+
218+ Parameters
219+ ----------
220+ base_dir : str
221+ The relative or absolute path to the base directory.
222+
223+ Returns
224+ -------
225+ Path
226+ The absolute path to the base directory.
227+ """
228+ # Check if we are in a subdirectory and need to go up one level
229+ project_dir = Path (__file__ ).resolve ().parents [1 ]
230+
231+ # If the base_dir is a relative path, resolve it from the project root
232+ base_dir_path = project_dir / base_dir
233+
234+ # Make sure the resolved base directory exists
235+ if not base_dir_path .is_dir ():
236+ raise ValueError (f"Base directory '{ base_dir } ' does not exist or is not a directory." )
237+
238+ return base_dir_path
239+
240+
241+ def create_yamlconfig_fromdir (self , base_dir : str ) -> Tuple [Dict [str , Union [str , List [Dict ]]], Path ]:
242+ """
243+ Generates a YAML-compatible config file from a directory. It also returns the resolved folder path.
244+
245+ Parameters
246+ ----------
247+ base_dir : str
248+ The base directory containing section and subsection folders.
249+
250+ Returns
251+ -------
252+ Tuple[Dict[str, Union[str, List[Dict]]], Path]
253+ The YAML config and the resolved directory path.
254+ """
255+ # Get absolute path from base directory
256+ base_dir_path = self ._resolve_base_dir (base_dir )
257+
258+ # Generate the YAML config
259+ yaml_config = {
260+ "report" : {
261+ "title" : self ._create_title_fromdir (base_dir_path .name ),
262+ "description" : "" ,
263+ "graphical_abstract" : "" ,
264+ "logo" : "" ,
265+ },
266+ "sections" : [],
267+ }
268+
269+ # Sort sections by their number prefix
270+ sorted_sections = self ._sort_paths_by_numprefix (list (base_dir_path .iterdir ()))
271+
272+ # Generate sections and subsections config
273+ for section_dir in sorted_sections :
274+ if section_dir .is_dir ():
275+ yaml_config ["sections" ].append (self ._create_sect_config_fromdir (section_dir ))
276+
277+ return yaml_config , base_dir_path
278+
20279 def initialize_report (self , config : dict ) -> tuple [r .Report , dict ]:
21280 """
22281 Extracts report metadata from a YAML config file and returns a Report object and the raw metadata.
0 commit comments