pyfileops 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.
- pyfileops/__init__.py +52 -0
- pyfileops/core.py +366 -0
- pyfileops/exceptions.py +53 -0
- pyfileops-1.0.0.dist-info/METADATA +294 -0
- pyfileops-1.0.0.dist-info/RECORD +8 -0
- pyfileops-1.0.0.dist-info/WHEEL +5 -0
- pyfileops-1.0.0.dist-info/licenses/LICENSE +21 -0
- pyfileops-1.0.0.dist-info/top_level.txt +1 -0
pyfileops/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pyfileops
|
|
3
|
+
~~~~~~~~~
|
|
4
|
+
A simple, friendly Python library for file and directory operations.
|
|
5
|
+
|
|
6
|
+
Quickstart::
|
|
7
|
+
|
|
8
|
+
from pyfileops import fs
|
|
9
|
+
|
|
10
|
+
fs.copy("report.pdf", "backup/")
|
|
11
|
+
fs.cut("draft.txt", "archive/")
|
|
12
|
+
fs.rename("old_name.txt", "new_name.txt")
|
|
13
|
+
fs.delete("temp.log")
|
|
14
|
+
|
|
15
|
+
fs.mkdir("project/assets")
|
|
16
|
+
fs.rmdir("tmp")
|
|
17
|
+
|
|
18
|
+
fs.zip("project", "project.zip")
|
|
19
|
+
fs.unzip("project.zip", "restored/")
|
|
20
|
+
|
|
21
|
+
print(fs.exists("config.ini")) # True / False
|
|
22
|
+
print(fs.size("video.mp4")) # bytes
|
|
23
|
+
print(fs.is_file("data.csv")) # True / False
|
|
24
|
+
print(fs.is_dir("src")) # True / False
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from .core import FileSystem
|
|
28
|
+
from .exceptions import (
|
|
29
|
+
CopyError,
|
|
30
|
+
DeleteError,
|
|
31
|
+
FileOpsError,
|
|
32
|
+
PathNotFoundError,
|
|
33
|
+
ZipError,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"fs",
|
|
38
|
+
"FileSystem",
|
|
39
|
+
# Exceptions
|
|
40
|
+
"FileOpsError",
|
|
41
|
+
"PathNotFoundError",
|
|
42
|
+
"CopyError",
|
|
43
|
+
"DeleteError",
|
|
44
|
+
"ZipError",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
__version__ = "0.1.0"
|
|
48
|
+
__author__ = "pyfileops contributors"
|
|
49
|
+
__license__ = "MIT"
|
|
50
|
+
|
|
51
|
+
#: Global, ready-to-use :class:`~pyfileops.core.FileSystem` instance.
|
|
52
|
+
fs: FileSystem = FileSystem()
|
pyfileops/core.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pyfileops.core
|
|
3
|
+
~~~~~~~~~~~~~~
|
|
4
|
+
Core implementation of the FileSystem class.
|
|
5
|
+
Provides a simple, unified API for common file and directory operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import zipfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Union
|
|
13
|
+
|
|
14
|
+
from .exceptions import (
|
|
15
|
+
CopyError,
|
|
16
|
+
DeleteError,
|
|
17
|
+
FileOpsError,
|
|
18
|
+
PathNotFoundError,
|
|
19
|
+
ZipError,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Type alias accepted for all path arguments
|
|
23
|
+
PathLike = Union[str, os.PathLike]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FileSystem:
|
|
27
|
+
"""
|
|
28
|
+
A simple, friendly interface for file and directory operations.
|
|
29
|
+
|
|
30
|
+
Usage::
|
|
31
|
+
|
|
32
|
+
from pyfileops import fs
|
|
33
|
+
|
|
34
|
+
fs.copy("report.pdf", "backup/")
|
|
35
|
+
fs.delete("temp.log")
|
|
36
|
+
print(fs.size("video.mp4"))
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
# Internal helpers
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def _resolve(path: PathLike) -> Path:
|
|
45
|
+
"""Return an absolute, resolved :class:`pathlib.Path`."""
|
|
46
|
+
return Path(path).resolve()
|
|
47
|
+
|
|
48
|
+
def _require_exists(self, path: PathLike) -> Path:
|
|
49
|
+
"""Resolve *path* and raise :exc:`PathNotFoundError` if it is missing."""
|
|
50
|
+
resolved = self._resolve(path)
|
|
51
|
+
if not resolved.exists():
|
|
52
|
+
raise PathNotFoundError(str(path))
|
|
53
|
+
return resolved
|
|
54
|
+
|
|
55
|
+
# ------------------------------------------------------------------
|
|
56
|
+
# File operations
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def copy(self, source: PathLike, destination: PathLike) -> Path:
|
|
60
|
+
"""
|
|
61
|
+
Copy a file or directory tree to *destination*.
|
|
62
|
+
|
|
63
|
+
If *destination* is an existing directory, the source is copied
|
|
64
|
+
**inside** that directory (mirroring the behaviour of the ``cp``
|
|
65
|
+
command). If *destination* does not exist it is used as the new
|
|
66
|
+
name / path for the copy.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
source: Path of the file or directory to copy.
|
|
70
|
+
destination: Target path or parent directory.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The :class:`~pathlib.Path` of the newly created copy.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
PathNotFoundError: If *source* does not exist.
|
|
77
|
+
CopyError: If the copy operation fails for any reason.
|
|
78
|
+
"""
|
|
79
|
+
src = self._require_exists(source)
|
|
80
|
+
dst = self._resolve(destination)
|
|
81
|
+
|
|
82
|
+
# If destination is an existing directory, place the copy inside it
|
|
83
|
+
if dst.is_dir():
|
|
84
|
+
dst = dst / src.name
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
if src.is_dir():
|
|
88
|
+
shutil.copytree(src, dst)
|
|
89
|
+
else:
|
|
90
|
+
# Ensure parent directories exist
|
|
91
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
shutil.copy2(src, dst)
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
raise CopyError(str(source), str(destination), str(exc)) from exc
|
|
95
|
+
|
|
96
|
+
return dst
|
|
97
|
+
|
|
98
|
+
def cut(self, source: PathLike, destination: PathLike) -> Path:
|
|
99
|
+
"""
|
|
100
|
+
Move a file or directory to *destination*.
|
|
101
|
+
|
|
102
|
+
Behaves like :meth:`copy` but removes the original afterwards.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
source: Path of the file or directory to move.
|
|
106
|
+
destination: Target path or parent directory.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The :class:`~pathlib.Path` where the item was moved to.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
PathNotFoundError: If *source* does not exist.
|
|
113
|
+
CopyError: If the move operation fails for any reason.
|
|
114
|
+
"""
|
|
115
|
+
src = self._require_exists(source)
|
|
116
|
+
dst = self._resolve(destination)
|
|
117
|
+
|
|
118
|
+
if dst.is_dir():
|
|
119
|
+
dst = dst / src.name
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
shutil.move(str(src), str(dst))
|
|
124
|
+
except Exception as exc:
|
|
125
|
+
raise CopyError(str(source), str(destination), str(exc)) from exc
|
|
126
|
+
|
|
127
|
+
return dst
|
|
128
|
+
|
|
129
|
+
def rename(self, path: PathLike, new_name: str) -> Path:
|
|
130
|
+
"""
|
|
131
|
+
Rename a file or directory.
|
|
132
|
+
|
|
133
|
+
Only the **name** of the item changes; it stays in the same
|
|
134
|
+
parent directory. To move an item to a different location use
|
|
135
|
+
:meth:`cut` instead.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
path: Path of the file or directory to rename.
|
|
139
|
+
new_name: The new name (not a full path, just the bare name).
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
The :class:`~pathlib.Path` of the renamed item.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
PathNotFoundError: If *path* does not exist.
|
|
146
|
+
FileOpsError: If the rename operation fails for any reason.
|
|
147
|
+
"""
|
|
148
|
+
src = self._require_exists(path)
|
|
149
|
+
dst = src.parent / new_name
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
src.rename(dst)
|
|
153
|
+
except Exception as exc:
|
|
154
|
+
raise FileOpsError(
|
|
155
|
+
f"Could not rename '{path}' to '{new_name}' — {exc}"
|
|
156
|
+
) from exc
|
|
157
|
+
|
|
158
|
+
return dst
|
|
159
|
+
|
|
160
|
+
def delete(self, path: PathLike) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Delete a file or directory (including all its contents).
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
path: Path of the file or directory to delete.
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
PathNotFoundError: If *path* does not exist.
|
|
169
|
+
DeleteError: If the deletion fails for any reason.
|
|
170
|
+
"""
|
|
171
|
+
target = self._require_exists(path)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
if target.is_dir():
|
|
175
|
+
shutil.rmtree(target)
|
|
176
|
+
else:
|
|
177
|
+
target.unlink()
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
raise DeleteError(str(path), str(exc)) from exc
|
|
180
|
+
|
|
181
|
+
# ------------------------------------------------------------------
|
|
182
|
+
# Directory operations
|
|
183
|
+
# ------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
def mkdir(self, path: PathLike) -> Path:
|
|
186
|
+
"""
|
|
187
|
+
Create a directory, including any missing intermediate directories.
|
|
188
|
+
|
|
189
|
+
Does nothing if the directory already exists (idempotent).
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
path: Path of the directory to create.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
The :class:`~pathlib.Path` of the created (or existing) directory.
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
FileOpsError: If the directory cannot be created.
|
|
199
|
+
"""
|
|
200
|
+
target = self._resolve(path)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
except Exception as exc:
|
|
205
|
+
raise FileOpsError(
|
|
206
|
+
f"Could not create directory '{path}' — {exc}"
|
|
207
|
+
) from exc
|
|
208
|
+
|
|
209
|
+
return target
|
|
210
|
+
|
|
211
|
+
def rmdir(self, path: PathLike) -> None:
|
|
212
|
+
"""
|
|
213
|
+
Remove a directory and **all** of its contents recursively.
|
|
214
|
+
|
|
215
|
+
This is an alias for calling :meth:`delete` on a directory and is
|
|
216
|
+
provided for semantic clarity.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
path: Path of the directory to remove.
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
PathNotFoundError: If *path* does not exist.
|
|
223
|
+
DeleteError: If the deletion fails for any reason.
|
|
224
|
+
"""
|
|
225
|
+
target = self._require_exists(path)
|
|
226
|
+
|
|
227
|
+
if not target.is_dir():
|
|
228
|
+
raise FileOpsError(f"'{path}' is not a directory. Use delete() to remove files.")
|
|
229
|
+
|
|
230
|
+
self.delete(target)
|
|
231
|
+
|
|
232
|
+
# ------------------------------------------------------------------
|
|
233
|
+
# Compression
|
|
234
|
+
# ------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
def zip(self, source: PathLike, zip_file: PathLike) -> Path:
|
|
237
|
+
"""
|
|
238
|
+
Compress a file or directory into a ZIP archive.
|
|
239
|
+
|
|
240
|
+
If *source* is a directory, the entire tree is added to the
|
|
241
|
+
archive preserving the internal structure.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
source: Path of the file or directory to compress.
|
|
245
|
+
zip_file: Path for the resulting ``.zip`` file.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
The :class:`~pathlib.Path` of the created ZIP file.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
PathNotFoundError: If *source* does not exist.
|
|
252
|
+
ZipError: If the compression fails for any reason.
|
|
253
|
+
"""
|
|
254
|
+
src = self._require_exists(source)
|
|
255
|
+
dst = self._resolve(zip_file)
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
259
|
+
|
|
260
|
+
with zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
261
|
+
if src.is_dir():
|
|
262
|
+
for file in src.rglob("*"):
|
|
263
|
+
zf.write(file, file.relative_to(src.parent))
|
|
264
|
+
else:
|
|
265
|
+
zf.write(src, src.name)
|
|
266
|
+
except Exception as exc:
|
|
267
|
+
raise ZipError(str(source), str(exc)) from exc
|
|
268
|
+
|
|
269
|
+
return dst
|
|
270
|
+
|
|
271
|
+
def unzip(self, zip_file: PathLike, destination: PathLike) -> Path:
|
|
272
|
+
"""
|
|
273
|
+
Extract a ZIP archive to *destination*.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
zip_file: Path to the ``.zip`` file.
|
|
277
|
+
destination: Directory where the contents will be extracted.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
The :class:`~pathlib.Path` of the destination directory.
|
|
281
|
+
|
|
282
|
+
Raises:
|
|
283
|
+
PathNotFoundError: If *zip_file* does not exist.
|
|
284
|
+
ZipError: If the file is not a valid ZIP or extraction fails.
|
|
285
|
+
"""
|
|
286
|
+
src = self._require_exists(zip_file)
|
|
287
|
+
dst = self._resolve(destination)
|
|
288
|
+
|
|
289
|
+
if not zipfile.is_zipfile(src):
|
|
290
|
+
raise ZipError(str(zip_file), "File is not a valid ZIP archive")
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
294
|
+
with zipfile.ZipFile(src, "r") as zf:
|
|
295
|
+
zf.extractall(dst)
|
|
296
|
+
except ZipError:
|
|
297
|
+
raise
|
|
298
|
+
except Exception as exc:
|
|
299
|
+
raise ZipError(str(zip_file), str(exc)) from exc
|
|
300
|
+
|
|
301
|
+
return dst
|
|
302
|
+
|
|
303
|
+
# ------------------------------------------------------------------
|
|
304
|
+
# Queries
|
|
305
|
+
# ------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
def exists(self, path: PathLike) -> bool:
|
|
308
|
+
"""
|
|
309
|
+
Check whether *path* exists on the filesystem.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
path: The path to check.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
``True`` if the path exists, ``False`` otherwise.
|
|
316
|
+
"""
|
|
317
|
+
return self._resolve(path).exists()
|
|
318
|
+
|
|
319
|
+
def size(self, path: PathLike) -> int:
|
|
320
|
+
"""
|
|
321
|
+
Return the size of a file or directory in bytes.
|
|
322
|
+
|
|
323
|
+
For directories, the size is the total of all files within the
|
|
324
|
+
tree (not the disk usage reported by the OS).
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
path: Path of the file or directory.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Size in bytes as an integer.
|
|
331
|
+
|
|
332
|
+
Raises:
|
|
333
|
+
PathNotFoundError: If *path* does not exist.
|
|
334
|
+
"""
|
|
335
|
+
target = self._require_exists(path)
|
|
336
|
+
|
|
337
|
+
if target.is_dir():
|
|
338
|
+
return sum(f.stat().st_size for f in target.rglob("*") if f.is_file())
|
|
339
|
+
|
|
340
|
+
return target.stat().st_size
|
|
341
|
+
|
|
342
|
+
def is_file(self, path: PathLike) -> bool:
|
|
343
|
+
"""
|
|
344
|
+
Return ``True`` if *path* points to a regular file.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
path: The path to check.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
``True`` if *path* is a file, ``False`` otherwise (including
|
|
351
|
+
when the path does not exist).
|
|
352
|
+
"""
|
|
353
|
+
return self._resolve(path).is_file()
|
|
354
|
+
|
|
355
|
+
def is_dir(self, path: PathLike) -> bool:
|
|
356
|
+
"""
|
|
357
|
+
Return ``True`` if *path* points to a directory.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
path: The path to check.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
``True`` if *path* is a directory, ``False`` otherwise
|
|
364
|
+
(including when the path does not exist).
|
|
365
|
+
"""
|
|
366
|
+
return self._resolve(path).is_dir()
|
pyfileops/exceptions.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pyfileops.exceptions
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Custom exceptions for the pyfileops library.
|
|
5
|
+
All exceptions inherit from FileOpsError, which inherits from Exception.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FileOpsError(Exception):
|
|
10
|
+
"""Base exception for all pyfileops errors."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, message: str) -> None:
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
self.message = message
|
|
15
|
+
|
|
16
|
+
def __str__(self) -> str:
|
|
17
|
+
return self.message
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PathNotFoundError(FileOpsError):
|
|
21
|
+
"""Raised when a given path does not exist on the filesystem."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, path: str) -> None:
|
|
24
|
+
super().__init__(f"Path not found: '{path}'")
|
|
25
|
+
self.path = path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CopyError(FileOpsError):
|
|
29
|
+
"""Raised when a copy or move operation fails."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, source: str, destination: str, reason: str = "") -> None:
|
|
32
|
+
detail = f" — {reason}" if reason else ""
|
|
33
|
+
super().__init__(f"Could not copy '{source}' to '{destination}'{detail}")
|
|
34
|
+
self.source = source
|
|
35
|
+
self.destination = destination
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DeleteError(FileOpsError):
|
|
39
|
+
"""Raised when a delete operation fails."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, path: str, reason: str = "") -> None:
|
|
42
|
+
detail = f" — {reason}" if reason else ""
|
|
43
|
+
super().__init__(f"Could not delete '{path}'{detail}")
|
|
44
|
+
self.path = path
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ZipError(FileOpsError):
|
|
48
|
+
"""Raised when a zip or unzip operation fails."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, path: str, reason: str = "") -> None:
|
|
51
|
+
detail = f" — {reason}" if reason else ""
|
|
52
|
+
super().__init__(f"Zip operation failed for '{path}'{detail}")
|
|
53
|
+
self.path = path
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyfileops
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A simple, friendly Python library for file and directory operations.
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 pyfileops contributors
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Project-URL: Homepage, https://github.com/FefinDev/pyfileops
|
|
28
|
+
Project-URL: Issues, https://github.com/FefinDev/pyfileops/issues
|
|
29
|
+
Keywords: files,filesystem,copy,move,zip,utilities
|
|
30
|
+
Classifier: Development Status :: 3 - Alpha
|
|
31
|
+
Classifier: Intended Audience :: Developers
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Operating System :: OS Independent
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Topic :: System :: Filesystems
|
|
41
|
+
Classifier: Topic :: Utilities
|
|
42
|
+
Requires-Python: >=3.8
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
License-File: LICENSE
|
|
45
|
+
Dynamic: license-file
|
|
46
|
+
|
|
47
|
+
# pyfileops
|
|
48
|
+
|
|
49
|
+
**pyfileops** is a simple, friendly Python library for everyday file and directory operations.
|
|
50
|
+
No need to juggle `os`, `shutil`, `pathlib`, or `zipfile` — everything lives behind one clean object: `fs`.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from pyfileops import fs
|
|
54
|
+
|
|
55
|
+
fs.copy("report.pdf", "backup/")
|
|
56
|
+
fs.delete("temp.log")
|
|
57
|
+
print(fs.size("video.mp4"))
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
[](https://pypi.org/project/pyfileops/)
|
|
61
|
+
[](https://pypi.org/project/pyfileops/)
|
|
62
|
+
[](LICENSE)
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install pyfileops
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Requires **Python 3.8+**. No external dependencies — only the standard library.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Quickstart
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from pyfileops import fs
|
|
80
|
+
|
|
81
|
+
# ── Files ──────────────────────────────────────────────────────────────
|
|
82
|
+
fs.copy("notes.txt", "backup/") # copy into an existing folder
|
|
83
|
+
fs.copy("notes.txt", "backup/notes2.txt") # copy with a new name
|
|
84
|
+
fs.cut("draft.docx", "archive/") # move file
|
|
85
|
+
fs.rename("old.txt", "new.txt") # rename in place
|
|
86
|
+
fs.delete("trash.log") # delete a file
|
|
87
|
+
|
|
88
|
+
# ── Directories ────────────────────────────────────────────────────────
|
|
89
|
+
fs.mkdir("project/src/utils") # create (nested) directories
|
|
90
|
+
fs.rmdir("tmp") # remove directory and contents
|
|
91
|
+
|
|
92
|
+
# ── Compression ────────────────────────────────────────────────────────
|
|
93
|
+
fs.zip("project", "project_v1.zip") # zip a directory
|
|
94
|
+
fs.zip("report.pdf", "report.zip") # zip a single file
|
|
95
|
+
fs.unzip("project_v1.zip", "restored/") # extract a zip
|
|
96
|
+
|
|
97
|
+
# ── Queries ────────────────────────────────────────────────────────────
|
|
98
|
+
fs.exists("config.ini") # True or False
|
|
99
|
+
fs.size("video.mp4") # size in bytes (int)
|
|
100
|
+
fs.is_file("data.csv") # True or False
|
|
101
|
+
fs.is_dir("assets") # True or False
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## API Reference
|
|
107
|
+
|
|
108
|
+
### File Operations
|
|
109
|
+
|
|
110
|
+
#### `fs.copy(source, destination) → Path`
|
|
111
|
+
|
|
112
|
+
Copies a file **or** directory to `destination`.
|
|
113
|
+
|
|
114
|
+
- If `destination` is an **existing directory**, the source is placed inside it.
|
|
115
|
+
- If `destination` does **not** exist, it becomes the new file/folder name.
|
|
116
|
+
- Missing intermediate directories are created automatically.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
fs.copy("logo.png", "assets/") # → assets/logo.png
|
|
120
|
+
fs.copy("logo.png", "assets/icon.png") # → assets/icon.png
|
|
121
|
+
fs.copy("src/", "src_backup/") # copies whole directory tree
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
#### `fs.cut(source, destination) → Path`
|
|
127
|
+
|
|
128
|
+
Moves a file or directory to `destination` (copy + delete original).
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
fs.cut("uploads/photo.jpg", "gallery/")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
#### `fs.rename(path, new_name) → Path`
|
|
137
|
+
|
|
138
|
+
Renames a file or directory **in place** (same parent directory).
|
|
139
|
+
To move to a different location, use `cut()`.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
fs.rename("report_draft.pdf", "report_final.pdf")
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
#### `fs.delete(path) → None`
|
|
148
|
+
|
|
149
|
+
Deletes a file or an entire directory tree.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
fs.delete("temp.log") # file
|
|
153
|
+
fs.delete("cache/") # directory and all its contents
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### Directory Operations
|
|
159
|
+
|
|
160
|
+
#### `fs.mkdir(path) → Path`
|
|
161
|
+
|
|
162
|
+
Creates a directory (and any missing parent directories). Safe to call even if it already exists.
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
fs.mkdir("project/assets/images")
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
#### `fs.rmdir(path) → None`
|
|
171
|
+
|
|
172
|
+
Removes a directory and everything inside it.
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
fs.rmdir("build/")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
### Compression
|
|
181
|
+
|
|
182
|
+
#### `fs.zip(source, zip_file) → Path`
|
|
183
|
+
|
|
184
|
+
Compresses `source` (file or directory) into a ZIP archive at `zip_file`.
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
fs.zip("my_project", "my_project.zip")
|
|
188
|
+
fs.zip("data.csv", "data.zip")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
#### `fs.unzip(zip_file, destination) → Path`
|
|
194
|
+
|
|
195
|
+
Extracts a ZIP archive into `destination`.
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
fs.unzip("my_project.zip", "restored/")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### Queries
|
|
204
|
+
|
|
205
|
+
#### `fs.exists(path) → bool`
|
|
206
|
+
|
|
207
|
+
Returns `True` if `path` exists (file or directory).
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
if fs.exists("config.ini"):
|
|
211
|
+
print("Config found!")
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
#### `fs.size(path) → int`
|
|
217
|
+
|
|
218
|
+
Returns the size in **bytes**.
|
|
219
|
+
|
|
220
|
+
- For files: the file size.
|
|
221
|
+
- For directories: the total size of all contained files.
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
bytes_used = fs.size("video.mp4")
|
|
225
|
+
print(f"{bytes_used / 1_000_000:.2f} MB")
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
#### `fs.is_file(path) → bool`
|
|
231
|
+
|
|
232
|
+
Returns `True` if `path` is a regular file (returns `False` if it doesn't exist).
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
fs.is_file("README.md") # True
|
|
236
|
+
fs.is_file("src/") # False
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
#### `fs.is_dir(path) → bool`
|
|
242
|
+
|
|
243
|
+
Returns `True` if `path` is a directory (returns `False` if it doesn't exist).
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
fs.is_dir("src/") # True
|
|
247
|
+
fs.is_dir("README.md") # False
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Error Handling
|
|
253
|
+
|
|
254
|
+
pyfileops raises clear, specific exceptions so you always know what went wrong.
|
|
255
|
+
|
|
256
|
+
| Exception | When it's raised |
|
|
257
|
+
|---|---|
|
|
258
|
+
| `FileOpsError` | Base class for all pyfileops errors |
|
|
259
|
+
| `PathNotFoundError` | Source path does not exist |
|
|
260
|
+
| `CopyError` | A copy or move operation failed |
|
|
261
|
+
| `DeleteError` | A delete operation failed |
|
|
262
|
+
| `ZipError` | A zip or unzip operation failed |
|
|
263
|
+
|
|
264
|
+
All exceptions inherit from `FileOpsError`, which inherits from `Exception`.
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
from pyfileops import fs
|
|
268
|
+
from pyfileops import PathNotFoundError, CopyError, ZipError, FileOpsError
|
|
269
|
+
|
|
270
|
+
# Catch a specific error
|
|
271
|
+
try:
|
|
272
|
+
fs.copy("missing.txt", "backup/")
|
|
273
|
+
except PathNotFoundError as e:
|
|
274
|
+
print(e) # Path not found: 'missing.txt'
|
|
275
|
+
|
|
276
|
+
# Catch any pyfileops error
|
|
277
|
+
try:
|
|
278
|
+
fs.zip("project", "output.zip")
|
|
279
|
+
fs.delete("tmp/")
|
|
280
|
+
except FileOpsError as e:
|
|
281
|
+
print(f"Operation failed: {e}")
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Author
|
|
287
|
+
|
|
288
|
+
Made by [Fede](https://github.com/FefinDev) · [github.com/FefinDev/pyfileops](https://github.com/FefinDev/pyfileops)
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## License
|
|
293
|
+
|
|
294
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pyfileops/__init__.py,sha256=bo6-3FUPJ1yGF8RHcK7h_DNFTHHvKURiiwjDIVxHLjw,1154
|
|
2
|
+
pyfileops/core.py,sha256=Z8orC-NPiYDI11GGEQIzWk2XoMw0d03i4UMmLbwgTzI,11536
|
|
3
|
+
pyfileops/exceptions.py,sha256=QoFyBRflTO-POv_Qb5LV1yryMETysWhPFo4X2GcIOI0,1659
|
|
4
|
+
pyfileops-1.0.0.dist-info/licenses/LICENSE,sha256=v75mLQK-p_AKlCjarAHQtaa1Fsvyttnd5z3EAGftD5U,1100
|
|
5
|
+
pyfileops-1.0.0.dist-info/METADATA,sha256=CkhNGHBjwOSF5TMM4ntccew8mkOk8DPFnRmeyH7VoSo,8698
|
|
6
|
+
pyfileops-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
pyfileops-1.0.0.dist-info/top_level.txt,sha256=1NW7GQhLW4ykXExgMcdsLT_-Hpr_HSTquOK9rfpMurs,10
|
|
8
|
+
pyfileops-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pyfileops contributors
|
|
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 @@
|
|
|
1
|
+
pyfileops
|