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 +1 -0
- cb_dsa/commands/create.py +341 -0
- cb_dsa/commands/generate.py +242 -0
- cb_dsa/commands/sync.py +233 -0
- cb_dsa/config.py +34 -0
- cb_dsa/core/utils.py +356 -0
- cb_dsa/main.py +24 -0
- cb_dsa-0.1.0.dist-info/METADATA +40 -0
- cb_dsa-0.1.0.dist-info/RECORD +12 -0
- cb_dsa-0.1.0.dist-info/WHEEL +5 -0
- cb_dsa-0.1.0.dist-info/entry_points.txt +2 -0
- cb_dsa-0.1.0.dist-info/top_level.txt +1 -0
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()
|
cb_dsa/commands/sync.py
ADDED
|
@@ -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 @@
|
|
|
1
|
+
cb_dsa
|