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.
- filesnap-0.1.2/.github/workflows/python-publish.yml +49 -0
- filesnap-0.1.2/.gitignore +11 -0
- filesnap-0.1.2/.python-version +1 -0
- filesnap-0.1.2/LICENSE +21 -0
- filesnap-0.1.2/PKG-INFO +79 -0
- filesnap-0.1.2/README.md +67 -0
- filesnap-0.1.2/pyproject.toml +25 -0
- filesnap-0.1.2/ruff.toml +68 -0
- filesnap-0.1.2/src/filesnap/__init__.py +0 -0
- filesnap-0.1.2/src/filesnap/constants.py +10 -0
- filesnap-0.1.2/src/filesnap/files/__init__.py +13 -0
- filesnap-0.1.2/src/filesnap/files/clean.py +90 -0
- filesnap-0.1.2/src/filesnap/files/count.py +71 -0
- filesnap-0.1.2/src/filesnap/files/export.py +49 -0
- filesnap-0.1.2/src/filesnap/files/scan.py +85 -0
- filesnap-0.1.2/src/filesnap/main.py +35 -0
- filesnap-0.1.2/src/filesnap/utils/__init__.py +0 -0
- filesnap-0.1.2/src/filesnap/utils/decorators.py +26 -0
- filesnap-0.1.2/src/filesnap/utils/filesystem.py +132 -0
- filesnap-0.1.2/src/filesnap/utils/formatting.py +23 -0
- filesnap-0.1.2/src/filesnap/version.py +18 -0
- filesnap-0.1.2/uv.lock +110 -0
|
@@ -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 @@
|
|
|
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.
|
filesnap-0.1.2/PKG-INFO
ADDED
|
@@ -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
|
+
|
filesnap-0.1.2/README.md
ADDED
|
@@ -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"
|
filesnap-0.1.2/ruff.toml
ADDED
|
@@ -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,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
|
+
]
|