treescript-builder 0.1__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.
- treescript_builder/__init__.py +4 -0
- treescript_builder/__main__.py +18 -0
- treescript_builder/data/__init__.py +0 -0
- treescript_builder/data/data_directory.py +85 -0
- treescript_builder/data/instruction_data.py +19 -0
- treescript_builder/data/path_stack.py +86 -0
- treescript_builder/data/tree_data.py +23 -0
- treescript_builder/data/tree_state.py +132 -0
- treescript_builder/input/__init__.py +28 -0
- treescript_builder/input/argument_data.py +22 -0
- treescript_builder/input/argument_parser.py +102 -0
- treescript_builder/input/file_validation.py +52 -0
- treescript_builder/input/input_data.py +19 -0
- treescript_builder/input/line_reader.py +136 -0
- treescript_builder/input/string_validation.py +130 -0
- treescript_builder/tree/__init__.py +56 -0
- treescript_builder/tree/build_validation.py +111 -0
- treescript_builder/tree/tree_builder.py +77 -0
- treescript_builder/tree/tree_trimmer.py +70 -0
- treescript_builder/tree/trim_validation.py +97 -0
- treescript_builder-0.1.dist-info/METADATA +101 -0
- treescript_builder-0.1.dist-info/RECORD +26 -0
- treescript_builder-0.1.dist-info/WHEEL +5 -0
- treescript_builder-0.1.dist-info/entry_points.txt +3 -0
- treescript_builder-0.1.dist-info/licenses/LICENSE +201 -0
- treescript_builder-0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,136 @@
|
|
1
|
+
"""Line Reader.
|
2
|
+
|
3
|
+
The Default Input Reader.
|
4
|
+
Processes a single line at a time, and determines its key properties.
|
5
|
+
The Depth is the Integer number of directories between the current line and the root.
|
6
|
+
The Directory Boolean indicates whether the line represents a Directory.
|
7
|
+
The Name String is the name of the line.
|
8
|
+
Author: DK96-OS 2024 - 2025
|
9
|
+
"""
|
10
|
+
from itertools import groupby
|
11
|
+
from sys import exit
|
12
|
+
from typing import Generator
|
13
|
+
|
14
|
+
from treescript_builder.data.tree_data import TreeData
|
15
|
+
from treescript_builder.input.string_validation import validate_dir_name, validate_name
|
16
|
+
|
17
|
+
SPACE_CHARS = (' ', ' ', ' ', ' ')
|
18
|
+
|
19
|
+
|
20
|
+
def read_input_tree(input_tree_data: str) -> Generator[TreeData, None, None]:
|
21
|
+
""" Generate structured Tree Data from the Input Data String.
|
22
|
+
|
23
|
+
**Parameters:**
|
24
|
+
- input_data (InputData): The Input.
|
25
|
+
|
26
|
+
**Returns:**
|
27
|
+
Generator[TreeData] - Produces TreeData from the Input Data.
|
28
|
+
|
29
|
+
**Raises:**
|
30
|
+
SystemExit - When any Line cannot be read successfully.
|
31
|
+
"""
|
32
|
+
line_number = 1
|
33
|
+
for is_newline, group in groupby(input_tree_data, lambda x: x in ["\n", "\r"]):
|
34
|
+
if is_newline:
|
35
|
+
line_number += sum(1 for _ in group) # Line number increase by size of group
|
36
|
+
else:
|
37
|
+
line = ''.join(group)
|
38
|
+
if len(lstr := line.lstrip()) == 0 or lstr.startswith('#'):
|
39
|
+
continue
|
40
|
+
yield _process_line(line_number, line)
|
41
|
+
|
42
|
+
|
43
|
+
def _process_line(
|
44
|
+
line_number: int,
|
45
|
+
line: str,
|
46
|
+
) -> TreeData:
|
47
|
+
""" Processes a single line of the input tree structure.
|
48
|
+
- Returns a tuple indicating the depth, type (file or directory), name of file or dir, and file data if available.
|
49
|
+
|
50
|
+
**Parameters:**
|
51
|
+
- line_number (int): The line-number in the input tree structure, starting from 1.
|
52
|
+
- line (str): A line from the input tree structure.
|
53
|
+
|
54
|
+
**Returns:**
|
55
|
+
tuple: (int, bool, str, str) where int is the depth, bool is true when is Directory, and str is name, followed by str data.
|
56
|
+
|
57
|
+
**Raises:**
|
58
|
+
SystemExit - When Line cannot be read successfully.
|
59
|
+
"""
|
60
|
+
# Calculate the Depth
|
61
|
+
depth = _calculate_depth(line)
|
62
|
+
if depth < 0:
|
63
|
+
exit(f"Invalid Space Count in Line: {line_number}")
|
64
|
+
# Remove Space
|
65
|
+
args = line.strip()
|
66
|
+
# Try to split line into multiple arguments
|
67
|
+
for space_char in SPACE_CHARS:
|
68
|
+
if space_char in args:
|
69
|
+
args = args.split(space_char)
|
70
|
+
break
|
71
|
+
# Check whether line was split or not
|
72
|
+
if isinstance(args, str):
|
73
|
+
name = args
|
74
|
+
data_label = ""
|
75
|
+
elif isinstance(args, list) and len(args) >= 2:
|
76
|
+
name = args[0]
|
77
|
+
data_label = args[1]
|
78
|
+
else:
|
79
|
+
exit(f"Invalid Line: {line_number}")
|
80
|
+
# Validate the Node Name and Type.
|
81
|
+
node_info = _validate_node_name(name)
|
82
|
+
if node_info is None:
|
83
|
+
exit(f'Invalid Node on Line: {line_number}')
|
84
|
+
(is_dir, name) = node_info
|
85
|
+
return TreeData(
|
86
|
+
line_number,
|
87
|
+
depth,
|
88
|
+
is_dir,
|
89
|
+
name,
|
90
|
+
data_label
|
91
|
+
)
|
92
|
+
|
93
|
+
|
94
|
+
def _validate_node_name(node_name: str) -> tuple[bool, str] | None:
|
95
|
+
""" Determine whether this Tree Node is a Directory, and validate the name.
|
96
|
+
|
97
|
+
**Parameters:**
|
98
|
+
- node_name (str): The argument received for the node name.
|
99
|
+
|
100
|
+
**Returns:**
|
101
|
+
tuple[bool, str] - Node information, first whether it is a directory, then the valid name of the node.
|
102
|
+
|
103
|
+
**Raises:**
|
104
|
+
SystemExit - When the directory name is invalid.
|
105
|
+
"""
|
106
|
+
try:
|
107
|
+
# Check if the line contains any slash characters
|
108
|
+
if (dir_name := validate_dir_name(node_name)) is not None:
|
109
|
+
return (True, dir_name)
|
110
|
+
# Fall-Through to File Node
|
111
|
+
except ValueError as e:
|
112
|
+
# An error in the dir name, such that it cannot be a file either
|
113
|
+
return None
|
114
|
+
# Is a File
|
115
|
+
if validate_name(node_name):
|
116
|
+
return (False, node_name)
|
117
|
+
return None
|
118
|
+
|
119
|
+
|
120
|
+
def _calculate_depth(line: str) -> int:
|
121
|
+
""" Calculates the depth of a line in the tree structure.
|
122
|
+
|
123
|
+
**Parameters:**
|
124
|
+
- line (str): A line from the tree command output.
|
125
|
+
|
126
|
+
**Returns:**
|
127
|
+
int: The depth of the line in the tree structure, or -1 if space count is invalid.
|
128
|
+
"""
|
129
|
+
from itertools import takewhile
|
130
|
+
space_count = len(list(
|
131
|
+
takewhile(lambda c: c in SPACE_CHARS, line)
|
132
|
+
))
|
133
|
+
# Bit Shift Shuffle Equivalence Validation (space_count is divisible by 2)
|
134
|
+
if (depth := space_count >> 1) << 1 == space_count:
|
135
|
+
return depth
|
136
|
+
return -1 # Invalid Space Count! Someone made an off-by-one whitespace mistake!
|
@@ -0,0 +1,130 @@
|
|
1
|
+
"""String Validation Methods.
|
2
|
+
Author: DK96-OS 2024 - 2025
|
3
|
+
"""
|
4
|
+
from typing import Literal
|
5
|
+
|
6
|
+
|
7
|
+
def validate_name(argument) -> bool:
|
8
|
+
""" Determine whether an argument is a non-empty string.
|
9
|
+
- Does not count whitespace.
|
10
|
+
- Uses the strip method to remove empty space.
|
11
|
+
|
12
|
+
**Parameters:**
|
13
|
+
- argument (str) : The given argument.
|
14
|
+
|
15
|
+
**Returns:**
|
16
|
+
bool - True if the argument qualifies as valid.
|
17
|
+
"""
|
18
|
+
if argument is None or not isinstance(argument, str):
|
19
|
+
return False
|
20
|
+
elif len(argument.strip()) < 1:
|
21
|
+
return False
|
22
|
+
return True
|
23
|
+
|
24
|
+
|
25
|
+
def validate_data_label(data_label: str) -> bool:
|
26
|
+
""" Determine whether a Data Label is Valid.
|
27
|
+
|
28
|
+
**Parameters:**
|
29
|
+
- data_label (str): The String to check for validity.
|
30
|
+
|
31
|
+
**Returns:**
|
32
|
+
bool - Whether the String is a valid Data Label.
|
33
|
+
"""
|
34
|
+
if not 0 < len(data_label) < 100:
|
35
|
+
return False
|
36
|
+
if '/' in data_label or '\\' in data_label:
|
37
|
+
return False
|
38
|
+
# Remove Dash Characters
|
39
|
+
if '-' in data_label:
|
40
|
+
data_label = data_label.replace('-', '')
|
41
|
+
# Remove Underscore Characters
|
42
|
+
if '_' in data_label:
|
43
|
+
data_label = data_label.replace('_', '')
|
44
|
+
# Remove Dot Characters
|
45
|
+
if '.' in data_label:
|
46
|
+
data_label = data_label.replace('.', '')
|
47
|
+
if '!' == data_label:
|
48
|
+
return True
|
49
|
+
# All Remaining Characters must be alphanumeric
|
50
|
+
return data_label.isalnum()
|
51
|
+
|
52
|
+
|
53
|
+
def validate_dir_name(dir_name: str) -> str | None:
|
54
|
+
""" Determine that a directory is correctly formatted.
|
55
|
+
- This method should be called once for each slash type.
|
56
|
+
|
57
|
+
**Parameters:**
|
58
|
+
- dir_name (str): The given input to be validated.
|
59
|
+
|
60
|
+
**Returns:**
|
61
|
+
str | None - The valid directory name, or none if it may be a file.
|
62
|
+
|
63
|
+
**Raises:**
|
64
|
+
ValueError - When the name is not suitable for directories or files.
|
65
|
+
"""
|
66
|
+
# Keep Name Length Reasonable
|
67
|
+
if (name_length := len(dir_name)) >= 100:
|
68
|
+
raise ValueError(f'Name too Long!: {name_length}')
|
69
|
+
# Check for slash characters
|
70
|
+
if (name := _filter_slash_chars(dir_name)) is not None:
|
71
|
+
# Is a Dir
|
72
|
+
if len(name) == 0:
|
73
|
+
raise ValueError('The name is empty')
|
74
|
+
# Check for invalid characters (parent dir, current dir)
|
75
|
+
if name in ['.', '..']:
|
76
|
+
raise ValueError('Invalid Directory')
|
77
|
+
return name
|
78
|
+
# Is a File
|
79
|
+
return None
|
80
|
+
|
81
|
+
|
82
|
+
def _validate_slash_char(dir_name: str) -> Literal['\\', '/'] | None:
|
83
|
+
""" Determine which slash char is used by the directory, if it is a directory.
|
84
|
+
- Discourages use of both slash chars, by raising ValueError.
|
85
|
+
|
86
|
+
**Parameters:**
|
87
|
+
- dir_name (str): The given input to be validated.
|
88
|
+
|
89
|
+
**Returns:**
|
90
|
+
str | None - The slash character used, or none if no chars were found.
|
91
|
+
|
92
|
+
**Raises:**
|
93
|
+
ValueError - When the name contains both slash characters.
|
94
|
+
"""
|
95
|
+
slash = None
|
96
|
+
if '/' in dir_name:
|
97
|
+
slash = '/'
|
98
|
+
if '\\' in dir_name:
|
99
|
+
if slash is not None:
|
100
|
+
raise ValueError('Invalid Directory slash character combination.')
|
101
|
+
slash = '\\'
|
102
|
+
return slash
|
103
|
+
|
104
|
+
|
105
|
+
def _filter_slash_chars(dir_name: str) -> str | None:
|
106
|
+
""" Remove all of the slash characters and return the directory name.
|
107
|
+
- Returns None when there are no slash characters found.
|
108
|
+
- Raises ValueError when slash characters are used improperly.
|
109
|
+
|
110
|
+
**Parameters:**
|
111
|
+
- dir_name (str): The given input to be validated.
|
112
|
+
|
113
|
+
**Returns:**
|
114
|
+
str | None - The valid directory name, or none if it may be a file.
|
115
|
+
|
116
|
+
**Raises:**
|
117
|
+
ValueError - When the name is not suitable for directories or files.
|
118
|
+
"""
|
119
|
+
slash = _validate_slash_char(dir_name)
|
120
|
+
if slash is None:
|
121
|
+
return None
|
122
|
+
if dir_name.endswith(slash) or dir_name.startswith(slash):
|
123
|
+
name = dir_name.strip(slash)
|
124
|
+
# Check for internal slash characters
|
125
|
+
if slash in name:
|
126
|
+
raise ValueError('Multi-dir line detected')
|
127
|
+
else:
|
128
|
+
# Found slash chars only within the node name (multi-dir line)
|
129
|
+
raise ValueError('Multi-dir line detected')
|
130
|
+
return name
|
@@ -0,0 +1,56 @@
|
|
1
|
+
"""The Tree Module.
|
2
|
+
"""
|
3
|
+
from treescript_builder.input.input_data import InputData
|
4
|
+
from treescript_builder.input.line_reader import read_input_tree
|
5
|
+
|
6
|
+
|
7
|
+
def build_tree(input_data: InputData) -> tuple[bool, ...]:
|
8
|
+
""" Build The Tree as defined by the InputData.
|
9
|
+
|
10
|
+
**Parameters:**
|
11
|
+
- input_data (str): The InputData produced by the Input Module.
|
12
|
+
|
13
|
+
**Returns:**
|
14
|
+
tuple[bool, ...] - The results of each individual Builder operation.
|
15
|
+
|
16
|
+
**Raises:**
|
17
|
+
SystemExit - If a Tree Validation error occurs.
|
18
|
+
"""
|
19
|
+
if input_data.is_reversed:
|
20
|
+
from treescript_builder.tree.trim_validation import validate_trim
|
21
|
+
instructions = validate_trim(
|
22
|
+
read_input_tree(input_data.tree_input),
|
23
|
+
input_data.data_dir
|
24
|
+
)
|
25
|
+
from treescript_builder.tree.tree_trimmer import trim
|
26
|
+
results = trim(instructions)
|
27
|
+
else:
|
28
|
+
from treescript_builder.tree.build_validation import validate_build
|
29
|
+
instructions = validate_build(
|
30
|
+
read_input_tree(input_data.tree_input),
|
31
|
+
input_data.data_dir
|
32
|
+
)
|
33
|
+
from treescript_builder.tree.tree_builder import build
|
34
|
+
results = build(instructions)
|
35
|
+
#
|
36
|
+
return results
|
37
|
+
|
38
|
+
|
39
|
+
def process_results(results: tuple[bool, ...]) -> str:
|
40
|
+
""" Process and Summarize the Results.
|
41
|
+
|
42
|
+
**Parameters:**
|
43
|
+
- results (tuple[bool]): A tuple containing the results of the operations.
|
44
|
+
|
45
|
+
**Returns:**
|
46
|
+
str - A summary of the number of operations that succeeded.
|
47
|
+
"""
|
48
|
+
if (length := len(results)) == 0:
|
49
|
+
return 'No operations ran.'
|
50
|
+
if (success := sum(iter(results))) == 0:
|
51
|
+
return f"All {length} operations failed."
|
52
|
+
elif success == length:
|
53
|
+
return f"All {length} operations succeeded."
|
54
|
+
# Compute the Fraction of success operations
|
55
|
+
success_percent = round(100 * success / length, 1)
|
56
|
+
return f"{success} out of {length} operations succeeded: {success_percent}%"
|
@@ -0,0 +1,111 @@
|
|
1
|
+
"""Tree Validation Methods for the Build Operation.
|
2
|
+
Author: DK96-OS 2024 - 2025
|
3
|
+
"""
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Generator
|
6
|
+
|
7
|
+
from treescript_builder.data.data_directory import DataDirectory
|
8
|
+
from treescript_builder.data.instruction_data import InstructionData
|
9
|
+
from treescript_builder.data.tree_data import TreeData
|
10
|
+
from treescript_builder.data.tree_state import TreeState
|
11
|
+
|
12
|
+
|
13
|
+
def validate_build(
|
14
|
+
tree_data: Generator[TreeData, None, None],
|
15
|
+
data_dir_path: Path | None = None,
|
16
|
+
verbose: bool = False,
|
17
|
+
) -> tuple[InstructionData, ...]:
|
18
|
+
""" Validate the Build Instructions.
|
19
|
+
|
20
|
+
**Parameters:**
|
21
|
+
- tree_data (Generator[TreeData]): The Generator that provides TreeData.
|
22
|
+
- data_dir_path (Path?): The optional Data Directory Path. Default: None.
|
23
|
+
- verbose (bool): Whether to print DataDirectory information during validation.
|
24
|
+
|
25
|
+
**Returns:**
|
26
|
+
tuple[InstructionData] - A generator that yields Instructions.
|
27
|
+
"""
|
28
|
+
if data_dir_path is None:
|
29
|
+
return tuple(iter(_validate_build_generator(tree_data)))
|
30
|
+
else:
|
31
|
+
data = DataDirectory(data_dir_path)
|
32
|
+
if verbose:
|
33
|
+
print(f"Validating Build With DataDir: {data_dir_path}")
|
34
|
+
return tuple(iter(_validate_build_generator_data(tree_data, data)))
|
35
|
+
|
36
|
+
|
37
|
+
def _validate_build_generator(
|
38
|
+
tree_data: Generator[TreeData, None, None]
|
39
|
+
) -> Generator[InstructionData, None, None]:
|
40
|
+
tree_state = TreeState()
|
41
|
+
for node in tree_data:
|
42
|
+
# Error if any Nodes have Data Labels
|
43
|
+
if node.data_label != '':
|
44
|
+
exit(f"Found Data Label on Line {node.line_number} with no Data Directory: {node.data_label}")
|
45
|
+
# Calculate Tree Depth Change
|
46
|
+
if tree_state.validate_tree_data(node) == 0:
|
47
|
+
if node.is_dir:
|
48
|
+
tree_state.add_to_queue(node.name)
|
49
|
+
else:
|
50
|
+
# Merge Queue into Stack
|
51
|
+
if (new_dir := tree_state.process_queue()) is not None:
|
52
|
+
yield InstructionData(True, new_dir)
|
53
|
+
yield InstructionData(
|
54
|
+
False, tree_state.get_current_path() / node.name
|
55
|
+
)
|
56
|
+
else:
|
57
|
+
# Merge Queue into Stack
|
58
|
+
if (new_dir := tree_state.process_queue()) is not None:
|
59
|
+
yield InstructionData(True, new_dir, None)
|
60
|
+
# Pop Stack to required Depth
|
61
|
+
if not tree_state.reduce_depth(node.depth):
|
62
|
+
exit(f"Invalid Depth at Line: {node.line_number}")
|
63
|
+
if node.is_dir:
|
64
|
+
tree_state.add_to_queue(node.name)
|
65
|
+
else:
|
66
|
+
yield InstructionData(
|
67
|
+
False, tree_state.get_current_path() / node.name
|
68
|
+
)
|
69
|
+
# Always Finish Build Sequence with ProcessQueue
|
70
|
+
if (dir := tree_state.process_queue()) is not None:
|
71
|
+
yield InstructionData(True, dir)
|
72
|
+
|
73
|
+
|
74
|
+
def _validate_build_generator_data(
|
75
|
+
tree_data: Generator[TreeData, None, None],
|
76
|
+
data_dir: DataDirectory
|
77
|
+
) -> Generator[InstructionData, None, None]:
|
78
|
+
tree_state = TreeState()
|
79
|
+
for node in tree_data:
|
80
|
+
# Calculate Tree Depth Change
|
81
|
+
if tree_state.validate_tree_data(node) == 0:
|
82
|
+
if node.is_dir:
|
83
|
+
tree_state.add_to_queue(node.name)
|
84
|
+
else:
|
85
|
+
# Build Queued Directories
|
86
|
+
if (new_dir := tree_state.process_queue()) is not None:
|
87
|
+
yield InstructionData(True, new_dir)
|
88
|
+
# Build File
|
89
|
+
yield InstructionData(
|
90
|
+
False,
|
91
|
+
tree_state.get_current_path() / node.name,
|
92
|
+
data_dir.validate_build(node)
|
93
|
+
)
|
94
|
+
else:
|
95
|
+
# Merge Queue into Stack
|
96
|
+
if (new_dir := tree_state.process_queue()) is not None:
|
97
|
+
yield InstructionData(True, new_dir)
|
98
|
+
# Pop Stack to required Depth
|
99
|
+
if not tree_state.reduce_depth(node.depth):
|
100
|
+
exit(f"Invalid Tree Depth on Line {node.line_number} : {node.name}")
|
101
|
+
if node.is_dir:
|
102
|
+
tree_state.add_to_queue(node.name)
|
103
|
+
else:
|
104
|
+
yield InstructionData(
|
105
|
+
False,
|
106
|
+
tree_state.get_current_path() / node.name,
|
107
|
+
data_dir.validate_build(node)
|
108
|
+
)
|
109
|
+
# Always Finish Build Sequence with ProcessQueue
|
110
|
+
if (dir := tree_state.process_queue()) is not None:
|
111
|
+
yield InstructionData(True, dir)
|
@@ -0,0 +1,77 @@
|
|
1
|
+
"""Tree Building Operations.
|
2
|
+
Author: DK96-OS 2024 - 2025
|
3
|
+
"""
|
4
|
+
from pathlib import Path
|
5
|
+
from shutil import copy2
|
6
|
+
|
7
|
+
from treescript_builder.data.instruction_data import InstructionData
|
8
|
+
|
9
|
+
|
10
|
+
def build(instructions: tuple[InstructionData, ...]) -> tuple[bool, ...]:
|
11
|
+
""" Execute the Instructions in build mode.
|
12
|
+
|
13
|
+
**Parameters:**
|
14
|
+
- instructions(tuple[InstructionData]): The Instructions to execute.
|
15
|
+
|
16
|
+
**Returns:**
|
17
|
+
tuple[bool] - The success or failure of each instruction.
|
18
|
+
"""
|
19
|
+
return tuple(_build(i) for i in instructions)
|
20
|
+
|
21
|
+
|
22
|
+
def _build(i: InstructionData) -> bool:
|
23
|
+
""" Execute a single instruction.
|
24
|
+
|
25
|
+
**Parameters:**
|
26
|
+
- instruction(InstructionData): The data required to execute the operation.
|
27
|
+
|
28
|
+
**Returns:**
|
29
|
+
bool - Whether the given operation succeeded.
|
30
|
+
"""
|
31
|
+
if i.is_dir:
|
32
|
+
return _make_dir_exist(i.path)
|
33
|
+
elif i.data_path is None:
|
34
|
+
i.path.touch(exist_ok=True)
|
35
|
+
return True
|
36
|
+
else:
|
37
|
+
return _create_file(i.path, i.data_path)
|
38
|
+
|
39
|
+
|
40
|
+
def _create_file(
|
41
|
+
path: Path,
|
42
|
+
data: Path
|
43
|
+
) -> bool:
|
44
|
+
""" Create a File at the given path, with data from the Data Directory.
|
45
|
+
|
46
|
+
**Parameters:**
|
47
|
+
- path (Path): The Path to the File to be created, and written to.
|
48
|
+
- data (Path): A Data Directory Path to be copied to the new File.
|
49
|
+
|
50
|
+
**Returns:**
|
51
|
+
bool - Whether the File operation succeeded.
|
52
|
+
"""
|
53
|
+
try:
|
54
|
+
copy2(data, path)
|
55
|
+
except BaseException as e:
|
56
|
+
return False
|
57
|
+
return True
|
58
|
+
|
59
|
+
|
60
|
+
def _make_dir_exist(
|
61
|
+
path: Path
|
62
|
+
) -> bool:
|
63
|
+
""" Ensure that the Directory at the given Path exists.
|
64
|
+
|
65
|
+
**Parameters:**
|
66
|
+
- path (Path): The Path to the File to be created, and written to.
|
67
|
+
|
68
|
+
**Returns:**
|
69
|
+
bool - True if the Operation Succeeded, or if the Path already exists.
|
70
|
+
"""
|
71
|
+
if path.exists():
|
72
|
+
return True
|
73
|
+
try:
|
74
|
+
path.mkdir(parents=True, exist_ok=True)
|
75
|
+
except IOError as e:
|
76
|
+
return False
|
77
|
+
return True
|
@@ -0,0 +1,70 @@
|
|
1
|
+
"""Tree Trimming Methods.
|
2
|
+
Author: DK96-OS 2024 - 2025
|
3
|
+
"""
|
4
|
+
from pathlib import Path
|
5
|
+
from shutil import move
|
6
|
+
|
7
|
+
from treescript_builder.data.instruction_data import InstructionData
|
8
|
+
|
9
|
+
|
10
|
+
def trim(instructions: tuple[InstructionData, ...]) -> tuple[bool, ...]:
|
11
|
+
""" Execute the Instructions in trim mode.
|
12
|
+
|
13
|
+
**Parameters:**
|
14
|
+
- instructions(tuple[InstructionData]): The Instructions to execute.
|
15
|
+
|
16
|
+
**Returns:**
|
17
|
+
tuple[bool] - The success or failure of each instruction.
|
18
|
+
"""
|
19
|
+
return tuple(_trim(i) for i in instructions)
|
20
|
+
|
21
|
+
|
22
|
+
def _trim(instruct: InstructionData) -> bool:
|
23
|
+
if instruct.is_dir:
|
24
|
+
return _remove_dir(instruct.path)
|
25
|
+
if instruct.data_path is None:
|
26
|
+
try:
|
27
|
+
instruct.path.unlink(missing_ok=True)
|
28
|
+
return True
|
29
|
+
except IOError as e:
|
30
|
+
print("IO File Error")
|
31
|
+
return False
|
32
|
+
return _extract_file(instruct.path, instruct.data_path)
|
33
|
+
|
34
|
+
|
35
|
+
def _extract_file(
|
36
|
+
path: Path,
|
37
|
+
data: Path
|
38
|
+
) -> bool:
|
39
|
+
""" Moves the File to the Data Directory.
|
40
|
+
|
41
|
+
**Parameters:**
|
42
|
+
- path (Path): The path to the File in the Tree.
|
43
|
+
- data (Path): A Path to a File in the Data Directory.
|
44
|
+
|
45
|
+
**Returns:**
|
46
|
+
bool - Whether the entire operation succeeded.
|
47
|
+
"""
|
48
|
+
try:
|
49
|
+
move(path, data)
|
50
|
+
return True
|
51
|
+
except:
|
52
|
+
return False
|
53
|
+
|
54
|
+
|
55
|
+
def _remove_dir(
|
56
|
+
path: Path,
|
57
|
+
) -> bool:
|
58
|
+
""" Tries to Remove a Directory, if it is Empty.
|
59
|
+
|
60
|
+
**Parameters:**
|
61
|
+
- path (Path): The path to the Directory.
|
62
|
+
|
63
|
+
**Returns:**
|
64
|
+
bool : Whether the Directory was Empty, and has been removed.
|
65
|
+
"""
|
66
|
+
try:
|
67
|
+
path.rmdir()
|
68
|
+
except BaseException as e:
|
69
|
+
return False
|
70
|
+
return True
|
@@ -0,0 +1,97 @@
|
|
1
|
+
"""Tree Validation Methods for the Trim Operation.
|
2
|
+
Author: DK96-OS 2024 - 2025
|
3
|
+
"""
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Generator
|
6
|
+
|
7
|
+
from treescript_builder.data.data_directory import DataDirectory
|
8
|
+
from treescript_builder.data.instruction_data import InstructionData
|
9
|
+
from treescript_builder.data.tree_data import TreeData
|
10
|
+
from treescript_builder.data.tree_state import TreeState
|
11
|
+
|
12
|
+
|
13
|
+
def validate_trim(
|
14
|
+
tree_data: Generator[TreeData, None, None],
|
15
|
+
data_dir_path: Path | None = None,
|
16
|
+
verbose: bool = False,
|
17
|
+
) -> tuple[InstructionData, ...]:
|
18
|
+
""" Validate the Trim Instructions.
|
19
|
+
|
20
|
+
**Parameters:**
|
21
|
+
- tree_data (Generator[TreeData]): The Generator that provides TreeData.
|
22
|
+
- data_dir_path (Path?): The optional Path to a Data Directory. Default: None.
|
23
|
+
- verbose (bool): Whether to print DataDirectory information during validation.
|
24
|
+
|
25
|
+
**Returns:**
|
26
|
+
tuple[InstructionData] - A generator that yields Instructions.
|
27
|
+
"""
|
28
|
+
if data_dir_path is None:
|
29
|
+
return tuple(iter(_validate_trim_generator(tree_data)))
|
30
|
+
else:
|
31
|
+
data = DataDirectory(data_dir_path)
|
32
|
+
if verbose:
|
33
|
+
print(f"Validating Trim With DataDir: {data_dir_path}")
|
34
|
+
return tuple(iter(_validate_trim_generator_data(tree_data, data)))
|
35
|
+
|
36
|
+
|
37
|
+
def _validate_trim_generator(
|
38
|
+
tree_data: Generator[TreeData, None, None]
|
39
|
+
) -> Generator[InstructionData, None, None]:
|
40
|
+
tree_state = TreeState()
|
41
|
+
for node in tree_data:
|
42
|
+
# Calculate Tree Depth Change
|
43
|
+
if tree_state.validate_tree_data(node) == 1:
|
44
|
+
if node.is_dir:
|
45
|
+
tree_state.add_to_stack(node.name)
|
46
|
+
else:
|
47
|
+
yield InstructionData(
|
48
|
+
False, tree_state.get_current_path() / node.name
|
49
|
+
)
|
50
|
+
else:
|
51
|
+
# Pop Stack to required Depth
|
52
|
+
for i in tree_state.process_stack(node.depth):
|
53
|
+
yield InstructionData(True, i)
|
54
|
+
# Dir or File
|
55
|
+
if node.is_dir:
|
56
|
+
tree_state.add_to_stack(node.name)
|
57
|
+
else:
|
58
|
+
yield InstructionData(
|
59
|
+
False, tree_state.get_current_path() / node.name
|
60
|
+
)
|
61
|
+
# Finish Trim Sequence with Pop Stack
|
62
|
+
for i in tree_state.process_stack(0):
|
63
|
+
yield InstructionData(True, i)
|
64
|
+
|
65
|
+
|
66
|
+
def _validate_trim_generator_data(
|
67
|
+
tree_data: Generator[TreeData, None, None],
|
68
|
+
data_dir: DataDirectory
|
69
|
+
) -> Generator[InstructionData, None, None]:
|
70
|
+
tree_state = TreeState()
|
71
|
+
for node in tree_data:
|
72
|
+
# Calculate Tree Depth Change
|
73
|
+
if tree_state.validate_tree_data(node) == 1:
|
74
|
+
if node.is_dir:
|
75
|
+
tree_state.add_to_stack(node.name)
|
76
|
+
else:
|
77
|
+
yield InstructionData(
|
78
|
+
False,
|
79
|
+
tree_state.get_current_path() / node.name,
|
80
|
+
data_dir.validate_trim(node)
|
81
|
+
)
|
82
|
+
else:
|
83
|
+
# Pop Stack to required Depth
|
84
|
+
for i in tree_state.process_stack(node.depth):
|
85
|
+
yield InstructionData(True, i)
|
86
|
+
# Dir or File
|
87
|
+
if node.is_dir:
|
88
|
+
tree_state.add_to_stack(node.name)
|
89
|
+
else:
|
90
|
+
yield InstructionData(
|
91
|
+
False,
|
92
|
+
tree_state.get_current_path() / node.name,
|
93
|
+
data_dir.validate_trim(node)
|
94
|
+
)
|
95
|
+
# Finish Trim Sequence with Pop Stack
|
96
|
+
for i in tree_state.process_stack(0):
|
97
|
+
yield InstructionData(True, i)
|