cb-dsa 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cb_dsa-0.1.0/PKG-INFO +40 -0
- cb_dsa-0.1.0/README.md +27 -0
- cb_dsa-0.1.0/pyproject.toml +23 -0
- cb_dsa-0.1.0/setup.cfg +4 -0
- cb_dsa-0.1.0/src/cb_dsa/__init__.py +1 -0
- cb_dsa-0.1.0/src/cb_dsa/commands/create.py +341 -0
- cb_dsa-0.1.0/src/cb_dsa/commands/generate.py +242 -0
- cb_dsa-0.1.0/src/cb_dsa/commands/sync.py +233 -0
- cb_dsa-0.1.0/src/cb_dsa/config.py +34 -0
- cb_dsa-0.1.0/src/cb_dsa/core/utils.py +356 -0
- cb_dsa-0.1.0/src/cb_dsa/main.py +24 -0
- cb_dsa-0.1.0/src/cb_dsa.egg-info/PKG-INFO +40 -0
- cb_dsa-0.1.0/src/cb_dsa.egg-info/SOURCES.txt +15 -0
- cb_dsa-0.1.0/src/cb_dsa.egg-info/dependency_links.txt +1 -0
- cb_dsa-0.1.0/src/cb_dsa.egg-info/entry_points.txt +2 -0
- cb_dsa-0.1.0/src/cb_dsa.egg-info/requires.txt +4 -0
- cb_dsa-0.1.0/src/cb_dsa.egg-info/top_level.txt +1 -0
cb_dsa-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cb-dsa
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A customizable CLI for automating Data Structures and Algorithms tracking.
|
|
5
|
+
Author-email: Cibidharan <your-email@example.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: typer>=0.9.0
|
|
10
|
+
Requires-Dist: rich>=13.0.0
|
|
11
|
+
Requires-Dist: requests>=2.28.0
|
|
12
|
+
Requires-Dist: beautifulsoup4>=4.12.0
|
|
13
|
+
|
|
14
|
+
# cb-dsa
|
|
15
|
+
|
|
16
|
+
A Command Line Interface (CLI) tool for automating your Data Structures and Algorithms (DSA) problem-solving workflow.
|
|
17
|
+
|
|
18
|
+
It helps you automatically create problem files, sync your LeetCode submissions, and generate detailed markdown documentation and statistics.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install cb-dsa
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Initialize inside your project folder
|
|
30
|
+
cb-dsa init
|
|
31
|
+
|
|
32
|
+
# Sync your recent accepted LeetCode submissions
|
|
33
|
+
cb-dsa sync
|
|
34
|
+
|
|
35
|
+
# Manually create a new problem template (grabs from clipboard)
|
|
36
|
+
cb-dsa create
|
|
37
|
+
|
|
38
|
+
# Re-generate the README.md and stats dashboard
|
|
39
|
+
cb-dsa generate
|
|
40
|
+
```
|
cb_dsa-0.1.0/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# cb-dsa
|
|
2
|
+
|
|
3
|
+
A Command Line Interface (CLI) tool for automating your Data Structures and Algorithms (DSA) problem-solving workflow.
|
|
4
|
+
|
|
5
|
+
It helps you automatically create problem files, sync your LeetCode submissions, and generate detailed markdown documentation and statistics.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install cb-dsa
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Initialize inside your project folder
|
|
17
|
+
cb-dsa init
|
|
18
|
+
|
|
19
|
+
# Sync your recent accepted LeetCode submissions
|
|
20
|
+
cb-dsa sync
|
|
21
|
+
|
|
22
|
+
# Manually create a new problem template (grabs from clipboard)
|
|
23
|
+
cb-dsa create
|
|
24
|
+
|
|
25
|
+
# Re-generate the README.md and stats dashboard
|
|
26
|
+
cb-dsa generate
|
|
27
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cb-dsa"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A customizable CLI for automating Data Structures and Algorithms tracking."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Cibidharan", email = "your-email@example.com" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"typer>=0.9.0",
|
|
17
|
+
"rich>=13.0.0",
|
|
18
|
+
"requests>=2.28.0",
|
|
19
|
+
"beautifulsoup4>=4.12.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
cb-dsa = "cb_dsa.main:app"
|
cb_dsa-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
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()
|