downsorter 0.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Your Name
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,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: downsorter
3
+ Version: 0.1.0
4
+ Summary: Organize files by extension into category folders from the command line.
5
+ Author: Your Name
6
+ License: MIT
7
+ Keywords: downloads,file sorting,organization,cli
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Environment :: Console
12
+ Classifier: Topic :: Utilities
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Dynamic: license-file
17
+
18
+ # Downsorter
19
+
20
+ A simple Python CLI tool to organize files in a folder by extension into category folders.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install .
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ Preview mode:
31
+
32
+ ```bash
33
+ downsorter --folder "C:\path\to\folder"
34
+ ```
35
+
36
+ Apply mode:
37
+
38
+ ```bash
39
+ downsorter --folder "C:\path\to\folder" --apply
40
+ ```
41
+
42
+ ## Options
43
+
44
+ - `--folder`: Folder to organize (defaults to your Downloads folder)
45
+ - `--apply`: Actually move files
46
+ - `--min-age-days`: Only move files at least this many days old
47
+ - `--log-file`: CSV file to write move logs
@@ -0,0 +1,30 @@
1
+ # Downsorter
2
+
3
+ A simple Python CLI tool to organize files in a folder by extension into category folders.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install .
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Preview mode:
14
+
15
+ ```bash
16
+ downsorter --folder "C:\path\to\folder"
17
+ ```
18
+
19
+ Apply mode:
20
+
21
+ ```bash
22
+ downsorter --folder "C:\path\to\folder" --apply
23
+ ```
24
+
25
+ ## Options
26
+
27
+ - `--folder`: Folder to organize (defaults to your Downloads folder)
28
+ - `--apply`: Actually move files
29
+ - `--min-age-days`: Only move files at least this many days old
30
+ - `--log-file`: CSV file to write move logs
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: downsorter
3
+ Version: 0.1.0
4
+ Summary: Organize files by extension into category folders from the command line.
5
+ Author: Your Name
6
+ License: MIT
7
+ Keywords: downloads,file sorting,organization,cli
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Environment :: Console
12
+ Classifier: Topic :: Utilities
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Dynamic: license-file
17
+
18
+ # Downsorter
19
+
20
+ A simple Python CLI tool to organize files in a folder by extension into category folders.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install .
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ Preview mode:
31
+
32
+ ```bash
33
+ downsorter --folder "C:\path\to\folder"
34
+ ```
35
+
36
+ Apply mode:
37
+
38
+ ```bash
39
+ downsorter --folder "C:\path\to\folder" --apply
40
+ ```
41
+
42
+ ## Options
43
+
44
+ - `--folder`: Folder to organize (defaults to your Downloads folder)
45
+ - `--apply`: Actually move files
46
+ - `--min-age-days`: Only move files at least this many days old
47
+ - `--log-file`: CSV file to write move logs
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ sorter.py
5
+ downsorter.egg-info/PKG-INFO
6
+ downsorter.egg-info/SOURCES.txt
7
+ downsorter.egg-info/dependency_links.txt
8
+ downsorter.egg-info/entry_points.txt
9
+ downsorter.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ downsorter = sorter:main
@@ -0,0 +1 @@
1
+ sorter
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "downsorter"
7
+ version = "0.1.0"
8
+ description = "Organize files by extension into category folders from the command line."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Your Name" }
14
+ ]
15
+ keywords = ["downloads", "file sorting", "organization", "cli"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: Microsoft :: Windows",
20
+ "Environment :: Console",
21
+ "Topic :: Utilities",
22
+ ]
23
+
24
+ [project.scripts]
25
+ downsorter = "sorter:main"
26
+
27
+ [tool.setuptools]
28
+ py-modules = ["sorter"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ import argparse
5
+ import csv
6
+ import shutil
7
+ from dataclasses import dataclass
8
+ from datetime import datetime, timedelta
9
+ from pathlib import Path
10
+
11
+
12
+ CATEGORIES = {
13
+ "Images": {".jpg" , ".jpeg" , ".png" , ".webp" , ".bmp"},
14
+ "PDFs" : {".pdf"},
15
+ "Documents" : {".doc" , ".docx" , ".txt" , ".md" , ".rtf"},
16
+ "Spreadsheets" : {".xls" , ".xlsx" , ".csv" , ".tsv"},
17
+ "PPTs" : {".ppt" , ".pptx"},
18
+ "archives" : {".rar" , ".zip" ,".7z" , ".tar" , ".gz"},
19
+ "installers" : {".exe" , ".msi"},
20
+ "Code" : {".py" , ".js" , ".html" , ".css" , ".json" , ".xml" , ".cpp" , ".java"},
21
+ "Audio" : {".mp3" , ".wav" , ".flac" , ".aac" , ".ogg"},
22
+ "Videos" : {".mp4" , ".gif" , ".mov" , ".mkv" , ".avi" , ".webm"}
23
+ }
24
+ IGNORE_NAMES = {
25
+ "desktop.ini",
26
+ "thumbs.db",
27
+ }
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class MovePlan:
32
+ source: Path
33
+ destination: Path
34
+
35
+ def default_downloads_folder():
36
+ return Path.home()/ "Downloads"
37
+
38
+
39
+ def category_in(file_path: Path):
40
+ suffix = file_path.suffix.lower()
41
+ for category, extensions in CATEGORIES.items():
42
+ if suffix in extensions:
43
+ return category
44
+ return "Other"
45
+
46
+ def agecheck(file_path: Path, minimum_days: int):
47
+ if minimum_days <= 0:
48
+ return True
49
+ mod_time = datetime.fromtimestamp(file_path.stat().st_mtime)
50
+ return mod_time <= datetime.now() - timedelta(days= minimum_days)
51
+
52
+ def unique_des(des: Path):
53
+ if not des.exists():
54
+ return des
55
+ counter = 1
56
+ while True:
57
+ candidate=des.with_name(
58
+ f"{des.stem} ({counter}){des.suffix}"
59
+ )
60
+ if not candidate.exists():
61
+ return candidate
62
+ counter = counter + 1
63
+
64
+ def BUILD_plan(downloads_folder:Path , minimum_days:int):
65
+ plans: list[MovePlan] = []
66
+ for item in downloads_folder.iterdir():
67
+ if not item.is_file():
68
+ continue
69
+ if item.name.lower() in IGNORE_NAMES:
70
+ continue
71
+ if not agecheck(item,minimum_days):
72
+ continue
73
+
74
+ category = category_in(item)
75
+ des = unique_des(downloads_folder / category / item.name)
76
+ plans.append(MovePlan(source=item , destination=des))
77
+ return plans
78
+
79
+ def PRINT_plan(plans :list[MovePlan] , apply : bool):
80
+ if not plans:
81
+ print("Nothing to clean , your folder is already tidy lmao")
82
+ return
83
+ action = "Moving" if apply else "Would move"
84
+ for plan in plans:
85
+ file_name = plan.source.name
86
+ destination = plan.destination.parent.name + "\\" + plan.destination.name
87
+ print(action + ": " + file_name + " -> " + destination)
88
+
89
+ def WRITE_log(log_file: Path , completed_changes : list[MovePlan]):
90
+ log_file.parent.mkdir(parents=True, exist_ok=True)
91
+ file_exists=log_file.exists()
92
+ with log_file.open("a" , newline="",encoding="utf-8") as f:
93
+ writer = csv.writer(f)
94
+ if not file_exists:
95
+ writer.writerow(["moved at", "source", "destination"])
96
+ moved_at = datetime.now().isoformat(timespec="seconds")
97
+ for move in completed_changes:
98
+ writer.writerow([moved_at, str(move.source) , str(move.destination)])
99
+
100
+ def APPLY_plan(plans : list[MovePlan] , log_file: Path):
101
+ completed_changes : list[MovePlan] = []
102
+
103
+ for plan in plans:
104
+ plan.destination.parent.mkdir(parents=True , exist_ok=True)
105
+ shutil.move(str(plan.source) , str(plan.destination))
106
+ completed_changes.append(plan)
107
+ if completed_changes:
108
+ WRITE_log(log_file,completed_changes)
109
+
110
+ def parse_args():
111
+ parser = argparse.ArgumentParser(
112
+ description="Safely organize your downloads folder by file type"
113
+ )
114
+ parser.add_argument(
115
+ "--folder",
116
+ type=Path,
117
+ default=default_downloads_folder(),
118
+ help="Folder to clean . By default downloads folder"
119
+ )
120
+ parser.add_argument(
121
+ "--apply",
122
+ action="store_true",
123
+ help="Actually move files. Without this flag, the script only previews changes.",
124
+ )
125
+ parser.add_argument(
126
+ "--min-age-days",
127
+ type=int,
128
+ default=0,
129
+ help="Only move files at least this many days old. can be edited",
130
+ )
131
+ parser.add_argument(
132
+ "--log-file",
133
+ type=Path,
134
+ default=Path("movelogs.csv"),
135
+ help="CSV file where completed moves are recorded.",
136
+ )
137
+ return parser.parse_args()
138
+
139
+ def main():
140
+ args = parse_args()
141
+ downloads_folder = args.folder.expanduser().resolve()
142
+
143
+ if not downloads_folder.exists():
144
+ raise SystemExit(f"Folder does not exist: {downloads_folder}")
145
+ if not downloads_folder.is_dir():
146
+ raise SystemExit(f"Not a folder: {downloads_folder}")
147
+ if args.min_age_days < 0:
148
+ raise SystemExit(f"--min-age-days cannot be NEGATIVE")
149
+
150
+ plans = BUILD_plan(downloads_folder, args.min_age_days)
151
+ PRINT_plan(plans , args.apply)
152
+
153
+ if args.apply and plans:
154
+ APPLY_plan(plans , args.log_file)
155
+ print(f"Done. Move log written to {args.log_file.resolve()}")
156
+ elif plans:
157
+ print("Preview only. Run again with --apply to move these files")
158
+
159
+ if __name__=="__main__":
160
+ main()