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 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()
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/pyfileops)](https://pypi.org/project/pyfileops/)
61
+ [![Python](https://img.shields.io/pypi/pyversions/pyfileops)](https://pypi.org/project/pyfileops/)
62
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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