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.
@@ -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,9 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ gu_funclib/__init__.py
5
+ gu_funclib/gu_funclib.py
6
+ gu_funclib.egg-info/PKG-INFO
7
+ gu_funclib.egg-info/SOURCES.txt
8
+ gu_funclib.egg-info/dependency_links.txt
9
+ gu_funclib.egg-info/top_level.txt
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+