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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ae_base
3
- Version: 0.3.70
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.70
68
+ # base 0.3.72
69
69
 
70
70
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_base/develop?logo=python)](
71
71
  https://gitlab.com/ae-group/ae_base)
72
72
  [![LatestPyPIrelease](
73
- https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.70?logo=python)](
74
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.70)
73
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.72?logo=python)](
74
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.72)
75
75
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
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.70
3
+ # base 0.3.72
4
4
 
5
5
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_base/develop?logo=python)](
6
6
  https://gitlab.com/ae-group/ae_base)
7
7
  [![LatestPyPIrelease](
8
- https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.70?logo=python)](
9
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.70)
8
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.72?logo=python)](
9
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.72)
10
10
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
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.70'
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
- env_vars: dict[str, str] = {}
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
- err_prefix = f"996 {mask_url(url)} raised {exception.errno=} {exception.reason=};"
1374
+ err_msg = f" {mask_url(url)} raised {exception.errno=} {exception.reason=};"
1366
1375
  if isinstance(exception.reason, socket.gaierror):
1367
- return f"{err_prefix} could not resolve hostname"
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"{err_prefix} SSL certificate verification failed"
1372
- return f"{err_prefix} could not reach the server"
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: # pylint: disable=broad-exception-caught
1375
- return f"999 {mask_url(url)} raised unexpected exception" # NOT put str(_exception) because contains password
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.70
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.70
68
+ # base 0.3.72
69
69
 
70
70
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_base/develop?logo=python)](
71
71
  https://gitlab.com/ae-group/ae_base)
72
72
  [![LatestPyPIrelease](
73
- https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.70?logo=python)](
74
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.70)
73
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.72?logo=python)](
74
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.72)
75
75
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
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.70\n'
28
+ '# base 0.3.72\n'
29
29
  '\n'
30
30
  '[![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_base/develop?logo=python)](\n'
31
31
  ' https://gitlab.com/ae-group/ae_base)\n'
32
32
  '[![LatestPyPIrelease](\n'
33
- ' https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.70?logo=python)](\n'
34
- ' https://gitlab.com/ae-group/ae_base/-/tree/release0.3.70)\n'
33
+ ' https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.72?logo=python)](\n'
34
+ ' https://gitlab.com/ae-group/ae_base/-/tree/release0.3.72)\n'
35
35
  '[![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](\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.70',
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 test_unset_truthiness():
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
- retries = 9
1082
- while True:
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 = url_failure(f"https://httpbin.org/delay/3", timeout=0.9)
1250
+ ret, message = self.url_failure_httpbin_503_retryer("https://httpbin.org/delay/3", timeout=0.9)
1219
1251
 
1220
- assert ret
1221
- assert int(ret[:3]) > 0
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 = url_failure(url2 := f"https://httpbin.org/status/504")
1258
+ ret, message = self.url_failure_httpbin_503_retryer(url2 := "https://httpbin.org/status/504")
1227
1259
 
1228
- assert ret
1229
- assert int(ret[:3]) == 504
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