pseek 1.0.2__tar.gz → 2.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pseek
3
- Version: 1.0.2
3
+ Version: 2.1.0
4
4
  Summary: Pseek is a Python library to search files, folders, and text
5
5
  Home-page: https://github.com/ArianN8610/pysearch
6
6
  Author: Arian
@@ -93,7 +93,7 @@ pseek "error\d+" --regex
93
93
  | `--file` | Search only in file names |
94
94
  | `--directory` | Search only in directory names |
95
95
  | `--content` | Search inside file contents |
96
- | `--ext` | Filter by file extension (e.g., `.txt`, `.log`) |
96
+ | `--ext`, `--exclude-ext` | Filter by file extension (e.g., `.txt`, `.log`) |
97
97
  | `--case-sensitive` | Make the search case-sensitive |
98
98
  | `--regex` | Use regular expression for searching |
99
99
  | `--include`, `--exclude` | Limit search results to specific set of directories or files |
@@ -69,7 +69,7 @@ pseek "error\d+" --regex
69
69
  | `--file` | Search only in file names |
70
70
  | `--directory` | Search only in directory names |
71
71
  | `--content` | Search inside file contents |
72
- | `--ext` | Filter by file extension (e.g., `.txt`, `.log`) |
72
+ | `--ext`, `--exclude-ext` | Filter by file extension (e.g., `.txt`, `.log`) |
73
73
  | `--case-sensitive` | Make the search case-sensitive |
74
74
  | `--regex` | Use regular expression for searching |
75
75
  | `--include`, `--exclude` | Limit search results to specific set of directories or files |
@@ -0,0 +1,73 @@
1
+ import click
2
+ from .searcher import Search
3
+
4
+
5
+ @click.command()
6
+ @click.argument('query')
7
+ @click.option('-p', '--path', type=click.Path(exists=True, file_okay=False, dir_okay=True),
8
+ default='.', show_default=True, help='Base directory to search in.')
9
+ # Search type options
10
+ @click.option('-f', '--file', is_flag=True, help='Search only in file names.')
11
+ @click.option('-d', '--directory', is_flag=True, help='Search only in directory names.')
12
+ @click.option('-c', '--content', is_flag=True, help='Search inside file contents.')
13
+ # Additional options
14
+ @click.option('-C', '--case-sensitive', is_flag=True, help='Make the search case-sensitive.')
15
+ @click.option('--regex', is_flag=True, help='Use regular expression for searching.')
16
+ @click.option('--word', is_flag=True, help='Match whole words only.')
17
+ # Extension filters
18
+ @click.option('--ext', multiple=True, type=click.STRING,
19
+ help='Include files with these extensions. Example: --ext py --ext js')
20
+ @click.option('-E', '--exclude-ext', multiple=True, type=click.STRING,
21
+ help='Exclude files with these extensions. Example: --exclude-ext jpg --exclude-ext exe')
22
+ # Include/Exclude specific paths (files or directories)
23
+ @click.option('-i', '--include', type=click.Path(exists=True, file_okay=True, dir_okay=True),
24
+ multiple=True, help='Directories or files to include in search.')
25
+ @click.option('-e', '--exclude', type=click.Path(exists=True, file_okay=True, dir_okay=True),
26
+ multiple=True, help='Directories or files to exclude from search.')
27
+ # Size filters
28
+ @click.option('--max-size', type=click.FLOAT, help='Maximum file/directory size (in MB).')
29
+ @click.option('--min-size', type=click.FLOAT, help='Minimum file/directory size (in MB).')
30
+ # Output option
31
+ @click.option('--full-path', is_flag=True, help='Display full paths for results.')
32
+ def search(query, path, file, directory, content, case_sensitive, ext, exclude_ext, regex,
33
+ include, exclude, word, max_size, min_size, full_path):
34
+ """Search for files, directories, and file content based on the query."""
35
+ # If no search type is specified, search in all types.
36
+ if not any((file, directory, content)):
37
+ file = directory = content = True
38
+
39
+ # Initialize the Search class with provided options.
40
+ search_instance = Search(
41
+ base_path=path,
42
+ query=query,
43
+ case_sensitive=case_sensitive,
44
+ ext=ext,
45
+ exclude_ext=exclude_ext,
46
+ regex=regex,
47
+ include=include,
48
+ exclude=exclude,
49
+ whole_word=word,
50
+ max_size=max_size,
51
+ min_size=min_size,
52
+ full_path=full_path
53
+ )
54
+
55
+ total_results = 0
56
+
57
+ # Search for files if requested.
58
+ if file:
59
+ total_results += search_instance.search('file').echo('Files', 'file')
60
+ # Search for directories if requested and extension filters are not active.
61
+ if directory and not (ext or exclude_ext):
62
+ total_results += search_instance.search('directory').echo('Directories', 'directory')
63
+ # Search for content inside files if requested.
64
+ if content:
65
+ total_results += search_instance.search('content').echo('Contents', 'content')
66
+
67
+ # Display final summary message.
68
+ message = f'\nTotal results: {total_results}' if total_results else 'No results found'
69
+ click.echo(click.style(message, fg='red'))
70
+
71
+
72
+ if __name__ == "__main__":
73
+ search()
@@ -0,0 +1,182 @@
1
+ import re
2
+ import sys
3
+ import click
4
+ from pathlib import Path
5
+ from concurrent.futures import ThreadPoolExecutor
6
+
7
+ # Extensions that are not suitable for content search (binary, media, etc.)
8
+ EXCLUDED_EXTENSIONS = {
9
+ 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp', 'svg',
10
+ 'mp4', 'mov', 'avi', 'mkv', 'webm', 'flv', 'm4v', 'mpg', 'wmv',
11
+ 'mp3', 'wav', 'ogg', 'flac', 'aac', 'wma', 'opus',
12
+ 'exe', 'dll', 'bin', 'iso', 'img', 'dat', 'dmg', 'class', 'so', 'o', 'obj',
13
+ 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz',
14
+ 'ttf', 'otf', 'woff', 'woff2', 'eot',
15
+ 'db', 'sqlite', 'mdf', 'bak', 'log', 'jsonl', 'dat',
16
+ 'apk', 'ipa', 'deb', 'rpm', 'pkg', 'appimage', 'jar', 'war',
17
+ 'pyc', 'ps1', 'pem', 'pyd', 'whl'
18
+ }
19
+
20
+
21
+ class Search:
22
+ def __init__(self, base_path, query, case_sensitive, ext, exclude_ext, regex, include, exclude, whole_word,
23
+ max_size, min_size, full_path):
24
+ """Initialize search parameters"""
25
+ self.base_path = base_path
26
+ self.query = query
27
+ self.case_sensitive = case_sensitive
28
+ self.ext = ext
29
+ self.exclude_ext = exclude_ext
30
+ self.regex = regex
31
+ self.include = include
32
+ self.exclude = exclude
33
+ self.whole_word = whole_word
34
+ self.max_size = max_size
35
+ self.min_size = min_size
36
+ self.full_path = full_path
37
+ self.result = None
38
+
39
+ def conditions(self, p_resolved: Path, search_type: str) -> bool:
40
+ """
41
+ Check whether the file/directory should be skipped based on various filters.
42
+ Returns True if the path should be skipped.
43
+ """
44
+ try:
45
+ p_size_mb = p_resolved.stat().st_size / 1_048_576 # Convert size to MB
46
+ except OSError:
47
+ # If path is inaccessible, skip it.
48
+ return True
49
+
50
+ # Prepare include/exclude sets as resolved Paths
51
+ include_paths = {Path(p).resolve() for p in self.include}
52
+ exclude_paths = {Path(p).resolve() for p in self.exclude}
53
+
54
+ file_ext = p_resolved.suffix[1:]
55
+
56
+ # Conditions for skipping:
57
+ if (include_paths and not any(p_resolved.is_relative_to(inc) for inc in include_paths)) \
58
+ or (exclude_paths and any(p_resolved.is_relative_to(exc) for exc in exclude_paths)) \
59
+ or (self.ext and file_ext not in self.ext) \
60
+ or (self.exclude_ext and file_ext in self.exclude_ext) \
61
+ or (search_type == 'content' and file_ext in EXCLUDED_EXTENSIONS) \
62
+ or (self.max_size and p_size_mb > self.max_size) \
63
+ or (self.min_size and p_size_mb < self.min_size) \
64
+ or ((search_type in ('file', 'content')) and not p_resolved.is_file()) \
65
+ or (search_type == 'directory' and not p_resolved.is_dir()):
66
+ return True
67
+
68
+ return False
69
+
70
+ def search(self, search_type: str):
71
+ """Main search function. search_type can be 'file', 'directory' or 'content'"""
72
+ base_path = Path(self.base_path).resolve()
73
+ query = self.query
74
+
75
+ # Prepare query: escape if not regex
76
+ if not self.regex:
77
+ query = re.escape(query)
78
+ else:
79
+ try:
80
+ re.compile(query) # Validate regex pattern
81
+ except re.error:
82
+ click.echo(click.style('Invalid regex pattern: ', fg='red') + query)
83
+ sys.exit(1)
84
+
85
+ # Apply whole-word matching only if not using regex (or if desired behavior is defined)
86
+ if self.whole_word and not self.regex:
87
+ query = rf'\b{query}\b'
88
+
89
+ flags = 0 if self.case_sensitive else re.IGNORECASE # Adjust case sensitivity
90
+
91
+ # Precompile the regex pattern for performance
92
+ try:
93
+ pattern = re.compile(query, flags)
94
+ except re.error as e:
95
+ click.echo(click.style(f"Regex compile error: {e}", fg='red'))
96
+ sys.exit(1)
97
+
98
+ if search_type in ('file', 'directory'):
99
+ matches = []
100
+ for p in base_path.rglob('*'):
101
+ try:
102
+ p_resolved = p.resolve()
103
+ except Exception:
104
+ continue
105
+ # Skip if conditions fail or if name doesn't match the query
106
+ if self.conditions(p_resolved, search_type) or not pattern.search(p.name):
107
+ continue
108
+
109
+ # Highlight matched query in the name
110
+ highlighted_name = pattern.sub(lambda m: click.style(m.group(), fg='green'), p.name)
111
+ # Choose parent path based on full_path flag
112
+ p_parent = p_resolved.parent if self.full_path else p.parent
113
+ matches.append(f'{p_parent}\\{highlighted_name}')
114
+ else: # For content search
115
+ # Use dictionary: key: file path (colored), value: list of line matches
116
+ matches = {}
117
+
118
+ def process_file(file_path: Path):
119
+ """Process a single file for content search"""
120
+ try:
121
+ p_resolved = file_path.resolve()
122
+ except Exception:
123
+ return
124
+
125
+ # Skip file if any filter condition is met
126
+ if self.conditions(p_resolved, 'content'):
127
+ return
128
+
129
+ line_matches = []
130
+ try:
131
+ # Read file line by line
132
+ with open(p_resolved, 'r', encoding='utf-8', errors='ignore') as f:
133
+ for num, line in enumerate(f, 1):
134
+ line = line.strip()
135
+ if pattern.search(line):
136
+ # Count occurrences of query in line
137
+ count = len(list(pattern.finditer(line)))
138
+ highlighted_line = pattern.sub(lambda m: click.style(m.group(), fg='green'), line)
139
+ count_query = f'- Repeated {count} times' if count >= 3 else ''
140
+ line_matches.append(
141
+ click.style(f'Line {num}{count_query}: ', fg='magenta') + highlighted_line
142
+ )
143
+ except Exception:
144
+ return # Skip unreadable files
145
+
146
+ if line_matches:
147
+ file_label = str(p_resolved) if self.full_path else str(file_path)
148
+ # Use colored file path as key
149
+ matches[click.style(file_label, fg='cyan')] = line_matches
150
+
151
+ # Create a list of files to process (filter out inaccessible ones early)
152
+ files_to_process = [file for file in base_path.rglob('*')]
153
+ with ThreadPoolExecutor() as executor:
154
+ executor.map(process_file, files_to_process)
155
+
156
+ self.result = matches
157
+ return self
158
+
159
+ def echo(self, title: str, result_name: str) -> int:
160
+ """
161
+ Display the search results with a title.
162
+ Returns the count of results.
163
+ """
164
+ count_result = 0
165
+
166
+ if self.result:
167
+ click.echo(click.style(f'\n{title}:\n', fg='yellow'))
168
+ if isinstance(self.result, dict):
169
+ # For content search results
170
+ for key, value in self.result.items():
171
+ click.echo(key)
172
+ click.echo('\n'.join(value) + '\n')
173
+ count_result += len(value)
174
+ else:
175
+ # For file/directory search results
176
+ count_result = len(self.result)
177
+ click.echo('\n'.join(self.result))
178
+
179
+ if count_result >= 3:
180
+ click.echo(click.style(f'\n{count_result} results found for {result_name}', fg='blue'))
181
+
182
+ return count_result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pseek
3
- Version: 1.0.2
3
+ Version: 2.1.0
4
4
  Summary: Pseek is a Python library to search files, folders, and text
5
5
  Home-page: https://github.com/ArianN8610/pysearch
6
6
  Author: Arian
@@ -93,7 +93,7 @@ pseek "error\d+" --regex
93
93
  | `--file` | Search only in file names |
94
94
  | `--directory` | Search only in directory names |
95
95
  | `--content` | Search inside file contents |
96
- | `--ext` | Filter by file extension (e.g., `.txt`, `.log`) |
96
+ | `--ext`, `--exclude-ext` | Filter by file extension (e.g., `.txt`, `.log`) |
97
97
  | `--case-sensitive` | Make the search case-sensitive |
98
98
  | `--regex` | Use regular expression for searching |
99
99
  | `--include`, `--exclude` | Limit search results to specific set of directories or files |
@@ -4,7 +4,6 @@ setup.py
4
4
  pseek/__init__.py
5
5
  pseek/cli.py
6
6
  pseek/searcher.py
7
- pseek/utils.py
8
7
  pseek.egg-info/PKG-INFO
9
8
  pseek.egg-info/SOURCES.txt
10
9
  pseek.egg-info/dependency_links.txt
@@ -5,7 +5,7 @@ with open("README.md", encoding="utf-8") as f:
5
5
 
6
6
  setup(
7
7
  name="pseek",
8
- version="1.0.2",
8
+ version="2.1.0",
9
9
  author="Arian",
10
10
  author_email="ariannasiri86@gmail.com",
11
11
  description="Pseek is a Python library to search files, folders, and text",
pseek-1.0.2/pseek/cli.py DELETED
@@ -1,65 +0,0 @@
1
- import re
2
- import click
3
-
4
- from .utils import display_results
5
- from .searcher import search_in_names, search_in_file_contents
6
-
7
-
8
- @click.command()
9
- @click.argument('query')
10
- @click.option('-p', '--path', type=click.Path(exists=True, file_okay=False, dir_okay=True), default='.',
11
- show_default=True, help='Base directory to search in.')
12
- @click.option('-f', '--file', is_flag=True, help='Search only in file names.')
13
- @click.option('-d', '--directory', is_flag=True, help='Search only in directory names.')
14
- @click.option('-c', '--content', is_flag=True, help='Search inside file contents.')
15
- @click.option('-C', '--case-sensitive', is_flag=True, help='Make the search case-sensitive.')
16
- @click.option('--ext', multiple=True, type=click.STRING,
17
- help='Filter results by file extension. Example: --ext py --ext js')
18
- @click.option('--regex', is_flag=True, help='Use regular expression for searching.')
19
- @click.option('-i', '--include', type=click.Path(exists=True, file_okay=True, dir_okay=True),
20
- multiple=True, help='Directories or files that contain search results.')
21
- @click.option('-e', '--exclude', type=click.Path(exists=True, file_okay=True, dir_okay=True),
22
- multiple=True, help='Directories or files not included in search results.')
23
- @click.option('--word', is_flag=True, help='Search for results that match the whole word.')
24
- @click.option('--max-size', type=click.FLOAT, help='Maximum size directory or file can have (MB)')
25
- @click.option('--min-size', type=click.FLOAT, help='Minimum size directory or file can have (MB)')
26
- @click.option('--full-path', is_flag=True, help='Display complete paths of files and directories.')
27
- def search(query, path, file, directory, content, case_sensitive, ext, regex, include, exclude, word,
28
- max_size, min_size, full_path):
29
- """Search for files, directories, and content based on the query."""
30
-
31
- if regex:
32
- try:
33
- re.compile(query) # Ensure valid regex
34
- except re.error:
35
- click.echo(click.style('Invalid regex pattern: ', fg='red') + query)
36
- return
37
-
38
- # Enable all search types if none are explicitly selected
39
- if not any([file, directory, content]):
40
- file = directory = content = True
41
-
42
- results = []
43
-
44
- if file:
45
- filename_results = search_in_names(path, query, case_sensitive, regex, include, exclude, word, max_size,
46
- min_size, full_path, ext)
47
- display_results(filename_results, 'Files', 'file')
48
- results.extend(filename_results)
49
- if directory and not ext:
50
- dir_results = search_in_names(path, query, case_sensitive, regex, include, exclude, word, max_size, min_size,
51
- full_path, is_file=False)
52
- display_results(dir_results, 'Directories', 'directory')
53
- results.extend(dir_results)
54
- if content:
55
- content_results = search_in_file_contents(path, query, case_sensitive, ext, regex, include, exclude, word,
56
- max_size, min_size, full_path)
57
- display_results(content_results, 'Contents', 'content')
58
- results.extend(content_results)
59
-
60
- message = f'\nTotal results: {len(results)}' if results else 'No results found'
61
- click.echo(click.style(message, fg='red')) # Display total of results
62
-
63
-
64
- if __name__ == "__main__":
65
- search()
@@ -1,109 +0,0 @@
1
- import re
2
- import click
3
- from pathlib import Path
4
- from concurrent.futures import ThreadPoolExecutor
5
-
6
- from .utils import highlight_matches, safe_is_file
7
-
8
-
9
- def search_in_names(base_path, query, case_sensitive, regex, include, exclude, whole_word, max_size, min_size,
10
- full_path, ext=tuple(), is_file=True):
11
- """Search for file and folder names"""
12
- base_path = Path(base_path)
13
- matches = []
14
-
15
- if not regex:
16
- query = re.escape(query) # If regex is disabled, convert query to plain text
17
-
18
- if whole_word:
19
- query = rf'\b{query}\b' # Ensure search for whole words
20
-
21
- flags = 0 if case_sensitive else re.IGNORECASE # Adjust case sensitivity
22
-
23
- # Convert include and exclude paths to `Path`
24
- include_paths = {Path(p).resolve() for p in include}
25
- exclude_paths = {Path(p).resolve() for p in exclude}
26
-
27
- for p in base_path.rglob('*'):
28
- p_resolved = p.resolve() # Actual file or folder path
29
- try:
30
- p_size_mb = p_resolved.stat().st_size / 1_048_576 # Convert to MB
31
- except OSError:
32
- continue # if path cannot be accessible
33
-
34
- if (
35
- # If include is given, at least one of them must be present in the name
36
- (include and not any(p_resolved.is_relative_to(inc) for inc in include_paths))
37
- # If exclude is given, none should be in the name
38
- or (exclude and any(p_resolved.is_relative_to(exc) for exc in exclude_paths))
39
- # Prevent unnecessary continuation in case of type mismatch
40
- or (is_file and ext and p.suffix[1:] not in ext)
41
- # Check file or directory size
42
- or (max_size and p_size_mb > max_size)
43
- or (min_size and p_size_mb < min_size)
44
- ):
45
- continue
46
-
47
- if re.search(query, p.name, flags) and ((is_file and p.is_file()) or (not is_file and p.is_dir())):
48
- # Specify the found part
49
- highlighted_name = re.sub(query, lambda m: click.style(m.group(), fg='green'), p.name, flags=flags)
50
- p_parent = p_resolved.parent if full_path else p.parent # Get full path if full_path is True
51
- matches.append(f'{p_parent}\\{highlighted_name}')
52
-
53
- return matches
54
-
55
-
56
- def search_in_file_contents(base_path, query, case_sensitive, ext, regex, include, exclude, whole_word,
57
- max_size, min_size, full_path):
58
- """Search inside file contents"""
59
- base_path = Path(base_path)
60
- matches = []
61
-
62
- if not regex:
63
- query = re.escape(query)
64
-
65
- if whole_word:
66
- query = rf'\b{query}\b'
67
-
68
- flags = 0 if case_sensitive else re.IGNORECASE
69
-
70
- include_paths = {Path(p).resolve() for p in include}
71
- exclude_paths = {Path(p).resolve() for p in exclude}
72
-
73
- def process_file(file_path):
74
- """Processes a single file and searches for the query inside its content."""
75
- p_resolved = file_path.resolve()
76
- p_size_mb = p_resolved.stat().st_size / 1_048_576
77
-
78
- if (
79
- # Filter by include/exclude directories
80
- (include and not any(p_resolved.is_relative_to(inc) for inc in include_paths))
81
- or (exclude and any(p_resolved.is_relative_to(exc) for exc in exclude_paths))
82
- # Filter by extension
83
- or (ext and p_resolved.suffix[1:] not in ext)
84
- # Filter by file size
85
- or (max_size and p_size_mb > max_size)
86
- or (min_size and p_size_mb < min_size)
87
- ):
88
- return
89
-
90
- try:
91
- for num, line in enumerate(p_resolved.read_text(encoding='utf-8', errors='ignore').splitlines(), 1):
92
- line = line.strip()
93
-
94
- if re.search(query, line, flags):
95
- highlighted_snippet, count_query = highlight_matches(line, query, case_sensitive, regex, whole_word)
96
- file_path = p_resolved if full_path else file_path
97
- matches.append(
98
- click.style(file_path, fg='cyan')
99
- + click.style(f' (Line {num}) (Repeated {count_query} time(s)): ', fg='magenta')
100
- + highlighted_snippet
101
- )
102
- except Exception:
103
- return # Ignore unreadable files
104
-
105
- # Use multi-threading for faster processing
106
- with ThreadPoolExecutor() as executor:
107
- executor.map(process_file, (file for file in base_path.rglob('*') if safe_is_file(file)))
108
-
109
- return matches
@@ -1,66 +0,0 @@
1
- import re
2
- import click
3
-
4
-
5
- def highlight_matches(line, query, case_sensitive, regex, whole_word, context=20):
6
- """Highlight matches and truncate long lines with proper handling"""
7
-
8
- flags = 0 if case_sensitive else re.IGNORECASE
9
-
10
- if not regex and not whole_word:
11
- query = re.escape(query) # If regex is disabled, use escape to search for regular text
12
-
13
- # Find all matches
14
- matches = list(re.finditer(query, line, flags))
15
-
16
- if not matches:
17
- return None # If there is no match, return None
18
-
19
- start_positions = [max(0, m.start() - context) for m in matches] # 20 characters before
20
- end_positions = [min(len(line), m.end() + context) for m in matches] # 20 characters after
21
-
22
- # Combine sections close together to avoid repetition
23
- merged_snippets = []
24
- current_start, current_end = start_positions[0], end_positions[0]
25
-
26
- for i in range(1, len(matches)):
27
- if start_positions[i] <= current_end: # If the next section is close, combine them
28
- current_end = max(current_end, end_positions[i])
29
- else:
30
- merged_snippets.append((current_start, current_end))
31
- current_start, current_end = start_positions[i], end_positions[i]
32
-
33
- merged_snippets.append((current_start, current_end)) # Add the last section
34
-
35
- result = []
36
- for start, end in merged_snippets:
37
- snippet = line[start:end]
38
- snippet = re.sub(query, lambda m: click.style(m.group(), fg='green'), snippet, flags=flags)
39
- result.append(snippet)
40
-
41
- final_output = ' ... '.join(result) # If there are multiple results, put `...` between them
42
-
43
- # Add `...` at the beginning and end if needed
44
- if merged_snippets[0][0] > 0:
45
- final_output = '...' + final_output
46
- if merged_snippets[-1][1] < len(line):
47
- final_output += '...'
48
-
49
- return final_output, len(matches) # Count the number of repetitions
50
-
51
-
52
- def safe_is_file(file):
53
- """Check if a file is accessible and return True/False."""
54
- try:
55
- return file.is_file()
56
- except OSError:
57
- return False # Ignore files that cannot be accessed
58
-
59
-
60
- def display_results(results, title, result_name):
61
- """Display search results"""
62
- if results:
63
- click.echo(click.style(f'\n{title}:\n', fg='yellow'))
64
- for result in results:
65
- click.echo(result)
66
- click.echo(click.style(f'\n{len(results)} result(s) found for {result_name}', fg='blue'))
File without changes
File without changes
File without changes
File without changes