ae-base 0.3.70__tar.gz → 0.3.72__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.70/ae_base.egg-info → ae_base-0.3.72}/PKG-INFO +4 -4
- {ae_base-0.3.70 → ae_base-0.3.72}/README.md +3 -3
- {ae_base-0.3.70 → ae_base-0.3.72}/ae/base.py +22 -10
- {ae_base-0.3.70 → ae_base-0.3.72/ae_base.egg-info}/PKG-INFO +4 -4
- {ae_base-0.3.70 → ae_base-0.3.72}/setup.py +4 -4
- {ae_base-0.3.70 → ae_base-0.3.72}/tests/test_base.py +63 -31
- {ae_base-0.3.70 → ae_base-0.3.72}/LICENSE.md +0 -0
- {ae_base-0.3.70 → ae_base-0.3.72}/ae_base.egg-info/SOURCES.txt +0 -0
- {ae_base-0.3.70 → ae_base-0.3.72}/ae_base.egg-info/dependency_links.txt +0 -0
- {ae_base-0.3.70 → ae_base-0.3.72}/ae_base.egg-info/requires.txt +0 -0
- {ae_base-0.3.70 → ae_base-0.3.72}/ae_base.egg-info/top_level.txt +0 -0
- {ae_base-0.3.70 → ae_base-0.3.72}/ae_base.egg-info/zip-safe +0 -0
- {ae_base-0.3.70 → ae_base-0.3.72}/pyproject.toml +0 -0
- {ae_base-0.3.70 → ae_base-0.3.72}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ae_base
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.72
|
|
4
4
|
Summary: ae namespace module portion base: basic constants, helper functions and context managers
|
|
5
5
|
Home-page: https://gitlab.com/ae-group/ae_base
|
|
6
6
|
Author: AndiEcker
|
|
@@ -65,13 +65,13 @@ Dynamic: summary
|
|
|
65
65
|
|
|
66
66
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.97 -->
|
|
67
67
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.namespace_root_tpls v0.3.19 -->
|
|
68
|
-
# base 0.3.
|
|
68
|
+
# base 0.3.72
|
|
69
69
|
|
|
70
70
|
[](
|
|
71
71
|
https://gitlab.com/ae-group/ae_base)
|
|
72
72
|
[](
|
|
74
|
+
https://gitlab.com/ae-group/ae_base/-/tree/release0.3.72)
|
|
75
75
|
[](
|
|
76
76
|
https://pypi.org/project/ae-base/#history)
|
|
77
77
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.97 -->
|
|
2
2
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.namespace_root_tpls v0.3.19 -->
|
|
3
|
-
# base 0.3.
|
|
3
|
+
# base 0.3.72
|
|
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.72)
|
|
10
10
|
[](
|
|
11
11
|
https://pypi.org/project/ae-base/#history)
|
|
12
12
|
|
|
@@ -246,7 +246,7 @@ from types import ModuleType
|
|
|
246
246
|
from typing import Any, Callable, Generator, Iterable, MutableMapping, Optional, Union, cast
|
|
247
247
|
|
|
248
248
|
|
|
249
|
-
__version__ = '0.3.
|
|
249
|
+
__version__ = '0.3.72'
|
|
250
250
|
|
|
251
251
|
|
|
252
252
|
os_path_abspath = os.path.abspath
|
|
@@ -1048,8 +1048,17 @@ def parse_dotenv(file_path: str) -> dict[str, str]:
|
|
|
1048
1048
|
:param file_path: string with the name/path of an existing ``.env``/:data:`DOTENV_FILE_NAME` file.
|
|
1049
1049
|
:return: dict with environment variable names and values
|
|
1050
1050
|
"""
|
|
1051
|
-
|
|
1051
|
+
lines = [] # unwrap multi-line .env variable values with backslash at line end (Docker/UNIX-style format)
|
|
1052
|
+
prev_lines = ""
|
|
1052
1053
|
for line in cast(str, read_file(file_path)).splitlines():
|
|
1054
|
+
if line.endswith('\\'):
|
|
1055
|
+
prev_lines += line[:-1]
|
|
1056
|
+
continue
|
|
1057
|
+
lines.append(prev_lines + line)
|
|
1058
|
+
prev_lines = ""
|
|
1059
|
+
|
|
1060
|
+
env_vars: dict[str, str] = {}
|
|
1061
|
+
for line in lines:
|
|
1053
1062
|
match = _env_line.search(line)
|
|
1054
1063
|
if not match:
|
|
1055
1064
|
if not re.search(r'^\s*(?:#.*)?$', line): # not comment or blank
|
|
@@ -1362,17 +1371,20 @@ def url_failure(url: str, token: str = "", username: str = "", password: str = "
|
|
|
1362
1371
|
return f"{exception.code} {mask_url(url)} raised HTTPError {exception.reason=}"
|
|
1363
1372
|
|
|
1364
1373
|
except URLError as exception:
|
|
1365
|
-
|
|
1374
|
+
err_msg = f" {mask_url(url)} raised {exception.errno=} {exception.reason=};"
|
|
1366
1375
|
if isinstance(exception.reason, socket.gaierror):
|
|
1367
|
-
return f"{
|
|
1368
|
-
if isinstance(exception.reason, socket.timeout):
|
|
1369
|
-
return f"{err_prefix} connection timed out after {timeout} seconds"
|
|
1376
|
+
return '995' + f"{err_msg} could not resolve hostname"
|
|
1370
1377
|
if isinstance(exception.reason, ssl.SSLCertVerificationError):
|
|
1371
|
-
return f"{
|
|
1372
|
-
|
|
1378
|
+
return '996' + f"{err_msg} SSL certificate verification failed"
|
|
1379
|
+
if isinstance(exception.reason, socket.timeout):
|
|
1380
|
+
return '997' + f"{err_msg} connection timed out after {timeout} seconds"
|
|
1381
|
+
return '998' + f"{err_msg} could not reach the server"
|
|
1382
|
+
|
|
1383
|
+
except socket.timeout as _exception: # noqa: F841 # str(_exception) could contain password|token
|
|
1384
|
+
return '997' + f" {mask_url(url)} raised socket-timeout exception after {timeout} seconds"
|
|
1373
1385
|
|
|
1374
|
-
except Exception:
|
|
1375
|
-
return f"
|
|
1386
|
+
except Exception as _exception: # noqa: F841 # pylint: disable=broad-exception-caught
|
|
1387
|
+
return '999' + f" {mask_url(url)} raised unexpected exception" # str(_exception) COULD contain password
|
|
1376
1388
|
|
|
1377
1389
|
|
|
1378
1390
|
def utc_datetime() -> datetime.datetime:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ae_base
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.72
|
|
4
4
|
Summary: ae namespace module portion base: basic constants, helper functions and context managers
|
|
5
5
|
Home-page: https://gitlab.com/ae-group/ae_base
|
|
6
6
|
Author: AndiEcker
|
|
@@ -65,13 +65,13 @@ Dynamic: summary
|
|
|
65
65
|
|
|
66
66
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.97 -->
|
|
67
67
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.namespace_root_tpls v0.3.19 -->
|
|
68
|
-
# base 0.3.
|
|
68
|
+
# base 0.3.72
|
|
69
69
|
|
|
70
70
|
[](
|
|
71
71
|
https://gitlab.com/ae-group/ae_base)
|
|
72
72
|
[](
|
|
74
|
+
https://gitlab.com/ae-group/ae_base/-/tree/release0.3.72)
|
|
75
75
|
[](
|
|
76
76
|
https://pypi.org/project/ae-base/#history)
|
|
77
77
|
|
|
@@ -25,13 +25,13 @@ setup_kwargs = {
|
|
|
25
25
|
'license': 'GPL-3.0-or-later',
|
|
26
26
|
'long_description': ('<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.97 -->\n'
|
|
27
27
|
'<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.namespace_root_tpls v0.3.19 -->\n'
|
|
28
|
-
'# base 0.3.
|
|
28
|
+
'# base 0.3.72\n'
|
|
29
29
|
'\n'
|
|
30
30
|
'[](\n'
|
|
31
31
|
' https://gitlab.com/ae-group/ae_base)\n'
|
|
32
32
|
'[](\n'
|
|
34
|
+
' https://gitlab.com/ae-group/ae_base/-/tree/release0.3.72)\n'
|
|
35
35
|
'[](\n'
|
|
36
36
|
' https://pypi.org/project/ae-base/#history)\n'
|
|
37
37
|
'\n'
|
|
@@ -108,7 +108,7 @@ setup_kwargs = {
|
|
|
108
108
|
'Source': 'https://ae.readthedocs.io/en/latest/_modules/ae/base.html'},
|
|
109
109
|
'python_requires': '>=3.9',
|
|
110
110
|
'url': 'https://gitlab.com/ae-group/ae_base',
|
|
111
|
-
'version': '0.3.
|
|
111
|
+
'version': '0.3.72',
|
|
112
112
|
'zip_safe': True,
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
""" ae.base unit tests """
|
|
2
2
|
import datetime
|
|
3
3
|
import os
|
|
4
|
-
import time
|
|
5
|
-
|
|
6
|
-
import pytest
|
|
7
4
|
import shutil
|
|
8
5
|
import socket
|
|
9
6
|
import ssl
|
|
@@ -11,16 +8,20 @@ import string
|
|
|
11
8
|
import sys
|
|
12
9
|
import tempfile
|
|
13
10
|
import textwrap
|
|
11
|
+
import time
|
|
12
|
+
import timeit
|
|
14
13
|
|
|
15
14
|
from collections import OrderedDict
|
|
16
15
|
from configparser import ConfigParser
|
|
17
16
|
# noinspection PyProtectedMember
|
|
18
17
|
from http.client import HTTPMessage
|
|
19
18
|
from types import ModuleType
|
|
20
|
-
from typing import cast, Any
|
|
19
|
+
from typing import cast, Any, Optional
|
|
21
20
|
from unittest.mock import patch
|
|
22
21
|
from urllib.error import HTTPError, URLError
|
|
23
22
|
|
|
23
|
+
import pytest
|
|
24
|
+
|
|
24
25
|
# noinspection PyProtectedMember
|
|
25
26
|
from ae.base import (
|
|
26
27
|
ASCII_TO_UNICODE, ASCII_UNICODE, BUILD_CONFIG_FILE, DOTENV_FILE_NAME, PY_EXT, PY_INIT, PY_MAIN, TESTS_FOLDER,
|
|
@@ -69,15 +70,32 @@ def os_env_test_env():
|
|
|
69
70
|
module_test_var = 'module_test_var_val' # used for stack_var()/try_exec() tests
|
|
70
71
|
|
|
71
72
|
|
|
72
|
-
def
|
|
73
|
+
def test_unset_truthiness_and_null_length():
|
|
73
74
|
assert not UNSET
|
|
74
75
|
assert bool(UNSET) is False
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
def test_unset_null_length():
|
|
78
77
|
assert len(UNSET) == 0
|
|
79
78
|
|
|
80
79
|
|
|
80
|
+
def test_proof_os_path_shortcuts_performance_win():
|
|
81
|
+
att_call_setup = textwrap.dedent("""
|
|
82
|
+
import os
|
|
83
|
+
path1, path2, path3 = "folder1", "folder2", "file.tst"
|
|
84
|
+
""")
|
|
85
|
+
sho_call_setup = att_call_setup + textwrap.dedent("""
|
|
86
|
+
os_path_join = os.path.join
|
|
87
|
+
""")
|
|
88
|
+
|
|
89
|
+
att_call_code = "os.path.join(path1, path2, path3)"
|
|
90
|
+
sho_call_code = "os_path_join(path1, path2, path3)"
|
|
91
|
+
|
|
92
|
+
time_att = timeit.timeit(att_call_code, setup=att_call_setup, number=3_000_000)
|
|
93
|
+
time_sho = timeit.timeit(sho_call_code, setup=sho_call_setup, number=3_000_000)
|
|
94
|
+
|
|
95
|
+
assert time_sho < time_att
|
|
96
|
+
print(f"\n¡!¡!¡! os_path_* shortcuts are ~{((time_att - time_sho) / time_att) * 100:.2f}% faster")
|
|
97
|
+
|
|
98
|
+
|
|
81
99
|
class TestErrorMsgMixin:
|
|
82
100
|
def test_instantiation(self):
|
|
83
101
|
ins = ErrorMsgMixin()
|
|
@@ -828,6 +846,24 @@ class TestBaseHelpers:
|
|
|
828
846
|
loaded = parse_dotenv(fp.name)
|
|
829
847
|
assert 'var_nam' not in loaded # added warning
|
|
830
848
|
|
|
849
|
+
def test_parse_dotenv_literal_dict_with_list(self):
|
|
850
|
+
with tempfile.NamedTemporaryFile(mode="w") as fp:
|
|
851
|
+
var_val = "{'key': {'sub-key': ['list-item', 'list-item with = char', ]}}"
|
|
852
|
+
fp.write("var_nam=" + var_val)
|
|
853
|
+
fp.seek(0)
|
|
854
|
+
loaded = parse_dotenv(fp.name)
|
|
855
|
+
assert 'var_nam' in loaded
|
|
856
|
+
assert loaded['var_nam'] == var_val
|
|
857
|
+
|
|
858
|
+
def test_parse_dotenv_multi_line_var_value(self):
|
|
859
|
+
with tempfile.NamedTemporaryFile(mode="w") as fp:
|
|
860
|
+
var_val = "{'key': {'sub-key':\\\n ['list-item',\\\n 'list-item with = char', ]}}"
|
|
861
|
+
fp.write("var_nam=" + var_val)
|
|
862
|
+
fp.seek(0)
|
|
863
|
+
loaded = parse_dotenv(fp.name)
|
|
864
|
+
assert 'var_nam' in loaded
|
|
865
|
+
assert loaded['var_nam'] == var_val.replace('\\\n', "")
|
|
866
|
+
|
|
831
867
|
def test_parse_dotenv_single_in_double_quoted_value(self):
|
|
832
868
|
with tempfile.NamedTemporaryFile(mode="w") as fp:
|
|
833
869
|
fp.write('''var_nam="'var val'"''')
|
|
@@ -844,15 +880,6 @@ class TestBaseHelpers:
|
|
|
844
880
|
assert 'var_nam' in loaded
|
|
845
881
|
assert loaded['var_nam'] == "var val"
|
|
846
882
|
|
|
847
|
-
def test_parse_dotenv_literal_dict_with_list(self):
|
|
848
|
-
with tempfile.NamedTemporaryFile(mode="w") as fp:
|
|
849
|
-
var_val = "{'key': {'sub-key': ['list-item', 'list-item with = char', ]}}"
|
|
850
|
-
fp.write("var_nam=" + var_val)
|
|
851
|
-
fp.seek(0)
|
|
852
|
-
loaded = parse_dotenv(fp.name)
|
|
853
|
-
assert 'var_nam' in loaded
|
|
854
|
-
assert loaded['var_nam'] == var_val
|
|
855
|
-
|
|
856
883
|
def test_parse_dotenv_literal_dict_with_list_quoted(self):
|
|
857
884
|
with tempfile.NamedTemporaryFile(mode="w") as fp:
|
|
858
885
|
var_val = "{'key': {'sub-key': ['list-item', 'list-item with = char']}}"
|
|
@@ -1069,6 +1096,17 @@ class TestBaseHelpers:
|
|
|
1069
1096
|
assert to_ascii('ß') == 'ss'
|
|
1070
1097
|
assert to_ascii('€') == 'Euro'
|
|
1071
1098
|
|
|
1099
|
+
@staticmethod
|
|
1100
|
+
def url_failure_httpbin_503_retryer(url: str, timeout: Optional[float] = None) -> tuple[str, str]:
|
|
1101
|
+
retries = 9
|
|
1102
|
+
while True:
|
|
1103
|
+
err_msg = url_failure(url, timeout=timeout)
|
|
1104
|
+
if not err_msg or int(err_msg[:3]) != 503 or retries == 0:
|
|
1105
|
+
break
|
|
1106
|
+
time.sleep(3)
|
|
1107
|
+
retries -= 1
|
|
1108
|
+
return err_msg, f"url_failure({url=}, {timeout=}) httpbin is sometimes unavailable/503. retry later; {err_msg=}"
|
|
1109
|
+
|
|
1072
1110
|
def test_url_failure(self):
|
|
1073
1111
|
assert not url_failure("https://gitlab.com/ae-group/ae_base")
|
|
1074
1112
|
|
|
@@ -1078,14 +1116,8 @@ class TestBaseHelpers:
|
|
|
1078
1116
|
|
|
1079
1117
|
assert not url_failure("https://www.google.com")
|
|
1080
1118
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
err_msg = url_failure(f"https://httpbin.org/status/200")
|
|
1084
|
-
if not err_msg or retries == 0:
|
|
1085
|
-
break
|
|
1086
|
-
time.sleep(3)
|
|
1087
|
-
retries -= 1
|
|
1088
|
-
assert not err_msg, f"httpbin is sometimes unavailable with error 503 - retry later; {err_msg=}"
|
|
1119
|
+
ret, message = self.url_failure_httpbin_503_retryer("https://httpbin.org/status/200")
|
|
1120
|
+
assert not ret, message
|
|
1089
1121
|
|
|
1090
1122
|
def test_url_failure_authentication_errors(self):
|
|
1091
1123
|
password, domain, path = "toBeMaskedPassword", "any-not_existing-host_domain.zzz", "any/not/existing/url/path"
|
|
@@ -1215,19 +1247,19 @@ class TestBaseHelpers:
|
|
|
1215
1247
|
assert int(ret[:3]) > 0
|
|
1216
1248
|
|
|
1217
1249
|
def test_url_failure_timeout_errors(self):
|
|
1218
|
-
ret =
|
|
1250
|
+
ret, message = self.url_failure_httpbin_503_retryer("https://httpbin.org/delay/3", timeout=0.9)
|
|
1219
1251
|
|
|
1220
|
-
assert ret
|
|
1221
|
-
assert
|
|
1252
|
+
assert ret, message
|
|
1253
|
+
assert ret[:3] == '997', message
|
|
1222
1254
|
|
|
1223
1255
|
def test_url_failure_url_errors(self):
|
|
1224
1256
|
assert url_failure("")
|
|
1225
1257
|
|
|
1226
|
-
ret =
|
|
1258
|
+
ret, message = self.url_failure_httpbin_503_retryer(url2 := "https://httpbin.org/status/504")
|
|
1227
1259
|
|
|
1228
|
-
assert ret
|
|
1229
|
-
assert
|
|
1230
|
-
assert ret[4:].startswith(mask_url(url2))
|
|
1260
|
+
assert ret, message
|
|
1261
|
+
assert ret[:3] == '504', message
|
|
1262
|
+
assert ret[4:].startswith(mask_url(url2)), message
|
|
1231
1263
|
|
|
1232
1264
|
with pytest.raises(AttributeError):
|
|
1233
1265
|
url_failure(cast(str, 123456))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|