kopipasta 0.28.0__py3-none-any.whl → 0.30.0__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.
@@ -0,0 +1,559 @@
1
+ import os
2
+ import shutil
3
+ from typing import Dict, List, Optional, Tuple
4
+ from rich.console import Console
5
+ from rich.tree import Tree
6
+ from rich.panel import Panel
7
+ from rich.text import Text
8
+ import click
9
+
10
+ from kopipasta.file import FileTuple, is_binary, is_ignored, get_human_readable_size
11
+ from kopipasta.prompt import get_file_snippet, get_language_for_file
12
+
13
+
14
+ class FileNode:
15
+ """Represents a file or directory in the tree"""
16
+ def __init__(self, path: str, is_dir: bool, parent: Optional['FileNode'] = None):
17
+ self.path = os.path.abspath(path) # Always store absolute paths
18
+ self.is_dir = is_dir
19
+ self.parent = parent
20
+ self.children: List['FileNode'] = []
21
+ self.expanded = False
22
+ self.selected = False
23
+ self.selected_as_snippet = False
24
+ self.size = 0 if is_dir else os.path.getsize(self.path)
25
+ self.is_root = path == "." # Mark if this is the root node
26
+
27
+ @property
28
+ def name(self):
29
+ if self.is_root:
30
+ return "." # Show root as "." instead of directory name
31
+ return os.path.basename(self.path) or self.path
32
+
33
+ @property
34
+ def relative_path(self):
35
+ if self.is_root:
36
+ return "."
37
+ return os.path.relpath(self.path)
38
+
39
+
40
+ class TreeSelector:
41
+ """Interactive file tree selector using Rich"""
42
+
43
+ def __init__(self, ignore_patterns: List[str], project_root_abs: str):
44
+ self.console = Console()
45
+ self.ignore_patterns = ignore_patterns
46
+ self.project_root_abs = project_root_abs
47
+ self.selected_files: Dict[str, Tuple[bool, Optional[List[str]]]] = {} # path -> (is_snippet, chunks)
48
+ self.current_index = 0
49
+ self.nodes: List[FileNode] = []
50
+ self.visible_nodes: List[FileNode] = []
51
+ self.char_count = 0
52
+ self.quit_selection = False
53
+ self.viewport_offset = 0 # First visible item index
54
+
55
+ def build_tree(self, paths: List[str]) -> FileNode:
56
+ """Build tree structure from given paths"""
57
+ # Use current directory as root
58
+ root = FileNode(".", True)
59
+ root.expanded = True # Always expand root
60
+
61
+ # Process each input path
62
+ for path in paths:
63
+ abs_path = os.path.abspath(path)
64
+
65
+ if os.path.isfile(abs_path):
66
+ # Single file - add to root
67
+ if not is_ignored(abs_path, self.ignore_patterns) and not is_binary(abs_path):
68
+ node = FileNode(abs_path, False, root)
69
+ root.children.append(node)
70
+ elif os.path.isdir(abs_path):
71
+ # If the directory is the current directory, scan its contents directly
72
+ if abs_path == os.path.abspath("."):
73
+ self._scan_directory(abs_path, root)
74
+ else:
75
+ # Otherwise add the directory as a child
76
+ dir_node = FileNode(abs_path, True, root)
77
+ root.children.append(dir_node)
78
+ # Auto-expand if it's the only child
79
+ if len(paths) == 1:
80
+ dir_node.expanded = True
81
+ self._scan_directory(abs_path, dir_node)
82
+
83
+ return root
84
+
85
+ def _scan_directory(self, dir_path: str, parent_node: FileNode):
86
+ """Recursively scan directory and build tree"""
87
+ abs_dir_path = os.path.abspath(dir_path)
88
+
89
+ # Check if we've already scanned this directory
90
+ if parent_node.children:
91
+ return
92
+
93
+ try:
94
+ items = sorted(os.listdir(abs_dir_path))
95
+ except PermissionError:
96
+ return
97
+
98
+ # Separate and sort directories and files
99
+ dirs = []
100
+ files = []
101
+
102
+ for item in items:
103
+ item_path = os.path.join(abs_dir_path, item)
104
+ if is_ignored(item_path, self.ignore_patterns):
105
+ continue
106
+
107
+ if os.path.isdir(item_path):
108
+ dirs.append(item)
109
+ elif os.path.isfile(item_path) and not is_binary(item_path):
110
+ files.append(item)
111
+
112
+ # Add directories first
113
+ for dir_name in sorted(dirs):
114
+ dir_path_full = os.path.join(abs_dir_path, dir_name)
115
+ # Check if this node already exists as a child
116
+ existing = next((child for child in parent_node.children
117
+ if os.path.abspath(child.path) == os.path.abspath(dir_path_full)), None)
118
+ if not existing:
119
+ dir_node = FileNode(dir_path_full, True, parent_node)
120
+ parent_node.children.append(dir_node)
121
+
122
+ # Then add files
123
+ for file_name in sorted(files):
124
+ file_path = os.path.join(abs_dir_path, file_name)
125
+ # Check if this node already exists as a child
126
+ existing = next((child for child in parent_node.children
127
+ if os.path.abspath(child.path) == os.path.abspath(file_path)), None)
128
+ if not existing:
129
+ file_node = FileNode(file_path, False, parent_node)
130
+ parent_node.children.append(file_node)
131
+
132
+ def _flatten_tree(self, node: FileNode, level: int = 0) -> List[Tuple[FileNode, int]]:
133
+ """Flatten tree into a list of (node, level) tuples for display"""
134
+ result = []
135
+
136
+ # Special handling for root - show its children at top level
137
+ if node.is_root:
138
+ # Don't include the root node itself in the display
139
+ for child in node.children:
140
+ result.extend(self._flatten_tree(child, 0)) # Start children at level 0
141
+ else:
142
+ # Include this node
143
+ result.append((node, level))
144
+
145
+ if node.is_dir and node.expanded:
146
+ # Load children on demand if not loaded
147
+ if not node.children:
148
+ self._scan_directory(node.path, node)
149
+
150
+ for child in node.children:
151
+ result.extend(self._flatten_tree(child, level + 1))
152
+
153
+ return result
154
+
155
+ def _build_display_tree(self) -> Tree:
156
+ """Build Rich tree for display with viewport"""
157
+ # Get terminal size
158
+ term_width, term_height = shutil.get_terminal_size()
159
+
160
+ # Reserve space for header, help panel, and status
161
+ available_height = term_height - 15 # Adjust based on your UI
162
+ available_height = max(5, available_height) # Minimum height
163
+
164
+ # Flatten tree to get all visible nodes
165
+ flat_tree = self._flatten_tree(self.root)
166
+ self.visible_nodes = [node for node, _ in flat_tree]
167
+
168
+ # Calculate viewport
169
+ if self.visible_nodes:
170
+ # Ensure current selection is visible
171
+ if self.current_index < self.viewport_offset:
172
+ self.viewport_offset = self.current_index
173
+ elif self.current_index >= self.viewport_offset + available_height:
174
+ self.viewport_offset = self.current_index - available_height + 1
175
+
176
+ # Clamp viewport to valid range
177
+ max_offset = max(0, len(self.visible_nodes) - available_height)
178
+ self.viewport_offset = max(0, min(self.viewport_offset, max_offset))
179
+ else:
180
+ self.viewport_offset = 0
181
+
182
+ # Create tree with scroll indicators
183
+ tree_title = "📁 Project Files"
184
+ if self.viewport_offset > 0:
185
+ tree_title += f" ↑ ({self.viewport_offset} more)"
186
+
187
+ tree = Tree(tree_title, guide_style="dim")
188
+
189
+ # Build tree structure - only for visible portion
190
+ node_map = {}
191
+ viewport_end = min(len(flat_tree), self.viewport_offset + available_height)
192
+
193
+ # Track what level each visible item is at for proper tree structure
194
+ level_stacks = {} # level -> stack of tree nodes
195
+
196
+ for i in range(self.viewport_offset, viewport_end):
197
+ node, level = flat_tree[i]
198
+
199
+ # Determine style and icon
200
+ is_current = i == self.current_index
201
+ style = "bold cyan" if is_current else ""
202
+
203
+ if node.is_dir:
204
+ icon = "📂" if node.expanded else "📁"
205
+ size_str = f" ({len(node.children)} items)" if node.children else ""
206
+ else:
207
+ icon = "📄"
208
+ size_str = f" ({get_human_readable_size(node.size)})"
209
+
210
+ # Selection indicator
211
+ abs_path = os.path.abspath(node.path)
212
+ if abs_path in self.selected_files:
213
+ is_snippet = self.selected_files[abs_path][0]
214
+ if is_snippet:
215
+ selection = "◐" # Half-selected (snippet)
216
+ else:
217
+ selection = "●" # Fully selected
218
+ style = "green " + style
219
+ else:
220
+ selection = "○"
221
+
222
+ # Build label
223
+ label = Text()
224
+ label.append(f"{selection} ", style="dim")
225
+ label.append(f"{icon} {node.name}{size_str}", style=style)
226
+
227
+ # Add to tree at correct level
228
+ if level == 0:
229
+ tree_node = tree.add(label)
230
+ level_stacks[0] = tree_node
231
+ else:
232
+ # Find parent at previous level
233
+ parent_level = level - 1
234
+ if parent_level in level_stacks:
235
+ parent_tree = level_stacks[parent_level]
236
+ tree_node = parent_tree.add(label)
237
+ level_stacks[level] = tree_node
238
+ else:
239
+ # Fallback - add to root with indentation indicator
240
+ indent_label = Text()
241
+ indent_label.append(" " * level + f"{selection} ", style="dim")
242
+ indent_label.append(f"{icon} {node.name}{size_str}", style=style)
243
+ tree_node = tree.add(indent_label)
244
+ level_stacks[level] = tree_node
245
+
246
+ # Add scroll indicator at bottom if needed
247
+ if viewport_end < len(self.visible_nodes):
248
+ remaining = len(self.visible_nodes) - viewport_end
249
+ tree.add(Text(f"↓ ({remaining} more items)", style="dim italic"))
250
+
251
+ return tree
252
+
253
+ def _show_help(self) -> Panel:
254
+ """Create help panel"""
255
+ help_text = """[bold]Navigation:[/bold]
256
+ ↑/k: Up ↓/j: Down →/l/Enter: Expand ←/h: Collapse PgUp/PgDn: Page G/End: Bottom
257
+
258
+ [bold]Selection:[/bold]
259
+ Space: Toggle file/dir a: Add all in dir s: Snippet mode
260
+
261
+ [bold]Actions:[/bold]
262
+ g: Grep in directory d: Show dependencies q: Quit selection"""
263
+
264
+ return Panel(help_text, title="Keys", border_style="dim", expand=False)
265
+
266
+ def _get_status_bar(self) -> str:
267
+ """Create status bar with selection info"""
268
+ # Count selections
269
+ full_count = sum(1 for _, (is_snippet, _) in self.selected_files.items() if not is_snippet)
270
+ snippet_count = sum(1 for _, (is_snippet, _) in self.selected_files.items() if is_snippet)
271
+
272
+ # Current item info
273
+ if self.visible_nodes and 0 <= self.current_index < len(self.visible_nodes):
274
+ current = self.visible_nodes[self.current_index]
275
+ current_info = f"[dim]Current:[/dim] {current.relative_path}"
276
+ else:
277
+ current_info = "No selection"
278
+
279
+ selection_info = f"[dim]Selected:[/dim] {full_count} full, {snippet_count} snippets | ~{self.char_count:,} chars (~{self.char_count//4:,} tokens)"
280
+
281
+ return f"\n{current_info} | {selection_info}\n"
282
+
283
+ def _handle_grep(self, node: FileNode):
284
+ """Handle grep search in directory"""
285
+ if not node.is_dir:
286
+ self.console.print("[red]Grep only works on directories[/red]")
287
+ return
288
+
289
+ pattern = click.prompt("Enter search pattern")
290
+ if not pattern:
291
+ return
292
+
293
+ self.console.print(f"Searching for '{pattern}' in {node.relative_path}...")
294
+
295
+ # Import here to avoid circular dependency
296
+ from kopipasta.main import grep_files_in_directory, select_from_grep_results
297
+
298
+ grep_results = grep_files_in_directory(pattern, node.path, self.ignore_patterns)
299
+ if not grep_results:
300
+ self.console.print(f"[yellow]No matches found for '{pattern}'[/yellow]")
301
+ return
302
+
303
+ # Show results and let user select
304
+ selected_files, new_char_count = select_from_grep_results(grep_results, self.char_count)
305
+
306
+ # Add selected files
307
+ added_count = 0
308
+ for file_tuple in selected_files:
309
+ file_path, is_snippet, chunks, _ = file_tuple
310
+ abs_path = os.path.abspath(file_path)
311
+
312
+ # Check if already selected
313
+ if abs_path not in self.selected_files:
314
+ self.selected_files[abs_path] = (is_snippet, chunks)
315
+ added_count += 1
316
+ # Ensure the file is visible in the tree
317
+ self._ensure_path_visible(abs_path)
318
+
319
+ self.char_count = new_char_count
320
+
321
+ # Show summary of what was added
322
+ if added_count > 0:
323
+ self.console.print(f"\n[green]Added {added_count} files from grep results[/green]")
324
+ else:
325
+ self.console.print(f"\n[yellow]All selected files were already in selection[/yellow]")
326
+
327
+ def _toggle_selection(self, node: FileNode, snippet_mode: bool = False):
328
+ """Toggle selection of a file or directory"""
329
+ if node.is_dir:
330
+ # For directories, toggle all children
331
+ self._toggle_directory(node)
332
+ else:
333
+ abs_path = os.path.abspath(node.path)
334
+ # For files, toggle individual selection
335
+ if abs_path in self.selected_files:
336
+ # Unselect
337
+ is_snippet, _ = self.selected_files[abs_path]
338
+ del self.selected_files[abs_path]
339
+ if is_snippet:
340
+ self.char_count -= len(get_file_snippet(node.path))
341
+ else:
342
+ self.char_count -= node.size
343
+ else:
344
+ # Select
345
+ if snippet_mode or (node.size > 102400 and not self._confirm_large_file(node)):
346
+ # Use snippet
347
+ self.selected_files[abs_path] = (True, None)
348
+ self.char_count += len(get_file_snippet(node.path))
349
+ else:
350
+ # Use full file
351
+ self.selected_files[abs_path] = (False, None)
352
+ self.char_count += node.size
353
+
354
+ def _toggle_directory(self, node: FileNode):
355
+ """Toggle all files in a directory"""
356
+ if not node.is_dir:
357
+ return
358
+
359
+ # Ensure children are loaded
360
+ if not node.children:
361
+ self._scan_directory(node.path, node)
362
+
363
+ # Collect all files recursively
364
+ all_files = []
365
+
366
+ def collect_files(n: FileNode):
367
+ if n.is_dir:
368
+ for child in n.children:
369
+ collect_files(child)
370
+ else:
371
+ all_files.append(n)
372
+
373
+ collect_files(node)
374
+
375
+ # Check if any are unselected
376
+ any_unselected = any(os.path.abspath(f.path) not in self.selected_files for f in all_files)
377
+
378
+ if any_unselected:
379
+ # Select all unselected
380
+ for file_node in all_files:
381
+ if file_node.path not in self.selected_files:
382
+ self._toggle_selection(file_node)
383
+ else:
384
+ # Unselect all
385
+ for file_node in all_files:
386
+ if file_node.path in self.selected_files:
387
+ self._toggle_selection(file_node)
388
+
389
+ def _ensure_path_visible(self, file_path: str):
390
+ """Ensure a file path is visible in the tree by expanding parent directories"""
391
+ abs_file_path = os.path.abspath(file_path)
392
+
393
+ # Build the path from root to the file
394
+ path_components = []
395
+ current = abs_file_path
396
+
397
+ while current != os.path.abspath(self.project_root_abs) and current != '/':
398
+ path_components.append(current)
399
+ parent = os.path.dirname(current)
400
+ if parent == current: # Reached root
401
+ break
402
+ current = parent
403
+
404
+ # Reverse to go from root to file
405
+ path_components.reverse()
406
+
407
+ # Find and expand each directory in the path
408
+ for component_path in path_components[:-1]: # All except the file itself
409
+ # Search through all nodes to find this path
410
+ found = False
411
+ for node in self._get_all_nodes(self.root):
412
+ if os.path.abspath(node.path) == component_path and node.is_dir:
413
+ if not node.expanded:
414
+ node.expanded = True
415
+ # Ensure children are loaded
416
+ if not node.children:
417
+ self._scan_directory(node.path, node)
418
+ found = True
419
+ break
420
+
421
+ if not found:
422
+ # This shouldn't happen if the tree is properly built
423
+ self.console.print(f"[yellow]Warning: Could not find directory {component_path} in tree[/yellow]")
424
+
425
+ def _get_all_nodes(self, node: FileNode) -> List[FileNode]:
426
+ """Get all nodes in the tree recursively"""
427
+ nodes = [node]
428
+ for child in node.children:
429
+ nodes.extend(self._get_all_nodes(child))
430
+ return nodes
431
+
432
+ def _confirm_large_file(self, node: FileNode) -> bool:
433
+ """Ask user about large file handling"""
434
+ size_str = get_human_readable_size(node.size)
435
+ return click.confirm(f"{node.name} is large ({size_str}). Include full content?", default=False)
436
+
437
+ def _show_dependencies(self, node: FileNode):
438
+ """Show and optionally add dependencies for a file"""
439
+ if node.is_dir:
440
+ return
441
+
442
+ self.console.print(f"\nAnalyzing dependencies for {node.relative_path}...")
443
+
444
+ # Import here to avoid circular dependency
445
+ from kopipasta.main import _propose_and_add_dependencies
446
+
447
+ # Create a temporary files list for the dependency analyzer
448
+ files_list = [(path, is_snippet, chunks, get_language_for_file(path))
449
+ for path, (is_snippet, chunks) in self.selected_files.items()]
450
+
451
+ new_deps, deps_char_count = _propose_and_add_dependencies(
452
+ node.path, self.project_root_abs, files_list, self.char_count
453
+ )
454
+
455
+ # Add new dependencies to our selection
456
+ for dep_path, is_snippet, chunks, _ in new_deps:
457
+ self.selected_files[dep_path] = (is_snippet, chunks)
458
+
459
+ self.char_count += deps_char_count
460
+
461
+ def run(self, initial_paths: List[str]) -> Tuple[List[FileTuple], int]:
462
+ """Run the interactive tree selector"""
463
+ self.root = self.build_tree(initial_paths)
464
+
465
+ # Don't use Live mode, instead manually control the display
466
+ while not self.quit_selection:
467
+ # Clear and redraw
468
+ self.console.clear()
469
+
470
+ # Draw tree
471
+ tree = self._build_display_tree()
472
+ self.console.print(tree)
473
+
474
+ # Draw help
475
+ self.console.print(self._show_help())
476
+
477
+ # Draw status bar
478
+ self.console.print(self._get_status_bar())
479
+
480
+ try:
481
+ # Get keyboard input
482
+ key = click.getchar()
483
+
484
+ if not self.visible_nodes:
485
+ continue
486
+
487
+ current_node = self.visible_nodes[self.current_index]
488
+
489
+ # Handle navigation
490
+ if key in ['\x1b[A', 'k']: # Up arrow or k
491
+ self.current_index = max(0, self.current_index - 1)
492
+ elif key in ['\x1b[B', 'j']: # Down arrow or j
493
+ self.current_index = min(len(self.visible_nodes) - 1, self.current_index + 1)
494
+ elif key == '\x1b[5~': # Page Up
495
+ term_width, term_height = shutil.get_terminal_size()
496
+ page_size = max(1, term_height - 15)
497
+ self.current_index = max(0, self.current_index - page_size)
498
+ elif key == '\x1b[6~': # Page Down
499
+ term_width, term_height = shutil.get_terminal_size()
500
+ page_size = max(1, term_height - 15)
501
+ self.current_index = min(len(self.visible_nodes) - 1, self.current_index + page_size)
502
+ elif key == '\x1b[H': # Home - go to top
503
+ self.current_index = 0
504
+ elif key == '\x1b[F': # End - go to bottom
505
+ self.current_index = len(self.visible_nodes) - 1
506
+ elif key == 'G': # Shift+G - go to bottom (vim style)
507
+ self.current_index = len(self.visible_nodes) - 1
508
+ elif key in ['\x1b[C', 'l', '\r']: # Right arrow, l, or Enter
509
+ if current_node.is_dir:
510
+ current_node.expanded = True
511
+ elif key in ['\x1b[D', 'h']: # Left arrow or h
512
+ if current_node.is_dir and current_node.expanded:
513
+ current_node.expanded = False
514
+ elif current_node.parent:
515
+ # Jump to parent
516
+ parent_idx = next((i for i, n in enumerate(self.visible_nodes)
517
+ if n == current_node.parent), None)
518
+ if parent_idx is not None:
519
+ self.current_index = parent_idx
520
+
521
+ # Handle selection
522
+ elif key == ' ': # Space - toggle selection
523
+ self._toggle_selection(current_node)
524
+ elif key == 's': # Snippet mode
525
+ if not current_node.is_dir:
526
+ self._toggle_selection(current_node, snippet_mode=True)
527
+ elif key == 'a': # Add all in directory
528
+ if current_node.is_dir:
529
+ self._toggle_directory(current_node)
530
+
531
+ # Handle actions
532
+ elif key == 'g': # Grep
533
+ self.console.print() # Add some space
534
+ self._handle_grep(current_node)
535
+ click.pause("Press any key to continue...")
536
+ elif key == 'd': # Dependencies
537
+ self.console.print() # Add some space
538
+ self._show_dependencies(current_node)
539
+ click.pause("Press any key to continue...")
540
+ elif key == 'q': # Quit
541
+ self.quit_selection = True
542
+ elif key == '\x03': # Ctrl+C
543
+ raise KeyboardInterrupt()
544
+
545
+ except Exception as e:
546
+ self.console.print(f"[red]Error: {e}[/red]")
547
+ click.pause("Press any key to continue...")
548
+
549
+ # Clear screen one more time
550
+ self.console.clear()
551
+
552
+ # Convert selections to FileTuple format
553
+ files_to_include = []
554
+ for abs_path, (is_snippet, chunks) in self.selected_files.items():
555
+ # Convert back to relative path for the output
556
+ rel_path = os.path.relpath(abs_path)
557
+ files_to_include.append((rel_path, is_snippet, chunks, get_language_for_file(abs_path)))
558
+
559
+ return files_to_include, self.char_count
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kopipasta
3
- Version: 0.28.0
3
+ Version: 0.30.0
4
4
  Summary: A CLI tool to generate prompts with project structure and file contents
5
5
  Home-page: https://github.com/mkorpela/kopipasta
6
6
  Author: Mikko Korpela
@@ -22,6 +22,8 @@ License-File: LICENSE
22
22
  Requires-Dist: pyperclip==1.9.0
23
23
  Requires-Dist: requests==2.32.3
24
24
  Requires-Dist: Pygments==2.18.0
25
+ Requires-Dist: rich==13.8.1
26
+ Requires-Dist: click==8.2.1
25
27
 
26
28
  # kopipasta
27
29
 
@@ -0,0 +1,12 @@
1
+ kopipasta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ kopipasta/file.py,sha256=2HEoNczbKH3TtLO0zYUcZfUoHqb0-o83xFUOgY9rG-Y,1244
3
+ kopipasta/import_parser.py,sha256=yLzkMlQm2avKjfqcpMY0PxbA_2ihV9gSYJplreWIPEQ,12424
4
+ kopipasta/main.py,sha256=MlfmP_eG0ZyvdhxdGyPsN2TxqnM4hq9pa2ToQEbbNP4,48894
5
+ kopipasta/prompt.py,sha256=fOCuJVTLUfR0fjKf5qIlnl_3pNsKNKsvt3C8f4tsmxk,6889
6
+ kopipasta/tree_selector.py,sha256=hoS2yENrI6cLJ8s3ZXesctZkn3kAx4LwpD2cnp_ilSc,23900
7
+ kopipasta-0.30.0.dist-info/LICENSE,sha256=xw4C9TAU7LFu4r_MwSbky90uzkzNtRwAo3c51IWR8lk,1091
8
+ kopipasta-0.30.0.dist-info/METADATA,sha256=Ol5GwqwEazcTaH_dsMc18We53YLIeSWRlgNT2CeLfWo,4894
9
+ kopipasta-0.30.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
10
+ kopipasta-0.30.0.dist-info/entry_points.txt,sha256=but54qDNz1-F8fVvGstq_QID5tHjczP7bO7rWLFkc6Y,50
11
+ kopipasta-0.30.0.dist-info/top_level.txt,sha256=iXohixMuCdw8UjGDUp0ouICLYBDrx207sgZIJ9lxn0o,10
12
+ kopipasta-0.30.0.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- kopipasta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- kopipasta/import_parser.py,sha256=yLzkMlQm2avKjfqcpMY0PxbA_2ihV9gSYJplreWIPEQ,12424
3
- kopipasta/main.py,sha256=MUTi4vj_OWTwb2Y0PqQvm--oaX3FKHSrqAUAIDvcPwU,43910
4
- kopipasta-0.28.0.dist-info/LICENSE,sha256=xw4C9TAU7LFu4r_MwSbky90uzkzNtRwAo3c51IWR8lk,1091
5
- kopipasta-0.28.0.dist-info/METADATA,sha256=XxmONaSfjOxhNSh4X31mdahvKxKwfwtQz0IxIA1lpFc,4838
6
- kopipasta-0.28.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
7
- kopipasta-0.28.0.dist-info/entry_points.txt,sha256=but54qDNz1-F8fVvGstq_QID5tHjczP7bO7rWLFkc6Y,50
8
- kopipasta-0.28.0.dist-info/top_level.txt,sha256=iXohixMuCdw8UjGDUp0ouICLYBDrx207sgZIJ9lxn0o,10
9
- kopipasta-0.28.0.dist-info/RECORD,,