cb-dsa 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cb_dsa-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: cb-dsa
3
+ Version: 0.1.0
4
+ Summary: A customizable CLI for automating Data Structures and Algorithms tracking.
5
+ Author-email: Cibidharan <your-email@example.com>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: typer>=0.9.0
10
+ Requires-Dist: rich>=13.0.0
11
+ Requires-Dist: requests>=2.28.0
12
+ Requires-Dist: beautifulsoup4>=4.12.0
13
+
14
+ # cb-dsa
15
+
16
+ A Command Line Interface (CLI) tool for automating your Data Structures and Algorithms (DSA) problem-solving workflow.
17
+
18
+ It helps you automatically create problem files, sync your LeetCode submissions, and generate detailed markdown documentation and statistics.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install cb-dsa
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```bash
29
+ # Initialize inside your project folder
30
+ cb-dsa init
31
+
32
+ # Sync your recent accepted LeetCode submissions
33
+ cb-dsa sync
34
+
35
+ # Manually create a new problem template (grabs from clipboard)
36
+ cb-dsa create
37
+
38
+ # Re-generate the README.md and stats dashboard
39
+ cb-dsa generate
40
+ ```
cb_dsa-0.1.0/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # cb-dsa
2
+
3
+ A Command Line Interface (CLI) tool for automating your Data Structures and Algorithms (DSA) problem-solving workflow.
4
+
5
+ It helps you automatically create problem files, sync your LeetCode submissions, and generate detailed markdown documentation and statistics.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install cb-dsa
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ # Initialize inside your project folder
17
+ cb-dsa init
18
+
19
+ # Sync your recent accepted LeetCode submissions
20
+ cb-dsa sync
21
+
22
+ # Manually create a new problem template (grabs from clipboard)
23
+ cb-dsa create
24
+
25
+ # Re-generate the README.md and stats dashboard
26
+ cb-dsa generate
27
+ ```
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cb-dsa"
7
+ version = "0.1.0"
8
+ description = "A customizable CLI for automating Data Structures and Algorithms tracking."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Cibidharan", email = "your-email@example.com" }
14
+ ]
15
+ dependencies = [
16
+ "typer>=0.9.0",
17
+ "rich>=13.0.0",
18
+ "requests>=2.28.0",
19
+ "beautifulsoup4>=4.12.0",
20
+ ]
21
+
22
+ [project.scripts]
23
+ cb-dsa = "cb_dsa.main:app"
cb_dsa-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,341 @@
1
+ import os
2
+ import sys
3
+ import json
4
+ import re
5
+ import datetime
6
+ import subprocess
7
+
8
+ from cb_dsa.core.utils import (
9
+ LANG_EXTENSIONS, get_session_cookie, get_csrf_token, fetch_problem_data,
10
+ fetch_submission_details, get_slug_from_id, get_last_category, save_last_category,
11
+ get_switch_topic, reset_switch_topic, get_metadata, clean_code, multiline_input
12
+ )
13
+
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.prompt import Prompt
17
+ from rich.table import Table
18
+ from rich import print as rprint
19
+
20
+ console = Console()
21
+
22
+ def get_clipboard_text():
23
+ try:
24
+ import tkinter as tk
25
+ root = tk.Tk()
26
+ root.withdraw()
27
+ clip = root.clipboard_get()
28
+ root.destroy()
29
+ return clip
30
+ except Exception:
31
+ return ""
32
+
33
+ def create_problem():
34
+ base_dir = os.getcwd()
35
+ console.print(Panel.fit(" [bold blue]Create New DSA Problem[/bold blue] ", border_style="blue"))
36
+
37
+ categories = [d for d in os.listdir(base_dir) if os.path.isdir(os.path.join(base_dir, d)) and not d.startswith('.')]
38
+ category_map = {c.lower(): c for c in categories}
39
+ category_map['array'] = 'Arrays'
40
+ category_map['hash table'] = 'HashMaps'
41
+ category_map['dynamic programming'] = 'DP'
42
+
43
+ clip_text = get_clipboard_text().strip()
44
+ match = re.search(r'leetcode\.com/problems/([a-zA-Z0-9\-]+)', clip_text)
45
+
46
+ lc_data = None
47
+ num = None
48
+ submission_link = ""
49
+ custom_code = ""
50
+ question_link = ""
51
+ lang_ext = ".cpp"
52
+
53
+ if match:
54
+ slug = match.group(1)
55
+ console.print(f"[bold green]✓[/bold green] Detected LeetCode URL for [bold cyan]'{slug}'[/bold cyan] in clipboard!")
56
+ if '/submissions/' in clip_text:
57
+ submission_link = clip_text
58
+ console.print("[bold green]✓[/bold green] Detected submission link in clipboard!")
59
+
60
+ with console.status(f"[bold cyan]Fetching data for '{slug}'...[/bold cyan]", spinner="dots"):
61
+ lc_data = fetch_problem_data(slug)
62
+ if lc_data:
63
+ num = lc_data['questionFrontendId']
64
+ console.print(f"[bold green]✓[/bold green] Fetched data for problem: [bold white]{num}. {lc_data['title']}[/bold white]")
65
+
66
+ if not lc_data:
67
+ console.print("\n[yellow]⚠ No valid LeetCode link in clipboard.[/yellow]")
68
+ console.print("1. LeetCode (Input ID)")
69
+ console.print("2. Other Platform")
70
+ choice = Prompt.ask("[bold cyan]Choose option[/bold cyan]", choices=["1", "2"], default="1")
71
+
72
+ if choice == "1":
73
+ while True:
74
+ num_input = Prompt.ask("[bold cyan]Problem number[/bold cyan]").strip()
75
+ if num_input:
76
+ num = num_input
77
+ break
78
+ slug = get_slug_from_id(num)
79
+ if slug:
80
+ with console.status(f"[bold cyan]Fetching data for problem {num}...[/bold cyan]", spinner="dots"):
81
+ lc_data = fetch_problem_data(slug)
82
+
83
+ # Process problem data
84
+ category = None
85
+ if lc_data:
86
+ title = lc_data['title']
87
+ slug = lc_data['titleSlug']
88
+ difficulty = lc_data['difficulty']
89
+
90
+ try:
91
+ similar_questions_list = json.loads(lc_data['similarQuestions'])
92
+ similar_problems = [(q['title'], q['titleSlug']) for q in similar_questions_list]
93
+ except:
94
+ similar_problems = []
95
+
96
+ tags = []
97
+ if 'topicTags' in lc_data and lc_data['topicTags']:
98
+ for tag in lc_data['topicTags']:
99
+ name = tag['name']
100
+ if 'contest' not in name.lower():
101
+ tags.append(name)
102
+ lc_data['filtered_tags'] = tags
103
+
104
+ if tags:
105
+ switch_topic = get_switch_topic(base_dir)
106
+ if not switch_topic:
107
+ last_category = get_last_category(base_dir)
108
+
109
+ # Check if any tag matches the last category
110
+ if last_category:
111
+ for tag in tags:
112
+ tag_lower = tag.lower()
113
+ mapped_category = category_map.get(tag_lower, category_map.get(tag_lower + 's', tag_lower))
114
+ if mapped_category.lower() == last_category.lower() or tag_lower == last_category.lower():
115
+ category = last_category
116
+ break
117
+
118
+ # Fallback to primary tag if no match with last_category
119
+ if not category:
120
+ primary_tag = tags[0].lower()
121
+ if primary_tag in category_map:
122
+ category = category_map[primary_tag]
123
+ elif primary_tag + 's' in category_map:
124
+ category = category_map[primary_tag + 's']
125
+
126
+ # Enforce topic-grinding mode: auto-map ONLY if it matches the last category you worked on
127
+ if last_category and category and category.lower() != last_category.lower():
128
+ category = None
129
+
130
+ name = title
131
+ else:
132
+ console.print("\n[yellow]⚠ Manual Problem Setup[/yellow]")
133
+ while True:
134
+ name = Prompt.ask("[bold cyan]Problem name[/bold cyan]").strip()
135
+ if name:
136
+ break
137
+
138
+ num_input = Prompt.ask("[bold cyan]Problem number/ID (e.g., 100, CF-123)[/bold cyan]", default="000").strip()
139
+ num = num_input if num_input else "000"
140
+
141
+ question_link = Prompt.ask("[bold cyan]Question Link[/bold cyan]").strip()
142
+ submission_link = Prompt.ask("[bold cyan]Submission Link (optional)[/bold cyan]").strip()
143
+
144
+ difficulty = Prompt.ask("[bold cyan]Difficulty[/bold cyan]", choices=["Easy", "Medium", "Hard", "Basic", "School", "Other"], default="Medium").strip()
145
+
146
+ custom_code = multiline_input("\nPaste your code here")
147
+
148
+ ext_input = Prompt.ask("[bold cyan]File extension (e.g., .cpp, .py)[/bold cyan]", default=".cpp").strip()
149
+ if ext_input:
150
+ lang_ext = ext_input if ext_input.startswith('.') else '.' + ext_input
151
+
152
+ similar_problems = []
153
+ tags = []
154
+ slug = name.lower().replace(' ', '-').replace(':', '')
155
+ title = name.title()
156
+
157
+ # Get Category if not auto-mapped
158
+ if not category:
159
+ options = list(categories)
160
+ if tags:
161
+ for t in tags:
162
+ # Add tags that don't already perfectly match an existing folder name
163
+ if not any(t.lower() == existing.lower() for existing in options):
164
+ options.append(t)
165
+
166
+ console.print("\n[bold yellow]Could not auto-map category. Where should this problem go?[/bold yellow]")
167
+
168
+ table = Table(show_header=False, box=None)
169
+ table.add_column("Index", style="cyan")
170
+ table.add_column("Category")
171
+ for i, opt in enumerate(options):
172
+ table.add_row(f"{i + 1}.", opt)
173
+ table.add_row(f"{len(options) + 1}.", "[bold]Create a new category folder[/bold]")
174
+ console.print(table)
175
+
176
+ while True:
177
+ category_input = Prompt.ask(f"[bold cyan]Choose one option (1-{len(options) + 1})[/bold cyan]").strip()
178
+ if category_input.isdigit():
179
+ idx = int(category_input)
180
+ if 1 <= idx <= len(options):
181
+ category = options[idx - 1]
182
+ break
183
+ elif idx == len(options) + 1:
184
+ category = Prompt.ask("[bold cyan]Enter new category name[/bold cyan]").strip()
185
+ if category: break
186
+ elif category_input:
187
+ category = category_input
188
+ break
189
+
190
+ save_last_category(base_dir, category)
191
+ reset_switch_topic(base_dir)
192
+
193
+ category_path = os.path.join(base_dir, category)
194
+ if not os.path.exists(category_path):
195
+ os.makedirs(category_path)
196
+ console.print(f"[bold green]✓ Created new category folder:[/bold green] [cyan]{category}[/cyan]")
197
+
198
+ if not submission_link and lc_data:
199
+ submission_link = Prompt.ask("[bold cyan]Submission link (optional, press Enter to skip)[/bold cyan]", default="").strip()
200
+
201
+ clean_name = re.sub(r'[^a-zA-Z0-9\s\-]', '', name)
202
+ formatted_name = clean_name.lower().replace(' ', '_').replace('-', '_')
203
+ folder_name = f"{num}_{formatted_name}"
204
+ folder_path = os.path.join(category_path, folder_name)
205
+
206
+ if lc_data:
207
+ question_link = f"https://leetcode.com/problems/{slug}/"
208
+ elif not question_link:
209
+ question_link = f"https://example.com/problems/{slug}/"
210
+
211
+ if not os.path.exists(folder_path):
212
+ os.makedirs(folder_path)
213
+
214
+ # Attempt to fetch code if submission link is provided
215
+ code_content = custom_code if not lc_data else ""
216
+ if lc_data and submission_link:
217
+ sub_id_match = re.search(r'submissions/(?:detail/)?(\d+)', submission_link)
218
+ if sub_id_match:
219
+ sub_id = sub_id_match.group(1)
220
+ session_cookie = get_session_cookie(base_dir, prompt_if_missing=True)
221
+
222
+ if session_cookie:
223
+ with console.status(f"[bold cyan]Fetching code for submission {sub_id}...[/bold cyan]", spinner="dots"):
224
+ csrf_token = get_csrf_token(session_cookie)
225
+ details = fetch_submission_details(sub_id, session_cookie, csrf_token)
226
+ if details:
227
+ code_content = details.get('code', '')
228
+ lang_name = details.get('lang', {}).get('name', '')
229
+ lang_ext = LANG_EXTENSIONS.get(lang_name, f".{lang_name}")
230
+ console.print(f"[bold green]✓ Successfully fetched {lang_name} code![/bold green]")
231
+ else:
232
+ console.print("[bold red]✗ Could not retrieve submission code (possibly expired or invalid cookie).[/bold red]")
233
+
234
+ if not code_content:
235
+ if lang_ext in ['.py', '.rb']:
236
+ code_content = f"# {num}. {title}\n\n"
237
+ else:
238
+ code_content = f"// {num}. {title}\n\n"
239
+
240
+ meta = get_metadata(code_content, interactive=True)
241
+ code_content = clean_code(code_content)
242
+
243
+ suffix = ""
244
+ counter = 1
245
+ while True:
246
+ code_file_name = f"{folder_name}_soln{suffix}{lang_ext}"
247
+ code_file_path = os.path.join(folder_path, code_file_name)
248
+
249
+ md_file_name = f"{folder_name}_doc{suffix}.md"
250
+ md_file_path = os.path.join(folder_path, md_file_name)
251
+
252
+ if not os.path.exists(code_file_path) and not os.path.exists(md_file_path):
253
+ break
254
+
255
+ suffix = str(counter)
256
+ counter += 1
257
+
258
+ with open(code_file_path, 'w', encoding='utf-8') as f:
259
+ f.write(code_content)
260
+
261
+ current_date = datetime.datetime.now().strftime("%Y-%m-%d")
262
+
263
+ md_content = f"# [{num}. {title}]({question_link})\n\n"
264
+ md_content += f"## Date\n{current_date}\n\n"
265
+ md_content += f"## Difficulty\n{difficulty}\n\n"
266
+
267
+ md_content += f"## Topics\n"
268
+ if tags:
269
+ for t in tags:
270
+ md_content += f"- {t}\n"
271
+ else:
272
+ md_content += f"- {category}\n"
273
+ md_content += f"\n---\n\n"
274
+
275
+ md_content += f"## Idea\n\n"
276
+ if submission_link:
277
+ if lc_data:
278
+ md_content += f"[View Submission on LeetCode]({submission_link})\n\n"
279
+ else:
280
+ md_content += f"[View Submission]({submission_link})\n\n"
281
+
282
+ if meta.get('idea'):
283
+ md_content += f"{meta['idea']}\n\n"
284
+
285
+ md_content += f"---\n\n"
286
+ md_content += f"## Time Complexity\n\n{meta['time']}\n\n## Space Complexity\n\n{meta['space']}\n\n---\n\n"
287
+
288
+ if meta.get('learning'):
289
+ md_content += f"## Key Learning\n\n{meta['learning']}\n\n---\n\n"
290
+
291
+ if meta.get('mistakes'):
292
+ md_content += f"## Mistakes Made\n\n{meta['mistakes']}\n\n---\n\n"
293
+
294
+ if similar_problems:
295
+ md_content += f"## Similar Problems\n\n"
296
+ for sp_title, sp_slug in similar_problems:
297
+ md_content += f"- [{sp_title}](https://leetcode.com/problems/{sp_slug}/)\n"
298
+ else:
299
+ md_content += f"## Similar Problems\n\n- \n"
300
+
301
+ with open(md_file_path, 'w', encoding='utf-8') as f:
302
+ f.write(md_content)
303
+
304
+ console.print("\n")
305
+ success_panel = Panel(
306
+ f"[bold]Folder:[/bold] {os.path.relpath(folder_path, base_dir)}\n"
307
+ f"[bold]Code:[/bold] {os.path.relpath(code_file_path, base_dir)}\n"
308
+ f"[bold]Doc:[/bold] {os.path.relpath(md_file_path, base_dir)}",
309
+ title="[bold green]Success! Created Files[/bold green]",
310
+ border_style="green",
311
+ expand=False
312
+ )
313
+ console.print(success_panel)
314
+
315
+ with console.status("[bold cyan]Updating README statistics...[/bold cyan]", spinner="dots"):
316
+ from cb_dsa.commands.generate import update_readme
317
+ update_readme()
318
+ console.print("[bold green]✓ README updated![/bold green]")
319
+
320
+ console.print("\n[bold magenta]" + "="*40 + "[/bold magenta]")
321
+ console.print("[bold white]Files have been generated and README updated![/bold white]")
322
+
323
+ ans = Prompt.ask("\n[bold cyan]Press Enter to commit and push, or type any character to skip[/bold cyan]", default="")
324
+ if ans == '':
325
+ try:
326
+ with console.status("[bold cyan]Committing to Git...[/bold cyan]", spinner="dots"):
327
+ subprocess.run(["git", "add", folder_path, "README.md", "docs/data.js", "docs/index.html"], check=True, capture_output=True)
328
+ commit_message = f"{num}. {title}"
329
+ subprocess.run(["git", "commit", "-m", commit_message], check=True, capture_output=True)
330
+ console.print(f"[bold green]✓ Successfully committed to Git:[/bold green] '{commit_message}'")
331
+
332
+ console.print("[bold cyan]Pushing to GitHub...[/bold cyan]")
333
+ subprocess.run(["git", "push", "origin", "main"], check=True)
334
+ console.print("[bold green]✓ Successfully pushed to GitHub![/bold green]")
335
+ except Exception as e:
336
+ console.print(f"[bold red]✗ Failed to auto-commit or push:[/bold red] {e}")
337
+ else:
338
+ console.print("[yellow]Skipped commit and push.[/yellow]")
339
+
340
+ if __name__ == "__main__":
341
+ create_problem()
@@ -0,0 +1,242 @@
1
+ import os
2
+ import re
3
+ import datetime
4
+ import json
5
+ import urllib.parse
6
+ from rich.console import Console
7
+
8
+ console = Console()
9
+
10
+ def normalize_topic(topic):
11
+ mapping = {
12
+ 'arrays': 'Array',
13
+ 'hashmaps': 'Hash Table',
14
+ 'hash map': 'Hash Table',
15
+ 'dp': 'Dynamic Programming'
16
+ }
17
+ return mapping.get(topic.lower().strip(), topic.strip())
18
+
19
+ def parse_doc_file(filepath, category):
20
+ with open(filepath, 'r', encoding='utf-8') as f:
21
+ content = f.read()
22
+
23
+ # Extract Title and Link
24
+ title_match = re.search(r'# \[([a-zA-Z0-9\-]+)\.\s+(.*?)\]\((.*?)\)', content)
25
+ if not title_match:
26
+ title_match = re.search(r'# ([a-zA-Z0-9\-]+)\.\s+(.*)', content)
27
+ if title_match:
28
+ num = title_match.group(1)
29
+ title = title_match.group(2).strip()
30
+ link = ""
31
+ else:
32
+ return None
33
+ else:
34
+ num = title_match.group(1)
35
+ title = title_match.group(2).strip()
36
+ link = title_match.group(3).strip()
37
+
38
+ # Extract Difficulty
39
+ difficulty_match = re.search(r'## Difficulty\s*\n\s*(Easy|Medium|Hard)', content, re.IGNORECASE)
40
+ difficulty = difficulty_match.group(1).capitalize() if difficulty_match else "Unknown"
41
+
42
+ # Extract Date
43
+ date_match = re.search(r'## Date\s*\n\s*(\d{4}-\d{2}-\d{2})', content)
44
+ if date_match:
45
+ date_solved = date_match.group(1)
46
+ else:
47
+ ctime = os.path.getctime(filepath)
48
+ date_solved = datetime.datetime.fromtimestamp(ctime).strftime("%Y-%m-%d")
49
+
50
+ # Extract Topics
51
+ topics_match = re.search(r'## Topics\s*\n(.*?)(?=\n##|\Z)', content, re.DOTALL)
52
+ topics = []
53
+ if topics_match:
54
+ for line in topics_match.group(1).split('\n'):
55
+ line = line.strip()
56
+ if line.startswith('- '):
57
+ topic = line[2:].strip()
58
+ if topic:
59
+ norm_topic = normalize_topic(topic)
60
+ if norm_topic not in topics:
61
+ topics.append(norm_topic)
62
+ if not topics:
63
+ topics.append(normalize_topic(category))
64
+
65
+ return {
66
+ 'num': num,
67
+ 'title': title,
68
+ 'link': link,
69
+ 'difficulty': difficulty,
70
+ 'category': category,
71
+ 'topics': topics,
72
+ 'date': date_solved,
73
+ 'folder_path': os.path.dirname(filepath)
74
+ }
75
+
76
+ def update_readme():
77
+ base_dir = os.getcwd()
78
+ problems = []
79
+
80
+ categories = [d for d in os.listdir(base_dir) if os.path.isdir(os.path.join(base_dir, d)) and not d.startswith('.') and d not in ['cb-dsa', 'docs', '__pycache__']]
81
+
82
+ for category in categories:
83
+ category_path = os.path.join(base_dir, category)
84
+
85
+ for item in os.listdir(category_path):
86
+ item_path = os.path.join(category_path, item)
87
+ if not os.path.isdir(item_path):
88
+ continue
89
+
90
+ doc_files = [f for f in os.listdir(item_path) if f.endswith('_doc.md') or f.endswith('_doc1.md') or '_doc' in f and f.endswith('.md')]
91
+
92
+ if not doc_files:
93
+ continue
94
+
95
+ doc_file = doc_files[0]
96
+ doc_path = os.path.join(item_path, doc_file)
97
+
98
+ problem_data = parse_doc_file(doc_path, category)
99
+ if problem_data:
100
+ problems.append(problem_data)
101
+
102
+ def sort_key(x):
103
+ num_str = str(x['num'])
104
+ m = re.match(r'(\d+)(.*)', num_str)
105
+ if m:
106
+ return (int(m.group(1)), m.group(2))
107
+ return (float('inf'), num_str)
108
+
109
+ problems.sort(key=sort_key)
110
+
111
+ total_problems = len(problems)
112
+ difficulty_counts = {'Easy': 0, 'Medium': 0, 'Hard': 0, 'Unknown': 0}
113
+ category_stats = {}
114
+ topic_stats = {}
115
+
116
+ for p in problems:
117
+ diff = p['difficulty']
118
+ cat = p['category']
119
+
120
+ difficulty_counts[diff] = difficulty_counts.get(diff, 0) + 1
121
+
122
+ if cat not in category_stats:
123
+ category_stats[cat] = {'Total': 0, 'Easy': 0, 'Medium': 0, 'Hard': 0, 'Unknown': 0}
124
+
125
+ category_stats[cat]['Total'] += 1
126
+ if diff in category_stats[cat]:
127
+ category_stats[cat][diff] += 1
128
+ else:
129
+ category_stats[cat]['Unknown'] += 1
130
+
131
+ for t in p['topics']:
132
+ if t not in topic_stats:
133
+ topic_stats[t] = 0
134
+ topic_stats[t] += 1
135
+
136
+ def get_bar(count, total, length=20):
137
+ if total == 0: return '░' * length
138
+ filled = int(round((count / total) * length))
139
+ return '█' * filled + '░' * (length - filled)
140
+
141
+ easy_count = difficulty_counts.get('Easy', 0)
142
+ medium_count = difficulty_counts.get('Medium', 0)
143
+ hard_count = difficulty_counts.get('Hard', 0)
144
+
145
+ easy_pct = (easy_count / total_problems * 100) if total_problems else 0
146
+ medium_pct = (medium_count / total_problems * 100) if total_problems else 0
147
+ hard_pct = (hard_count / total_problems * 100) if total_problems else 0
148
+
149
+ stats_content = f"**Total Problems Solved:** {total_problems}\n\n"
150
+ stats_content += "### Difficulty Breakdown\n"
151
+ stats_content += f"- 🟢 **Easy** ({easy_pct:.1f}%): `{get_bar(easy_count, total_problems)}` ({easy_count})\n"
152
+ stats_content += f"- 🟡 **Medium** ({medium_pct:.1f}%): `{get_bar(medium_count, total_problems)}` ({medium_count})\n"
153
+ stats_content += f"- 🔴 **Hard** ({hard_pct:.1f}%): `{get_bar(hard_count, total_problems)}` ({hard_count})\n\n"
154
+
155
+ stats_content += "### Topic Breakdown\n\n"
156
+ stats_content += "| Topic | Total | 🟢 Easy | 🟡 Medium | 🔴 Hard |\n"
157
+ stats_content += "|---|---|---|---|---|\n"
158
+
159
+ for cat, stats in sorted(category_stats.items(), key=lambda item: item[1]['Total'], reverse=True):
160
+ stats_content += f"| **{cat}** | {stats['Total']} | {stats['Easy']} | {stats['Medium']} | {stats['Hard']} |\n"
161
+
162
+ table_content = "| # | Title | Difficulty | Topic | Solution |\n"
163
+ table_content += "|---|---|---|---|---|\n"
164
+
165
+ for p in problems:
166
+ rel_folder = os.path.relpath(p['folder_path'], base_dir).replace('\\', '/')
167
+ encoded_folder = urllib.parse.quote(rel_folder)
168
+
169
+ p['rel_folder'] = rel_folder
170
+ p['encoded_folder'] = encoded_folder
171
+
172
+ title_link = f"[{p['title']}]({p['link']})" if p['link'] else p['title']
173
+
174
+ diff_emoji = ""
175
+ if p['difficulty'] == 'Easy': diff_emoji = "🟢 Easy"
176
+ elif p['difficulty'] == 'Medium': diff_emoji = "🟡 Medium"
177
+ elif p['difficulty'] == 'Hard': diff_emoji = "🔴 Hard"
178
+ else: diff_emoji = p['difficulty']
179
+
180
+ folder_link = f"[Code & Doc](./{encoded_folder})"
181
+
182
+ table_content += f"| {p['num']} | {title_link} | {diff_emoji} | {p['category']} | {folder_link} |\n"
183
+
184
+ readme_path = os.path.join(base_dir, "README.md")
185
+
186
+ if os.path.exists(readme_path):
187
+ with open(readme_path, 'r', encoding='utf-8') as f:
188
+ readme_text = f.read()
189
+ else:
190
+ readme_text = "# [DSA Practice Repository](https://cibidharan-rv.github.io/dsa/)\n\n## 📊 Statistics\n\n<!-- STATS:START -->\n<!-- STATS:END -->\n\n---\n\n## 📝 Problem Index\n\n<!-- TABLE:START -->\n<!-- TABLE:END -->\n"
191
+
192
+ stats_pattern = r'<!-- STATS:START -->.*?<!-- STATS:END -->'
193
+ stats_replacement = f"<!-- STATS:START -->\n{stats_content}\n<!-- STATS:END -->"
194
+ if re.search(stats_pattern, readme_text, re.DOTALL):
195
+ readme_text = re.sub(stats_pattern, stats_replacement, readme_text, flags=re.DOTALL)
196
+ else:
197
+ readme_text += f"\n\n## 📊 Statistics\n\n{stats_replacement}"
198
+
199
+ table_pattern = r'<!-- TABLE:START -->.*?<!-- TABLE:END -->'
200
+ table_replacement = f"<!-- TABLE:START -->\n{table_content}\n<!-- TABLE:END -->"
201
+ if re.search(table_pattern, readme_text, re.DOTALL):
202
+ readme_text = re.sub(table_pattern, table_replacement, readme_text, flags=re.DOTALL)
203
+ else:
204
+ readme_text += f"\n\n## 📝 Problem Index\n\n{table_replacement}"
205
+
206
+ with open(readme_path, 'w', encoding='utf-8') as f:
207
+ f.write(readme_text)
208
+
209
+ docs_dir = os.path.join(base_dir, "docs")
210
+ if not os.path.exists(docs_dir):
211
+ os.makedirs(docs_dir)
212
+
213
+ dashboard_data = {
214
+ 'total': total_problems,
215
+ 'difficulty': difficulty_counts,
216
+ 'categories': category_stats,
217
+ 'topic_stats': topic_stats,
218
+ 'problems': problems
219
+ }
220
+
221
+ js_content = f"const dsaData = {json.dumps(dashboard_data, indent=2)};"
222
+ with open(os.path.join(docs_dir, "data.js"), 'w', encoding='utf-8') as f:
223
+ f.write(js_content)
224
+
225
+ import time
226
+ index_path = os.path.join(docs_dir, "index.html")
227
+ if os.path.exists(index_path):
228
+ with open(index_path, 'r', encoding='utf-8') as f:
229
+ index_content = f.read()
230
+
231
+ timestamp = int(time.time())
232
+ index_content = re.sub(r'data\.js(\?v=\d+)?', f'data.js?v={timestamp}', index_content)
233
+ index_content = re.sub(r'app\.js(\?v=\d+)?', f'app.js?v={timestamp}', index_content)
234
+ index_content = re.sub(r'styles\.css(\?v=\d+)?', f'styles.css?v={timestamp}', index_content)
235
+
236
+ with open(index_path, 'w', encoding='utf-8') as f:
237
+ f.write(index_content)
238
+
239
+ console.print(f"[bold green]✓ Successfully updated README.md and docs/data.js with {total_problems} problems.[/bold green]")
240
+
241
+ if __name__ == "__main__":
242
+ update_readme()