kaparoo-python 0.1.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.
kaparoo/__about__.py ADDED
@@ -0,0 +1,3 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ __version__ = "0.1.0"
kaparoo/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,26 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ __all__ = (
4
+ "PosInt",
5
+ "NegInt",
6
+ "NonPosInt",
7
+ "NonNegInt",
8
+ "PositiveInt",
9
+ "NegativeInt",
10
+ "NonPositiveInt",
11
+ "NonNegativeInt",
12
+ )
13
+
14
+ from typing import Annotated, TypeAlias
15
+
16
+ from beartype.vale import Is
17
+
18
+ NegativeInt: TypeAlias = Annotated[int, Is[lambda x: x < 0]]
19
+ PositiveInt: TypeAlias = Annotated[int, Is[lambda x: x > 0]]
20
+ NonNegativeInt: TypeAlias = Annotated[int, Is[lambda x: x >= 0]]
21
+ NonPositiveInt: TypeAlias = Annotated[int, Is[lambda x: x <= 0]]
22
+
23
+ NegInt: TypeAlias = NegativeInt
24
+ PosInt: TypeAlias = PositiveInt
25
+ NonPosInt: TypeAlias = NonPositiveInt
26
+ NonNegInt: TypeAlias = NonNegativeInt
@@ -0,0 +1,51 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ __all__ = (
4
+ # exceptions
5
+ "DirectoryNotFoundError",
6
+ "NotAFileError",
7
+ # types
8
+ "StrPath",
9
+ "StrPaths",
10
+ # path
11
+ # - stringify
12
+ "stringify_path",
13
+ "stringify_paths",
14
+ # - single existence
15
+ "check_if_path_exists",
16
+ "check_if_file_exists",
17
+ "check_if_dir_exists",
18
+ # - multiple existences
19
+ "check_if_paths_exist",
20
+ "check_if_files_exist",
21
+ "check_if_dirs_exist",
22
+ # - child path(s) search
23
+ "get_paths",
24
+ "get_files",
25
+ "get_dirs",
26
+ # - empty directory check
27
+ "is_empty_dir",
28
+ "is_empty_dir_unsafe",
29
+ "are_empty_dirs",
30
+ "are_empty_dirs_unsafe",
31
+ )
32
+
33
+ from kaparoo.filesystem.exceptions import DirectoryNotFoundError, NotAFileError
34
+ from kaparoo.filesystem.path import (
35
+ are_empty_dirs,
36
+ are_empty_dirs_unsafe,
37
+ check_if_dir_exists,
38
+ check_if_dirs_exist,
39
+ check_if_file_exists,
40
+ check_if_files_exist,
41
+ check_if_path_exists,
42
+ check_if_paths_exist,
43
+ get_dirs,
44
+ get_files,
45
+ get_paths,
46
+ is_empty_dir,
47
+ is_empty_dir_unsafe,
48
+ stringify_path,
49
+ stringify_paths,
50
+ )
51
+ from kaparoo.filesystem.types import StrPath, StrPaths
@@ -0,0 +1,11 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ __all__ = ("NotAFileError", "DirectoryNotFoundError")
4
+
5
+
6
+ class NotAFileError(OSError):
7
+ pass
8
+
9
+
10
+ class DirectoryNotFoundError(FileNotFoundError):
11
+ pass
@@ -0,0 +1,760 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = (
6
+ # stringify
7
+ "stringify_path",
8
+ "stringify_paths",
9
+ # single existence
10
+ "check_if_path_exists",
11
+ "check_if_file_exists",
12
+ "check_if_dir_exists",
13
+ # multiple existences
14
+ "check_if_paths_exist",
15
+ "check_if_files_exist",
16
+ "check_if_dirs_exist",
17
+ # child path(s) search
18
+ "get_paths",
19
+ "get_files",
20
+ "get_dirs",
21
+ # empty directory check
22
+ "is_empty_dir",
23
+ "is_empty_dir_unsafe",
24
+ "are_empty_dirs",
25
+ "are_empty_dirs_unsafe",
26
+ )
27
+
28
+ import os
29
+ import random
30
+ from collections.abc import Sequence
31
+ from pathlib import Path
32
+ from typing import TYPE_CHECKING, overload
33
+
34
+ from kaparoo.filesystem.exceptions import DirectoryNotFoundError, NotAFileError
35
+
36
+ if TYPE_CHECKING:
37
+ from collections.abc import Callable
38
+ from typing import Literal
39
+
40
+ from kaparoo.filesystem.types import StrPath, StrPaths
41
+
42
+
43
+ # ========================== #
44
+ # Stringify #
45
+ # ========================== #
46
+
47
+
48
+ def stringify_path(path: StrPath) -> str:
49
+ """Convert a path to a string representation."""
50
+ return os.fspath(path)
51
+
52
+
53
+ def stringify_paths(paths: StrPaths) -> Sequence[str]:
54
+ """Convert a sequence of paths to a sequence of string representations."""
55
+ return [stringify_path(p) for p in paths]
56
+
57
+
58
+ # ========================== #
59
+ # Single Existence #
60
+ # ========================== #
61
+
62
+
63
+ @overload
64
+ def check_if_path_exists(path: StrPath, *, stringify: Literal[False] = False) -> Path:
65
+ ...
66
+
67
+
68
+ @overload
69
+ def check_if_path_exists(path: StrPath, *, stringify: Literal[True]) -> str:
70
+ ...
71
+
72
+
73
+ @overload
74
+ def check_if_path_exists(path: StrPath, *, stringify: bool) -> Path | str:
75
+ ...
76
+
77
+
78
+ def check_if_path_exists(path: StrPath, *, stringify: bool = False) -> Path | str:
79
+ """Check if a given path exists and return it as a `Path` object.
80
+
81
+ Args:
82
+ path: The path to check for existence.
83
+ stringify: Whether to return the path as a string. Defaults to `False`.
84
+
85
+ Returns:
86
+ The path as a `Path` object or a string, depending on the value of `stringify`.
87
+
88
+ Raises:
89
+ FileNotFoundError: If the path does not exist.
90
+ """
91
+
92
+ if not (path := Path(path)).exists():
93
+ raise FileNotFoundError(f"no such path: {path}")
94
+
95
+ return stringify_path(path) if stringify else path
96
+
97
+
98
+ @overload
99
+ def check_if_file_exists(path: StrPath, *, stringify: Literal[False] = False) -> Path:
100
+ ...
101
+
102
+
103
+ @overload
104
+ def check_if_file_exists(path: StrPath, *, stringify: Literal[True]) -> str:
105
+ ...
106
+
107
+
108
+ @overload
109
+ def check_if_file_exists(path: StrPath, *, stringify: bool) -> Path | str:
110
+ ...
111
+
112
+
113
+ def check_if_file_exists(path: StrPath, *, stringify: bool = False) -> Path | str:
114
+ """Check if a given path exists and is a file, and return it as a `Path` object.
115
+
116
+ Args:
117
+ path: The file path to check for existence.
118
+ stringify: Whether to return the path as a string. Defaults to `False`.
119
+
120
+ Returns:
121
+ The path as a `Path` object or a string, depending on the value of `stringify`.
122
+
123
+ Raises:
124
+ FileNotFoundError: If the path does not exist.
125
+ NotAFileError: If the path exists but is not a file.
126
+ """
127
+
128
+ if not (path := Path(path)).exists():
129
+ raise FileNotFoundError(f"no such file: {path}")
130
+ elif not path.is_file():
131
+ raise NotAFileError(f"not a file: {path}")
132
+
133
+ return stringify_path(path) if stringify else path
134
+
135
+
136
+ @overload
137
+ def check_if_dir_exists(
138
+ path: StrPath, *, make: bool | int = False, stringify: Literal[False] = False
139
+ ) -> Path:
140
+ ...
141
+
142
+
143
+ @overload
144
+ def check_if_dir_exists(
145
+ path: StrPath, *, make: bool | int = False, stringify: Literal[True]
146
+ ) -> str:
147
+ ...
148
+
149
+
150
+ @overload
151
+ def check_if_dir_exists(
152
+ path: StrPath, *, make: bool | int = False, stringify: bool
153
+ ) -> Path | str:
154
+ ...
155
+
156
+
157
+ def check_if_dir_exists(
158
+ path: StrPath, *, make: bool | int = False, stringify: bool = False
159
+ ) -> Path | str:
160
+ """Check if a given path exists and is a directory, and return it as a `Path` object.
161
+
162
+ Args:
163
+ path: The directory path to check for existence.
164
+ make: Whether to create the directory if it does not exist. If an `int` is provided,
165
+ use it as the octal mode for the directory. Defaults to `False`.
166
+ stringify: Whether to return the path as a string. Defaults to `False`.
167
+
168
+ Returns:
169
+ The path as a `Path` object or a string, depending on the value of `stringify`.
170
+
171
+ Raises:
172
+ DirectoryNotFoundError: If the path does not exist and `make` is False.
173
+ NotADirectoryError: If the path exists but is not a directory.
174
+ """ # noqa: E501
175
+
176
+ if not (path := Path(path)).exists():
177
+ if make is False:
178
+ raise DirectoryNotFoundError(f"no such directory: {path}")
179
+ path.mkdir(mode=511 if make is True else make, parents=True) # 511 == 0o777
180
+ elif not path.is_dir():
181
+ raise NotADirectoryError(f"not a directory: {path}")
182
+
183
+ return stringify_path(path) if stringify else path
184
+
185
+
186
+ # ========================== #
187
+ # Multiple Existences #
188
+ # ========================== #
189
+
190
+
191
+ @overload
192
+ def check_if_paths_exist(
193
+ paths: StrPaths,
194
+ *,
195
+ root: StrPath | None = None,
196
+ stringify: Literal[False] = False,
197
+ ) -> Sequence[Path]:
198
+ ...
199
+
200
+
201
+ @overload
202
+ def check_if_paths_exist(
203
+ paths: StrPaths, *, root: StrPath | None = None, stringify: Literal[True]
204
+ ) -> Sequence[str]:
205
+ ...
206
+
207
+
208
+ @overload
209
+ def check_if_paths_exist(
210
+ paths: StrPaths, *, root: StrPath | None = None, stringify: bool
211
+ ) -> Sequence[Path] | Sequence[str]:
212
+ ...
213
+
214
+
215
+ def check_if_paths_exist(
216
+ paths: StrPaths, *, root: StrPath | None = None, stringify: bool = False
217
+ ) -> Sequence[Path] | Sequence[str]:
218
+ """Check if multiple paths exist and return them as a sequence of `Path` objects.
219
+
220
+ Args:
221
+ paths: A sequence of paths to check for existence.
222
+ root: The root directory to resolve relative paths. If provided, the `paths`
223
+ will be resolved relative to the `root` directory. Defaults to `None`.
224
+ stringify: Whether to return a sequence of strings. Defaults to `False`.
225
+
226
+ Returns:
227
+ The paths as a sequence of `Path` objects or a sequence of strings,
228
+ depending on the value of `stringify`.
229
+
230
+ Raises:
231
+ DirectoryNotFoundError: If the `root` directory does not exist.
232
+ NotADirectoryError: If `root` exists but is not a directory.
233
+ FileNotFoundError: If any of the paths does not exist.
234
+ """
235
+
236
+ if root is not None:
237
+ root = check_if_dir_exists(root)
238
+ paths = [root / p for p in paths]
239
+
240
+ paths = [check_if_path_exists(p) for p in paths]
241
+
242
+ return stringify_paths(paths) if stringify else paths
243
+
244
+
245
+ @overload
246
+ def check_if_files_exist(
247
+ paths: StrPaths,
248
+ *,
249
+ root: StrPath | None = None,
250
+ stringify: Literal[False] = False,
251
+ ) -> Sequence[Path]:
252
+ ...
253
+
254
+
255
+ @overload
256
+ def check_if_files_exist(
257
+ paths: StrPaths, *, root: StrPath | None = None, stringify: Literal[True]
258
+ ) -> Sequence[str]:
259
+ ...
260
+
261
+
262
+ @overload
263
+ def check_if_files_exist(
264
+ paths: StrPaths, *, root: StrPath | None = None, stringify: bool
265
+ ) -> Sequence[Path] | Sequence[str]:
266
+ ...
267
+
268
+
269
+ def check_if_files_exist(
270
+ paths: StrPaths, *, root: StrPath | None = None, stringify: bool = False
271
+ ) -> Sequence[Path] | Sequence[str]:
272
+ """Check if multiple files exist and return them as a sequence of `Path` objects.
273
+
274
+ Args:
275
+ paths: A sequence of file paths to check for existence.
276
+ root: The root directory to resolve relative paths. If provided, the `paths`
277
+ will be resolved relative to the `root` directory. Defaults to `None`.
278
+ stringify: Whether to return a sequence of strings. Defaults to `False`.
279
+
280
+ Returns:
281
+ The file paths as a sequence of `Path` objects or a sequence of strings,
282
+ depending on the value of `stringify`.
283
+
284
+ Raises:
285
+ DirectoryNotFoundError: If the `root` directory does not exist.
286
+ NotADirectoryError: If `root` exists but is not a directory.
287
+ FileNotFoundError: If any of the file paths does not exist.
288
+ NotAFileError: If any of the paths exists but is not a file.
289
+ """ # noqa: E501
290
+
291
+ if root is not None:
292
+ root = check_if_dir_exists(root)
293
+ paths = [root / p for p in paths]
294
+
295
+ paths = [check_if_file_exists(p) for p in paths]
296
+
297
+ return stringify_paths(paths) if stringify else paths
298
+
299
+
300
+ @overload
301
+ def check_if_dirs_exist(
302
+ paths: StrPaths,
303
+ *,
304
+ root: StrPath | None = None,
305
+ make: bool | int = False,
306
+ stringify: Literal[False] = False,
307
+ ) -> Sequence[Path]:
308
+ ...
309
+
310
+
311
+ @overload
312
+ def check_if_dirs_exist(
313
+ paths: StrPaths,
314
+ *,
315
+ root: StrPath | None = None,
316
+ make: bool | int = False,
317
+ stringify: Literal[True],
318
+ ) -> Sequence[str]:
319
+ ...
320
+
321
+
322
+ @overload
323
+ def check_if_dirs_exist(
324
+ paths: StrPaths,
325
+ *,
326
+ root: StrPath | None = None,
327
+ make: bool | int = False,
328
+ stringify: bool,
329
+ ) -> Sequence[Path] | Sequence[str]:
330
+ ...
331
+
332
+
333
+ def check_if_dirs_exist(
334
+ paths: StrPaths,
335
+ *,
336
+ root: StrPath | None = None,
337
+ make: bool | int = False,
338
+ stringify: bool = False,
339
+ ) -> Sequence[Path] | Sequence[str]:
340
+ """Check if multiple directories exist and return them as a sequence of `Path` objects.
341
+
342
+ Args:
343
+ paths: A sequence of directory paths to check for existence.
344
+ root: The root directory to resolve relative paths. If provided, the `paths`
345
+ will be resolved relative to the `root` directory. Defaults to `None`.
346
+ make: Whether to create the directory if it does not exist. If an `int` is provided,
347
+ use it as the octal mode for the directory. Defaults to `False`.
348
+ stringify: Whether to return a sequence of strings. Defaults to `False`.
349
+
350
+ Returns:
351
+ The directory paths as a sequence of `Path` objects or a sequence of strings,
352
+ depending on the value of `stringify`.
353
+
354
+ Raises:
355
+ DirectoryNotFoundError: If the `root` directory does not exist
356
+ DirectoryNotFoundError: If any of the directory paths does not exist.
357
+ NotADirectoryError: If `root` exists but is not a directory.
358
+ NotADirectoryError: If any of the paths exists but is not a directory.
359
+ """ # noqa: E501
360
+
361
+ if root is not None:
362
+ root = check_if_dir_exists(root)
363
+ paths = [root / p for p in paths]
364
+
365
+ paths = [check_if_dir_exists(p, make=make) for p in paths]
366
+
367
+ return stringify_paths(paths) if stringify else paths
368
+
369
+
370
+ # ========================== #
371
+ # Child Path(s) Search #
372
+ # ========================== #
373
+
374
+
375
+ @overload
376
+ def get_paths(
377
+ root: StrPath,
378
+ *,
379
+ pattern: str | None = None,
380
+ num_samples: int | None = None,
381
+ ignores: StrPaths | None = None,
382
+ condition: Callable[[Path], bool] | None = None,
383
+ recursive: bool = True,
384
+ stringify: Literal[False] = False,
385
+ ) -> Sequence[Path]:
386
+ ...
387
+
388
+
389
+ @overload
390
+ def get_paths(
391
+ root: StrPath,
392
+ *,
393
+ pattern: str | None = None,
394
+ num_samples: int | None = None,
395
+ ignores: StrPaths | None = None,
396
+ condition: Callable[[Path], bool] | None = None,
397
+ recursive: bool = True,
398
+ stringify: Literal[True],
399
+ ) -> Sequence[str]:
400
+ ...
401
+
402
+
403
+ @overload
404
+ def get_paths(
405
+ root: StrPath,
406
+ *,
407
+ pattern: str | None = None,
408
+ num_samples: int | None = None,
409
+ ignores: StrPaths | None = None,
410
+ condition: Callable[[Path], bool] | None = None,
411
+ recursive: bool = True,
412
+ stringify: bool,
413
+ ) -> Sequence[Path] | Sequence[str]:
414
+ ...
415
+
416
+
417
+ def get_paths(
418
+ root: StrPath,
419
+ *,
420
+ pattern: str | None = None,
421
+ num_samples: int | None = None,
422
+ ignores: StrPaths | None = None,
423
+ condition: Callable[[Path], bool] | None = None,
424
+ recursive: bool = True,
425
+ stringify: bool = False,
426
+ ) -> Sequence[Path] | Sequence[str]:
427
+ """Get paths of files or directories in a given directory.
428
+
429
+ Args:
430
+ root: The directory to search for paths.
431
+ pattern: A glob pattern to match the paths against. Defaults to `None` and
432
+ automatically uses "*" to list all paths included in the `root` directory.
433
+ num_samples: A maximum number of paths to return. If given and its value is
434
+ smaller than the total number of paths, only the `num_samples` paths of the
435
+ total are randomly selected and returned. Hence, even using the same value
436
+ of `num_samples`, may return a different result. Defaults to `None`.
437
+ ignores: A sequence of paths to ignore. If any path in `ignores` does not start
438
+ with `root`, it is treated as a relative path. For example, `any/path` is
439
+ treated as `root/any/path`. Defaults to `None`.
440
+ condition: A predicate that takes a `Path` object and decides whether to include
441
+ the path in the results. Defaults to `None`.
442
+ recursive: Whether to search for paths recursively in subdirectories of the
443
+ `root` directory. Defaults to `True`.
444
+ stringify: Whether to return a sequence of strings. Defaults to `False`.
445
+
446
+ Returns:
447
+ The paths that match the specified criteria as a sequence of `Path` objects or a
448
+ sequence of strings, depending on the value of `stringify`.
449
+
450
+ Raises:
451
+ DirectoryNotFoundError: If `root` does not exist.
452
+ NotADirectoryError: If `root` exists but is not a directory.
453
+ ValueError: If `num_samples` is not a positive int.
454
+ """ # noqa: E501
455
+
456
+ root = check_if_dir_exists(root)
457
+
458
+ if not isinstance(pattern, str):
459
+ pattern = "*"
460
+
461
+ matched = root.rglob(pattern) if recursive else root.glob(pattern)
462
+ paths = [p for p in matched]
463
+
464
+ if root in paths:
465
+ paths.remove(root)
466
+
467
+ if not ignores:
468
+ ignores = []
469
+
470
+ for ignore in ignores:
471
+ ignore_path = Path(ignore)
472
+ if root not in ignore_path.parents:
473
+ ignore_path = root / ignore_path
474
+
475
+ if ignore_path in paths:
476
+ paths.remove(ignore_path)
477
+
478
+ if callable(condition):
479
+ paths = [p for p in paths if condition(p)]
480
+
481
+ if isinstance(num_samples, int) and num_samples < len(paths):
482
+ if num_samples <= 0:
483
+ raise ValueError("`num_samples` must be a positive int")
484
+ paths = random.sample(paths, num_samples)
485
+
486
+ return stringify_paths(paths) if stringify else paths
487
+
488
+
489
+ @overload
490
+ def get_files(
491
+ root: StrPath,
492
+ *,
493
+ pattern: str | None = None,
494
+ num_samples: int | None = None,
495
+ ignores: StrPaths | None = None,
496
+ condition: Callable[[Path], bool] | None = None,
497
+ recursive: bool = True,
498
+ stringify: Literal[False] = False,
499
+ ) -> Sequence[Path]:
500
+ ...
501
+
502
+
503
+ @overload
504
+ def get_files(
505
+ root: StrPath,
506
+ *,
507
+ pattern: str | None = None,
508
+ num_samples: int | None = None,
509
+ ignores: StrPaths | None = None,
510
+ condition: Callable[[Path], bool] | None = None,
511
+ recursive: bool = True,
512
+ stringify: Literal[True],
513
+ ) -> Sequence[str]:
514
+ ...
515
+
516
+
517
+ @overload
518
+ def get_files(
519
+ root: StrPath,
520
+ *,
521
+ pattern: str | None = None,
522
+ num_samples: int | None = None,
523
+ ignores: StrPaths | None = None,
524
+ condition: Callable[[Path], bool] | None = None,
525
+ recursive: bool = True,
526
+ stringify: bool,
527
+ ) -> Sequence[Path] | Sequence[str]:
528
+ ...
529
+
530
+
531
+ def get_files(
532
+ root: StrPath,
533
+ *,
534
+ pattern: str | None = None,
535
+ num_samples: int | None = None,
536
+ ignores: StrPaths | None = None,
537
+ condition: Callable[[Path], bool] | None = None,
538
+ recursive: bool = True,
539
+ stringify: bool = False,
540
+ ) -> Sequence[Path] | Sequence[str]:
541
+ """Get paths of files in a given directory.
542
+
543
+ Args:
544
+ root: The directory to search for paths.
545
+ pattern: A glob pattern to match the paths against. Defaults to `None` and
546
+ automatically uses "*" to list all paths included in the `root` directory.
547
+ num_samples: A maximum number of paths to return. If given and its value is
548
+ smaller than the total number of paths, only the `num_samples` paths of the
549
+ total are randomly selected and returned. Hence, even using the same value
550
+ of `num_samples`, may return a different result. Defaults to `None`.
551
+ ignores: A sequence of paths to ignore. If any path in `ignores` does not start
552
+ with `root`, it is treated as a relative path. For example, `any/path` is
553
+ treated as `root/any/path`. Defaults to `None`.
554
+ condition: A predicate that takes a `Path` object and decides whether to include
555
+ the path in the results. Defaults to `None`.
556
+ recursive: Whether to search for paths recursively in subdirectories of the
557
+ `root` directory. Defaults to `True`.
558
+ stringify: Whether to return a sequence of strings. Defaults to `False`.
559
+
560
+ Returns:
561
+ The file paths that match the specified criteria as a sequence of `Path` objects
562
+ or a sequence of strings, depending on the value of `stringify`.
563
+
564
+ Raises:
565
+ DirectoryNotFoundError: If `root` does not exist.
566
+ NotADirectoryError: If `root` exists but is not a directory.
567
+ ValueError: If `num_samples` is not a positive int.
568
+ """ # noqa: E501
569
+
570
+ if not callable(condition):
571
+ file_condition = lambda p: p.is_file() # noqa: E731
572
+ else:
573
+ file_condition = lambda p: p.is_file() and condition(p) # type: ignore[misc] # noqa: E501, E731
574
+
575
+ file_paths = get_paths(
576
+ root,
577
+ pattern=pattern,
578
+ num_samples=num_samples,
579
+ ignores=ignores,
580
+ condition=file_condition,
581
+ recursive=recursive,
582
+ stringify=stringify,
583
+ )
584
+
585
+ return file_paths
586
+
587
+
588
+ @overload
589
+ def get_dirs(
590
+ root: StrPath,
591
+ *,
592
+ pattern: str | None = None,
593
+ num_samples: int | None = None,
594
+ ignores: StrPaths | None = None,
595
+ condition: Callable[[Path], bool] | None = None,
596
+ recursive: bool = True,
597
+ stringify: Literal[False] = False,
598
+ ) -> Sequence[Path]:
599
+ ...
600
+
601
+
602
+ @overload
603
+ def get_dirs(
604
+ root: StrPath,
605
+ *,
606
+ pattern: str | None = None,
607
+ num_samples: int | None = None,
608
+ ignores: StrPaths | None = None,
609
+ condition: Callable[[Path], bool] | None = None,
610
+ recursive: bool = True,
611
+ stringify: Literal[True],
612
+ ) -> Sequence[str]:
613
+ ...
614
+
615
+
616
+ @overload
617
+ def get_dirs(
618
+ root: StrPath,
619
+ *,
620
+ pattern: str | None = None,
621
+ num_samples: int | None = None,
622
+ ignores: StrPaths | None = None,
623
+ condition: Callable[[Path], bool] | None = None,
624
+ recursive: bool = True,
625
+ stringify: bool,
626
+ ) -> Sequence[Path] | Sequence[str]:
627
+ ...
628
+
629
+
630
+ def get_dirs(
631
+ root: StrPath,
632
+ *,
633
+ pattern: str | None = None,
634
+ num_samples: int | None = None,
635
+ ignores: StrPaths | None = None,
636
+ condition: Callable[[Path], bool] | None = None,
637
+ recursive: bool = True,
638
+ stringify: bool = False,
639
+ ) -> Sequence[Path] | Sequence[str]:
640
+ """Get paths of directories in a given directory.
641
+
642
+ Args:
643
+ root: The directory to search for paths.
644
+ pattern: A glob pattern to match the paths against. Defaults to `None` and
645
+ automatically uses "*" to list all paths included in the `root` directory.
646
+ num_samples: A maximum number of paths to return. If given and its value is
647
+ smaller than the total number of paths, only the `num_samples` paths of the
648
+ total are randomly selected and returned. Hence, even using the same value
649
+ of `num_samples`, may return a different result. Defaults to `None`.
650
+ ignores: A sequence of paths to ignore. If any path in `ignores` does not start
651
+ with `root`, it is treated as a relative path. For example, `any/path` is
652
+ treated as `root/any/path`. Defaults to `None`.
653
+ condition: A predicate that takes a `Path` object and decides whether to include
654
+ the path in the results. Defaults to `None`.
655
+ recursive: Whether to search for paths recursively in subdirectories of the
656
+ `root` directory. Defaults to `True`.
657
+ stringify: Whether to return a sequence of strings. Defaults to `False`.
658
+
659
+ Returns:
660
+ The directory paths that match the specified criteria as a sequence of `Path`
661
+ objects or a sequence of strings, depending on the value of `stringify`.
662
+
663
+ Raises:
664
+ DirectoryNotFoundError: If `root` does not exist.
665
+ NotADirectoryError: If `root` exists but is not a directory.
666
+ ValueError: If `num_samples` is not a positive int.
667
+ """ # noqa: E501
668
+
669
+ if not callable(condition):
670
+ dir_condition = lambda p: p.is_dir() # noqa: E731
671
+ else:
672
+ dir_condition = lambda p: p.is_dir() and condition(p) # type: ignore[misc] # noqa: E501, E731
673
+
674
+ dir_paths = get_paths(
675
+ root,
676
+ pattern=pattern,
677
+ num_samples=num_samples,
678
+ ignores=ignores,
679
+ condition=dir_condition,
680
+ recursive=recursive,
681
+ stringify=stringify,
682
+ )
683
+
684
+ return dir_paths
685
+
686
+
687
+ # ========================== #
688
+ # Empty Directory Check #
689
+ # ========================== #
690
+
691
+
692
+ def is_empty_dir_unsafe(path: StrPath) -> bool:
693
+ """Check if a directory is empty.
694
+
695
+ Unlike the function `is_empty_dir()`, this function does not check the existence of
696
+ the input argument `path`. Use this function only if you are sure it exists.
697
+
698
+ Args:
699
+ path: The path to the directory.
700
+
701
+ Returns:
702
+ A boolean indicating whether the directory is empty.
703
+ """
704
+ return not os.listdir(path)
705
+
706
+
707
+ def is_empty_dir(path: StrPath) -> bool:
708
+ """Check if a directory is empty.
709
+
710
+ Args:
711
+ path: The path to the directory.
712
+
713
+ Returns:
714
+ A boolean indicating whether the directory is empty.
715
+
716
+ Raises:
717
+ DirectoryNotFoundError: If the directory does not exist.
718
+ NotADirectoryError: If `path` exists but is not a directory.
719
+ """
720
+ path = check_if_dir_exists(path)
721
+ return is_empty_dir_unsafe(path)
722
+
723
+
724
+ def are_empty_dirs_unsafe(paths: StrPaths, *, root: StrPath | None = None) -> bool:
725
+ """Check if multiple directories are empty.
726
+
727
+ Unlike the function `are_empty_dirs()`, this function does not check the existence
728
+ of the input arguments `paths` and `root`. Use this function only if you are sure
729
+ they exist.
730
+
731
+ Args:
732
+ paths: A sequence of directory paths to check.
733
+ root: The root directory to resolve relative paths. Defaults to `None`.
734
+
735
+ Returns:
736
+ A boolean indicating whether all directories are empty.
737
+ """
738
+ if root is not None:
739
+ paths = [os.path.join(root, p) for p in paths]
740
+ return all(is_empty_dir_unsafe(p) for p in paths)
741
+
742
+
743
+ def are_empty_dirs(paths: StrPaths, *, root: StrPath | None = None) -> bool:
744
+ """Check if multiple directories are empty.
745
+
746
+ Args:
747
+ paths: A sequence of directory paths to check.
748
+ root: The root directory to resolve relative paths. Defaults to `None`.
749
+
750
+ Returns:
751
+ A boolean indicating whether all directories are empty.
752
+
753
+ Raises:
754
+ DirectoryNotFoundError: If the `root` directory does not exist.
755
+ DirectoryNotFoundError: If any of the directory paths does not exist.
756
+ NotADirectoryError: If `root` exists but is not a directory.
757
+ NotADirectoryError: If any of the paths exists but is not a directory.
758
+ """
759
+ paths = check_if_dirs_exist(paths, root=root)
760
+ return all(is_empty_dir_unsafe(p) for p in paths)
@@ -0,0 +1,10 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ __all__ = ("StrPath", "StrPaths")
4
+
5
+ from collections.abc import Sequence
6
+ from os import PathLike
7
+ from typing import TypeAlias
8
+
9
+ StrPath: TypeAlias = str | PathLike[str]
10
+ StrPaths: TypeAlias = Sequence[StrPath]
kaparoo/py.typed ADDED
File without changes
File without changes
@@ -0,0 +1,112 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = (
6
+ "replace_if_none",
7
+ "factory_if_none",
8
+ "unwrap_or_default",
9
+ "unwrap_or_factory",
10
+ )
11
+
12
+ from typing import TYPE_CHECKING, overload
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Callable
16
+
17
+ from kaparoo.utils.types import T, U
18
+
19
+
20
+ @overload
21
+ def replace_if_none(optional: None, surrogate: U) -> U:
22
+ ...
23
+
24
+
25
+ @overload
26
+ def replace_if_none(optional: T, surrogate: U) -> T:
27
+ ...
28
+
29
+
30
+ def replace_if_none(optional: T | None, surrogate: U) -> T | U:
31
+ """Replace the value with a surrogate if it is None.
32
+
33
+ Args:
34
+ optional: The optional value to be checked.
35
+ surrogate: The surrogate value to use if `optional` is None.
36
+
37
+ Returns:
38
+ The `optional` value if it is not None, otherwise the `surrogate` value.
39
+ """
40
+ return surrogate if optional is None else optional
41
+
42
+
43
+ @overload
44
+ def factory_if_none(
45
+ optional: None,
46
+ factory: Callable[[], U],
47
+ ) -> U:
48
+ ...
49
+
50
+
51
+ @overload
52
+ def factory_if_none(
53
+ optional: T,
54
+ factory: Callable[[], U],
55
+ ) -> T:
56
+ ...
57
+
58
+
59
+ def factory_if_none(
60
+ optional: T | None,
61
+ factory: Callable[[], U],
62
+ ) -> T | U:
63
+ """Create a value using a factory if the optional value is None.
64
+
65
+ Args:
66
+ optional: The optional value to be checked.
67
+ factory: A callable that returns the value to be used if `optional` is None.
68
+
69
+ Returns:
70
+ The `optional` value if it is not None, otherwise the value returned by `factory`.
71
+ """ # noqa: E501
72
+ return factory() if optional is None else optional
73
+
74
+
75
+ def unwrap_or_default(
76
+ optional: T | None,
77
+ default: T,
78
+ callback: Callable[[T], T] | None = None,
79
+ ) -> T:
80
+ """Unwrap the value or return a default value if it is None.
81
+
82
+ Args:
83
+ optional: The optional value to be checked.
84
+ default: The default value to be returned if `optional` is None.
85
+ callback: An optional callable to be applied to the result. Defaults to None.
86
+
87
+ Returns:
88
+ The `optional` value if it is not None, otherwise the `default` value.
89
+ If a `callback` is provided, it is applied to the result before returning.
90
+ """
91
+ result = replace_if_none(optional, default)
92
+ return callback(result) if callable(callback) else result
93
+
94
+
95
+ def unwrap_or_factory(
96
+ optional: T | None,
97
+ factory: Callable[[], T],
98
+ callback: Callable[[T], T] | None = None,
99
+ ) -> T:
100
+ """Unwrap the value or create a value using a factory if it is None.
101
+
102
+ Args:
103
+ optional: The optional value to be checked.
104
+ factory: A callable that returns the value to be used if `optional` is None.
105
+ callback: An optional callable to be applied to the result. Defaults to None.
106
+
107
+ Returns:
108
+ The `optional` value if it is not None, otherwise the value returned by `factory`.
109
+ If a `callback` is provided, it is applied to the result before returning.
110
+ """ # noqa: E501
111
+ result = factory_if_none(optional, factory)
112
+ return callback(result) if callable(callback) else result
kaparoo/utils/types.py ADDED
@@ -0,0 +1,15 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from typing import TypeVar
4
+
5
+ # type variables
6
+ T = TypeVar("T")
7
+ U = TypeVar("U")
8
+ K = TypeVar("K")
9
+ V = TypeVar("V")
10
+
11
+ # covariant type variables
12
+ T_co = TypeVar("T_co", covariant=True)
13
+ U_co = TypeVar("U_co", covariant=True)
14
+ K_co = TypeVar("K_co", covariant=True)
15
+ V_co = TypeVar("V_co", covariant=True)
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.1
2
+ Name: kaparoo-python
3
+ Version: 0.1.0
4
+ Summary: A Python package for (personally) common and useful features.
5
+ Project-URL: GitHub, https://www.github.com/kaparoo/python-package
6
+ Author-email: Jaewoo Park <kaparoo2001@gmail.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2023 Jaewoo Park
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ License-File: LICENSE
29
+ Classifier: Development Status :: 4 - Beta
30
+ Classifier: Intended Audience :: Developers
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Operating System :: OS Independent
33
+ Classifier: Programming Language :: Python
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3 :: Only
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: Implementation :: CPython
38
+ Classifier: Typing :: Typed
39
+ Requires-Python: >=3.10
40
+ Requires-Dist: beartype>=0.14.1
41
+ Provides-Extra: dev
42
+ Requires-Dist: black>=23.3.0; extra == 'dev'
43
+ Requires-Dist: hatch>=1.7.0; extra == 'dev'
44
+ Requires-Dist: mypy>=1.3.0; extra == 'dev'
45
+ Requires-Dist: pytest-order>=1.1.0; extra == 'dev'
46
+ Requires-Dist: pytest>=7.3.2; extra == 'dev'
47
+ Requires-Dist: ruff>=0.0.274; extra == 'dev'
48
+ Description-Content-Type: text/markdown
49
+
50
+ # ***kaparoo-python-package***
51
+
52
+
53
+
54
+ <!-- [ MARKDOWN BADGES ] -->
55
+
56
+ [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
57
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
58
+ [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch)
59
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
60
+ [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
61
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
62
+ [![Gitomoji](https://img.shields.io/badge/gitmoji-%20😜%20😍-FFDD67.svg?style=flat-square")](https://gitmoji.dev)
63
+
64
+ <!-- [ END ] -->
65
+
66
+
67
+
68
+ <!-- [ TABLE OF CONTENTS ] -->
69
+
70
+ <details>
71
+ <summary><strong>Table of Contents</strong></summary>
72
+
73
+ - [***kaparoo-python-package***](#kaparoo-python-package)
74
+ - [:wave: **Overview**](#wave-overview)
75
+ - [:balance\_scale: **License**](#balance_scale-license)
76
+
77
+ </details>
78
+
79
+ <!-- [ END ] -->
80
+
81
+
82
+
83
+ ## :wave: **Overview**
84
+
85
+ A Python package for (personally) common and useful features.
86
+
87
+ ## :balance_scale: **License**
88
+
89
+ This project is distributed under the terms of [MIT](./LICENSE) license.
@@ -0,0 +1,16 @@
1
+ kaparoo/__about__.py,sha256=b2fCZ20Exwr_01zDxnOFRvxbDylQLSzlPJRJj2l7ZmI,50
2
+ kaparoo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ kaparoo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ kaparoo/beartype/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ kaparoo/beartype/numerics.py,sha256=rDTmnuOGhBW7wQ_9vesHqx7sHkELIxyaUwuq9TQIHa4,674
6
+ kaparoo/filesystem/__init__.py,sha256=K9KUnRcFXZQQRTEcP5jALce0QGn2XSN0gtf7558I6iQ,1206
7
+ kaparoo/filesystem/exceptions.py,sha256=E9IOyZdA37QefamPrG9QMV9afMcmMf3zNyKk8n4Aewk,191
8
+ kaparoo/filesystem/path.py,sha256=FYN4alF1f3ubA72fdKhGxvDtAhyNrFyAKDZeDag8y3M,23982
9
+ kaparoo/filesystem/types.py,sha256=qISJYLhd8rW7nOdH5GQJEPiwkxwQD5b75zZi8Fowzqg,242
10
+ kaparoo/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ kaparoo/utils/optional.py,sha256=oX05_K04xSx1mKqiDra5V4pIm8F7h_1dKtJV03ojCis,3134
12
+ kaparoo/utils/types.py,sha256=5LNQkhtVO9QLRGHcSc6hplwPxiEeA9U_J4SSYIKQfGY,337
13
+ kaparoo_python-0.1.0.dist-info/METADATA,sha256=QSerqz89GVxr52Y7cdr3gK8NHuPndolW1aYPZQteMa4,3876
14
+ kaparoo_python-0.1.0.dist-info/WHEEL,sha256=9QBuHhg6FNW7lppboF2vKVbCGTVzsFykgRQjjlajrhA,87
15
+ kaparoo_python-0.1.0.dist-info/licenses/LICENSE,sha256=_6aA_CoB_YWkSfqkGFxghm2GIsBRE2rp_g7vhz11DM8,1087
16
+ kaparoo_python-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.18.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Jaewoo Park
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.