ae-base 0.3.69__tar.gz → 0.3.71__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.69
3
+ Version: 0.3.71
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
@@ -63,15 +63,15 @@ Dynamic: provides-extra
63
63
  Dynamic: requires-python
64
64
  Dynamic: summary
65
65
 
66
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.96 -->
67
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
68
- # base 0.3.69
66
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.97 -->
67
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.namespace_root_tpls v0.3.19 -->
68
+ # base 0.3.71
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.68?logo=python)](
74
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.68)
73
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.71?logo=python)](
74
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.71)
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
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.96 -->
2
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
3
- # base 0.3.69
1
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.97 -->
2
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.namespace_root_tpls v0.3.19 -->
3
+ # base 0.3.71
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.68?logo=python)](
9
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.68)
8
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.71?logo=python)](
9
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.71)
10
10
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
11
11
  https://pypi.org/project/ae-base/#history)
12
12
 
@@ -114,7 +114,7 @@ dynamically inspect modules, execution frames, and variables on the call stack.
114
114
  networking utilities
115
115
  --------------------
116
116
 
117
- * :func:`url_failure`: determines if and why a HTTP|FTP target is unavailable.
117
+ * :func:`url_failure`: determines if and why an HTTP|FTP target is unavailable.
118
118
  * :func:`mask_url`: hides or replaces the password/token portion of a URL for safe logging.
119
119
 
120
120
 
@@ -218,6 +218,7 @@ the following are direct references to functions in the :mod:`os.path` module fo
218
218
  * :data:`os_path_splitext`: :func:`os.path.splitext`
219
219
  """
220
220
  # pylint: disable=too-many-lines
221
+ import base64
221
222
  import datetime
222
223
  import getpass
223
224
  import importlib.abc
@@ -240,12 +241,12 @@ from importlib.machinery import ModuleSpec
240
241
  from inspect import getinnerframes, getouterframes, getsourcefile
241
242
  from urllib.error import HTTPError, URLError
242
243
  from urllib.parse import urlparse, urlunparse
243
- from urllib.request import urlopen
244
+ from urllib.request import Request, urlopen
244
245
  from types import ModuleType
245
246
  from typing import Any, Callable, Generator, Iterable, MutableMapping, Optional, Union, cast
246
247
 
247
248
 
248
- __version__ = '0.3.69'
249
+ __version__ = '0.3.71'
249
250
 
250
251
 
251
252
  os_path_abspath = os.path.abspath
@@ -1316,18 +1317,44 @@ def to_ascii(unicode_str: str) -> str:
1316
1317
  return "".join([c for c in nfkd_form if not unicodedata.combining(c)]).replace('ß', "ss").replace('€', "Euro")
1317
1318
 
1318
1319
 
1319
- def url_failure(url: str, timeout: Optional[float] = None) -> str: # pylint: disable=too-many-return-statements
1320
+ # pylint: disable-next=too-many-arguments,too-many-positional-arguments,too-many-return-statements
1321
+ def url_failure(url: str, token: str = "", username: str = "", password: str = "", git_repo: bool = False,
1322
+ timeout: Optional[float] = None) -> str:
1320
1323
  """ determine if and why an FTP or HTTP[S] target is not available via a GET request.
1321
1324
 
1322
- :param url: URL of an target|page|file to check (not downloaded, fetching only the header).
1325
+ :param url: URL of a target|page|file to check (not downloaded, fetching only the header).
1326
+ :param token: optional bearer token to authenticate (only for HTTPS protocol).
1327
+ :param username: optional username to authenticate (for HTTPS, together with the password argument).
1328
+ :param password: optional password to authenticate (for HTTPS, together with the username argument).
1329
+ :param git_repo: optimized check for Git repository HTTP servers/sites (like GitHub, GitLab, Bitbucket,
1330
+ Gitea, SourceHut, Mercury, etc. as long as they implement Smart HTTP).
1323
1331
  :param timeout: connection timeout in seconds (see :func:`urllib.request.urlopen`).
1324
1332
  :return: empty string if target header is available, else an error description. if an
1325
1333
  FTP|HTTP response error occurred then the error/status code
1326
1334
  will be returned in the first 3 characters.
1335
+
1336
+ .. note::
1337
+ credentials for server authentication can be specified either (1) embedded into the specified url argument,
1338
+ (2) as bearer token in the token argument or (3) via the username/password arguments. in all cases the
1339
+ functino will remove these secrets from the returned error description string.
1327
1340
  """
1341
+ if git_repo:
1342
+ if not url.endswith(".git"):
1343
+ url += ".git"
1344
+ url += "/info/refs?service=git-upload-pack"
1345
+
1346
+ headers = {}
1347
+ if token:
1348
+ assert not username and not password, "url_failure accepts either a token or username/password, not both"
1349
+ headers['Authorization'] = "Bearer " + token
1350
+ elif username or password:
1351
+ creds = f"{username}:{password}".encode('utf-8')
1352
+ headers['Authorization'] = "Basic " + base64.b64encode(creds).decode('utf-8')
1353
+
1328
1354
  # noinspection PyBroadException
1329
1355
  try:
1330
- with urlopen(url, timeout=timeout) as response: # open connection and read header
1356
+ request = Request(url, method='GET', headers=headers)
1357
+ with urlopen(request, timeout=timeout) as response: # open connection and only read the header
1331
1358
  status = response.getcode() # no need to call response.read()
1332
1359
  return "" if 200 <= status < 300 else f"{status} {mask_url(url)} {response.reason=}"
1333
1360
 
@@ -1335,17 +1362,20 @@ def url_failure(url: str, timeout: Optional[float] = None) -> str: # pylint: di
1335
1362
  return f"{exception.code} {mask_url(url)} raised HTTPError {exception.reason=}"
1336
1363
 
1337
1364
  except URLError as exception:
1338
- err_prefix = f"996 {mask_url(url)} raised {exception.errno=} {exception.reason=};"
1365
+ err_msg = f" {mask_url(url)} raised {exception.errno=} {exception.reason=};"
1339
1366
  if isinstance(exception.reason, socket.gaierror):
1340
- return f"{err_prefix} could not resolve hostname"
1341
- if isinstance(exception.reason, socket.timeout):
1342
- return f"{err_prefix} connection timed out after {timeout} seconds"
1367
+ return '995' + f"{err_msg} could not resolve hostname"
1343
1368
  if isinstance(exception.reason, ssl.SSLCertVerificationError):
1344
- return f"{err_prefix} SSL certificate verification failed"
1345
- return f"{err_prefix} could not reach the server"
1369
+ return '996' + f"{err_msg} SSL certificate verification failed"
1370
+ if isinstance(exception.reason, socket.timeout):
1371
+ return '997' + f"{err_msg} connection timed out after {timeout} seconds"
1372
+ return '998' + f"{err_msg} could not reach the server"
1373
+
1374
+ except socket.timeout as _exception: # noqa: F841 # str(_exception) could contain password|token
1375
+ return '997' + f" {mask_url(url)} raised socket-timeout exception after {timeout} seconds"
1346
1376
 
1347
- except Exception: # pylint: disable=broad-exception-caught
1348
- return f"999 {mask_url(url)} raised unexpected exception" # NOT put str(_exception) because contains password
1377
+ except Exception as _exception: # noqa: F841 # pylint: disable=broad-exception-caught
1378
+ return '999' + f" {mask_url(url)} raised unexpected exception" # str(_exception) COULD contain password
1349
1379
 
1350
1380
 
1351
1381
  def utc_datetime() -> datetime.datetime:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ae_base
3
- Version: 0.3.69
3
+ Version: 0.3.71
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
@@ -63,15 +63,15 @@ Dynamic: provides-extra
63
63
  Dynamic: requires-python
64
64
  Dynamic: summary
65
65
 
66
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.96 -->
67
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
68
- # base 0.3.69
66
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.97 -->
67
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.namespace_root_tpls v0.3.19 -->
68
+ # base 0.3.71
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.68?logo=python)](
74
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.68)
73
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.71?logo=python)](
74
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.71)
75
75
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
76
76
  https://pypi.org/project/ae-base/#history)
77
77
 
@@ -23,15 +23,15 @@ setup_kwargs = {
23
23
  'install_requires': [],
24
24
  'keywords': ['configuration', 'development', 'environment', 'productivity'],
25
25
  'license': 'GPL-3.0-or-later',
26
- 'long_description': ('<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.96 -->\n'
27
- '<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->\n'
28
- '# base 0.3.69\n'
26
+ 'long_description': ('<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.97 -->\n'
27
+ '<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.namespace_root_tpls v0.3.19 -->\n'
28
+ '# base 0.3.71\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.68?logo=python)](\n'
34
- ' https://gitlab.com/ae-group/ae_base/-/tree/release0.3.68)\n'
33
+ ' https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.71?logo=python)](\n'
34
+ ' https://gitlab.com/ae-group/ae_base/-/tree/release0.3.71)\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.69',
111
+ 'version': '0.3.71',
112
112
  'zip_safe': True,
113
113
  }
114
114
 
@@ -1,7 +1,6 @@
1
1
  """ ae.base unit tests """
2
2
  import datetime
3
3
  import os
4
- import pytest
5
4
  import shutil
6
5
  import socket
7
6
  import ssl
@@ -9,16 +8,20 @@ import string
9
8
  import sys
10
9
  import tempfile
11
10
  import textwrap
11
+ import time
12
+ import timeit
12
13
 
13
14
  from collections import OrderedDict
14
15
  from configparser import ConfigParser
15
16
  # noinspection PyProtectedMember
16
17
  from http.client import HTTPMessage
17
18
  from types import ModuleType
18
- from typing import cast, Any
19
+ from typing import cast, Any, Optional
19
20
  from unittest.mock import patch
20
21
  from urllib.error import HTTPError, URLError
21
22
 
23
+ import pytest
24
+
22
25
  # noinspection PyProtectedMember
23
26
  from ae.base import (
24
27
  ASCII_TO_UNICODE, ASCII_UNICODE, BUILD_CONFIG_FILE, DOTENV_FILE_NAME, PY_EXT, PY_INIT, PY_MAIN, TESTS_FOLDER,
@@ -67,15 +70,32 @@ def os_env_test_env():
67
70
  module_test_var = 'module_test_var_val' # used for stack_var()/try_exec() tests
68
71
 
69
72
 
70
- def test_unset_truthiness():
73
+ def test_unset_truthiness_and_null_length():
71
74
  assert not UNSET
72
75
  assert bool(UNSET) is False
73
76
 
74
-
75
- def test_unset_null_length():
76
77
  assert len(UNSET) == 0
77
78
 
78
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
+
79
99
  class TestErrorMsgMixin:
80
100
  def test_instantiation(self):
81
101
  ins = ErrorMsgMixin()
@@ -1067,23 +1087,35 @@ class TestBaseHelpers:
1067
1087
  assert to_ascii('ß') == 'ss'
1068
1088
  assert to_ascii('€') == 'Euro'
1069
1089
 
1090
+ @staticmethod
1091
+ def url_failure_httpbin_503_retryer(url: str, timeout: Optional[float] = None) -> tuple[str, str]:
1092
+ retries = 9
1093
+ while True:
1094
+ err_msg = url_failure(url, timeout=timeout)
1095
+ if not err_msg or int(err_msg[:3]) != 503 or retries == 0:
1096
+ break
1097
+ time.sleep(3)
1098
+ retries -= 1
1099
+ return err_msg, f"url_failure({url=}, {timeout=}) httpbin is sometimes unavailable/503. retry later; {err_msg=}"
1100
+
1070
1101
  def test_url_failure(self):
1071
1102
  assert not url_failure("https://gitlab.com/ae-group/ae_base")
1072
1103
 
1073
1104
  assert not url_failure("https://gitlab.com/ae-group/ae_base.git")
1074
1105
 
1075
- assert not url_failure("https://www.google.com")
1106
+ assert not url_failure("https://gitlab.com/ae-group/ae_base", git_repo=True)
1076
1107
 
1077
- assert not url_failure(f"https://httpbin.org/status/200")
1108
+ assert not url_failure("https://www.google.com")
1078
1109
 
1079
- def test_url_failure_errors(self):
1080
- assert url_failure("")
1110
+ ret, message = self.url_failure_httpbin_503_retryer("https://httpbin.org/status/200")
1111
+ assert not ret, message
1081
1112
 
1113
+ def test_url_failure_authentication_errors(self):
1082
1114
  password, domain, path = "toBeMaskedPassword", "any-not_existing-host_domain.zzz", "any/not/existing/url/path"
1083
1115
  url = f"https://username:{password}@{domain}/{path}"
1084
1116
  err_msg = "raised exception error message"
1085
1117
 
1086
- ret = url_failure(url)
1118
+ ret = url_failure(url, token=password)
1087
1119
 
1088
1120
  assert ret
1089
1121
  assert int(ret[:3]) > 0
@@ -1091,24 +1123,21 @@ class TestBaseHelpers:
1091
1123
  assert domain in ret
1092
1124
  assert path in ret
1093
1125
 
1094
- ret = url_failure(url2 := f"https://httpbin.org/status/504")
1095
-
1096
- assert ret
1097
- assert int(ret[:3]) == 504
1098
- assert ret[4:].startswith(mask_url(url2))
1099
-
1100
- ret = url_failure(f"https://httpbin.org/delay/3", timeout=0.9)
1126
+ ret = url_failure(url, username="any user name", password=password)
1101
1127
 
1102
1128
  assert ret
1103
1129
  assert int(ret[:3]) > 0
1130
+ assert password not in ret
1131
+ assert domain in ret
1132
+ assert path in ret
1104
1133
 
1105
- ret = url_failure(f"https://expired.badssl.com")
1134
+ ret = url_failure(url)
1106
1135
 
1107
1136
  assert ret
1108
1137
  assert int(ret[:3]) > 0
1109
-
1110
- with pytest.raises(AttributeError):
1111
- url_failure(cast(str, 123456))
1138
+ assert password not in ret
1139
+ assert domain in ret
1140
+ assert path in ret
1112
1141
 
1113
1142
  mocked_headers = cast(HTTPMessage, {})
1114
1143
 
@@ -1202,6 +1231,30 @@ class TestBaseHelpers:
1202
1231
  assert domain in ret
1203
1232
  assert path in ret
1204
1233
 
1234
+ def test_url_failure_ssl_errors(self):
1235
+ ret = url_failure(f"https://expired.badssl.com")
1236
+
1237
+ assert ret
1238
+ assert int(ret[:3]) > 0
1239
+
1240
+ def test_url_failure_timeout_errors(self):
1241
+ ret, message = self.url_failure_httpbin_503_retryer("https://httpbin.org/delay/3", timeout=0.9)
1242
+
1243
+ assert ret, message
1244
+ assert ret[:3] == '997', message
1245
+
1246
+ def test_url_failure_url_errors(self):
1247
+ assert url_failure("")
1248
+
1249
+ ret, message = self.url_failure_httpbin_503_retryer(url2 := "https://httpbin.org/status/504")
1250
+
1251
+ assert ret, message
1252
+ assert ret[:3] == '504', message
1253
+ assert ret[4:].startswith(mask_url(url2)), message
1254
+
1255
+ with pytest.raises(AttributeError):
1256
+ url_failure(cast(str, 123456))
1257
+
1205
1258
  def test_utc_datetime(self):
1206
1259
  dt1 = utc_datetime()
1207
1260
  dt2 = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
File without changes
File without changes
File without changes