fo-core 2.0.0b2__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.
Files changed (302) hide show
  1. cli/__init__.py +65 -0
  2. cli/analytics.py +391 -0
  3. cli/autotag_v2.py +252 -0
  4. cli/benchmark.py +1352 -0
  5. cli/completion.py +54 -0
  6. cli/config_cli.py +102 -0
  7. cli/copilot.py +99 -0
  8. cli/daemon.py +236 -0
  9. cli/dedupe_renderer.py +465 -0
  10. cli/dedupe_v2.py +385 -0
  11. cli/doctor.py +460 -0
  12. cli/interactive.py +107 -0
  13. cli/lazy.py +173 -0
  14. cli/main.py +452 -0
  15. cli/models_cli.py +52 -0
  16. cli/organize.py +260 -0
  17. cli/path_validation.py +147 -0
  18. cli/profile.py +520 -0
  19. cli/rules.py +292 -0
  20. cli/setup.py +261 -0
  21. cli/state.py +47 -0
  22. cli/suggest.py +244 -0
  23. cli/undo_history.py +319 -0
  24. cli/undo_recover.py +149 -0
  25. cli/undo_redo.py +269 -0
  26. cli/update.py +98 -0
  27. cli/utilities.py +463 -0
  28. config/__init__.py +18 -0
  29. config/manager.py +488 -0
  30. config/migrations.py +218 -0
  31. config/path_manager.py +157 -0
  32. config/path_migration.py +112 -0
  33. config/provider_env.py +362 -0
  34. config/schema.py +113 -0
  35. core/__init__.py +28 -0
  36. core/backend_detector.py +267 -0
  37. core/dispatcher.py +404 -0
  38. core/display.py +99 -0
  39. core/file_ops.py +206 -0
  40. core/hardware_profile.py +317 -0
  41. core/initializer.py +117 -0
  42. core/organizer.py +669 -0
  43. core/path_guard.py +202 -0
  44. core/setup_wizard.py +373 -0
  45. core/types.py +124 -0
  46. daemon/__init__.py +20 -0
  47. daemon/config.py +59 -0
  48. daemon/pid.py +446 -0
  49. daemon/scheduler.py +197 -0
  50. daemon/service.py +524 -0
  51. events/__init__.py +82 -0
  52. events/audit.py +286 -0
  53. events/config.py +46 -0
  54. events/consumer.py +260 -0
  55. events/discovery.py +266 -0
  56. events/health.py +275 -0
  57. events/middleware.py +464 -0
  58. events/monitor.py +251 -0
  59. events/publisher.py +179 -0
  60. events/pubsub.py +301 -0
  61. events/replay.py +322 -0
  62. events/service_bus.py +374 -0
  63. events/stream.py +432 -0
  64. events/subscription.py +285 -0
  65. events/types.py +134 -0
  66. fo_core-2.0.0b2.dist-info/METADATA +274 -0
  67. fo_core-2.0.0b2.dist-info/RECORD +302 -0
  68. fo_core-2.0.0b2.dist-info/WHEEL +5 -0
  69. fo_core-2.0.0b2.dist-info/entry_points.txt +2 -0
  70. fo_core-2.0.0b2.dist-info/licenses/LICENSE +21 -0
  71. fo_core-2.0.0b2.dist-info/top_level.txt +18 -0
  72. history/__init__.py +30 -0
  73. history/cleanup.py +425 -0
  74. history/database.py +464 -0
  75. history/export.py +347 -0
  76. history/models.py +306 -0
  77. history/tracker.py +370 -0
  78. history/transaction.py +307 -0
  79. integrations/__init__.py +25 -0
  80. integrations/base.py +73 -0
  81. integrations/browser.py +68 -0
  82. integrations/manager.py +109 -0
  83. integrations/obsidian.py +141 -0
  84. integrations/vscode.py +96 -0
  85. integrations/workflow.py +108 -0
  86. interfaces/__init__.py +43 -0
  87. interfaces/intelligence.py +63 -0
  88. interfaces/model.py +75 -0
  89. interfaces/pipeline.py +116 -0
  90. interfaces/processor.py +71 -0
  91. interfaces/search.py +97 -0
  92. interfaces/storage.py +77 -0
  93. methodologies/__init__.py +3 -0
  94. methodologies/johnny_decimal/__init__.py +147 -0
  95. methodologies/johnny_decimal/adapters.py +392 -0
  96. methodologies/johnny_decimal/categories.py +491 -0
  97. methodologies/johnny_decimal/compatibility.py +426 -0
  98. methodologies/johnny_decimal/config.py +417 -0
  99. methodologies/johnny_decimal/migrator.py +448 -0
  100. methodologies/johnny_decimal/numbering.py +526 -0
  101. methodologies/johnny_decimal/scanner.py +363 -0
  102. methodologies/johnny_decimal/system.py +574 -0
  103. methodologies/johnny_decimal/transformer.py +360 -0
  104. methodologies/johnny_decimal/validator.py +349 -0
  105. methodologies/para/__init__.py +59 -0
  106. methodologies/para/ai/__init__.py +59 -0
  107. methodologies/para/ai/feature_extractor.py +540 -0
  108. methodologies/para/ai/feedback.py +549 -0
  109. methodologies/para/ai/file_mover.py +492 -0
  110. methodologies/para/ai/suggestion_engine.py +620 -0
  111. methodologies/para/categories.py +379 -0
  112. methodologies/para/config.py +351 -0
  113. methodologies/para/default_config.yaml +123 -0
  114. methodologies/para/detection/__init__.py +27 -0
  115. methodologies/para/detection/heuristics.py +1170 -0
  116. methodologies/para/folder_generator.py +267 -0
  117. methodologies/para/folder_mapper.py +382 -0
  118. methodologies/para/migration_manager.py +725 -0
  119. methodologies/para/rules/__init__.py +37 -0
  120. methodologies/para/rules/engine.py +585 -0
  121. models/__init__.py +60 -0
  122. models/_claude_client.py +76 -0
  123. models/_claude_response.py +56 -0
  124. models/_llama_cpp_helpers.py +53 -0
  125. models/_ollama_response.py +90 -0
  126. models/_openai_client.py +81 -0
  127. models/_openai_response.py +35 -0
  128. models/_vision_helpers.py +90 -0
  129. models/analytics.py +263 -0
  130. models/audio_model.py +154 -0
  131. models/audio_registry.py +51 -0
  132. models/audio_transcriber.py +462 -0
  133. models/base.py +256 -0
  134. models/claude_text_model.py +174 -0
  135. models/claude_vision_model.py +279 -0
  136. models/llama_cpp_text_model.py +243 -0
  137. models/mlx_text_model.py +237 -0
  138. models/model_manager.py +335 -0
  139. models/openai_text_model.py +183 -0
  140. models/openai_vision_model.py +255 -0
  141. models/provider_factory.py +56 -0
  142. models/provider_registry.py +302 -0
  143. models/registry.py +74 -0
  144. models/suggestion_types.py +171 -0
  145. models/text_model.py +367 -0
  146. models/text_registry.py +49 -0
  147. models/vision_model.py +336 -0
  148. models/vision_registry.py +51 -0
  149. optimization/__init__.py +58 -0
  150. optimization/batch_sizer.py +275 -0
  151. optimization/buffer_pool.py +218 -0
  152. optimization/connection_pool.py +240 -0
  153. optimization/database.py +491 -0
  154. optimization/lazy_loader.py +206 -0
  155. optimization/leak_detector.py +219 -0
  156. optimization/memory_limiter.py +207 -0
  157. optimization/memory_profiler.py +286 -0
  158. optimization/model_cache.py +284 -0
  159. optimization/query_cache.py +226 -0
  160. optimization/resource_monitor.py +290 -0
  161. optimization/warmup.py +208 -0
  162. parallel/__init__.py +45 -0
  163. parallel/checkpoint.py +262 -0
  164. parallel/config.py +61 -0
  165. parallel/executor.py +66 -0
  166. parallel/models.py +158 -0
  167. parallel/persistence.py +163 -0
  168. parallel/priority_queue.py +191 -0
  169. parallel/processor.py +554 -0
  170. parallel/resource_manager.py +196 -0
  171. parallel/result.py +86 -0
  172. parallel/resume.py +279 -0
  173. parallel/scheduler.py +117 -0
  174. parallel/throttle.py +155 -0
  175. pipeline/__init__.py +24 -0
  176. pipeline/config.py +145 -0
  177. pipeline/orchestrator.py +744 -0
  178. pipeline/processor_pool.py +198 -0
  179. pipeline/resource_aware_executor.py +406 -0
  180. pipeline/router.py +217 -0
  181. pipeline/stages/__init__.py +25 -0
  182. pipeline/stages/analyzer.py +86 -0
  183. pipeline/stages/postprocessor.py +153 -0
  184. pipeline/stages/preprocessor.py +79 -0
  185. pipeline/stages/writer.py +114 -0
  186. services/__init__.py +47 -0
  187. services/analytics/__init__.py +17 -0
  188. services/analytics/analytics_service.py +425 -0
  189. services/analytics/metrics_calculator.py +131 -0
  190. services/analytics/storage_analyzer.py +287 -0
  191. services/analyzer.py +165 -0
  192. services/audio/__init__.py +58 -0
  193. services/audio/classifier.py +541 -0
  194. services/audio/content_analyzer.py +274 -0
  195. services/audio/data/content_analyzer_lexicon.json +405 -0
  196. services/audio/lexicons.py +191 -0
  197. services/audio/metadata_extractor.py +391 -0
  198. services/audio/organizer.py +404 -0
  199. services/audio/preprocessor.py +378 -0
  200. services/audio/transcriber.py +343 -0
  201. services/audio/utils.py +384 -0
  202. services/auto_tagging/__init__.py +92 -0
  203. services/auto_tagging/content_analyzer.py +467 -0
  204. services/auto_tagging/tag_learning.py +500 -0
  205. services/auto_tagging/tag_recommender.py +406 -0
  206. services/copilot/__init__.py +31 -0
  207. services/copilot/conversation.py +156 -0
  208. services/copilot/engine.py +262 -0
  209. services/copilot/executor.py +460 -0
  210. services/copilot/intent_parser.py +301 -0
  211. services/copilot/models.py +118 -0
  212. services/copilot/rules/__init__.py +26 -0
  213. services/copilot/rules/models.py +202 -0
  214. services/copilot/rules/preview.py +253 -0
  215. services/copilot/rules/rule_manager.py +209 -0
  216. services/deduplication/__init__.py +70 -0
  217. services/deduplication/backup.py +419 -0
  218. services/deduplication/detector.py +308 -0
  219. services/deduplication/document_dedup.py +163 -0
  220. services/deduplication/embedder.py +366 -0
  221. services/deduplication/extractor.py +517 -0
  222. services/deduplication/hasher.py +209 -0
  223. services/deduplication/image_dedup.py +493 -0
  224. services/deduplication/image_utils.py +573 -0
  225. services/deduplication/index.py +207 -0
  226. services/deduplication/quality.py +441 -0
  227. services/deduplication/reporter.py +153 -0
  228. services/deduplication/semantic.py +332 -0
  229. services/deduplication/viewer.py +683 -0
  230. services/intelligence/__init__.py +104 -0
  231. services/intelligence/confidence.py +530 -0
  232. services/intelligence/conflict_resolver.py +412 -0
  233. services/intelligence/directory_prefs.py +251 -0
  234. services/intelligence/feedback_processor.py +445 -0
  235. services/intelligence/folder_learner.py +337 -0
  236. services/intelligence/naming_analyzer.py +457 -0
  237. services/intelligence/pattern_extractor.py +495 -0
  238. services/intelligence/pattern_learner.py +409 -0
  239. services/intelligence/preference_database.py +591 -0
  240. services/intelligence/preference_storage.py +727 -0
  241. services/intelligence/preference_store.py +592 -0
  242. services/intelligence/preference_tracker.py +466 -0
  243. services/intelligence/profile_exporter.py +337 -0
  244. services/intelligence/profile_importer.py +532 -0
  245. services/intelligence/profile_manager.py +499 -0
  246. services/intelligence/profile_merger.py +497 -0
  247. services/intelligence/profile_migrator.py +482 -0
  248. services/intelligence/scoring.py +412 -0
  249. services/intelligence/template_manager.py +556 -0
  250. services/misplacement_detector.py +491 -0
  251. services/pattern_analyzer.py +453 -0
  252. services/search/__init__.py +18 -0
  253. services/search/bm25_index.py +138 -0
  254. services/search/embedding_cache.py +265 -0
  255. services/search/hybrid_retriever.py +347 -0
  256. services/search/vector_index.py +126 -0
  257. services/smart_suggestions.py +525 -0
  258. services/suggestion_feedback.py +401 -0
  259. services/text_processor.py +558 -0
  260. services/video/__init__.py +25 -0
  261. services/video/metadata_extractor.py +291 -0
  262. services/video/organizer.py +171 -0
  263. services/video/scene_detector.py +426 -0
  264. services/vision_processor.py +584 -0
  265. undo/__init__.py +26 -0
  266. undo/_journal.py +29 -0
  267. undo/durable_move.py +1551 -0
  268. undo/models.py +125 -0
  269. undo/rollback.py +680 -0
  270. undo/trash_gc.py +541 -0
  271. undo/undo_manager.py +454 -0
  272. undo/validator.py +580 -0
  273. undo/viewer.py +384 -0
  274. updater/__init__.py +34 -0
  275. updater/background.py +43 -0
  276. updater/checker.py +231 -0
  277. updater/installer.py +413 -0
  278. updater/manager.py +165 -0
  279. updater/sidecar_updater.py +285 -0
  280. updater/state.py +100 -0
  281. utils/__init__.py +15 -0
  282. utils/atomic_io.py +36 -0
  283. utils/atomic_write.py +331 -0
  284. utils/chart_generator.py +150 -0
  285. utils/cli_errors.py +55 -0
  286. utils/epub_enhanced.py +770 -0
  287. utils/file_readers.py +62 -0
  288. utils/log_redact.py +474 -0
  289. utils/readers/__init__.py +379 -0
  290. utils/readers/_base.py +71 -0
  291. utils/readers/archives.py +413 -0
  292. utils/readers/cad.py +564 -0
  293. utils/readers/documents.py +429 -0
  294. utils/readers/ebook.py +119 -0
  295. utils/readers/scientific.py +321 -0
  296. utils/safedir.py +579 -0
  297. utils/text_processing.py +344 -0
  298. watcher/__init__.py +21 -0
  299. watcher/config.py +137 -0
  300. watcher/handler.py +494 -0
  301. watcher/monitor.py +331 -0
  302. watcher/queue.py +219 -0
cli/__init__.py ADDED
@@ -0,0 +1,65 @@
1
+ """Command-line interface modules for File Organizer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ # Lazy-load all CLI sub-apps and utilities to reduce startup latency.
6
+ # The entrypoint (cli:main) accesses `main` and `app` which
7
+ # trigger cli.main imports — this module defers everything else.
8
+
9
+ __all__ = [
10
+ "app",
11
+ "autotag_app",
12
+ "copilot_app",
13
+ "daemon_app",
14
+ "main",
15
+ "complete_directory",
16
+ "complete_file",
17
+ "confirm_action",
18
+ "create_progress",
19
+ "dedupe_app",
20
+ "history_command",
21
+ "prompt_choice",
22
+ "prompt_directory",
23
+ "redo_command",
24
+ "rules_app",
25
+ "suggest_app",
26
+ "profile_command",
27
+ "undo_command",
28
+ "update_app",
29
+ ]
30
+
31
+ _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
32
+ "app": ("cli.main", "app"),
33
+ "main": ("cli.main", "main"),
34
+ "autotag_app": ("cli.autotag_v2", "autotag_app"),
35
+ "complete_directory": ("cli.completion", "complete_directory"),
36
+ "complete_file": ("cli.completion", "complete_file"),
37
+ "copilot_app": ("cli.copilot", "copilot_app"),
38
+ "daemon_app": ("cli.daemon", "daemon_app"),
39
+ "dedupe_app": ("cli.dedupe_v2", "dedupe_app"),
40
+ "confirm_action": ("cli.interactive", "confirm_action"),
41
+ "create_progress": ("cli.interactive", "create_progress"),
42
+ "prompt_choice": ("cli.interactive", "prompt_choice"),
43
+ "prompt_directory": ("cli.interactive", "prompt_directory"),
44
+ "profile_command": ("cli.profile", "profile_command"),
45
+ "rules_app": ("cli.rules", "rules_app"),
46
+ "suggest_app": ("cli.suggest", "suggest_app"),
47
+ "history_command": ("cli.undo_redo", "history_command"),
48
+ "redo_command": ("cli.undo_redo", "redo_command"),
49
+ "undo_command": ("cli.undo_redo", "undo_command"),
50
+ "update_app": ("cli.update", "update_app"),
51
+ }
52
+
53
+
54
+ def __getattr__(name: str) -> object:
55
+ """Lazily import CLI attributes on first access."""
56
+ if name in _LAZY_IMPORTS:
57
+ module_path, attr = _LAZY_IMPORTS[name]
58
+ import importlib
59
+
60
+ module = importlib.import_module(module_path)
61
+ value = getattr(module, attr)
62
+ # Cache in module globals to avoid repeated lookups
63
+ globals()[name] = value
64
+ return value
65
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
cli/analytics.py ADDED
@@ -0,0 +1,391 @@
1
+ # pyre-ignore-all-errors
2
+ #!/usr/bin/env python3
3
+ """Analytics CLI - Display comprehensive analytics dashboard.
4
+
5
+ This module provides a command-line interface for viewing storage analytics,
6
+ quality metrics, and organization insights.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ from loguru import logger
16
+ from rich.console import Console
17
+ from rich.table import Table
18
+
19
+ from models.analytics import (
20
+ DuplicateStats,
21
+ FileDistribution,
22
+ QualityMetrics,
23
+ StorageStats,
24
+ TimeSavings,
25
+ )
26
+ from services.analytics import AnalyticsService
27
+ from utils.chart_generator import ChartGenerator
28
+
29
+ console = Console()
30
+
31
+
32
+ def _format_bytes(size_bytes: int) -> str:
33
+ """Format bytes as human-readable size."""
34
+ value: float = size_bytes
35
+ for unit in ["B", "KB", "MB", "GB", "TB"]:
36
+ if value < 1024.0:
37
+ return f"{value:.1f} {unit}"
38
+ value /= 1024.0
39
+ return f"{value:.1f} PB"
40
+
41
+
42
+ def _format_duration(seconds: float) -> str:
43
+ """Format seconds as human-readable duration."""
44
+ if seconds < 60:
45
+ return f"{seconds:.1f}s"
46
+ minutes = seconds / 60
47
+ if minutes < 60:
48
+ return f"{minutes:.1f}m"
49
+ hours = minutes / 60
50
+ return f"{hours:.1f}h"
51
+
52
+
53
+ def display_storage_stats(stats: StorageStats, chart_gen: ChartGenerator | None) -> None:
54
+ """Display storage statistics with optional charts."""
55
+ console.print("\n[bold cyan]STORAGE STATISTICS[/bold cyan]")
56
+ console.print("=" * 70)
57
+
58
+ # Create table for stats
59
+ table = Table(show_header=False, box=None, padding=(0, 2))
60
+ table.add_column("Metric", style="bold")
61
+ table.add_column("Value", style="cyan")
62
+
63
+ table.add_row("Total Size", stats.formatted_total_size)
64
+ table.add_row("Files", str(stats.file_count))
65
+ table.add_row("Directories", str(stats.directory_count))
66
+ table.add_row("Space Saved", stats.formatted_saved_size)
67
+ table.add_row("Savings", f"{stats.savings_percentage:.1f}%")
68
+
69
+ console.print(table)
70
+
71
+ # Show file type distribution
72
+ if stats.size_by_type and chart_gen:
73
+ console.print("\n[bold]File Type Distribution (by size)[/bold]")
74
+ # Convert bytes to percentages
75
+ total_size = sum(stats.size_by_type.values())
76
+ if total_size > 0:
77
+ type_percentages = {k: (v / total_size) * 100 for k, v in stats.size_by_type.items()}
78
+ chart = chart_gen.create_pie_chart(type_percentages, "File Types", width=40)
79
+ console.print(chart)
80
+
81
+ # Show largest files
82
+ if stats.largest_files:
83
+ console.print("\n[bold]Largest Files (Top 10)[/bold]")
84
+ file_table = Table(show_header=True)
85
+ file_table.add_column("Size", style="green", justify="right")
86
+ file_table.add_column("Type", style="yellow")
87
+ file_table.add_column("Path", style="cyan")
88
+
89
+ for file_info in stats.largest_files[:10]:
90
+ # Format size using public method
91
+ size_str = _format_bytes(file_info.size)
92
+ file_table.add_row(size_str, file_info.type, str(file_info.path.name))
93
+
94
+ console.print(file_table)
95
+
96
+
97
+ def display_quality_metrics(metrics: QualityMetrics) -> None:
98
+ """Display quality metrics."""
99
+ console.print("\n[bold cyan]QUALITY METRICS[/bold cyan]")
100
+ console.print("=" * 70)
101
+
102
+ # Overall score with color coding
103
+ score_color = (
104
+ "green"
105
+ if metrics.quality_score >= 70
106
+ else "yellow"
107
+ if metrics.quality_score >= 50
108
+ else "red"
109
+ )
110
+
111
+ console.print(
112
+ f"\n[bold]Overall Quality Score:[/bold] [{score_color}]{metrics.formatted_score}[/{score_color}]"
113
+ )
114
+
115
+ # Individual metrics
116
+ table = Table(show_header=True, box=None, padding=(0, 2))
117
+ table.add_column("Metric", style="bold")
118
+ table.add_column("Score", style="cyan", justify="right")
119
+ table.add_column("Bar", style="green")
120
+
121
+ def format_percentage_bar(value: float, width: int = 20) -> str:
122
+ """Create a simple percentage bar."""
123
+ filled = int(value * width)
124
+ bar = "█" * filled + "░" * (width - filled)
125
+ return f"{bar} {value * 100:.0f}%"
126
+
127
+ table.add_row(
128
+ "Naming Compliance",
129
+ f"{metrics.naming_compliance:.2f}",
130
+ format_percentage_bar(metrics.naming_compliance),
131
+ )
132
+ table.add_row(
133
+ "Structure Consistency",
134
+ f"{metrics.structure_consistency:.2f}",
135
+ format_percentage_bar(metrics.structure_consistency),
136
+ )
137
+ table.add_row(
138
+ "Metadata Completeness",
139
+ f"{metrics.metadata_completeness:.2f}",
140
+ format_percentage_bar(metrics.metadata_completeness),
141
+ )
142
+ table.add_row(
143
+ "Categorization Accuracy",
144
+ f"{metrics.categorization_accuracy:.2f}",
145
+ format_percentage_bar(metrics.categorization_accuracy),
146
+ )
147
+
148
+ console.print(table)
149
+
150
+
151
+ def display_duplicate_stats(stats: DuplicateStats) -> None:
152
+ """Display duplicate statistics."""
153
+ console.print("\n[bold cyan]DUPLICATE STATISTICS[/bold cyan]")
154
+ console.print("=" * 70)
155
+
156
+ if stats.total_duplicates == 0:
157
+ console.print("\n[green]✓ No duplicates found![/green]")
158
+ return
159
+
160
+ table = Table(show_header=False, box=None, padding=(0, 2))
161
+ table.add_column("Metric", style="bold")
162
+ table.add_column("Value", style="yellow")
163
+
164
+ table.add_row("Duplicate Groups", str(stats.duplicate_groups))
165
+ table.add_row("Total Duplicates", str(stats.total_duplicates))
166
+ table.add_row("Space Wasted", stats.formatted_space_wasted)
167
+ table.add_row("Space Recoverable", stats.formatted_recoverable)
168
+
169
+ console.print(table)
170
+
171
+ # Show duplicates by type
172
+ if stats.by_type:
173
+ console.print("\n[bold]Duplicates by File Type[/bold]")
174
+ type_table = Table(show_header=True)
175
+ type_table.add_column("Type", style="cyan")
176
+ type_table.add_column("Count", style="yellow", justify="right")
177
+
178
+ for file_type, count in sorted(stats.by_type.items(), key=lambda x: x[1], reverse=True)[
179
+ :10
180
+ ]:
181
+ type_table.add_row(file_type, str(count))
182
+
183
+ console.print(type_table)
184
+
185
+
186
+ def display_time_savings(savings: TimeSavings) -> None:
187
+ """Display time savings information."""
188
+ console.print("\n[bold cyan]TIME SAVINGS[/bold cyan]")
189
+ console.print("=" * 70)
190
+
191
+ # Highlight the time saved
192
+ console.print(
193
+ f"\n[bold green]⏱ Estimated Time Saved: {savings.formatted_time_saved}[/bold green]"
194
+ )
195
+
196
+ table = Table(show_header=False, box=None, padding=(0, 2))
197
+ table.add_column("Metric", style="bold")
198
+ table.add_column("Value", style="cyan")
199
+
200
+ table.add_row("Total Operations", str(savings.total_operations))
201
+ table.add_row("Automated Operations", str(savings.automated_operations))
202
+ table.add_row("Automation Rate", f"{savings.automation_percentage:.1f}%")
203
+ table.add_row("Manual Time", _format_duration(savings.manual_time_seconds))
204
+ table.add_row("Automated Time", _format_duration(savings.automated_time_seconds))
205
+
206
+ console.print(table)
207
+
208
+
209
+ def display_file_distribution(
210
+ distribution: FileDistribution, chart_gen: ChartGenerator | None
211
+ ) -> None:
212
+ """Display file distribution charts."""
213
+ console.print("\n[bold cyan]FILE DISTRIBUTION[/bold cyan]")
214
+ console.print("=" * 70)
215
+
216
+ console.print(f"\n[bold]Total Files:[/bold] {distribution.total_files}")
217
+
218
+ # By type
219
+ if distribution.by_type and chart_gen:
220
+ # Show top 10 types
221
+ top_types = dict(
222
+ sorted(distribution.by_type.items(), key=lambda x: x[1], reverse=True)[:10]
223
+ )
224
+ chart = chart_gen.create_bar_chart(top_types, "Top File Types", width=50)
225
+ console.print(f"\n{chart}")
226
+
227
+ # By size range
228
+ if distribution.by_size_range:
229
+ console.print("\n[bold]Files by Size Range[/bold]")
230
+ size_table = Table(show_header=True)
231
+ size_table.add_column("Range", style="cyan")
232
+ size_table.add_column("Count", style="yellow", justify="right")
233
+ size_table.add_column("Percentage", style="green", justify="right")
234
+
235
+ total = distribution.total_files
236
+ for range_name, count in distribution.by_size_range.items():
237
+ percentage = (count / total) * 100 if total > 0 else 0
238
+ size_table.add_row(range_name.title(), str(count), f"{percentage:.1f}%")
239
+
240
+ console.print(size_table)
241
+
242
+
243
+ def analytics_command(args: list[str] | None = None) -> int:
244
+ """Execute the analytics command.
245
+
246
+ Args:
247
+ args: Command-line arguments (None to use sys.argv)
248
+
249
+ Returns:
250
+ Exit code (0 for success, non-zero for error)
251
+ """
252
+ parser = argparse.ArgumentParser(
253
+ description="Display comprehensive analytics dashboard for file organization",
254
+ formatter_class=argparse.RawDescriptionHelpFormatter,
255
+ epilog="""
256
+ Examples:
257
+ # Show analytics for current directory
258
+ fo analytics .
259
+
260
+ # Show analytics with depth limit
261
+ fo analytics ~/Documents --max-depth 3
262
+
263
+ # Export analytics to JSON
264
+ fo analytics ~/Downloads --export report.json
265
+
266
+ # Export as text report
267
+ fo analytics ~/Pictures --export report.txt --format text
268
+ """,
269
+ )
270
+
271
+ parser.add_argument("directory", type=str, help="Directory to analyze")
272
+
273
+ parser.add_argument(
274
+ "--max-depth",
275
+ type=int,
276
+ default=None,
277
+ help="Maximum directory depth to analyze (default: unlimited)",
278
+ )
279
+
280
+ parser.add_argument(
281
+ "--export",
282
+ type=str,
283
+ default=None,
284
+ help="Export analytics to file",
285
+ )
286
+
287
+ parser.add_argument(
288
+ "--format",
289
+ type=str,
290
+ choices=["json", "text"],
291
+ default="json",
292
+ help="Export format (default: json)",
293
+ )
294
+
295
+ parser.add_argument(
296
+ "--no-charts",
297
+ action="store_true",
298
+ help="Disable chart visualizations",
299
+ )
300
+
301
+ parser.add_argument("--verbose", action="store_true", help="Enable verbose logging")
302
+
303
+ parsed_args = parser.parse_args(args)
304
+
305
+ # Configure logging
306
+ if parsed_args.verbose:
307
+ logger.remove()
308
+ logger.add(sys.stderr, level="DEBUG")
309
+ else:
310
+ logger.remove()
311
+ logger.add(sys.stderr, level="WARNING")
312
+
313
+ # Validate directory
314
+ directory = Path(parsed_args.directory).resolve()
315
+ if not directory.exists():
316
+ console.print(f"[red]Error: Directory not found: {directory}[/red]")
317
+ return 1
318
+
319
+ if not directory.is_dir():
320
+ console.print(f"[red]Error: Not a directory: {directory}[/red]")
321
+ return 1
322
+
323
+ try:
324
+ # Display banner
325
+ console.print()
326
+ console.print("=" * 70, style="bold blue")
327
+ console.print("File Organizer Analytics Dashboard", style="bold blue", justify="center")
328
+ console.print("=" * 70, style="bold blue")
329
+ console.print()
330
+
331
+ console.print(f"[bold]Analyzing:[/bold] {directory}")
332
+ if parsed_args.max_depth:
333
+ console.print(f"[bold]Max Depth:[/bold] {parsed_args.max_depth}")
334
+ console.print()
335
+
336
+ # Initialize services
337
+ console.print("[dim]Initializing analytics service...[/dim]")
338
+ analytics_service = AnalyticsService()
339
+
340
+ # Determine chart generation based on --no-charts flag
341
+ generate_charts = not parsed_args.no_charts
342
+ chart_gen = ChartGenerator(use_unicode=True) if generate_charts else None
343
+
344
+ # Generate dashboard
345
+ console.print("[dim]Analyzing directory...[/dim]")
346
+ dashboard = analytics_service.generate_dashboard(
347
+ directory=directory,
348
+ max_depth=parsed_args.max_depth,
349
+ )
350
+
351
+ console.print("[green]✓ Analysis complete[/green]\n")
352
+
353
+ # Display dashboard sections
354
+ display_storage_stats(dashboard.storage_stats, chart_gen)
355
+ display_quality_metrics(dashboard.quality_metrics)
356
+ display_duplicate_stats(dashboard.duplicate_stats)
357
+ display_time_savings(dashboard.time_savings)
358
+ display_file_distribution(dashboard.file_distribution, chart_gen)
359
+
360
+ # Export if requested
361
+ if parsed_args.export:
362
+ export_path = Path(parsed_args.export)
363
+ console.print(f"\n[dim]Exporting to {export_path}...[/dim]")
364
+ analytics_service.export_dashboard(dashboard, export_path, format=parsed_args.format)
365
+ console.print(f"[green]✓ Exported to {export_path}[/green]")
366
+
367
+ # Footer
368
+ console.print("\n" + "=" * 70)
369
+ console.print(
370
+ f"[dim]Generated: {dashboard.generated_at.strftime('%Y-%m-%d %H:%M:%S')} UTC[/dim]"
371
+ )
372
+ console.print("=" * 70 + "\n")
373
+
374
+ return 0
375
+
376
+ except KeyboardInterrupt:
377
+ console.print("\n\n[yellow]Operation cancelled by user[/yellow]")
378
+ return 130
379
+ except Exception as e:
380
+ console.print(f"\n[red]Error: {e}[/red]")
381
+ logger.exception("Analytics generation failed")
382
+ return 1
383
+
384
+
385
+ def main() -> None:
386
+ """Main entry point for standalone execution."""
387
+ sys.exit(analytics_command())
388
+
389
+
390
+ if __name__ == "__main__":
391
+ main()