filesnap 0.1.2__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.
@@ -0,0 +1,49 @@
1
+ name: Upload Python Package
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ permissions:
7
+ contents: read
8
+
9
+ jobs:
10
+ release-build:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ with:
16
+ fetch-depth: 0
17
+ - uses: astral-sh/setup-uv@v7
18
+ with:
19
+ enable-cache: true
20
+
21
+ - name: Build release dist
22
+ run: uv build
23
+
24
+ - name: Upload dist
25
+ uses: actions/upload-artifact@v4
26
+ with:
27
+ name: release-dists
28
+ path: dist/
29
+
30
+ pypi-publish:
31
+ runs-on: ubuntu-latest
32
+ needs:
33
+ - release-build
34
+ permissions:
35
+ id-token: write
36
+ environment:
37
+ name: pypi
38
+
39
+ steps:
40
+ - name: Download dist
41
+ uses: actions/download-artifact@v4
42
+ with:
43
+ name: release-dists
44
+ path: dist/
45
+
46
+ - name: Publish release dist
47
+ uses: pypa/gh-action-pypi-publish@release/v1
48
+ with:
49
+ packages-dir: dist/
@@ -0,0 +1,11 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ .env
@@ -0,0 +1 @@
1
+ 3.14
filesnap-0.1.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jesusjsg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: filesnap
3
+ Version: 0.1.2
4
+ Summary: A simple CLI to handle your files
5
+ Project-URL: Repository, https://github.com/jesusjsg/filesnap
6
+ Author-email: jesus <jesusjsgdev@gmail.com>
7
+ License-File: LICENSE
8
+ Keywords: cli,files
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: typer>=0.21.1
11
+ Description-Content-Type: text/markdown
12
+
13
+ # filesnap
14
+
15
+ `filesnap` is a command-line tool for managing files and directories.
16
+
17
+ ## Commands
18
+
19
+ ### `filesnap scan`
20
+
21
+ Scans all the files in the path.
22
+
23
+ | Argument | Description | Default |
24
+ |---|---|---|
25
+ | `path` | Path to scan | Current directory |
26
+
27
+ | Option | Alias | Description |
28
+ |---|---|---|
29
+ | `--recursive` | `-r` | Recursive search. |
30
+ | `--pretty` | `-p` | Pretty print the output in a table. |
31
+ | `--exclude` | | Exclude files/directories from scanning. |
32
+ | `--ext` | `-e` | Scan only files with these extensions. |
33
+
34
+ ### `filesnap count`
35
+
36
+ Counts all the files by extension in the path selected.
37
+
38
+ | Argument | Description | Default |
39
+ |---|---|---|
40
+ | `path` | Path to count | Current directory |
41
+
42
+ | Option | Alias | Description |
43
+ |---|---|---|
44
+ | `--recursive` | `-r` | Recursive search. |
45
+ | `--exclude` | | Exclude files/directories from counting. |
46
+
47
+ ### `filesnap clean`
48
+
49
+ Cleans the content of a path.
50
+
51
+ | Argument | Description |
52
+ |---|---|
53
+ | `path` | Path to clean |
54
+
55
+ | Option | Alias | Description |
56
+ |---|---|---|
57
+ | `--recursive` | `-r` | Recursive cleaning. |
58
+ | `--contain` | `-c` | Clean only files containing this string. |
59
+ | `--ext` | `-e` | Clean only files with these extensions. |
60
+ | `--exclude` | | Exclude files/directories from cleaning. |
61
+ | `--force` | `-f` | Force deletion without confirmation. |
62
+ | `--dry-run` | `--dry` | Simulate cleaning without deleting files. |
63
+
64
+ ### `filesnap export`
65
+
66
+ Exports the filenames to a file.
67
+
68
+ | Argument | Description |
69
+ |---|---|
70
+ | `path` | Path to scan for filenames |
71
+
72
+ | Option | Alias | Description |
73
+ |---|---|---|
74
+ | `--type` | `-t` | The type of file to export to (e.g., `txt`, `csv`, `json`). |
75
+ | `--recursive` | `-r` | Recursive scanning. |
76
+ | `--output` | `-o` | The output file name. |
77
+ | `--format` | `-f` | The format of the output. |
78
+ | `--column` | `-c` | The column to export (defaults to `file_name`). |
79
+
@@ -0,0 +1,67 @@
1
+ # filesnap
2
+
3
+ `filesnap` is a command-line tool for managing files and directories.
4
+
5
+ ## Commands
6
+
7
+ ### `filesnap scan`
8
+
9
+ Scans all the files in the path.
10
+
11
+ | Argument | Description | Default |
12
+ |---|---|---|
13
+ | `path` | Path to scan | Current directory |
14
+
15
+ | Option | Alias | Description |
16
+ |---|---|---|
17
+ | `--recursive` | `-r` | Recursive search. |
18
+ | `--pretty` | `-p` | Pretty print the output in a table. |
19
+ | `--exclude` | | Exclude files/directories from scanning. |
20
+ | `--ext` | `-e` | Scan only files with these extensions. |
21
+
22
+ ### `filesnap count`
23
+
24
+ Counts all the files by extension in the path selected.
25
+
26
+ | Argument | Description | Default |
27
+ |---|---|---|
28
+ | `path` | Path to count | Current directory |
29
+
30
+ | Option | Alias | Description |
31
+ |---|---|---|
32
+ | `--recursive` | `-r` | Recursive search. |
33
+ | `--exclude` | | Exclude files/directories from counting. |
34
+
35
+ ### `filesnap clean`
36
+
37
+ Cleans the content of a path.
38
+
39
+ | Argument | Description |
40
+ |---|---|
41
+ | `path` | Path to clean |
42
+
43
+ | Option | Alias | Description |
44
+ |---|---|---|
45
+ | `--recursive` | `-r` | Recursive cleaning. |
46
+ | `--contain` | `-c` | Clean only files containing this string. |
47
+ | `--ext` | `-e` | Clean only files with these extensions. |
48
+ | `--exclude` | | Exclude files/directories from cleaning. |
49
+ | `--force` | `-f` | Force deletion without confirmation. |
50
+ | `--dry-run` | `--dry` | Simulate cleaning without deleting files. |
51
+
52
+ ### `filesnap export`
53
+
54
+ Exports the filenames to a file.
55
+
56
+ | Argument | Description |
57
+ |---|---|
58
+ | `path` | Path to scan for filenames |
59
+
60
+ | Option | Alias | Description |
61
+ |---|---|---|
62
+ | `--type` | `-t` | The type of file to export to (e.g., `txt`, `csv`, `json`). |
63
+ | `--recursive` | `-r` | Recursive scanning. |
64
+ | `--output` | `-o` | The output file name. |
65
+ | `--format` | `-f` | The format of the output. |
66
+ | `--column` | `-c` | The column to export (defaults to `file_name`). |
67
+
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "filesnap"
3
+ description = "A simple CLI to handle your files"
4
+ readme = "README.md"
5
+ requires-python = ">=3.12"
6
+ license-files = ["LICENSE"]
7
+ authors = [{ name = "jesus", email = "jesusjsgdev@gmail.com" }]
8
+ keywords = ["cli", "files"]
9
+ dependencies = [
10
+ "typer>=0.21.1",
11
+ ]
12
+ dynamic = ["version"]
13
+
14
+ [project.urls]
15
+ Repository = "https://github.com/jesusjsg/filesnap"
16
+
17
+ [project.scripts]
18
+ filesnap = "filesnap.main:app"
19
+
20
+ [build-system]
21
+ requires = ["hatch-vcs", "hatchling"]
22
+ build-backend = "hatchling.build"
23
+
24
+ [tool.hatch.version]
25
+ source = "vcs"
@@ -0,0 +1,68 @@
1
+ # Exclude a variety of commonly ignored directories.
2
+ exclude = [
3
+ ".bzr",
4
+ ".direnv",
5
+ ".eggs",
6
+ ".git",
7
+ ".git-rewrite",
8
+ ".hg",
9
+ ".ipynb_checkpoints",
10
+ ".mypy_cache",
11
+ ".nox",
12
+ ".pants.d",
13
+ ".pyenv",
14
+ ".pytest_cache",
15
+ ".pytype",
16
+ ".ruff_cache",
17
+ ".svn",
18
+ ".tox",
19
+ ".venv",
20
+ ".vscode",
21
+ "__pypackages__",
22
+ "_build",
23
+ "buck-out",
24
+ "build",
25
+ "dist",
26
+ "node_modules",
27
+ "site-packages",
28
+ "venv",
29
+ ]
30
+ # Same as Black.
31
+ line-length = 72
32
+ indent-width = 4
33
+ # Assume Python 3.9
34
+ target-version = "py314"
35
+
36
+ [lint]
37
+ # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
38
+ # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
39
+ # McCabe complexity (`C901`) by default.
40
+ select = ["E4", "E7", "E9", "F"]
41
+ ignore = []
42
+ # Allow fix for all enabled rules (when `--fix`) is provided.
43
+ fixable = ["ALL"]
44
+ unfixable = []
45
+ # Allow unused variables when underscore-prefixed.
46
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
47
+
48
+ [format]
49
+ # Like Black, use double quotes for strings.
50
+ quote-style = "double"
51
+ # Like Black, indent with spaces, rather than tabs.
52
+ indent-style = "space"
53
+ # Like Black, respect magic trailing commas.
54
+ skip-magic-trailing-comma = false
55
+ # Like Black, automatically detect the appropriate line ending.
56
+ line-ending = "auto"
57
+ # Enable auto-formatting of code examples in docstrings. Markdown,
58
+ # reStructuredText code/literal blocks and doctests are all supported.
59
+ #
60
+ # This is currently disabled by default, but it is planned for this
61
+ # to be opt-out in the future.
62
+ docstring-code-format = false
63
+ # Set the line length limit used when formatting code snippets in
64
+ # docstrings.
65
+ #
66
+ # This only has an effect when the `docstring-code-format` setting is
67
+ # enabled.
68
+ docstring-code-line-length = "dynamic"
File without changes
@@ -0,0 +1,10 @@
1
+ """I don't know if this is the best approach"""
2
+
3
+ DEFAULT_LIST_IGNORED = [
4
+ "node_modules",
5
+ ".git",
6
+ "vendor",
7
+ "__pycache__",
8
+ ".venv",
9
+ "venv",
10
+ ]
@@ -0,0 +1,13 @@
1
+ import typer
2
+
3
+ from filesnap.files.clean import app as clean_app
4
+ from filesnap.files.count import app as count_app
5
+ from filesnap.files.export import app as export_app
6
+ from filesnap.files.scan import app as scan_app
7
+
8
+ app = typer.Typer()
9
+
10
+ app.add_typer(scan_app)
11
+ app.add_typer(count_app)
12
+ app.add_typer(clean_app)
13
+ app.add_typer(export_app)
@@ -0,0 +1,90 @@
1
+ import os
2
+ import shutil
3
+ from typing import Annotated, List, Optional
4
+
5
+ import typer
6
+ from rich import print
7
+
8
+ from filesnap.utils.filesystem import (
9
+ get_exclude_list,
10
+ get_extension_list,
11
+ scandir,
12
+ validate_path_exist,
13
+ )
14
+ from filesnap.utils.formatting import task_progress
15
+
16
+ app = typer.Typer()
17
+
18
+
19
+ @app.command()
20
+ def clean(
21
+ path: str,
22
+ recursive: Annotated[
23
+ bool, typer.Option("--recursive", "-r")
24
+ ] = False,
25
+ contain: Annotated[str, typer.Option("--contain", "-c")] = "",
26
+ extensions: Annotated[
27
+ Optional[List[str]], typer.Option("--ext", "-e")
28
+ ] = None,
29
+ exclude: Annotated[Optional[List[str]], typer.Option()] = None,
30
+ force: Annotated[bool, typer.Option("--force", "-f")] = False,
31
+ dry_run: Annotated[
32
+ bool, typer.Option("--dry-run", "--dry")
33
+ ] = False,
34
+ ):
35
+ """Clean the content of the path"""
36
+ validate_path_exist(path)
37
+
38
+ if not dry_run:
39
+ if force:
40
+ typer.confirm(
41
+ f"Are you sure you want to delete the entire {path}?",
42
+ abort=True,
43
+ )
44
+ shutil.rmtree(path)
45
+ print(
46
+ f"[green]The directory {path} was removed successfully![/green]"
47
+ )
48
+ raise typer.Exit()
49
+
50
+ typer.confirm(
51
+ "Are you sure you want to delete the content of the path?",
52
+ abort=True,
53
+ )
54
+
55
+ scan_options = {
56
+ "exclude": get_exclude_list(exclude),
57
+ "extensions": get_extension_list(extensions),
58
+ "contain": contain,
59
+ }
60
+
61
+ entries = scandir(path, recursive, **scan_options)
62
+
63
+ track_entries = task_progress(
64
+ entries, description="Cleaning content..."
65
+ )
66
+
67
+ count = 0
68
+
69
+ for entry in track_entries:
70
+ count += 1
71
+ try:
72
+ if dry_run:
73
+ print(
74
+ f"[yellow][DRY RUN][/yellow] Would remove: [white]{entry.path}[/white]"
75
+ )
76
+ continue
77
+
78
+ if entry.is_file() or entry.is_symlink():
79
+ os.remove(entry)
80
+ elif entry.is_dir():
81
+ if not contain:
82
+ os.rmdir(entry.path)
83
+ except OSError:
84
+ pass
85
+ message = (
86
+ f"Dry run completed! {count} total files affected"
87
+ if dry_run
88
+ else "The content of the path was removed successfully!"
89
+ )
90
+ print(f"[green]{message}[/green]")
@@ -0,0 +1,71 @@
1
+ import os
2
+ from collections import defaultdict
3
+ from typing import Annotated, List, Optional
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.filesize import decimal
8
+ from rich.table import Table
9
+
10
+ from filesnap.utils.filesystem import (
11
+ get_exclude_list,
12
+ get_extension,
13
+ scandir,
14
+ validate_path_exist,
15
+ )
16
+ from filesnap.utils.formatting import task_progress
17
+
18
+ app = typer.Typer()
19
+ console = Console()
20
+
21
+
22
+ @app.command()
23
+ def count(
24
+ path: Annotated[
25
+ str, typer.Argument(help="Path to count")
26
+ ] = os.getcwd(),
27
+ recursive: Annotated[
28
+ bool,
29
+ typer.Option("--recursive", "-r", help="Recursive search."),
30
+ ] = False,
31
+ exclude: Annotated[Optional[List[str]], typer.Option()] = None,
32
+ ):
33
+ """Count all the files by extension in the path selected"""
34
+
35
+ validate_path_exist(path)
36
+
37
+ scan_options = {
38
+ "exclude": get_exclude_list(exclude),
39
+ }
40
+
41
+ info_stats = defaultdict(lambda: {"size": 0, "count": 0})
42
+
43
+ entries = scandir(path, recursive, **scan_options)
44
+
45
+ track_entries = task_progress(
46
+ entries, description="Scanning extensions..."
47
+ )
48
+
49
+ for entry in track_entries:
50
+ if entry.is_file():
51
+ ext = get_extension(entry.name)
52
+ file_info = entry.stat()
53
+
54
+ info_stats[ext]["size"] += file_info.st_size
55
+ info_stats[ext]["count"] += 1
56
+
57
+ table = Table(title=f"File statistics for {path}")
58
+ table.add_column("Extension", style="cyan")
59
+ table.add_column("Size", style="magenta", justify="right")
60
+ table.add_column("Count", style="green", justify="right")
61
+
62
+ sorted_stats = sorted(
63
+ info_stats.items(),
64
+ key=lambda item: item[1]["size"],
65
+ reverse=True,
66
+ )
67
+
68
+ for ext, info in sorted_stats:
69
+ table.add_row(ext, decimal(info["size"]), str(info["count"]))
70
+
71
+ console.print(table)
@@ -0,0 +1,49 @@
1
+ from pathlib import Path
2
+ from typing import Annotated, Optional
3
+
4
+ import typer
5
+ from rich import print
6
+
7
+ from filesnap.utils.filesystem import (
8
+ export_file,
9
+ scandir,
10
+ validate_path_exist,
11
+ )
12
+ from filesnap.utils.formatting import task_progress
13
+
14
+ app = typer.Typer()
15
+
16
+
17
+ @app.command()
18
+ def export(
19
+ path: str,
20
+ type: Annotated[str, typer.Option("--type", "-t")],
21
+ recursive: Annotated[
22
+ bool, typer.Option("--recursive", "-r")
23
+ ] = False,
24
+ output: Annotated[
25
+ Optional[str], typer.Option("--output", "-o")
26
+ ] = None,
27
+ format: Annotated[
28
+ Optional[str], typer.Option("--format", "-f")
29
+ ] = None,
30
+ column: Annotated[
31
+ str, typer.Option("--column", "-c")
32
+ ] = "file_name",
33
+ ):
34
+ """Export the filename to a txt file"""
35
+ validate_path_exist(path)
36
+
37
+ if output is None:
38
+ output = f"{Path(path).name}.{type}"
39
+
40
+ entries = scandir(path, recursive)
41
+ track_entries = task_progress(
42
+ entries, description=f"Generating {type.upper()} file..."
43
+ )
44
+
45
+ export_file(track_entries, type, output, column, format)
46
+
47
+ print(
48
+ f"[green]{type.upper()} file generated successfully[/green] :star:"
49
+ )
@@ -0,0 +1,85 @@
1
+ import os
2
+ from typing import Annotated, List, Optional
3
+
4
+ import typer
5
+ from rich import print
6
+ from rich.console import Console
7
+ from rich.filesize import decimal
8
+ from rich.table import Table
9
+
10
+ from filesnap.utils.filesystem import (
11
+ get_exclude_list,
12
+ get_extension_list,
13
+ scandir,
14
+ validate_path_exist,
15
+ )
16
+ from filesnap.utils.formatting import format_date, task_progress
17
+
18
+ app = typer.Typer()
19
+ console = Console()
20
+
21
+ MAX_TABLE_ROWS = 1000
22
+
23
+
24
+ @app.command()
25
+ def scan(
26
+ path: Annotated[
27
+ str, typer.Argument(help="Path to count")
28
+ ] = os.getcwd(),
29
+ recursive: Annotated[
30
+ bool,
31
+ typer.Option("--recursive", "-r", help="Recursive search."),
32
+ ] = False,
33
+ pretty: Annotated[
34
+ bool,
35
+ typer.Option(
36
+ "--pretty",
37
+ "-p",
38
+ ),
39
+ ] = False,
40
+ exclude: Annotated[Optional[List[str]], typer.Option()] = None,
41
+ extensions: Annotated[
42
+ Optional[List[str]], typer.Option("--ext", "-e")
43
+ ] = None,
44
+ ):
45
+ """
46
+ Scan all the files in the path
47
+ """
48
+
49
+ validate_path_exist(path)
50
+
51
+ scan_options = {
52
+ "exclude": get_exclude_list(exclude),
53
+ "extensions": get_extension_list(extensions),
54
+ }
55
+
56
+ entries = scandir(path, recursive, **scan_options)
57
+ count = 0
58
+
59
+ table = Table("Name", "Size", "Created") if pretty else None
60
+
61
+ track_entries = task_progress(
62
+ entries, description="Scanning path..."
63
+ )
64
+
65
+ for entry in track_entries:
66
+ count += 1
67
+ if pretty and table is not None:
68
+ if count <= MAX_TABLE_ROWS:
69
+ file_info = entry.stat()
70
+ table.add_row(
71
+ entry.name,
72
+ decimal(file_info.st_size),
73
+ format_date(file_info.st_ctime),
74
+ )
75
+
76
+ if pretty and table:
77
+ with console.pager(styles=True):
78
+ console.print(table)
79
+
80
+ if count > MAX_TABLE_ROWS:
81
+ print(
82
+ f"\n:warning:[yellow]Warning[/yellow]: Table output truncated. Showing first {MAX_TABLE_ROWS}"
83
+ )
84
+
85
+ print(f"{count} files found in [green]{path}[/green]")
@@ -0,0 +1,35 @@
1
+ from typing import Annotated
2
+
3
+ import typer
4
+
5
+ from filesnap.files import app as files_app
6
+ from filesnap.version import version_callback
7
+
8
+ app = typer.Typer(
9
+ no_args_is_help=True,
10
+ add_completion=False,
11
+ help="A simple CLI to handle your files",
12
+ )
13
+
14
+ app.add_typer(files_app)
15
+
16
+
17
+ @app.callback()
18
+ def main(
19
+ version: Annotated[
20
+ bool | None,
21
+ typer.Option(
22
+ "--version",
23
+ "-v",
24
+ is_eager=True,
25
+ callback=version_callback,
26
+ help="Show the current version",
27
+ ),
28
+ ] = None,
29
+ ):
30
+ "Callback to show the package version"
31
+ pass
32
+
33
+
34
+ if __name__ == "__main__":
35
+ app()
File without changes
@@ -0,0 +1,26 @@
1
+ import functools
2
+ import time
3
+
4
+ from rich.console import Console
5
+
6
+ console = Console()
7
+
8
+
9
+ def benchmark(func):
10
+ """Decorator to get the execution time of a function"""
11
+
12
+ @functools.wraps(func)
13
+ def wrapper(*args, **kwargs):
14
+ start_time = time.perf_counter()
15
+
16
+ result = func(*args, **kwargs)
17
+
18
+ end_time = time.perf_counter()
19
+ duration = end_time - start_time
20
+
21
+ console.print(
22
+ f"\n[bold magenta]Time:[/bold magenta] [cyan]{func.__name__}[/cyan] took [bold]{duration:.4f}[/bold] seconds."
23
+ )
24
+ return result
25
+
26
+ return wrapper
@@ -0,0 +1,132 @@
1
+ import csv
2
+ import json
3
+ import os
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Generator, Iterable, List, Optional
7
+
8
+ import typer
9
+ from rich import print
10
+
11
+ from filesnap.constants import DEFAULT_LIST_IGNORED
12
+
13
+
14
+ def export_file(
15
+ entries: Iterable,
16
+ file_type: str,
17
+ output: str,
18
+ column_name: str,
19
+ pattern: Optional[str] = None,
20
+ ):
21
+ regex = re.compile(pattern) if pattern else None
22
+ file_type = file_type.lower()
23
+
24
+ with open(output, "w", newline="", encoding="utf-8") as file:
25
+ if file_type == "txt":
26
+ file.write(f"{column_name}\n")
27
+ for entry in entries:
28
+ if entry.is_file() and not entry.name.startswith("."):
29
+ file_name = Path(entry.name).stem
30
+ file.write(
31
+ f"{regex.sub('', file_name) if regex else file_name}\n"
32
+ )
33
+
34
+ if file_type == "csv":
35
+ writer = csv.writer(file)
36
+ writer.writerow([column_name])
37
+ for entry in entries:
38
+ if entry.is_file() and not entry.name.startswith("."):
39
+ file_name = Path(entry.name).stem
40
+ writer.writerow(
41
+ [
42
+ regex.sub("", file_name)
43
+ if regex
44
+ else file_name
45
+ ]
46
+ )
47
+
48
+ if file_type == "json":
49
+ file.write("[\n")
50
+ first = True
51
+ for entry in entries:
52
+ if not entry.is_file() or entry.name.startswith("."):
53
+ continue
54
+
55
+ if not first:
56
+ file.write(",\n")
57
+
58
+ file_name = Path(entry.name).stem
59
+ clean_name = (
60
+ regex.sub("", file_name) if regex else file_name
61
+ )
62
+ json.dump({column_name: clean_name}, file, indent=4)
63
+ first = False
64
+ file.write("\n]")
65
+
66
+
67
+ def get_extension(file_name: str) -> str:
68
+ _, ext = os.path.splitext(file_name)
69
+ return ext.lower() if ext else "Invalid extension"
70
+
71
+
72
+ def get_exclude_list(exclude_names: Optional[List[str]]) -> set[str]:
73
+ final_ignores = set(DEFAULT_LIST_IGNORED)
74
+
75
+ if exclude_names:
76
+ for item in exclude_names:
77
+ user_list = [
78
+ file.strip() for file in item.split(",") if item.strip()
79
+ ]
80
+ final_ignores.update(user_list)
81
+ return final_ignores
82
+
83
+
84
+ def get_extension_list(extensions: Optional[List[str]]) -> set[str]:
85
+ if not extensions:
86
+ return set()
87
+
88
+ final_extensions = set()
89
+ for item in extensions:
90
+ parts = [i.strip() for i in item.split(",") if i.strip()]
91
+ for ext in parts:
92
+ final_extensions.add(f".{ext.lstrip('.')}")
93
+ return final_extensions
94
+
95
+
96
+ def scandir(
97
+ path: str, recursive: bool = False, **kwargs
98
+ ) -> Generator[os.DirEntry, None, None]:
99
+ exclude_names = kwargs.get("exclude", set())
100
+ valid_extensions = kwargs.get("extensions", set())
101
+ contain = kwargs.get("contain", "")
102
+
103
+ try:
104
+ for entry in os.scandir(path):
105
+ if entry.name in exclude_names:
106
+ continue
107
+
108
+ if contain and contain.lower() not in entry.name.lower():
109
+ continue
110
+
111
+ if entry.is_file():
112
+ if valid_extensions:
113
+ _, ext = os.path.splitext(entry.name)
114
+ if ext.lower() in valid_extensions:
115
+ yield entry
116
+ else:
117
+ yield entry
118
+
119
+ elif entry.is_dir():
120
+ if recursive:
121
+ yield from scandir(entry.path, recursive, **kwargs)
122
+ yield entry
123
+ except PermissionError:
124
+ pass
125
+
126
+
127
+ def validate_path_exist(path: str) -> None:
128
+ if not os.path.isdir(path):
129
+ print(
130
+ f"[bold red]Error:[/bold red] The path [yellow]{path}[/yellow] donsn't exist!"
131
+ )
132
+ raise typer.Exit(code=1)
@@ -0,0 +1,23 @@
1
+ import time
2
+ from typing import Iterable
3
+
4
+ from rich.progress import Progress, SpinnerColumn, TextColumn
5
+
6
+
7
+ def format_date(date: int | float) -> str:
8
+ return str(time.ctime(date))
9
+
10
+
11
+ def task_progress(
12
+ iterable: Iterable, description: str = "Processing..."
13
+ ) -> Iterable:
14
+ with Progress(
15
+ SpinnerColumn(),
16
+ TextColumn("[progress.description]{task.description}"),
17
+ transient=True,
18
+ ) as progress:
19
+ task = progress.add_task(description=description, total=None)
20
+
21
+ for item in iterable:
22
+ yield item
23
+ progress.update(task, advance=1)
@@ -0,0 +1,18 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ import typer
4
+ from rich import print
5
+
6
+
7
+ def version_callback(value: bool):
8
+ if value:
9
+ try:
10
+ pkg_version = version("filesnap")
11
+ print(
12
+ f":pushpin: Filesnap version [green]{pkg_version}[/green]"
13
+ )
14
+
15
+ except PackageNotFoundError:
16
+ print(":error: Unknown filesnap version")
17
+
18
+ raise typer.Exit()
filesnap-0.1.2/uv.lock ADDED
@@ -0,0 +1,110 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.12"
4
+
5
+ [[package]]
6
+ name = "click"
7
+ version = "8.3.1"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ dependencies = [
10
+ { name = "colorama", marker = "sys_platform == 'win32'" },
11
+ ]
12
+ sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
13
+ wheels = [
14
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
15
+ ]
16
+
17
+ [[package]]
18
+ name = "colorama"
19
+ version = "0.4.6"
20
+ source = { registry = "https://pypi.org/simple" }
21
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
22
+ wheels = [
23
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
24
+ ]
25
+
26
+ [[package]]
27
+ name = "filesnap"
28
+ source = { editable = "." }
29
+ dependencies = [
30
+ { name = "typer" },
31
+ ]
32
+
33
+ [package.metadata]
34
+ requires-dist = [{ name = "typer", specifier = ">=0.21.1" }]
35
+
36
+ [[package]]
37
+ name = "markdown-it-py"
38
+ version = "4.0.0"
39
+ source = { registry = "https://pypi.org/simple" }
40
+ dependencies = [
41
+ { name = "mdurl" },
42
+ ]
43
+ sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
44
+ wheels = [
45
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
46
+ ]
47
+
48
+ [[package]]
49
+ name = "mdurl"
50
+ version = "0.1.2"
51
+ source = { registry = "https://pypi.org/simple" }
52
+ sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
53
+ wheels = [
54
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
55
+ ]
56
+
57
+ [[package]]
58
+ name = "pygments"
59
+ version = "2.19.2"
60
+ source = { registry = "https://pypi.org/simple" }
61
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
62
+ wheels = [
63
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
64
+ ]
65
+
66
+ [[package]]
67
+ name = "rich"
68
+ version = "14.2.0"
69
+ source = { registry = "https://pypi.org/simple" }
70
+ dependencies = [
71
+ { name = "markdown-it-py" },
72
+ { name = "pygments" },
73
+ ]
74
+ sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
75
+ wheels = [
76
+ { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
77
+ ]
78
+
79
+ [[package]]
80
+ name = "shellingham"
81
+ version = "1.5.4"
82
+ source = { registry = "https://pypi.org/simple" }
83
+ sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
84
+ wheels = [
85
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
86
+ ]
87
+
88
+ [[package]]
89
+ name = "typer"
90
+ version = "0.21.1"
91
+ source = { registry = "https://pypi.org/simple" }
92
+ dependencies = [
93
+ { name = "click" },
94
+ { name = "rich" },
95
+ { name = "shellingham" },
96
+ { name = "typing-extensions" },
97
+ ]
98
+ sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" }
99
+ wheels = [
100
+ { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" },
101
+ ]
102
+
103
+ [[package]]
104
+ name = "typing-extensions"
105
+ version = "4.15.0"
106
+ source = { registry = "https://pypi.org/simple" }
107
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
108
+ wheels = [
109
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
110
+ ]