rdxz2-utill 0.1.3__py3-none-any.whl → 0.1.4__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.
Potentially problematic release.
This version of rdxz2-utill might be problematic. Click here for more details.
- {rdxz2_utill-0.1.3.dist-info → rdxz2_utill-0.1.4.dist-info}/METADATA +2 -1
- rdxz2_utill-0.1.4.dist-info/RECORD +37 -0
- utill/cmd/_bq.py +16 -3
- utill/cmd/_conf.py +15 -15
- utill/cmd/_enc.py +8 -4
- utill/cmd/_mb.py +116 -36
- utill/cmd/_pg.py +4 -2
- utill/cmd/utill.py +193 -72
- utill/my_bq.py +271 -158
- utill/my_compare.py +1 -1
- utill/my_const.py +11 -8
- utill/my_csv.py +31 -15
- utill/my_datetime.py +21 -10
- utill/my_encryption.py +31 -13
- utill/my_env.py +22 -13
- utill/my_file.py +15 -13
- utill/my_gcs.py +40 -16
- utill/my_gdrive.py +195 -0
- utill/my_input.py +8 -4
- utill/my_json.py +6 -6
- utill/my_mb.py +351 -357
- utill/my_pg.py +76 -46
- utill/my_queue.py +37 -24
- utill/my_string.py +23 -5
- utill/my_style.py +18 -16
- utill/my_tunnel.py +29 -9
- utill/my_xlsx.py +11 -8
- rdxz2_utill-0.1.3.dist-info/RECORD +0 -36
- {rdxz2_utill-0.1.3.dist-info → rdxz2_utill-0.1.4.dist-info}/WHEEL +0 -0
- {rdxz2_utill-0.1.3.dist-info → rdxz2_utill-0.1.4.dist-info}/entry_points.txt +0 -0
- {rdxz2_utill-0.1.3.dist-info → rdxz2_utill-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {rdxz2_utill-0.1.3.dist-info → rdxz2_utill-0.1.4.dist-info}/top_level.txt +0 -0
utill/my_gdrive.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from google.auth import default
|
|
2
|
+
from googleapiclient.discovery import build
|
|
3
|
+
from googleapiclient.http import MediaFileUpload
|
|
4
|
+
from googleapiclient.http import MediaIoBaseDownload
|
|
5
|
+
from humanize import naturalsize
|
|
6
|
+
import enum
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Role(enum.StrEnum):
|
|
15
|
+
READER = "reader"
|
|
16
|
+
WRITER = "writer"
|
|
17
|
+
COMMENTER = "commenter"
|
|
18
|
+
OWNER = "owner"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GDrive:
|
|
22
|
+
"""
|
|
23
|
+
Custom hook for Google Drive integration in Airflow.
|
|
24
|
+
This hook can be used to interact with Google Drive APIs.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
credentials, project = default(
|
|
29
|
+
scopes=[
|
|
30
|
+
"https://www.googleapis.com/auth/drive",
|
|
31
|
+
"https://www.googleapis.com/auth/drive.file",
|
|
32
|
+
]
|
|
33
|
+
)
|
|
34
|
+
drive_service = build("drive", "v3", credentials=credentials)
|
|
35
|
+
self.connection = drive_service
|
|
36
|
+
|
|
37
|
+
# region Folder operations
|
|
38
|
+
|
|
39
|
+
def get_folder_by_name(self, *, parent_folder_id: str, name: str) -> str | None:
|
|
40
|
+
"""
|
|
41
|
+
Retrieves a folder by its name within a specified Google Drive folder.
|
|
42
|
+
:param folder_id: The ID of the parent folder to search in.
|
|
43
|
+
:param name: The name of the folder to find.
|
|
44
|
+
:return: The ID of the found folder or None if not found.
|
|
45
|
+
"""
|
|
46
|
+
query = f"'{parent_folder_id}' in parents and name='{name}' and mimeType='application/vnd.google-apps.folder' and trashed=false"
|
|
47
|
+
results = (
|
|
48
|
+
self.connection.files()
|
|
49
|
+
.list(q=query, fields="files(id)", supportsAllDrives=True)
|
|
50
|
+
.execute()
|
|
51
|
+
)
|
|
52
|
+
items = results.get("files", [])
|
|
53
|
+
|
|
54
|
+
return items[0]["id"] if items else None
|
|
55
|
+
|
|
56
|
+
def create_folder(
|
|
57
|
+
self, folder_name: str, parent_folder_id: str | None = None
|
|
58
|
+
) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Creates a folder in Google Drive.
|
|
61
|
+
:param folder_name: The name of the folder to create.
|
|
62
|
+
:param parent_folder_id: The ID of the parent folder (optional).
|
|
63
|
+
:return: The ID of the created folder.
|
|
64
|
+
"""
|
|
65
|
+
file_metadata = {
|
|
66
|
+
"name": folder_name,
|
|
67
|
+
"mimeType": "application/vnd.google-apps.folder",
|
|
68
|
+
}
|
|
69
|
+
if parent_folder_id:
|
|
70
|
+
file_metadata["parents"] = [parent_folder_id]
|
|
71
|
+
|
|
72
|
+
file = (
|
|
73
|
+
self.connection.files()
|
|
74
|
+
.create(body=file_metadata, fields="id", supportsAllDrives=True)
|
|
75
|
+
.execute()
|
|
76
|
+
)
|
|
77
|
+
log.debug(
|
|
78
|
+
f"Folder {folder_name} created under {self.generate_gdrive_folder_url(parent_folder_id)}"
|
|
79
|
+
)
|
|
80
|
+
return file.get("id")
|
|
81
|
+
|
|
82
|
+
def grant_folder_access(
|
|
83
|
+
self,
|
|
84
|
+
folder_id: str,
|
|
85
|
+
email: str,
|
|
86
|
+
role: Role = Role.READER,
|
|
87
|
+
send_notification_email: bool = False,
|
|
88
|
+
):
|
|
89
|
+
"""
|
|
90
|
+
Grants access to a Google Drive folder to a user by email.
|
|
91
|
+
:param folder_id: The ID of the folder to grant access to.
|
|
92
|
+
:param email: The email address of the user to grant access to.
|
|
93
|
+
:param role: The role to assign (reader, writer, commenter, owner).
|
|
94
|
+
"""
|
|
95
|
+
self.connection.permissions().create(
|
|
96
|
+
fileId=folder_id,
|
|
97
|
+
body={
|
|
98
|
+
"type": "user",
|
|
99
|
+
"role": role,
|
|
100
|
+
"emailAddress": email,
|
|
101
|
+
},
|
|
102
|
+
sendNotificationEmail=send_notification_email,
|
|
103
|
+
supportsAllDrives=True,
|
|
104
|
+
).execute()
|
|
105
|
+
log.debug(
|
|
106
|
+
f"Granted {role} access to {email} for folder {self.generate_gdrive_folder_url(folder_id)}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# endregion
|
|
110
|
+
|
|
111
|
+
# region File operations
|
|
112
|
+
|
|
113
|
+
def get_file(self, file_id: str):
|
|
114
|
+
raise NotImplementedError()
|
|
115
|
+
|
|
116
|
+
def list_files(self, folder_id: str, mime_type: str | None = None):
|
|
117
|
+
"""
|
|
118
|
+
Lists files in a specified Google Drive folder.
|
|
119
|
+
:param folder_id: The ID of the folder to search in.
|
|
120
|
+
:param mime_type: Optional MIME type to filter files by.
|
|
121
|
+
:return: A list of files in the specified folder.
|
|
122
|
+
"""
|
|
123
|
+
query = f"'{folder_id}' in parents and trashed=false"
|
|
124
|
+
if mime_type:
|
|
125
|
+
query += f" and mimeType='{mime_type}'"
|
|
126
|
+
|
|
127
|
+
results = (
|
|
128
|
+
self.connection.files()
|
|
129
|
+
.list(q=query, fields="files(id, name)", supportsAllDrives=True)
|
|
130
|
+
.execute()
|
|
131
|
+
)
|
|
132
|
+
return results.get("files", [])
|
|
133
|
+
|
|
134
|
+
def upload_file(
|
|
135
|
+
self, src_filepath: str, folder_id: str, mime_type: str | None = None
|
|
136
|
+
):
|
|
137
|
+
media = MediaFileUpload(src_filepath, mimetype=mime_type, resumable=True)
|
|
138
|
+
request = self.connection.files().create(
|
|
139
|
+
body={"name": os.path.basename(src_filepath), "parents": [folder_id]},
|
|
140
|
+
media_body=media,
|
|
141
|
+
supportsAllDrives=True,
|
|
142
|
+
)
|
|
143
|
+
response = None
|
|
144
|
+
while response is None:
|
|
145
|
+
status, response = request.next_chunk()
|
|
146
|
+
if status:
|
|
147
|
+
log.debug(f"Upload progress: {int(status.progress() * 100)}%")
|
|
148
|
+
|
|
149
|
+
log.debug(
|
|
150
|
+
f"File {src_filepath} [{naturalsize(os.path.getsize(src_filepath))}] uploaded to {self.generate_gdrive_folder_url(folder_id)}"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def download_gdrive_file(self, file_id: str, dst_filepath: str):
|
|
154
|
+
request = self.connection.files().get_media(
|
|
155
|
+
fileId=file_id, supportsAllDrives=True
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Stream directly to disk
|
|
159
|
+
with open(dst_filepath, "wb") as f:
|
|
160
|
+
downloader = MediaIoBaseDownload(f, request)
|
|
161
|
+
done = False
|
|
162
|
+
while not done:
|
|
163
|
+
_, done = downloader.next_chunk()
|
|
164
|
+
|
|
165
|
+
log.debug(
|
|
166
|
+
f"GDrive file {file_id} downloaded to {dst_filepath} with size {naturalsize(os.path.getsize(dst_filepath))}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def delete(self, file_id: str):
|
|
170
|
+
"""
|
|
171
|
+
Deletes a file from Google Drive using its ID.
|
|
172
|
+
:param file_id: The ID of the file to delete.
|
|
173
|
+
"""
|
|
174
|
+
self.connection.files().delete(fileId=file_id, supportsAllDrives=True).execute()
|
|
175
|
+
log.debug(f"GDrive file with ID {file_id} deleted")
|
|
176
|
+
|
|
177
|
+
# endregion
|
|
178
|
+
|
|
179
|
+
# region Other utilieis
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def generate_gdrive_folder_url(folder_id: str):
|
|
183
|
+
"""
|
|
184
|
+
Generate a valid GDrive folder URL
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
folder_id (str): Folder ID
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
str: A valid GDrive folder URL
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
return f"https://drive.google.com/drive/folders/{folder_id}"
|
|
194
|
+
|
|
195
|
+
# endregion
|
utill/my_input.py
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
from .my_style import italic
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
def ask_yes_no(
|
|
5
|
-
prompt =
|
|
6
|
-
|
|
4
|
+
def ask_yes_no(
|
|
5
|
+
prompt: str = "Continue?",
|
|
6
|
+
yes_strings: tuple[str] = ("y",),
|
|
7
|
+
throw_if_no: bool = False,
|
|
8
|
+
) -> str:
|
|
9
|
+
prompt = f"{prompt} ({yes_strings[0]}/no) : "
|
|
10
|
+
yes = input(f"\n{italic(prompt)}") in yes_strings
|
|
7
11
|
if not yes:
|
|
8
12
|
if throw_if_no:
|
|
9
|
-
raise Exception(
|
|
13
|
+
raise Exception("Aborted by user")
|
|
10
14
|
|
|
11
15
|
return yes
|
utill/my_json.py
CHANGED
|
@@ -6,7 +6,7 @@ def _crawl_dictionary_keys(d: dict, path: tuple = ()) -> list[str]:
|
|
|
6
6
|
paths: list[tuple] = []
|
|
7
7
|
|
|
8
8
|
for key in d.keys():
|
|
9
|
-
key_path = path + (key,
|
|
9
|
+
key_path = path + (key,)
|
|
10
10
|
|
|
11
11
|
# Recursively traverse nested dictionary
|
|
12
12
|
if type(d[key]) is dict:
|
|
@@ -35,11 +35,11 @@ def flatten(data: str | dict) -> list:
|
|
|
35
35
|
|
|
36
36
|
def get_path(data: dict, path: str) -> str:
|
|
37
37
|
if type(data) != dict:
|
|
38
|
-
raise ValueError(
|
|
38
|
+
raise ValueError("data is not a dictionary!")
|
|
39
39
|
|
|
40
|
-
items = path.split(
|
|
40
|
+
items = path.split(".")
|
|
41
41
|
item = items[0]
|
|
42
|
-
path_remaining =
|
|
42
|
+
path_remaining = ".".join(items[1:]) if len(items) > 1 else None
|
|
43
43
|
|
|
44
44
|
if item not in data:
|
|
45
45
|
return None
|
|
@@ -55,8 +55,8 @@ def load_jsonc_file(path) -> dict:
|
|
|
55
55
|
Read a .jsonc (JSON with comment) files, as json.loads cannot read it
|
|
56
56
|
"""
|
|
57
57
|
|
|
58
|
-
with open(path,
|
|
58
|
+
with open(path, "r") as f:
|
|
59
59
|
content = f.read()
|
|
60
60
|
pattern = r'("(?:\\.|[^"\\])*")|\/\/.*|\/\*[\s\S]*?\*\/'
|
|
61
|
-
content = re.sub(pattern, lambda m: m.group(1) if m.group(1) else
|
|
61
|
+
content = re.sub(pattern, lambda m: m.group(1) if m.group(1) else "", content)
|
|
62
62
|
return json.loads(content)
|