4
4
import dataclasses
5
5
import io
6
6
import logging
7
+ import os
8
+ import shutil
7
9
import sys
8
10
from abc import abstractmethod , ABC
9
11
from collections .abc import Iterable
14
16
15
17
from astroid import NodeNG # type: ignore
16
18
19
+ from databricks .labs .blueprint .paths import WorkspacePath
17
20
from databricks .sdk .service import compute
18
21
from databricks .sdk .service .workspace import Language
19
22
20
- from databricks .labs .blueprint .paths import WorkspacePath
21
-
22
23
23
24
if sys .version_info >= (3 , 11 ):
24
25
from typing import Self
@@ -412,6 +413,40 @@ def safe_read_text(path: Path, size: int = -1) -> str | None:
412
413
return None
413
414
414
415
416
+ def write_text (path : Path , contents : str , * , encoding : str | None = None ) -> int :
417
+ """Write content to a file as text, encode according to the BOM marker if that is present.
418
+
419
+ This differs to the normal `.read_text()` method on path which does not support BOM markers.
420
+
421
+ Arguments:
422
+ path (Path): The file path to write text to.
423
+ contents (str) : The content to write to the file.
424
+ encoding (str) : Force encoding with a specific locale. If not present the file BOM and
425
+ system locale are used.
426
+
427
+ Returns:
428
+ int : The number of characters written to the file.
429
+ """
430
+ if not encoding and path .exists ():
431
+ with path .open ("rb" ) as binary_io :
432
+ encoding = _detect_encoding_bom (binary_io , preserve_position = False )
433
+ # If encoding=None, the system locale is used for encoding (as per open()).
434
+ return path .write_text (contents , encoding = encoding )
435
+
436
+
437
+ def safe_write_text (path : Path , contents : str , * , encoding : str | None = None ) -> int | None :
438
+ """Safe write content to a file by handling writing exceptions, see :func:write_text.
439
+
440
+ Returns:
441
+ int | None : The number of characters written to the file. If None, no content was written.
442
+ """
443
+ try :
444
+ return write_text (path , contents , encoding = encoding )
445
+ except OSError as e :
446
+ logger .warning (f"Cannot write to file: { path } " , exc_info = e )
447
+ return None
448
+
449
+
415
450
# duplicated from CellLanguage to prevent cyclic import
416
451
LANGUAGE_COMMENT_PREFIXES = {Language .PYTHON : '#' , Language .SCALA : '//' , Language .SQL : '--' }
417
452
NOTEBOOK_HEADER = "Databricks notebook source"
@@ -430,3 +465,64 @@ def is_a_notebook(path: Path, content: str | None = None) -> bool:
430
465
return content .startswith (magic_header )
431
466
file_header = safe_read_text (path , size = len (magic_header ))
432
467
return file_header == magic_header
468
+
469
+
470
+ def _add_backup_suffix (path : Path ) -> Path :
471
+ """Add a backup suffix to a path.
472
+
473
+ The backed up path is the same as the original path with an additional
474
+ `.bak` appended to the suffix.
475
+
476
+ Reuse this method so that the backup path is consistent in this module.
477
+ """
478
+ # Not checking for the backup suffix to allow making backups of backups.
479
+ return path .with_suffix (path .suffix + ".bak" )
480
+
481
+
482
+ def back_up_path (path : Path ) -> Path | None :
483
+ """Back up a path.
484
+
485
+ The backed up path is the same as the original path with an additional
486
+ `.bak` appended to the suffix.
487
+
488
+ Returns :
489
+ path | None : The backed up path. If None, the backup failed.
490
+ """
491
+ path_backed_up = _add_backup_suffix (path )
492
+ try :
493
+ shutil .copyfile (path , path_backed_up )
494
+ except OSError as e :
495
+ logger .warning (f"Cannot back up file: { path } " , exc_info = e )
496
+ return None
497
+ return path_backed_up
498
+
499
+
500
+ def revert_back_up_path (path : Path ) -> bool | None :
501
+ """Revert a backed up path, see :func:back_up_path.
502
+
503
+ The backed up path is the same as the original path with an additional
504
+ `.bak` appended to the suffix.
505
+
506
+ Args :
507
+ path : The original path, NOT the backed up path.
508
+
509
+ Returns :
510
+ bool : Flag if the revert was successful. If None, the backed up path
511
+ does not exist, thus it cannot be reverted and the operation is not
512
+ successful nor failed.
513
+ """
514
+ path_backed_up = _add_backup_suffix (path )
515
+ if not path_backed_up .exists ():
516
+ logger .warning (f"Backup is missing: { path_backed_up } " )
517
+ return None
518
+ try :
519
+ shutil .copyfile (path_backed_up , path )
520
+ except OSError as e :
521
+ logger .warning (f"Cannot revert backup: { path } " , exc_info = e )
522
+ return False
523
+ try :
524
+ os .unlink (path_backed_up )
525
+ except OSError as e :
526
+ # The backup revert is successful, but the backup file cannot be removed
527
+ logger .warning (f"Cannot remove backup file: { path_backed_up } " , exc_info = e )
528
+ return True
0 commit comments