python-fsutil 0.14.1__py3-none-any.whl → 0.15.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.
fsutil/__init__.py CHANGED
@@ -1,27 +1,51 @@
1
1
  from __future__ import annotations
2
2
 
3
- import glob
4
- import hashlib
5
- import json
6
- import os
7
- import pathlib
8
- import re
9
- import shutil
10
- import tarfile
11
- import tempfile
12
- import uuid
13
- import zipfile
14
- from datetime import datetime
15
- from typing import Any, Callable, Generator, Iterable, Union
16
- from urllib.parse import urlsplit
17
-
18
- try:
19
- import requests
20
-
21
- requests_installed = True
22
- except ImportError:
23
- requests_installed = False
24
-
3
+ from fsutil.archives import (
4
+ create_tar_file,
5
+ create_zip_file,
6
+ extract_tar_file,
7
+ extract_zip_file,
8
+ )
9
+ from fsutil.checks import (
10
+ assert_dir,
11
+ assert_exists,
12
+ assert_file,
13
+ assert_not_dir,
14
+ assert_not_exists,
15
+ assert_not_file,
16
+ exists,
17
+ is_dir,
18
+ is_empty,
19
+ is_empty_dir,
20
+ is_empty_file,
21
+ is_file,
22
+ )
23
+ from fsutil.converters import convert_size_bytes_to_string, convert_size_string_to_bytes
24
+ from fsutil.info import (
25
+ get_dir_creation_date,
26
+ get_dir_creation_date_formatted,
27
+ get_dir_hash,
28
+ get_dir_last_modified_date,
29
+ get_dir_last_modified_date_formatted,
30
+ get_dir_size,
31
+ get_dir_size_formatted,
32
+ get_file_creation_date,
33
+ get_file_creation_date_formatted,
34
+ get_file_hash,
35
+ get_file_last_modified_date,
36
+ get_file_last_modified_date_formatted,
37
+ get_file_size,
38
+ get_file_size_formatted,
39
+ )
40
+ from fsutil.io import (
41
+ read_file,
42
+ read_file_from_url,
43
+ read_file_json,
44
+ read_file_lines,
45
+ read_file_lines_count,
46
+ write_file,
47
+ write_file_json,
48
+ )
25
49
  from fsutil.metadata import (
26
50
  __author__,
27
51
  __copyright__,
@@ -31,6 +55,54 @@ from fsutil.metadata import (
31
55
  __title__,
32
56
  __version__,
33
57
  )
58
+ from fsutil.operations import (
59
+ clean_dir,
60
+ copy_dir,
61
+ copy_dir_content,
62
+ copy_file,
63
+ create_dir,
64
+ create_file,
65
+ delete_dir,
66
+ delete_dir_content,
67
+ delete_dirs,
68
+ delete_file,
69
+ delete_files,
70
+ download_file,
71
+ list_dirs,
72
+ list_files,
73
+ make_dirs,
74
+ make_dirs_for_file,
75
+ move_dir,
76
+ move_file,
77
+ remove_dir,
78
+ remove_dir_content,
79
+ remove_dirs,
80
+ remove_file,
81
+ remove_files,
82
+ rename_dir,
83
+ rename_file,
84
+ rename_file_basename,
85
+ rename_file_extension,
86
+ replace_dir,
87
+ replace_file,
88
+ search_dirs,
89
+ search_files,
90
+ )
91
+ from fsutil.paths import (
92
+ get_file_basename,
93
+ get_file_extension,
94
+ get_filename,
95
+ get_parent_dir,
96
+ get_unique_name,
97
+ join_filename,
98
+ join_filepath,
99
+ join_path,
100
+ split_filename,
101
+ split_filepath,
102
+ split_path,
103
+ transform_filepath,
104
+ )
105
+ from fsutil.perms import get_permissions, set_permissions
34
106
 
35
107
  __all__ = [
36
108
  "__author__",
@@ -54,6 +126,7 @@ __all__ = [
54
126
  "copy_file",
55
127
  "create_dir",
56
128
  "create_file",
129
+ "create_tar_file",
57
130
  "create_zip_file",
58
131
  "delete_dir",
59
132
  "delete_dir_content",
@@ -62,9 +135,11 @@ __all__ = [
62
135
  "delete_files",
63
136
  "download_file",
64
137
  "exists",
138
+ "extract_tar_file",
65
139
  "extract_zip_file",
66
140
  "get_dir_creation_date",
67
141
  "get_dir_creation_date_formatted",
142
+ "get_dir_hash",
68
143
  "get_dir_last_modified_date",
69
144
  "get_dir_last_modified_date_formatted",
70
145
  "get_dir_size",
@@ -118,1378 +193,7 @@ __all__ = [
118
193
  "split_filename",
119
194
  "split_filepath",
120
195
  "split_path",
196
+ "transform_filepath",
121
197
  "write_file",
122
198
  "write_file_json",
123
199
  ]
124
-
125
- PathIn = Union[str, pathlib.Path]
126
-
127
- SIZE_UNITS = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
128
-
129
-
130
- def _require_requests_installed() -> None:
131
- if not requests_installed:
132
- raise ModuleNotFoundError(
133
- "'requests' module is not installed, "
134
- "it can be installed by running: 'pip install requests'"
135
- )
136
-
137
-
138
- def _get_path(path: PathIn) -> str:
139
- if isinstance(path, str):
140
- return path
141
- return str(path)
142
-
143
-
144
- def assert_dir(path: PathIn) -> None:
145
- """
146
- Raise an OSError if the given path doesn't exist or it is not a directory.
147
- """
148
- path = _get_path(path)
149
- if not is_dir(path):
150
- raise OSError(f"Invalid directory path: {path}")
151
-
152
-
153
- def assert_exists(path: PathIn) -> None:
154
- """
155
- Raise an OSError if the given path doesn't exist.
156
- """
157
- path = _get_path(path)
158
- if not exists(path):
159
- raise OSError(f"Invalid item path: {path}")
160
-
161
-
162
- def assert_file(path: PathIn) -> None:
163
- """
164
- Raise an OSError if the given path doesn't exist or it is not a file.
165
- """
166
- path = _get_path(path)
167
- if not is_file(path):
168
- raise OSError(f"Invalid file path: {path}")
169
-
170
-
171
- def assert_not_dir(path: PathIn) -> None:
172
- """
173
- Raise an OSError if the given path is an existing directory.
174
- """
175
- path = _get_path(path)
176
- if is_dir(path):
177
- raise OSError(f"Invalid path, directory already exists: {path}")
178
-
179
-
180
- def assert_not_exists(path: PathIn) -> None:
181
- """
182
- Raise an OSError if the given path already exists.
183
- """
184
- path = _get_path(path)
185
- if exists(path):
186
- raise OSError(f"Invalid path, item already exists: {path}")
187
-
188
-
189
- def assert_not_file(path: PathIn) -> None:
190
- """
191
- Raise an OSError if the given path is an existing file.
192
- """
193
- path = _get_path(path)
194
- if is_file(path):
195
- raise OSError(f"Invalid path, file already exists: {path}")
196
-
197
-
198
- def _clean_dir_empty_dirs(path: PathIn) -> None:
199
- path = _get_path(path)
200
- for basepath, dirnames, _ in os.walk(path, topdown=False):
201
- for dirname in dirnames:
202
- dirpath = os.path.join(basepath, dirname)
203
- if is_empty_dir(dirpath):
204
- remove_dir(dirpath)
205
-
206
-
207
- def _clean_dir_empty_files(path: PathIn) -> None:
208
- path = _get_path(path)
209
- for basepath, _, filenames in os.walk(path, topdown=False):
210
- for filename in filenames:
211
- filepath = os.path.join(basepath, filename)
212
- if is_empty_file(filepath):
213
- remove_file(filepath)
214
-
215
-
216
- def clean_dir(path: PathIn, *, dirs: bool = True, files: bool = True) -> None:
217
- """
218
- Clean a directory by removing empty directories and/or empty files.
219
- """
220
- path = _get_path(path)
221
- assert_dir(path)
222
- if files:
223
- _clean_dir_empty_files(path)
224
- if dirs:
225
- _clean_dir_empty_dirs(path)
226
-
227
-
228
- def convert_size_bytes_to_string(size: int) -> str:
229
- """
230
- Convert the given size bytes to string using the right unit suffix.
231
- """
232
- size_num = float(size)
233
- units = SIZE_UNITS
234
- factor = 0
235
- factor_limit = len(units) - 1
236
- while (size_num >= 1024) and (factor <= factor_limit):
237
- size_num /= 1024
238
- factor += 1
239
- size_units = units[factor]
240
- size_str = f"{size_num:.2f}" if (factor > 1) else f"{size_num:.0f}"
241
- size_str = f"{size_str} {size_units}"
242
- return size_str
243
-
244
-
245
- def convert_size_string_to_bytes(size: str) -> float | int:
246
- """
247
- Convert the given size string to bytes.
248
- """
249
- units = [item.lower() for item in SIZE_UNITS]
250
- parts = size.strip().replace(" ", " ").split(" ")
251
- amount = float(parts[0])
252
- unit = parts[1]
253
- factor = units.index(unit.lower())
254
- if not factor:
255
- return amount
256
- return int((1024**factor) * amount)
257
-
258
-
259
- def copy_dir(
260
- path: PathIn, dest: PathIn, *, overwrite: bool = False, **kwargs: Any
261
- ) -> None:
262
- """
263
- Copy the directory at the given path and all its content to dest path.
264
- If overwrite is not allowed and dest path exists, an OSError is raised.
265
- More informations about kwargs supported options here:
266
- https://docs.python.org/3/library/shutil.html#shutil.copytree
267
- """
268
- path = _get_path(path)
269
- dest = _get_path(dest)
270
- assert_dir(path)
271
- dirname = os.path.basename(os.path.normpath(path))
272
- dest = os.path.join(dest, dirname)
273
- assert_not_file(dest)
274
- if not overwrite:
275
- assert_not_exists(dest)
276
- copy_dir_content(path, dest, **kwargs)
277
-
278
-
279
- def copy_dir_content(path: PathIn, dest: PathIn, **kwargs: Any) -> None:
280
- """
281
- Copy the content of the directory at the given path to dest path.
282
- More informations about kwargs supported options here:
283
- https://docs.python.org/3/library/shutil.html#shutil.copytree
284
- """
285
- path = _get_path(path)
286
- dest = _get_path(dest)
287
- assert_dir(path)
288
- assert_not_file(dest)
289
- make_dirs(dest)
290
- kwargs.setdefault("dirs_exist_ok", True)
291
- shutil.copytree(path, dest, **kwargs)
292
-
293
-
294
- def copy_file(
295
- path: PathIn, dest: PathIn, *, overwrite: bool = False, **kwargs: Any
296
- ) -> None:
297
- """
298
- Copy the file at the given path and its metadata to dest path.
299
- If overwrite is not allowed and dest path exists, an OSError is raised.
300
- More informations about kwargs supported options here:
301
- https://docs.python.org/3/library/shutil.html#shutil.copy2
302
- """
303
- path = _get_path(path)
304
- dest = _get_path(dest)
305
- assert_file(path)
306
- assert_not_dir(dest)
307
- if not overwrite:
308
- assert_not_exists(dest)
309
- make_dirs_for_file(dest)
310
- shutil.copy2(path, dest, **kwargs)
311
-
312
-
313
- def create_dir(path: PathIn, *, overwrite: bool = False) -> None:
314
- """
315
- Create directory at the given path.
316
- If overwrite is not allowed and path exists, an OSError is raised.
317
- """
318
- path = _get_path(path)
319
- assert_not_file(path)
320
- if not overwrite:
321
- assert_not_exists(path)
322
- make_dirs(path)
323
-
324
-
325
- def create_file(path: PathIn, content: str = "", *, overwrite: bool = False) -> None:
326
- """
327
- Create file with the specified content at the given path.
328
- If overwrite is not allowed and path exists, an OSError is raised.
329
- """
330
- path = _get_path(path)
331
- assert_not_dir(path)
332
- if not overwrite:
333
- assert_not_exists(path)
334
- write_file(path, content)
335
-
336
-
337
- def create_tar_file(
338
- path: PathIn,
339
- content_paths: list[PathIn],
340
- *,
341
- overwrite: bool = True,
342
- compression: str = "",
343
- ) -> None:
344
- """
345
- Create tar file at path compressing directories/files listed in content_paths.
346
- If overwrite is allowed and dest tar already exists, it will be overwritten.
347
- """
348
- path = _get_path(path)
349
- assert_not_dir(path)
350
- if not overwrite:
351
- assert_not_exists(path)
352
- make_dirs_for_file(path)
353
-
354
- def _write_content_to_tar_file(
355
- file: tarfile.TarFile, content_path: PathIn, basedir: str = ""
356
- ) -> None:
357
- path = _get_path(content_path)
358
- assert_exists(path)
359
- if is_file(path):
360
- filename = get_filename(path)
361
- filepath = join_path(basedir, filename)
362
- file.add(path, filepath)
363
- elif is_dir(path):
364
- for item_name in os.listdir(path):
365
- item_path = join_path(path, item_name)
366
- item_basedir = (
367
- join_path(basedir, item_name) if is_dir(item_path) else basedir
368
- )
369
- _write_content_to_tar_file(file, item_path, item_basedir)
370
-
371
- compression = f"w:{compression}" if compression else "w"
372
- with tarfile.open(path, compression) as file:
373
- for content_path in content_paths:
374
- _write_content_to_tar_file(file, content_path)
375
-
376
-
377
- def create_zip_file(
378
- path: PathIn,
379
- content_paths: list[PathIn],
380
- *,
381
- overwrite: bool = True,
382
- compression: int = zipfile.ZIP_DEFLATED,
383
- ) -> None:
384
- """
385
- Create zip file at path compressing directories/files listed in content_paths.
386
- If overwrite is allowed and dest zip already exists, it will be overwritten.
387
- """
388
- path = _get_path(path)
389
- assert_not_dir(path)
390
- if not overwrite:
391
- assert_not_exists(path)
392
- make_dirs_for_file(path)
393
-
394
- def _write_content_to_zip_file(
395
- file: zipfile.ZipFile, content_path: PathIn, basedir: str = ""
396
- ) -> None:
397
- path = _get_path(content_path)
398
- assert_exists(path)
399
- if is_file(path):
400
- filename = get_filename(path)
401
- filepath = join_path(basedir, filename)
402
- file.write(path, filepath)
403
- elif is_dir(path):
404
- for item_name in os.listdir(path):
405
- item_path = join_path(path, item_name)
406
- item_basedir = (
407
- join_path(basedir, item_name) if is_dir(item_path) else basedir
408
- )
409
- _write_content_to_zip_file(file, item_path, item_basedir)
410
-
411
- with zipfile.ZipFile(path, "w", compression) as file:
412
- for content_path in content_paths:
413
- _write_content_to_zip_file(file, content_path)
414
-
415
-
416
- def delete_dir(path: PathIn) -> bool:
417
- """
418
- Alias for remove_dir.
419
- """
420
- removed = remove_dir(path)
421
- return removed
422
-
423
-
424
- def delete_dir_content(path: PathIn) -> None:
425
- """
426
- Alias for remove_dir_content.
427
- """
428
- remove_dir_content(path)
429
-
430
-
431
- def delete_dirs(*paths: PathIn) -> None:
432
- """
433
- Alias for remove_dirs.
434
- """
435
- remove_dirs(*paths)
436
-
437
-
438
- def delete_file(path: PathIn) -> bool:
439
- """
440
- Alias for remove_file.
441
- """
442
- removed = remove_file(path)
443
- return removed
444
-
445
-
446
- def delete_files(*paths: PathIn) -> None:
447
- """
448
- Alias for remove_files.
449
- """
450
- remove_files(*paths)
451
-
452
-
453
- def download_file(
454
- url: str,
455
- *,
456
- dirpath: PathIn | None = None,
457
- filename: str | None = None,
458
- chunk_size: int = 8192,
459
- **kwargs: Any,
460
- ) -> str:
461
- """
462
- Download a file from url to dirpath.
463
- If dirpath is not provided, the file will be downloaded to a temp directory.
464
- If filename is provided, the file will be named using filename.
465
- It is possible to pass extra request options
466
- (eg. for authentication) using **kwargs.
467
- """
468
- _require_requests_installed()
469
- # https://stackoverflow.com/a/16696317/2096218
470
-
471
- kwargs["stream"] = True
472
- with requests.get(url, **kwargs) as response:
473
- response.raise_for_status()
474
-
475
- # build filename
476
- if not filename:
477
- # detect filename from headers
478
- content_disposition = response.headers.get("content-disposition", "") or ""
479
- filename_pattern = r'filename="(.*)"'
480
- filename_match = re.search(filename_pattern, content_disposition)
481
- if filename_match:
482
- filename = filename_match.group(1)
483
- # or detect filename from url
484
- if not filename:
485
- filename = get_filename(url)
486
- # or fallback to a unique name
487
- if not filename:
488
- filename_uuid = str(uuid.uuid4())
489
- filename = f"download-{filename_uuid}"
490
-
491
- # build filepath
492
- dirpath = dirpath or tempfile.gettempdir()
493
- dirpath = _get_path(dirpath)
494
- filepath = join_path(dirpath, filename)
495
- make_dirs_for_file(filepath)
496
-
497
- # write file to disk
498
- with open(filepath, "wb") as file:
499
- for chunk in response.iter_content(chunk_size=chunk_size):
500
- if chunk:
501
- file.write(chunk)
502
- return filepath
503
-
504
-
505
- def exists(path: PathIn) -> bool:
506
- """
507
- Check if a directory of a file exists at the given path.
508
- """
509
- path = _get_path(path)
510
- return os.path.exists(path)
511
-
512
-
513
- def extract_tar_file(
514
- path: PathIn,
515
- dest: PathIn,
516
- *,
517
- autodelete: bool = False,
518
- content_paths: Iterable[tarfile.TarInfo] | None = None,
519
- ) -> None:
520
- """
521
- Extract tar file at path to dest path.
522
- If autodelete, the archive will be deleted after extraction.
523
- If content_paths list is defined,
524
- only listed items will be extracted, otherwise all.
525
- """
526
- path = _get_path(path)
527
- dest = _get_path(dest)
528
- assert_file(path)
529
- assert_not_file(dest)
530
- make_dirs(dest)
531
- with tarfile.TarFile(path, "r") as file:
532
- file.extractall(dest, members=content_paths)
533
- if autodelete:
534
- remove_file(path)
535
-
536
-
537
- def extract_zip_file(
538
- path: PathIn,
539
- dest: PathIn,
540
- *,
541
- autodelete: bool = False,
542
- content_paths: Iterable[str | zipfile.ZipInfo] | None = None,
543
- ) -> None:
544
- """
545
- Extract zip file at path to dest path.
546
- If autodelete, the archive will be deleted after extraction.
547
- If content_paths list is defined,
548
- only listed items will be extracted, otherwise all.
549
- """
550
- path = _get_path(path)
551
- dest = _get_path(dest)
552
- assert_file(path)
553
- assert_not_file(dest)
554
- make_dirs(dest)
555
- with zipfile.ZipFile(path, "r") as file:
556
- file.extractall(dest, members=content_paths)
557
- if autodelete:
558
- remove_file(path)
559
-
560
-
561
- def _filter_paths(
562
- basepath: str,
563
- relpaths: list[str],
564
- *,
565
- predicate: Callable[[str], bool] | None = None,
566
- ) -> list[str]:
567
- """
568
- Filter paths relative to basepath according to the optional predicate function.
569
- If predicate is defined, paths are filtered using it,
570
- otherwise all paths will be listed.
571
- """
572
- paths = []
573
- for relpath in relpaths:
574
- abspath = os.path.join(basepath, relpath)
575
- if predicate is None or predicate(abspath):
576
- paths.append(abspath)
577
- paths.sort()
578
- return paths
579
-
580
-
581
- def get_dir_creation_date(path: PathIn) -> datetime:
582
- """
583
- Get the directory creation date.
584
- """
585
- path = _get_path(path)
586
- assert_dir(path)
587
- creation_timestamp = os.path.getctime(path)
588
- creation_date = datetime.fromtimestamp(creation_timestamp)
589
- return creation_date
590
-
591
-
592
- def get_dir_creation_date_formatted(
593
- path: PathIn, *, format: str = "%Y-%m-%d %H:%M:%S"
594
- ) -> str:
595
- """
596
- Get the directory creation date formatted using the given format.
597
- """
598
- path = _get_path(path)
599
- date = get_dir_creation_date(path)
600
- return date.strftime(format)
601
-
602
-
603
- def get_dir_hash(path: PathIn, *, func: str = "md5") -> str:
604
- """
605
- Get the hash of the directory at the given path using
606
- the specified algorithm function (md5 by default).
607
- """
608
- path = _get_path(path)
609
- assert_dir(path)
610
- hash = hashlib.new(func)
611
- files = search_files(path)
612
- for file in sorted(files):
613
- file_hash = get_file_hash(file, func=func)
614
- file_hash_b = bytes(file_hash, "utf-8")
615
- hash.update(file_hash_b)
616
- hash_hex = hash.hexdigest()
617
- return hash_hex
618
-
619
-
620
- def get_dir_last_modified_date(path: PathIn) -> datetime:
621
- """
622
- Get the directory last modification date.
623
- """
624
- path = _get_path(path)
625
- assert_dir(path)
626
- last_modified_timestamp = os.path.getmtime(path)
627
- for basepath, dirnames, filenames in os.walk(path):
628
- for dirname in dirnames:
629
- dirpath = os.path.join(basepath, dirname)
630
- last_modified_timestamp = max(
631
- last_modified_timestamp, os.path.getmtime(dirpath)
632
- )
633
- for filename in filenames:
634
- filepath = os.path.join(basepath, filename)
635
- last_modified_timestamp = max(
636
- last_modified_timestamp, os.path.getmtime(filepath)
637
- )
638
- last_modified_date = datetime.fromtimestamp(last_modified_timestamp)
639
- return last_modified_date
640
-
641
-
642
- def get_dir_last_modified_date_formatted(
643
- path: PathIn, *, format: str = "%Y-%m-%d %H:%M:%S"
644
- ) -> str:
645
- """
646
- Get the directory last modification date formatted using the given format.
647
- """
648
- path = _get_path(path)
649
- date = get_dir_last_modified_date(path)
650
- return date.strftime(format)
651
-
652
-
653
- def get_dir_size(path: PathIn) -> int:
654
- """
655
- Get the directory size in bytes.
656
- """
657
- path = _get_path(path)
658
- assert_dir(path)
659
- size = 0
660
- for basepath, _, filenames in os.walk(path):
661
- for filename in filenames:
662
- filepath = os.path.join(basepath, filename)
663
- if not os.path.islink(filepath):
664
- size += get_file_size(filepath)
665
- return size
666
-
667
-
668
- def get_dir_size_formatted(path: PathIn) -> str:
669
- """
670
- Get the directory size formatted using the right unit suffix.
671
- """
672
- size = get_dir_size(path)
673
- size_formatted = convert_size_bytes_to_string(size)
674
- return size_formatted
675
-
676
-
677
- def get_file_basename(path: PathIn) -> str:
678
- """
679
- Get the file basename from the given path/url.
680
- """
681
- path = _get_path(path)
682
- basename, _ = split_filename(path)
683
- return basename
684
-
685
-
686
- def get_file_creation_date(path: PathIn) -> datetime:
687
- """
688
- Get the file creation date.
689
- """
690
- path = _get_path(path)
691
- assert_file(path)
692
- creation_timestamp = os.path.getctime(path)
693
- creation_date = datetime.fromtimestamp(creation_timestamp)
694
- return creation_date
695
-
696
-
697
- def get_file_creation_date_formatted(
698
- path: PathIn, *, format: str = "%Y-%m-%d %H:%M:%S"
699
- ) -> str:
700
- """
701
- Get the file creation date formatted using the given format.
702
- """
703
- path = _get_path(path)
704
- date = get_file_creation_date(path)
705
- return date.strftime(format)
706
-
707
-
708
- def get_file_extension(path: PathIn) -> str:
709
- """
710
- Get the file extension from the given path/url.
711
- """
712
- path = _get_path(path)
713
- _, extension = split_filename(path)
714
- return extension
715
-
716
-
717
- def get_file_hash(path: PathIn, *, func: str = "md5") -> str:
718
- """
719
- Get the hash of the file at the given path using
720
- the specified algorithm function (md5 by default).
721
- """
722
- path = _get_path(path)
723
- assert_file(path)
724
- hash = hashlib.new(func)
725
- with open(path, "rb") as file:
726
- for chunk in iter(lambda: file.read(4096), b""):
727
- hash.update(chunk)
728
- hash_hex = hash.hexdigest()
729
- return hash_hex
730
-
731
-
732
- def get_file_last_modified_date(path: PathIn) -> datetime:
733
- """
734
- Get the file last modification date.
735
- """
736
- path = _get_path(path)
737
- assert_file(path)
738
- last_modified_timestamp = os.path.getmtime(path)
739
- last_modified_date = datetime.fromtimestamp(last_modified_timestamp)
740
- return last_modified_date
741
-
742
-
743
- def get_file_last_modified_date_formatted(
744
- path: PathIn, *, format: str = "%Y-%m-%d %H:%M:%S"
745
- ) -> str:
746
- """
747
- Get the file last modification date formatted using the given format.
748
- """
749
- path = _get_path(path)
750
- date = get_file_last_modified_date(path)
751
- return date.strftime(format)
752
-
753
-
754
- def get_file_size(path: PathIn) -> int:
755
- """
756
- Get the directory size in bytes.
757
- """
758
- path = _get_path(path)
759
- assert_file(path)
760
- # size = os.stat(path).st_size
761
- size = os.path.getsize(path)
762
- return size
763
-
764
-
765
- def get_file_size_formatted(path: PathIn) -> str:
766
- """
767
- Get the directory size formatted using the right unit suffix.
768
- """
769
- path = _get_path(path)
770
- size = get_file_size(path)
771
- size_formatted = convert_size_bytes_to_string(size)
772
- return size_formatted
773
-
774
-
775
- def get_filename(path: PathIn) -> str:
776
- """
777
- Get the filename from the given path/url.
778
- """
779
- path = _get_path(path)
780
- filepath = urlsplit(path).path
781
- filename = os.path.basename(filepath)
782
- return filename
783
-
784
-
785
- def get_parent_dir(path: PathIn, *, levels: int = 1) -> str:
786
- """
787
- Get the parent directory for the given path going up N levels.
788
- """
789
- path = _get_path(path)
790
- return join_path(path, *([os.pardir] * max(1, levels)))
791
-
792
-
793
- def get_permissions(path: PathIn) -> int:
794
- """
795
- Get the file/directory permissions.
796
- """
797
- path = _get_path(path)
798
- assert_exists(path)
799
- st_mode = os.stat(path).st_mode
800
- permissions = int(str(oct(st_mode & 0o777))[2:])
801
- return permissions
802
-
803
-
804
- def get_unique_name(
805
- path: PathIn,
806
- *,
807
- prefix: str = "",
808
- suffix: str = "",
809
- extension: str = "",
810
- separator: str = "-",
811
- ) -> str:
812
- """
813
- Get a unique name for a directory/file ath the given directory path.
814
- """
815
- path = _get_path(path)
816
- assert_dir(path)
817
- name = ""
818
- while True:
819
- if prefix:
820
- name += f"{prefix}{separator}"
821
- uid = uuid.uuid4()
822
- name += f"{uid}"
823
- if suffix:
824
- name += f"{separator}{suffix}"
825
- if extension:
826
- extension = extension.lstrip(".").lower()
827
- name += f".{extension}"
828
- if exists(join_path(path, name)):
829
- continue
830
- break
831
- return name
832
-
833
-
834
- def is_dir(path: PathIn) -> bool:
835
- """
836
- Determine whether the specified path represents an existing directory.
837
- """
838
- path = _get_path(path)
839
- return os.path.isdir(path)
840
-
841
-
842
- def is_empty(path: PathIn) -> bool:
843
- """
844
- Determine whether the specified path represents an empty directory or an empty file.
845
- """
846
- path = _get_path(path)
847
- assert_exists(path)
848
- if is_dir(path):
849
- return is_empty_dir(path)
850
- return is_empty_file(path)
851
-
852
-
853
- def is_empty_dir(path: PathIn) -> bool:
854
- """
855
- Determine whether the specified path represents an empty directory.
856
- """
857
- path = _get_path(path)
858
- assert_dir(path)
859
- return len(os.listdir(path)) == 0
860
-
861
-
862
- def is_empty_file(path: PathIn) -> bool:
863
- """
864
- Determine whether the specified path represents an empty file.
865
- """
866
- path = _get_path(path)
867
- return get_file_size(path) == 0
868
-
869
-
870
- def is_file(path: PathIn) -> bool:
871
- """
872
- Determine whether the specified path represents an existing file.
873
- """
874
- path = _get_path(path)
875
- return os.path.isfile(path)
876
-
877
-
878
- def join_filename(basename: str, extension: str) -> str:
879
- """
880
- Create a filename joining the file basename and the extension.
881
- """
882
- basename = basename.rstrip(".").strip()
883
- extension = extension.replace(".", "").strip()
884
- if basename and extension:
885
- filename = f"{basename}.{extension}"
886
- return filename
887
- return basename or extension
888
-
889
-
890
- def join_filepath(dirpath: PathIn, filename: str) -> str:
891
- """
892
- Create a filepath joining the directory path and the filename.
893
- """
894
- dirpath = _get_path(dirpath)
895
- return join_path(dirpath, filename)
896
-
897
-
898
- def join_path(path: PathIn, *paths: PathIn) -> str:
899
- """
900
- Create a path joining path and paths.
901
- If path is __file__ (or a .py file), the resulting path will be relative
902
- to the directory path of the module in which it's used.
903
- """
904
- path = _get_path(path)
905
- basepath = path
906
- if get_file_extension(path) in ["py", "pyc", "pyo"]:
907
- basepath = os.path.dirname(os.path.realpath(path))
908
- paths_str = [_get_path(path).lstrip(os.sep) for path in paths]
909
- return os.path.normpath(os.path.join(basepath, *paths_str))
910
-
911
-
912
- def list_dirs(path: PathIn) -> list[str]:
913
- """
914
- List all directories contained at the given directory path.
915
- """
916
- path = _get_path(path)
917
- return _filter_paths(path, os.listdir(path), predicate=is_dir)
918
-
919
-
920
- def list_files(path: PathIn) -> list[str]:
921
- """
922
- List all files contained at the given directory path.
923
- """
924
- path = _get_path(path)
925
- return _filter_paths(path, os.listdir(path), predicate=is_file)
926
-
927
-
928
- def make_dirs(path: PathIn) -> None:
929
- """
930
- Create the directories needed to ensure that the given path exists.
931
- If a file already exists at the given path an OSError is raised.
932
- """
933
- path = _get_path(path)
934
- if is_dir(path):
935
- return
936
- assert_not_file(path)
937
- os.makedirs(path, exist_ok=True)
938
-
939
-
940
- def make_dirs_for_file(path: PathIn) -> None:
941
- """
942
- Create the directories needed to ensure that the given path exists.
943
- If a directory already exists at the given path an OSError is raised.
944
- """
945
- path = _get_path(path)
946
- if is_file(path):
947
- return
948
- assert_not_dir(path)
949
- dirpath, _ = split_filepath(path)
950
- if dirpath:
951
- make_dirs(dirpath)
952
-
953
-
954
- def move_dir(
955
- path: PathIn, dest: PathIn, *, overwrite: bool = False, **kwargs: Any
956
- ) -> None:
957
- """
958
- Move an existing dir from path to dest directory.
959
- If overwrite is not allowed and dest path exists, an OSError is raised.
960
- More informations about kwargs supported options here:
961
- https://docs.python.org/3/library/shutil.html#shutil.move
962
- """
963
- path = _get_path(path)
964
- dest = _get_path(dest)
965
- assert_dir(path)
966
- assert_not_file(dest)
967
- if not overwrite:
968
- assert_not_exists(dest)
969
- make_dirs(dest)
970
- shutil.move(path, dest, **kwargs)
971
-
972
-
973
- def move_file(
974
- path: PathIn, dest: PathIn, *, overwrite: bool = False, **kwargs: Any
975
- ) -> None:
976
- """
977
- Move an existing file from path to dest directory.
978
- If overwrite is not allowed and dest path exists, an OSError is raised.
979
- More informations about kwargs supported options here:
980
- https://docs.python.org/3/library/shutil.html#shutil.move
981
- """
982
- path = _get_path(path)
983
- dest = _get_path(dest)
984
- assert_file(path)
985
- assert_not_file(dest)
986
- dest = os.path.join(dest, get_filename(path))
987
- assert_not_dir(dest)
988
- if not overwrite:
989
- assert_not_exists(dest)
990
- make_dirs_for_file(dest)
991
- shutil.move(path, dest, **kwargs)
992
-
993
-
994
- def read_file(path: PathIn, *, encoding: str = "utf-8") -> str:
995
- """
996
- Read the content of the file at the given path using the specified encoding.
997
- """
998
- path = _get_path(path)
999
- assert_file(path)
1000
- content = ""
1001
- with open(path, encoding=encoding) as file:
1002
- content = file.read()
1003
- return content
1004
-
1005
-
1006
- def read_file_from_url(url: str, **kwargs: Any) -> str:
1007
- """
1008
- Read the content of the file at the given url.
1009
- """
1010
- _require_requests_installed()
1011
- response = requests.get(url, **kwargs)
1012
- response.raise_for_status()
1013
- content = response.text
1014
- return content
1015
-
1016
-
1017
- def read_file_json(path: PathIn, **kwargs: Any) -> Any:
1018
- """
1019
- Read and decode a json encoded file at the given path.
1020
- """
1021
- path = _get_path(path)
1022
- content = read_file(path)
1023
- data = json.loads(content, **kwargs)
1024
- return data
1025
-
1026
-
1027
- def _read_file_lines_in_range(
1028
- path: PathIn,
1029
- *,
1030
- line_start: int = 0,
1031
- line_end: int = -1,
1032
- encoding: str = "utf-8",
1033
- ) -> Generator[str, None, None]:
1034
- path = _get_path(path)
1035
- line_start_negative = line_start < 0
1036
- line_end_negative = line_end < 0
1037
- if line_start_negative or line_end_negative:
1038
- # pre-calculate lines count only if using negative line indexes
1039
- lines_count = read_file_lines_count(path)
1040
- # normalize negative indexes
1041
- if line_start_negative:
1042
- line_start = max(0, line_start + lines_count)
1043
- if line_end_negative:
1044
- line_end = min(line_end + lines_count, lines_count - 1)
1045
- with open(path, "rb") as file:
1046
- file.seek(0)
1047
- line_index = 0
1048
- for line in file:
1049
- if line_index >= line_start and line_index <= line_end:
1050
- yield line.decode(encoding)
1051
- line_index += 1
1052
-
1053
-
1054
- def read_file_lines(
1055
- path: PathIn,
1056
- *,
1057
- line_start: int = 0,
1058
- line_end: int = -1,
1059
- strip_white: bool = True,
1060
- skip_empty: bool = True,
1061
- encoding: str = "utf-8",
1062
- ) -> list[str]:
1063
- """
1064
- Read file content lines.
1065
- It is possible to specify the line indexes (negative indexes too),
1066
- very useful especially when reading large files.
1067
- """
1068
- path = _get_path(path)
1069
- assert_file(path)
1070
- if line_start == 0 and line_end == -1:
1071
- content = read_file(path, encoding=encoding)
1072
- lines = content.splitlines()
1073
- else:
1074
- lines = list(
1075
- _read_file_lines_in_range(
1076
- path,
1077
- line_start=line_start,
1078
- line_end=line_end,
1079
- encoding=encoding,
1080
- )
1081
- )
1082
- if strip_white:
1083
- lines = [line.strip() for line in lines]
1084
- if skip_empty:
1085
- lines = [line for line in lines if line]
1086
- return lines
1087
-
1088
-
1089
- def read_file_lines_count(path: PathIn) -> int:
1090
- """
1091
- Read file lines count.
1092
- """
1093
- path = _get_path(path)
1094
- assert_file(path)
1095
- lines_count = 0
1096
- with open(path, "rb") as file:
1097
- file.seek(0)
1098
- lines_count = sum(1 for line in file)
1099
- return lines_count
1100
-
1101
-
1102
- def remove_dir(path: PathIn, **kwargs: Any) -> bool:
1103
- """
1104
- Remove a directory at the given path and all its content.
1105
- If the directory is removed with success returns True, otherwise False.
1106
- More informations about kwargs supported options here:
1107
- https://docs.python.org/3/library/shutil.html#shutil.rmtree
1108
- """
1109
- path = _get_path(path)
1110
- if not exists(path):
1111
- return False
1112
- assert_dir(path)
1113
- shutil.rmtree(path, **kwargs)
1114
- return not exists(path)
1115
-
1116
-
1117
- def remove_dir_content(path: PathIn) -> None:
1118
- """
1119
- Removes all directory content (both sub-directories and files).
1120
- """
1121
- path = _get_path(path)
1122
- assert_dir(path)
1123
- remove_dirs(*list_dirs(path))
1124
- remove_files(*list_files(path))
1125
-
1126
-
1127
- def remove_dirs(*paths: PathIn) -> None:
1128
- """
1129
- Remove multiple directories at the given paths and all their content.
1130
- """
1131
- for path in paths:
1132
- remove_dir(path)
1133
-
1134
-
1135
- def remove_file(path: PathIn) -> bool:
1136
- """
1137
- Remove a file at the given path.
1138
- If the file is removed with success returns True, otherwise False.
1139
- """
1140
- path = _get_path(path)
1141
- if not exists(path):
1142
- return False
1143
- assert_file(path)
1144
- os.remove(path)
1145
- return not exists(path)
1146
-
1147
-
1148
- def remove_files(*paths: PathIn) -> None:
1149
- """
1150
- Remove multiple files at the given paths.
1151
- """
1152
- for path in paths:
1153
- remove_file(path)
1154
-
1155
-
1156
- def rename_dir(path: PathIn, name: str) -> None:
1157
- """
1158
- Rename a directory with the given name.
1159
- If a directory or a file with the given name already exists, an OSError is raised.
1160
- """
1161
- path = _get_path(path)
1162
- assert_dir(path)
1163
- comps = list(os.path.split(path))
1164
- comps[-1] = name
1165
- dest = os.path.join(*comps)
1166
- assert_not_exists(dest)
1167
- os.rename(path, dest)
1168
-
1169
-
1170
- def rename_file(path: PathIn, name: str) -> None:
1171
- """
1172
- Rename a file with the given name.
1173
- If a directory or a file with the given name already exists, an OSError is raised.
1174
- """
1175
- path = _get_path(path)
1176
- assert_file(path)
1177
- dirpath, filename = split_filepath(path)
1178
- dest = join_filepath(dirpath, name)
1179
- assert_not_exists(dest)
1180
- os.rename(path, dest)
1181
-
1182
-
1183
- def rename_file_basename(path: PathIn, basename: str) -> None:
1184
- """
1185
- Rename a file basename with the given basename.
1186
- """
1187
- path = _get_path(path)
1188
- extension = get_file_extension(path)
1189
- filename = join_filename(basename, extension)
1190
- rename_file(path, filename)
1191
-
1192
-
1193
- def rename_file_extension(path: PathIn, extension: str) -> None:
1194
- """
1195
- Rename a file extension with the given extension.
1196
- """
1197
- path = _get_path(path)
1198
- basename = get_file_basename(path)
1199
- filename = join_filename(basename, extension)
1200
- rename_file(path, filename)
1201
-
1202
-
1203
- def replace_dir(path: PathIn, src: PathIn, *, autodelete: bool = False) -> None:
1204
- """
1205
- Replace directory at the specified path with the directory located at src.
1206
- If autodelete, the src directory will be removed at the end of the operation.
1207
- Optimized for large files.
1208
- """
1209
- path = _get_path(path)
1210
- src = _get_path(src)
1211
- assert_not_file(path)
1212
- assert_dir(src)
1213
-
1214
- if path == src:
1215
- return
1216
-
1217
- make_dirs(path)
1218
-
1219
- dirpath, dirname = split_filepath(path)
1220
- # safe temporary name to avoid clashes with existing files/directories
1221
- temp_dirname = get_unique_name(dirpath)
1222
- temp_dest = join_path(dirpath, temp_dirname)
1223
- copy_dir_content(src, temp_dest)
1224
-
1225
- if exists(path):
1226
- temp_dirname = get_unique_name(dirpath)
1227
- temp_path = join_path(dirpath, temp_dirname)
1228
- rename_dir(path=path, name=temp_dirname)
1229
- rename_dir(path=temp_dest, name=dirname)
1230
- remove_dir(path=temp_path)
1231
- else:
1232
- rename_dir(path=temp_dest, name=dirname)
1233
-
1234
- if autodelete:
1235
- remove_dir(path=src)
1236
-
1237
-
1238
- def replace_file(path: PathIn, src: PathIn, *, autodelete: bool = False) -> None:
1239
- """
1240
- Replace file at the specified path with the file located at src.
1241
- If autodelete, the src file will be removed at the end of the operation.
1242
- Optimized for large files.
1243
- """
1244
- path = _get_path(path)
1245
- src = _get_path(src)
1246
- assert_not_dir(path)
1247
- assert_file(src)
1248
- if path == src:
1249
- return
1250
-
1251
- make_dirs_for_file(path)
1252
-
1253
- dirpath, filename = split_filepath(path)
1254
- _, extension = split_filename(filename)
1255
- # safe temporary name to avoid clashes with existing files/directories
1256
- temp_filename = get_unique_name(dirpath, extension=extension)
1257
- temp_dest = join_path(dirpath, temp_filename)
1258
- copy_file(path=src, dest=temp_dest, overwrite=False)
1259
-
1260
- if exists(path):
1261
- temp_filename = get_unique_name(dirpath, extension=extension)
1262
- temp_path = join_path(dirpath, temp_filename)
1263
- rename_file(path=path, name=temp_filename)
1264
- rename_file(path=temp_dest, name=filename)
1265
- remove_file(path=temp_path)
1266
- else:
1267
- rename_file(path=temp_dest, name=filename)
1268
-
1269
- if autodelete:
1270
- remove_file(path=src)
1271
-
1272
-
1273
- def _search_paths(path: PathIn, pattern: str) -> list[str]:
1274
- """
1275
- Search all paths relative to path matching the given pattern.
1276
- """
1277
- path = _get_path(path)
1278
- assert_dir(path)
1279
- pathname = os.path.join(path, pattern)
1280
- paths = glob.glob(pathname, recursive=True)
1281
- return paths
1282
-
1283
-
1284
- def search_dirs(path: PathIn, pattern: str = "**/*") -> list[str]:
1285
- """
1286
- Search for directories at path matching the given pattern.
1287
- """
1288
- path = _get_path(path)
1289
- return _filter_paths(path, _search_paths(path, pattern), predicate=is_dir)
1290
-
1291
-
1292
- def search_files(path: PathIn, pattern: str = "**/*.*") -> list[str]:
1293
- """
1294
- Search for files at path matching the given pattern.
1295
- """
1296
- path = _get_path(path)
1297
- return _filter_paths(path, _search_paths(path, pattern), predicate=is_file)
1298
-
1299
-
1300
- def set_permissions(path: PathIn, value: int) -> None:
1301
- """
1302
- Set the file/directory permissions.
1303
- """
1304
- path = _get_path(path)
1305
- assert_exists(path)
1306
- permissions = int(str(value), 8) & 0o777
1307
- os.chmod(path, permissions)
1308
-
1309
-
1310
- def split_filename(path: PathIn) -> tuple[str, str]:
1311
- """
1312
- Split a filename and returns its basename and extension.
1313
- """
1314
- path = _get_path(path)
1315
- filename = get_filename(path)
1316
- basename, extension = os.path.splitext(filename)
1317
- extension = extension.replace(".", "").strip()
1318
- return (basename, extension)
1319
-
1320
-
1321
- def split_filepath(path: PathIn) -> tuple[str, str]:
1322
- """
1323
- Split a filepath and returns its directory-path and filename.
1324
- """
1325
- path = _get_path(path)
1326
- dirpath = os.path.dirname(path)
1327
- filename = get_filename(path)
1328
- return (dirpath, filename)
1329
-
1330
-
1331
- def split_path(path: PathIn) -> list[str]:
1332
- """
1333
- Split a path and returns its path-names.
1334
- """
1335
- path = _get_path(path)
1336
- head, tail = os.path.split(path)
1337
- names = head.split(os.sep) + [tail]
1338
- names = list(filter(None, names))
1339
- return names
1340
-
1341
-
1342
- def transform_filepath(
1343
- path: PathIn,
1344
- *,
1345
- dirpath: str | Callable[[str], str] | None = None,
1346
- basename: str | Callable[[str], str] | None = None,
1347
- extension: str | Callable[[str], str] | None = None,
1348
- ) -> str:
1349
- """
1350
- Trasform a filepath by applying the provided optional changes.
1351
-
1352
- :param path: The path.
1353
- :type path: PathIn
1354
- :param dirpath: The new dirpath or a callable.
1355
- :type dirpath: str | Callable[[str], str] | None
1356
- :param basename: The new basename or a callable.
1357
- :type basename: str | Callable[[str], str] | None
1358
- :param extension: The new extension or a callable.
1359
- :type extension: str | Callable[[str], str] | None
1360
-
1361
- :returns: The filepath with the applied changes.
1362
- :rtype: str
1363
- """
1364
-
1365
- def _get_value(
1366
- new_value: str | Callable[[str], str] | None,
1367
- old_value: str,
1368
- ) -> str:
1369
- value = old_value
1370
- if new_value is not None:
1371
- if callable(new_value):
1372
- value = new_value(old_value)
1373
- elif isinstance(new_value, str):
1374
- value = new_value
1375
- else:
1376
- value = old_value
1377
- return value
1378
-
1379
- if all([dirpath is None, basename is None, extension is None]):
1380
- raise ValueError(
1381
- "Invalid arguments: at least one of "
1382
- "'dirpath', 'basename' or 'extension' is required."
1383
- )
1384
- old_dirpath, old_filename = split_filepath(path)
1385
- old_basename, old_extension = split_filename(old_filename)
1386
- new_dirpath = _get_value(dirpath, old_dirpath)
1387
- new_basename = _get_value(basename, old_basename)
1388
- new_extension = _get_value(extension, old_extension)
1389
- if not any([new_dirpath, new_basename, new_extension]):
1390
- raise ValueError(
1391
- "Invalid arguments: at least one of "
1392
- "'dirpath', 'basename' or 'extension' is required."
1393
- )
1394
- new_filename = join_filename(new_basename, new_extension)
1395
- new_filepath = join_filepath(new_dirpath, new_filename)
1396
- return new_filepath
1397
-
1398
-
1399
- def _write_file_atomic(
1400
- path: PathIn,
1401
- content: str,
1402
- *,
1403
- append: bool = False,
1404
- encoding: str = "utf-8",
1405
- ) -> None:
1406
- path = _get_path(path)
1407
- mode = "a" if append else "w"
1408
- if append:
1409
- content = read_file(path, encoding=encoding) + content
1410
- dirpath, _ = split_filepath(path)
1411
- try:
1412
- with tempfile.NamedTemporaryFile(
1413
- mode=mode,
1414
- dir=dirpath,
1415
- delete=True,
1416
- encoding=encoding,
1417
- ) as file:
1418
- file.write(content)
1419
- file.flush()
1420
- os.fsync(file.fileno())
1421
- temp_path = file.name
1422
- permissions = get_permissions(path) if exists(path) else None
1423
- os.replace(temp_path, path)
1424
- if permissions:
1425
- set_permissions(path, permissions)
1426
- except FileNotFoundError:
1427
- # success - the NamedTemporaryFile has not been able
1428
- # to remove the temp file on __exit__ because the temp file
1429
- # has replaced atomically the file at path.
1430
- pass
1431
-
1432
-
1433
- def _write_file_non_atomic(
1434
- path: PathIn,
1435
- content: str,
1436
- *,
1437
- append: bool = False,
1438
- encoding: str = "utf-8",
1439
- ) -> None:
1440
- mode = "a" if append else "w"
1441
- with open(path, mode, encoding=encoding) as file:
1442
- file.write(content)
1443
-
1444
-
1445
- def write_file(
1446
- path: PathIn,
1447
- content: str,
1448
- *,
1449
- append: bool = False,
1450
- encoding: str = "utf-8",
1451
- atomic: bool = False,
1452
- ) -> None:
1453
- """
1454
- Write file with the specified content at the given path.
1455
- """
1456
- path = _get_path(path)
1457
- assert_not_dir(path)
1458
- make_dirs_for_file(path)
1459
- write_file_func = _write_file_atomic if atomic else _write_file_non_atomic
1460
- write_file_func(
1461
- path,
1462
- content,
1463
- append=append,
1464
- encoding=encoding,
1465
- )
1466
-
1467
-
1468
- def write_file_json(
1469
- path: PathIn,
1470
- data: Any,
1471
- encoding: str = "utf-8",
1472
- atomic: bool = False,
1473
- **kwargs: Any,
1474
- ) -> None:
1475
- """
1476
- Write a json file at the given path with the specified data encoded in json format.
1477
- """
1478
- path = _get_path(path)
1479
-
1480
- def default_encoder(obj: Any) -> Any:
1481
- if isinstance(obj, datetime):
1482
- return obj.isoformat()
1483
- elif isinstance(obj, set):
1484
- return list(obj)
1485
- return str(obj)
1486
-
1487
- kwargs.setdefault("default", default_encoder)
1488
- content = json.dumps(data, **kwargs)
1489
- write_file(
1490
- path,
1491
- content,
1492
- append=False,
1493
- encoding=encoding,
1494
- atomic=atomic,
1495
- )