Skip to content

Commit ef62a13

Browse files
committed
Add caching layer with SQLite backend
- Implement SQLite-based caching with 24h TTL - Add --no-cache flag to bypass cache - Add --cache-stats and --clear-cache commands - Integrate caching into all search functions (AUR, pacman, APT, DNF, Flatpak, Snap) - Privacy-conscious design filters sensitive queries - Cache stored in user config directory (~/.cache/archpkg-helper/) - Significant performance improvements for repeated searches Fixes: Search performance optimization Features: Caching layer, cache management commands
1 parent f9152f0 commit ef62a13

9 files changed

Lines changed: 672 additions & 20 deletions

File tree

archpkg/cache.py

Lines changed: 499 additions & 0 deletions
Large diffs are not rendered by default.

archpkg/cli.py

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from archpkg.command_gen import generate_command
2525
from archpkg.logging_config import get_logger, PackageHelperLogger
2626
from archpkg.suggest import suggest_apps, list_purposes
27+
from archpkg.cache import get_cache_manager, CacheConfig
2728

2829
console = Console()
2930
logger = get_logger(__name__)
@@ -323,6 +324,7 @@ def main() -> None:
323324
search_parser = subparsers.add_parser('search', help='Search for packages by name')
324325
search_parser.add_argument('query', type=str, nargs='*', help='Name of the software to search for')
325326
search_parser.add_argument('--aur', action='store_true', help='Prefer AUR packages over Pacman when both are available')
327+
search_parser.add_argument('--no-cache', action='store_true', help='Bypass cache and perform fresh search')
326328

327329
# Suggest command
328330
suggest_parser = subparsers.add_parser('suggest', help='Get app suggestions based on purpose')
@@ -332,14 +334,53 @@ def main() -> None:
332334
# Global arguments
333335
parser.add_argument('--debug', action='store_true', help='Enable debug logging to console')
334336
parser.add_argument('--log-info', action='store_true', help='Show logging configuration and exit')
337+
parser.add_argument('--no-cache', action='store_true', help='Bypass cache and perform fresh search')
338+
parser.add_argument('--cache-stats', action='store_true', help='Show cache statistics and exit')
339+
parser.add_argument('--clear-cache', choices=['all', 'aur', 'pacman', 'apt', 'dnf', 'flatpak', 'snap'],
340+
help='Clear cache for specified source or all sources')
335341

336342
args = parser.parse_args()
337343

344+
# Initialize cache manager
345+
cache_config = CacheConfig(enabled=not args.no_cache)
346+
cache_manager = get_cache_manager(cache_config)
347+
338348
# Enable debug mode if requested
339349
if args.debug:
340350
PackageHelperLogger.set_debug_mode(True)
341351
logger.info("Debug mode enabled via command line argument")
342352

353+
# Handle cache-related commands
354+
if args.cache_stats:
355+
stats = cache_manager.get_stats()
356+
console.print(Panel(
357+
f"[bold cyan]Cache Statistics:[/bold cyan]\n"
358+
f"Enabled: {'[green]Yes[/green]' if stats.get('enabled') else '[red]No[/red]'}\n"
359+
f"Total entries: [yellow]{stats.get('total_entries', 0)}[/yellow]\n"
360+
f"Valid entries: [green]{stats.get('valid_entries', 0)}[/green]\n"
361+
f"Total accesses: [blue]{stats.get('total_accesses', 0)}[/blue]\n"
362+
f"Average access count: [magenta]{stats.get('avg_access_count', 0)}[/magenta]\n"
363+
f"Database path: [cyan]{stats.get('db_path', 'N/A')}[/cyan]\n"
364+
f"TTL: [yellow]{stats.get('config', {}).get('ttl_seconds', 0)}s[/yellow]\n"
365+
f"Max entries: [yellow]{stats.get('config', {}).get('max_entries', 0)}[/yellow]\n\n"
366+
f"[bold]Source breakdown:[/bold]\n" +
367+
'\n'.join([f" {source}: {count}" for source, count in stats.get('source_breakdown', {}).items()]),
368+
title="Cache Statistics",
369+
border_style="blue"
370+
))
371+
return
372+
373+
if args.clear_cache:
374+
source = None if args.clear_cache == 'all' else args.clear_cache
375+
cleared_count = cache_manager.clear(source)
376+
target = args.clear_cache if args.clear_cache != 'all' else 'all sources'
377+
console.print(Panel(
378+
f"[green]Successfully cleared {cleared_count} cache entries for {target}.[/green]",
379+
title="Cache Cleared",
380+
border_style="green"
381+
))
382+
return
383+
343384
# Show logging info if requested
344385
if args.log_info:
345386
from archpkg.logging_config import get_log_info
@@ -361,7 +402,7 @@ def main() -> None:
361402
return
362403
elif args.command == 'search' or args.command is None:
363404
# Default to search behavior for backward compatibility
364-
handle_search_command(args)
405+
handle_search_command(args, cache_manager)
365406
return
366407
else:
367408
console.print(Panel(
@@ -417,7 +458,7 @@ def handle_suggest_command(args) -> None:
417458
suggest_apps(purpose)
418459

419460

420-
def handle_search_command(args) -> None:
461+
def handle_search_command(args, cache_manager) -> None:
421462
"""Handle the search command (original functionality)."""
422463
if not args.query:
423464
console.print(Panel(
@@ -455,14 +496,15 @@ def handle_search_command(args) -> None:
455496

456497
results = []
457498
search_errors = []
499+
use_cache = not args.no_cache
458500

459501
# Search based on detected distribution
460502
if detected == "arch":
461503
logger.info("Searching Arch-based repositories (AUR + pacman)")
462504

463505
try:
464506
logger.debug("Starting AUR search")
465-
aur_results = search_aur(query)
507+
aur_results = search_aur(query, cache_manager if use_cache else None)
466508
results.extend(aur_results)
467509
logger.info(f"AUR search returned {len(aur_results)} results")
468510
except Exception as e:
@@ -471,7 +513,7 @@ def handle_search_command(args) -> None:
471513

472514
try:
473515
logger.debug("Starting pacman search")
474-
pacman_results = search_pacman(query)
516+
pacman_results = search_pacman(query, cache_manager if use_cache else None)
475517
results.extend(pacman_results)
476518
logger.info(f"Pacman search returned {len(pacman_results)} results")
477519
except Exception as e:
@@ -483,7 +525,7 @@ def handle_search_command(args) -> None:
483525

484526
try:
485527
logger.debug("Starting APT search")
486-
apt_results = search_apt(query)
528+
apt_results = search_apt(query, cache_manager if use_cache else None)
487529
results.extend(apt_results)
488530
logger.info(f"APT search returned {len(apt_results)} results")
489531
except Exception as e:
@@ -495,7 +537,7 @@ def handle_search_command(args) -> None:
495537

496538
try:
497539
logger.debug("Starting DNF search")
498-
dnf_results = search_dnf(query)
540+
dnf_results = search_dnf(query, cache_manager if use_cache else None)
499541
results.extend(dnf_results)
500542
logger.info(f"DNF search returned {len(dnf_results)} results")
501543
except Exception as e:
@@ -507,7 +549,7 @@ def handle_search_command(args) -> None:
507549

508550
try:
509551
logger.debug("Starting Flatpak search")
510-
flatpak_results = search_flatpak(query)
552+
flatpak_results = search_flatpak(query, cache_manager if use_cache else None)
511553
results.extend(flatpak_results)
512554
logger.info(f"Flatpak search returned {len(flatpak_results)} results")
513555
except Exception as e:
@@ -516,7 +558,7 @@ def handle_search_command(args) -> None:
516558

517559
try:
518560
logger.debug("Starting Snap search")
519-
snap_results = search_snap(query)
561+
snap_results = search_snap(query, cache_manager if use_cache else None)
520562
results.extend(snap_results)
521563
logger.info(f"Snap search returned {len(snap_results)} results")
522564
except Exception as e:

archpkg/config.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,31 @@
9797
'log_slow_operations_threshold': 5.0, # seconds
9898
'enable_memory_logging': False,
9999
'enable_detailed_tracing': False
100+
}
101+
102+
# Cache configuration settings
103+
CACHE_CONFIG = {
104+
'enabled': True,
105+
'ttl_seconds': 24 * 60 * 60, # 24 hours default
106+
'max_entries': 1000,
107+
'cleanup_interval': 3600, # 1 hour
108+
'cache_dir': None, # Auto-detect using XDG_CACHE_HOME
109+
'db_name': 'cache.db',
110+
111+
# Privacy settings
112+
'respect_privacy': True,
113+
'exclude_sensitive_queries': True,
114+
'max_description_length': 500,
115+
116+
# Performance settings
117+
'enable_compression': False, # Future feature
118+
'batch_size': 100, # For bulk operations
119+
'connection_timeout': 5.0, # Database connection timeout
120+
}
121+
122+
# Cache-related timeouts (separate from search timeouts)
123+
CACHE_TIMEOUTS = {
124+
'db_connection': 5.0,
125+
'cleanup_operation': 30.0,
126+
'stats_query': 10.0,
100127
}

archpkg/search_apt.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@
33
IMPROVEMENTS: Standardized source name to lowercase, used config timeouts, unified exception handling."""
44

55
import subprocess
6-
from typing import List, Tuple
6+
from typing import List, Tuple, Optional
77
from archpkg.config import TIMEOUTS
88
from archpkg.exceptions import PackageManagerNotFound, PackageSearchException, TimeoutError, ValidationError
99
from archpkg.logging_config import get_logger, PackageHelperLogger
1010

1111
logger = get_logger(__name__)
1212

13-
def search_apt(query: str) -> List[Tuple[str, str, str]]:
13+
def search_apt(query: str, cache_manager: Optional[object] = None) -> List[Tuple[str, str, str]]:
1414
"""Search for packages using the APT package manager.
1515
1616
Args:
1717
query: Search query string
18+
cache_manager: Optional cache manager for storing/retrieving results
1819
1920
Returns:
2021
List[Tuple[str, str, str]]: List of (name, description, source) tuples
@@ -32,6 +33,13 @@ def search_apt(query: str) -> List[Tuple[str, str, str]]:
3233
logger.error("Empty search query provided to APT search")
3334
raise ValidationError("Search query cannot be empty. Please provide a package name to search for.")
3435

36+
# Check cache first if available
37+
if cache_manager:
38+
cached_results = cache_manager.get(query, 'apt')
39+
if cached_results is not None:
40+
logger.info(f"Retrieved {len(cached_results)} APT results from cache")
41+
return cached_results
42+
3543
# Check if apt-cache is available
3644
logger.debug("Checking APT availability")
3745
try:
@@ -108,6 +116,12 @@ def search_apt(query: str) -> List[Tuple[str, str, str]]:
108116
logger.debug(f"Found APT package: {name.strip()}")
109117

110118
logger.info(f"APT search completed: {len(packages)} packages found from {lines_processed} lines")
119+
120+
# Cache results if cache manager is available
121+
if cache_manager and packages:
122+
cache_manager.set(query, 'apt', packages)
123+
logger.debug(f"Cached {len(packages)} APT results")
124+
111125
return packages
112126

113127
except subprocess.TimeoutExpired:

archpkg/search_aur.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@
44

55
import requests
66
import json
7-
from typing import List, Tuple
7+
from typing import List, Tuple, Optional
88
from archpkg.config import TIMEOUTS
99
from archpkg.exceptions import NetworkError, TimeoutError, ValidationError, PackageSearchException
1010
from archpkg.logging_config import get_logger, PackageHelperLogger
1111

1212
logger = get_logger(__name__)
1313

14-
def search_aur(query: str) -> List[Tuple[str, str, str]]:
14+
def search_aur(query: str, cache_manager: Optional[object] = None) -> List[Tuple[str, str, str]]:
1515
"""Search for packages in the Arch User Repository (AUR).
1616
1717
Args:
1818
query: Search query string
19+
cache_manager: Optional cache manager for storing/retrieving results
1920
2021
Returns:
2122
List[Tuple[str, str, str]]: List of (name, description, source) tuples
@@ -33,6 +34,13 @@ def search_aur(query: str) -> List[Tuple[str, str, str]]:
3334
logger.error("Empty search query provided to AUR search")
3435
raise ValidationError("Empty search query provided")
3536

37+
# Check cache first if available
38+
if cache_manager:
39+
cached_results = cache_manager.get(query, 'aur')
40+
if cached_results is not None:
41+
logger.info(f"Retrieved {len(cached_results)} AUR results from cache")
42+
return cached_results
43+
3644
# Construct AUR RPC search API URL
3745
url = f"https://aur.archlinux.org/rpc/?v=5&type=search&arg={query.strip()}"
3846
logger.debug(f"AUR API URL: {url}")
@@ -80,6 +88,12 @@ def search_aur(query: str) -> List[Tuple[str, str, str]]:
8088

8189
logger.info(f"AUR search completed: {len(processed_results)} valid packages found")
8290
# IMPROVED: Standardized source name to lowercase
91+
92+
# Cache results if cache manager is available
93+
if cache_manager and processed_results:
94+
cache_manager.set(query, 'aur', processed_results)
95+
logger.debug(f"Cached {len(processed_results)} AUR results")
96+
8397
return processed_results
8498

8599
except requests.exceptions.ConnectionError as e:

archpkg/search_dnf.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@
44

55
import subprocess
66
import re
7-
from typing import List, Tuple
7+
from typing import List, Tuple, Optional
88
from archpkg.config import TIMEOUTS
99
from archpkg.exceptions import PackageManagerNotFound, PackageSearchException, TimeoutError, ValidationError, NetworkError
1010
from archpkg.logging_config import get_logger, PackageHelperLogger
1111

1212
logger = get_logger(__name__)
1313

14-
def search_dnf(query: str) -> List[Tuple[str, str, str]]:
14+
def search_dnf(query: str, cache_manager: Optional[object] = None) -> List[Tuple[str, str, str]]:
1515
"""Search for packages using DNF package manager.
1616
1717
Args:
1818
query: Search query string
19+
cache_manager: Optional cache manager for storing/retrieving results
1920
2021
Returns:
2122
List[Tuple[str, str, str]]: List of (name, description, source) tuples
@@ -33,6 +34,13 @@ def search_dnf(query: str) -> List[Tuple[str, str, str]]:
3334
logger.error("Empty search query provided to DNF search")
3435
raise ValidationError("Empty search query provided")
3536

37+
# Check cache first if available
38+
if cache_manager:
39+
cached_results = cache_manager.get(query, 'dnf')
40+
if cached_results is not None:
41+
logger.info(f"Retrieved {len(cached_results)} DNF results from cache")
42+
return cached_results
43+
3644
# Check if DNF is available and working
3745
logger.debug("Checking DNF availability")
3846
try:
@@ -137,6 +145,12 @@ def search_dnf(query: str) -> List[Tuple[str, str, str]]:
137145
logger.debug(f"Found DNF package: {name}")
138146

139147
logger.info(f"DNF search completed: {len(packages)} packages found from {lines_processed} lines")
148+
149+
# Cache results if cache manager is available
150+
if cache_manager and packages:
151+
cache_manager.set(query, 'dnf', packages)
152+
logger.debug(f"Cached {len(packages)} DNF results")
153+
140154
return packages
141155

142156
except subprocess.TimeoutExpired:

archpkg/search_flatpak.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@
33
IMPROVEMENTS: Standardized source name to lowercase, used config timeouts, unified exception handling."""
44

55
import subprocess
6-
from typing import List, Tuple
6+
from typing import List, Tuple, Optional
77
from archpkg.config import TIMEOUTS
88
from archpkg.exceptions import PackageManagerNotFound, PackageSearchException, TimeoutError, ValidationError
99
from archpkg.logging_config import get_logger, PackageHelperLogger
1010

1111
logger = get_logger(__name__)
1212

13-
def search_flatpak(query: str) -> List[Tuple[str, str, str]]:
13+
def search_flatpak(query: str, cache_manager: Optional[object] = None) -> List[Tuple[str, str, str]]:
1414
"""Search for packages using the Flatpak package manager.
1515
1616
Args:
1717
query: Search query string
18+
cache_manager: Optional cache manager for storing/retrieving results
1819
1920
Returns:
2021
List[Tuple[str, str, str]]: List of (name, description, source) tuples
@@ -31,6 +32,13 @@ def search_flatpak(query: str) -> List[Tuple[str, str, str]]:
3132
logger.error("Empty search query provided to Flatpak search")
3233
raise ValidationError("Empty search query provided")
3334

35+
# Check cache first if available
36+
if cache_manager:
37+
cached_results = cache_manager.get(query, 'flatpak')
38+
if cached_results is not None:
39+
logger.info(f"Retrieved {len(cached_results)} Flatpak results from cache")
40+
return cached_results
41+
3442
# Check if flatpak is available and working
3543
logger.debug("Checking Flatpak availability")
3644
try:
@@ -115,6 +123,12 @@ def search_flatpak(query: str) -> List[Tuple[str, str, str]]:
115123
logger.debug(f"Skipping malformed Flatpak result line: {line}")
116124

117125
logger.info(f"Flatpak search completed: {len(packages)} packages found from {lines_processed} lines")
126+
127+
# Cache results if cache manager is available
128+
if cache_manager and packages:
129+
cache_manager.set(query, 'flatpak', packages)
130+
logger.debug(f"Cached {len(packages)} Flatpak results")
131+
118132
return packages
119133

120134
except subprocess.TimeoutExpired:

0 commit comments

Comments
 (0)