1+ #!/usr/bin/env python3
2+ """
3+ Script for automatic README generation based on the /docs structure
4+ Analyzes markdown files and creates a README following the existing concept
5+ """
6+
7+ import os
8+ import re
9+ from pathlib import Path
10+ from typing import Dict , List , Tuple
11+
12+ def is_file_complete (file_path : str ) -> bool :
13+ """
14+ Checks if a markdown file is completely filled out or just a template
15+ """
16+ try :
17+ with open (file_path , 'r' , encoding = 'utf-8' ) as f :
18+ content = f .read ()
19+
20+ # Check for empty or template indicators
21+ if not content .strip ():
22+ return False
23+
24+ # Check for template indicators
25+ template_indicators = [
26+ '## \n **Description:**\n \n **Latest status:**\n \n ```\n \n ```' ,
27+ '**Description:**\n \n **Latest status:**\n \n ```\n \n ```' ,
28+ 'Example |' ,
29+ '| Name | Type | Description | Example | Required |' ,
30+ ]
31+
32+ # If it only contains template structure, it's not complete
33+ for indicator in template_indicators :
34+ if indicator in content and len (content .strip ()) < 500 :
35+ return False
36+
37+ # Check for actual API endpoints
38+ if 'GET /' in content or 'POST /' in content or 'DELETE /' in content or 'PUT /' in content :
39+ return True
40+
41+ # Check for filled query parameters or response examples
42+ if ('```json\n {' in content and content .count ('```json' ) > 1 ) or \
43+ ('startAt | number' in content ) or \
44+ ('"x":' in content and '"y":' in content ):
45+ return True
46+
47+ # Check for index/overview pages with meaningful content and links
48+ # These are complete if they have a title, description, and multiple internal links
49+ has_title = bool (re .search (r'^## \w+' , content , re .MULTILINE ))
50+ has_description = 'Description:' in content and not content .count ('Description:' ) == content .count ('**Description:**\n \n **Latest status:**' )
51+ has_internal_links = content .count ('](/docs/' ) >= 3 # At least 3 internal documentation links
52+ has_structure = content .count ('###' ) >= 2 # At least 2 subsections
53+
54+ if has_title and (has_description or has_internal_links ) and has_structure :
55+ return True
56+
57+ return False
58+
59+ except Exception :
60+ return False
61+
62+ def extract_title_from_file (file_path : str ) -> str :
63+ """
64+ Extracts the title from a markdown file
65+ """
66+ try :
67+ with open (file_path , 'r' , encoding = 'utf-8' ) as f :
68+ content = f .read ()
69+
70+ # Search for ## Title
71+ match = re .search (r'^## (.+)' , content , re .MULTILINE )
72+ if match :
73+ title = match .group (1 ).strip ()
74+ if title and title != '' :
75+ return title
76+ except Exception :
77+ pass
78+
79+ # Fallback: Use filename
80+ filename = Path (file_path ).stem
81+ return filename .replace ('-' , ' ' ).title ()
82+
83+ def scan_directory (base_path : str ) -> Dict :
84+ """
85+ Scans a directory recursively and creates a structure of markdown files
86+ """
87+ structure = {}
88+ docs_path = Path (base_path ) / 'docs'
89+
90+ if not docs_path .exists ():
91+ print (f"Error: { docs_path } does not exist!" )
92+ return structure
93+
94+ for root , dirs , files in os .walk (docs_path ):
95+ # Skip template.md
96+ if 'template.md' in files :
97+ files .remove ('template.md' )
98+
99+ for file in files :
100+ if file .endswith ('.md' ):
101+ file_path = os .path .join (root , file )
102+ relative_path = os .path .relpath (file_path , docs_path )
103+
104+ # Create structure based on path
105+ parts = relative_path .split (os .sep )
106+ current = structure
107+
108+ for part in parts [:- 1 ]: # All except the file
109+ if part not in current :
110+ current [part ] = {}
111+ current = current [part ]
112+
113+ # Add the file
114+ filename = parts [- 1 ]
115+ title = extract_title_from_file (file_path )
116+ is_complete = is_file_complete (file_path )
117+
118+ if '_files' not in current :
119+ current ['_files' ] = []
120+
121+ current ['_files' ].append ({
122+ 'filename' : filename ,
123+ 'title' : title ,
124+ 'path' : relative_path ,
125+ 'complete' : is_complete
126+ })
127+
128+ return structure
129+
130+ def generate_table_entries (files : List [Dict ]) -> List [str ]:
131+ """
132+ Generates table entries for a list of files with links to the documents
133+ """
134+ entries = []
135+ for file_info in sorted (files , key = lambda x : x ['filename' ]):
136+ state = "✅" if file_info ['complete' ] else "❌"
137+ title = file_info ['title' ]
138+ # Create relative link to the markdown file
139+ link = f"/docs/{ file_info ['path' ]} "
140+ linked_title = f"[{ title } ]({ link } )"
141+ entries .append (f"| { state } | { linked_title } |" )
142+
143+ return entries
144+
145+ def generate_table_of_contents (structure : Dict ) -> str :
146+ """
147+ Dynamically generates table of contents based on actual structure
148+ """
149+ toc = ["## Table of Contents" , "" ]
150+
151+ # Websites section
152+ if 'websites' in structure :
153+ toc .append ("- [Websites](#websites)" )
154+ websites = structure ['websites' ]
155+
156+ for section_key in sorted (websites .keys ()):
157+ if section_key != '_files' :
158+ section_name = section_key .title ()
159+ toc .append (f" - [{ section_name } ](#{ section_key .lower ()} )" )
160+
161+ # Add subsections if they exist
162+ section = websites [section_key ]
163+ if isinstance (section , dict ):
164+ for subsection_key in sorted (section .keys ()):
165+ if subsection_key != '_files' :
166+ subsection_name = subsection_key .title ()
167+ toc .append (f" - [{ subsection_name } ](#{ subsection_key .lower ()} )" )
168+
169+ # Root level sections
170+ for section_key in sorted (structure .keys ()):
171+ if section_key not in ['websites' ] and section_key != '_files' :
172+ section_name = section_key .title ()
173+ toc .append (f"- [{ section_name } ](#{ section_key .lower ()} )" )
174+
175+ # Add subsections if they exist
176+ section = structure [section_key ]
177+ if isinstance (section , dict ):
178+ for subsection_key in sorted (section .keys ()):
179+ if subsection_key != '_files' :
180+ subsection_name = subsection_key .title ()
181+ toc .append (f" - [{ subsection_name } ](#{ subsection_key .lower ()} )" )
182+
183+ return '\n ' .join (toc ) + '\n \n '
184+
185+ def generate_section_content (section_name : str , section_data : Dict , level : int = 3 ) -> str :
186+ """
187+ Recursively generates content for any section
188+ """
189+ content = ""
190+ heading = "#" * level
191+
192+ content += f"{ heading } { section_name .title ()} \n "
193+
194+ # Add files in this section
195+ if '_files' in section_data and section_data ['_files' ]:
196+ content += "| State | Name |\n "
197+ content += "| :---: | :--- |\n "
198+ entries = generate_table_entries (section_data ['_files' ])
199+ content += '\n ' .join (entries ) + '\n '
200+ else :
201+ # Check if there are subsections
202+ has_subsections = any (key != '_files' and isinstance (section_data [key ], dict )
203+ for key in section_data .keys ())
204+
205+ if not has_subsections :
206+ # No files and no subsections, add empty placeholder
207+ content += "| State | Name |\n "
208+ content += "| :---: | :--- |\n "
209+ content += "| | |\n "
210+
211+ content += "\n "
212+
213+ # Process subsections
214+ for subsection_key in sorted (section_data .keys ()):
215+ if subsection_key != '_files' and isinstance (section_data [subsection_key ], dict ):
216+ content += generate_section_content (subsection_key , section_data [subsection_key ], level + 1 )
217+
218+ return content
219+
220+ def generate_readme_content (structure : Dict ) -> str :
221+ """
222+ Generates README content based on the structure - now completely dynamic
223+ """
224+ content = "# umami API docs\n \n "
225+
226+ # Add status legend
227+ content += "### 📊 Documentation Status Legend\n "
228+ content += "| Symbol | Meaning |\n "
229+ content += "| :---: | :--- |\n "
230+ content += "| ✅ | Documentation complete – file contains full API specification |\n "
231+ content += "| ❌ | Documentation incomplete – file is template or missing content |\n \n "
232+
233+ # Generate dynamic table of contents
234+ content += generate_table_of_contents (structure )
235+
236+ content += "---\n ---\n \n "
237+ # Generate Websites section
238+ if 'websites' in structure :
239+ content += "## Websites\n \n "
240+ websites_content = generate_section_content ("websites" , structure ['websites' ], level = 2 )
241+ # Remove the first line (## Websites) since we already added it
242+ content += '\n ' .join (websites_content .split ('\n ' )[1 :])
243+
244+ content += "--- \n \n "
245+
246+ # Generate all other root-level sections dynamically
247+ for section_key in sorted (structure .keys ()):
248+ if section_key not in ['websites' ] and section_key != '_files' :
249+ content += generate_section_content (section_key , structure [section_key ], level = 2 )
250+ content += "---\n \n "
251+
252+ return content .rstrip () + "\n "
253+
254+ def main ():
255+ """
256+ Main function of the script
257+ """
258+ # Get the parent directory of .github (the repository root)
259+ base_path = os .path .dirname (os .path .dirname (os .path .abspath (__file__ )))
260+
261+ print ("🔍 Scanning /docs directory..." )
262+ structure = scan_directory (base_path )
263+
264+ print ("📝 Generating README content..." )
265+ readme_content = generate_readme_content (structure )
266+
267+ # Write new README
268+ readme_path = os .path .join (base_path , 'README.md' )
269+
270+ print (f"💾 Writing README to { readme_path } ..." )
271+ with open (readme_path , 'w' , encoding = 'utf-8' ) as f :
272+ f .write (readme_content )
273+
274+ print ("✅ README successfully generated!" )
275+
276+ # Output statistics
277+ total_files = 0
278+ complete_files = 0
279+
280+ def count_files (struct ):
281+ nonlocal total_files , complete_files
282+ if isinstance (struct , dict ):
283+ if '_files' in struct :
284+ for file_info in struct ['_files' ]:
285+ total_files += 1
286+ if file_info ['complete' ]:
287+ complete_files += 1
288+ for key , value in struct .items ():
289+ if key != '_files' :
290+ count_files (value )
291+
292+ count_files (structure )
293+
294+ print (f"\n 📊 Statistics:" )
295+ print (f" Total files found: { total_files } " )
296+ print (f" Completely filled: { complete_files } " )
297+ print (f" Still to be processed: { total_files - complete_files } " )
298+ print (f" Progress: { (complete_files / total_files * 100 ):.1f} %" if total_files > 0 else " Progress: 0%" )
299+
300+ if __name__ == "__main__" :
301+ main ()
0 commit comments