pseek 2.0.0__tar.gz → 2.1.1__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: 2.0.0
3
+ Version: 2.1.1
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)
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: 2.0.0
3
+ Version: 2.1.1
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 |
@@ -5,7 +5,7 @@ with open("README.md", encoding="utf-8") as f:
5
5
 
6
6
  setup(
7
7
  name="pseek",
8
- version="2.0.0",
8
+ version="2.1.1",
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-2.0.0/pseek/cli.py DELETED
@@ -1,48 +0,0 @@
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), default='.',
8
- show_default=True, help='Base directory to search in.')
9
- @click.option('-f', '--file', is_flag=True, help='Search only in file names.')
10
- @click.option('-d', '--directory', is_flag=True, help='Search only in directory names.')
11
- @click.option('-c', '--content', is_flag=True, help='Search inside file contents.')
12
- @click.option('-C', '--case-sensitive', is_flag=True, help='Make the search case-sensitive.')
13
- @click.option('--ext', multiple=True, type=click.STRING,
14
- help='Filter results by file extension. Example: --ext py --ext js')
15
- @click.option('--regex', is_flag=True, help='Use regular expression for searching.')
16
- @click.option('-i', '--include', type=click.Path(exists=True, file_okay=True, dir_okay=True),
17
- multiple=True, help='Directories or files that contain search results.')
18
- @click.option('-e', '--exclude', type=click.Path(exists=True, file_okay=True, dir_okay=True),
19
- multiple=True, help='Directories or files not included in search results.')
20
- @click.option('--word', is_flag=True, help='Search for results that match the whole word.')
21
- @click.option('--max-size', type=click.FLOAT, help='Maximum size directory or file can have (MB)')
22
- @click.option('--min-size', type=click.FLOAT, help='Minimum size directory or file can have (MB)')
23
- @click.option('--full-path', is_flag=True, help='Display complete paths of files and directories.')
24
- def search(query, path, file, directory, content, case_sensitive, ext, regex, include, exclude, word,
25
- max_size, min_size, full_path):
26
- """Search for files, directories, and content based on the query."""
27
-
28
- # Enable all search types if none are explicitly selected
29
- if not any([file, directory, content]):
30
- file = directory = content = True
31
-
32
- count_results = 0
33
- search_class = Search(path, query, case_sensitive, ext, regex, include, exclude, word, max_size, min_size,
34
- full_path)
35
-
36
- if file:
37
- count_results += search_class.search('file').echo('Files', 'file')
38
- if directory and not ext:
39
- count_results += search_class.search('directory').echo('Directories', 'directory')
40
- if content:
41
- count_results += search_class.search('content').echo('Contents', 'content')
42
-
43
- message = f'\nTotal results: {count_results}' if count_results else 'No results found'
44
- click.echo(click.style(message, fg='red')) # Display total of results
45
-
46
-
47
- if __name__ == "__main__":
48
- search()
@@ -1,146 +0,0 @@
1
- import re
2
- import sys
3
- import click
4
- from pathlib import Path
5
- from concurrent.futures import ThreadPoolExecutor
6
-
7
-
8
- class Search:
9
- def __init__(self, base_path, query, case_sensitive, ext, regex, include, exclude, whole_word,
10
- max_size, min_size, full_path):
11
- self.base_path = base_path
12
- self.query = query
13
- self.case_sensitive = case_sensitive
14
- self.ext = ext
15
- self.regex = regex
16
- self.include = include
17
- self.exclude = exclude
18
- self.whole_word = whole_word
19
- self.max_size = max_size
20
- self.min_size = min_size
21
- self.full_path = full_path
22
-
23
- self.result = []
24
-
25
- def conditions(self, p_resolved: Path, search_type: str) -> bool:
26
- """Check the correctness of path based on data"""
27
-
28
- try:
29
- p_size_mb = p_resolved.stat().st_size / 1_048_576 # Convert to MB
30
- except OSError:
31
- return True # if path cannot be accessible
32
-
33
- # Convert include and exclude paths to `Path`
34
- include_paths = {Path(p).resolve() for p in self.include}
35
- exclude_paths = {Path(p).resolve() for p in self.exclude}
36
-
37
- if (
38
- # Filter by include/exclude directories and files
39
- (self.include and not any(p_resolved.is_relative_to(inc) for inc in include_paths))
40
- or (self.exclude and any(p_resolved.is_relative_to(exc) for exc in exclude_paths))
41
- # Filter by extension
42
- or (self.ext and p_resolved.suffix[1:] not in self.ext)
43
- # Filter by file size
44
- or (self.max_size and p_size_mb > self.max_size)
45
- or (self.min_size and p_size_mb < self.min_size)
46
- # Filter by path type
47
- or ((search_type == 'file' or search_type == 'content') and not p_resolved.is_file())
48
- or (search_type == 'directory' and not p_resolved.is_dir())
49
- ):
50
- return True
51
-
52
- return False
53
-
54
- def search(self, search_type: str):
55
- """Search for paths based on the query"""
56
-
57
- base_path = Path(self.base_path)
58
- query = self.query
59
-
60
- if not self.regex:
61
- query = re.escape(query) # If regex is disabled, convert query to plain text
62
- else:
63
- try:
64
- re.compile(query) # Ensure valid regex
65
- except re.error:
66
- click.echo(click.style('Invalid regex pattern: ', fg='red') + query)
67
- sys.exit(1) # Exit the program with error code
68
-
69
- if self.whole_word:
70
- query = rf'\b{query}\b' # Ensure search for whole words
71
-
72
- flags = 0 if self.case_sensitive else re.IGNORECASE # Adjust case sensitivity
73
-
74
- if search_type == 'file' or search_type == 'directory':
75
- matches = []
76
- for p in base_path.rglob('*'):
77
- p_resolved = p.resolve() # Actual file or folder path
78
- if self.conditions(p_resolved, search_type) or (not re.search(query, p.name, flags)):
79
- continue
80
-
81
- # Specify the found part
82
- highlighted_name = re.sub(query, lambda m: click.style(m.group(), fg='green'), p.name, flags=flags)
83
- # Get full path if full_path is True
84
- p_parent = p_resolved.parent if self.full_path else p.parent
85
-
86
- matches.append(f'{p_parent}\\{highlighted_name}')
87
- else:
88
- matches = dict()
89
- def process_file(file_path):
90
- """Processes a single file and searches for the query inside its content."""
91
- p_resolved = file_path.resolve()
92
- line_matches = []
93
-
94
- try:
95
- for num, line in enumerate(p_resolved.read_text(encoding='utf-8', errors='ignore').splitlines(), 1):
96
- line = line.strip()
97
- if re.search(query, line, flags):
98
- # Count query in each line
99
- count_query = len(list(re.finditer(query, line, flags)))
100
- highlighted_name = re.sub(query, lambda m: click.style(m.group(), fg='green'), line,
101
- flags=flags)
102
- count_query = f'- Repeated {count_query} times' if count_query >= 3 else ''
103
-
104
- line_matches.append(
105
- click.style(f'Line {num}{count_query}: ', fg='magenta')
106
- + highlighted_name
107
- )
108
- except Exception:
109
- return # Ignore unreadable files
110
-
111
- if line_matches:
112
- file_path = p_resolved if self.full_path else file_path
113
- matches.update({click.style(file_path, fg='cyan'): line_matches})
114
-
115
- # Use multi-threading for faster processing
116
- with ThreadPoolExecutor() as executor:
117
- executor.map(
118
- process_file,
119
- (file for file in base_path.rglob('*') if not self.conditions(file, 'content'))
120
- )
121
-
122
- self.result = matches
123
- return self
124
-
125
- def echo(self, title: str, result_name: str) -> int:
126
- """Display search results"""
127
- count_result = 0
128
-
129
- if self.result:
130
- click.echo(click.style(f'\n{title}:\n', fg='yellow'))
131
-
132
- if isinstance(self.result, dict):
133
- # Display for content results
134
- for key, value in self.result.items():
135
- click.echo(key)
136
- click.echo('\n'.join(value) + '\n')
137
- count_result += len(value)
138
- else:
139
- # Display for directory and file results
140
- count_result = len(self.result)
141
- click.echo('\n'.join(self.result))
142
-
143
- if count_result >= 3:
144
- click.echo(click.style(f'\n{count_result} results found for {result_name}', fg='blue'))
145
-
146
- return count_result
File without changes
File without changes
File without changes
File without changes
File without changes