abstract-essentials 0.0.0.1__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,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: abstract_essentials
3
+ Version: 0.0.0.1
4
+ Summary: The lean, dependency-free core of abstract_utilities: stdlib-only helpers (json/path/file/string/list/log) safe to import anywhere, including Termux/phone.
5
+ Author-email: putkoff <partners@abstractendeavors.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/AbstractEndeavors/abstract_essentials
8
+ Keywords: utilities,stdlib,helpers,dependency-free
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+
18
+ # abstract_essentials
19
+
20
+ The lean, **dependency-free** core carved out of `abstract_utilities`.
21
+
22
+ Every symbol here depends only on the Python standard library — no third-party
23
+ imports, no `from .imports import *` chains, no submodule fan-out. It imports fast
24
+ and installs anywhere (desktop, server, **Termux/Android phone**), which makes it a
25
+ safe foundation for the rest of the `abstract_*` tree to build on without dragging
26
+ in the monolith.
27
+
28
+ ## Why this exists
29
+
30
+ `abstract_utilities` is imported by nearly every `abstract_*` package, so its size
31
+ and entanglement (star-exports across a dozen submodules) ripple everywhere.
32
+ `abstract_essentials` is the ~50-function subset that is actually used in practice,
33
+ extracted as a clean, explicit API.
34
+
35
+ ## Install
36
+
37
+ ```sh
38
+ pip install abstract_essentials
39
+ ```
40
+
41
+ ## Use
42
+
43
+ ```python
44
+ from abstract_essentials import make_list, get_any_value, safe_read_from_json, get_logFile
45
+ ```
46
+
47
+ The public API is whatever is listed in `abstract_essentials.__all__` (53 symbols:
48
+ json/path/file/string/list/type/log helpers).
49
+
50
+ ## Migrating off abstract_utilities
51
+
52
+ `abstract_utilities` can become a thin compatibility shim that re-exports from here
53
+ (see `abstract_utilities_compat_shim.py` shipped alongside this scaffold), so existing
54
+ `from abstract_utilities import X` keeps working while new code imports from
55
+ `abstract_essentials`.
56
+
57
+ ## License
58
+
59
+ MIT — putkoff / Abstract Endeavors.
@@ -0,0 +1,42 @@
1
+ # abstract_essentials
2
+
3
+ The lean, **dependency-free** core carved out of `abstract_utilities`.
4
+
5
+ Every symbol here depends only on the Python standard library — no third-party
6
+ imports, no `from .imports import *` chains, no submodule fan-out. It imports fast
7
+ and installs anywhere (desktop, server, **Termux/Android phone**), which makes it a
8
+ safe foundation for the rest of the `abstract_*` tree to build on without dragging
9
+ in the monolith.
10
+
11
+ ## Why this exists
12
+
13
+ `abstract_utilities` is imported by nearly every `abstract_*` package, so its size
14
+ and entanglement (star-exports across a dozen submodules) ripple everywhere.
15
+ `abstract_essentials` is the ~50-function subset that is actually used in practice,
16
+ extracted as a clean, explicit API.
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ pip install abstract_essentials
22
+ ```
23
+
24
+ ## Use
25
+
26
+ ```python
27
+ from abstract_essentials import make_list, get_any_value, safe_read_from_json, get_logFile
28
+ ```
29
+
30
+ The public API is whatever is listed in `abstract_essentials.__all__` (53 symbols:
31
+ json/path/file/string/list/type/log helpers).
32
+
33
+ ## Migrating off abstract_utilities
34
+
35
+ `abstract_utilities` can become a thin compatibility shim that re-exports from here
36
+ (see `abstract_utilities_compat_shim.py` shipped alongside this scaffold), so existing
37
+ `from abstract_utilities import X` keeps working while new code imports from
38
+ `abstract_essentials`.
39
+
40
+ ## License
41
+
42
+ MIT — putkoff / Abstract Endeavors.
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "abstract_essentials"
7
+ version = "0.0.0.1"
8
+ description = "The lean, dependency-free core of abstract_utilities: stdlib-only helpers (json/path/file/string/list/log) safe to import anywhere, including Termux/phone."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ authors = [{ name = "putkoff", email = "partners@abstractendeavors.com" }]
12
+ license = { text = "MIT" }
13
+ keywords = ["utilities", "stdlib", "helpers", "dependency-free"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Operating System :: OS Independent",
20
+ "Topic :: Software Development :: Libraries",
21
+ ]
22
+ # Intentionally EMPTY — this package's whole reason for existing is to have no deps.
23
+ dependencies = []
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/AbstractEndeavors/abstract_essentials"
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["src"]
30
+ include = ["abstract_essentials*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ """abstract_essentials — the lean, stdlib-only core carved out of abstract_utilities.
2
+
3
+ Zero third-party dependencies; safe to import on any platform (incl. Termux/phone).
4
+ """
5
+ from .utils import * # noqa: F401,F403
6
+ from .utils import __all__ # noqa: F401
7
+
8
+ __version__ = "0.0.0.1"
@@ -0,0 +1,1087 @@
1
+ """
2
+ standalone_utils.py
3
+ ===================
4
+
5
+ Standalone, dependency-free extractions of selected ``abstract_utilities``
6
+ functions.
7
+
8
+ Every public function in this module was pulled out of the ``abstract_utilities``
9
+ package and rewritten / inlined so that it depends **only on the Python standard
10
+ library**. There are no ``from abstract_utilities ...`` imports and no
11
+ ``from .imports import *`` style imports anywhere in this file, so it can be
12
+ dropped into any project and used on its own.
13
+
14
+ Functions exported here (with their original home in the package):
15
+
16
+ derive_media_type <- type_utils/mime_types.py
17
+ eatAll <- string_utils/eat_utils.py
18
+ get_any_value <- json_utils/json_utils.py
19
+ get_caller_dir <- class_utils/caller_utils.py
20
+ get_dirlist <- path_utils/path_utils.py
21
+ get_env_value <- env_utils/* (re-implemented, no abstractEnv class)
22
+ get_file_parts <- path_utils/path_utils.py
23
+ get_files <- path_utils/path_utils.py
24
+ get_files_and_dirs <- file_utils/.../find_collect.py (local-only rewrite)
25
+ get_home_dir <- path_utils/path_utils.py
26
+ get_inputs <- class_utils/abstract_classes.py
27
+ get_logFile <- log_utils/log_file.py
28
+ is_dir <- file_utils/.../classes.py (local-only)
29
+ is_file <- file_utils/.../classes.py (local-only)
30
+ is_number <- type_utils/is_type.py
31
+ join_path <- path_utils/path_utils.py
32
+ lazy_import <- import_utils/.../lazy_utils.py
33
+ make_list <- list_utils/list_utils.py
34
+ makedirs <- directory_utils/directory_utils.py
35
+ read_from_file <- read_write_utils/read_write_utils.py (local-only)
36
+ safe_dump_to_file <- json_utils/json_utils.py
37
+ safe_dump_to_json <- json_utils/json_utils.py
38
+ safe_load_from_json <- json_utils/json_utils.py
39
+ safe_read_from_json <- json_utils/json_utils.py
40
+ write_to_file <- read_write_utils/read_write_utils.py (local-only)
41
+
42
+ Notes on the local-only rewrites
43
+ ---------------------------------
44
+ ``read_from_file``, ``write_to_file``, ``get_files_and_dirs``, ``is_file`` and
45
+ ``is_dir`` originally supported remote (SSH) execution through the
46
+ ``abstract_utilities.ssh_utils`` machinery. Since that machinery is exactly the
47
+ "abstract_utilities module imports" we are removing, those code paths have been
48
+ dropped and these functions now operate purely on the local filesystem. They
49
+ still accept ``**kwargs`` so existing call sites that passed remote options will
50
+ not raise; the remote options are simply ignored.
51
+ """
52
+
53
+ import os
54
+ import sys
55
+ import json
56
+ import logging
57
+ import importlib
58
+ from pathlib import Path
59
+ from functools import lru_cache
60
+ from logging.handlers import RotatingFileHandler
61
+ from typing import Optional
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # type / number helpers
66
+ # ---------------------------------------------------------------------------
67
+ def is_number(s):
68
+ """Return True if ``s`` can be parsed as a float."""
69
+ try:
70
+ float(s)
71
+ return True
72
+ except Exception:
73
+ return False
74
+
75
+
76
+ def make_list(obj, commaparse=True):
77
+ """
78
+ Convert ``obj`` to a list.
79
+
80
+ - A comma-containing string is split on commas (unless ``commaparse`` is False).
81
+ - sets and tuples become lists.
82
+ - lists are returned unchanged.
83
+ - anything else is wrapped in a single-element list.
84
+ """
85
+ if isinstance(obj, str):
86
+ if ',' in obj and commaparse is True:
87
+ obj = obj.split(',')
88
+ if isinstance(obj, (set, tuple)):
89
+ return list(obj)
90
+ if isinstance(obj, list):
91
+ return obj
92
+ return [obj]
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # string helpers (eatAll + its inner/outer helpers)
97
+ # ---------------------------------------------------------------------------
98
+ def eatInner(string, list_objects):
99
+ """Strip leading characters that appear in ``list_objects``."""
100
+ if not isinstance(list_objects, list):
101
+ list_objects = [list_objects]
102
+ if not isinstance(string, str):
103
+ string = str(string)
104
+ if string and list_objects:
105
+ for char in string:
106
+ if string:
107
+ if char not in list_objects:
108
+ return string
109
+ string = string[1:]
110
+ return string
111
+
112
+
113
+ def eatOuter(string, list_objects):
114
+ """Strip trailing characters that appear in ``list_objects``."""
115
+ if not isinstance(list_objects, list):
116
+ list_objects = [list_objects]
117
+ if not isinstance(string, str):
118
+ string = str(string)
119
+ if string and list_objects:
120
+ for _ in range(len(string)):
121
+ if string:
122
+ if string[-1] not in list_objects:
123
+ return string
124
+ string = string[:-1]
125
+ return string
126
+
127
+
128
+ def eatAll(string, list_objects):
129
+ """Strip leading and trailing characters that appear in ``list_objects``."""
130
+ if not isinstance(list_objects, list):
131
+ list_objects = [list_objects]
132
+ if not isinstance(string, str):
133
+ string = str(string)
134
+ if string and list_objects:
135
+ string = eatInner(string, list_objects)
136
+ if string and list_objects:
137
+ string = eatOuter(string, list_objects)
138
+ return string
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # basic path predicates / joins (local filesystem only)
143
+ # ---------------------------------------------------------------------------
144
+ def is_file(path, *args, **kwargs):
145
+ """Local-only check: True if ``path`` is an existing file."""
146
+ if path:
147
+ return os.path.isfile(path)
148
+ return False
149
+
150
+
151
+ def is_dir(path, *args, **kwargs):
152
+ """Local-only check: True if ``path`` is an existing directory."""
153
+ if path:
154
+ return os.path.isdir(path)
155
+ return False
156
+
157
+
158
+ def join_path(directory, basename):
159
+ """Join two path components with ``os.path.join``."""
160
+ return os.path.join(directory, basename)
161
+
162
+
163
+ def get_files(directory):
164
+ """Recursively collect every file path under ``directory``."""
165
+ file_list = []
166
+ for root, _dirs, files in os.walk(directory):
167
+ for file in files:
168
+ file_list.append(os.path.join(root, file))
169
+ return file_list
170
+
171
+
172
+ def get_directory(directory):
173
+ """Return ``directory``, creating it (and parents) if it does not exist."""
174
+ if not os.path.isdir(directory):
175
+ os.makedirs(directory, exist_ok=True)
176
+ return directory
177
+
178
+
179
+ def get_dirlist(directory):
180
+ """
181
+ Return the contents of ``directory``.
182
+
183
+ The directory is created if missing. If ``directory`` happens to be a file,
184
+ a single-element list with its basename is returned.
185
+ """
186
+ path = get_directory(directory)
187
+ if not path:
188
+ return path
189
+ dir_list = []
190
+ if is_dir(path):
191
+ dir_list = os.listdir(path)
192
+ elif is_file(path):
193
+ dir_list = [os.path.basename(path)]
194
+ return dir_list
195
+
196
+
197
+ # ---------------------------------------------------------------------------
198
+ # pathlib-aware helpers (get_home_dir / get_file_parts)
199
+ # ---------------------------------------------------------------------------
200
+ def is_pathlike(obj):
201
+ return bool(obj) and isinstance(obj, Path)
202
+
203
+
204
+ def get_pathlib_path(obj):
205
+ if obj and not is_pathlike(obj):
206
+ obj = Path(obj)
207
+ return obj
208
+
209
+
210
+ def get_str_path(obj):
211
+ if is_pathlike(obj):
212
+ obj = str(obj)
213
+ return obj
214
+
215
+
216
+ def return_path(path, isPath=False):
217
+ if isPath:
218
+ path = get_pathlib_path(path)
219
+ else:
220
+ path = get_str_path(path)
221
+ return path
222
+
223
+
224
+ def get_home_dir(path=None):
225
+ """Return the home directory, preserving str/Path type of ``path``."""
226
+ isPath = is_pathlike(path)
227
+ path = path or os.getcwd()
228
+ nupath = get_pathlib_path(path)
229
+ home_path = nupath.home()
230
+ return return_path(home_path, isPath=isPath)
231
+
232
+
233
+ def get_safe_basename(path=None):
234
+ if path:
235
+ return os.path.basename(str(path))
236
+
237
+
238
+ def get_safe_dirname(path=None):
239
+ if path:
240
+ isPath = is_pathlike(path)
241
+ dirname = os.path.dirname(str(path))
242
+ return return_path(dirname, isPath=isPath)
243
+
244
+
245
+ def get_safe_splitext(path=None, basename=None):
246
+ basename = basename or get_safe_basename(path=path)
247
+ if basename:
248
+ filename, ext = os.path.splitext(str(basename))
249
+ return filename, ext
250
+ return None, None
251
+
252
+
253
+ def get_file_parts(path):
254
+ """
255
+ Decompose ``path`` into a dict of useful components: basename, filename,
256
+ extension, and the names of the parent / grandparent / great-grandparent
257
+ directories.
258
+ """
259
+ if path:
260
+ path = str(path)
261
+ basename = get_safe_basename(path)
262
+ filename, ext = get_safe_splitext(basename=basename)
263
+
264
+ dirname = get_safe_dirname(path)
265
+ dirbase = get_safe_basename(dirname)
266
+
267
+ parent_dirname = get_safe_dirname(dirname)
268
+ parent_dirbase = get_safe_basename(parent_dirname)
269
+
270
+ super_dirname = get_safe_dirname(parent_dirname)
271
+ super_dirbase = get_safe_basename(super_dirname)
272
+
273
+ return {
274
+ "file_path": path,
275
+ "dirname": dirname,
276
+ "basename": basename,
277
+ "filename": filename,
278
+ "ext": ext,
279
+ "dirbase": dirbase,
280
+ "parent_dirname": parent_dirname,
281
+ "parent_dirbase": parent_dirbase,
282
+ "super_dirname": super_dirname,
283
+ "super_dirbase": super_dirbase,
284
+ }
285
+
286
+
287
+ # ---------------------------------------------------------------------------
288
+ # directory creation (makedirs)
289
+ # ---------------------------------------------------------------------------
290
+ def safe_join(*paths):
291
+ """``os.path.join`` that silently drops falsy components."""
292
+ paths = [path for path in paths if path]
293
+ return os.path.join(*paths)
294
+
295
+
296
+ def raw_create_dirs(*paths):
297
+ """Recursively create every directory along the given path and return it."""
298
+ full_path = os.path.abspath(safe_join(*paths))
299
+ sub_parts = [p for p in full_path.split(os.sep) if p]
300
+
301
+ current_path = "/" if full_path.startswith(os.sep) else ""
302
+ for part in sub_parts:
303
+ current_path = safe_join(current_path, part)
304
+ os.makedirs(current_path, exist_ok=True)
305
+ return full_path
306
+
307
+
308
+ # ``makedirs`` in the package is just an alias of ``raw_create_dirs``.
309
+ makedirs = raw_create_dirs
310
+
311
+
312
+ # ---------------------------------------------------------------------------
313
+ # file collection (get_files_and_dirs) -- local-only rewrite
314
+ # ---------------------------------------------------------------------------
315
+ def get_files_and_dirs(*args, recursive=True, include_files=True, **kwargs):
316
+ """
317
+ Return ``(dirs, files)`` collected from one or more directories.
318
+
319
+ Args:
320
+ *args: directories to scan (defaults to the current working directory).
321
+ recursive: walk subdirectories when True, otherwise only the top level.
322
+ include_files: include files in the result when True.
323
+
324
+ Remote/SSH options accepted by the original implementation are ignored.
325
+ """
326
+ directories = [a for a in args if a] or [os.getcwd()]
327
+ dirs, files = [], []
328
+ for directory in directories:
329
+ if not os.path.isdir(directory):
330
+ continue
331
+ if recursive:
332
+ for root, dnames, fnames in os.walk(directory):
333
+ for d in dnames:
334
+ dirs.append(os.path.join(root, d))
335
+ if include_files:
336
+ for fn in fnames:
337
+ files.append(os.path.join(root, fn))
338
+ else:
339
+ for name in os.listdir(directory):
340
+ full = os.path.join(directory, name)
341
+ if os.path.isdir(full):
342
+ dirs.append(full)
343
+ elif include_files and os.path.isfile(full):
344
+ files.append(full)
345
+ return dirs, files
346
+
347
+
348
+ # ---------------------------------------------------------------------------
349
+ # read / write (read_from_file / write_to_file) -- local-only rewrite
350
+ # ---------------------------------------------------------------------------
351
+ def _write_to_file(contents, file_path, **kwargs):
352
+ os.makedirs(os.path.dirname(file_path) or ".", exist_ok=True)
353
+ with open(file_path, "w", encoding="utf-8") as f:
354
+ f.write(str(contents))
355
+ return file_path
356
+
357
+
358
+ def write_to_file(*, contents, file_path, **kwargs):
359
+ """Overwrite ``file_path`` with ``contents`` (creating parent dirs)."""
360
+ try:
361
+ return _write_to_file(contents=contents, file_path=file_path, **kwargs)
362
+ except Exception as e:
363
+ print("WRITE ERROR:", e)
364
+ raise RuntimeError(f"Failed writing: {file_path}")
365
+
366
+
367
+ def read_from_file(file_path=None, **kwargs):
368
+ """Read and return the text contents of ``file_path``."""
369
+ with open(file_path, "r", encoding="utf-8") as f:
370
+ return f.read()
371
+
372
+
373
+ # ---------------------------------------------------------------------------
374
+ # JSON utilities
375
+ # ---------------------------------------------------------------------------
376
+ import re # noqa: E402 (kept next to the JSON helpers that use it)
377
+
378
+ _json_logger = logging.getLogger(__name__)
379
+
380
+
381
+ def try_json_loads(data):
382
+ try:
383
+ return json.loads(data)
384
+ except Exception:
385
+ return None
386
+
387
+
388
+ def safe_json_loads(data):
389
+ if not isinstance(data, dict):
390
+ data = try_json_loads(data) or data
391
+ return data
392
+
393
+
394
+ def clean_invalid_newlines(json_string, line_replacement_value=''):
395
+ """Remove newlines that are not inside double-quoted strings."""
396
+ pattern = r'(?<!\\)\n(?!([^"]*"[^"]*")*[^"]*$)'
397
+ return re.sub(pattern, line_replacement_value, json_string)
398
+
399
+
400
+ def read_malformed_json(json_string, line_replacement_value="*n"):
401
+ """Attempt to parse a (possibly malformed) JSON string after cleaning it."""
402
+ if isinstance(json_string, str):
403
+ json_string = clean_invalid_newlines(json_string, line_replacement_value=line_replacement_value)
404
+ return safe_json_loads(json_string)
405
+
406
+
407
+ def get_value_from_path(json_data, path, line_replacement_value='*n*'):
408
+ """Traverse a nested JSON object along ``path`` and return the value found."""
409
+ current_data = safe_json_loads(json_data)
410
+ for step in path:
411
+ current_data = safe_json_loads(current_data[step])
412
+ if isinstance(current_data, str):
413
+ current_data = read_malformed_json(current_data, line_replacement_value=line_replacement_value)
414
+ return current_data
415
+
416
+
417
+ def find_paths_to_key(json_data, key_to_find, line_replacement_value='*n*'):
418
+ """Return every path (list of keys/indices) that leads to ``key_to_find``."""
419
+ def _search_path(data, current_path):
420
+ if isinstance(data, dict):
421
+ for key, value in data.items():
422
+ new_path = current_path + [key]
423
+ if key == key_to_find:
424
+ paths.append(new_path)
425
+ if isinstance(value, str):
426
+ try:
427
+ nested = read_malformed_json(value, line_replacement_value=line_replacement_value)
428
+ _search_path(nested, new_path)
429
+ except json.JSONDecodeError:
430
+ pass
431
+ _search_path(value, new_path)
432
+ elif isinstance(data, list):
433
+ for index, item in enumerate(data):
434
+ _search_path(item, current_path + [index])
435
+
436
+ paths = []
437
+ _search_path(json_data, [])
438
+ return paths
439
+
440
+
441
+ def get_any_value(json_obj, key, line_replacement_value="*n*"):
442
+ """
443
+ Fetch the value(s) associated with ``key`` from a JSON object, JSON string,
444
+ or a path to a JSON file.
445
+ """
446
+ if isinstance(json_obj, str):
447
+ if os.path.isfile(json_obj):
448
+ with open(json_obj, 'r', encoding='UTF-8') as f:
449
+ json_obj = f.read()
450
+ json_data = read_malformed_json(json_obj)
451
+ paths_to_value = find_paths_to_key(json_data, key)
452
+ if not isinstance(paths_to_value, list):
453
+ paths_to_value = [paths_to_value]
454
+ for i, path_to_value in enumerate(paths_to_value):
455
+ paths_to_value[i] = get_value_from_path(json_data, path_to_value)
456
+ if isinstance(paths_to_value[i], str):
457
+ paths_to_value[i] = paths_to_value[i].replace(line_replacement_value, '\n')
458
+ if isinstance(paths_to_value, list):
459
+ if len(paths_to_value) == 0:
460
+ paths_to_value = None
461
+ elif len(paths_to_value) == 1:
462
+ paths_to_value = paths_to_value[0]
463
+ return paths_to_value
464
+
465
+
466
+ def validate_file_path(file_path, is_read=False):
467
+ if file_path and isinstance(file_path, str):
468
+ if os.path.isfile(file_path) or os.path.isdir(file_path):
469
+ return file_path
470
+ if not is_read:
471
+ dirname = os.path.dirname(file_path)
472
+ if os.path.isdir(dirname):
473
+ return file_path
474
+
475
+
476
+ def get_file_path(*args, is_read=False, **kwargs):
477
+ args = list(args)
478
+ for file_path in args:
479
+ if validate_file_path(file_path, is_read=is_read):
480
+ return file_path
481
+ for file_path in list(kwargs.values()):
482
+ if validate_file_path(file_path, is_read=is_read):
483
+ return file_path
484
+
485
+
486
+ def _write_file(data, file_path):
487
+ with open(file_path, 'w', encoding='utf-8') as file:
488
+ file.write(str(data))
489
+
490
+
491
+ def _write_json(data, file_path, ensure_ascii=False, indent=4):
492
+ with open(file_path, 'w', encoding='utf-8') as file:
493
+ json.dump(data, file, ensure_ascii=ensure_ascii, indent=indent)
494
+
495
+
496
+ def _safe_write_json(data, file_path, ensure_ascii=False, indent=4):
497
+ if isinstance(data, (dict, list, tuple)):
498
+ _write_json(data, file_path, ensure_ascii=ensure_ascii, indent=indent)
499
+ else:
500
+ _write_file(data, file_path)
501
+
502
+
503
+ def _read_json(file_path):
504
+ with open(file_path, 'r', encoding='utf-8') as file:
505
+ return json.load(file)
506
+
507
+
508
+ def _output_read_write_error(e, function_name, file_path, valid_file_path=None, data=None, is_read=False):
509
+ error_text = f"Error in {function_name};{e}\nFile path: {file_path} "
510
+ if valid_file_path is None:
511
+ error_text += f"\nValid File path: {valid_file_path} "
512
+ if not is_read:
513
+ error_text += f"\nData: {data} "
514
+ _json_logger.error(error_text)
515
+
516
+
517
+ def safe_dump_to_file(data, file_path=None, ensure_ascii=False, indent=4, *args, **kwargs):
518
+ """Serialize ``data`` to ``file_path`` (JSON for dict/list/tuple, else text)."""
519
+ is_read = False
520
+ file_args = [file_path, data]
521
+ valid_file_path = get_file_path(*file_args, *args, is_read=is_read, **kwargs)
522
+
523
+ if valid_file_path:
524
+ file_path = valid_file_path
525
+ if file_path == file_args[-1]:
526
+ data = file_args[0]
527
+ if file_path is not None and data is not None:
528
+ try:
529
+ _safe_write_json(data, file_path, ensure_ascii=ensure_ascii, indent=indent)
530
+ except Exception as e:
531
+ _output_read_write_error(e, 'safe_dump_to_file', file_path, valid_file_path, is_read=is_read)
532
+ else:
533
+ _json_logger.error("file_path and data must be provided to safe_dump_to_file")
534
+
535
+
536
+ def safe_read_from_json(file_path, *args, **kwargs):
537
+ """Read and return JSON content from ``file_path`` (None on failure)."""
538
+ is_read = True
539
+ valid_file_path = get_file_path(file_path, *args, is_read=is_read, **kwargs)
540
+ if valid_file_path:
541
+ file_path = valid_file_path
542
+ try:
543
+ return _read_json(file_path)
544
+ except Exception as e:
545
+ _output_read_write_error(e, 'safe_read_from_json', file_path, valid_file_path, is_read=is_read)
546
+ return None
547
+
548
+
549
+ def safe_load_from_json(*args, **kwargs):
550
+ """Alias for :func:`safe_read_from_json`."""
551
+ return safe_read_from_json(*args, **kwargs)
552
+
553
+
554
+ def safe_dump_to_json(*args, **kwargs):
555
+ """Alias for :func:`safe_dump_to_file`."""
556
+ return safe_dump_to_file(*args, **kwargs)
557
+
558
+
559
+ # ---------------------------------------------------------------------------
560
+ # media type detection (derive_media_type)
561
+ # ---------------------------------------------------------------------------
562
+ MIME_TYPES = {
563
+ 'image': {
564
+ '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
565
+ '.gif': 'image/gif', '.bmp': 'image/bmp', '.tiff': 'image/tiff',
566
+ '.webp': 'image/webp', '.svg': 'image/svg+xml', '.ico': 'image/x-icon',
567
+ '.heic': 'image/heic', '.psd': 'image/vnd.adobe.photoshop',
568
+ '.raw': 'image/x-raw', '.apng': 'image/apng', '.heif': 'image/heif',
569
+ '.jp2': 'image/jp2', '.jxl': 'image/jxl', '.eps': 'application/postscript',
570
+ '.ai': 'application/postscript',
571
+ },
572
+ 'video': {
573
+ '.mp4': 'video/mp4', '.webm': 'video/webm', '.ogg': 'video/ogg',
574
+ '.mov': 'video/quicktime', '.avi': 'video/x-msvideo', '.mkv': 'video/x-matroska',
575
+ '.flv': 'video/x-flv', '.wmv': 'video/x-ms-wmv', '.3gp': 'video/3gpp',
576
+ '.mpeg': 'video/mpeg', '.mpg': 'video/mpg', '.m4v': 'video/x-m4v',
577
+ '.f4v': 'video/x-f4v', '.asf': 'video/x-ms-asf', '.vob': 'video/dvd',
578
+ '.m2ts': 'video/mp2t', '.mts': 'video/mp2t',
579
+ },
580
+ 'audio': {
581
+ '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.flac': 'audio/flac',
582
+ '.aac': 'audio/aac', '.ogg': 'audio/ogg', '.m4a': 'audio/mp4',
583
+ '.opus': 'audio/opus', '.aif': 'audio/x-aiff', '.aiff': 'audio/x-aiff',
584
+ '.amr': 'audio/amr', '.mid': 'audio/midi', '.midi': 'audio/midi',
585
+ '.wma': 'audio/x-ms-wma', '.mka': 'audio/x-matroska',
586
+ },
587
+ 'document': {
588
+ '.pdf': 'application/pdf', '.doc': 'application/msword',
589
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
590
+ '.odt': 'application/vnd.oasis.opendocument.text', '.txt': 'text/plain',
591
+ '.rtf': 'application/rtf', '.md': 'text/markdown', '.markdown': 'text/markdown',
592
+ '.tex': 'application/x-tex', '.log': 'text/plain', '.json': 'application/json',
593
+ '.xml': 'application/xml', '.yaml': 'application/x-yaml', '.yml': 'application/x-yaml',
594
+ '.ini': 'text/plain', '.cfg': 'text/plain', '.toml': 'application/toml',
595
+ '.csv': 'text/csv', '.tsv': 'text/tab-separated-values', '.epub': 'application/epub+zip',
596
+ '.mobi': 'application/x-mobipocket-ebook', '.azw': 'application/vnd.amazon.ebook',
597
+ '.pages': 'application/x-iwork-pages-sffpages',
598
+ '.numbers': 'application/x-iwork-numbers-sffnumbers',
599
+ '.key': 'application/x-iwork-keynote-sffkey',
600
+ },
601
+ 'presentation': {
602
+ '.ppt': 'application/vnd.ms-powerpoint',
603
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
604
+ '.odp': 'application/vnd.oasis.opendocument.presentation',
605
+ },
606
+ 'spreadsheet': {
607
+ '.xls': 'application/vnd.ms-excel',
608
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
609
+ '.ods': 'application/vnd.oasis.opendocument.spreadsheet',
610
+ '.csv': 'text/csv', '.tsv': 'text/tab-separated-values',
611
+ },
612
+ 'data_science': {
613
+ '.parquet': 'application/x-parquet', '.avro': 'application/avro',
614
+ '.hdf5': 'application/x-hdf5', '.h5': 'application/x-hdf5',
615
+ '.pickle': 'application/octet-stream', '.npy': 'application/x-npy',
616
+ '.npz': 'application/x-npz', '.ipynb': 'application/x-ipynb+json',
617
+ '.sqlite': 'application/x-sqlite3', '.db': 'application/x-sqlite3',
618
+ },
619
+ 'code': {
620
+ '.py': 'text/x-python', '.java': 'text/x-java-source', '.c': 'text/x-c',
621
+ '.cpp': 'text/x-c++', '.h': 'text/x-c', '.hpp': 'text/x-c++',
622
+ '.js': 'application/javascript', '.cjs': 'application/javascript',
623
+ '.mjs': 'application/javascript', '.jsx': 'application/javascript',
624
+ '.ts': 'application/javascript', '.tsx': 'application/typescript',
625
+ '.rb': 'text/x-ruby', '.php': 'application/x-php', '.go': 'text/x-go',
626
+ '.rs': 'text/rust', '.swift': 'text/x-swift', '.kt': 'text/x-kotlin',
627
+ '.sh': 'application/x-shellscript', '.bash': 'application/x-shellscript',
628
+ '.ps1': 'application/x-powershell', '.sql': 'application/sql',
629
+ '.yml': 'application/x-yaml', '.coffee': 'text/coffeescript', '.lua': 'text/x-lua',
630
+ '.css': 'text/css', '.scss': 'text/x-scss', '.sass': 'text/x-sass',
631
+ '.less': 'text/x-less', '.html': 'text/html', '.htm': 'text/html',
632
+ '.vue': 'text/x-vue', '.svelte': 'text/x-svelte', '.graphql': 'application/graphql',
633
+ '.gql': 'application/graphql', '.dockerfile': 'text/x-dockerfile',
634
+ '.makefile': 'text/x-makefile', '.sol': 'text/x-solidity',
635
+ },
636
+ 'three_d': {
637
+ '.obj': 'model/obj', '.stl': 'model/stl', '.glb': 'model/gltf-binary',
638
+ '.gltf': 'model/gltf+json', '.fbx': 'application/octet-stream',
639
+ '.usd': 'model/usd', '.usdz': 'model/vnd.usdz+zip', '.blend': 'application/x-blender',
640
+ },
641
+ 'archive': {
642
+ '.zip': 'application/zip', '.tar': 'application/x-tar', '.gz': 'application/gzip',
643
+ '.tgz': 'application/gzip', '.bz2': 'application/x-bzip2', '.xz': 'application/x-xz',
644
+ '.rar': 'application/vnd.rar', '.7z': 'application/x-7z-compressed',
645
+ '.iso': 'application/x-iso9660-image', '.dmg': 'application/x-apple-diskimage',
646
+ '.jar': 'application/java-archive', '.war': 'application/java-archive',
647
+ '.whl': 'application/python-wheel', '.egg': 'application/python-egg',
648
+ '.zpaq': 'application/x-zpaq',
649
+ },
650
+ 'font': {
651
+ '.ttf': 'font/ttf', '.otf': 'font/otf', '.woff': 'font/woff',
652
+ '.woff2': 'font/woff2', '.eot': 'application/vnd.ms-fontobject',
653
+ },
654
+ 'executable': {
655
+ '.exe': 'application/vnd.microsoft.portable-executable',
656
+ '.dll': 'application/vnd.microsoft.portable-executable',
657
+ '.bin': 'application/octet-stream', '.deb': 'application/vnd.debian.binary-package',
658
+ '.rpm': 'application/x-rpm', '.app': 'application/x-executable',
659
+ '.ipa': 'application/x-itunes-ipa', '.apk': 'application/vnd.android.package-archive',
660
+ },
661
+ 'geospatial': {
662
+ '.geojson': 'application/geo+json', '.kml': 'application/vnd.google-earth.kml+xml',
663
+ '.kmz': 'application/vnd.google-earth.kmz', '.shp': 'application/x-qgis-main-file',
664
+ '.shx': 'application/x-qgis-shape-index', '.dbf': 'application/x-dbf',
665
+ '.gpkg': 'application/geopackage+sqlite3', '.gpx': 'application/gpx+xml',
666
+ '.tif': 'image/tiff', '.tiff': 'image/tiff', '.osm': 'application/xml',
667
+ '.wkt': 'text/plain',
668
+ },
669
+ 'pandas_data': {
670
+ '.parquet': 'application/x-parquet', '.feather': 'application/x-feather',
671
+ '.orc': 'application/x-orc', '.hdf': 'application/x-hdf', '.h5': 'application/x-hdf5',
672
+ '.pickle': 'application/octet-stream', '.pkl': 'application/octet-stream',
673
+ '.msgpack': 'application/x-msgpack', '.stata': 'application/x-stata',
674
+ '.dta': 'application/x-stata', '.sas7bdat': 'application/x-sas-data',
675
+ '.sav': 'application/x-spss-sav',
676
+ },
677
+ }
678
+
679
+ # Category -> set of extensions (equivalent to the package's MEDIA_TYPES).
680
+ MEDIA_TYPES = {category: set(mapping.keys()) for category, mapping in MIME_TYPES.items()}
681
+
682
+
683
+ def derive_media_type(obj):
684
+ """Return the media category (e.g. 'image', 'video') for a path or extension."""
685
+ ext = os.path.splitext(str(obj))[-1] or obj
686
+ if ext:
687
+ for typ, exts in MEDIA_TYPES.items():
688
+ if ext in exts:
689
+ return typ
690
+
691
+
692
+ # ---------------------------------------------------------------------------
693
+ # caller introspection (get_caller_dir)
694
+ # ---------------------------------------------------------------------------
695
+ import inspect # noqa: E402 (kept next to the caller helpers that use it)
696
+
697
+
698
+ def get_caller(i=None):
699
+ """Return the filename of the calling frame ``i`` levels up (default 1)."""
700
+ depth = 1 if i is None else int(i)
701
+ stack = inspect.stack()
702
+ if depth >= len(stack):
703
+ depth = len(stack) - 1
704
+ return stack[depth].filename
705
+
706
+
707
+ def get_caller_path(i=None):
708
+ """Return the absolute, real path of the caller's file."""
709
+ depth = 1 if i is None else int(i)
710
+ file_path = get_caller(depth + 1)
711
+ return os.path.realpath(file_path)
712
+
713
+
714
+ def get_caller_dir(i=None):
715
+ """Return the absolute directory of the caller's file."""
716
+ depth = 1 if i is None else int(i)
717
+ abspath = get_caller_path(depth + 1)
718
+ return os.path.dirname(abspath)
719
+
720
+
721
+ # ---------------------------------------------------------------------------
722
+ # environment values (get_env_value) -- standalone, no abstractEnv class
723
+ # ---------------------------------------------------------------------------
724
+ def _split_eq(line):
725
+ """Split a ``KEY=VALUE`` line into a cleaned ``[key, value]`` pair."""
726
+ if '=' in line:
727
+ key_side = line.split('=')[0]
728
+ value_side = line[len(key_side + '='):]
729
+ return [eatOuter(key_side, [' ', '', '\t']),
730
+ eatAll(value_side, [' ', '', '\t', '\n'])]
731
+ return [line, None]
732
+
733
+
734
+ def _search_for_env_key(key, path, deep_scan=False):
735
+ """Return the value for ``key`` within the env file at ``path`` (or None)."""
736
+ highest = [None, 0]
737
+ if path and os.path.isfile(path):
738
+ with open(path, "r") as f:
739
+ for line in f:
740
+ line_key, line_value = _split_eq(line)
741
+ if line_key == key:
742
+ return line_value
743
+ if deep_scan and key:
744
+ key_parts = 0
745
+ for key_part in key.split('_'):
746
+ if key_part and key_part in line_key:
747
+ key_parts += len(key_part)
748
+ if float(key_parts / len(key)) >= 0.5 and key_parts > highest[1]:
749
+ highest = [line_value, key_parts]
750
+ return line_value
751
+ return None
752
+
753
+
754
+ def get_env_value(key=None, path=None, file_name=None, deep_scan=False):
755
+ """
756
+ Retrieve the value of an environment variable from a ``.env`` style file.
757
+
758
+ Searches, in order: the supplied ``path``, the current working directory,
759
+ the user's home directory, and ``~/.envy_all``.
760
+
761
+ Args:
762
+ key: variable name to look up (defaults to ``MY_PASSWORD``).
763
+ path: a directory to search, or a direct path to an env file.
764
+ file_name: env file name (defaults to ``.env``).
765
+ deep_scan: when True, fall back to fuzzy key matching.
766
+ """
767
+ key = key or 'MY_PASSWORD'
768
+ file_name = file_name or '.env'
769
+ current_folder = os.getcwd()
770
+
771
+ if path and os.path.isfile(path):
772
+ file_name = os.path.basename(path)
773
+ path = os.path.dirname(path)
774
+ else:
775
+ path = path or current_folder
776
+
777
+ home_folder = os.path.expanduser("~")
778
+ envy_all = os.path.join(home_folder, '.envy_all')
779
+
780
+ directories = []
781
+ for directory in [path, current_folder, home_folder, envy_all]:
782
+ if directory and os.path.isdir(directory) and directory not in directories:
783
+ directories.append(directory)
784
+
785
+ for directory in directories:
786
+ env_path = os.path.join(directory, file_name)
787
+ if os.path.isfile(env_path):
788
+ value = _search_for_env_key(key=key, path=env_path, deep_scan=deep_scan)
789
+ if value:
790
+ return value
791
+ return None
792
+
793
+
794
+ # ---------------------------------------------------------------------------
795
+ # dataclass construction helper (get_inputs)
796
+ # ---------------------------------------------------------------------------
797
+ def get_inputs(cls, *args, **kwargs):
798
+ """
799
+ Dynamically construct a (data)class instance from ``args``/``kwargs``,
800
+ filling any missing values from the class's own defaults.
801
+ """
802
+ fields = list(cls.__annotations__.keys())
803
+ values = {}
804
+
805
+ args = list(args)
806
+ for field in fields:
807
+ if field in kwargs:
808
+ values[field] = kwargs[field]
809
+ elif args:
810
+ values[field] = args.pop(0)
811
+ else:
812
+ values[field] = getattr(cls(), field) # default from the class
813
+
814
+ return cls(**values)
815
+
816
+
817
+ # ---------------------------------------------------------------------------
818
+ # lazy import (lazy_import)
819
+ # ---------------------------------------------------------------------------
820
+ nullProxy_logger = logging.getLogger("abstract.lazy_import")
821
+
822
+
823
+ class nullProxy:
824
+ """Safe, chainable, callable placeholder returned for missing modules/attrs."""
825
+
826
+ def __init__(self, name, path=(), fallback=None):
827
+ self._name = name
828
+ self._path = path
829
+ self.fallback = fallback
830
+
831
+ def __getattr__(self, attr):
832
+ return nullProxy(self._name, self._path + (attr,))
833
+
834
+ def __call__(self, *args, **kwargs):
835
+ if self.fallback is not None:
836
+ try:
837
+ return self.fallback(*args, **kwargs)
838
+ except Exception as e:
839
+ nullProxy_logger.info("%s", e)
840
+ nullProxy_logger.warning(
841
+ "[lazy_import] Call to missing module/attr: %s.%s args=%s kwargs=%s",
842
+ self._name, ".".join(self._path), args, kwargs,
843
+ )
844
+ return None
845
+
846
+ def __repr__(self):
847
+ full = ".".join((self._name, *self._path))
848
+ return f"<nullProxy {full}>"
849
+
850
+ def __bool__(self):
851
+ return False
852
+
853
+
854
+ @lru_cache(maxsize=None)
855
+ def lazy_import_single(name, fallback=None):
856
+ """Import ``name`` safely, returning a :class:`nullProxy` if unavailable."""
857
+ if name in sys.modules:
858
+ return sys.modules[name]
859
+ try:
860
+ return importlib.import_module(name)
861
+ except Exception as e:
862
+ nullProxy_logger.warning("[lazy_import] Failed to import '%s': %s", name, e)
863
+ return nullProxy(name, fallback=fallback)
864
+
865
+
866
+ def get_lazy_attr(module_name, *attrs, fallback=None):
867
+ obj = lazy_import(module_name, fallback=fallback)
868
+ for attr in attrs:
869
+ try:
870
+ obj = getattr(obj, attr)
871
+ except Exception:
872
+ return nullProxy(module_name, attrs, fallback=fallback)
873
+ return obj
874
+
875
+
876
+ def lazy_import(name, *attrs, fallback=None):
877
+ """
878
+ Import a module (and optionally walk into ``attrs``) safely.
879
+
880
+ Returns the module/attribute, or a :class:`nullProxy` placeholder if the
881
+ import fails — so the result is always safe to reference.
882
+ """
883
+ if attrs:
884
+ return get_lazy_attr(name, *attrs, fallback=fallback)
885
+ return lazy_import_single(name, fallback=fallback)
886
+
887
+
888
+ # ---------------------------------------------------------------------------
889
+ # logging (get_logFile)
890
+ # ---------------------------------------------------------------------------
891
+ _PACKAGE_NAME = "abstract_utilities"
892
+
893
+ LOG_FORMAT = (
894
+ "[%(asctime)s] "
895
+ "%(levelname)-8s "
896
+ "%(name)s:%(lineno)d | "
897
+ "%(message)s "
898
+ "[target=%(target_file)s:%(target_line)s]"
899
+ )
900
+ DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
901
+
902
+
903
+ class LevelFilter(logging.Filter):
904
+ """Filter that allows selective level enablement/disablement."""
905
+
906
+ def __init__(self):
907
+ super().__init__()
908
+ self.enabled_levels = {
909
+ logging.DEBUG, logging.INFO, logging.WARNING,
910
+ logging.ERROR, logging.CRITICAL,
911
+ }
912
+
913
+ def filter(self, record):
914
+ return record.levelno in self.enabled_levels
915
+
916
+ def enable_level(self, level):
917
+ self.enabled_levels.add(level)
918
+
919
+ def disable_level(self, level):
920
+ self.enabled_levels.discard(level)
921
+
922
+ def is_enabled(self, level):
923
+ return level in self.enabled_levels
924
+
925
+
926
+ class SafeFormatter(logging.Formatter):
927
+ """Formatter that tolerates missing ``target_*`` extra fields."""
928
+
929
+ def format(self, record):
930
+ record.target_file = getattr(record, "target_file", "-")
931
+ record.target_line = getattr(record, "target_line", "-")
932
+ return super().format(record)
933
+
934
+
935
+ def _resolve_log_root():
936
+ venv = os.getenv("VIRTUAL_ENV") or os.getenv("CONDA_PREFIX")
937
+ if venv:
938
+ p = Path(venv) / ".logs" / _PACKAGE_NAME
939
+ p.mkdir(parents=True, exist_ok=True)
940
+ return p
941
+
942
+ home = Path.home() / ".cache" / _PACKAGE_NAME / "logs"
943
+ try:
944
+ home.mkdir(parents=True, exist_ok=True)
945
+ return home
946
+ except PermissionError:
947
+ pass
948
+
949
+ try:
950
+ syslog = Path("/var/log") / _PACKAGE_NAME
951
+ syslog.mkdir(parents=True, exist_ok=True)
952
+ return syslog
953
+ except PermissionError:
954
+ fallback = Path("/tmp") / _PACKAGE_NAME / "logs"
955
+ fallback.mkdir(parents=True, exist_ok=True)
956
+ return fallback
957
+
958
+
959
+ LOG_ROOT = _resolve_log_root()
960
+
961
+ # Filters keyed by logger name for runtime control.
962
+ _level_filters = {}
963
+
964
+
965
+ def get_logFile(name, *, level=logging.INFO, console=True,
966
+ max_bytes=5 * 1024 * 1024, backup_count=5):
967
+ """
968
+ Return a configured rotating-file logger named ``name``.
969
+
970
+ The logger writes to ``LOG_ROOT/<name>.log`` and (optionally) the console,
971
+ using :class:`SafeFormatter` and a per-logger :class:`LevelFilter`.
972
+ """
973
+ logger = logging.getLogger(name)
974
+
975
+ # Skip re-initialization if already configured.
976
+ if logger.handlers:
977
+ return logger
978
+
979
+ logger.setLevel(logging.DEBUG) # Logger accepts everything.
980
+
981
+ formatter = SafeFormatter(LOG_FORMAT, DATE_FORMAT)
982
+
983
+ level_filter = LevelFilter()
984
+ if level > logging.DEBUG:
985
+ level_filter.disable_level(logging.DEBUG)
986
+ _level_filters[name] = level_filter
987
+
988
+ try:
989
+ file_handler = RotatingFileHandler(
990
+ LOG_ROOT / f"{name}.log",
991
+ maxBytes=max_bytes,
992
+ backupCount=backup_count,
993
+ encoding="utf-8",
994
+ )
995
+ file_handler.setFormatter(formatter)
996
+ file_handler.setLevel(logging.DEBUG)
997
+ file_handler.addFilter(level_filter)
998
+ logger.addHandler(file_handler)
999
+ except PermissionError:
1000
+ logger.addHandler(logging.NullHandler())
1001
+
1002
+ if console:
1003
+ console_handler = logging.StreamHandler(sys.stdout)
1004
+ console_handler.setFormatter(formatter)
1005
+ console_handler.setLevel(logging.DEBUG)
1006
+ console_handler.addFilter(level_filter)
1007
+ logger.addHandler(console_handler)
1008
+
1009
+ logger.propagate = False
1010
+ return logger
1011
+
1012
+
1013
+ # ---------------------------------------------------------------------------
1014
+ # SingletonMeta (abstract_utilities.SingletonMeta)
1015
+ # ---------------------------------------------------------------------------
1016
+ import threading as _threading # noqa: E402
1017
+
1018
+
1019
+ class SingletonMeta(type):
1020
+ """Thread-safe singleton metaclass: one instance per class."""
1021
+
1022
+ _instances: dict = {}
1023
+ _lock = _threading.Lock()
1024
+
1025
+ def __call__(cls, *args, **kwargs):
1026
+ with cls._lock:
1027
+ if cls not in cls._instances:
1028
+ cls._instances[cls] = super().__call__(*args, **kwargs)
1029
+ return cls._instances[cls]
1030
+
1031
+
1032
+ # ---------------------------------------------------------------------------
1033
+ # parse_url (abstract_utilities url helper)
1034
+ # ---------------------------------------------------------------------------
1035
+ from urllib.parse import urlparse as _urlparse # noqa: E402
1036
+
1037
+
1038
+ def parse_url(url):
1039
+ """Decompose a URL into a dict of components.
1040
+
1041
+ ``path`` is returned without its leading slash so ``path.split('/')[0]``
1042
+ yields the first path segment (e.g. the owner in an ``owner/repo`` URL),
1043
+ matching how the original abstract_utilities helper was consumed.
1044
+ """
1045
+ p = _urlparse(str(url or ""))
1046
+ return {
1047
+ "scheme": p.scheme,
1048
+ "netloc": p.netloc,
1049
+ "host": p.hostname,
1050
+ "port": p.port,
1051
+ "path": (p.path or "").strip("/"),
1052
+ "params": p.params,
1053
+ "query": p.query,
1054
+ "fragment": p.fragment,
1055
+ }
1056
+
1057
+
1058
+ __all__ = [
1059
+ "derive_media_type",
1060
+ "eatAll",
1061
+ "get_any_value",
1062
+ "get_caller_dir",
1063
+ "get_dirlist",
1064
+ "get_env_value",
1065
+ "get_file_parts",
1066
+ "get_files",
1067
+ "get_files_and_dirs",
1068
+ "get_home_dir",
1069
+ "get_inputs",
1070
+ "get_logFile",
1071
+ "is_dir",
1072
+ "is_file",
1073
+ "is_number",
1074
+ "join_path",
1075
+ "lazy_import",
1076
+ "make_list",
1077
+ "makedirs",
1078
+ "nullProxy",
1079
+ "parse_url",
1080
+ "read_from_file",
1081
+ "safe_dump_to_file",
1082
+ "safe_dump_to_json",
1083
+ "safe_load_from_json",
1084
+ "safe_read_from_json",
1085
+ "SingletonMeta",
1086
+ "write_to_file",
1087
+ ]
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: abstract_essentials
3
+ Version: 0.0.0.1
4
+ Summary: The lean, dependency-free core of abstract_utilities: stdlib-only helpers (json/path/file/string/list/log) safe to import anywhere, including Termux/phone.
5
+ Author-email: putkoff <partners@abstractendeavors.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/AbstractEndeavors/abstract_essentials
8
+ Keywords: utilities,stdlib,helpers,dependency-free
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+
18
+ # abstract_essentials
19
+
20
+ The lean, **dependency-free** core carved out of `abstract_utilities`.
21
+
22
+ Every symbol here depends only on the Python standard library — no third-party
23
+ imports, no `from .imports import *` chains, no submodule fan-out. It imports fast
24
+ and installs anywhere (desktop, server, **Termux/Android phone**), which makes it a
25
+ safe foundation for the rest of the `abstract_*` tree to build on without dragging
26
+ in the monolith.
27
+
28
+ ## Why this exists
29
+
30
+ `abstract_utilities` is imported by nearly every `abstract_*` package, so its size
31
+ and entanglement (star-exports across a dozen submodules) ripple everywhere.
32
+ `abstract_essentials` is the ~50-function subset that is actually used in practice,
33
+ extracted as a clean, explicit API.
34
+
35
+ ## Install
36
+
37
+ ```sh
38
+ pip install abstract_essentials
39
+ ```
40
+
41
+ ## Use
42
+
43
+ ```python
44
+ from abstract_essentials import make_list, get_any_value, safe_read_from_json, get_logFile
45
+ ```
46
+
47
+ The public API is whatever is listed in `abstract_essentials.__all__` (53 symbols:
48
+ json/path/file/string/list/type/log helpers).
49
+
50
+ ## Migrating off abstract_utilities
51
+
52
+ `abstract_utilities` can become a thin compatibility shim that re-exports from here
53
+ (see `abstract_utilities_compat_shim.py` shipped alongside this scaffold), so existing
54
+ `from abstract_utilities import X` keeps working while new code imports from
55
+ `abstract_essentials`.
56
+
57
+ ## License
58
+
59
+ MIT — putkoff / Abstract Endeavors.
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.cfg
4
+ src/abstract_essentials/__init__.py
5
+ src/abstract_essentials/utils.py
6
+ src/abstract_essentials.egg-info/PKG-INFO
7
+ src/abstract_essentials.egg-info/SOURCES.txt
8
+ src/abstract_essentials.egg-info/dependency_links.txt
9
+ src/abstract_essentials.egg-info/top_level.txt