cb-dsa 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -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()
@@ -0,0 +1,233 @@
1
+ import os
2
+ import sys
3
+ import json
4
+ import re
5
+ import time
6
+ import datetime
7
+ from rich.console import Console
8
+
9
+ from cb_dsa.core.utils import (
10
+ LANG_EXTENSIONS, get_session_cookie, get_csrf_token,
11
+ fetch_problem_data, fetch_submission_details, fetch_submissions,
12
+ get_metadata, clean_code
13
+ )
14
+ from cb_dsa.commands.generate import update_readme
15
+
16
+ console = Console()
17
+
18
+ def sync_leetcode():
19
+ base_dir = os.getcwd()
20
+ console.print("[bold blue]=== LeetCode Sync ===[/bold blue]")
21
+ console.print("This script will download your past Accepted submissions.")
22
+
23
+ session_cookie = get_session_cookie(base_dir, prompt_if_missing=True)
24
+ if not session_cookie:
25
+ console.print("[bold red]Session cookie is required. Exiting.[/bold red]")
26
+ return
27
+
28
+ console.print("\nFetching CSRF token...")
29
+ csrf_token = get_csrf_token(session_cookie)
30
+
31
+ console.print("Fetching submissions (this may take a minute)...")
32
+ offset = 0
33
+ limit = 40
34
+
35
+ # slug -> { lang: submission_id }
36
+ accepted_problems = {}
37
+
38
+ while True:
39
+ data = fetch_submissions(session_cookie, offset, limit)
40
+ if not data:
41
+ break
42
+
43
+ submissions = data.get('submissions_dump', [])
44
+ if not submissions:
45
+ break
46
+
47
+ for sub in submissions:
48
+ if sub['status_display'] == "Accepted":
49
+ slug = sub['title_slug']
50
+ lang = sub['lang']
51
+ if slug not in accepted_problems:
52
+ accepted_problems[slug] = {}
53
+ # Only keep the most recent per language
54
+ if lang not in accepted_problems[slug]:
55
+ accepted_problems[slug][lang] = {'id': sub['id'], 'timestamp': sub.get('timestamp', time.time())}
56
+
57
+ if not data.get('has_next'):
58
+ break
59
+
60
+ offset += limit
61
+ console.print(f"Scanned {offset} total submissions...")
62
+ time.sleep(2.5) # Rate limit protection
63
+
64
+ console.print(f"\n[bold green]Found {len(accepted_problems)} unique accepted problems.[/bold green]")
65
+
66
+ if not accepted_problems:
67
+ console.print("No accepted submissions found. Exiting.")
68
+ return
69
+
70
+ 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__']]
71
+ category_map = {c.lower(): c for c in categories}
72
+ category_map['array'] = 'Arrays'
73
+ category_map['hash table'] = 'HashMaps'
74
+ category_map['dynamic programming'] = 'DP'
75
+
76
+ success_count = 0
77
+
78
+ for slug, langs_dict in accepted_problems.items():
79
+ console.print(f"\nProcessing '{slug}'...")
80
+ lc_data = fetch_problem_data(slug, session_cookie, csrf_token)
81
+ if not lc_data:
82
+ console.print(f"[bold red]Failed to fetch problem metadata for {slug}. Skipping.[/bold red]")
83
+ continue
84
+
85
+ num = lc_data['questionFrontendId']
86
+ title = lc_data['title']
87
+ difficulty = lc_data['difficulty']
88
+
89
+ # Tags logic
90
+ tags = []
91
+ if 'topicTags' in lc_data and lc_data['topicTags']:
92
+ for tag in lc_data['topicTags']:
93
+ name = tag['name']
94
+ if 'contest' not in name.lower():
95
+ tags.append(name)
96
+
97
+ category = "Other"
98
+ if tags:
99
+ primary_tag = tags[0].lower()
100
+ if primary_tag in category_map:
101
+ category = category_map[primary_tag]
102
+ elif primary_tag + 's' in category_map:
103
+ category = category_map[primary_tag + 's']
104
+
105
+ category_path = os.path.join(base_dir, category)
106
+ if not os.path.exists(category_path):
107
+ os.makedirs(category_path)
108
+
109
+ clean_name = re.sub(r'[^a-zA-Z0-9\s\-]', '', title)
110
+ formatted_name = clean_name.lower().replace(' ', '_').replace('-', '_')
111
+ folder_name = f"{num}_{formatted_name}"
112
+ folder_path = os.path.join(category_path, folder_name)
113
+
114
+ # If problem already exists locally, patch it instead of skipping
115
+ exists = False
116
+ existing_folder_path = None
117
+ for cat in os.listdir(base_dir):
118
+ if os.path.isdir(os.path.join(base_dir, cat)) and not cat.startswith('.'):
119
+ if os.path.exists(os.path.join(base_dir, cat, folder_name)):
120
+ exists = True
121
+ existing_folder_path = os.path.join(base_dir, cat, folder_name)
122
+ break
123
+
124
+ most_recent_timestamp = max([info['timestamp'] for info in langs_dict.values()])
125
+ most_recent_sub_id = [info['id'] for info in langs_dict.values() if info['timestamp'] == most_recent_timestamp][0]
126
+ true_date = datetime.datetime.fromtimestamp(most_recent_timestamp).strftime("%Y-%m-%d")
127
+ sub_link_md = f"[View Submission on LeetCode](https://leetcode.com/submissions/detail/{most_recent_sub_id}/)"
128
+
129
+ if exists:
130
+ # Patch existing file
131
+ doc_files = [f for f in os.listdir(existing_folder_path) if f.endswith('.md')]
132
+ if doc_files:
133
+ doc_path = os.path.join(existing_folder_path, doc_files[0])
134
+ with open(doc_path, 'r', encoding='utf-8') as f:
135
+ content = f.read()
136
+
137
+ # Update Date
138
+ if re.search(r'## Date\s*\n\s*\d{4}-\d{2}-\d{2}', content):
139
+ content = re.sub(r'## Date\s*\n\s*\d{4}-\d{2}-\d{2}', f'## Date\n{true_date}', content)
140
+ else:
141
+ content += f'\n\n## Date\n{true_date}\n'
142
+
143
+ # Add submission link if not present
144
+ if 'leetcode.com/submissions/detail' not in content:
145
+ if '## Idea\n' in content:
146
+ content = content.replace('## Idea\n', f'## Idea\n\n{sub_link_md}\n')
147
+ else:
148
+ content += f'\n\n## Submission\n{sub_link_md}\n'
149
+
150
+ # Clean up the old submission link format if the new one exists
151
+ if 'View Submission on LeetCode' in content and 'Click here to see the submission' in content:
152
+ content = re.sub(r'\[Click here to see the submission\]\(.*?\)\s*\n?', '', content)
153
+
154
+ with open(doc_path, 'w', encoding='utf-8') as f:
155
+ f.write(content)
156
+
157
+ console.print(f"[bold yellow]Problem {num} already exists. Patched true date and submission link.[/bold yellow]")
158
+ success_count += 1
159
+ continue
160
+
161
+ os.makedirs(folder_path)
162
+
163
+ # Fetch actual code for each language
164
+ console.print(f"Fetching source code for {len(langs_dict)} language(s)...")
165
+ meta = {'idea': '', 'time': 'O(?)', 'space': 'O(?)'}
166
+ for lang, sub_info in langs_dict.items():
167
+ sub_id = sub_info['id']
168
+ details = fetch_submission_details(sub_id, session_cookie, csrf_token)
169
+ code = details.get('code') if details else None
170
+
171
+ if not code:
172
+ code = f"// Failed to fetch code automatically."
173
+
174
+ ext = LANG_EXTENSIONS.get(lang, f".{lang}")
175
+ file_name = f"{folder_name}_soln{ext}"
176
+
177
+ # Format header appropriately depending on language extension
178
+ if ext in ['.py', '.rb']:
179
+ header = f"# {num}. {title}\n\n"
180
+ else:
181
+ header = f"// {num}. {title}\n\n"
182
+
183
+ code = clean_code(code)
184
+ with open(os.path.join(folder_path, file_name), 'w', encoding='utf-8') as f:
185
+ f.write(header + code + "\n")
186
+
187
+ if sub_id == most_recent_sub_id:
188
+ meta = get_metadata(code, interactive=False)
189
+
190
+ # Write doc
191
+ md_file_name = f"{folder_name}_doc.md"
192
+ leet_link = f"https://leetcode.com/problems/{slug}/"
193
+
194
+ md_content = f"# [{num}. {title}]({leet_link})\n\n"
195
+ md_content += f"## Date\n{true_date}\n\n"
196
+ md_content += f"## Difficulty\n{difficulty}\n\n"
197
+ md_content += f"## Topics\n"
198
+ if tags:
199
+ for t in tags: md_content += f"- {t}\n"
200
+ else:
201
+ md_content += f"- {category}\n"
202
+ md_content += f"\n---\n\n## Idea\n\n{sub_link_md}\n\n"
203
+ if meta.get('idea'):
204
+ md_content += f"{meta['idea']}\n\n"
205
+ md_content += f"---\n\n"
206
+ md_content += f"## Time Complexity\n\n{meta.get('time', 'O(?)')}\n\n## Space Complexity\n\n{meta.get('space', 'O(?)')}\n\n---\n\n"
207
+ md_content += f"## Key Learning\n\n\n\n---\n\n## Mistakes Made\n\n\n\n---\n\n"
208
+
209
+ try:
210
+ similar_questions_list = json.loads(lc_data['similarQuestions'])
211
+ similar_problems = [q['title'] for q in similar_questions_list]
212
+ except:
213
+ similar_problems = []
214
+
215
+ md_content += f"## Similar Problems\n\n"
216
+ if similar_problems:
217
+ for sp in similar_problems: md_content += f"- {sp}\n"
218
+ else:
219
+ md_content += f"- \n"
220
+
221
+ with open(os.path.join(folder_path, md_file_name), 'w', encoding='utf-8') as f:
222
+ f.write(md_content)
223
+
224
+ success_count += 1
225
+ time.sleep(1) # Protect against rate limiting
226
+
227
+ console.print(f"\n[bold green]Done! Successfully imported {success_count} problems.[/bold green]")
228
+ if success_count > 0:
229
+ console.print("Updating README statistics...")
230
+ update_readme()
231
+
232
+ if __name__ == "__main__":
233
+ sync_leetcode()
cb_dsa/config.py ADDED
@@ -0,0 +1,34 @@
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+ from typing import Dict, Any
5
+
6
+ CONFIG_FILE_NAME = "dsa-config.json"
7
+
8
+ DEFAULT_CONFIG = {
9
+ "default_language": ".py",
10
+ "structure": "category", # flat or category
11
+ "folder_format": "{id}_{name}",
12
+ "file_name_format": "{id}_{name}_soln",
13
+ "doc_name_format": "{id}_{name}_doc"
14
+ }
15
+
16
+ def load_config(base_dir: str) -> Dict[str, Any]:
17
+ config_path = os.path.join(base_dir, CONFIG_FILE_NAME)
18
+ if not os.path.exists(config_path):
19
+ return DEFAULT_CONFIG.copy()
20
+
21
+ with open(config_path, "r", encoding="utf-8") as f:
22
+ try:
23
+ user_config = json.load(f)
24
+ # Merge with default config to ensure all keys exist
25
+ config = DEFAULT_CONFIG.copy()
26
+ config.update(user_config)
27
+ return config
28
+ except json.JSONDecodeError:
29
+ return DEFAULT_CONFIG.copy()
30
+
31
+ def save_config(base_dir: str, config: Dict[str, Any]):
32
+ config_path = os.path.join(base_dir, CONFIG_FILE_NAME)
33
+ with open(config_path, "w", encoding="utf-8") as f:
34
+ json.dump(config, f, indent=4)
cb_dsa/core/utils.py ADDED
@@ -0,0 +1,356 @@
1
+ import os
2
+ import urllib.request
3
+ import json
4
+ import ssl
5
+ import time
6
+
7
+ from rich.console import Console
8
+ from rich.prompt import Prompt
9
+
10
+ console = Console()
11
+
12
+ LANG_EXTENSIONS = {
13
+ 'cpp': '.cpp', 'python': '.py', 'python3': '.py', 'java': '.java',
14
+ 'c': '.c', 'csharp': '.cs', 'javascript': '.js', 'typescript': '.ts',
15
+ 'golang': '.go', 'rust': '.rs', 'ruby': '.rb', 'swift': '.swift',
16
+ 'kotlin': '.kt', 'php': '.php'
17
+ }
18
+
19
+ def get_session_cookie(base_dir, prompt_if_missing=True):
20
+ env_path = os.path.join(base_dir, ".env")
21
+ session_cookie = ""
22
+ if os.path.exists(env_path):
23
+ with open(env_path, 'r', encoding='utf-8') as f:
24
+ for line in f:
25
+ if line.startswith("LEETCODE_SESSION="):
26
+ session_cookie = line.strip().split('=', 1)[1]
27
+
28
+ if not session_cookie and prompt_if_missing:
29
+ session_cookie = Prompt.ask("\n[bold cyan]Paste your LEETCODE_SESSION cookie (will be saved to .env)[/bold cyan]").strip()
30
+ if session_cookie:
31
+ with open(env_path, 'a', encoding='utf-8') as f:
32
+ f.write(f"LEETCODE_SESSION={session_cookie}\n")
33
+ return session_cookie
34
+
35
+ def get_last_category(base_dir):
36
+ env_path = os.path.join(base_dir, ".env")
37
+ if os.path.exists(env_path):
38
+ with open(env_path, 'r', encoding='utf-8') as f:
39
+ for line in f:
40
+ if line.startswith("LAST_CATEGORY="):
41
+ return line.strip().split('=', 1)[1]
42
+ return None
43
+
44
+ def save_last_category(base_dir, category):
45
+ env_path = os.path.join(base_dir, ".env")
46
+ lines = []
47
+ found = False
48
+ if os.path.exists(env_path):
49
+ with open(env_path, 'r', encoding='utf-8') as f:
50
+ lines = f.readlines()
51
+
52
+ with open(env_path, 'w', encoding='utf-8') as f:
53
+ for line in lines:
54
+ if line.startswith("LAST_CATEGORY="):
55
+ f.write(f"LAST_CATEGORY={category}\n")
56
+ found = True
57
+ else:
58
+ f.write(line)
59
+ if not found:
60
+ f.write(f"LAST_CATEGORY={category}\n")
61
+
62
+ def get_switch_topic(base_dir):
63
+ env_path = os.path.join(base_dir, ".env")
64
+ if os.path.exists(env_path):
65
+ with open(env_path, 'r', encoding='utf-8') as f:
66
+ for line in f:
67
+ if line.startswith("SWITCH_TOPIC="):
68
+ return line.strip().split('=', 1)[1].lower() == 'true'
69
+ return False
70
+
71
+ def reset_switch_topic(base_dir):
72
+ env_path = os.path.join(base_dir, ".env")
73
+ lines = []
74
+ found = False
75
+ if os.path.exists(env_path):
76
+ with open(env_path, 'r', encoding='utf-8') as f:
77
+ lines = f.readlines()
78
+
79
+ with open(env_path, 'w', encoding='utf-8') as f:
80
+ for line in lines:
81
+ if line.startswith("SWITCH_TOPIC="):
82
+ f.write("SWITCH_TOPIC=false\n")
83
+ found = True
84
+ else:
85
+ f.write(line)
86
+ if not found:
87
+ f.write("SWITCH_TOPIC=false\n")
88
+
89
+ def get_csrf_token(session_cookie):
90
+ context = ssl._create_unverified_context()
91
+ req = urllib.request.Request("https://leetcode.com/", headers={'Cookie': f'LEETCODE_SESSION={session_cookie}', 'User-Agent': 'Mozilla/5.0'})
92
+ try:
93
+ response = urllib.request.urlopen(req, context=context)
94
+ for header, value in response.getheaders():
95
+ if header.lower() == 'set-cookie' and 'csrftoken' in value:
96
+ return value.split('csrftoken=')[1].split(';')[0]
97
+ except:
98
+ pass
99
+ return "csrftoken_placeholder"
100
+
101
+ def fetch_graphql(query, variables, session_cookie=None, csrf_token=None, retries=3):
102
+ context = ssl._create_unverified_context()
103
+ graphql_url = "https://leetcode.com/graphql/"
104
+ payload = {"query": query, "variables": variables}
105
+
106
+ headers = {
107
+ 'Content-Type': 'application/json',
108
+ 'User-Agent': 'Mozilla/5.0'
109
+ }
110
+
111
+ if session_cookie and csrf_token:
112
+ headers['Cookie'] = f'LEETCODE_SESSION={session_cookie}; csrftoken={csrf_token}'
113
+ headers['X-CSRFToken'] = csrf_token
114
+
115
+ req = urllib.request.Request(graphql_url, data=json.dumps(payload).encode('utf-8'), headers=headers)
116
+
117
+ for attempt in range(retries):
118
+ try:
119
+ response = urllib.request.urlopen(req, context=context, timeout=10)
120
+ return json.loads(response.read())
121
+ except Exception as e:
122
+ if attempt < retries - 1:
123
+ time.sleep(2)
124
+ else:
125
+ console.print(f"[bold red]GraphQL request failed after {retries} attempts: {e}[/bold red]")
126
+ return None
127
+
128
+ def fetch_problem_data(slug, session_cookie=None, csrf_token=None):
129
+ query = """
130
+ query questionData($titleSlug: String!) {
131
+ question(titleSlug: $titleSlug) {
132
+ questionId
133
+ questionFrontendId
134
+ title
135
+ titleSlug
136
+ difficulty
137
+ similarQuestions
138
+ note
139
+ topicTags {
140
+ name
141
+ }
142
+ }
143
+ }
144
+ """
145
+ res = fetch_graphql(query, {"titleSlug": slug}, session_cookie, csrf_token)
146
+ if res and 'data' in res:
147
+ return res['data'].get('question')
148
+ return None
149
+
150
+ def fetch_submission_details(submission_id, session_cookie, csrf_token):
151
+ query = """
152
+ query submissionDetails($submissionId: Int!) {
153
+ submissionDetails(submissionId: $submissionId) {
154
+ code
155
+ lang {
156
+ name
157
+ }
158
+ }
159
+ }
160
+ """
161
+ res = fetch_graphql(query, {"submissionId": int(submission_id)}, session_cookie, csrf_token)
162
+ if res and 'data' in res and res['data'].get('submissionDetails'):
163
+ return res['data']['submissionDetails']
164
+ return None
165
+
166
+ def fetch_submissions(session_cookie, offset, limit, retries=5):
167
+ context = ssl._create_unverified_context()
168
+ url = f"https://leetcode.com/api/submissions/?offset={offset}&limit={limit}"
169
+ req = urllib.request.Request(url, headers={'Cookie': f'LEETCODE_SESSION={session_cookie}', 'User-Agent': 'Mozilla/5.0'})
170
+ for attempt in range(retries):
171
+ try:
172
+ response = urllib.request.urlopen(req, context=context, timeout=30)
173
+ return json.loads(response.read())
174
+ except Exception as e:
175
+ if attempt < retries - 1:
176
+ console.print(f"[yellow]Timeout at offset {offset}. Retrying ({attempt+1}/{retries})...[/yellow]")
177
+ time.sleep(5)
178
+ else:
179
+ console.print(f"[bold red]Failed to fetch submissions at offset {offset}: {e}[/bold red]")
180
+ return None
181
+
182
+ def get_slug_from_id(question_id):
183
+ context = ssl._create_unverified_context()
184
+ all_url = "https://leetcode.com/api/problems/all/"
185
+ req = urllib.request.Request(all_url, headers={'User-Agent': 'Mozilla/5.0'})
186
+ try:
187
+ response = urllib.request.urlopen(req, context=context)
188
+ data = json.loads(response.read())
189
+ except Exception as e:
190
+ console.print(f"[bold red]Failed to fetch problems list: {e}[/bold red]")
191
+ return None
192
+
193
+ for p in data.get('stat_status_pairs', []):
194
+ if str(p['stat']['frontend_question_id']) == str(question_id):
195
+ return p['stat']['question__title_slug']
196
+
197
+ console.print(f"[bold yellow]Could not find a problem with ID {question_id}[/bold yellow]")
198
+ return None
199
+
200
+ import re
201
+
202
+ def get_clipboard_text():
203
+ try:
204
+ import tkinter as tk
205
+ root = tk.Tk()
206
+ root.withdraw()
207
+ clip = root.clipboard_get()
208
+ root.destroy()
209
+ return clip
210
+ except Exception:
211
+ return ""
212
+
213
+ def extract_metadata(code_content):
214
+ """
215
+ Extracts idea, time, space, learning, and mistakes from code comments.
216
+ Supports both old format ($t: <time>) and new block format ($t <time> $$).
217
+ """
218
+ if code_content is None:
219
+ code_content = ""
220
+
221
+ metadata = {'idea': None, 'time': None, 'space': None, 'learning': None, 'mistakes': None}
222
+
223
+ def extract_block(tag):
224
+ # Look for $tag ... $$
225
+ pattern = r'\$' + tag + r'\s*(.*?)\$\$'
226
+ match = re.search(pattern, code_content, re.DOTALL | re.IGNORECASE)
227
+ if match:
228
+ raw_text = match.group(1)
229
+ cleaned_lines = []
230
+ for line in raw_text.split('\n'):
231
+ # remove leading comment chars
232
+ clean = re.sub(r'^(\s*\*\s*|\s*//\s*|\s*#\s*)', '', line)
233
+ cleaned_lines.append(clean)
234
+ return '\n'.join(cleaned_lines).strip()
235
+ return None
236
+
237
+ metadata['idea'] = extract_block('i')
238
+ metadata['time'] = extract_block('t')
239
+ metadata['space'] = extract_block('s')
240
+ metadata['learning'] = extract_block('l')
241
+ metadata['mistakes'] = extract_block('m')
242
+
243
+ # Fallback for old format if $$ block not found
244
+ if not metadata['idea']:
245
+ idea_match = re.search(r'\$i\s*\n(.*?)(?=\$t:|\$s:|\*/|\Z)', code_content, re.DOTALL | re.IGNORECASE)
246
+ if idea_match:
247
+ raw_idea = idea_match.group(1)
248
+ cleaned_lines = []
249
+ for line in raw_idea.split('\n'):
250
+ clean = re.sub(r'^(\s*\*\s*|\s*//\s*|\s*#\s*)', '', line)
251
+ cleaned_lines.append(clean)
252
+ metadata['idea'] = '\n'.join(cleaned_lines).strip()
253
+
254
+ if not metadata['time']:
255
+ time_match = re.search(r'\$t:\s*(.*)', code_content, re.IGNORECASE)
256
+ if time_match:
257
+ metadata['time'] = time_match.group(1).strip()
258
+
259
+ if not metadata['space']:
260
+ space_match = re.search(r'\$s:\s*(.*)', code_content, re.IGNORECASE)
261
+ if space_match:
262
+ metadata['space'] = space_match.group(1).strip()
263
+
264
+ return metadata
265
+
266
+ def multiline_input(prompt):
267
+ console.print(f"[bold cyan]{prompt}[/bold cyan]")
268
+ console.print("[dim](Press ENTER twice on an empty line to finish)[/dim]")
269
+ lines = []
270
+ consecutive_empty = 0
271
+
272
+ while True:
273
+ try:
274
+ line = input()
275
+ except EOFError:
276
+ break
277
+
278
+ if line == "":
279
+ consecutive_empty += 1
280
+ if consecutive_empty >= 2:
281
+ ans = Prompt.ask("[bold cyan]Continue typing?[/bold cyan]", choices=["y", "n", ""], default="").strip().lower()
282
+ if ans == 'y':
283
+ consecutive_empty = 0
284
+ continue
285
+ else:
286
+ break
287
+ else:
288
+ consecutive_empty = 0
289
+
290
+ lines.append(line)
291
+
292
+ return '\n'.join(lines).strip()
293
+
294
+ def get_metadata(code_content, interactive=True):
295
+ meta = extract_metadata(code_content)
296
+
297
+ if interactive:
298
+ if not meta.get('idea'):
299
+ console.print("\n[bold yellow]Idea not found in code comments.[/bold yellow]")
300
+ console.print("1. Read docs block from clipboard")
301
+ console.print("2. Paste docs block manually")
302
+ console.print("3. Skip / Default")
303
+ choice = Prompt.ask("[bold cyan]Choose option[/bold cyan]", choices=["1", "2", "3"], default="1")
304
+
305
+ raw_docs = ""
306
+ if choice == "1":
307
+ raw_docs = get_clipboard_text()
308
+ if raw_docs:
309
+ console.print("[bold green]✓ Read docs from clipboard![/bold green]")
310
+ else:
311
+ console.print("[bold red]✗ Could not read from clipboard or clipboard is empty.[/bold red]")
312
+ raw_docs = multiline_input("\nPaste your docs block here")
313
+ elif choice == "2":
314
+ raw_docs = multiline_input("\nPaste your docs block here")
315
+
316
+ if raw_docs:
317
+ new_meta = extract_metadata(raw_docs)
318
+ for k, v in new_meta.items():
319
+ if v:
320
+ meta[k] = v
321
+
322
+ if not meta.get('time'):
323
+ meta['time'] = Prompt.ask("[bold cyan]Enter Time Complexity (e.g., O(n))[/bold cyan]", default="O(n)").strip()
324
+ if not meta.get('space'):
325
+ meta['space'] = Prompt.ask("[bold cyan]Enter Space Complexity (e.g., O(1))[/bold cyan]", default="O(n)").strip()
326
+
327
+ if not meta.get('idea'): meta['idea'] = ""
328
+ if not meta.get('time'): meta['time'] = "O(n)"
329
+ if not meta.get('space'): meta['space'] = "O(n)"
330
+
331
+ return meta
332
+
333
+ def clean_code(code_content):
334
+ # Strip C++/Java/JS block comments
335
+ code_content = re.sub(r'/\*\s*\$i.*?\*/\s*', '', code_content, flags=re.DOTALL | re.IGNORECASE)
336
+ # Strip Python block strings
337
+ code_content = re.sub(r'\"\"\"\s*\$i.*?\"\"\"\s*', '', code_content, flags=re.DOTALL | re.IGNORECASE)
338
+ code_content = re.sub(r"'''\s*\$i.*?'''\s*", '', code_content, flags=re.DOTALL | re.IGNORECASE)
339
+
340
+ # Strip contiguous Python '#' comments if one contains $i
341
+ lines = code_content.split('\n')
342
+ cleaned_lines = []
343
+ in_idea_block = False
344
+ for line in lines:
345
+ is_comment = bool(re.match(r'^\s*#', line))
346
+ if is_comment and '$i' in line.lower():
347
+ in_idea_block = True
348
+ continue
349
+ if in_idea_block:
350
+ if is_comment:
351
+ continue # Skip all contiguous comment lines
352
+ else:
353
+ in_idea_block = False # Block ended
354
+ cleaned_lines.append(line)
355
+
356
+ return '\n'.join(cleaned_lines).strip()
cb_dsa/main.py ADDED
@@ -0,0 +1,24 @@
1
+ import typer
2
+ from rich.console import Console
3
+
4
+ from cb_dsa.commands.create import create_problem
5
+ from cb_dsa.commands.generate import update_readme
6
+ from cb_dsa.commands.sync import sync_leetcode
7
+
8
+ app = typer.Typer(help="cb-dsa: A CLI to automate your LeetCode and DSA workflow.")
9
+ console = Console()
10
+
11
+ @app.command()
12
+ def init():
13
+ """
14
+ Initialize cb-dsa configuration in the current directory.
15
+ """
16
+ console.print("[bold green]Initializing cb-dsa...[/bold green]")
17
+ # TODO: Implement config generation
18
+
19
+ app.command(name="create")(create_problem)
20
+ app.command(name="generate")(update_readme)
21
+ app.command(name="sync")(sync_leetcode)
22
+
23
+ if __name__ == "__main__":
24
+ app()
@@ -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
+ ```
@@ -0,0 +1,12 @@
1
+ cb_dsa/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ cb_dsa/config.py,sha256=UrH5mRYRjYyqq-sGNYcpJvzUoqeCFlD2W9Q3r6OhFsI,1099
3
+ cb_dsa/main.py,sha256=Ow_3HQkIkdZ00vIa2rNoO0Fv5w1f17pkvlL4kU7oJLE,681
4
+ cb_dsa/commands/create.py,sha256=NRLX0J4hzagf6dvILA3Busn_wHJAE1xBJ11KHGfSI6s,14472
5
+ cb_dsa/commands/generate.py,sha256=qx4a7zXo21euOcb-rPriQbJtyhdDP9ynmszZVCoD6fw,9442
6
+ cb_dsa/commands/sync.py,sha256=-9ozCeci815AckYtpfNLWVh8aQnMBxN-83aDCQM6bTc,9786
7
+ cb_dsa/core/utils.py,sha256=W051IPx3ToOiDWEUEuUGC4VYVCVAiiygxXkeuWz3E7Y,13259
8
+ cb_dsa-0.1.0.dist-info/METADATA,sha256=pzFMlBox_GQzVDvB_kwE7bSxb6cE7vrp4U0dYCZxQ1U,1041
9
+ cb_dsa-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
10
+ cb_dsa-0.1.0.dist-info/entry_points.txt,sha256=wyacBe1_TzqgZbhyUJ9Y8EH8yJedvGL942awe1NV28I,43
11
+ cb_dsa-0.1.0.dist-info/top_level.txt,sha256=UPSZ8_24UIszdN6x66Yao_z8ZkLXH_knRr8oScWBc0M,7
12
+ cb_dsa-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cb-dsa = cb_dsa.main:app
@@ -0,0 +1 @@
1
+ cb_dsa