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