pdf-file-renamer 0.4.2__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.
- pdf_file_renamer-0.4.2.dist-info/METADATA +245 -0
- pdf_file_renamer-0.4.2.dist-info/RECORD +26 -0
- pdf_file_renamer-0.4.2.dist-info/WHEEL +5 -0
- pdf_file_renamer-0.4.2.dist-info/entry_points.txt +2 -0
- pdf_file_renamer-0.4.2.dist-info/licenses/LICENSE +21 -0
- pdf_file_renamer-0.4.2.dist-info/top_level.txt +1 -0
- pdf_renamer/__init__.py +3 -0
- pdf_renamer/application/__init__.py +7 -0
- pdf_renamer/application/filename_service.py +70 -0
- pdf_renamer/application/pdf_rename_workflow.py +144 -0
- pdf_renamer/application/rename_service.py +79 -0
- pdf_renamer/domain/__init__.py +25 -0
- pdf_renamer/domain/models.py +80 -0
- pdf_renamer/domain/ports.py +106 -0
- pdf_renamer/infrastructure/__init__.py +5 -0
- pdf_renamer/infrastructure/config.py +94 -0
- pdf_renamer/infrastructure/llm/__init__.py +5 -0
- pdf_renamer/infrastructure/llm/pydantic_ai_provider.py +234 -0
- pdf_renamer/infrastructure/pdf/__init__.py +7 -0
- pdf_renamer/infrastructure/pdf/composite.py +57 -0
- pdf_renamer/infrastructure/pdf/docling_extractor.py +116 -0
- pdf_renamer/infrastructure/pdf/pymupdf_extractor.py +165 -0
- pdf_renamer/main.py +6 -0
- pdf_renamer/presentation/__init__.py +6 -0
- pdf_renamer/presentation/cli.py +233 -0
- pdf_renamer/presentation/formatters.py +216 -0
@@ -0,0 +1,216 @@
|
|
1
|
+
"""Display formatters and UI components."""
|
2
|
+
|
3
|
+
from rich.console import Console
|
4
|
+
from rich.layout import Layout
|
5
|
+
from rich.panel import Panel
|
6
|
+
from rich.prompt import Prompt
|
7
|
+
from rich.table import Table
|
8
|
+
from rich.text import Text
|
9
|
+
|
10
|
+
from pdf_renamer.domain.models import FileRenameOperation
|
11
|
+
|
12
|
+
|
13
|
+
class ProgressDisplay:
|
14
|
+
"""Handles progress display for batch processing."""
|
15
|
+
|
16
|
+
def __init__(self, console: Console, total_files: int) -> None:
|
17
|
+
"""
|
18
|
+
Initialize the progress display.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
console: Rich console for output
|
22
|
+
total_files: Total number of files to process
|
23
|
+
"""
|
24
|
+
self.console = console
|
25
|
+
self.total_files = total_files
|
26
|
+
self.status_tracker: dict[str, dict[str, str]] = {}
|
27
|
+
|
28
|
+
def update_status(self, filename: str, status: dict[str, str]) -> None:
|
29
|
+
"""Update status for a file."""
|
30
|
+
self.status_tracker[filename] = status
|
31
|
+
|
32
|
+
def create_display(self) -> Layout:
|
33
|
+
"""Create the live display layout."""
|
34
|
+
# Create table
|
35
|
+
table = Table(
|
36
|
+
title="Processing Status",
|
37
|
+
expand=True,
|
38
|
+
show_header=True,
|
39
|
+
header_style="bold magenta",
|
40
|
+
)
|
41
|
+
table.add_column("File", style="cyan", no_wrap=False, width=40)
|
42
|
+
table.add_column("Stage", justify="center", width=8)
|
43
|
+
table.add_column("Status", style="yellow", width=12)
|
44
|
+
table.add_column("Details", style="dim", no_wrap=False)
|
45
|
+
|
46
|
+
# Count statuses
|
47
|
+
completed = sum(1 for s in self.status_tracker.values() if s.get("status") == "Complete")
|
48
|
+
extracting = sum(1 for s in self.status_tracker.values() if s.get("status") == "Extracting")
|
49
|
+
analyzing = sum(1 for s in self.status_tracker.values() if s.get("status") == "Analyzing")
|
50
|
+
errors = sum(1 for s in self.status_tracker.values() if s.get("status") == "Error")
|
51
|
+
pending = self.total_files - completed - extracting - analyzing - errors
|
52
|
+
|
53
|
+
# Separate files by status
|
54
|
+
active_files = []
|
55
|
+
completed_files = []
|
56
|
+
|
57
|
+
for filename, info in self.status_tracker.items():
|
58
|
+
if info.get("status") in ["Extracting", "Analyzing"]:
|
59
|
+
active_files.append((filename, info))
|
60
|
+
elif info.get("status") in ["Complete", "Error"]:
|
61
|
+
completed_files.append((filename, info))
|
62
|
+
|
63
|
+
# Show active files
|
64
|
+
for filename, info in active_files:
|
65
|
+
display_name = filename if len(filename) <= 40 else filename[:37] + "..."
|
66
|
+
table.add_row(
|
67
|
+
display_name,
|
68
|
+
info.get("stage", ""),
|
69
|
+
info.get("status", ""),
|
70
|
+
info.get("confidence", ""),
|
71
|
+
)
|
72
|
+
|
73
|
+
# Show last 5 completed files
|
74
|
+
for filename, info in completed_files[-5:]:
|
75
|
+
display_name = filename if len(filename) <= 40 else filename[:37] + "..."
|
76
|
+
status = info.get("status", "")
|
77
|
+
style = "green" if status == "Complete" else "red"
|
78
|
+
details = info.get("confidence", "") or info.get("error", "")[:50]
|
79
|
+
table.add_row(
|
80
|
+
f"[{style}]{display_name}[/{style}]",
|
81
|
+
info.get("stage", ""),
|
82
|
+
status,
|
83
|
+
details,
|
84
|
+
)
|
85
|
+
|
86
|
+
# Stats panel
|
87
|
+
stats = Text()
|
88
|
+
stats.append("Total: ", style="bold")
|
89
|
+
stats.append(f"{self.total_files}", style="white")
|
90
|
+
stats.append(" | ", style="dim")
|
91
|
+
stats.append("Pending: ", style="bold")
|
92
|
+
stats.append(f"{pending}", style="cyan")
|
93
|
+
stats.append(" | ", style="dim")
|
94
|
+
stats.append("Extracting: ", style="bold")
|
95
|
+
stats.append(f"{extracting}", style="blue")
|
96
|
+
stats.append(" | ", style="dim")
|
97
|
+
stats.append("Analyzing: ", style="bold")
|
98
|
+
stats.append(f"{analyzing}", style="yellow")
|
99
|
+
stats.append(" | ", style="dim")
|
100
|
+
stats.append("Complete: ", style="bold green")
|
101
|
+
stats.append(f"{completed}", style="green")
|
102
|
+
|
103
|
+
if errors > 0:
|
104
|
+
stats.append(" | ", style="dim")
|
105
|
+
stats.append("Errors: ", style="bold red")
|
106
|
+
stats.append(f"{errors}", style="red")
|
107
|
+
|
108
|
+
# Progress bar
|
109
|
+
progress_pct = (completed / self.total_files * 100) if self.total_files > 0 else 0
|
110
|
+
filled = int(progress_pct / 2)
|
111
|
+
progress_bar = f"[{'█' * filled}{' ' * (50 - filled)}] {progress_pct:.1f}%"
|
112
|
+
|
113
|
+
# Layout
|
114
|
+
layout = Layout()
|
115
|
+
layout.split_column(
|
116
|
+
Layout(Panel(stats, title="📊 Progress", border_style="blue"), size=3),
|
117
|
+
Layout(Panel(Text(progress_bar, style="green bold"), border_style="green"), size=3),
|
118
|
+
Layout(table),
|
119
|
+
)
|
120
|
+
|
121
|
+
return layout
|
122
|
+
|
123
|
+
|
124
|
+
class InteractivePrompt:
|
125
|
+
"""Handles interactive prompts for rename confirmation."""
|
126
|
+
|
127
|
+
def __init__(self, console: Console) -> None:
|
128
|
+
"""Initialize the interactive prompt."""
|
129
|
+
self.console = console
|
130
|
+
|
131
|
+
async def prompt_for_action(self, operation: FileRenameOperation) -> tuple[str, bool]:
|
132
|
+
"""
|
133
|
+
Prompt user for action on a rename operation.
|
134
|
+
|
135
|
+
Args:
|
136
|
+
operation: The rename operation
|
137
|
+
|
138
|
+
Returns:
|
139
|
+
Tuple of (final_filename, should_rename)
|
140
|
+
"""
|
141
|
+
while True:
|
142
|
+
# Display info panel
|
143
|
+
info_text = Text()
|
144
|
+
info_text.append("Original: ", style="bold cyan")
|
145
|
+
info_text.append(f"{operation.original_path.name}\n", style="cyan")
|
146
|
+
info_text.append("Suggested: ", style="bold green")
|
147
|
+
info_text.append(f"{operation.new_filename}\n", style="green")
|
148
|
+
info_text.append("Confidence: ", style="bold yellow")
|
149
|
+
info_text.append(f"{operation.confidence.value}\n", style="yellow")
|
150
|
+
info_text.append("Reasoning: ", style="bold white")
|
151
|
+
info_text.append(operation.reasoning, style="dim white")
|
152
|
+
|
153
|
+
panel = Panel(
|
154
|
+
info_text,
|
155
|
+
title="[bold magenta]Rename Suggestion[/bold magenta]",
|
156
|
+
border_style="magenta",
|
157
|
+
padding=(1, 2),
|
158
|
+
)
|
159
|
+
|
160
|
+
self.console.print("\n")
|
161
|
+
self.console.print(panel)
|
162
|
+
|
163
|
+
# Show options
|
164
|
+
self.console.print("\n[bold]Actions:[/bold]")
|
165
|
+
self.console.print(
|
166
|
+
" [green on default][[/green on default][green bold on default] Y [/green bold on default][green on default]][/green on default] Accept "
|
167
|
+
"[yellow on default][[/yellow on default][yellow bold on default] E [/yellow bold on default][yellow on default]][/yellow on default] Edit "
|
168
|
+
"[red on default][[/red on default][red bold on default] N [/red bold on default][red on default]][/red on default] Skip"
|
169
|
+
)
|
170
|
+
|
171
|
+
choice = Prompt.ask("\nChoice", default="y", show_default=False).lower().strip()
|
172
|
+
|
173
|
+
if choice in ["y", "yes", ""]:
|
174
|
+
return (operation.suggested_filename, True)
|
175
|
+
elif choice in ["e", "edit"]:
|
176
|
+
manual_name = Prompt.ask(
|
177
|
+
"[yellow]Enter filename (without .pdf)[/yellow]",
|
178
|
+
default=operation.suggested_filename,
|
179
|
+
).strip()
|
180
|
+
if manual_name:
|
181
|
+
return (manual_name, True)
|
182
|
+
else:
|
183
|
+
self.console.print("[red]Empty filename, try again[/red]")
|
184
|
+
continue
|
185
|
+
elif choice in ["n", "no", "skip"]:
|
186
|
+
self.console.print("[yellow]⊘ Skipped[/yellow]")
|
187
|
+
return ("", False)
|
188
|
+
else:
|
189
|
+
self.console.print(f"[red]Invalid: {choice}[/red]")
|
190
|
+
continue
|
191
|
+
|
192
|
+
|
193
|
+
class ResultsTable:
|
194
|
+
"""Formats results as a table."""
|
195
|
+
|
196
|
+
@staticmethod
|
197
|
+
def create(operations: list[FileRenameOperation], console: Console) -> None:
|
198
|
+
"""Create and print a results table."""
|
199
|
+
table = Table(title="Rename Suggestions")
|
200
|
+
table.add_column("Original", style="cyan", no_wrap=False)
|
201
|
+
table.add_column("Suggested", style="green", no_wrap=False)
|
202
|
+
table.add_column("Confidence", style="yellow")
|
203
|
+
table.add_column("Reasoning", style="white", no_wrap=False)
|
204
|
+
|
205
|
+
for op in operations:
|
206
|
+
reasoning = op.reasoning
|
207
|
+
if len(reasoning) > 100:
|
208
|
+
reasoning = reasoning[:100] + "..."
|
209
|
+
table.add_row(
|
210
|
+
op.original_path.name,
|
211
|
+
op.new_filename,
|
212
|
+
op.confidence.value,
|
213
|
+
reasoning,
|
214
|
+
)
|
215
|
+
|
216
|
+
console.print(table)
|