ae-base 0.3.41__tar.gz → 0.3.43__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.
- {ae_base-0.3.41/ae_base.egg-info → ae_base-0.3.43}/PKG-INFO +5 -5
- {ae_base-0.3.41 → ae_base-0.3.43}/README.md +4 -4
- {ae_base-0.3.41 → ae_base-0.3.43}/ae/base.py +158 -72
- {ae_base-0.3.41 → ae_base-0.3.43/ae_base.egg-info}/PKG-INFO +5 -5
- {ae_base-0.3.41 → ae_base-0.3.43}/tests/test_base.py +40 -21
- {ae_base-0.3.41 → ae_base-0.3.43}/LICENSE.md +0 -0
- {ae_base-0.3.41 → ae_base-0.3.43}/ae_base.egg-info/SOURCES.txt +0 -0
- {ae_base-0.3.41 → ae_base-0.3.43}/ae_base.egg-info/dependency_links.txt +0 -0
- {ae_base-0.3.41 → ae_base-0.3.43}/ae_base.egg-info/requires.txt +0 -0
- {ae_base-0.3.41 → ae_base-0.3.43}/ae_base.egg-info/top_level.txt +0 -0
- {ae_base-0.3.41 → ae_base-0.3.43}/ae_base.egg-info/zip-safe +0 -0
- {ae_base-0.3.41 → ae_base-0.3.43}/setup.cfg +0 -0
- {ae_base-0.3.41 → ae_base-0.3.43}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ae_base
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.43
|
|
4
4
|
Summary: ae namespace module portion base: basic constants, helper functions and context manager
|
|
5
5
|
Home-page: https://gitlab.com/ae-group/ae_base
|
|
6
6
|
Author: AndiEcker
|
|
@@ -55,17 +55,17 @@ Requires-Dist: twine; extra == "tests"
|
|
|
55
55
|
|
|
56
56
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.94 -->
|
|
57
57
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
|
|
58
|
-
# base 0.3.
|
|
58
|
+
# base 0.3.43
|
|
59
59
|
|
|
60
60
|
[](
|
|
61
61
|
https://gitlab.com/ae-group/ae_base)
|
|
62
62
|
[](
|
|
64
|
+
https://gitlab.com/ae-group/ae_base/-/tree/release0.3.42)
|
|
65
65
|
[](
|
|
66
66
|
https://pypi.org/project/ae-base/#history)
|
|
67
67
|
|
|
68
|
-
>ae_base module 0.3.
|
|
68
|
+
>ae_base module 0.3.43.
|
|
69
69
|
|
|
70
70
|
[](
|
|
71
71
|
https://ae-group.gitlab.io/ae_base/coverage/index.html)
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.94 -->
|
|
2
2
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
|
|
3
|
-
# base 0.3.
|
|
3
|
+
# base 0.3.43
|
|
4
4
|
|
|
5
5
|
[](
|
|
6
6
|
https://gitlab.com/ae-group/ae_base)
|
|
7
7
|
[](
|
|
9
|
+
https://gitlab.com/ae-group/ae_base/-/tree/release0.3.42)
|
|
10
10
|
[](
|
|
11
11
|
https://pypi.org/project/ae-base/#history)
|
|
12
12
|
|
|
13
|
-
>ae_base module 0.3.
|
|
13
|
+
>ae_base module 0.3.43.
|
|
14
14
|
|
|
15
15
|
[](
|
|
16
16
|
https://ae-group.gitlab.io/ae_base/coverage/index.html)
|
|
@@ -46,8 +46,9 @@ the function :func:`duplicates` returns the duplicates of an iterable type.
|
|
|
46
46
|
to normalize a file path, in order to remove `.`, `..` placeholders, to resolve symbolic links or to make it relative or
|
|
47
47
|
absolute, call the function :func:`norm_path`.
|
|
48
48
|
|
|
49
|
-
:func:`
|
|
50
|
-
use the function :func:`
|
|
49
|
+
:func:`defuse` converts special characters of a URI/URL or a file path string, resulting in a string that can be used
|
|
50
|
+
either as a URL slug or as a file name. use the function :func:`dedefuse` to convert this string back to the
|
|
51
|
+
corresponding URL/URI or file path.
|
|
51
52
|
|
|
52
53
|
:func:`camel_to_snake` and :func:`snake_to_camel` providing name conversions of class and method names.
|
|
53
54
|
|
|
@@ -143,6 +144,13 @@ to determine e.g. variable values of the callers of a function/method.
|
|
|
143
144
|
:attr:`title <AppBase.app_title>` of an application, if these values are not specified in the instance initializer.
|
|
144
145
|
|
|
145
146
|
another useful helper function provided by this portion to inspect and debug your code is :func:`full_stack_trace`.
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
os.path shortcuts
|
|
150
|
+
-----------------
|
|
151
|
+
|
|
152
|
+
the following data items are pointers to shortcut the lookup to their related functions in the
|
|
153
|
+
Python module :mod:`os.path`:
|
|
146
154
|
"""
|
|
147
155
|
import datetime
|
|
148
156
|
import getpass
|
|
@@ -155,7 +163,6 @@ import shutil
|
|
|
155
163
|
import socket
|
|
156
164
|
import sys
|
|
157
165
|
import unicodedata
|
|
158
|
-
import urllib.parse
|
|
159
166
|
import warnings
|
|
160
167
|
|
|
161
168
|
from configparser import ConfigParser, ExtendedInterpolation
|
|
@@ -166,7 +173,21 @@ from types import ModuleType
|
|
|
166
173
|
from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Tuple, Union, cast
|
|
167
174
|
|
|
168
175
|
|
|
169
|
-
__version__ = '0.3.
|
|
176
|
+
__version__ = '0.3.43'
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
os_path_abspath = os.path.abspath
|
|
180
|
+
os_path_basename = os.path.basename
|
|
181
|
+
os_path_dirname = os.path.dirname
|
|
182
|
+
os_path_expanduser = os.path.expanduser
|
|
183
|
+
os_path_isdir = os.path.isdir
|
|
184
|
+
os_path_isfile = os.path.isfile
|
|
185
|
+
os_path_join = os.path.join
|
|
186
|
+
os_path_normpath = os.path.normpath
|
|
187
|
+
os_path_realpath = os.path.realpath
|
|
188
|
+
os_path_relpath = os.path.relpath
|
|
189
|
+
os_path_sep = os.path.sep
|
|
190
|
+
os_path_splitext = os.path.splitext
|
|
170
191
|
|
|
171
192
|
|
|
172
193
|
DOCS_FOLDER = 'docs' #: project documentation root folder name
|
|
@@ -256,10 +277,10 @@ def app_name_guess() -> str:
|
|
|
256
277
|
if not app_name:
|
|
257
278
|
unspecified_app_names = ('ae_base', 'app', '_jb_pytest_runner', 'main', '__main__', 'pydevconsole', 'src')
|
|
258
279
|
path = sys.argv[0]
|
|
259
|
-
app_name =
|
|
280
|
+
app_name = os_path_splitext(os_path_basename(path))[0]
|
|
260
281
|
if app_name.lower() in unspecified_app_names:
|
|
261
282
|
path = os.getcwd()
|
|
262
|
-
app_name =
|
|
283
|
+
app_name = os_path_basename(path)
|
|
263
284
|
if app_name.lower() in unspecified_app_names:
|
|
264
285
|
app_name = "unguessable"
|
|
265
286
|
return app_name
|
|
@@ -273,7 +294,7 @@ def build_config_variable_values(*names_defaults: Tuple[str, Any], section: str
|
|
|
273
294
|
:return: tuple of build config variable values (using the passed default value if not specified
|
|
274
295
|
in the :data:`BUILD_CONFIG_FILE` spec file or if the spec file does not exist in cwd).
|
|
275
296
|
"""
|
|
276
|
-
if not
|
|
297
|
+
if not os_path_isfile(BUILD_CONFIG_FILE):
|
|
277
298
|
return tuple(def_val for name, def_val in names_defaults)
|
|
278
299
|
|
|
279
300
|
config = instantiate_config_parser()
|
|
@@ -315,6 +336,96 @@ def deep_dict_update(data: dict, update: dict):
|
|
|
315
336
|
data[upd_key] = upd_val
|
|
316
337
|
|
|
317
338
|
|
|
339
|
+
URI_SEP_CHAR = '⫻' # U+2AFB: TRIPLE SOLIDUS BINARY RELATION
|
|
340
|
+
ASCII_UNICODE = (
|
|
341
|
+
('/', '⁄'), # U+2044: Fraction Slash; '∕' U+2215: Division Slash; '⧸' U+29F8: Big Solidus
|
|
342
|
+
# ; '╱' U+FF0F: Fullwidth Solidus; '╱' U+2571: Box Drawings Light Diagonal Upper Right to Lower Left
|
|
343
|
+
('|', '।'), # U+0964: Devanagari Danda
|
|
344
|
+
('\\', '﹨'), # U+FE68: SMALL REVERSE SOLIDUS; '⑊' U+244A OCR DOUBLE BACKSLASH; '⧵' U+29F5 REV. SOLIDUS OPERATOR
|
|
345
|
+
(':', '﹕'), # U+FE55: Small Colon
|
|
346
|
+
('*', '﹡'), # U+FE61: Small Asterisk
|
|
347
|
+
('?', '﹖'), # U+FE56: Small Question Mark
|
|
348
|
+
('"', '"'), # U+FF02: Fullwidth Quotation Mark
|
|
349
|
+
("'", '‘'), # U+2018: Left Single; '’' U+2019: Right Single; '‛' U+201B: Single High-Reversed-9 Quotation Mark
|
|
350
|
+
('<', '⟨'), # U+27E8: LEFT ANGLE BRACKET; '‹' U+2039: Single Left-Pointing Angle Quotation Mark
|
|
351
|
+
('>', '⟩'), # U+27E9: RIGHT ANGLE BRACKET; '›' U+203A: Single Right-Pointing Angle Quotation Mark
|
|
352
|
+
('(', '⟮'), # U+27EE: MATHEMATICAL LEFT FLATTENED PARENTHESIS
|
|
353
|
+
(')', '⟯'), # U+27EF: MATHEMATICAL RIGHT FLATTENED PARENTHESIS
|
|
354
|
+
('[', '⟦'), # U+27E6: MATHEMATICAL LEFT WHITE SQUARE BRACKET
|
|
355
|
+
(']', '⟧'), # U+27E7: MATHEMATICAL RIGHT WHITE SQUARE BRACKET
|
|
356
|
+
('_', '𛲖'), # U+1BC96: Duployan Affix Low Line; '_' U+FF3F Fullwidth Low Line
|
|
357
|
+
('#', '﹟'), # U+FE5F: Small Number Sign
|
|
358
|
+
(';', '﹔'), # U+FE54: Small Semicolon
|
|
359
|
+
('@', '﹫'), # U+FE6B: Small Commercial At
|
|
360
|
+
('&', '﹠'), # U+FE60: Small Ampersand
|
|
361
|
+
('=', '﹦'), # U+FE66: Small Equals Sign
|
|
362
|
+
('+', '﹢'), # U+FE62: Small Plus Sign
|
|
363
|
+
('$', '﹩'), # U+FE69: Small Dollar Sign
|
|
364
|
+
('%', '﹪'), # U+FE6A: Small Percent Sign
|
|
365
|
+
('^', '^'), # U+FF3E: Fullwidth Circumflex Accent
|
|
366
|
+
(',', '﹐'), # U+FE50: Small Comma
|
|
367
|
+
(' ', ' '), # U+3000: Ideographic Space; ' ' U+200A Hair Space; ' ' U+2007 Figure Space;
|
|
368
|
+
# ' ' U+2009 Thin; ' ' U+2003 Em Space; ' ' U+2002 En Space; ' ' U+2008 Punctuation Space
|
|
369
|
+
# ' ' U+00A0: No-Break Space (NBSP); ' ' U+202F: Narrow No-Break Space (NNBSP)
|
|
370
|
+
(chr(127), '␡'), # U+2421: DELETE SYMBOL
|
|
371
|
+
)
|
|
372
|
+
""" transformation table of special ASCII to Unicode alternative character,
|
|
373
|
+
see https://www.compart.com/en/unicode/category/Po and https://xahlee.info/comp/unicode_naming_slash.html (http!) """
|
|
374
|
+
|
|
375
|
+
ASCII_TO_UNICODE = dict(ASCII_UNICODE) #: map to convert ASCII to an alternative defused Unicode character
|
|
376
|
+
UNICODE_TO_ASCII = {unicode_char: ascii_char for ascii_char, unicode_char in ASCII_UNICODE} #: Unicode to ASCII map
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def dedefuse(value: str) -> str:
|
|
380
|
+
""" convert a string that got defused with :func:`defuse` back to its original form.
|
|
381
|
+
|
|
382
|
+
:param value: string defused with the function :func:`defuse`.
|
|
383
|
+
:return: re-activated form of the string (with all ASCII special characters recovered).
|
|
384
|
+
"""
|
|
385
|
+
original = ""
|
|
386
|
+
for char in value:
|
|
387
|
+
if char in UNICODE_TO_ASCII:
|
|
388
|
+
char = UNICODE_TO_ASCII[char]
|
|
389
|
+
elif 0x2400 <= (code := ord(char)) <= 0x241F:
|
|
390
|
+
char = chr(code - 0x2400)
|
|
391
|
+
original += char
|
|
392
|
+
|
|
393
|
+
return original.replace(URI_SEP_CHAR, '://')
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def defuse(value: str) -> str:
|
|
397
|
+
""" convert a file path or a URI into a defused/presentational form to be usable as URL slug or file/folder name.
|
|
398
|
+
|
|
399
|
+
:param value: any string to defuse (replace special chars with Unicode alternatives).
|
|
400
|
+
:return: string with its special characters replaced by its pure presentational alternatives.
|
|
401
|
+
|
|
402
|
+
the ASCII character range 0..31 gets converted to the Unicode range U+2400 + ord(char): 0==U+2400 ... 31==U+241F.
|
|
403
|
+
|
|
404
|
+
in *nix only / and \0 are not allowed characters in file names.
|
|
405
|
+
|
|
406
|
+
in MS Windows are not allowed: ASCII 0...31): / | \\ : * ? ” % < > ( ). some blogs recommend to also not allow
|
|
407
|
+
(convert) the characters # and '.
|
|
408
|
+
only old POSIX seems to be even more restricted (only allowing alphanumeric characters plus . - and _).
|
|
409
|
+
|
|
410
|
+
more on allowed characters in file names in the answers of RedGrittyBrick on https://superuser.com/questions/358855
|
|
411
|
+
and of Christopher Oezbek on https://stackoverflow.com/questions/1976007.
|
|
412
|
+
|
|
413
|
+
file name length is not restricted/shortened by this function, although the maximum is 255 characters on most OSs.
|
|
414
|
+
|
|
415
|
+
.. hint:: use :func:`dedefuse` to convert the defused string back to the corresponding URI/file-path.
|
|
416
|
+
|
|
417
|
+
"""
|
|
418
|
+
defused = ""
|
|
419
|
+
value = value.replace('://', URI_SEP_CHAR) # make URIs shorter
|
|
420
|
+
for char in value:
|
|
421
|
+
if char in ASCII_TO_UNICODE:
|
|
422
|
+
char = ASCII_TO_UNICODE[char]
|
|
423
|
+
elif (code := ord(char)) <= 31:
|
|
424
|
+
char = chr(0x2400 + code)
|
|
425
|
+
defused += char
|
|
426
|
+
return defused
|
|
427
|
+
|
|
428
|
+
|
|
318
429
|
def dummy_function(*_args, **_kwargs):
|
|
319
430
|
""" null function accepting any arguments and returning None.
|
|
320
431
|
|
|
@@ -358,17 +469,6 @@ def env_str(name: str, convert_name: bool = False) -> Optional[str]:
|
|
|
358
469
|
return os.environ.get(name)
|
|
359
470
|
|
|
360
471
|
|
|
361
|
-
def filename2uri(file_name: str) -> str:
|
|
362
|
-
""" convert a file name converted by :func:`uri2filename` back to its representation as a URI
|
|
363
|
-
|
|
364
|
-
:param file_name: name of the file/folder to convert back to its URI representation.
|
|
365
|
-
:return: URI string.
|
|
366
|
-
|
|
367
|
-
.. hint:: to ensure proper conversion the specified file name has to be created by :func:`uri2filename`.
|
|
368
|
-
"""
|
|
369
|
-
return urllib.parse.unquote(file_name)
|
|
370
|
-
|
|
371
|
-
|
|
372
472
|
def force_encoding(text: Union[str, bytes], encoding: str = DEF_ENCODING, errors: str = DEF_ENCODE_ERRORS) -> str:
|
|
373
473
|
""" force/ensure the encoding of text (str or bytes) without any UnicodeDecodeError/UnicodeEncodeError.
|
|
374
474
|
|
|
@@ -417,8 +517,8 @@ def import_module(import_name: str, path: Optional[Union[str, UnsetType]] = UNSE
|
|
|
417
517
|
:return: a reference to the loaded module or ``None`` if module could not be imported.
|
|
418
518
|
"""
|
|
419
519
|
if path is UNSET:
|
|
420
|
-
path = import_name.replace('.',
|
|
421
|
-
path += PY_EXT if
|
|
520
|
+
path = import_name.replace('.', os_path_sep)
|
|
521
|
+
path += PY_EXT if os_path_isfile(path + PY_EXT) else os_path_sep + PY_INIT
|
|
422
522
|
mod_ref = None
|
|
423
523
|
|
|
424
524
|
spec = importlib.util.spec_from_file_location(import_name, path) # type: ignore # silly mypy
|
|
@@ -469,7 +569,7 @@ def load_dotenvs():
|
|
|
469
569
|
"""
|
|
470
570
|
load_env_var_defaults(os.getcwd())
|
|
471
571
|
if file_name := stack_var('__file__'):
|
|
472
|
-
load_env_var_defaults(
|
|
572
|
+
load_env_var_defaults(os_path_dirname(os_path_abspath(file_name)))
|
|
473
573
|
|
|
474
574
|
|
|
475
575
|
def load_env_var_defaults(start_dir: str):
|
|
@@ -485,18 +585,18 @@ def load_env_var_defaults(start_dir: str):
|
|
|
485
585
|
only variables that are not declared in :data:`os.environ` will be added (with the
|
|
486
586
|
value specified in the ``.env`` file to be loaded).
|
|
487
587
|
"""
|
|
488
|
-
file_path =
|
|
489
|
-
if not
|
|
490
|
-
file_path =
|
|
588
|
+
file_path = os_path_abspath(os_path_join(start_dir, DOTENV_FILE_NAME))
|
|
589
|
+
if not os_path_isfile(file_path):
|
|
590
|
+
file_path = os_path_join(os_path_dirname(start_dir), DOTENV_FILE_NAME)
|
|
491
591
|
|
|
492
|
-
while
|
|
592
|
+
while os_path_isfile(file_path):
|
|
493
593
|
for var_nam, var_val in parse_dotenv(file_path).items():
|
|
494
594
|
if var_nam not in os.environ:
|
|
495
595
|
os.environ[var_nam] = var_val
|
|
496
596
|
|
|
497
597
|
if os.sep not in file_path:
|
|
498
598
|
break # pragma: no cover # prevent endless-loop for ``.env`` file in root dir (os.sep == '/')
|
|
499
|
-
file_path =
|
|
599
|
+
file_path = os_path_join(os_path_dirname(os_path_dirname(file_path)), DOTENV_FILE_NAME)
|
|
500
600
|
|
|
501
601
|
|
|
502
602
|
def main_file_paths_parts(portion_name: str) -> Tuple[Tuple[str, ...], ...]:
|
|
@@ -620,20 +720,20 @@ def norm_path(path: str, make_absolute: bool = True, remove_base_path: str = "",
|
|
|
620
720
|
"""
|
|
621
721
|
path = path or "."
|
|
622
722
|
if path[0] == "~":
|
|
623
|
-
path =
|
|
723
|
+
path = os_path_expanduser(path)
|
|
624
724
|
|
|
625
725
|
if remove_dots:
|
|
626
|
-
path =
|
|
726
|
+
path = os_path_normpath(path)
|
|
627
727
|
|
|
628
728
|
if resolve_sym_links:
|
|
629
|
-
path =
|
|
729
|
+
path = os_path_realpath(path)
|
|
630
730
|
elif make_absolute:
|
|
631
|
-
path =
|
|
731
|
+
path = os_path_abspath(path)
|
|
632
732
|
|
|
633
733
|
if remove_base_path:
|
|
634
734
|
if remove_base_path[0] == "~":
|
|
635
|
-
remove_base_path =
|
|
636
|
-
path =
|
|
735
|
+
remove_base_path = os_path_expanduser(remove_base_path)
|
|
736
|
+
path = os_path_relpath(path, remove_base_path)
|
|
637
737
|
|
|
638
738
|
return path
|
|
639
739
|
|
|
@@ -768,23 +868,22 @@ def project_main_file(import_name: str, project_path: str = "") -> str:
|
|
|
768
868
|
sister project (under the same project parent folder).
|
|
769
869
|
:return: absolute file path/name of main module or empty string if no main/version file found.
|
|
770
870
|
"""
|
|
771
|
-
join = os.path.join
|
|
772
871
|
*namespace_dirs, portion_name = import_name.split('.')
|
|
773
872
|
project_name = ('_'.join(namespace_dirs) + '_' if namespace_dirs else "") + portion_name
|
|
774
873
|
paths_parts = main_file_paths_parts(portion_name)
|
|
775
874
|
|
|
776
875
|
project_path = norm_path(project_path)
|
|
777
876
|
module_paths = []
|
|
778
|
-
if
|
|
779
|
-
module_paths.append(
|
|
877
|
+
if os_path_basename(project_path) != project_name:
|
|
878
|
+
module_paths.append(os_path_join(os_path_dirname(project_path), project_name, *namespace_dirs))
|
|
780
879
|
if namespace_dirs:
|
|
781
|
-
module_paths.append(
|
|
880
|
+
module_paths.append(os_path_join(project_path, *namespace_dirs))
|
|
782
881
|
module_paths.append(project_path)
|
|
783
882
|
|
|
784
883
|
for module_path in module_paths:
|
|
785
884
|
for path_parts in paths_parts:
|
|
786
|
-
main_file =
|
|
787
|
-
if
|
|
885
|
+
main_file = os_path_join(module_path, *path_parts)
|
|
886
|
+
if os_path_isfile(main_file):
|
|
788
887
|
return main_file
|
|
789
888
|
return ""
|
|
790
889
|
|
|
@@ -899,7 +998,7 @@ def stack_vars(*skip_modules: str,
|
|
|
899
998
|
:param max_depth: the maximum depth in the call stack from which to return the variables. if the specified
|
|
900
999
|
argument is not zero and no :paramref:`~stack_vars.skip_modules` are specified then the
|
|
901
1000
|
first deeper stack frame that is not within the default :data:`SKIPPED_MODULES` will be
|
|
902
|
-
returned. if this argument and :paramref:`~
|
|
1001
|
+
returned. if this argument and :paramref:`~stack_vars.find_name` get not passed then the
|
|
903
1002
|
variables of the top stack frame will be returned.
|
|
904
1003
|
:return: tuple of the global and local variable dicts and the depth in the call stack.
|
|
905
1004
|
"""
|
|
@@ -985,29 +1084,6 @@ def to_ascii(unicode_str: str) -> str:
|
|
|
985
1084
|
return "".join([c for c in nfkd_form if not unicodedata.combining(c)]).replace('ß', "ss").replace('€', "Euro")
|
|
986
1085
|
|
|
987
1086
|
|
|
988
|
-
def uri2filename(uri: str) -> str:
|
|
989
|
-
""" convert a URI to be usable as name of a file or folder
|
|
990
|
-
|
|
991
|
-
:param uri: URI to convert to a corresponding file name, that will be revertible back to this URI.
|
|
992
|
-
:return: name of a file/folder representing the specified URI.
|
|
993
|
-
|
|
994
|
-
in *nix only / and \0 are not allowed characters in file names.
|
|
995
|
-
in MS Windows are not allowed: ASCII 0...31): / \\ : * ? ” < > | (). some blogs recommend to also not allow
|
|
996
|
-
(convert) the characters # and '.
|
|
997
|
-
only old POSIX seems to be even more restricted (only allowing alphanumeric characters plus . - and _).
|
|
998
|
-
|
|
999
|
-
file name length is not restricted/shortened by this function, although the maximum is 255 characters on most OSs.
|
|
1000
|
-
|
|
1001
|
-
more on allowed characters in file names in the answers of RedGrittyBrick on https://superuser.com/questions/358855
|
|
1002
|
-
and of Christopher Oezbek on https://stackoverflow.com/questions/1976007.
|
|
1003
|
-
|
|
1004
|
-
.. hint:: use :func:`filename2uri` to convert the resulting file name back to the corresponding URO
|
|
1005
|
-
"""
|
|
1006
|
-
# using urllib.parse.quote(uri, safe="") instead would convert also any non-ascii (e.g. umlaut) characters into hex
|
|
1007
|
-
# added [] to str.join() argument because List comprehensions are faster than generator expressions
|
|
1008
|
-
return "".join([f"%{hex(ord(_))[2:].upper()}" if _ in '/|\\:*?"<>%' else _ for _ in uri])
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
1087
|
def utc_datetime() -> datetime.datetime:
|
|
1012
1088
|
""" return the current UTC timestamp as string (to use as suffix for file and variable/attribute names).
|
|
1013
1089
|
|
|
@@ -1020,14 +1096,18 @@ def write_file(file_path: str, content: Union[str, bytes], extra_mode: str = "",
|
|
|
1020
1096
|
""" (over)write the file specified by :paramref:`~write_file.file_path` with text or binary/bytes content.
|
|
1021
1097
|
|
|
1022
1098
|
:param file_path: file path/name to write the passed content into (overwriting any previous content!).
|
|
1023
|
-
:param content: new file content either
|
|
1024
|
-
|
|
1025
|
-
:param extra_mode: open mode flag characters. passed
|
|
1026
|
-
this argument starts with 'a', else this argument value will be appended to 'w'
|
|
1099
|
+
:param content: new file content passed either as string or bytes array. if a bytes array get passed
|
|
1100
|
+
then this method will automatically write the content as binary.
|
|
1101
|
+
:param extra_mode: additional open mode flag characters. passed to the `mode` argument of :func:`open` if
|
|
1102
|
+
this argument starts with 'a' or 'w', else this argument value will be appended to 'w'
|
|
1103
|
+
before it get passed to the `mode` argument of :func:`open`.
|
|
1104
|
+
if the :paramref:`~write_file.content` is a bytes array, then a 'b' character will
|
|
1105
|
+
be automatically added to the `mode` argument of :func:`open` (if not already specified
|
|
1106
|
+
in this argument).
|
|
1027
1107
|
:param encoding: encoding used to write/convert/interpret the file content to write.
|
|
1028
1108
|
:raises FileExistsError: if file exists already and is write-protected.
|
|
1029
1109
|
:raises FileNotFoundError: if parts of the file path do not exist.
|
|
1030
|
-
:raises OSError: if :paramref:`~
|
|
1110
|
+
:raises OSError: if :paramref:`~write_file.file_path` is misspelled or contains invalid characters.
|
|
1031
1111
|
:raises PermissionError: if current OS user account lacks permissions to read the file content.
|
|
1032
1112
|
:raises ValueError: on decoding errors.
|
|
1033
1113
|
|
|
@@ -1040,7 +1120,13 @@ def write_file(file_path: str, content: Union[str, bytes], extra_mode: str = "",
|
|
|
1040
1120
|
for an intent result.
|
|
1041
1121
|
Related german docs: https://developer.android.com/training/data-storage/shared/media?hl=de
|
|
1042
1122
|
"""
|
|
1043
|
-
|
|
1123
|
+
if isinstance(content, bytes) and 'b' not in extra_mode:
|
|
1124
|
+
extra_mode += 'b'
|
|
1125
|
+
|
|
1126
|
+
if extra_mode == '' or extra_mode[0] not in ('a', 'w'):
|
|
1127
|
+
extra_mode = 'w' + extra_mode
|
|
1128
|
+
|
|
1129
|
+
with open(file_path, mode=extra_mode, encoding=encoding) as file_handle:
|
|
1044
1130
|
file_handle.write(content)
|
|
1045
1131
|
|
|
1046
1132
|
|
|
@@ -1069,16 +1155,16 @@ class ErrorMsgMixin:
|
|
|
1069
1155
|
PACKAGE_NAME = stack_var('__name__') or 'unspecified_package'
|
|
1070
1156
|
PACKAGE_DOMAIN = 'org.test'
|
|
1071
1157
|
PERMISSIONS = "INTERNET, VIBRATE, READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE"
|
|
1072
|
-
if
|
|
1158
|
+
if os_path_isfile(BUILD_CONFIG_FILE): # pragma: no cover
|
|
1073
1159
|
PACKAGE_NAME, PACKAGE_DOMAIN, PERMISSIONS = build_config_variable_values(
|
|
1074
1160
|
('package.name', PACKAGE_NAME),
|
|
1075
1161
|
('package.domain', PACKAGE_DOMAIN),
|
|
1076
1162
|
('android.permissions', PERMISSIONS))
|
|
1077
1163
|
elif os_platform == 'android': # pragma: no cover
|
|
1078
1164
|
_importing_package = norm_path(stack_var('__file__') or 'empty_package' + PY_EXT)
|
|
1079
|
-
if
|
|
1080
|
-
_importing_package =
|
|
1081
|
-
_importing_package =
|
|
1165
|
+
if os_path_basename(_importing_package) in (PY_INIT, PY_MAIN):
|
|
1166
|
+
_importing_package = os_path_dirname(_importing_package)
|
|
1167
|
+
_importing_package = os_path_splitext(os_path_basename(_importing_package))[0]
|
|
1082
1168
|
write_file(f'{_importing_package}_debug.log', f"{BUILD_CONFIG_FILE} not bundled - using defaults\n", extra_mode='a')
|
|
1083
1169
|
|
|
1084
1170
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ae_base
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.43
|
|
4
4
|
Summary: ae namespace module portion base: basic constants, helper functions and context manager
|
|
5
5
|
Home-page: https://gitlab.com/ae-group/ae_base
|
|
6
6
|
Author: AndiEcker
|
|
@@ -55,17 +55,17 @@ Requires-Dist: twine; extra == "tests"
|
|
|
55
55
|
|
|
56
56
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.94 -->
|
|
57
57
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
|
|
58
|
-
# base 0.3.
|
|
58
|
+
# base 0.3.43
|
|
59
59
|
|
|
60
60
|
[](
|
|
61
61
|
https://gitlab.com/ae-group/ae_base)
|
|
62
62
|
[](
|
|
64
|
+
https://gitlab.com/ae-group/ae_base/-/tree/release0.3.42)
|
|
65
65
|
[](
|
|
66
66
|
https://pypi.org/project/ae-base/#history)
|
|
67
67
|
|
|
68
|
-
>ae_base module 0.3.
|
|
68
|
+
>ae_base module 0.3.43.
|
|
69
69
|
|
|
70
70
|
[](
|
|
71
71
|
https://ae-group.gitlab.io/ae_base/coverage/index.html)
|
|
@@ -15,17 +15,20 @@ from typing import cast
|
|
|
15
15
|
|
|
16
16
|
# noinspection PyProtectedMember
|
|
17
17
|
from ae.base import (
|
|
18
|
-
BUILD_CONFIG_FILE, DOTENV_FILE_NAME, PY_EXT, PY_INIT, PY_MAIN, TESTS_FOLDER,
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
ASCII_TO_UNICODE, BUILD_CONFIG_FILE, DOTENV_FILE_NAME, PY_EXT, PY_INIT, PY_MAIN, TESTS_FOLDER, UNICODE_TO_ASCII,
|
|
19
|
+
UNSET,
|
|
20
|
+
URI_SEP_CHAR, app_name_guess, build_config_variable_values, camel_to_snake, deep_dict_update, dummy_function,
|
|
21
|
+
duplicates, env_str,
|
|
22
|
+
dedefuse, force_encoding, full_stack_trace, import_module, instantiate_config_parser, in_wd,
|
|
21
23
|
load_env_var_defaults, load_dotenvs, main_file_paths_parts, module_attr, module_file_path, module_name,
|
|
22
24
|
norm_line_sep, norm_name, norm_path, now_str, os_host_name, os_local_ip, _os_platform, os_user_name,
|
|
23
25
|
parse_dotenv, project_main_file, read_file, round_traditional, snake_to_camel, stack_frames, stack_var, stack_vars,
|
|
24
|
-
sys_env_dict, sys_env_text, to_ascii,
|
|
26
|
+
sys_env_dict, sys_env_text, to_ascii, defuse, utc_datetime, write_file, ErrorMsgMixin)
|
|
25
27
|
|
|
26
28
|
|
|
27
|
-
tst_uri1 = "schema://user:pwd@domain/path_root/path_sub\\path+file% Üml?ä|ït.path_ext*\"
|
|
28
|
-
tst_fna1 = "schema
|
|
29
|
+
tst_uri1 = "schema://user:pwd@domain/path_root/path_sub\\path+file% Üml?ä|ït.path_ext*\"<>|*'()[]{}#^;&=$,~" + chr(127)
|
|
30
|
+
tst_fna1 = "schema⫻user﹕pwd﹫domain⁄path𛲖root⁄path𛲖sub﹨path﹢file﹪ Üml﹖ä।ït.path𛲖ext﹡"⟨⟩।﹡‘⟮⟯⟦⟧{}﹟^﹔﹠﹦﹩﹐~␡"
|
|
31
|
+
tst_fna2 = "test control chars" + "".join(chr(_) for _ in range(1, 32))
|
|
29
32
|
|
|
30
33
|
env_var_name = 'env_var_nam1'
|
|
31
34
|
env_var_val = 'value of env var'
|
|
@@ -171,6 +174,32 @@ class TestBaseHelpers:
|
|
|
171
174
|
assert pev['setup_kwargs']['entry_points']['console_scripts'] == lst_val
|
|
172
175
|
assert pev['setup_kwargs']['entry_points']['console_scripts'][0] == str_new
|
|
173
176
|
|
|
177
|
+
def test_dedefuse_file_name(self):
|
|
178
|
+
assert dedefuse(tst_fna1) == tst_uri1
|
|
179
|
+
assert defuse(dedefuse(tst_fna1)) == tst_fna1
|
|
180
|
+
|
|
181
|
+
assert dedefuse(defuse(tst_fna2)) == tst_fna2
|
|
182
|
+
|
|
183
|
+
def test_defuse_file_name(self):
|
|
184
|
+
assert defuse(tst_uri1) == tst_fna1
|
|
185
|
+
assert dedefuse(defuse(tst_uri1)) == tst_uri1
|
|
186
|
+
|
|
187
|
+
def test_defuse_os_file_name(self):
|
|
188
|
+
try:
|
|
189
|
+
write_file(tst_fna1, "tst uri file content1")
|
|
190
|
+
assert os.path.exists(tst_fna1)
|
|
191
|
+
write_file(tst_fna2, "tst uri file content2")
|
|
192
|
+
assert os.path.exists(tst_fna2)
|
|
193
|
+
finally:
|
|
194
|
+
if os.path.exists(tst_fna1):
|
|
195
|
+
os.remove(tst_fna1)
|
|
196
|
+
if os.path.exists(tst_fna2):
|
|
197
|
+
os.remove(tst_fna2)
|
|
198
|
+
|
|
199
|
+
def test_defuse_maps(self):
|
|
200
|
+
assert URI_SEP_CHAR not in UNICODE_TO_ASCII
|
|
201
|
+
assert len(UNICODE_TO_ASCII) == len(ASCII_TO_UNICODE) # check for duplicates in the ASCII_UNICODE map
|
|
202
|
+
|
|
174
203
|
def test_dummy_function(self):
|
|
175
204
|
assert dummy_function() is None
|
|
176
205
|
assert dummy_function(999, "any_args") is None
|
|
@@ -204,10 +233,6 @@ class TestBaseHelpers:
|
|
|
204
233
|
os.environ['NON_ALPHA_NUM_CHARS_69'] = vv
|
|
205
234
|
assert env_str(ev, convert_name=True) == vv
|
|
206
235
|
|
|
207
|
-
def test_filename2uri(self):
|
|
208
|
-
assert filename2uri(tst_fna1) == tst_uri1
|
|
209
|
-
assert uri2filename(filename2uri(tst_fna1)) == tst_fna1
|
|
210
|
-
|
|
211
236
|
def test_force_encoding_bytes(self):
|
|
212
237
|
s = 'äöü'
|
|
213
238
|
|
|
@@ -708,22 +733,11 @@ class TestBaseHelpers:
|
|
|
708
733
|
assert to_ascii('ß') == 'ss'
|
|
709
734
|
assert to_ascii('€') == 'Euro'
|
|
710
735
|
|
|
711
|
-
def test_uri2filename(self):
|
|
712
|
-
assert uri2filename(tst_uri1) == tst_fna1
|
|
713
|
-
assert filename2uri(uri2filename(tst_uri1)) == tst_uri1
|
|
714
|
-
|
|
715
736
|
def test_utc_datetime(self):
|
|
716
737
|
dt1 = utc_datetime()
|
|
717
738
|
dt2 = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
|
|
718
739
|
assert dt2 - dt1 < datetime.timedelta(seconds=1)
|
|
719
740
|
|
|
720
|
-
def test_uri_file_name(self):
|
|
721
|
-
try:
|
|
722
|
-
write_file(tst_fna1, "tst uri file content")
|
|
723
|
-
finally:
|
|
724
|
-
if os.path.exists(tst_fna1):
|
|
725
|
-
os.remove(tst_fna1)
|
|
726
|
-
|
|
727
741
|
def test_write_file_as_text(self):
|
|
728
742
|
test_file = os.path.join(TESTS_FOLDER, 'tst_file_written.ext')
|
|
729
743
|
content = "any content"
|
|
@@ -746,6 +760,11 @@ class TestBaseHelpers:
|
|
|
746
760
|
assert os.path.exists(test_file)
|
|
747
761
|
assert os.path.isfile(test_file)
|
|
748
762
|
assert read_file(test_file, extra_mode="b") == content
|
|
763
|
+
|
|
764
|
+
write_file(test_file, content) # 'b' in extra_mode arg is optional because content is bytes array
|
|
765
|
+
assert os.path.exists(test_file)
|
|
766
|
+
assert os.path.isfile(test_file)
|
|
767
|
+
assert read_file(test_file, extra_mode="b") == content
|
|
749
768
|
finally:
|
|
750
769
|
if os.path.exists(test_file):
|
|
751
770
|
os.remove(test_file)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|