-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathutility.py
More file actions
172 lines (143 loc) · 5.34 KB
/
Copy pathutility.py
File metadata and controls
172 lines (143 loc) · 5.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# Functions for handling local files and folders
import os
import sys
import toml
import json
import tkinter
import shutil
import platform
import subprocess
from typing import Any
from pathlib import Path
from tkinter import filedialog
from tenacity import retry
from tenacity import wait_fixed
from tenacity import stop_after_attempt
def get_app_directory() -> Path:
"""
Returns the correct path that crmscript fetcher resides in
regardless of whether its run as a .exe or in a code editor, and regardless of OS
"""
# When ran as .exe bundled by PyInstaller
if getattr(sys, 'frozen', False):
return Path(sys._MEIPASS)
# When running from source
return Path(__file__).resolve().parent
def ask_directory_path_macos() -> str:
"""
Uses AppleScript's native folder chooser instead of Tkinter.
Tkinter's Cocoa/NSApplication integration doesn't tear down and
reinitialize cleanly between calls on macOS, which is what causes
the picker to misbehave (jump around, fail to close, refuse to reopen)
when a fresh Tk root is created and destroyed on every call.
"""
script = 'POSIX path of (choose folder with prompt "Select a folder")'
try:
result = subprocess.run(
["osascript", "-e", script],
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()
except subprocess.CalledProcessError:
# User cancelled the dialog
return ""
def ask_directory_path_tk() -> str:
"""
Used for Windows and Linux.
"""
root = tkinter.Tk()
root.withdraw() # Hide the root window
root.attributes('-topmost', True) # Appear on top of browser window
folder: str = filedialog.askdirectory(parent=root)
root.destroy()
return folder
def ask_directory_path() -> str:
"""
Opens a native folder picker and returns the folder path the user selected.
Returns an empty string if the user cancels.
"""
if platform.system() == "Darwin":
return ask_directory_path_macos()
return ask_directory_path_tk()
def get_fetcher_script() -> str:
"""
Opens crmscript_fetcher.crmscript file and returns the contents.
"""
file_path: Path = get_app_directory() / "crmscript_fetcher.crmscript"
with open(file_path) as f:
return f.read()
def open_directory(directory_path: str):
"""Opens the folder in user's OS. Handles both Windows and Linux."""
path = Path(directory_path).resolve()
system: str = platform.system()
if system == "Windows":
subprocess.Popen(f'explorer "{str(path)}"')
elif system == "Linux":
subprocess.Popen(["xdg-open", str(path)])
elif system == "Darwin": # macOS
subprocess.Popen(["open", str(path)])
def safe_name(text: str) -> str:
"""Replace characters that are not allowed in Windows folders/files"""
replace_chars: list[tuple[str, str]] = [
("/", "."),
('"', "'"),
("\\", ".."),
(":", " - "),
("*", "X"),
("<", " Lt "),
(">", " Gt "),
("|", "I"),
("?", "")
]
for chars in replace_chars:
text = text.replace(chars[0], chars[1])
return text
def create_folder(path: str) -> None:
try:
os.mkdir(path)
except OSError:
print(f"Creation of the directory failed. Folder might already exist: {path}")
else:
print(f"Successfully created directory: {path}")
# Sometimes throws PermissionError if os module has recently accessed the folder.
# Usually works after retrying a number of times.
@retry(wait=wait_fixed(1), stop=stop_after_attempt(10))
def move_folder(src: str, dst: str) -> None:
try:
shutil.move(src, dst)
except FileNotFoundError:
return
except shutil.Error:
shutil.rmtree(dst)
move_folder(src, dst)
# shutil.rmtree() often throws PermissionError since os module has just accessed the folder.
# Usually works after retrying a number of times.
@retry(wait=wait_fixed(1), stop=stop_after_attempt(10))
def delete_folder(directory: str) -> None:
if not os.path.isdir(directory):
return
print(f"Deleting folder: {directory}")
shutil.rmtree(directory)
def create_file(directory: str, file_name: str, body: str) -> None:
"""Creates a file in the given directory. file_name must include file extension."""
print(f"Creating file: {file_name}")
full_path: str = f"{directory}/{file_name}"
""" Normalize newlines """
# TODO: What to do here? This fix caused every line to be two linebreaks, in Windows at least
# body = body.replace("\r\n", "\n")
# body = body.replace("\n", "\r\n")
with open(full_path, "w", encoding="utf-8") as f:
f.write(body)
def create_json_file(directory: str, file_name: str, content: Any) -> None:
"""Creates a JSON file in the given directory. file_name must include file extension."""
print(f"Creating file: {file_name}")
full_path: str = f"{directory}/{file_name}"
with open(full_path, "w", encoding="utf8") as f:
json.dump(content, f, indent=4, ensure_ascii=False)
def get_current_version() -> str:
"""Returns the version of CRMScript Fetcher from pyproject.toml file."""
pyproject_path: Path = get_app_directory() / "pyproject.toml"
pyproject: dict = toml.load(pyproject_path)
return pyproject["project"]["version"]