gu-funclib 1.8.3__tar.gz
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.
- gu_funclib-1.8.3/LICENSE +21 -0
- gu_funclib-1.8.3/PKG-INFO +11 -0
- gu_funclib-1.8.3/README.md +66 -0
- gu_funclib-1.8.3/gu_funclib/__init__.py +14 -0
- gu_funclib-1.8.3/gu_funclib/gu_funclib.py +361 -0
- gu_funclib-1.8.3/gu_funclib.egg-info/PKG-INFO +11 -0
- gu_funclib-1.8.3/gu_funclib.egg-info/SOURCES.txt +9 -0
- gu_funclib-1.8.3/gu_funclib.egg-info/dependency_links.txt +1 -0
- gu_funclib-1.8.3/gu_funclib.egg-info/top_level.txt +1 -0
- gu_funclib-1.8.3/pyproject.toml +17 -0
- gu_funclib-1.8.3/setup.cfg +4 -0
gu_funclib-1.8.3/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexander Guryev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gu_funclib
|
|
3
|
+
Version: 1.8.3
|
|
4
|
+
Summary: GU Functions Library
|
|
5
|
+
Author-email: Alexander Guryev <alexguryev@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://alexguryev.com
|
|
8
|
+
Project-URL: Repository, https://github.com/alexguryev/gu_funclib
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# gu_funclib
|
|
2
|
+
|
|
3
|
+
GU Functions Library — personal Python utilities for everyday scripting tasks.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install git+https://github.com/alexguryev/gu_funclib.git
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from gu_funclib import conlog, get_datetime_str, make_unique_filename
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Functions
|
|
18
|
+
|
|
19
|
+
### MISC UTILS
|
|
20
|
+
| Function | Description |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `conclear()` | Clear console |
|
|
23
|
+
| `conlog(s)` | Colored print — `^R` red, `^G` green, `^B` blue, `~` reset |
|
|
24
|
+
| `conlog_arr(arr)` | Pretty-print list/dict |
|
|
25
|
+
| `arr_unify(arr)` | Remove duplicates, preserve order |
|
|
26
|
+
| `split_list_into_chunks(lst, n)` | Split list into chunks of size n |
|
|
27
|
+
| `get_key_by_value(dict, value)` | Reverse dict lookup |
|
|
28
|
+
|
|
29
|
+
### STRING UTILS
|
|
30
|
+
| Function | Description |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `get_datetime_str(dateonly, timeonly, filesafe, utcplus)` | Datetime string, UTC+ time shift |
|
|
33
|
+
| `sec_to_hms(s)` | Seconds → `hh:mm:ss` |
|
|
34
|
+
| `safe_string(s, forfile)` | Replace illegal chars for filename or prompt |
|
|
35
|
+
| `check_arr_elem_in_str(arr, s)` | Check if any array element is substring of s |
|
|
36
|
+
| `strlist_to_str(lst)` | Join list with `, ` |
|
|
37
|
+
| `int_2_str_z(i, zeros)` | Integer with leading zeros |
|
|
38
|
+
| `illeg_in_name(s)` | Detect illegal filename chars |
|
|
39
|
+
| `legalize_name(s, CutNum)` | Replace illegal chars with `_` |
|
|
40
|
+
| `camelcase_name(s)` | Convert to CamelCase |
|
|
41
|
+
| `get_str_tail_number(s)` | Extract trailing number from string |
|
|
42
|
+
| `unique_shortid(length)` | UUID-based short ID (1–32 chars) |
|
|
43
|
+
|
|
44
|
+
### FILE UTILS
|
|
45
|
+
| Function | Description |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `get_filenameext(path)` | `(name, ext)` tuple |
|
|
48
|
+
| `get_pathsplit(path)` | `(dir, name, ext)` tuple |
|
|
49
|
+
| `get_filedir(path)` | Directory of file or path itself if dir |
|
|
50
|
+
| `make_unique_filename(path, digits)` | Auto-numbered filename to avoid overwrite |
|
|
51
|
+
| `is_media_file(path)` | Check for `.png .jpg .wav .mp3 .mp4` |
|
|
52
|
+
| `calc_SHA256(path)` | SHA-256 hex digest |
|
|
53
|
+
| `write_datelog(rootpath, text, indent)` | Append to daily `.log` file |
|
|
54
|
+
| `check_archive(path, extensions)` | Validate ZIP contents |
|
|
55
|
+
| `unpack_archive(path, dest, temporary)` | Extract ZIP |
|
|
56
|
+
| `pack_archive_unique(files, path)` | Create ZIP with deduplicated filenames |
|
|
57
|
+
| `rem_arch_tmp(path)` | Remove temp extraction folder |
|
|
58
|
+
|
|
59
|
+
### NET UTILS
|
|
60
|
+
| Function | Description |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `get_local_ip()` | Local IP address string |
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .gu_funclib import (
|
|
2
|
+
__version__,
|
|
3
|
+
conclear, conlog, conlog_arr,
|
|
4
|
+
arr_unify, split_list_into_chunks, get_key_by_value,
|
|
5
|
+
get_datetime_str, sec_to_hms, safe_string,
|
|
6
|
+
check_arr_elem_in_str, strlist_to_str, int_2_str_z,
|
|
7
|
+
illeg_in_name, legalize_name, camelcase_name,
|
|
8
|
+
get_str_tail_number, unique_shortid,
|
|
9
|
+
get_filenameext, get_pathsplit, get_filedir,
|
|
10
|
+
make_unique_filename, is_media_file, calc_SHA256,
|
|
11
|
+
write_datelog, check_archive, unpack_archive,
|
|
12
|
+
pack_archive_unique, rem_arch_tmp,
|
|
13
|
+
get_local_ip,
|
|
14
|
+
)
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# GU Functions Library (C) Alexander Guryev, 2026 | https://alexguryev.com
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone, timedelta
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import socket
|
|
9
|
+
import shutil
|
|
10
|
+
import uuid
|
|
11
|
+
import zipfile
|
|
12
|
+
|
|
13
|
+
__version__ = "1.8.3" # maj:arch.changes . min:new functionality . tuning:fixes,tuning
|
|
14
|
+
|
|
15
|
+
_ILLEGAL_FILENAME_CHARS = "/ \"\'\\,.;:#$!?@%*"
|
|
16
|
+
|
|
17
|
+
# #########################################################################
|
|
18
|
+
# ############################## MISC UTILS ###############################
|
|
19
|
+
# #########################################################################
|
|
20
|
+
|
|
21
|
+
def conclear(): # clear console log
|
|
22
|
+
os.system("cls" if os.name == "nt" else "clear")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# #########################################################################
|
|
26
|
+
def conlog(s_in): # colored console print / ^X = color code / ~ = reset color
|
|
27
|
+
colors = {
|
|
28
|
+
"R": "\033[91m", # red
|
|
29
|
+
"G": "\033[32m", # green / 92 = bright
|
|
30
|
+
"B": "\033[94m", # blue
|
|
31
|
+
"C": "\033[36m", # cyan / 96 = bright
|
|
32
|
+
"M": "\033[95m", # magenta
|
|
33
|
+
"Y": "\033[93m", # yellow
|
|
34
|
+
"N": "\033[33m", # brown
|
|
35
|
+
"P": "\033[35m", # purple
|
|
36
|
+
"A": "\033[90m", # gray
|
|
37
|
+
"W": "\033[97m", # white
|
|
38
|
+
"U": "\033[4m" # underline
|
|
39
|
+
}
|
|
40
|
+
colored_text = ""
|
|
41
|
+
i = 0
|
|
42
|
+
s = str(s_in)
|
|
43
|
+
while i < len(s):
|
|
44
|
+
char = s[i]
|
|
45
|
+
if char == "^": # color marker
|
|
46
|
+
i += 1
|
|
47
|
+
if i < len(s):
|
|
48
|
+
next_char = s[i]
|
|
49
|
+
colored_text += colors.get(next_char, next_char)
|
|
50
|
+
elif char == "~": # color reset
|
|
51
|
+
colored_text += "\033[0m"
|
|
52
|
+
else:
|
|
53
|
+
colored_text += char
|
|
54
|
+
i += 1
|
|
55
|
+
print(colored_text)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# #########################################################################
|
|
59
|
+
def conlog_arr(arr): # print list of arr.elems
|
|
60
|
+
print(f"\n{json.dumps(arr, indent=2, ensure_ascii=False)}\n")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# #########################################################################
|
|
64
|
+
def arr_unify(arr_in): # remove duplicates from array (preserve order)
|
|
65
|
+
seen = set()
|
|
66
|
+
return [x for x in arr_in if not (x in seen or seen.add(x))]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# #########################################################################
|
|
70
|
+
def split_list_into_chunks(lst, n): # split list to n parts
|
|
71
|
+
return [lst[i:i + n] for i in range(0, len(lst), n)]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# #########################################################################
|
|
75
|
+
def get_key_by_value(dictionary, target_value): # get dict key from value
|
|
76
|
+
return next((k for k, v in dictionary.items() if v == target_value), None)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# #########################################################################
|
|
80
|
+
# ############################ STRING UTILS ###############################
|
|
81
|
+
# #########################################################################
|
|
82
|
+
|
|
83
|
+
def get_datetime_str(dateonly=False, timeonly=False, filesafe=True, utcplus=3): # filesafe: "YYYY_MM_DD_HH_MM_SS" / "YYYY_MM_DD" / "HH_MM_SS" , not-filesafe: "YYYY_MM_DD / HH:MM:SS"
|
|
84
|
+
# https://strftime.org/
|
|
85
|
+
now = datetime.now(timezone(timedelta(hours=utcplus))) # UTC + utcplus
|
|
86
|
+
if dateonly:
|
|
87
|
+
return now.strftime("%Y_%m_%d" if filesafe else "%Y/%m/%d")
|
|
88
|
+
if timeonly:
|
|
89
|
+
return now.strftime("%H_%M_%S" if filesafe else "%H:%M:%S")
|
|
90
|
+
return now.strftime("%Y_%m_%d_%H_%M_%S" if filesafe else "%Y/%m/%d | %H:%M:%S")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# #########################################################################
|
|
94
|
+
def sec_to_hms(s): # seconds to hh:mm:ss
|
|
95
|
+
return str(timedelta(seconds=s))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# #########################################################################
|
|
99
|
+
def safe_string(s_in, forfile=True): # make string safe for filename or prompt?
|
|
100
|
+
s_out = s_in
|
|
101
|
+
if forfile:
|
|
102
|
+
chars = " ,;:#$~?@%*^&<>{}" # filename-safe
|
|
103
|
+
else:
|
|
104
|
+
chars = ";#$?%" # prompt-safe
|
|
105
|
+
|
|
106
|
+
fix = any(c in s_in for c in chars) # detect fact of illegal chars
|
|
107
|
+
|
|
108
|
+
if forfile:
|
|
109
|
+
s1 = s_out.replace("\\", "/")
|
|
110
|
+
if s1 != s_out:
|
|
111
|
+
fix = True
|
|
112
|
+
s_out = s1
|
|
113
|
+
for c in chars:
|
|
114
|
+
s_out = s_out.replace(c, "_" if forfile else " ")
|
|
115
|
+
|
|
116
|
+
return s_out, fix
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# #########################################################################
|
|
120
|
+
def check_arr_elem_in_str(arr, s): # check if any array element presents as substring in S
|
|
121
|
+
return any(a in s for a in arr)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# #########################################################################
|
|
125
|
+
def strlist_to_str(strlist): # join list of strings with comma separator
|
|
126
|
+
return ", ".join(strlist)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# #########################################################################
|
|
130
|
+
def int_2_str_z(i, zeros=1): # convert int to str w/leading zeros
|
|
131
|
+
return str(i).zfill(zeros)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# #########################################################################
|
|
135
|
+
def illeg_in_name(s_in): # does string contain illegal for filenames
|
|
136
|
+
return any(c in s_in for c in _ILLEGAL_FILENAME_CHARS)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# #########################################################################
|
|
140
|
+
def legalize_name(s_in, CutNum=False): # cut .### number from name
|
|
141
|
+
try:
|
|
142
|
+
head, tail = s_in.rsplit(".", 1)
|
|
143
|
+
if tail.isnumeric() and CutNum:
|
|
144
|
+
s_out = head
|
|
145
|
+
else:
|
|
146
|
+
s_out = s_in
|
|
147
|
+
except Exception: s_out = s_in
|
|
148
|
+
|
|
149
|
+
for ch in _ILLEGAL_FILENAME_CHARS:
|
|
150
|
+
if ch in s_out:
|
|
151
|
+
s_out = s_out.replace(ch, "_")
|
|
152
|
+
|
|
153
|
+
return s_out
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# #########################################################################
|
|
157
|
+
def camelcase_name(s_in): # recreate name in CamelCaseNotation
|
|
158
|
+
s_out = s_in
|
|
159
|
+
# convert all separators to space
|
|
160
|
+
s_out = s_out.replace("_", " ")
|
|
161
|
+
s_out = s_out.replace(".", " ")
|
|
162
|
+
slst = s_out.split(" ") # split by space
|
|
163
|
+
s_out = ""
|
|
164
|
+
for s in slst:
|
|
165
|
+
if len(s) > 0:
|
|
166
|
+
s1 = s[0]
|
|
167
|
+
s2 = s[1:]
|
|
168
|
+
s_out += s1.upper() + s2 # manual capitalize to keep existing CamelCase
|
|
169
|
+
|
|
170
|
+
return s_out
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# #########################################################################
|
|
174
|
+
def get_str_tail_number(s_in): # get the number from tail of string
|
|
175
|
+
i = len(s_in)-1
|
|
176
|
+
if i < 0:
|
|
177
|
+
return 0
|
|
178
|
+
|
|
179
|
+
s = []
|
|
180
|
+
while i >= 0: # get digits from tail
|
|
181
|
+
if s_in[i].isdigit():
|
|
182
|
+
s.append(s_in[i])
|
|
183
|
+
else:
|
|
184
|
+
break
|
|
185
|
+
i -= 1
|
|
186
|
+
|
|
187
|
+
if not s:
|
|
188
|
+
return 0
|
|
189
|
+
return int("".join(s[::-1])) # reversed string
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# #########################################################################
|
|
193
|
+
def unique_shortid(length=8): # create 1..32-char uuid-string
|
|
194
|
+
return uuid.uuid4().hex[:(length if (length > 0 and length <= 32) else 8)]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# #########################################################################
|
|
198
|
+
# ############################# FILE UTILS ################################
|
|
199
|
+
# #########################################################################
|
|
200
|
+
|
|
201
|
+
def get_filenameext(fpath): # return tuple (name, ext)
|
|
202
|
+
return os.path.splitext(os.path.basename(fpath))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# #########################################################################
|
|
206
|
+
def get_pathsplit(fpath): # return tuple (dir, name, ext)
|
|
207
|
+
dir = os.path.dirname(fpath)
|
|
208
|
+
name, ext = os.path.splitext(os.path.basename(fpath))
|
|
209
|
+
return dir, name, ext
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# #########################################################################
|
|
213
|
+
def get_filedir(fpath):
|
|
214
|
+
return fpath if os.path.isdir(fpath) else os.path.dirname(fpath)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# #########################################################################
|
|
218
|
+
def make_unique_filename(given_path, digits=5): # unique-numbered filename from a full path
|
|
219
|
+
if not os.path.exists(given_path):
|
|
220
|
+
return given_path
|
|
221
|
+
|
|
222
|
+
# else make a new name
|
|
223
|
+
if digits < 1: digits = 1 # clamp lower
|
|
224
|
+
|
|
225
|
+
fpath = os.path.normpath(given_path)
|
|
226
|
+
fdir = os.path.dirname(fpath)
|
|
227
|
+
fname, fext = get_filenameext(fpath)
|
|
228
|
+
os.makedirs(fdir, exist_ok=True)
|
|
229
|
+
|
|
230
|
+
#find existing files by template <name>_#####<.ext>
|
|
231
|
+
pattern = fr"^.+?_[0-9]{{{digits}}}\.[a-zA-Z0-9]+$"
|
|
232
|
+
match_files = []
|
|
233
|
+
try:
|
|
234
|
+
for f in os.listdir(fdir):
|
|
235
|
+
if re.match(pattern, f):
|
|
236
|
+
if f.lower().endswith(fext.lower()) and (fname.lower() in f.lower()):
|
|
237
|
+
p = os.path.join(fdir, f)
|
|
238
|
+
match_files.append(p)
|
|
239
|
+
except Exception: pass
|
|
240
|
+
|
|
241
|
+
num = 1
|
|
242
|
+
l = len(match_files)
|
|
243
|
+
if l > 0: # some found
|
|
244
|
+
n, e = os.path.splitext(sorted(match_files)[l-1]) #get last path from sorted list
|
|
245
|
+
num = int(n[len(n)-digits:]) + 1 #get number part -> increment
|
|
246
|
+
|
|
247
|
+
return os.path.join(fdir, f"{fname}_{str(num).zfill(digits)}{fext}") #insert formatted num string
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# #########################################################################
|
|
251
|
+
def is_media_file(path): # check media extensions
|
|
252
|
+
ext = os.path.splitext(str(path))[1].lower()
|
|
253
|
+
return ext in {".png", ".jpg", ".wav", ".mp3", ".mp4"}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# #########################################################################
|
|
257
|
+
def calc_SHA256(file_path): # calculate hash 256
|
|
258
|
+
sha256_hash = hashlib.sha256()
|
|
259
|
+
with open(file_path, "rb") as f:
|
|
260
|
+
for chunk in iter(lambda: f.read(4096), b""):
|
|
261
|
+
sha256_hash.update(chunk)
|
|
262
|
+
return sha256_hash.hexdigest()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# #########################################################################
|
|
266
|
+
def write_datelog(rootpath, text, indent=0): # write logfile @ current date / path to log, message, indent level
|
|
267
|
+
fpath = os.path.join(rootpath, f"{get_datetime_str(dateonly=True)}.log")
|
|
268
|
+
os.makedirs(rootpath, exist_ok=True)
|
|
269
|
+
mode = "a" if os.path.exists(fpath) else "w" # append or start new
|
|
270
|
+
if indent > 0:
|
|
271
|
+
text = str(" " * indent) + text
|
|
272
|
+
text += "\n"
|
|
273
|
+
|
|
274
|
+
with open(fpath, mode, encoding="utf-8") as file:
|
|
275
|
+
file.write(text)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# #########################################################################
|
|
279
|
+
def check_archive(arch_path, extensions):
|
|
280
|
+
if os.path.splitext(arch_path)[1].lower() != ".zip":
|
|
281
|
+
return 0, "Not a ZIP!"
|
|
282
|
+
|
|
283
|
+
count = 0
|
|
284
|
+
try:
|
|
285
|
+
with zipfile.ZipFile(arch_path, "r") as zip_file:
|
|
286
|
+
count = len(zip_file.namelist())
|
|
287
|
+
for member in zip_file.infolist():
|
|
288
|
+
name = member.filename.strip()
|
|
289
|
+
ext = os.path.splitext(name)[1].lower()
|
|
290
|
+
|
|
291
|
+
if "/" in name or "\\" in name:
|
|
292
|
+
return 0, f"No subfolders allowed in archive: '{member.filename}'"
|
|
293
|
+
|
|
294
|
+
if member.file_size == 0:
|
|
295
|
+
return 0, f"No empty files allowed in archive: '{member.filename}'"
|
|
296
|
+
|
|
297
|
+
if not ext:
|
|
298
|
+
return 0, f"Unknown file type: '{member.filename}'"
|
|
299
|
+
if ext not in extensions:
|
|
300
|
+
return 0, f"Unknown file type: '{member.filename}'"
|
|
301
|
+
except Exception:
|
|
302
|
+
return 0, "Unzip error!"
|
|
303
|
+
|
|
304
|
+
return count, ""
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# #########################################################################
|
|
308
|
+
def unpack_archive(arch_path, extr_root, temporary=True):
|
|
309
|
+
extract_path = os.path.join(extr_root, unique_shortid()) if temporary else extr_root
|
|
310
|
+
os.makedirs(extract_path, exist_ok=True)
|
|
311
|
+
try:
|
|
312
|
+
with zipfile.ZipFile(arch_path, "r") as zip_file:
|
|
313
|
+
zip_file.extractall(extract_path)
|
|
314
|
+
except Exception:
|
|
315
|
+
return None, "Unzip error!"
|
|
316
|
+
|
|
317
|
+
return extract_path, ""
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# #########################################################################
|
|
321
|
+
def pack_archive_unique(all_files, fpath): # pack archive + make unique names if duplicate
|
|
322
|
+
try:
|
|
323
|
+
with zipfile.ZipFile(fpath, "w", zipfile.ZIP_DEFLATED) as zipf:
|
|
324
|
+
added_filenames = set()
|
|
325
|
+
for f in all_files:
|
|
326
|
+
fname, fext = get_filenameext(os.path.basename(f))
|
|
327
|
+
counter = 1 # prevent name duplicates in ZIP
|
|
328
|
+
new_fname = f"{fname}{fext}"
|
|
329
|
+
while new_fname in zipf.namelist() or new_fname in added_filenames:
|
|
330
|
+
new_fname = f"{fname}_{str(counter).zfill(4)}{fext}"
|
|
331
|
+
counter += 1
|
|
332
|
+
zipf.write(f, new_fname)
|
|
333
|
+
added_filenames.add(new_fname)
|
|
334
|
+
return True
|
|
335
|
+
|
|
336
|
+
except Exception:
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# #########################################################################
|
|
341
|
+
def rem_arch_tmp(extract_path):
|
|
342
|
+
try:
|
|
343
|
+
shutil.rmtree(extract_path)
|
|
344
|
+
except Exception:
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
return True
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# #########################################################################
|
|
351
|
+
# ############################## NET UTILS ################################
|
|
352
|
+
# #########################################################################
|
|
353
|
+
|
|
354
|
+
def get_local_ip(): # get client ip-address string
|
|
355
|
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
|
356
|
+
try:
|
|
357
|
+
s.connect(("10.255.255.255", 1)) # fake connection
|
|
358
|
+
#s.connect(("8.8.8.8", 80)) # wait for external DNS / not for LAN
|
|
359
|
+
return s.getsockname()[0]
|
|
360
|
+
except Exception:
|
|
361
|
+
return "127.0.0.1"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gu_funclib
|
|
3
|
+
Version: 1.8.3
|
|
4
|
+
Summary: GU Functions Library
|
|
5
|
+
Author-email: Alexander Guryev <alexguryev@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://alexguryev.com
|
|
8
|
+
Project-URL: Repository, https://github.com/alexguryev/gu_funclib
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Dynamic: license-file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gu_funclib
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gu_funclib"
|
|
7
|
+
version = "1.8.3"
|
|
8
|
+
description = "GU Functions Library"
|
|
9
|
+
license = { text = "MIT" }
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Alexander Guryev", email = "alexguryev@gmail.com" }
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://alexguryev.com"
|
|
17
|
+
Repository = "https://github.com/alexguryev/gu_funclib"
|