Skip to content

Commit 1e02008

Browse files
author
William Yang
committed
feat: Phase 4 - Remote checks loading
- Add ChecksManager for downloading and caching checks from API - Update check command with --language and --update options - Update submit command with --language option - Add cache command (list/clear) - Use batch API for efficient checks download - Cache checks locally with 24h TTL
1 parent 95d537b commit 1e02008

File tree

3 files changed

+469
-18
lines changed

3 files changed

+469
-18
lines changed

bootcs/__main__.py

Lines changed: 122 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,40 @@ def main():
3232

3333
# Check command
3434
check_parser = subparsers.add_parser("check", help="Check your code against tests")
35-
check_parser.add_argument("slug", help="The check slug (e.g., course-cs50/mario-less)")
35+
check_parser.add_argument("slug", help="The check slug (e.g., cs50/mario-less)")
3636
check_parser.add_argument("-o", "--output", choices=["ansi", "json"], default="ansi",
3737
help="Output format (default: ansi)")
3838
check_parser.add_argument("--log", action="store_true", help="Show detailed log")
3939
check_parser.add_argument("--target", action="append", metavar="check",
4040
help="Run only the specified check(s)")
41+
check_parser.add_argument("-L", "--language", default="c",
42+
help="Language for checks (default: c)")
43+
check_parser.add_argument("-u", "--update", action="store_true",
44+
help="Force update checks from remote")
4145
check_parser.add_argument("--local", metavar="PATH", help="Path to local checks directory")
4246

4347
# Submit command
4448
submit_parser = subparsers.add_parser("submit", help="Submit your code")
45-
submit_parser.add_argument("slug", help="The submission slug (e.g., course-cs50/hello)")
49+
submit_parser.add_argument("slug", help="The submission slug (e.g., cs50/hello)")
4650
submit_parser.add_argument("-m", "--message", help="Commit message")
4751
submit_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation")
52+
submit_parser.add_argument("-L", "--language", help="Language of submission (auto-detected if not specified)")
4853
submit_parser.add_argument("--local", metavar="PATH", help="Path to local checks directory (for file list)")
4954

5055
# Auth commands
5156
subparsers.add_parser("login", help="Log in with GitHub")
5257
subparsers.add_parser("logout", help="Log out")
5358
subparsers.add_parser("whoami", help="Show logged in user")
5459

60+
# Cache command
61+
cache_parser = subparsers.add_parser("cache", help="Manage checks cache")
62+
cache_parser.add_argument("action", choices=["clear", "list"],
63+
help="Action to perform")
64+
cache_parser.add_argument("slug", nargs="?",
65+
help="Specific course or course/stage slug (optional)")
66+
cache_parser.add_argument("-L", "--language",
67+
help="Specific language (optional)")
68+
5569
args = parser.parse_args()
5670

5771
if args.command == "check":
@@ -64,6 +78,8 @@ def main():
6478
return run_logout(args)
6579
elif args.command == "whoami":
6680
return run_whoami(args)
81+
elif args.command == "cache":
82+
return run_cache(args)
6783
else:
6884
parser.print_help()
6985
return 1
@@ -72,17 +88,19 @@ def main():
7288
def run_check(args):
7389
"""Run the check command."""
7490
slug = args.slug
91+
language = getattr(args, 'language', 'c') or 'c'
92+
force_update = getattr(args, 'update', False)
7593

7694
# Determine check directory
7795
if args.local:
7896
check_dir = Path(args.local).resolve()
7997
else:
80-
# For now, look for checks in a local directory structure
81-
# Future: download from remote like check50 does
82-
check_dir = find_check_dir(slug)
98+
# Try remote download first, then fall back to local search
99+
check_dir = find_check_dir(slug, language=language, force_update=force_update)
83100

84101
if not check_dir or not check_dir.exists():
85102
termcolor.cprint(f"Error: Could not find checks for '{slug}'", "red", file=sys.stderr)
103+
termcolor.cprint("Use --local to specify a local checks directory.", "yellow", file=sys.stderr)
86104
return 1
87105

88106
# Set internal state
@@ -220,12 +238,50 @@ def output_json(results, show_log=False):
220238
print(json.dumps(output, indent=2))
221239

222240

223-
def find_check_dir(slug):
224-
"""Find the check directory for a given slug."""
225-
# Extract stage name from slug (e.g., "cs50/credit" -> "credit")
226-
stage_name = slug.split("/")[-1] if "/" in slug else slug
241+
def find_check_dir(slug, language: str = "c", force_update: bool = False):
242+
"""
243+
Find the check directory for a given slug.
244+
245+
Priority:
246+
1. BOOTCS_CHECKS_PATH environment variable (for evaluator)
247+
2. Remote API download (with local cache)
248+
3. Local directories (for development)
249+
"""
250+
# Extract parts from slug (e.g., "cs50/credit" -> course="cs50", stage="credit")
251+
parts = slug.split("/")
252+
if len(parts) == 2:
253+
course_slug, stage_name = parts
254+
else:
255+
stage_name = slug
256+
course_slug = None
227257

228-
# Common locations to search
258+
# 1. Check environment variable first (used by evaluator)
259+
if "BOOTCS_CHECKS_PATH" in os.environ:
260+
checks_path = Path(os.environ["BOOTCS_CHECKS_PATH"])
261+
# Try with stage name directly
262+
path = checks_path / stage_name
263+
if path.exists():
264+
return path
265+
# Try with full slug
266+
if course_slug:
267+
path = checks_path / slug
268+
if path.exists():
269+
return path
270+
271+
# 2. Try remote download (if slug has course/stage format)
272+
if course_slug and "/" in slug:
273+
try:
274+
from .api.checks import get_checks_manager
275+
manager = get_checks_manager()
276+
check_path = manager.get_checks(slug, language=language, force_update=force_update)
277+
if check_path.exists():
278+
return check_path
279+
except Exception as e:
280+
# Log error but continue to local fallback
281+
import sys
282+
print(f"Warning: Could not download checks: {e}", file=sys.stderr)
283+
284+
# 3. Local directories fallback (for development)
229285
search_paths = [
230286
Path.cwd() / "checks" / slug,
231287
Path.cwd() / "checks" / stage_name,
@@ -234,12 +290,6 @@ def find_check_dir(slug):
234290
Path.home() / ".local" / "share" / "bootcs" / slug,
235291
]
236292

237-
# Also check environment variable (try both full slug and stage name)
238-
if "BOOTCS_CHECKS_PATH" in os.environ:
239-
checks_path = Path(os.environ["BOOTCS_CHECKS_PATH"])
240-
search_paths.insert(0, checks_path / stage_name)
241-
search_paths.insert(0, checks_path / slug)
242-
243293
for path in search_paths:
244294
if path.exists():
245295
return path
@@ -254,6 +304,7 @@ def run_submit(args):
254304
from .api.submit import collect_files, submit_files, SubmitFile
255305

256306
slug = args.slug
307+
language = getattr(args, 'language', None) # Optional, will be auto-detected if not provided
257308

258309
# Check if logged in
259310
if not is_logged_in():
@@ -264,10 +315,12 @@ def run_submit(args):
264315
token = get_token()
265316

266317
# Determine check directory for file list
318+
# Use provided language or default to 'c' for finding checks
319+
check_language = language or 'c'
267320
if args.local:
268321
check_dir = Path(args.local).resolve()
269322
else:
270-
check_dir = find_check_dir(slug)
323+
check_dir = find_check_dir(slug, language=check_language)
271324

272325
if not check_dir or not check_dir.exists():
273326
termcolor.cprint(f"Error: Could not find config for '{slug}'", "red", file=sys.stderr)
@@ -331,6 +384,7 @@ def run_submit(args):
331384
files=files,
332385
token=token,
333386
message=args.message,
387+
language=language, # Pass language if specified
334388
)
335389
print()
336390
print()
@@ -469,5 +523,56 @@ def run_whoami(args):
469523
return 0
470524

471525

526+
def run_cache(args):
527+
"""Run the cache command."""
528+
from .api.checks import get_checks_manager
529+
530+
action = args.action
531+
slug = getattr(args, 'slug', None)
532+
language = getattr(args, 'language', None)
533+
534+
manager = get_checks_manager()
535+
536+
if action == "list":
537+
# List cached checks
538+
cached = manager.list_cache()
539+
540+
if not cached:
541+
termcolor.cprint("No cached checks.", "yellow")
542+
return 0
543+
544+
print()
545+
termcolor.cprint("📦 Cached Checks:", "cyan", attrs=["bold"])
546+
print()
547+
print(f" {'Course':<10} {'Language':<10} {'Stage':<15} {'Version':<10} {'Age':<6}")
548+
print(f" {'-'*10} {'-'*10} {'-'*15} {'-'*10} {'-'*6}")
549+
550+
for item in cached:
551+
print(f" {item['course']:<10} {item['language']:<10} {item['stage']:<15} {item['version']:<10} {item['age']:<6}")
552+
553+
print()
554+
print(f" Total: {len(cached)} cached checks")
555+
print(f" Location: {manager.cache_dir}")
556+
print()
557+
return 0
558+
559+
elif action == "clear":
560+
# Clear cache
561+
try:
562+
manager.clear_cache(slug=slug, language=language)
563+
564+
if slug:
565+
termcolor.cprint(f"✅ Cleared cache for '{slug}'", "green")
566+
else:
567+
termcolor.cprint("✅ Cleared all cached checks", "green")
568+
569+
return 0
570+
except ValueError as e:
571+
termcolor.cprint(f"❌ Error: {e}", "red")
572+
return 1
573+
574+
return 0
575+
576+
472577
if __name__ == "__main__":
473578
sys.exit(main())

0 commit comments

Comments
 (0)