zhmiscellany 6.2.3__py3-none-any.whl → 6.2.5__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.
- zhmiscellany/__init__.py +2 -2
- zhmiscellany/_discord_supportfuncs.py +7 -10
- zhmiscellany/_fileio_supportfuncs.py +1 -2
- zhmiscellany/_misc_supportfuncs.py +248 -147
- zhmiscellany/_processing_supportfuncs.py +29 -13
- zhmiscellany/_py_resources.py +1 -1
- zhmiscellany/dict.py +1 -3
- zhmiscellany/discord.py +45 -23
- zhmiscellany/fileio.py +70 -29
- zhmiscellany/gui.py +19 -12
- zhmiscellany/image.py +7 -8
- zhmiscellany/list.py +1 -2
- zhmiscellany/macro.py +40 -44
- zhmiscellany/math.py +3 -3
- zhmiscellany/misc.py +43 -18
- zhmiscellany/netio.py +11 -13
- zhmiscellany/pastebin.py +9 -8
- zhmiscellany/pipes.py +13 -15
- zhmiscellany/processing.py +25 -8
- zhmiscellany/rust.py +3 -1
- zhmiscellany/string.py +3 -3
- {zhmiscellany-6.2.3.dist-info → zhmiscellany-6.2.5.dist-info}/METADATA +1 -1
- zhmiscellany-6.2.5.dist-info/RECORD +27 -0
- zhmiscellany-6.2.3.dist-info/RECORD +0 -27
- {zhmiscellany-6.2.3.dist-info → zhmiscellany-6.2.5.dist-info}/WHEEL +0 -0
- {zhmiscellany-6.2.3.dist-info → zhmiscellany-6.2.5.dist-info}/top_level.txt +0 -0
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
# these lines are purposefully the first thing to run when zhmiscellany is imported
|
|
2
|
-
import
|
|
3
|
-
from itertools import chain
|
|
4
|
-
import zhmiscellany.fileio
|
|
5
|
-
from io import StringIO
|
|
6
|
-
import sys
|
|
7
|
-
import io
|
|
8
|
-
from unittest.mock import patch
|
|
2
|
+
import sys # cannot be moved
|
|
9
3
|
|
|
10
4
|
# Ray availability check
|
|
11
5
|
if sys.platform == "win32" or True:
|
|
12
|
-
|
|
13
|
-
import ray
|
|
14
|
-
RAY_AVAILABLE = True
|
|
15
|
-
except ImportError:
|
|
16
|
-
RAY_AVAILABLE = False
|
|
6
|
+
RAY_AVAILABLE = True
|
|
17
7
|
else:
|
|
18
8
|
RAY_AVAILABLE = False
|
|
19
9
|
|
|
10
|
+
import os # needed for module-level log clearing and cause detection
|
|
20
11
|
|
|
21
12
|
def clear_logs():
|
|
13
|
+
import tempfile
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import zhmiscellany.fileio
|
|
22
17
|
ray_dir = tempfile.gettempdir()
|
|
23
18
|
ray_dir = os.path.join(ray_dir, 'ray')
|
|
24
19
|
|
|
@@ -50,6 +45,7 @@ if 'ray_logs_cleared' not in os.environ:
|
|
|
50
45
|
|
|
51
46
|
|
|
52
47
|
def safe_open_log(path, unbuffered=False, **kwargs):
|
|
48
|
+
import os
|
|
53
49
|
try:
|
|
54
50
|
kwargs.setdefault("buffering", 1)
|
|
55
51
|
kwargs.setdefault("mode", "a")
|
|
@@ -69,6 +65,8 @@ def safe_open_log(path, unbuffered=False, **kwargs):
|
|
|
69
65
|
|
|
70
66
|
|
|
71
67
|
def ray_init(auto=False):
|
|
68
|
+
import threading
|
|
69
|
+
import os
|
|
72
70
|
if not RAY_AVAILABLE:
|
|
73
71
|
print("ray_init() only supports Windows! Functionality disabled")
|
|
74
72
|
return
|
|
@@ -100,6 +98,10 @@ def _ray_init():
|
|
|
100
98
|
|
|
101
99
|
try:
|
|
102
100
|
def safe_ray_init():
|
|
101
|
+
import sys
|
|
102
|
+
import io
|
|
103
|
+
import ray
|
|
104
|
+
|
|
103
105
|
def ensure_valid_handles():
|
|
104
106
|
"""Ensure stdout and stderr are valid file-like objects"""
|
|
105
107
|
if not hasattr(sys.stdout, 'write') or sys.stdout.closed:
|
|
@@ -140,6 +142,7 @@ def _ray_init():
|
|
|
140
142
|
|
|
141
143
|
|
|
142
144
|
def get_import_chain():
|
|
145
|
+
import inspect
|
|
143
146
|
frame = inspect.currentframe()
|
|
144
147
|
chain = []
|
|
145
148
|
while frame:
|
|
@@ -153,6 +156,8 @@ def get_import_chain():
|
|
|
153
156
|
frame = frame.f_back
|
|
154
157
|
return chain[::-1]
|
|
155
158
|
|
|
159
|
+
|
|
160
|
+
# Cause detection for auto-initializing ray
|
|
156
161
|
cause_strings = [
|
|
157
162
|
'processing.multiprocess(',
|
|
158
163
|
'processing.batch_multiprocess(',
|
|
@@ -175,6 +180,7 @@ for file in cause_files:
|
|
|
175
180
|
cause = True
|
|
176
181
|
break
|
|
177
182
|
|
|
183
|
+
import threading
|
|
178
184
|
_ray_init_thread = threading.Thread() # initialize variable to completed thread
|
|
179
185
|
_ray_init_thread.start()
|
|
180
186
|
|
|
@@ -198,6 +204,8 @@ class ThreadWithResult(threading.Thread):
|
|
|
198
204
|
|
|
199
205
|
|
|
200
206
|
def batch_multiprocess(targets_and_args, max_retries=0, expect_crashes=False, disable_warning=False, flatten=False):
|
|
207
|
+
import logging
|
|
208
|
+
from itertools import chain
|
|
201
209
|
if not RAY_AVAILABLE:
|
|
202
210
|
print("batch_multiprocess() only supports Windows! Returning empty list")
|
|
203
211
|
return []
|
|
@@ -217,6 +225,7 @@ from zhmiscellany._processing_supportfuncs import _ray_init_thread; _ray_init_th
|
|
|
217
225
|
_ray_init_thread.join()
|
|
218
226
|
|
|
219
227
|
if not expect_crashes:
|
|
228
|
+
import ray
|
|
220
229
|
@ray.remote(max_retries=max_retries, num_cpus=0)
|
|
221
230
|
def worker(func, *args):
|
|
222
231
|
return func(*args)
|
|
@@ -227,12 +236,14 @@ from zhmiscellany._processing_supportfuncs import _ray_init_thread; _ray_init_th
|
|
|
227
236
|
results = list(chain.from_iterable(results))
|
|
228
237
|
return results
|
|
229
238
|
else:
|
|
239
|
+
import ray
|
|
230
240
|
def wrap_exception(task, disable_warning, max_retries):
|
|
231
241
|
try:
|
|
232
242
|
result = multiprocess(*task, disable_warning=disable_warning, max_retries=max_retries)
|
|
233
243
|
return result
|
|
234
244
|
except ray.exceptions.WorkerCrashedError:
|
|
235
245
|
return None
|
|
246
|
+
import threading # this import is explicitly in the original code
|
|
236
247
|
threads = []
|
|
237
248
|
for task in targets_and_args:
|
|
238
249
|
t = ThreadWithResult(
|
|
@@ -248,6 +259,7 @@ from zhmiscellany._processing_supportfuncs import _ray_init_thread; _ray_init_th
|
|
|
248
259
|
results = list(chain.from_iterable(results))
|
|
249
260
|
return results
|
|
250
261
|
|
|
262
|
+
|
|
251
263
|
def multiprocess(target, args=(), max_retries=0, disable_warning=False):
|
|
252
264
|
if not RAY_AVAILABLE:
|
|
253
265
|
print("multiprocess() only supports Windows! Returning None")
|
|
@@ -259,10 +271,12 @@ class RayActorWrapper:
|
|
|
259
271
|
def __init__(self, actor_instance):
|
|
260
272
|
self._actor = actor_instance
|
|
261
273
|
|
|
274
|
+
import ray
|
|
262
275
|
ray.get(self._actor._ready.remote())
|
|
263
276
|
|
|
264
277
|
def __getattr__(self, name):
|
|
265
278
|
# When you access an attribute, assume it's a remote method.
|
|
279
|
+
import ray
|
|
266
280
|
remote_method = getattr(self._actor, name)
|
|
267
281
|
if not callable(remote_method):
|
|
268
282
|
# If it's not callable, try to get its value.
|
|
@@ -278,6 +292,8 @@ class RayActorWrapper:
|
|
|
278
292
|
|
|
279
293
|
|
|
280
294
|
def synchronous_class_multiprocess(cls, *args, disable_warning=False, **kwargs):
|
|
295
|
+
import logging
|
|
296
|
+
import ray
|
|
281
297
|
if not RAY_AVAILABLE:
|
|
282
298
|
print("synchronous_class_multiprocess() only supports Windows! Returning None")
|
|
283
299
|
return None
|
|
@@ -303,4 +319,4 @@ from zhmiscellany._processing_supportfuncs import _ray_init_thread; _ray_init_th
|
|
|
303
319
|
|
|
304
320
|
remote_cls = ray.remote(num_cpus=0)(cls)
|
|
305
321
|
actor_instance = remote_cls.remote(*args, **kwargs)
|
|
306
|
-
return RayActorWrapper(actor_instance)
|
|
322
|
+
return RayActorWrapper(actor_instance)
|
zhmiscellany/_py_resources.py
CHANGED
zhmiscellany/dict.py
CHANGED
zhmiscellany/discord.py
CHANGED
|
@@ -1,32 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
import sys
|
|
3
|
-
import requests
|
|
4
|
-
import copy
|
|
5
|
-
import zhmiscellany.fileio
|
|
6
|
-
import zhmiscellany.netio
|
|
7
|
-
import zhmiscellany.processing
|
|
1
|
+
import sys # cannot be touched because it's needed
|
|
8
2
|
from ._discord_supportfuncs import scrape_guild
|
|
9
3
|
|
|
10
|
-
import base64
|
|
11
|
-
import os
|
|
12
|
-
import json
|
|
13
|
-
import re
|
|
14
|
-
|
|
15
4
|
# Windows-specific imports
|
|
16
5
|
if sys.platform == "win32":
|
|
17
|
-
|
|
18
|
-
import win32crypt
|
|
19
|
-
from Crypto.Cipher import AES
|
|
20
|
-
WIN32_AVAILABLE = True
|
|
21
|
-
except ImportError:
|
|
22
|
-
WIN32_AVAILABLE = False
|
|
23
|
-
print("Warning: Windows modules not available - local Discord user detection disabled")
|
|
6
|
+
WIN32_AVAILABLE = True
|
|
24
7
|
else:
|
|
25
8
|
WIN32_AVAILABLE = False
|
|
26
9
|
|
|
27
10
|
|
|
28
11
|
def add_reactions_to_message(user_token, emojis, channel_id, message_id):
|
|
29
|
-
|
|
12
|
+
import time
|
|
13
|
+
import requests
|
|
30
14
|
for emoji in emojis:
|
|
31
15
|
url = f'https://discord.com/api/v9/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me'
|
|
32
16
|
#headers = {**zhmiscellany.netio.generate_headers(url), 'Authorization': user_token}
|
|
@@ -54,7 +38,9 @@ def get_channel_messages(user_token, channel_id, limit=0, use_cache=True, show_p
|
|
|
54
38
|
'''
|
|
55
39
|
Function to get all client messages in a specific channel. Script by @z_h_ on discord.
|
|
56
40
|
'''
|
|
57
|
-
|
|
41
|
+
import requests
|
|
42
|
+
import zhmiscellany.fileio
|
|
43
|
+
import os
|
|
58
44
|
if use_cache:
|
|
59
45
|
cache_folder = 'zhmiscellany_cache'
|
|
60
46
|
zhmiscellany.fileio.create_folder(cache_folder)
|
|
@@ -118,10 +104,18 @@ def get_channel_messages(user_token, channel_id, limit=0, use_cache=True, show_p
|
|
|
118
104
|
|
|
119
105
|
|
|
120
106
|
def get_local_discord_user(show_output=False):
|
|
107
|
+
import requests
|
|
108
|
+
import os
|
|
109
|
+
import json
|
|
110
|
+
import re
|
|
111
|
+
import base64
|
|
112
|
+
import win32crypt
|
|
113
|
+
from Crypto.Cipher import AES
|
|
114
|
+
import zhmiscellany.netio
|
|
121
115
|
if not WIN32_AVAILABLE:
|
|
122
116
|
print("get_local_discord_user() only supports Windows! Returning None")
|
|
123
117
|
return None
|
|
124
|
-
|
|
118
|
+
|
|
125
119
|
global _cached_user_info
|
|
126
120
|
try:
|
|
127
121
|
a = _cached_user_info
|
|
@@ -241,6 +235,9 @@ def get_local_discord_user(show_output=False):
|
|
|
241
235
|
|
|
242
236
|
|
|
243
237
|
def get_guild_channels(user_token, guild_id, use_cache=True):
|
|
238
|
+
import requests
|
|
239
|
+
import os
|
|
240
|
+
import zhmiscellany.netio
|
|
244
241
|
if use_cache:
|
|
245
242
|
potential_path = os.path.join('zhmiscellany_cache', f'{guild_id}_channels.json')
|
|
246
243
|
if os.path.exists(potential_path):
|
|
@@ -260,12 +257,17 @@ def get_guild_channels(user_token, guild_id, use_cache=True):
|
|
|
260
257
|
|
|
261
258
|
|
|
262
259
|
def send_type(user_token, channel_id): # after sending the typing post request, the account will be shown as "typing" in the given channel for 10 seconds, or until a message is sent.
|
|
260
|
+
import requests
|
|
261
|
+
import zhmiscellany.netio
|
|
263
262
|
url = f'https://discord.com/api/v9/channels/{channel_id}/typing'
|
|
264
263
|
headers = {**zhmiscellany.netio.generate_headers(url), 'Authorization': user_token}
|
|
265
264
|
return requests.post(url, headers=headers)
|
|
266
265
|
|
|
267
266
|
|
|
268
267
|
def send_message(user_token, text, channel_id, attachments=None, typing_time=0):
|
|
268
|
+
import time
|
|
269
|
+
import requests
|
|
270
|
+
import zhmiscellany.processing
|
|
269
271
|
typing_time_increments = 9.5 # not set to 10 because then every 10 seconds the typing would stop very briefly
|
|
270
272
|
while typing_time > 0:
|
|
271
273
|
zhmiscellany.processing.start_daemon(target=send_type, args=(user_token, channel_id))
|
|
@@ -294,6 +296,8 @@ def send_message(user_token, text, channel_id, attachments=None, typing_time=0):
|
|
|
294
296
|
|
|
295
297
|
|
|
296
298
|
def get_message(user_token, channel_id, message_id):
|
|
299
|
+
import requests
|
|
300
|
+
import zhmiscellany.netio
|
|
297
301
|
message_url = f'https://discord.com/api/v9/channels/{channel_id}/messages?limit=1&around={message_id}'
|
|
298
302
|
message = requests.get(message_url, headers={**zhmiscellany.netio.generate_headers(message_url), 'Authorization': user_token})
|
|
299
303
|
message = message.json()
|
|
@@ -308,6 +312,7 @@ def ids_to_message_url(channel_id, message_id, guild_id=None):
|
|
|
308
312
|
|
|
309
313
|
|
|
310
314
|
def message_url_to_ids(message_url):
|
|
315
|
+
import re
|
|
311
316
|
# Regular expressions to extract IDs
|
|
312
317
|
guild_channel_message_regex = r'https:\/\/discord\.com\/channels\/(\d+)\/(\d+)\/(\d+)'
|
|
313
318
|
channel_message_regex = r'https:\/\/discord\.com\/channels\/(\d+)\/(\d+)'
|
|
@@ -336,6 +341,8 @@ def decode_user_id(user_token):
|
|
|
336
341
|
padded_base64 = payload_base64 + '=' * (4 - len(payload_base64) % 4)
|
|
337
342
|
|
|
338
343
|
# Decoding the base64 and converting to a JSON object
|
|
344
|
+
import base64
|
|
345
|
+
import json
|
|
339
346
|
payload_json = base64.b64decode(padded_base64).decode('utf-8')
|
|
340
347
|
user_id = json.loads(payload_json)
|
|
341
348
|
|
|
@@ -343,6 +350,9 @@ def decode_user_id(user_token):
|
|
|
343
350
|
|
|
344
351
|
|
|
345
352
|
def get_guilds(user_token, use_cache=True):
|
|
353
|
+
import requests
|
|
354
|
+
import os
|
|
355
|
+
import zhmiscellany.netio
|
|
346
356
|
if use_cache:
|
|
347
357
|
potential_path = os.path.join('zhmiscellany_cache', f'{decode_user_id(user_token)}_guilds.json')
|
|
348
358
|
if os.path.exists(potential_path):
|
|
@@ -361,6 +371,9 @@ def get_guilds(user_token, use_cache=True):
|
|
|
361
371
|
|
|
362
372
|
|
|
363
373
|
def get_dm_channels(user_token, use_cache=True):
|
|
374
|
+
import requests
|
|
375
|
+
import os
|
|
376
|
+
import zhmiscellany.netio
|
|
364
377
|
if use_cache:
|
|
365
378
|
potential_path = os.path.join('zhmiscellany_cache', f'{decode_user_id(user_token)}_dm_channels.json')
|
|
366
379
|
if os.path.exists(potential_path):
|
|
@@ -379,6 +392,9 @@ def get_dm_channels(user_token, use_cache=True):
|
|
|
379
392
|
|
|
380
393
|
|
|
381
394
|
def get_invite_info(user_token, invite_code, use_cache=True):
|
|
395
|
+
import requests
|
|
396
|
+
import os
|
|
397
|
+
import zhmiscellany.netio
|
|
382
398
|
if use_cache:
|
|
383
399
|
potential_path = os.path.join('zhmiscellany_cache', f'{invite_code}_invite.json')
|
|
384
400
|
if os.path.exists(potential_path):
|
|
@@ -397,6 +413,8 @@ def get_invite_info(user_token, invite_code, use_cache=True):
|
|
|
397
413
|
|
|
398
414
|
|
|
399
415
|
def generate_server_invite(user_token, channel_id):
|
|
416
|
+
import zhmiscellany.netio
|
|
417
|
+
import requests
|
|
400
418
|
url = f"https://discord.com/api/v9/channels/{channel_id}/invites"
|
|
401
419
|
response = requests.get(url, headers={**zhmiscellany.netio.generate_headers(url), 'Authorization': user_token})
|
|
402
420
|
|
|
@@ -408,6 +426,8 @@ def generate_server_invite(user_token, channel_id):
|
|
|
408
426
|
|
|
409
427
|
|
|
410
428
|
def get_approximate_member_count(user_token, channel_id, use_cache=True):
|
|
429
|
+
import os
|
|
430
|
+
import zhmiscellany.netio
|
|
411
431
|
if use_cache:
|
|
412
432
|
potential_path = os.path.join('zhmiscellany_cache', f'{channel_id}_member_count.json')
|
|
413
433
|
if os.path.exists(potential_path):
|
|
@@ -429,12 +449,14 @@ def id_to_timestamp(id):
|
|
|
429
449
|
|
|
430
450
|
|
|
431
451
|
def timestamp_to_id(timestamp):
|
|
432
|
-
id = int(id)
|
|
433
452
|
DISCORD_EPOCH = 1420070400000
|
|
434
453
|
return int((timestamp * 1000 - DISCORD_EPOCH) * 4194304)
|
|
435
454
|
|
|
436
455
|
|
|
437
456
|
def get_user_avatar_url(user_token, user_id, use_cache=True):
|
|
457
|
+
import requests
|
|
458
|
+
import os
|
|
459
|
+
import zhmiscellany.netio
|
|
438
460
|
url = f"https://discord.com/api/v10/users/{user_id}"
|
|
439
461
|
|
|
440
462
|
if use_cache:
|
zhmiscellany/fileio.py
CHANGED
|
@@ -1,22 +1,9 @@
|
|
|
1
|
-
from ._fileio_supportfuncs import is_junction
|
|
2
|
-
import json, os, shutil, dill, sys, pickle, base64, zlib
|
|
3
|
-
import zhmiscellany.string
|
|
4
|
-
import zhmiscellany.misc
|
|
5
|
-
import hashlib
|
|
6
|
-
from collections import defaultdict
|
|
7
|
-
from itertools import chain
|
|
8
|
-
import tempfile
|
|
9
|
-
import random
|
|
10
|
-
import string
|
|
11
|
-
import orjson
|
|
12
|
-
from datetime import datetime
|
|
13
|
-
import inspect
|
|
14
|
-
|
|
15
|
-
|
|
16
1
|
def read_json_file(file_path):
|
|
17
2
|
"""
|
|
18
3
|
Reads JSON data from a file and returns it as a dictionary.
|
|
19
4
|
"""
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
20
7
|
if os.path.exists(file_path):
|
|
21
8
|
with open(file_path, 'r') as file:
|
|
22
9
|
data = json.load(file)
|
|
@@ -31,61 +18,65 @@ def write_json_file(file_path, data):
|
|
|
31
18
|
"""
|
|
32
19
|
Writes a dictionary to a JSON file.
|
|
33
20
|
"""
|
|
21
|
+
import json
|
|
34
22
|
with open(file_path, 'w') as file:
|
|
35
23
|
json.dump(data, file, indent=4)
|
|
36
24
|
|
|
37
25
|
|
|
38
26
|
def create_folder(folder_name):
|
|
27
|
+
import os
|
|
39
28
|
if not os.path.exists(folder_name):
|
|
40
29
|
os.makedirs(folder_name)
|
|
41
30
|
|
|
42
31
|
|
|
43
32
|
def remove_folder(folder_name):
|
|
33
|
+
import os
|
|
34
|
+
import shutil
|
|
44
35
|
if os.path.exists(folder_name):
|
|
45
36
|
shutil.rmtree(folder_name)
|
|
46
37
|
|
|
47
38
|
|
|
48
39
|
def base_name_no_ext(file_path):
|
|
40
|
+
import os
|
|
49
41
|
base_name = os.path.basename(file_path)
|
|
50
42
|
base_name_without_extension, _ = os.path.splitext(base_name)
|
|
51
43
|
return base_name_without_extension
|
|
52
44
|
|
|
53
45
|
|
|
54
46
|
def convert_name_to_filename(name):
|
|
47
|
+
import zhmiscellany.string
|
|
55
48
|
return zhmiscellany.string.multi_replace(name, [("/","["), (":","]"), (".","+")])
|
|
56
49
|
|
|
57
50
|
|
|
58
51
|
def convert_filename_to_name(filename):
|
|
52
|
+
import zhmiscellany.string
|
|
59
53
|
return zhmiscellany.string.multi_replace(filename, [("[","/"), ("]",":"), ("+",".")])
|
|
60
54
|
|
|
61
55
|
|
|
62
56
|
def recursive_copy_files(source_dir, destination_dir, prints=False):
|
|
57
|
+
import os
|
|
58
|
+
import shutil
|
|
63
59
|
if prints:
|
|
64
60
|
print('Validating matching directory structure')
|
|
65
61
|
for root, dirs, files in os.walk(source_dir):
|
|
66
62
|
for dir in dirs:
|
|
67
63
|
dir_path = os.path.join(root, dir)
|
|
68
64
|
dest_dir_path = os.path.join(destination_dir, os.path.relpath(dir_path, source_dir))
|
|
69
|
-
|
|
70
65
|
if not os.path.exists(dest_dir_path):
|
|
71
66
|
print(f'Creating missing directory {dest_dir_path}')
|
|
72
67
|
os.makedirs(dest_dir_path)
|
|
73
|
-
|
|
74
68
|
if prints:
|
|
75
69
|
print('Getting a list of files in the source directory')
|
|
76
70
|
source_files = []
|
|
77
71
|
for root, _, files in os.walk(source_dir):
|
|
78
72
|
for file in files:
|
|
79
73
|
source_files.append(os.path.join(root, file))
|
|
80
|
-
|
|
81
74
|
if prints:
|
|
82
75
|
print('Getting a list of files in the destination directory')
|
|
83
76
|
dest_files = []
|
|
84
77
|
for root, _, files in os.walk(destination_dir):
|
|
85
78
|
for file in files:
|
|
86
79
|
dest_files.append(os.path.join(root, file))
|
|
87
|
-
|
|
88
|
-
|
|
89
80
|
if prints:
|
|
90
81
|
print('Copying files from source to destination, skipping duplicates')
|
|
91
82
|
for root, dirs, files in os.walk(source_dir):
|
|
@@ -93,7 +84,6 @@ def recursive_copy_files(source_dir, destination_dir, prints=False):
|
|
|
93
84
|
source_file = os.path.join(root, file)
|
|
94
85
|
rel_path = os.path.relpath(source_file, source_dir)
|
|
95
86
|
dest_file = os.path.join(destination_dir, rel_path)
|
|
96
|
-
|
|
97
87
|
if not os.path.exists(dest_file):
|
|
98
88
|
if prints:
|
|
99
89
|
print(f'Copying {source_file}')
|
|
@@ -105,10 +95,11 @@ def recursive_copy_files(source_dir, destination_dir, prints=False):
|
|
|
105
95
|
|
|
106
96
|
|
|
107
97
|
def empty_directory(directory_path):
|
|
98
|
+
import os
|
|
99
|
+
import shutil
|
|
108
100
|
# Iterate over all items in the directory
|
|
109
101
|
for item in os.listdir(directory_path):
|
|
110
102
|
item_path = os.path.join(directory_path, item)
|
|
111
|
-
|
|
112
103
|
if os.path.isfile(item_path):
|
|
113
104
|
# If it's a file, delete it
|
|
114
105
|
os.unlink(item_path)
|
|
@@ -118,10 +109,12 @@ def empty_directory(directory_path):
|
|
|
118
109
|
|
|
119
110
|
|
|
120
111
|
def abs_listdir(path):
|
|
112
|
+
import os
|
|
121
113
|
return [os.path.join(path, file) for file in os.listdir(path)]
|
|
122
114
|
|
|
123
115
|
|
|
124
116
|
def delete_ends_with(directory, string_endswith, avoid=[]):
|
|
117
|
+
import os
|
|
125
118
|
files = abs_listdir(directory)
|
|
126
119
|
for file in files:
|
|
127
120
|
if file.endswith(string_endswith):
|
|
@@ -134,17 +127,20 @@ def read_bytes_section(file_path, section_start, section_end):
|
|
|
134
127
|
file.seek(section_start) # Move the file pointer to the 'start' position
|
|
135
128
|
bytes_to_read = section_end - section_start
|
|
136
129
|
data = file.read(bytes_to_read) # Read 'bytes_to_read' number of bytes
|
|
137
|
-
|
|
138
130
|
return data
|
|
139
131
|
|
|
140
132
|
|
|
141
133
|
def copy_file_with_overwrite(src, dst):
|
|
134
|
+
import os
|
|
135
|
+
import shutil
|
|
142
136
|
if os.path.exists(dst):
|
|
143
137
|
os.remove(dst)
|
|
144
138
|
shutil.copy2(src, dst)
|
|
145
139
|
|
|
146
140
|
|
|
147
141
|
def fast_dill_dumps(object):
|
|
142
|
+
import pickle
|
|
143
|
+
import dill
|
|
148
144
|
try:
|
|
149
145
|
data = pickle.dumps(object, protocol=5) # pickle is much faster so at least attempt to use it at first
|
|
150
146
|
except:
|
|
@@ -153,6 +149,8 @@ def fast_dill_dumps(object):
|
|
|
153
149
|
|
|
154
150
|
|
|
155
151
|
def fast_dill_loads(data):
|
|
152
|
+
import pickle
|
|
153
|
+
import dill
|
|
156
154
|
try:
|
|
157
155
|
object = pickle.loads(data) # pickle is much faster so at least attempt to use it at first
|
|
158
156
|
except:
|
|
@@ -161,6 +159,7 @@ def fast_dill_loads(data):
|
|
|
161
159
|
|
|
162
160
|
|
|
163
161
|
def save_object_to_file(object, file_name, compressed=False):
|
|
162
|
+
import zlib
|
|
164
163
|
with open(file_name, 'wb') as f:
|
|
165
164
|
if compressed:
|
|
166
165
|
f.write(zlib.compress(fast_dill_dumps(object)))
|
|
@@ -169,6 +168,7 @@ def save_object_to_file(object, file_name, compressed=False):
|
|
|
169
168
|
|
|
170
169
|
|
|
171
170
|
def load_object_from_file(file_name, compressed=False):
|
|
171
|
+
import zlib
|
|
172
172
|
with open(file_name, 'rb') as f:
|
|
173
173
|
if compressed:
|
|
174
174
|
return fast_dill_loads(zlib.decompress(f.read()))
|
|
@@ -178,26 +178,33 @@ def load_object_from_file(file_name, compressed=False):
|
|
|
178
178
|
|
|
179
179
|
def pickle_and_encode(obj):
|
|
180
180
|
"""Pickles an object and URL-safe encodes it."""
|
|
181
|
+
import base64
|
|
182
|
+
import zlib
|
|
181
183
|
pickled_data = zlib.compress(fast_dill_dumps(obj), 9) # Serialize the object
|
|
182
184
|
encoded_data = base64.urlsafe_b64encode(pickled_data).decode() # Base64 encode
|
|
183
185
|
return encoded_data
|
|
184
186
|
|
|
187
|
+
|
|
185
188
|
def decode_and_unpickle(encoded_str):
|
|
186
189
|
"""Decodes a URL-safe encoded string and unpickles the object."""
|
|
190
|
+
import base64
|
|
191
|
+
import zlib
|
|
187
192
|
pickled_data = base64.urlsafe_b64decode(encoded_str) # Decode from Base64
|
|
188
193
|
obj = fast_dill_loads(zlib.decompress(pickled_data)) # Deserialize
|
|
189
194
|
return obj
|
|
190
195
|
|
|
191
196
|
|
|
192
197
|
def list_files_by_modified_time(directory):
|
|
198
|
+
import os
|
|
193
199
|
files_with_times = [(file, os.path.getmtime(os.path.join(directory, file))) for file in os.listdir(directory) if os.path.isfile(os.path.join(directory, file))]
|
|
194
200
|
sorted_files = sorted(files_with_times, key=lambda x: x[1], reverse=True)
|
|
195
201
|
sorted_file_names = [file for file, _ in sorted_files]
|
|
196
|
-
|
|
197
202
|
return sorted_file_names
|
|
198
203
|
|
|
199
204
|
|
|
200
205
|
def get_script_path():
|
|
206
|
+
"""Returns the path to the current script or executable."""
|
|
207
|
+
import sys
|
|
201
208
|
if getattr(sys, 'frozen', False):
|
|
202
209
|
# Running as a standalone executable
|
|
203
210
|
return sys.executable
|
|
@@ -207,10 +214,21 @@ def get_script_path():
|
|
|
207
214
|
|
|
208
215
|
|
|
209
216
|
def chdir_to_script_dir():
|
|
217
|
+
import os
|
|
210
218
|
os.chdir(os.path.dirname(get_script_path()))
|
|
211
219
|
|
|
212
220
|
|
|
213
221
|
def cache(function, *args, **kwargs):
|
|
222
|
+
"""
|
|
223
|
+
Caches the result of a function call to disk.
|
|
224
|
+
"""
|
|
225
|
+
import os
|
|
226
|
+
import inspect
|
|
227
|
+
import orjson
|
|
228
|
+
import hashlib
|
|
229
|
+
from datetime import datetime
|
|
230
|
+
import zhmiscellany.fileio
|
|
231
|
+
|
|
214
232
|
cache_folder = 'zhmiscellany_cache'
|
|
215
233
|
|
|
216
234
|
def get_hash_orjson(data):
|
|
@@ -267,6 +285,10 @@ def cache(function, *args, **kwargs):
|
|
|
267
285
|
|
|
268
286
|
|
|
269
287
|
def load_all_cached():
|
|
288
|
+
"""
|
|
289
|
+
Loads all cached objects from the cache folder.
|
|
290
|
+
"""
|
|
291
|
+
import os
|
|
270
292
|
cache_folder = 'zhmiscellany_cache'
|
|
271
293
|
if os.path.exists(cache_folder):
|
|
272
294
|
files = abs_listdir(cache_folder)
|
|
@@ -280,6 +302,11 @@ def load_all_cached():
|
|
|
280
302
|
|
|
281
303
|
|
|
282
304
|
def list_files_recursive(folder):
|
|
305
|
+
"""
|
|
306
|
+
Recursively lists all files in a directory, excluding symlinks and junctions.
|
|
307
|
+
"""
|
|
308
|
+
import os
|
|
309
|
+
from ._fileio_supportfuncs import is_junction
|
|
283
310
|
files = []
|
|
284
311
|
try:
|
|
285
312
|
for entry in os.scandir(folder):
|
|
@@ -295,6 +322,9 @@ def list_files_recursive(folder):
|
|
|
295
322
|
|
|
296
323
|
|
|
297
324
|
def list_files_recursive_multiprocessed(dir_path, return_folders=False):
|
|
325
|
+
import os
|
|
326
|
+
import zhmiscellany.processing
|
|
327
|
+
|
|
298
328
|
def is_junction(entry):
|
|
299
329
|
try:
|
|
300
330
|
st = entry.stat(follow_symlinks=False)
|
|
@@ -342,6 +372,8 @@ def list_files_recursive_multiprocessed(dir_path, return_folders=False):
|
|
|
342
372
|
|
|
343
373
|
def encode_safe_filename(s, max_length=16):
|
|
344
374
|
"""Encodes a string into a short, URL-safe, and file name-safe string."""
|
|
375
|
+
import base64
|
|
376
|
+
import hashlib
|
|
345
377
|
encoded = base64.urlsafe_b64encode(s.encode()).decode().rstrip("=") # URL-safe encoding
|
|
346
378
|
if len(encoded) > max_length: # Truncate if too long
|
|
347
379
|
encoded = hashlib.md5(s.encode()).hexdigest()[:max_length] # Use a hash
|
|
@@ -349,6 +381,15 @@ def encode_safe_filename(s, max_length=16):
|
|
|
349
381
|
|
|
350
382
|
|
|
351
383
|
def list_files_recursive_cache_optimised_multiprocessed(dir_path, show_timings=False, cache_in_temp=True):
|
|
384
|
+
import os
|
|
385
|
+
import zhmiscellany.processing
|
|
386
|
+
import zhmiscellany.fileio
|
|
387
|
+
import tempfile
|
|
388
|
+
from collections import defaultdict
|
|
389
|
+
import random
|
|
390
|
+
from itertools import chain
|
|
391
|
+
import zhmiscellany.misc
|
|
392
|
+
|
|
352
393
|
def is_junction(entry):
|
|
353
394
|
try:
|
|
354
395
|
st = entry.stat(follow_symlinks=False)
|
|
@@ -501,7 +542,6 @@ def list_files_recursive_cache_optimised_multiprocessed(dir_path, show_timings=F
|
|
|
501
542
|
|
|
502
543
|
groups = split_into_n_groups(changed_folders, scan_changed_folders_thread_group_count)
|
|
503
544
|
tasks = [(atom, (group,)) for group in groups]
|
|
504
|
-
|
|
505
545
|
if not tasks:
|
|
506
546
|
results = []
|
|
507
547
|
else:
|
|
@@ -516,19 +556,16 @@ def list_files_recursive_cache_optimised_multiprocessed(dir_path, show_timings=F
|
|
|
516
556
|
if len(changed_folders) > fully_update_cache_threshold:
|
|
517
557
|
new_folders.update(get_m_times(new_new_folders))
|
|
518
558
|
if show_timings: zhmiscellany.misc.time_it(f'get m times of {len(new_new_folders)} new folders')
|
|
519
|
-
|
|
520
559
|
zhmiscellany.fileio.save_object_to_file((files, new_folders), cache_file)
|
|
521
|
-
|
|
522
560
|
if show_timings: zhmiscellany.misc.time_it(f'writing to cache')
|
|
523
561
|
|
|
524
562
|
ret = list(chain.from_iterable(files.values()))
|
|
525
|
-
|
|
526
563
|
if show_timings: zhmiscellany.misc.time_it('Everything together', 'lfrcomt')
|
|
527
|
-
|
|
528
564
|
return ret
|
|
529
565
|
|
|
530
566
|
|
|
531
567
|
def save_chunk(name, data):
|
|
568
|
+
import zhmiscellany.string
|
|
532
569
|
create_folder(name)
|
|
533
570
|
chunk_path = f'{name}/chunk_{zhmiscellany.string.get_universally_unique_string()}.pkl'
|
|
534
571
|
save_object_to_file(data, chunk_path)
|
|
@@ -544,8 +581,12 @@ def load_chunks(name):
|
|
|
544
581
|
|
|
545
582
|
|
|
546
583
|
def clear_chunks(name):
|
|
584
|
+
import os
|
|
547
585
|
if os.path.exists(name):
|
|
548
586
|
empty_directory(name)
|
|
549
587
|
|
|
588
|
+
|
|
550
589
|
def list_drives():
|
|
590
|
+
import os
|
|
591
|
+
import string
|
|
551
592
|
return [f"{d}:\\" for d in string.ascii_uppercase if os.path.exists(f"{d}:\\")]
|