dancealgorithm 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.
- dancealgorithm/__init__.py +128 -0
- dancealgorithm/__main__.py +8 -0
- dancealgorithm/assignments/DA - Campus/Solution.java +198 -0
- dancealgorithm/assignments/DA - Campus/__init__.py +34 -0
- dancealgorithm/assignments/DA - Flight/Solution.java +204 -0
- dancealgorithm/assignments/DA - Flight/__init__.py +34 -0
- dancealgorithm/assignments/DA - Smart/Solution.java +220 -0
- dancealgorithm/assignments/DA - Smart/__init__.py +34 -0
- dancealgorithm/assignments/DA.txt +29 -0
- dancealgorithm/assignments/FK - Advertisement/Solution.java +141 -0
- dancealgorithm/assignments/FK - Advertisement/__init__.py +34 -0
- dancealgorithm/assignments/FK - Electric/Solution.java +168 -0
- dancealgorithm/assignments/FK - Electric/__init__.py +34 -0
- dancealgorithm/assignments/FK - Emergency/Solution.java +159 -0
- dancealgorithm/assignments/FK - Emergency/__init__.py +34 -0
- dancealgorithm/assignments/FK.txt +26 -0
- dancealgorithm/assignments/GC - Classroom/Solution.java +87 -0
- dancealgorithm/assignments/GC - Classroom/__init__.py +34 -0
- dancealgorithm/assignments/GC - Interference/Solution.java +88 -0
- dancealgorithm/assignments/GC - Interference/__init__.py +34 -0
- dancealgorithm/assignments/GC - University/Solution.java +88 -0
- dancealgorithm/assignments/GC - University/__init__.py +34 -0
- dancealgorithm/assignments/GC.txt +25 -0
- dancealgorithm/assignments/K - Cloud/Solution.java +115 -0
- dancealgorithm/assignments/K - Cloud/__init__.py +34 -0
- dancealgorithm/assignments/K - Disaster/Solution.java +116 -0
- dancealgorithm/assignments/K - Disaster/__init__.py +34 -0
- dancealgorithm/assignments/K - Hospital/Solution.java +115 -0
- dancealgorithm/assignments/K - Hospital/__init__.py +34 -0
- dancealgorithm/assignments/K.txt +38 -0
- dancealgorithm/assignments/LC - Delivery/Solution.java +180 -0
- dancealgorithm/assignments/LC - Delivery/__init__.py +34 -0
- dancealgorithm/assignments/LC - Drilling/Solution.java +197 -0
- dancealgorithm/assignments/LC - Drilling/__init__.py +34 -0
- dancealgorithm/assignments/LC - Flight/Solution.java +180 -0
- dancealgorithm/assignments/LC - Flight/__init__.py +34 -0
- dancealgorithm/assignments/LC.txt +31 -0
- dancealgorithm/assignments/MS - Commerce/Solution.java +162 -0
- dancealgorithm/assignments/MS - Commerce/__init__.py +34 -0
- dancealgorithm/assignments/MS - Commerce/dataset.csv +16 -0
- dancealgorithm/assignments/MS - Customer/Solution.java +144 -0
- dancealgorithm/assignments/MS - Customer/__init__.py +34 -0
- dancealgorithm/assignments/MS - Customer/dataset.csv +16 -0
- dancealgorithm/assignments/MS - Library/Solution.java +162 -0
- dancealgorithm/assignments/MS - Library/__init__.py +34 -0
- dancealgorithm/assignments/MS - Library/dataset.csv +16 -0
- dancealgorithm/assignments/MS.txt +32 -0
- dancealgorithm/assignments/MSG - Emergency/Solution.java +211 -0
- dancealgorithm/assignments/MSG - Emergency/__init__.py +34 -0
- dancealgorithm/assignments/MSG - Logistics/Solution.java +189 -0
- dancealgorithm/assignments/MSG - Logistics/__init__.py +34 -0
- dancealgorithm/assignments/MSG - Skyconnect/Solution.java +204 -0
- dancealgorithm/assignments/MSG - Skyconnect/__init__.py +34 -0
- dancealgorithm/assignments/MSG.txt +28 -0
- dancealgorithm/assignments/QS - Commerce/Solution.java +147 -0
- dancealgorithm/assignments/QS - Commerce/__init__.py +34 -0
- dancealgorithm/assignments/QS - Commerce/dataset.csv +16 -0
- dancealgorithm/assignments/QS - Movie/Solution.java +148 -0
- dancealgorithm/assignments/QS - Movie/__init__.py +34 -0
- dancealgorithm/assignments/QS - Movie/dataset.csv +16 -0
- dancealgorithm/assignments/QS - Online/Solution.java +147 -0
- dancealgorithm/assignments/QS - Online/__init__.py +34 -0
- dancealgorithm/assignments/QS - Online/dataset.csv +16 -0
- dancealgorithm/assignments/QS.txt +33 -0
- dancealgorithm/assignments/__init__.py +65 -0
- dancealgorithm/cli.py +349 -0
- dancealgorithm/config.py +125 -0
- dancealgorithm/downloader.py +330 -0
- dancealgorithm-0.1.0.dist-info/METADATA +199 -0
- dancealgorithm-0.1.0.dist-info/RECORD +74 -0
- dancealgorithm-0.1.0.dist-info/WHEEL +5 -0
- dancealgorithm-0.1.0.dist-info/entry_points.txt +3 -0
- dancealgorithm-0.1.0.dist-info/licenses/LICENSE +22 -0
- dancealgorithm-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DanceAlgorithm Bundled Problem Sheets & Assignments Vault.
|
|
3
|
+
Contains all 24 DAA algorithms and problem sheets.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import importlib
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List
|
|
10
|
+
|
|
11
|
+
ASSIGNMENTS_DIR = Path(__file__).resolve().parent
|
|
12
|
+
|
|
13
|
+
REGISTRY: Dict[Any, Any] = {}
|
|
14
|
+
SHEETS: List[Any] = []
|
|
15
|
+
DAA_ASSIGNMENTS: Dict[str, Any] = {}
|
|
16
|
+
|
|
17
|
+
ORDERED_NAMES = [
|
|
18
|
+
"MS - Customer",
|
|
19
|
+
"MS - Commerce",
|
|
20
|
+
"MS - Library",
|
|
21
|
+
"QS - Movie",
|
|
22
|
+
"QS - Commerce",
|
|
23
|
+
"QS - Online",
|
|
24
|
+
"FK - Emergency",
|
|
25
|
+
"FK - Electric",
|
|
26
|
+
"FK - Advertisement",
|
|
27
|
+
"DA - Smart",
|
|
28
|
+
"DA - Campus",
|
|
29
|
+
"DA - Flight",
|
|
30
|
+
"MSG - Logistics",
|
|
31
|
+
"MSG - Skyconnect",
|
|
32
|
+
"MSG - Emergency",
|
|
33
|
+
"K - Disaster",
|
|
34
|
+
"K - Hospital",
|
|
35
|
+
"K - Cloud",
|
|
36
|
+
"GC - Classroom",
|
|
37
|
+
"GC - Interference",
|
|
38
|
+
"GC - University",
|
|
39
|
+
"LC - Delivery",
|
|
40
|
+
"LC - Drilling",
|
|
41
|
+
"LC - Flight",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
for idx, name in enumerate(ORDERED_NAMES, 1):
|
|
45
|
+
folder = ASSIGNMENTS_DIR / name
|
|
46
|
+
if folder.is_dir():
|
|
47
|
+
try:
|
|
48
|
+
mod = importlib.import_module(f".{name}", package=__name__)
|
|
49
|
+
globals()[name] = mod
|
|
50
|
+
SHEETS.append(mod)
|
|
51
|
+
|
|
52
|
+
REGISTRY[idx] = mod
|
|
53
|
+
REGISTRY[f"ps{idx}"] = mod
|
|
54
|
+
REGISTRY[f"PS{idx}"] = mod
|
|
55
|
+
REGISTRY[f"ps{idx:02d}"] = mod
|
|
56
|
+
REGISTRY[f"PS{idx:02d}"] = mod
|
|
57
|
+
|
|
58
|
+
norm = name.lower().replace(" ", "").replace("-", "").replace("_", "")
|
|
59
|
+
DAA_ASSIGNMENTS[norm] = mod
|
|
60
|
+
REGISTRY[name] = mod
|
|
61
|
+
REGISTRY[norm] = mod
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
__all__ = ["REGISTRY", "SHEETS", "DAA_ASSIGNMENTS", "ASSIGNMENTS_DIR"]
|
dancealgorithm/cli.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI interface for DanceAlgorithm.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from dancealgorithm import __version__
|
|
13
|
+
from dancealgorithm.config import (
|
|
14
|
+
get_branch,
|
|
15
|
+
get_destination,
|
|
16
|
+
get_repo,
|
|
17
|
+
get_token,
|
|
18
|
+
load_config,
|
|
19
|
+
reset_config,
|
|
20
|
+
set_branch,
|
|
21
|
+
set_destination,
|
|
22
|
+
set_repo,
|
|
23
|
+
)
|
|
24
|
+
from dancealgorithm.downloader import (
|
|
25
|
+
BUNDLED_DIR,
|
|
26
|
+
DanceAlgorithmError,
|
|
27
|
+
extract_archive,
|
|
28
|
+
extract_bundled,
|
|
29
|
+
fetch_archive,
|
|
30
|
+
inspect_archive,
|
|
31
|
+
parse_repo_string,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Reconfigure console streams on Windows to prevent cp1252 charmap errors
|
|
35
|
+
if sys.platform == "win32":
|
|
36
|
+
try:
|
|
37
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
38
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
39
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
40
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run_install(
|
|
46
|
+
destination: Path,
|
|
47
|
+
subject: Optional[str] = None,
|
|
48
|
+
repo_str: Optional[str] = None,
|
|
49
|
+
branch: Optional[str] = None,
|
|
50
|
+
token: Optional[str] = None,
|
|
51
|
+
force: bool = True,
|
|
52
|
+
quiet: bool = False,
|
|
53
|
+
) -> int:
|
|
54
|
+
"""
|
|
55
|
+
Executes assignment extraction and outputs the destination path.
|
|
56
|
+
"""
|
|
57
|
+
target_path = destination.resolve()
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
if repo_str:
|
|
61
|
+
owner, repo = parse_repo_string(repo_str)
|
|
62
|
+
branch = branch or get_branch()
|
|
63
|
+
archive_data = fetch_archive(
|
|
64
|
+
owner=owner,
|
|
65
|
+
repo=repo,
|
|
66
|
+
branch=branch,
|
|
67
|
+
token=token,
|
|
68
|
+
silent=True,
|
|
69
|
+
)
|
|
70
|
+
extracted = extract_archive(
|
|
71
|
+
archive_data=archive_data,
|
|
72
|
+
destination=destination,
|
|
73
|
+
subject_filter=subject,
|
|
74
|
+
overwrite=force,
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
extracted = extract_bundled(
|
|
78
|
+
destination=destination,
|
|
79
|
+
subject_filter=subject,
|
|
80
|
+
overwrite=force,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
except DanceAlgorithmError as e:
|
|
84
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
85
|
+
return 1
|
|
86
|
+
except Exception as e:
|
|
87
|
+
print(f"Unexpected error: {e}", file=sys.stderr)
|
|
88
|
+
return 1
|
|
89
|
+
|
|
90
|
+
if not extracted:
|
|
91
|
+
if not quiet:
|
|
92
|
+
if subject:
|
|
93
|
+
print(f"Error: No problem sheets found matching '{subject}'.", file=sys.stderr)
|
|
94
|
+
else:
|
|
95
|
+
print("Error: No files were extracted.", file=sys.stderr)
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
if not quiet:
|
|
99
|
+
out_path = target_path
|
|
100
|
+
if subject and extracted:
|
|
101
|
+
try:
|
|
102
|
+
sub_parts = [
|
|
103
|
+
p.relative_to(target_path).parts
|
|
104
|
+
for p in extracted
|
|
105
|
+
if len(p.relative_to(target_path).parts) > 1
|
|
106
|
+
]
|
|
107
|
+
if sub_parts:
|
|
108
|
+
out_path = target_path / sub_parts[0][0]
|
|
109
|
+
except Exception:
|
|
110
|
+
out_path = target_path
|
|
111
|
+
print(out_path)
|
|
112
|
+
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def check_and_display_text(target: str) -> bool:
|
|
117
|
+
"""
|
|
118
|
+
If target matches an algorithm notes .txt file (e.g. DA.txt, da, ms.txt, etc.),
|
|
119
|
+
print its contents directly to the terminal screen without extracting any files.
|
|
120
|
+
"""
|
|
121
|
+
if not BUNDLED_DIR.exists():
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
clean_target = target.strip().replace("/", "\\").split("\\")[-1]
|
|
125
|
+
norm = clean_target.lower().replace("-", "").replace("_", "").replace(" ", "")
|
|
126
|
+
norm_base = norm.removesuffix(".txt")
|
|
127
|
+
|
|
128
|
+
for f in BUNDLED_DIR.glob("*.txt"):
|
|
129
|
+
f_norm = f.name.lower().replace("-", "").replace("_", "").replace(" ", "")
|
|
130
|
+
f_base = f_norm.removesuffix(".txt")
|
|
131
|
+
if norm == f_norm or norm == f_base or norm_base == f_base:
|
|
132
|
+
try:
|
|
133
|
+
print(f.read_text(encoding="utf-8"))
|
|
134
|
+
return True
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def handle_download(args: argparse.Namespace) -> int:
|
|
141
|
+
subject = " ".join(args.subject).strip() if isinstance(args.subject, list) else (args.subject or "").strip()
|
|
142
|
+
if not subject:
|
|
143
|
+
subject = None
|
|
144
|
+
|
|
145
|
+
if subject:
|
|
146
|
+
if check_and_display_text(subject):
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
destination = get_destination(args.output)
|
|
150
|
+
repo = get_repo(args.repo)
|
|
151
|
+
branch = get_branch(args.branch)
|
|
152
|
+
token = get_token(args.token)
|
|
153
|
+
return run_install(
|
|
154
|
+
destination=destination,
|
|
155
|
+
subject=subject,
|
|
156
|
+
repo_str=repo,
|
|
157
|
+
branch=branch,
|
|
158
|
+
token=token,
|
|
159
|
+
force=args.force,
|
|
160
|
+
quiet=args.quiet,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def handle_list(args: argparse.Namespace) -> int:
|
|
165
|
+
repo_str = get_repo(args.repo)
|
|
166
|
+
if not repo_str:
|
|
167
|
+
print("📚 Available Bundled DAA Problem Sheets:\n")
|
|
168
|
+
if BUNDLED_DIR.exists():
|
|
169
|
+
for folder in sorted(
|
|
170
|
+
[f for f in BUNDLED_DIR.iterdir() if f.is_dir() and not f.name.startswith((".", "__")) and f.name != "__pycache__"],
|
|
171
|
+
key=lambda p: p.name,
|
|
172
|
+
):
|
|
173
|
+
files = [
|
|
174
|
+
f.relative_to(folder).as_posix()
|
|
175
|
+
for f in folder.rglob("*")
|
|
176
|
+
if f.is_file()
|
|
177
|
+
and "__pycache__" not in f.parts
|
|
178
|
+
and not f.name.endswith((".pyc", ".class"))
|
|
179
|
+
and f.name != "__init__.py"
|
|
180
|
+
]
|
|
181
|
+
print(f"📁 {folder.name} ({len(files)} files)")
|
|
182
|
+
for f in files:
|
|
183
|
+
print(f" ├── {f}")
|
|
184
|
+
|
|
185
|
+
txt_files = sorted(list(BUNDLED_DIR.glob("*.txt")))
|
|
186
|
+
if txt_files:
|
|
187
|
+
print("\n📝 Available Algorithm Notes & Complexities:")
|
|
188
|
+
for tf in txt_files:
|
|
189
|
+
print(f" • {tf.name} (run: dance {tf.name})")
|
|
190
|
+
|
|
191
|
+
print("\n💡 Tip: Run 'dance <folder-name>' (e.g. 'dance MSG - Logistics') to extract only that folder.")
|
|
192
|
+
print("💡 Tip: Run 'dance <ALGO.txt>' (e.g. 'dance DA.txt') to display algorithm notes directly on screen.")
|
|
193
|
+
return 0
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
owner, repo = parse_repo_string(repo_str)
|
|
197
|
+
branch = get_branch(args.branch)
|
|
198
|
+
token = get_token(args.token)
|
|
199
|
+
print(f"🔍 Inspecting remote catalogue in {owner}/{repo} (branch: {branch})...\n")
|
|
200
|
+
archive_data = fetch_archive(owner=owner, repo=repo, branch=branch, token=token, silent=True)
|
|
201
|
+
structure = inspect_archive(archive_data)
|
|
202
|
+
print("📚 Available Problem Sheets & Lab Files:")
|
|
203
|
+
for subject, files in sorted(structure.items()):
|
|
204
|
+
print(f"\n📁 {subject} ({len(files)} files)")
|
|
205
|
+
for f in files[:8]:
|
|
206
|
+
print(f" ├── {f}")
|
|
207
|
+
if len(files) > 8:
|
|
208
|
+
print(f" └── ... and {len(files) - 8} more files")
|
|
209
|
+
return 0
|
|
210
|
+
except DanceAlgorithmError as e:
|
|
211
|
+
print(f"❌ Error: {e}")
|
|
212
|
+
return 1
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def handle_config(args: argparse.Namespace) -> int:
|
|
216
|
+
if args.reset:
|
|
217
|
+
reset_config()
|
|
218
|
+
print("✅ Configuration reset to defaults.")
|
|
219
|
+
return 0
|
|
220
|
+
|
|
221
|
+
if args.set_dest:
|
|
222
|
+
set_destination(args.set_dest)
|
|
223
|
+
print(f"✅ Default destination set to: '{get_destination()}'")
|
|
224
|
+
|
|
225
|
+
if args.set_repo:
|
|
226
|
+
try:
|
|
227
|
+
parse_repo_string(args.set_repo)
|
|
228
|
+
set_repo(args.set_repo)
|
|
229
|
+
print(f"✅ Default GitHub repo set to: '{args.set_repo}'")
|
|
230
|
+
except DanceAlgorithmError as e:
|
|
231
|
+
print(f"❌ {e}")
|
|
232
|
+
return 1
|
|
233
|
+
|
|
234
|
+
if args.set_branch:
|
|
235
|
+
set_branch(args.set_branch)
|
|
236
|
+
print(f"✅ Default branch set to: '{args.set_branch}'")
|
|
237
|
+
|
|
238
|
+
cfg = load_config()
|
|
239
|
+
print("\n⚙️ DanceAlgorithm Configuration:")
|
|
240
|
+
print(f" • Destination: {get_destination()} {'(custom)' if 'destination' in cfg else '(system default)'}")
|
|
241
|
+
print(f" • Remote Repo: {get_repo() or 'None (using bundled offline vault)'}")
|
|
242
|
+
print(f" • Config File: {Path.home() / '.dancealgorithm' / 'config.json'}\n")
|
|
243
|
+
return 0
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
247
|
+
parser = argparse.ArgumentParser(
|
|
248
|
+
prog="dancealgorithm",
|
|
249
|
+
description="DanceAlgorithm: Instant DAA Algorithms, Lab Problem Sheets, Datasets & Code Vault.",
|
|
250
|
+
)
|
|
251
|
+
parser.add_argument(
|
|
252
|
+
"-v", "--version", action="version", version=f"%(prog)s {__version__}"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
subparsers = parser.add_subparsers(dest="command", help="Available subcommands")
|
|
256
|
+
|
|
257
|
+
# download subcommand (default action)
|
|
258
|
+
p_dl = subparsers.add_parser("download", help="Download / extract problem sheets or view notes")
|
|
259
|
+
p_dl.add_argument(
|
|
260
|
+
"subject",
|
|
261
|
+
nargs="*",
|
|
262
|
+
default=[],
|
|
263
|
+
help="Problem sheet or notes (e.g. 'MSG - Logistics', 'DA - Smart', 'DA.txt')",
|
|
264
|
+
)
|
|
265
|
+
p_dl.add_argument(
|
|
266
|
+
"-o",
|
|
267
|
+
"--output",
|
|
268
|
+
default=None,
|
|
269
|
+
help="Target folder (default: C:\\DAA_Assignments)",
|
|
270
|
+
)
|
|
271
|
+
p_dl.add_argument(
|
|
272
|
+
"-r",
|
|
273
|
+
"--repo",
|
|
274
|
+
default=None,
|
|
275
|
+
help="Remote GitHub repository ('owner/repo')",
|
|
276
|
+
)
|
|
277
|
+
p_dl.add_argument(
|
|
278
|
+
"-b",
|
|
279
|
+
"--branch",
|
|
280
|
+
default=None,
|
|
281
|
+
help="Branch name (default: main)",
|
|
282
|
+
)
|
|
283
|
+
p_dl.add_argument(
|
|
284
|
+
"-t",
|
|
285
|
+
"--token",
|
|
286
|
+
default=None,
|
|
287
|
+
help="GitHub PAT for private repos",
|
|
288
|
+
)
|
|
289
|
+
p_dl.add_argument(
|
|
290
|
+
"-f",
|
|
291
|
+
"--force",
|
|
292
|
+
action="store_true",
|
|
293
|
+
default=True,
|
|
294
|
+
help="Overwrite existing files",
|
|
295
|
+
)
|
|
296
|
+
p_dl.add_argument(
|
|
297
|
+
"-q",
|
|
298
|
+
"--quiet",
|
|
299
|
+
action="store_true",
|
|
300
|
+
help="Run silently without output",
|
|
301
|
+
)
|
|
302
|
+
p_dl.set_defaults(func=handle_download)
|
|
303
|
+
|
|
304
|
+
# list subcommand
|
|
305
|
+
p_list = subparsers.add_parser("list", help="List available problem sheets and notes")
|
|
306
|
+
p_list.add_argument("-r", "--repo", default=None, help="Remote GitHub repository")
|
|
307
|
+
p_list.add_argument("-b", "--branch", default=None, help="Branch name")
|
|
308
|
+
p_list.add_argument("-t", "--token", default=None, help="GitHub PAT for private repos")
|
|
309
|
+
p_list.set_defaults(func=handle_list)
|
|
310
|
+
|
|
311
|
+
# config subcommand
|
|
312
|
+
p_cfg = subparsers.add_parser("config", help="View or modify saved settings")
|
|
313
|
+
p_cfg.add_argument("--set-dest", metavar="PATH", help="Set default destination directory")
|
|
314
|
+
p_cfg.add_argument("--set-repo", metavar="OWNER/REPO", help="Set default remote repository")
|
|
315
|
+
p_cfg.add_argument("--set-branch", metavar="BRANCH", help="Set default branch")
|
|
316
|
+
p_cfg.add_argument("--reset", action="store_true", help="Reset configuration back to defaults")
|
|
317
|
+
p_cfg.set_defaults(func=handle_config)
|
|
318
|
+
|
|
319
|
+
return parser
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
323
|
+
if argv is None:
|
|
324
|
+
argv = sys.argv[1:]
|
|
325
|
+
else:
|
|
326
|
+
argv = list(argv)
|
|
327
|
+
|
|
328
|
+
subcommands = {"download", "list", "config", "-h", "--help", "-v", "--version"}
|
|
329
|
+
|
|
330
|
+
# Seamless shortcut routing:
|
|
331
|
+
# 1. `dance` -> `dance download`
|
|
332
|
+
# 2. `dance MSG - Logistics` -> `dance download MSG - Logistics`
|
|
333
|
+
# 3. `dance DA.txt` -> `dance download DA.txt`
|
|
334
|
+
# 4. `dance -o <path>` -> `dance download -o <path>`
|
|
335
|
+
if not argv:
|
|
336
|
+
argv = ["download"]
|
|
337
|
+
elif argv[0] not in subcommands:
|
|
338
|
+
if not argv[0].startswith("-"):
|
|
339
|
+
argv = ["download"] + argv
|
|
340
|
+
elif argv[0] not in {"-h", "--help", "-v", "--version"}:
|
|
341
|
+
argv = ["download"] + argv
|
|
342
|
+
|
|
343
|
+
parser = build_parser()
|
|
344
|
+
args = parser.parse_args(argv)
|
|
345
|
+
return args.func(args)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
if __name__ == "__main__":
|
|
349
|
+
sys.exit(main())
|
dancealgorithm/config.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for DanceAlgorithm.
|
|
3
|
+
Supports user configuration stored in ~/.dancealgorithm/config.json,
|
|
4
|
+
environment variables, and command-line overrides.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict
|
|
14
|
+
|
|
15
|
+
CONFIG_DIR = Path.home() / ".dancealgorithm"
|
|
16
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_system_default_destination() -> Path:
|
|
20
|
+
r"""
|
|
21
|
+
Default target folder for assignments:
|
|
22
|
+
- Windows: C:\DAA_Assignments (falls back to ~/DAA_Assignments if C:\ is not writable)
|
|
23
|
+
- macOS / Linux: ~/DAA_Assignments
|
|
24
|
+
"""
|
|
25
|
+
if sys.platform == "win32":
|
|
26
|
+
candidate = Path("C:/DAA_Assignments")
|
|
27
|
+
try:
|
|
28
|
+
candidate.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
return candidate
|
|
30
|
+
except (PermissionError, OSError):
|
|
31
|
+
return Path.home() / "DAA_Assignments"
|
|
32
|
+
return Path.home() / "DAA_Assignments"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_config() -> Dict[str, Any]:
|
|
36
|
+
"""Load configuration from ~/.dancealgorithm/config.json."""
|
|
37
|
+
if not CONFIG_FILE.exists():
|
|
38
|
+
return {}
|
|
39
|
+
try:
|
|
40
|
+
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
41
|
+
data = json.load(f)
|
|
42
|
+
return data if isinstance(data, dict) else {}
|
|
43
|
+
except (json.JSONDecodeError, OSError):
|
|
44
|
+
return {}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def save_config(config: Dict[str, Any]) -> None:
|
|
48
|
+
"""Save configuration to ~/.dancealgorithm/config.json."""
|
|
49
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
51
|
+
json.dump(config, f, indent=2)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_destination(cli_dest: str | None = None) -> Path:
|
|
55
|
+
r"""
|
|
56
|
+
Resolve target directory for problem sheets:
|
|
57
|
+
1. CLI argument (--output / -o)
|
|
58
|
+
2. Environment variable DANCEALGORITHM_DEST
|
|
59
|
+
3. User saved config
|
|
60
|
+
4. System default (C:\DAA_Assignments on Windows)
|
|
61
|
+
"""
|
|
62
|
+
if cli_dest:
|
|
63
|
+
return Path(cli_dest)
|
|
64
|
+
|
|
65
|
+
env_dest = os.environ.get("DANCEALGORITHM_DEST")
|
|
66
|
+
if env_dest:
|
|
67
|
+
return Path(env_dest)
|
|
68
|
+
|
|
69
|
+
cfg = load_config()
|
|
70
|
+
if "destination" in cfg and cfg["destination"]:
|
|
71
|
+
return Path(cfg["destination"])
|
|
72
|
+
|
|
73
|
+
return get_system_default_destination()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def set_destination(dest_path: str) -> Path:
|
|
77
|
+
"""Persist a new default target folder in ~/.dancealgorithm/config.json."""
|
|
78
|
+
target = Path(dest_path).expanduser().resolve()
|
|
79
|
+
cfg = load_config()
|
|
80
|
+
cfg["destination"] = str(target)
|
|
81
|
+
save_config(cfg)
|
|
82
|
+
return target
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_repo(cli_repo: str | None = None) -> str | None:
|
|
86
|
+
if cli_repo:
|
|
87
|
+
return cli_repo
|
|
88
|
+
env_repo = os.environ.get("DANCEALGORITHM_REPO")
|
|
89
|
+
if env_repo:
|
|
90
|
+
return env_repo
|
|
91
|
+
cfg = load_config()
|
|
92
|
+
return cfg.get("repo")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def set_repo(repo_str: str) -> None:
|
|
96
|
+
cfg = load_config()
|
|
97
|
+
cfg["repo"] = repo_str.strip()
|
|
98
|
+
save_config(cfg)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_branch(cli_branch: str | None = None) -> str:
|
|
102
|
+
if cli_branch:
|
|
103
|
+
return cli_branch
|
|
104
|
+
env_branch = os.environ.get("DANCEALGORITHM_BRANCH")
|
|
105
|
+
if env_branch:
|
|
106
|
+
return env_branch
|
|
107
|
+
cfg = load_config()
|
|
108
|
+
return cfg.get("branch", "main")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def set_branch(branch_str: str) -> None:
|
|
112
|
+
cfg = load_config()
|
|
113
|
+
cfg["branch"] = branch_str.strip()
|
|
114
|
+
save_config(cfg)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_token(cli_token: str | None = None) -> str | None:
|
|
118
|
+
if cli_token:
|
|
119
|
+
return cli_token
|
|
120
|
+
return os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def reset_config() -> None:
|
|
124
|
+
if CONFIG_FILE.exists():
|
|
125
|
+
CONFIG_FILE.unlink()
|