bookcli 0.1.0__tar.gz → 0.2.1__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.
Files changed (32) hide show
  1. {bookcli-0.1.0 → bookcli-0.2.1}/PKG-INFO +1 -1
  2. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/__init__.py +1 -1
  3. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/cli.py +423 -2
  4. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli.egg-info/PKG-INFO +1 -1
  5. {bookcli-0.1.0 → bookcli-0.2.1}/pyproject.toml +1 -1
  6. {bookcli-0.1.0 → bookcli-0.2.1}/README.md +0 -0
  7. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/cache.py +0 -0
  8. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/config.py +0 -0
  9. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/database/__init__.py +0 -0
  10. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/database/migrations.py +0 -0
  11. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/downloader.py +0 -0
  12. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/exceptions.py +0 -0
  13. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/models.py +0 -0
  14. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/opener.py +0 -0
  15. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/providers/__init__.py +0 -0
  16. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/providers/base.py +0 -0
  17. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/providers/google_books.py +0 -0
  18. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/providers/gutenberg.py +0 -0
  19. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/providers/internet_archive.py +0 -0
  20. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/providers/openlibrary.py +0 -0
  21. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/services/__init__.py +0 -0
  22. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/services/history.py +0 -0
  23. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/services/ranking.py +0 -0
  24. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/services/search.py +0 -0
  25. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/settings.py +0 -0
  26. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli/utils.py +0 -0
  27. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli.egg-info/SOURCES.txt +0 -0
  28. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli.egg-info/dependency_links.txt +0 -0
  29. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli.egg-info/entry_points.txt +0 -0
  30. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli.egg-info/requires.txt +0 -0
  31. {bookcli-0.1.0 → bookcli-0.2.1}/bookcli.egg-info/top_level.txt +0 -0
  32. {bookcli-0.1.0 → bookcli-0.2.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bookcli
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: A production-grade Python CLI book search and download application.
5
5
  License: MIT
6
6
  Requires-Python: >=3.12
@@ -1,3 +1,3 @@
1
1
  """BookCLI package."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.1"
@@ -14,6 +14,8 @@ from rich.console import Console
14
14
  from rich.table import Table
15
15
  from rich.panel import Panel
16
16
  from rich.theme import Theme
17
+ from rich.align import Align
18
+ from rich.text import Text
17
19
 
18
20
  from bookcli.settings import DB_PATH
19
21
  from bookcli.config import load_config, save_config, AppConfig
@@ -45,7 +47,7 @@ console = Console()
45
47
  app = typer.Typer(
46
48
  name="book",
47
49
  help="BookCLI: Search, inspect, and legally download books from public sources.",
48
- no_args_is_help=True
50
+ no_args_is_help=False
49
51
  )
50
52
 
51
53
  logger = logging.getLogger("bookcli")
@@ -292,8 +294,417 @@ async def add_favorite_internal(db_manager: DatabaseManager, book_id_input: str,
292
294
 
293
295
  # --- CLI Commands ---
294
296
 
295
- @app.callback()
297
+ def make_banner() -> Panel:
298
+ banner_text = Text()
299
+ banner_text.append("██████╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗██╗ ██╗\n", style="bold cyan")
300
+ banner_text.append("██╔══██╗██╔═══██╗██╔═══██╗██║ ██╔╝██╔════╝██║ ██║\n", style="bold cyan")
301
+ banner_text.append("██████╔╝██║ ██║██║ ██║█████╔╝ ██║ ██║ ██║\n", style="bold green")
302
+ banner_text.append("██╔══██╗██║ ██║██║ ██║██╔═██╗ ██║ ██║ ██║\n", style="bold green")
303
+ banner_text.append("██████╔╝╚██████╔╝╚██████╔╝██║ ██╗╚██████╗███████╗██║\n", style="bold yellow")
304
+ banner_text.append("╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝\n", style="bold yellow")
305
+ banner_text.append(" Search, inspect, and legally download books\n", style="italic dim white")
306
+ banner_text.append(" Powered by Open Library & Gutenberg\n", style="italic dim white")
307
+ banner_text.append(" Developer - Dipesh Malla\n", style="italic dim white")
308
+ banner_text.append(" GitHub : greycode009 ", style="italic dim white")
309
+
310
+
311
+ from rich.box import ROUNDED
312
+ return Panel(
313
+ Align.center(banner_text),
314
+ box=ROUNDED,
315
+ border_style="bold blue",
316
+ padding=(1, 2)
317
+ )
318
+
319
+
320
+ def make_menu_panel() -> Panel:
321
+ menu_table = Table.grid(padding=(0, 2))
322
+ menu_table.add_column(style="bold yellow", justify="right")
323
+ menu_table.add_column(style="bold white")
324
+
325
+ menu_table.add_row("[1]", "🔍 Search Books")
326
+ menu_table.add_row("[2]", "📜 Search History")
327
+ menu_table.add_row("[3]", "⭐ Favorite Books")
328
+ menu_table.add_row("[4]", "⚡ Metadata Cache Stats")
329
+ menu_table.add_row("[5]", "🧹 Clear Metadata Cache")
330
+ menu_table.add_row("[6]", "⚙️ Settings / Configuration")
331
+ menu_table.add_row("[7]", "❌ Exit Dashboard")
332
+
333
+ from rich.box import ROUNDED
334
+ return Panel(
335
+ menu_table,
336
+ title="[bold green]Main Menu[/bold green]",
337
+ box=ROUNDED,
338
+ border_style="green",
339
+ padding=(1, 4),
340
+ expand=False
341
+ )
342
+
343
+
344
+ def make_commands_panel() -> Panel:
345
+ cmd_table = Table(box=None, padding=(0, 1), show_header=True, header_style="bold magenta")
346
+ cmd_table.add_column("Command Syntax", style="cyan")
347
+ cmd_table.add_column("Description", style="dim white")
348
+
349
+ cmd_table.add_row("book search \"<query>\"", "Search books across all providers")
350
+ cmd_table.add_row("book info <id>", "Show detailed metadata for a book (use ID or index)")
351
+ cmd_table.add_row("book download <id>", "Download a book locally")
352
+ cmd_table.add_row("book open <id>", "Open a downloaded book in OS default reader")
353
+ cmd_table.add_row("book history", "Show search query history")
354
+ cmd_table.add_row("book favorite list", "List all favorite books")
355
+ cmd_table.add_row("book favorite add <id>", "Add a book to favorites")
356
+ cmd_table.add_row("book favorite remove <id>", "Remove a book from favorites")
357
+ cmd_table.add_row("book cache stats / clear", "Show cache statistics or clear cached items")
358
+ cmd_table.add_row("book config [set <key> <val>]", "View/modify configuration settings")
359
+
360
+ from rich.box import ROUNDED
361
+ return Panel(
362
+ cmd_table,
363
+ title="[bold magenta]Direct CLI Commands Guide[/bold magenta]",
364
+ box=ROUNDED,
365
+ border_style="magenta",
366
+ padding=(1, 2)
367
+ )
368
+
369
+
370
+ async def run_dashboard() -> None:
371
+ """Persistent dashboard interactive menu loop."""
372
+ db_manager = DatabaseManager(str(DB_PATH))
373
+ await db_manager.initialize()
374
+ config = load_config()
375
+
376
+ while True:
377
+ console.clear()
378
+ console.print(make_banner())
379
+ console.print(make_menu_panel())
380
+ console.print(make_commands_panel())
381
+
382
+ try:
383
+ choice = console.input("\n[bold yellow]Select an option (1-7) > [/bold yellow]").strip()
384
+ except (KeyboardInterrupt, EOFError):
385
+ console.print("\n[yellow]Goodbye![/yellow]")
386
+ break
387
+
388
+ if not choice:
389
+ continue
390
+
391
+ if choice == "1":
392
+ # 🔍 Search Books
393
+ console.clear()
394
+ console.print(Panel("[bold green]🔍 Search Books[/bold green]", border_style="green"))
395
+ query = console.input("[bold white]Enter search query (or press Enter to go back): [/bold white]").strip()
396
+ if not query:
397
+ continue
398
+
399
+ # Optional filters
400
+ author = console.input("[dim]Filter by Author (optional, press Enter to skip): [/dim]").strip() or None
401
+ isbn = console.input("[dim]Filter by ISBN (optional, press Enter to skip): [/dim]").strip() or None
402
+ publisher = console.input("[dim]Filter by Publisher (optional, press Enter to skip): [/dim]").strip() or None
403
+ subject = console.input("[dim]Filter by Subject (optional, press Enter to skip): [/dim]").strip() or None
404
+
405
+ with console.status("[bold green]Searching enabled providers..."):
406
+ try:
407
+ results = await search_books(
408
+ query=query,
409
+ config=config,
410
+ db_manager=db_manager,
411
+ author=author,
412
+ isbn=isbn,
413
+ publisher=publisher,
414
+ subject=subject
415
+ )
416
+ except Exception as e:
417
+ console.print(Panel(f"[bold red]Search Error:[/bold red] {e}", title="Error", border_style="red"))
418
+ console.input("\nPress Enter to return to main menu...")
419
+ continue
420
+
421
+ if not results:
422
+ console.print("[yellow]No books found matching the search criteria.[/yellow]")
423
+ console.input("\nPress Enter to return to main menu...")
424
+ continue
425
+
426
+ # Store search results session in DB
427
+ async with db_manager.connection() as conn:
428
+ await conn.execute("DELETE FROM search_results_session")
429
+ for idx, book in enumerate(results, start=1):
430
+ await conn.execute(
431
+ "INSERT INTO search_results_session (result_index, book_id) VALUES (?, ?)",
432
+ (idx, book.id)
433
+ )
434
+ await conn.commit()
435
+
436
+ # Format output as Rich table
437
+ table = Table(title=f"Search Results for '{query}'")
438
+ table.add_column("ID", justify="right", style="cyan", no_wrap=True)
439
+ table.add_column("Title", style="magenta")
440
+ table.add_column("Author(s)", style="green")
441
+ table.add_column("Year", justify="center", style="yellow")
442
+ table.add_column("Source", style="blue")
443
+ table.add_column("Download", justify="center")
444
+
445
+ for idx, book in enumerate(results, start=1):
446
+ if book.download_availability:
447
+ avail_str = f"[green]{book.file_format.upper()}[/green]" if book.file_format else "[green]Yes[/green]"
448
+ else:
449
+ avail_str = "[red]No[/red]"
450
+ author_str = ", ".join(book.authors) if book.authors else "Unknown"
451
+ year_str = str(book.published_year) if book.published_year else "N/A"
452
+ table.add_row(
453
+ str(idx),
454
+ book.title,
455
+ author_str,
456
+ year_str,
457
+ book.source,
458
+ avail_str
459
+ )
460
+
461
+ console.print(table)
462
+ console.print(f"[dim]Total: {len(results)} books.[/dim]")
463
+
464
+ # Start Interactive Explorer mode for the search results
465
+ console.print("\n[bold cyan]Interactive Explorer Mode[/bold cyan]")
466
+ console.print("[dim]Enter <ID> to download, 'i <ID>' for info, 'f <ID>' to favorite, 'o <ID>' to open, or 'q' to quit explorer.[/dim]")
467
+ while True:
468
+ try:
469
+ action = console.input("\n[bold yellow]Action > [/bold yellow]").strip()
470
+ if not action or action.lower() in ("q", "quit", "exit"):
471
+ break
472
+
473
+ tokens = action.split()
474
+ if not tokens:
475
+ continue
476
+
477
+ if tokens[0].isdigit():
478
+ val = tokens[0]
479
+ custom_path = None
480
+ if len(tokens) >= 3 and tokens[1] in ("-o", "--output"):
481
+ custom_path = " ".join(tokens[2:])
482
+ elif len(tokens) > 1:
483
+ console.print("[red]Invalid output path syntax. Use: <ID> -o <path>[/red]")
484
+ continue
485
+
486
+ try:
487
+ await download_book_internal(db_manager, val, config, custom_path=Path(custom_path) if custom_path else None)
488
+ except BookCLIError as e:
489
+ console.print(f"[red]Error: {e}[/red]")
490
+ elif len(tokens) >= 2:
491
+ cmd, val = tokens[0].lower(), " ".join(tokens[1:])
492
+ try:
493
+ if cmd == "i":
494
+ await show_info_internal(db_manager, val, config)
495
+ elif cmd == "f":
496
+ await add_favorite_internal(db_manager, val, config)
497
+ elif cmd == "o":
498
+ await open_book_internal(db_manager, val)
499
+ else:
500
+ console.print("[red]Unknown command prefix. Use 'i', 'f', 'o', or just the ID.[/red]")
501
+ except BookCLIError as e:
502
+ console.print(f"[red]Error: {e}[/red]")
503
+ else:
504
+ console.print("[red]Invalid input. Enter an ID index number or 'q' to exit.[/red]")
505
+ except (KeyboardInterrupt, EOFError):
506
+ break
507
+
508
+ elif choice == "2":
509
+ # 📜 Search History
510
+ console.clear()
511
+ console.print(Panel("[bold green]📜 Search History[/bold green]", border_style="green"))
512
+ hist = await get_search_history(db_manager, limit=20)
513
+ if not hist:
514
+ console.print("[yellow]Search history is empty.[/yellow]")
515
+ else:
516
+ table = Table(title="Search History")
517
+ table.add_column("Timestamp", style="cyan")
518
+ table.add_column("Query/Filters", style="magenta")
519
+ table.add_column("Results Count", style="green", justify="right")
520
+
521
+ for entry in hist:
522
+ table.add_row(
523
+ entry["timestamp"],
524
+ entry["query"],
525
+ str(entry["results_count"])
526
+ )
527
+ console.print(table)
528
+
529
+ console.print("\nOptions: [c] Clear history, [Any other key] Go back")
530
+ hist_choice = console.input("[bold yellow]Choose > [/bold yellow]").strip().lower()
531
+ if hist_choice == 'c':
532
+ await clear_search_history(db_manager)
533
+ console.print("[green]Search history cleared successfully.[/green]")
534
+ await asyncio.sleep(1)
535
+ continue
536
+ console.input("\nPress Enter to return to main menu...")
537
+
538
+ elif choice == "3":
539
+ # ⭐ Favorite Books
540
+ console.clear()
541
+ console.print(Panel("[bold green]⭐ Favorite Books[/bold green]", border_style="green"))
542
+ async with db_manager.connection() as conn:
543
+ async with conn.execute("SELECT book_id, title, added_at FROM favorites ORDER BY added_at DESC") as cursor:
544
+ rows = await cursor.fetchall()
545
+
546
+ if not rows:
547
+ console.print("[yellow]Favorites list is empty.[/yellow]")
548
+ console.input("\nPress Enter to return to main menu...")
549
+ continue
550
+
551
+ table = Table(title="Favorite Books")
552
+ table.add_column("Index", style="cyan", justify="right")
553
+ table.add_column("Book ID", style="cyan")
554
+ table.add_column("Title", style="magenta")
555
+ table.add_column("Added At", style="green")
556
+
557
+ favorites_map = {}
558
+ for idx, row in enumerate(rows, start=1):
559
+ table.add_row(
560
+ str(idx),
561
+ row["book_id"],
562
+ row["title"],
563
+ row["added_at"]
564
+ )
565
+ favorites_map[idx] = row["book_id"]
566
+
567
+ console.print(table)
568
+
569
+ console.print("\nOptions: [d <index>] Download, [o <index>] Open, [r <index>] Remove, [Any other key] Go back")
570
+ fav_action = console.input("[bold yellow]Choose > [/bold yellow]").strip()
571
+ if fav_action:
572
+ tokens = fav_action.split()
573
+ if len(tokens) >= 2 and tokens[0].lower() in ('d', 'o', 'r') and tokens[1].isdigit():
574
+ cmd = tokens[0].lower()
575
+ idx = int(tokens[1])
576
+ if idx in favorites_map:
577
+ book_id = favorites_map[idx]
578
+ if cmd == 'd':
579
+ try:
580
+ await download_book_internal(db_manager, book_id, config)
581
+ console.input("\nPress Enter...")
582
+ except BookCLIError as e:
583
+ console.print(f"[red]Error: {e}[/red]")
584
+ console.input("\nPress Enter...")
585
+ elif cmd == 'o':
586
+ try:
587
+ await open_book_internal(db_manager, book_id)
588
+ console.input("\nPress Enter...")
589
+ except BookCLIError as e:
590
+ console.print(f"[red]Error: {e}[/red]")
591
+ console.input("\nPress Enter...")
592
+ elif cmd == 'r':
593
+ async with db_manager.connection() as conn:
594
+ await conn.execute("DELETE FROM favorites WHERE book_id = ?", (book_id,))
595
+ await conn.commit()
596
+ console.print(f"[green]Removed book from favorites.[/green]")
597
+ await asyncio.sleep(1)
598
+ else:
599
+ console.print("[red]Invalid favorite index.[/red]")
600
+ await asyncio.sleep(1)
601
+ else:
602
+ continue
603
+
604
+ elif choice == "4":
605
+ # ⚡ Cache Statistics
606
+ console.clear()
607
+ console.print(Panel("[bold green]⚡ Cache Statistics[/bold green]", border_style="green"))
608
+ stats = await get_cache_db_stats(db_manager)
609
+ console.print(Panel(
610
+ f"Total Cached Books: [green]{stats['total_items']}[/green]\n"
611
+ f"Oldest Record: [yellow]{stats['oldest_item']}[/yellow]\n"
612
+ f"Newest Record: [yellow]{stats['newest_item']}[/yellow]",
613
+ title="Metadata Cache Statistics",
614
+ border_style="cyan"
615
+ ))
616
+ console.input("\nPress Enter to return to main menu...")
617
+
618
+ elif choice == "5":
619
+ # 🧹 Clear Cache
620
+ console.clear()
621
+ console.print(Panel("[bold green]🧹 Clear Cache[/bold green]", border_style="green"))
622
+ deleted_count = await clear_cache_db(db_manager)
623
+ console.print(f"[green]Metadata cache cleared. Removed {deleted_count} items.[/green]")
624
+ await asyncio.sleep(1.5)
625
+
626
+ elif choice == "6":
627
+ # ⚙️ Settings / Configuration
628
+ while True:
629
+ console.clear()
630
+ console.print(Panel("[bold green]⚙️ Settings / Configuration[/bold green]", border_style="green"))
631
+ config = load_config()
632
+ table = Table(title="Current Configuration Settings")
633
+ table.add_column("Index", style="cyan", justify="right")
634
+ table.add_column("Setting Key", style="cyan")
635
+ table.add_column("Current Value", style="yellow")
636
+
637
+ settings_list = [
638
+ ("download-dir", config.download_dir),
639
+ ("cache-ttl", str(config.cache_ttl_seconds)),
640
+ ("timeout", str(config.timeout_seconds)),
641
+ ("theme", config.theme),
642
+ ("provider:google-books", str(config.providers.google_books)),
643
+ ("provider:openlibrary", str(config.providers.openlibrary)),
644
+ ("provider:gutenberg", str(config.providers.gutenberg)),
645
+ ("provider:internet-archive", str(config.providers.internet_archive)),
646
+ ]
647
+
648
+ for idx, (k, v) in enumerate(settings_list, start=1):
649
+ table.add_row(str(idx), k, v)
650
+
651
+ console.print(table)
652
+ console.print("\nOptions: Enter index (1-8) to edit, or [q] to return to Main Menu")
653
+ cfg_choice = console.input("[bold yellow]Select option > [/bold yellow]").strip()
654
+ if cfg_choice.lower() in ('q', 'quit', 'back', 'exit'):
655
+ break
656
+ if cfg_choice.isdigit():
657
+ idx = int(cfg_choice)
658
+ if 1 <= idx <= len(settings_list):
659
+ key, current_val = settings_list[idx - 1]
660
+ new_val = console.input(f"Enter new value for [cyan]{key}[/cyan] (current: {current_val}): ").strip()
661
+ if not new_val:
662
+ continue
663
+
664
+ # Save config
665
+ if key == "download-dir":
666
+ config.download_dir = new_val
667
+ os.makedirs(new_val, exist_ok=True)
668
+ elif key == "cache-ttl":
669
+ if not new_val.isdigit():
670
+ console.print("[red]Cache TTL must be an integer.[/red]")
671
+ console.input("\nPress Enter...")
672
+ continue
673
+ config.cache_ttl_seconds = int(new_val)
674
+ elif key == "timeout":
675
+ if not new_val.isdigit():
676
+ console.print("[red]Timeout must be an integer.[/red]")
677
+ console.input("\nPress Enter...")
678
+ continue
679
+ config.timeout_seconds = int(new_val)
680
+ elif key == "theme":
681
+ config.theme = new_val
682
+ elif key.startswith("provider:"):
683
+ provider_name = key.split(":")[1]
684
+ prov_key = provider_name.replace("-", "_")
685
+ bool_val = new_val.lower() in ("true", "1", "yes", "on")
686
+ if hasattr(config.providers, prov_key):
687
+ setattr(config.providers, prov_key, bool_val)
688
+
689
+ try:
690
+ save_config(config)
691
+ console.print(f"[green]Successfully updated configuration setting '{key}' to '{new_val}'.[/green]")
692
+ await asyncio.sleep(1)
693
+ except Exception as e:
694
+ console.print(f"[red]Error saving configuration: {e}[/red]")
695
+ console.input("\nPress Enter...")
696
+ else:
697
+ console.print("[red]Invalid index.[/red]")
698
+ await asyncio.sleep(1)
699
+
700
+ elif choice == "7" or choice.lower() in ("q", "exit", "quit"):
701
+ console.print("[yellow]Goodbye![/yellow]")
702
+ break
703
+
704
+
705
+ @app.callback(invoke_without_command=True)
296
706
  def global_callback(
707
+ ctx: typer.Context,
297
708
  verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging (INFO)"),
298
709
  debug: bool = typer.Option(False, "--debug", "-d", help="Enable debug logging (DEBUG)")
299
710
  ):
@@ -310,6 +721,16 @@ def global_callback(
310
721
  )
311
722
  logger.setLevel(level)
312
723
 
724
+ # Check if we should run the interactive dashboard
725
+ if ctx.invoked_subcommand is None:
726
+ is_interactive = sys.stdin.isatty() or os.environ.get("BOOKCLI_INTERACTIVE") == "true"
727
+ if is_interactive:
728
+ asyncio.run(run_dashboard())
729
+ else:
730
+ console.print(make_banner())
731
+ console.print(make_commands_panel())
732
+ console.print("\n[bold yellow]Note:[/bold yellow] Run in an interactive terminal to access the full visual dashboard menu.")
733
+
313
734
 
314
735
  @app.command()
315
736
  def search(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bookcli
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: A production-grade Python CLI book search and download application.
5
5
  License: MIT
6
6
  Requires-Python: >=3.12
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "bookcli"
7
- version = "0.1.0"
7
+ version = "0.2.1"
8
8
  description = "A production-grade Python CLI book search and download application."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes