filesysman 1.0.0__py3-none-any.whl

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.
filesysman/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ from .filesysman import (
2
+ File,
3
+ Folder,
4
+ create_files,
5
+ delete_files,
6
+ rename_files,
7
+ get_files,
8
+ find_files,
9
+ rename_by_keyword,
10
+ delete_by_keyword,
11
+ get_files_count,
12
+ copy_files,
13
+ move_files,
14
+ organize_by_extension,
15
+ get_files_recursive,
16
+ find_files_recursive,
17
+ get_folders,
18
+ clear_empty_files,
19
+ clear_empty_folders,
20
+ )
@@ -0,0 +1,1006 @@
1
+ """Utilities for creating, deleting, renaming, searching, copying, moving,
2
+ organizing, and managing files, folders, and collections of files and folders.
3
+ """
4
+
5
+ from datetime import datetime as _datetime
6
+ from pathlib import Path as _Path
7
+ import shutil as _shutil
8
+
9
+
10
+ class File:
11
+ """Represents a single file and provides operations for managing it.
12
+
13
+ The class supports creating, deleting, renaming, copying, moving, reading,
14
+ writing, appending, and inspecting the represented file.
15
+ """
16
+
17
+ def __init__(self, file_path: str):
18
+ """Initializes a File instance with the path of a file.
19
+
20
+ Args:
21
+ file_path: The path of the file to represent.
22
+ """
23
+ self.__file_path = _Path(file_path)
24
+
25
+ def __str__(self) -> str:
26
+ """Returns the path of the represented file."""
27
+ return str(self.__file_path)
28
+
29
+ def create(
30
+ self,
31
+ folder_name: str = ".",
32
+ exist_ok: bool = True,
33
+ ) -> None:
34
+ """Creates the represented file in the current directory or a folder.
35
+
36
+ Args:
37
+ folder_name: The name or path of the folder in which to create
38
+ the file.
39
+ exist_ok: Whether to ignore an existing file instead of raising
40
+ an error.
41
+ """
42
+ folder = _Path(folder_name)
43
+ folder.mkdir(parents=True, exist_ok=True)
44
+ file = folder / self.__file_path
45
+ file.touch(exist_ok=exist_ok)
46
+ self.__file_path = file
47
+
48
+ def delete(self, missing_ok: bool = False) -> None:
49
+ """Deletes the represented file.
50
+
51
+ Args:
52
+ missing_ok: Whether to ignore the file if it does not exist.
53
+ """
54
+ self.__file_path.unlink(missing_ok=missing_ok)
55
+
56
+ def rename(self, new_name: str) -> None:
57
+ """Renames the represented file while keeping it in its current folder.
58
+
59
+ Args:
60
+ new_name: The new name of the file.
61
+ """
62
+ path = self.__file_path
63
+ new_path = path.rename(path.parent / new_name)
64
+ self.__file_path = new_path
65
+
66
+ def copy(self, destination: str = ".") -> None:
67
+ """Copies the represented file to a destination folder.
68
+
69
+ The destination folder is created if it does not exist. The original
70
+ file remains unchanged, and the File instance continues to represent
71
+ the original file.
72
+
73
+ Args:
74
+ destination: The name or path of the destination folder.
75
+ """
76
+ _Path(destination).mkdir(parents=True, exist_ok=True)
77
+ _shutil.copy2(self.__file_path, destination)
78
+
79
+ def move(self, destination: str = ".") -> None:
80
+ """Moves the represented file to a destination folder.
81
+
82
+ The destination folder is created if it does not exist. The File
83
+ instance is updated to represent the file at its new location.
84
+
85
+ Args:
86
+ destination: The name or path of the destination folder.
87
+ """
88
+ destination = _Path(destination)
89
+ destination.mkdir(parents=True, exist_ok=True)
90
+ _shutil.move(self.__file_path, destination)
91
+ self.__file_path = destination / self.name
92
+
93
+ def exists(self) -> bool:
94
+ """Returns whether the represented file exists."""
95
+ return self.__file_path.exists()
96
+
97
+ def is_empty(self) -> bool:
98
+ """Returns whether the represented file is empty."""
99
+ return self.size == 0
100
+
101
+ def read(self) -> str:
102
+ """Returns the text content of the represented file."""
103
+ return _Path.read_text(self.__file_path)
104
+
105
+ def write(self, text: str) -> None:
106
+ """Writes text to the represented file, replacing its existing
107
+ content.
108
+ """
109
+ self.__file_path.write_text(text)
110
+
111
+ def append(self, text: str) -> None:
112
+ """Adds text to the end of the represented file."""
113
+ with self.__file_path.open("a") as file:
114
+ file.write(text)
115
+
116
+ @property
117
+ def path(self) -> _Path:
118
+ """Returns the path of the represented file."""
119
+ return self.__file_path
120
+
121
+ @property
122
+ def name(self) -> str:
123
+ """Returns the name of the represented file."""
124
+ return self.__file_path.name
125
+
126
+ @property
127
+ def stem(self) -> str:
128
+ """Returns the stem of the represented file."""
129
+ return self.__file_path.stem
130
+
131
+ @property
132
+ def suffix(self) -> str:
133
+ """Returns the extension of the represented file."""
134
+ return self.__file_path.suffix
135
+
136
+ @property
137
+ def size(self) -> int:
138
+ """Returns the size of the represented file in bytes."""
139
+ return self.__file_path.stat().st_size
140
+
141
+ @property
142
+ def parent(self) -> _Path:
143
+ """Returns the path of the folder containing the file."""
144
+ return self.__file_path.parent
145
+
146
+ @property
147
+ def absolute_path(self) -> _Path:
148
+ """Returns the absolute path of the represented file."""
149
+ return self.__file_path.absolute()
150
+
151
+ @property
152
+ def is_absolute(self) -> bool:
153
+ """Returns whether the file's path is absolute."""
154
+ return self.__file_path.is_absolute()
155
+
156
+ @property
157
+ def created_time(self) -> _datetime:
158
+ """Returns the file's creation time as a datetime object."""
159
+ return _datetime.fromtimestamp(self.__file_path.stat().st_ctime)
160
+
161
+ @property
162
+ def modified_time(self) -> _datetime:
163
+ """Returns the file's last modification time as a datetime object."""
164
+ return _datetime.fromtimestamp(self.__file_path.stat().st_mtime)
165
+
166
+ @property
167
+ def accessed_time(self) -> _datetime:
168
+ """Returns the file's last access time as a datetime object."""
169
+ return _datetime.fromtimestamp(self.__file_path.stat().st_atime)
170
+
171
+
172
+ class Folder:
173
+ """Represents a single folder and provides operations for managing it.
174
+
175
+ The class supports creating, deleting, renaming, copying, moving, clearing,
176
+ and inspecting the represented folder.
177
+ """
178
+
179
+ def __init__(self, folder_path: str):
180
+ """Initializes a Folder instance with the path of a folder.
181
+
182
+ Args:
183
+ folder_path: The path of the folder to represent.
184
+ """
185
+ self.__folder_path = _Path(folder_path)
186
+
187
+ def __str__(self) -> str:
188
+ """Returns the path of the represented folder."""
189
+ return str(self.__folder_path)
190
+
191
+ def create(
192
+ self,
193
+ folder_name: str = ".",
194
+ parents: bool = False,
195
+ exist_ok: bool = False,
196
+ ) -> None:
197
+ """Creates the represented folder in the current directory or another
198
+ folder.
199
+
200
+ Args:
201
+ folder_name: The name or path of the folder in which to create
202
+ the folder.
203
+ parents: Whether to create missing parent folders.
204
+ exist_ok: Whether to ignore an existing folder instead of raising
205
+ an error.
206
+ """
207
+ folder = _Path(folder_name) / self.__folder_path
208
+ folder.mkdir(parents=parents, exist_ok=exist_ok)
209
+ self.__folder_path = folder
210
+
211
+ def delete(self, filled_ok: bool = False) -> None:
212
+ """Deletes the represented folder.
213
+
214
+ Args:
215
+ filled_ok: Whether to delete the folder and all of its contents
216
+ if it is not empty.
217
+ """
218
+ if filled_ok:
219
+ _shutil.rmtree(self.__folder_path)
220
+ else:
221
+ self.__folder_path.rmdir()
222
+
223
+ def rename(self, new_name: str) -> None:
224
+ """Renames the represented folder while keeping it in its current
225
+ parent folder.
226
+
227
+ Args:
228
+ new_name: The new name of the folder.
229
+ """
230
+ folder = self.__folder_path
231
+ new_folder = folder.rename(folder.parent / new_name)
232
+ self.__folder_path = new_folder
233
+
234
+ def copy(self, destination: str = ".") -> None:
235
+ """Copies the represented folder to a destination folder.
236
+
237
+ The original folder remains unchanged, and the Folder instance
238
+ continues to represent the original folder. If the destination folder
239
+ already exists, its contents are preserved and the copied contents are
240
+ added to it.
241
+
242
+ Args:
243
+ destination: The name or path of the destination folder.
244
+ """
245
+ _shutil.copytree(
246
+ self.__folder_path,
247
+ destination,
248
+ dirs_exist_ok=True,
249
+ )
250
+
251
+ def move(self, destination: str = ".") -> None:
252
+ """Moves the represented folder to a destination folder.
253
+
254
+ The destination folder is created if it does not exist. The represented
255
+ folder is moved inside the destination folder, and the Folder instance
256
+ is updated to represent the folder at its new location.
257
+
258
+ Args:
259
+ destination: The name or path of the destination folder.
260
+ """
261
+ destination = _Path(destination)
262
+ destination.mkdir(parents=True, exist_ok=True)
263
+ _shutil.move(self.__folder_path, destination)
264
+ self.__folder_path = destination / self.name
265
+
266
+ def clear(self) -> None:
267
+ """Deletes all contents of the represented folder without deleting
268
+ the folder itself.
269
+ """
270
+ for item in self.__folder_path.iterdir():
271
+ if item.is_file():
272
+ item.unlink()
273
+ else:
274
+ _shutil.rmtree(item)
275
+
276
+ def exists(self) -> bool:
277
+ """Returns whether the represented folder exists."""
278
+ return self.__folder_path.exists()
279
+
280
+ def is_empty(self) -> bool:
281
+ """Returns whether the represented folder is empty."""
282
+ return not any(self.__folder_path.iterdir())
283
+
284
+ @property
285
+ def path(self) -> _Path:
286
+ """Returns the path of the represented folder."""
287
+ return self.__folder_path
288
+
289
+ @property
290
+ def name(self) -> str:
291
+ """Returns the name of the represented folder."""
292
+ return self.__folder_path.name
293
+
294
+ @property
295
+ def parent(self) -> _Path:
296
+ """Returns the path of the folder containing the folder."""
297
+ return self.__folder_path.parent
298
+
299
+ @property
300
+ def absolute_path(self) -> _Path:
301
+ """Returns the absolute path of the represented folder."""
302
+ return self.__folder_path.absolute()
303
+
304
+ @property
305
+ def created_time(self) -> _datetime:
306
+ """Returns the folder's creation time as a datetime object."""
307
+ return _datetime.fromtimestamp(self.__folder_path.stat().st_ctime)
308
+
309
+ @property
310
+ def modified_time(self) -> _datetime:
311
+ """Returns the folder's last modification time as a datetime object."""
312
+ return _datetime.fromtimestamp(self.__folder_path.stat().st_mtime)
313
+
314
+ @property
315
+ def accessed_time(self) -> _datetime:
316
+ """Returns the folder's last access time as a datetime object."""
317
+ return _datetime.fromtimestamp(self.__folder_path.stat().st_atime)
318
+
319
+ @property
320
+ def current_size(self) -> int:
321
+ """Returns the total size in bytes of files directly inside the
322
+ folder.
323
+ """
324
+ size = 0
325
+ for item in self.__folder_path.iterdir():
326
+ if item.is_file():
327
+ size += item.stat().st_size
328
+ return size
329
+
330
+ @property
331
+ def total_size(self) -> int:
332
+ """Returns the total size in bytes of all files in the folder and
333
+ its subfolders.
334
+ """
335
+ size = 0
336
+ for item in self.__folder_path.rglob("*"):
337
+ if item.is_file():
338
+ size += item.stat().st_size
339
+ return size
340
+
341
+ @property
342
+ def is_absolute(self) -> bool:
343
+ """Returns whether the folder's path is absolute."""
344
+ return self.__folder_path.is_absolute()
345
+
346
+
347
+ def create_files(
348
+ number: int,
349
+ file_name: str = "File",
350
+ sep: str = " ",
351
+ extension: str = ".txt",
352
+ folder_name: str = ".",
353
+ ) -> None:
354
+ """Creates multiple numbered files in the current directory or a folder.
355
+
356
+ The files are named using the specified base name, separator, number,
357
+ and extension. The extension is normalized to lowercase.
358
+
359
+ Args:
360
+ number: The number of files to create.
361
+ file_name: The base name to use for the files.
362
+ sep: The separator placed between the file name and number.
363
+ extension: The file extension.
364
+ folder_name: The name or path of the folder in which to create
365
+ the files.
366
+
367
+ Raises:
368
+ ValueError: If number is not positive.
369
+ """
370
+ if number > 0:
371
+ extension = (
372
+ extension.lower()
373
+ if extension.startswith(".")
374
+ else f".{extension.lower()}"
375
+ )
376
+ folder = _Path(folder_name)
377
+ folder.mkdir(parents=True, exist_ok=True)
378
+
379
+ for number in range(1, number + 1):
380
+ (folder / f"{file_name}{sep}{number}{extension}").touch()
381
+ else:
382
+ raise ValueError("The number must be positive!")
383
+
384
+
385
+ def delete_files(
386
+ number: int,
387
+ file_name: str,
388
+ sep: str = " ",
389
+ extension: str = ".txt",
390
+ folder_name: str = ".",
391
+ ) -> None:
392
+ """Deletes multiple numbered files from the current directory or a folder.
393
+
394
+ The files are identified using the specified base name, separator, number,
395
+ and extension. The extension is normalized to lowercase.
396
+
397
+ Args:
398
+ number: The number of files to delete.
399
+ file_name: The base name of the files.
400
+ sep: The separator placed between the file name and number.
401
+ extension: The file extension.
402
+ folder_name: The name or path of the folder containing the files.
403
+
404
+ Raises:
405
+ ValueError: If number is not positive.
406
+ """
407
+ if number > 0:
408
+ extension = (
409
+ extension.lower()
410
+ if extension.startswith(".")
411
+ else f".{extension.lower()}"
412
+ )
413
+ folder = _Path(folder_name)
414
+
415
+ for number in range(1, number + 1):
416
+ (folder / f"{file_name}{sep}{number}{extension}").unlink(
417
+ missing_ok=True
418
+ )
419
+ else:
420
+ raise ValueError("The number must be positive!")
421
+
422
+
423
+ def rename_files(
424
+ number: int,
425
+ old_name: str,
426
+ new_name: str,
427
+ old_sep: str = " ",
428
+ new_sep: str = " ",
429
+ extension: str = ".txt",
430
+ folder_name: str = ".",
431
+ ) -> None:
432
+ """Renames multiple numbered files while preserving their numbering.
433
+
434
+ The base name and separator can be changed while the extension is
435
+ normalized to lowercase.
436
+
437
+ Args:
438
+ number: The number of files to rename.
439
+ old_name: The current base name of the files.
440
+ new_name: The new base name for the files.
441
+ old_sep: The separator currently used between the file name and number.
442
+ new_sep: The separator to use between the new file name and number.
443
+ extension: The file extension of the files.
444
+ folder_name: The name or path of the folder containing the files.
445
+
446
+ Raises:
447
+ ValueError: If number is not positive.
448
+ """
449
+ if number > 0:
450
+ extension = (
451
+ extension.lower()
452
+ if extension.startswith(".")
453
+ else f".{extension.lower()}"
454
+ )
455
+ folder = _Path(folder_name)
456
+
457
+ for number in range(1, number + 1):
458
+ path = folder / f"{old_name}{old_sep}{number}{extension}"
459
+
460
+ if path.exists():
461
+ new_path = folder / (
462
+ f"{new_name}{new_sep}{number}{extension}"
463
+ )
464
+ path.rename(new_path)
465
+ else:
466
+ raise ValueError("The number must be positive!")
467
+
468
+
469
+ def get_files(
470
+ folder_name: str = ".",
471
+ extension: str | None = None,
472
+ ) -> list[_Path]:
473
+ """Returns files from the specified folder, optionally filtered by extension.
474
+
475
+ Args:
476
+ folder_name: The name or path of the folder to search.
477
+ extension: The file extension used to filter the results.
478
+
479
+ Returns:
480
+ A list containing the paths of the matching files.
481
+ """
482
+ files = []
483
+ folder = _Path(folder_name)
484
+
485
+ if extension:
486
+ extension = (
487
+ extension.lower()
488
+ if extension.startswith(".")
489
+ else f".{extension.lower()}"
490
+ )
491
+
492
+ for item in folder.glob(f"*{extension}"):
493
+ if item.is_file():
494
+ files.append(item)
495
+ else:
496
+ for item in folder.iterdir():
497
+ if item.is_file():
498
+ files.append(item)
499
+
500
+ return files
501
+
502
+
503
+ def find_files(
504
+ keyword: str,
505
+ folder_name: str = ".",
506
+ extension: str | None = None,
507
+ ) -> list[_Path]:
508
+ """Returns files whose names contain a specified keyword.
509
+
510
+ Matching can optionally be filtered by file extension. The keyword must
511
+ not be empty.
512
+
513
+ Args:
514
+ keyword: The keyword to search for in file names.
515
+ folder_name: The name or path of the folder to search.
516
+ extension: The file extension used to filter the results.
517
+
518
+ Returns:
519
+ A list containing the paths of the matching files.
520
+
521
+ Raises:
522
+ ValueError: If keyword is empty.
523
+ """
524
+ if not keyword:
525
+ raise ValueError("The keyword must not be empty!")
526
+ files = []
527
+ folder = _Path(folder_name)
528
+ keyword = keyword.lower()
529
+
530
+ if extension:
531
+ extension = (
532
+ extension.lower()
533
+ if extension.startswith(".")
534
+ else f".{extension.lower()}"
535
+ )
536
+
537
+ for item in folder.iterdir():
538
+ if item.is_file():
539
+ name = item.stem.lower()
540
+ exten = item.suffix.lower()
541
+
542
+ if extension:
543
+ if keyword in name and extension == exten:
544
+ files.append(item)
545
+ else:
546
+ if keyword in name:
547
+ files.append(item)
548
+
549
+ return files
550
+
551
+
552
+ def rename_by_keyword(
553
+ keyword: str,
554
+ new_name: str,
555
+ sep: str = " ",
556
+ extension: str | None = None,
557
+ folder_name: str = ".",
558
+ ) -> None:
559
+ """Renames files whose names contain a specified keyword using sequential
560
+ names.
561
+
562
+ Original file extensions are preserved, and files can optionally be
563
+ filtered by extension. The keyword must not be empty.
564
+
565
+ Args:
566
+ keyword: The keyword to search for in file names.
567
+ new_name: The base name to assign to matching files.
568
+ sep: The separator placed between the new file name and number.
569
+ extension: The file extension used to filter matching files.
570
+ folder_name: The name or path of the folder to search.
571
+
572
+ Raises:
573
+ ValueError: If keyword is empty.
574
+ """
575
+ if not keyword:
576
+ raise ValueError("The keyword must not be empty!")
577
+
578
+ keyword = keyword.lower()
579
+
580
+ if extension:
581
+ extension = (
582
+ extension.lower()
583
+ if extension.startswith(".")
584
+ else f".{extension.lower()}"
585
+ )
586
+
587
+ num = 1
588
+ folder = _Path(folder_name)
589
+
590
+ for item in folder.iterdir():
591
+ if item.is_file():
592
+ name = item.stem.lower()
593
+ exten = item.suffix.lower()
594
+
595
+ if extension:
596
+ if keyword in name and extension == exten:
597
+ item.rename(folder / f"{new_name}{sep}{num}{exten}")
598
+ num += 1
599
+ else:
600
+ if keyword in name:
601
+ item.rename(folder / f"{new_name}{sep}{num}{exten}")
602
+ num += 1
603
+
604
+
605
+ def delete_by_keyword(
606
+ keyword: str,
607
+ extension: str | None = None,
608
+ folder_name: str = ".",
609
+ ) -> None:
610
+ """Deletes files whose names contain a specified keyword.
611
+
612
+ Files can optionally be filtered by extension. The keyword must not be
613
+ empty.
614
+
615
+ Args:
616
+ keyword: The keyword to search for in file names.
617
+ extension: The file extension used to filter the files.
618
+ folder_name: The name or path of the folder to search.
619
+
620
+ Raises:
621
+ ValueError: If keyword is empty.
622
+ """
623
+ if not keyword:
624
+ raise ValueError("The keyword must not be empty!")
625
+
626
+ keyword = keyword.lower()
627
+
628
+ if extension:
629
+ extension = (
630
+ extension.lower()
631
+ if extension.startswith(".")
632
+ else f".{extension.lower()}"
633
+ )
634
+
635
+ folder = _Path(folder_name)
636
+
637
+ for item in folder.iterdir():
638
+ if item.is_file():
639
+ name = item.stem.lower()
640
+
641
+ if extension:
642
+ exten = item.suffix.lower()
643
+
644
+ if keyword in name and extension == exten:
645
+ item.unlink()
646
+ else:
647
+ if keyword in name:
648
+ item.unlink()
649
+
650
+
651
+ def get_files_count(
652
+ folder_name: str = ".",
653
+ extension: str | None = None,
654
+ ) -> int:
655
+ """Returns the number of files in the specified folder, optionally
656
+ filtered by file extension.
657
+
658
+ Args:
659
+ folder_name: The name or path of the folder to search.
660
+ extension: The file extension used to filter the results.
661
+
662
+ Returns:
663
+ The number of matching files.
664
+ """
665
+ if extension:
666
+ extension = (
667
+ extension.lower()
668
+ if extension.startswith(".")
669
+ else f".{extension.lower()}"
670
+ )
671
+
672
+ files_count = 0
673
+ folder = _Path(folder_name)
674
+
675
+ for item in folder.iterdir():
676
+ if item.is_file():
677
+ if extension:
678
+ exten = item.suffix.lower()
679
+
680
+ if extension == exten:
681
+ files_count += 1
682
+ else:
683
+ files_count += 1
684
+
685
+ return files_count
686
+
687
+
688
+ def copy_files(
689
+ source_folder: str = ".",
690
+ destination_folder: str = ".",
691
+ extension: str | None = None,
692
+ ) -> None:
693
+ """Copies files from a source folder to a destination folder.
694
+
695
+ Existing files in the destination folder are overwritten. Files can
696
+ optionally be filtered by file extension.
697
+
698
+ Args:
699
+ source_folder: The name or path of the folder containing the files
700
+ to copy.
701
+ destination_folder: The name or path of the folder to copy the files
702
+ to.
703
+ extension: The file extension used to filter the files to copy.
704
+
705
+ Raises:
706
+ FileExistsError: If the source and destination folders are the same.
707
+ """
708
+ if source_folder == destination_folder:
709
+ raise FileExistsError(
710
+ f"'{source_folder}' Folder is the same as "
711
+ f"'{destination_folder}' Folder"
712
+ )
713
+
714
+ if extension:
715
+ extension = (
716
+ extension.lower()
717
+ if extension.startswith(".")
718
+ else f".{extension.lower()}"
719
+ )
720
+
721
+ source = _Path(source_folder)
722
+ destination = _Path(destination_folder)
723
+ destination.mkdir(parents=True, exist_ok=True)
724
+
725
+ for item in source.iterdir():
726
+ if item.is_file():
727
+ if extension:
728
+ exten = item.suffix.lower()
729
+
730
+ if extension == exten:
731
+ _shutil.copy2(item, destination)
732
+ else:
733
+ _shutil.copy2(item, destination)
734
+
735
+
736
+ def move_files(
737
+ source_folder: str = ".",
738
+ destination_folder: str = ".",
739
+ extension: str | None = None,
740
+ ) -> None:
741
+ """Moves files from a source folder to a destination folder.
742
+
743
+ Files can optionally be filtered by file extension.
744
+
745
+ Args:
746
+ source_folder: The name or path of the source folder.
747
+ destination_folder: The name or path of the destination folder.
748
+ extension: The file extension used to filter the files to move.
749
+
750
+ Raises:
751
+ FileExistsError: If the source and destination folders are the same.
752
+ shutil.Error: If a file cannot be moved to the destination folder.
753
+ """
754
+ if source_folder == destination_folder:
755
+ raise FileExistsError(
756
+ f"'{source_folder}' Folder is the same as "
757
+ f"'{destination_folder}' Folder"
758
+ )
759
+
760
+ if extension:
761
+ extension = (
762
+ extension.lower()
763
+ if extension.startswith(".")
764
+ else f".{extension.lower()}"
765
+ )
766
+
767
+ source = _Path(source_folder)
768
+ destination = _Path(destination_folder)
769
+ destination.mkdir(parents=True, exist_ok=True)
770
+
771
+ for item in source.iterdir():
772
+ if item.is_file():
773
+ if extension:
774
+ exten = item.suffix.lower()
775
+
776
+ if extension == exten:
777
+ _shutil.move(item, destination)
778
+ else:
779
+ _shutil.move(item, destination)
780
+
781
+
782
+ def organize_by_extension(
783
+ folder_name: str = ".",
784
+ destination_folder: str | None = None,
785
+ no_extension_folder: str = "No Extension",
786
+ ) -> None:
787
+ """Organizes files into folders based on their file extensions.
788
+
789
+ Files with the same extension are moved into the same folder. If a
790
+ destination folder is specified, copies of the files are organized there
791
+ while the original files remain unchanged. Files without an extension are
792
+ placed in a folder with the specified name.
793
+
794
+ Args:
795
+ folder_name: The name or path of the folder containing the files
796
+ to organize.
797
+ destination_folder: The name or path of the folder in which to
798
+ organize copies of the files. If None, the files are organized
799
+ in the original folder.
800
+ no_extension_folder: The name of the folder in which to place files
801
+ without an extension.
802
+
803
+ Raises:
804
+ FileExistsError: If the source and destination folders are the same.
805
+ """
806
+ if folder_name == destination_folder:
807
+ raise FileExistsError(
808
+ f"'{folder_name}' Folder is the same as "
809
+ f"'{destination_folder}' Folder"
810
+ )
811
+
812
+ folder = _Path(folder_name)
813
+
814
+ if destination_folder:
815
+ destination = _Path(destination_folder)
816
+ destination.mkdir(parents=True, exist_ok=True)
817
+
818
+ for item in folder.iterdir():
819
+ if item.is_file():
820
+ exten = item.suffix
821
+
822
+ if exten:
823
+ if destination_folder:
824
+ exten_folder = destination / exten.lstrip(".").upper()
825
+ exten_folder.mkdir(exist_ok=True)
826
+ _shutil.copy2(item, exten_folder)
827
+ else:
828
+ exten_folder = folder / exten.lstrip(".").upper()
829
+ exten_folder.mkdir(exist_ok=True)
830
+ _shutil.move(item, exten_folder)
831
+ else:
832
+ if destination_folder:
833
+ no_exten = destination / no_extension_folder
834
+ no_exten.mkdir(exist_ok=True)
835
+ _shutil.copy2(item, no_exten)
836
+ else:
837
+ no_exten = folder / no_extension_folder
838
+ no_exten.mkdir(exist_ok=True)
839
+ _shutil.move(item, no_exten)
840
+
841
+
842
+ def get_files_recursive(
843
+ folder_name: str = ".",
844
+ extension: str | None = None,
845
+ ) -> list[_Path]:
846
+ """Returns files from the specified folder and its subfolders, optionally
847
+ filtered by file extension.
848
+
849
+ Args:
850
+ folder_name: The name or path of the folder to search.
851
+ extension: The file extension used to filter the results.
852
+
853
+ Returns:
854
+ A list containing the paths of the matching files.
855
+ """
856
+ if extension:
857
+ extension = (
858
+ extension.lower()
859
+ if extension.startswith(".")
860
+ else f".{extension.lower()}"
861
+ )
862
+ files = []
863
+ folder = _Path(folder_name)
864
+
865
+ for item in folder.rglob(f"*{extension if extension else ''}"):
866
+ if item.is_file():
867
+ files.append(item)
868
+
869
+ return files
870
+
871
+
872
+ def find_files_recursive(
873
+ keyword: str,
874
+ folder_name: str = ".",
875
+ extension: str | None = None,
876
+ ) -> list[_Path]:
877
+ """Returns files from the specified folder and its subfolders whose names
878
+ contain a specified keyword.
879
+
880
+ Matching can optionally be filtered by file extension. The keyword must
881
+ not be empty.
882
+
883
+ Args:
884
+ keyword: The keyword to search for in file names.
885
+ folder_name: The name or path of the folder to search.
886
+ extension: The file extension used to filter the results.
887
+
888
+ Returns:
889
+ A list containing the paths of the matching files.
890
+
891
+ Raises:
892
+ ValueError: If keyword is empty.
893
+ """
894
+ if not keyword:
895
+ raise ValueError("The keyword must not be empty!")
896
+ if extension:
897
+ extension = (
898
+ extension.lower()
899
+ if extension.startswith(".")
900
+ else f".{extension.lower()}"
901
+ )
902
+
903
+ files = []
904
+ folder = _Path(folder_name)
905
+
906
+ for item in folder.rglob(f"*{extension if extension else ''}"):
907
+ if item.is_file():
908
+ if keyword.lower() in item.stem.lower():
909
+ files.append(item)
910
+
911
+ return files
912
+
913
+
914
+ def get_folders(
915
+ folder_name: str = ".",
916
+ recursive: bool = False,
917
+ ) -> list[_Path]:
918
+ """Returns folders from the specified folder, optionally including
919
+ subfolders.
920
+
921
+ Args:
922
+ folder_name: The name or path of the folder to search.
923
+ recursive: Whether to include folders inside subfolders.
924
+
925
+ Returns:
926
+ A list containing the paths of the matching folders.
927
+ """
928
+ folders = []
929
+ folder = _Path(folder_name)
930
+
931
+ if recursive:
932
+ for item in folder.rglob("*"):
933
+ if item.is_dir():
934
+ folders.append(item)
935
+ else:
936
+ for item in folder.iterdir():
937
+ if item.is_dir():
938
+ folders.append(item)
939
+
940
+ return folders
941
+
942
+
943
+ def clear_empty_files(
944
+ folder_name: str = ".",
945
+ recursive: bool = False,
946
+ extension: str | None = None,
947
+ ) -> None:
948
+ """Deletes empty files from the specified folder.
949
+
950
+ Files can optionally be filtered by file extension. When recursive is
951
+ True, empty files inside subfolders are also deleted.
952
+
953
+ Args:
954
+ folder_name: The name or path of the folder to search.
955
+ recursive: Whether to check files inside subfolders.
956
+ extension: The file extension used to filter the files.
957
+ """
958
+ if extension:
959
+ extension = (
960
+ extension.lower()
961
+ if extension.startswith(".")
962
+ else f".{extension.lower()}"
963
+ )
964
+
965
+ folder = _Path(folder_name)
966
+
967
+ if recursive:
968
+ for item in folder.rglob(f"*{extension if extension else ''}"):
969
+ if item.is_file() and item.stat().st_size == 0:
970
+ item.unlink()
971
+ else:
972
+ for item in folder.iterdir():
973
+ if item.is_file() and item.stat().st_size == 0:
974
+ if extension:
975
+ exten = item.suffix.lower()
976
+
977
+ if extension == exten:
978
+ item.unlink()
979
+ else:
980
+ item.unlink()
981
+
982
+
983
+ def clear_empty_folders(
984
+ folder_name: str = ".",
985
+ recursive: bool = False,
986
+ ) -> None:
987
+ """Deletes empty folders from the specified folder.
988
+
989
+ Only folders that are empty when they are checked are deleted. When
990
+ recursive is True, folders inside subfolders are also checked. A folder
991
+ that becomes empty after it has already been checked will not be deleted.
992
+
993
+ Args:
994
+ folder_name: The name or path of the folder to search.
995
+ recursive: Whether to check folders inside subfolders.
996
+ """
997
+ folder = _Path(folder_name)
998
+
999
+ if recursive:
1000
+ for item in folder.rglob("*"):
1001
+ if item.is_dir() and not any(item.iterdir()):
1002
+ item.rmdir()
1003
+ else:
1004
+ for item in folder.iterdir():
1005
+ if item.is_dir() and not any(item.iterdir()):
1006
+ item.rmdir()
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.4
2
+ Name: filesysman
3
+ Version: 1.0.0
4
+ Summary: Utilities for managing files and folders in Python.
5
+ Author: Saif Bayoumy
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Dynamic: license-file
11
+
12
+ # filesysman
13
+
14
+ A simple Python library for creating, managing, searching, copying, moving, and organizing files and folders.
15
+
16
+ ## Features
17
+
18
+ - Create and delete files and folders
19
+ - Read, write, and append file content
20
+ - Rename files and folders
21
+ - Copy and move files and folders
22
+ - Search for files by keyword
23
+ - Search recursively through subfolders
24
+ - Find files by extension
25
+ - Organize files by extension
26
+ - Get information about files and folders
27
+ - Count files
28
+ - Find and remove empty folders
29
+ - Work with multiple files at once
30
+
31
+ ## Installation
32
+
33
+ Install the package using pip:
34
+
35
+ pip install filesysman
36
+
37
+ ## Quick Start
38
+
39
+ ### Working with Files
40
+
41
+ from filesysman import File
42
+
43
+ file = File("example.txt")
44
+
45
+ file.create()
46
+ file.write("Hello, World!")
47
+
48
+ print(file.read())
49
+ print(file.name)
50
+ print(file.suffix)
51
+ print(file.size)
52
+
53
+ ### Working with Folders
54
+
55
+ from filesysman import Folder
56
+
57
+ folder = Folder("my_folder")
58
+
59
+ folder.create()
60
+
61
+ print(folder.exists())
62
+ print(folder.name)
63
+ print(folder.path)
64
+
65
+ ### Working with Multiple Files
66
+
67
+ from filesysman import create_files, get_files
68
+
69
+ create_files(
70
+ "file1.txt",
71
+ "file2.txt",
72
+ "file3.txt"
73
+ )
74
+
75
+ files = get_files()
76
+
77
+ for file in files:
78
+ print(file)
79
+
80
+ ## Searching for Files
81
+
82
+ Search for files containing a specific keyword in their names:
83
+
84
+ from filesysman import find_files
85
+
86
+ files = find_files("report")
87
+
88
+ for file in files:
89
+ print(file)
90
+
91
+ You can also search recursively through subfolders:
92
+
93
+ from filesysman import find_files_recursive
94
+
95
+ files = find_files_recursive("report")
96
+
97
+ for file in files:
98
+ print(file)
99
+
100
+ ## Organizing Files by Extension
101
+
102
+ Files can be organized into folders based on their extensions:
103
+
104
+ from filesysman import organize_by_extension
105
+
106
+ organize_by_extension()
107
+
108
+ For example:
109
+
110
+ Before:
111
+
112
+ folder/
113
+ ├── photo.jpg
114
+ ├── document.pdf
115
+ ├── script.py
116
+ └── notes.txt
117
+
118
+ After:
119
+
120
+ folder/
121
+ ├── JPG/
122
+ │ └── photo.jpg
123
+ ├── PDF/
124
+ │ └── document.pdf
125
+ ├── PY/
126
+ │ └── script.py
127
+ └── TXT/
128
+ └── notes.txt
129
+
130
+ ## Main API
131
+
132
+ ### Classes
133
+
134
+ - File
135
+ - Folder
136
+
137
+ ### File and Folder Operations
138
+
139
+ - create_files()
140
+ - delete_files()
141
+ - rename_files()
142
+ - copy_files()
143
+ - move_files()
144
+
145
+ ### Searching and Retrieving
146
+
147
+ - get_files()
148
+ - find_files()
149
+ - get_files_count()
150
+ - get_files_recursive()
151
+ - find_files_recursive()
152
+ - get_folders()
153
+
154
+ ### Organization and Cleanup
155
+
156
+ - organize_by_extension()
157
+ - rename_by_keyword()
158
+ - delete_by_keyword()
159
+ - clear_empty_folders()
160
+
161
+ ## Requirements
162
+
163
+ - Python 3.8 or newer
164
+
165
+ ## License
166
+
167
+ This project is licensed under the MIT License.
@@ -0,0 +1,7 @@
1
+ filesysman/__init__.py,sha256=CWPC0uLqJZMevreRd-twKEuSo0ukGxC27hWWrXQpQv8,395
2
+ filesysman/filesysman.py,sha256=jGIO5GP1DgCl5v92J0apTM5tBVuPugncXr-1TtLZ9bM,31930
3
+ filesysman-1.0.0.dist-info/licenses/LICENSE,sha256=q7KJbcPCqUAvkBuI1QNc8Kg9XPfBfnNkLN9WKwudO8U,11
4
+ filesysman-1.0.0.dist-info/METADATA,sha256=lUxAn_z-99vAverogHYFrMS60lwapbFaUSk_4b4kzjs,3173
5
+ filesysman-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
6
+ filesysman-1.0.0.dist-info/top_level.txt,sha256=18ljtwWrbPlMPFixrziWBkfZnEIixTMFrGYtXj2tkyY,11
7
+ filesysman-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ MIT License
@@ -0,0 +1 @@
1
+ filesysman