ae-base 0.3.38__tar.gz → 0.3.40__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.38/ae_base.egg-info → ae_base-0.3.40}/PKG-INFO +5 -5
- {ae_base-0.3.38 → ae_base-0.3.40}/README.md +4 -4
- {ae_base-0.3.38 → ae_base-0.3.40}/ae/base.py +88 -8
- {ae_base-0.3.38 → ae_base-0.3.40/ae_base.egg-info}/PKG-INFO +5 -5
- {ae_base-0.3.38 → ae_base-0.3.40}/tests/test_base.py +40 -1
- {ae_base-0.3.38 → ae_base-0.3.40}/LICENSE.md +0 -0
- {ae_base-0.3.38 → ae_base-0.3.40}/ae_base.egg-info/SOURCES.txt +0 -0
- {ae_base-0.3.38 → ae_base-0.3.40}/ae_base.egg-info/dependency_links.txt +0 -0
- {ae_base-0.3.38 → ae_base-0.3.40}/ae_base.egg-info/requires.txt +0 -0
- {ae_base-0.3.38 → ae_base-0.3.40}/ae_base.egg-info/top_level.txt +0 -0
- {ae_base-0.3.38 → ae_base-0.3.40}/ae_base.egg-info/zip-safe +0 -0
- {ae_base-0.3.38 → ae_base-0.3.40}/setup.cfg +0 -0
- {ae_base-0.3.38 → ae_base-0.3.40}/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.40
|
|
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
|
|
@@ -53,17 +53,17 @@ Requires-Dist: twine; extra == "tests"
|
|
|
53
53
|
|
|
54
54
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.94 -->
|
|
55
55
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
|
|
56
|
-
# base 0.3.
|
|
56
|
+
# base 0.3.40
|
|
57
57
|
|
|
58
58
|
[](
|
|
59
59
|
https://gitlab.com/ae-group/ae_base)
|
|
60
60
|
[](
|
|
62
|
+
https://gitlab.com/ae-group/ae_base/-/tree/release0.3.39)
|
|
63
63
|
[](
|
|
64
64
|
https://pypi.org/project/ae-base/#history)
|
|
65
65
|
|
|
66
|
-
>ae_base module 0.3.
|
|
66
|
+
>ae_base module 0.3.40.
|
|
67
67
|
|
|
68
68
|
[](
|
|
69
69
|
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.40
|
|
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.39)
|
|
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.40.
|
|
14
14
|
|
|
15
15
|
[](
|
|
16
16
|
https://ae-group.gitlab.io/ae_base/coverage/index.html)
|
|
@@ -3,7 +3,7 @@ basic constants, helper functions and context manager
|
|
|
3
3
|
=====================================================
|
|
4
4
|
|
|
5
5
|
this module is pure python, has no external dependencies, and is providing base constants, common helper
|
|
6
|
-
functions and context managers.
|
|
6
|
+
functions, useful classes and context managers.
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
base constants
|
|
@@ -24,6 +24,9 @@ the constants :data:`PACKAGE_NAME`, :data:`PACKAGE_DOMAIN` and :data:`PERMISSION
|
|
|
24
24
|
on mobile devices. to avoid redundancies, these values get loaded from the
|
|
25
25
|
:data:`build config file <BUILD_CONFIG_FILE>` - if it exists in the current working directory.
|
|
26
26
|
|
|
27
|
+
with the help of the format string constant :data:`NOW_STR_FORMAT` and the function :func:`now_str` you can create a
|
|
28
|
+
sortable and compact string from a timestamp.
|
|
29
|
+
|
|
27
30
|
|
|
28
31
|
base helper functions
|
|
29
32
|
---------------------
|
|
@@ -42,7 +45,7 @@ absolute, call the function :func:`norm_path`.
|
|
|
42
45
|
|
|
43
46
|
:func:`camel_to_snake` and :func:`snake_to_camel` providing name conversions of class and method names.
|
|
44
47
|
|
|
45
|
-
to encode
|
|
48
|
+
to encode Unicode strings to other codecs the functions :func:`force_encoding` and :func:`to_ascii` can be used.
|
|
46
49
|
|
|
47
50
|
the :func:`round_traditional` function get provided by this module for traditional rounding of float values. the
|
|
48
51
|
function signature is fully compatible to Python's :func:`round` function.
|
|
@@ -99,6 +102,15 @@ code and build Kivy apps for the Android OS, and to `Gabriel Pettier <https://gi
|
|
|
99
102
|
osc example.
|
|
100
103
|
|
|
101
104
|
|
|
105
|
+
types, classes and mixins
|
|
106
|
+
-------------------------
|
|
107
|
+
|
|
108
|
+
the :class:`UnsetType` class can be used e.g. for the declaration of optional function and method parameters,
|
|
109
|
+
allowing also `None` is an accepted argument value.
|
|
110
|
+
|
|
111
|
+
to extend any class with an intelligent error message property, add the mixin :class:`ErrorMsgMixin` to it.
|
|
112
|
+
|
|
113
|
+
|
|
102
114
|
generic context manager
|
|
103
115
|
-----------------------
|
|
104
116
|
|
|
@@ -137,6 +149,7 @@ import shutil
|
|
|
137
149
|
import socket
|
|
138
150
|
import sys
|
|
139
151
|
import unicodedata
|
|
152
|
+
import urllib.parse
|
|
140
153
|
import warnings
|
|
141
154
|
|
|
142
155
|
from configparser import ConfigParser, ExtendedInterpolation
|
|
@@ -146,7 +159,8 @@ from inspect import getinnerframes, getouterframes, getsourcefile
|
|
|
146
159
|
from types import ModuleType
|
|
147
160
|
from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Tuple, Union, cast
|
|
148
161
|
|
|
149
|
-
|
|
162
|
+
|
|
163
|
+
__version__ = '0.3.40'
|
|
150
164
|
|
|
151
165
|
|
|
152
166
|
DOCS_FOLDER = 'docs' #: project documentation root folder name
|
|
@@ -200,7 +214,9 @@ _env_variable = re.compile(r"""
|
|
|
200
214
|
""", re.IGNORECASE | re.VERBOSE)
|
|
201
215
|
|
|
202
216
|
|
|
203
|
-
NAME_PARTS_SEP = '_'
|
|
217
|
+
NAME_PARTS_SEP = '_' #: name parts separator character, e.g. for :func:`norm_name`
|
|
218
|
+
|
|
219
|
+
NOW_STR_FORMAT = "{sep}%Y%m%d{sep}%H%M%S{sep}%f" #: timestamp format of :func:`now_str`
|
|
204
220
|
|
|
205
221
|
SKIPPED_MODULES = ('ae.base', 'ae.paths', 'ae.dynamicod', 'ae.core', 'ae.console', 'ae.gui_app', 'ae.gui_help',
|
|
206
222
|
'ae.kivy', 'ae.kivy.apps', 'ae.kivy.behaviors', 'ae.kivy.i18n', 'ae.kivy.tours', 'ae.kivy.widgets',
|
|
@@ -610,9 +626,12 @@ def now_str(sep: str = "") -> str:
|
|
|
610
626
|
|
|
611
627
|
:param sep: optional prefix and separator character (separating date from time and in time part
|
|
612
628
|
the seconds from the microseconds).
|
|
613
|
-
:return: UTC timestamp as string (length=20 + 3 * len(sep)).
|
|
629
|
+
:return: naive UTC timestamp (without timezone info) as string (length=20 + 3 * len(sep)).
|
|
614
630
|
"""
|
|
615
|
-
|
|
631
|
+
# if sys.version_info >= (3, 12):
|
|
632
|
+
return datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None).strftime(NOW_STR_FORMAT.format(sep=sep))
|
|
633
|
+
# else:
|
|
634
|
+
# return datetime.datetime.utcnow().strftime(NOW_STR_FORMAT.format(sep=sep))
|
|
616
635
|
|
|
617
636
|
|
|
618
637
|
def os_host_name() -> str:
|
|
@@ -921,7 +940,7 @@ def sys_env_text(ind_ch: str = " ", ind_len: int = 12, key_ch: str = "=", key_le
|
|
|
921
940
|
extra_sys_env_dict: Optional[Dict[str, str]] = None) -> str:
|
|
922
941
|
""" compile formatted text block with system environment info.
|
|
923
942
|
|
|
924
|
-
:param ind_ch: indent character (
|
|
943
|
+
:param ind_ch: indent character (defaults to " ").
|
|
925
944
|
:param ind_len: indent depths (default=12 characters).
|
|
926
945
|
:param key_ch: key-value separator character (default="=").
|
|
927
946
|
:param key_len: key-name minimum length (default=15 characters).
|
|
@@ -940,7 +959,7 @@ def sys_env_text(ind_ch: str = " ", ind_len: int = 12, key_ch: str = "=", key_le
|
|
|
940
959
|
|
|
941
960
|
|
|
942
961
|
def to_ascii(unicode_str: str) -> str:
|
|
943
|
-
""" converts
|
|
962
|
+
""" converts Unicode string into ascii representation.
|
|
944
963
|
|
|
945
964
|
useful for fuzzy string compare; inspired by MiniQuark's answer
|
|
946
965
|
in: https://stackoverflow.com/questions/517923/what-is-the-best-way-to-remove-accents-in-a-python-unicode-string
|
|
@@ -952,6 +971,36 @@ def to_ascii(unicode_str: str) -> str:
|
|
|
952
971
|
return "".join([c for c in nfkd_form if not unicodedata.combining(c)]).replace('ß', "ss").replace('€', "Euro")
|
|
953
972
|
|
|
954
973
|
|
|
974
|
+
def uri2filename(uri: str) -> str:
|
|
975
|
+
""" convert a URI to be usable as name of a file or folder
|
|
976
|
+
|
|
977
|
+
:param uri: URI to convert to a corresponding file name, that will be revertible back to this URI.
|
|
978
|
+
:return: name of a file/folder representing the specified URI.
|
|
979
|
+
|
|
980
|
+
in *nix only / and \0 are not allowed characters in file names.
|
|
981
|
+
in MS Windows are not allowed: ASCII 0...31): / \\ : * ? ” < > | (). some blogs recommend to also not allow
|
|
982
|
+
(convert) the characters # and '.
|
|
983
|
+
only old POSIX seems to be even more restricted (only allowing alphanumeric characters plus . - and _).
|
|
984
|
+
|
|
985
|
+
file name length is not restricted/shortened by this function, although the maximum is 255 characters on most OSs.
|
|
986
|
+
|
|
987
|
+
more on allowed characters in file names in the answers of RedGrittyBrick on https://superuser.com/questions/358855
|
|
988
|
+
and of Christopher Oezbek on https://stackoverflow.com/questions/1976007.
|
|
989
|
+
"""
|
|
990
|
+
# using urllib.parse.quote(uri, safe="") instead would convert also any non-ascii (e.g. umlaut) characters into hex
|
|
991
|
+
# added [] to str.join() argument because List comprehensions are faster than generator expressions
|
|
992
|
+
return "".join([f"%{hex(ord(_))[2:].upper()}" if _ in '/|\\:*?"<>%' else _ for _ in uri])
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
def filename2uri(file_name: str) -> str:
|
|
996
|
+
""" convert a file name converted by :func:`uri2filename` back to its representation as a URI
|
|
997
|
+
|
|
998
|
+
:param file_name: name of the file/folder to convert back to its URI representation.
|
|
999
|
+
:return: URI string.
|
|
1000
|
+
"""
|
|
1001
|
+
return urllib.parse.unquote(file_name)
|
|
1002
|
+
|
|
1003
|
+
|
|
955
1004
|
def write_file(file_path: str, content: Union[str, bytes], extra_mode: str = "", encoding: Optional[str] = None):
|
|
956
1005
|
""" (over)write the file specified by :paramref:`~write_file.file_path` with text or binary/bytes content.
|
|
957
1006
|
|
|
@@ -966,11 +1015,42 @@ def write_file(file_path: str, content: Union[str, bytes], extra_mode: str = "",
|
|
|
966
1015
|
:raises OSError: if :paramref:`~read_file.file_path` is misspelled or contains invalid characters.
|
|
967
1016
|
:raises PermissionError: if current OS user account lacks permissions to read the file content.
|
|
968
1017
|
:raises ValueError: on decoding errors.
|
|
1018
|
+
|
|
1019
|
+
to extend this function for Android 14+ see https://github.com/beeware/toga/pull/1158#issuecomment-2254564657
|
|
1020
|
+
and https://gist.github.com/neonankiti/05922cf0a44108a2e2732671ed9ef386
|
|
1021
|
+
Yes, to use ACTION_CREATE_DOCUMENT, you don't supply a URI in the intent. You wait for the intent result, and that
|
|
1022
|
+
will contain a URI which you can write to.
|
|
1023
|
+
See #1158 (comment - https://github.com/beeware/toga/pull/1158#issuecomment-2254564657) for a link to a Java
|
|
1024
|
+
example, and #1158 (comment - https://github.com/beeware/toga/pull/1158#issuecomment-1446196973) for how to wait
|
|
1025
|
+
for an intent result.
|
|
1026
|
+
Related german docs: https://developer.android.com/training/data-storage/shared/media?hl=de
|
|
969
1027
|
"""
|
|
970
1028
|
with open(file_path, ('' if extra_mode.startswith('a') else 'w') + extra_mode, encoding=encoding) as file_handle:
|
|
971
1029
|
file_handle.write(content)
|
|
972
1030
|
|
|
973
1031
|
|
|
1032
|
+
class ErrorMsgMixin:
|
|
1033
|
+
""" mixin class providing error message """
|
|
1034
|
+
_err_msg: str = ""
|
|
1035
|
+
|
|
1036
|
+
@property
|
|
1037
|
+
def error_message(self) -> str:
|
|
1038
|
+
""" error message string if an error occurred or an empty string if not.
|
|
1039
|
+
|
|
1040
|
+
:getter: return the accumulated error message of the recently occurred error(s).
|
|
1041
|
+
:setter: any assigned error message will be accumulated to recent error messages.
|
|
1042
|
+
pass an empty string to reset the error message.
|
|
1043
|
+
"""
|
|
1044
|
+
return self._err_msg
|
|
1045
|
+
|
|
1046
|
+
@error_message.setter
|
|
1047
|
+
def error_message(self, next_err_msg: str):
|
|
1048
|
+
if next_err_msg:
|
|
1049
|
+
self._err_msg += ("\n\n" if self._err_msg else "") + next_err_msg
|
|
1050
|
+
else:
|
|
1051
|
+
self._err_msg = ""
|
|
1052
|
+
|
|
1053
|
+
|
|
974
1054
|
PACKAGE_NAME = stack_var('__name__') or 'unspecified_package'
|
|
975
1055
|
PACKAGE_DOMAIN = 'org.test'
|
|
976
1056
|
PERMISSIONS = "INTERNET, VIBRATE, READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ae_base
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.40
|
|
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
|
|
@@ -53,17 +53,17 @@ Requires-Dist: twine; extra == "tests"
|
|
|
53
53
|
|
|
54
54
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.94 -->
|
|
55
55
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
|
|
56
|
-
# base 0.3.
|
|
56
|
+
# base 0.3.40
|
|
57
57
|
|
|
58
58
|
[](
|
|
59
59
|
https://gitlab.com/ae-group/ae_base)
|
|
60
60
|
[](
|
|
62
|
+
https://gitlab.com/ae-group/ae_base/-/tree/release0.3.39)
|
|
63
63
|
[](
|
|
64
64
|
https://pypi.org/project/ae-base/#history)
|
|
65
65
|
|
|
66
|
-
>ae_base module 0.3.
|
|
66
|
+
>ae_base module 0.3.40.
|
|
67
67
|
|
|
68
68
|
[](
|
|
69
69
|
https://ae-group.gitlab.io/ae_base/coverage/index.html)
|
|
@@ -20,9 +20,12 @@ from ae.base import (
|
|
|
20
20
|
load_env_var_defaults, load_dotenvs, main_file_paths_parts, module_attr, module_file_path, module_name,
|
|
21
21
|
norm_line_sep, norm_name, norm_path, now_str, os_host_name, os_local_ip, _os_platform, os_user_name,
|
|
22
22
|
parse_dotenv, project_main_file, read_file, round_traditional, snake_to_camel, stack_frames, stack_var, stack_vars,
|
|
23
|
-
sys_env_dict, sys_env_text, to_ascii, write_file)
|
|
23
|
+
sys_env_dict, sys_env_text, to_ascii, filename2uri, uri2filename, write_file, ErrorMsgMixin)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
tst_uri1 = "schema://user:pwd@domain/path_root/path_sub\\path+file% Üml?ä|ït.path_ext*\"<>"
|
|
27
|
+
tst_fna1 = "schema%3A%2F%2Fuser%3Apwd@domain%2Fpath_root%2Fpath_sub%5Cpath+file%25 Üml%3Fä%7Cït.path_ext%2A%22%3C%3E"
|
|
28
|
+
|
|
26
29
|
env_var_name = 'env_var_nam1'
|
|
27
30
|
env_var_val = 'value of env var'
|
|
28
31
|
folder_name = 'fdr'
|
|
@@ -60,6 +63,27 @@ def test_unset_null_length():
|
|
|
60
63
|
assert len(UNSET) == 0
|
|
61
64
|
|
|
62
65
|
|
|
66
|
+
class TestErrorMsgMixin:
|
|
67
|
+
def test_instantiation(self):
|
|
68
|
+
assert ErrorMsgMixin()
|
|
69
|
+
|
|
70
|
+
def test_error_message_property(self):
|
|
71
|
+
ins = ErrorMsgMixin()
|
|
72
|
+
assert ins.error_message == ""
|
|
73
|
+
|
|
74
|
+
err_msg = "set new error message"
|
|
75
|
+
ins.error_message = err_msg
|
|
76
|
+
assert ins.error_message == err_msg
|
|
77
|
+
|
|
78
|
+
err_msg2 = "added error message"
|
|
79
|
+
ins.error_message = err_msg2
|
|
80
|
+
assert err_msg in ins.error_message
|
|
81
|
+
assert err_msg2 in ins.error_message
|
|
82
|
+
|
|
83
|
+
ins.error_message = ""
|
|
84
|
+
assert ins.error_message == ""
|
|
85
|
+
|
|
86
|
+
|
|
63
87
|
class TestBaseHelpers:
|
|
64
88
|
def test_app_name_guess(self):
|
|
65
89
|
assert app_name_guess() # app.exe name in pytest returning '_jb_pytest_runner'(PyCharm)/'__main__'(console)
|
|
@@ -678,6 +702,21 @@ class TestBaseHelpers:
|
|
|
678
702
|
assert to_ascii('ß') == 'ss'
|
|
679
703
|
assert to_ascii('€') == 'Euro'
|
|
680
704
|
|
|
705
|
+
def test_filename2uri(self):
|
|
706
|
+
assert filename2uri(tst_fna1) == tst_uri1
|
|
707
|
+
assert uri2filename(filename2uri(tst_fna1)) == tst_fna1
|
|
708
|
+
|
|
709
|
+
def test_uri2filename(self):
|
|
710
|
+
assert uri2filename(tst_uri1) == tst_fna1
|
|
711
|
+
assert filename2uri(uri2filename(tst_uri1)) == tst_uri1
|
|
712
|
+
|
|
713
|
+
def test_uri_file_name(self):
|
|
714
|
+
try:
|
|
715
|
+
write_file(tst_fna1, "tst uri file content")
|
|
716
|
+
finally:
|
|
717
|
+
if os.path.exists(tst_fna1):
|
|
718
|
+
os.remove(tst_fna1)
|
|
719
|
+
|
|
681
720
|
def test_write_file_as_text(self):
|
|
682
721
|
test_file = os.path.join(TESTS_FOLDER, 'tst_file_written.ext')
|
|
683
722
|
content = "any content"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|