ae-base 0.3.65__tar.gz → 0.3.67__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,4 +1,4 @@
1
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.project_tpls V0.3.45 -->
1
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.project_tpls v0.3.53 -->
2
2
  ### GNU GENERAL PUBLIC LICENSE
3
3
 
4
4
  Version 3, 29 June 2007
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ae_base
3
- Version: 0.3.65
4
- Summary: ae namespace module portion base: basic constants, helper functions and context manager
3
+ Version: 0.3.67
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
7
7
  Author-email: aecker2@gmail.com
@@ -36,8 +36,6 @@ Requires-Dist: pytest-cov; extra == "dev"
36
36
  Requires-Dist: pytest-django; extra == "dev"
37
37
  Requires-Dist: typing; extra == "dev"
38
38
  Requires-Dist: types-setuptools; extra == "dev"
39
- Requires-Dist: wheel; extra == "dev"
40
- Requires-Dist: twine; extra == "dev"
41
39
  Provides-Extra: docs
42
40
  Provides-Extra: tests
43
41
  Requires-Dist: anybadge; extra == "tests"
@@ -51,8 +49,6 @@ Requires-Dist: pytest-cov; extra == "tests"
51
49
  Requires-Dist: pytest-django; extra == "tests"
52
50
  Requires-Dist: typing; extra == "tests"
53
51
  Requires-Dist: types-setuptools; extra == "tests"
54
- Requires-Dist: wheel; extra == "tests"
55
- Requires-Dist: twine; extra == "tests"
56
52
  Dynamic: author
57
53
  Dynamic: author-email
58
54
  Dynamic: classifier
@@ -67,19 +63,19 @@ Dynamic: provides-extra
67
63
  Dynamic: requires-python
68
64
  Dynamic: summary
69
65
 
70
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.96 -->
66
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.96 -->
71
67
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
72
- # base 0.3.65
68
+ # base 0.3.67
73
69
 
74
70
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_base/develop?logo=python)](
75
71
  https://gitlab.com/ae-group/ae_base)
76
72
  [![LatestPyPIrelease](
77
- https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.64?logo=python)](
78
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.64)
73
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.66?logo=python)](
74
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.66)
79
75
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
80
76
  https://pypi.org/project/ae-base/#history)
81
77
 
82
- >ae namespace module portion base: basic constants, helper functions and context manager.
78
+ >ae namespace module portion base: basic constants, helper functions and context managers.
83
79
 
84
80
  [![Coverage](https://ae-group.gitlab.io/ae_base/coverage.svg)](
85
81
  https://ae-group.gitlab.io/ae_base/coverage/index.html)
@@ -1,16 +1,16 @@
1
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.96 -->
1
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.96 -->
2
2
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
3
- # base 0.3.65
3
+ # base 0.3.67
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.64?logo=python)](
9
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.64)
8
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.66?logo=python)](
9
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.66)
10
10
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
11
11
  https://pypi.org/project/ae-base/#history)
12
12
 
13
- >ae namespace module portion base: basic constants, helper functions and context manager.
13
+ >ae namespace module portion base: basic constants, helper functions and context managers.
14
14
 
15
15
  [![Coverage](https://ae-group.gitlab.io/ae_base/coverage.svg)](
16
16
  https://ae-group.gitlab.io/ae_base/coverage/index.html)
@@ -1,156 +1,221 @@
1
1
  """
2
- basic constants, helper functions and context manager
3
- =====================================================
2
+ basic constants, helper functions and context managers
3
+ ======================================================
4
4
 
5
- this module is pure python, has no external dependencies, and is providing base constants, common helper
6
- functions, useful classes and context managers.
5
+ this module is pure python, has no external dependencies, and provides a comprehensive toolkit of base constants,
6
+ common helper functions, useful classes, and context managers for a wide variety of programming tasks.
7
7
 
8
8
  .. note::
9
- on import of this module, while running on Android OS, it will monkey patch the :mod:`shutil` module
10
- to allow using them on Android devices. therefore, the import of this module should be one of the first ones
11
- in your app's main module.
9
+ on import, this module checks if it is running on the Android OS. if so, it will monkey patch the
10
+ :mod:`shutil` module to ensure functions like ``copy`` and ``move`` work correctly. to prevent
11
+ permission-related errors, this module should be one of the first imports in your Android app's main module.
12
12
 
13
13
 
14
- base constants
15
- --------------
16
-
17
- ISO format strings for ``date`` and ``datetime`` values are provided by the constants :data:`DATE_ISO` and
18
- :data:`DATE_TIME_ISO`.
14
+ string manipulation
15
+ -------------------
19
16
 
20
- the :data:`UNSET` constant is useful in cases where ``None`` is a valid data value and another special value is needed
21
- to specify that e.g., an argument or attribute has no (valid) value or did not get specified/passed.
17
+ functions for converting, cleaning, normalizing, and formatting strings.
22
18
 
23
- default values to compile file and folder names for a package or an app project are provided by the constants:
24
- :data:`DOCS_FOLDER`, :data:`TESTS_FOLDER`, :data:`TEMPLATES_FOLDER`, :data:`BUILD_CONFIG_FILE`,
25
- :data:`PACKAGE_INCLUDE_FILES_PREFIX`, :data:`PY_EXT`, :data:`PY_INIT`, :data:`PY_MAIN`, :data:`CFG_EXT`
26
- and :data:`INI_EXT`.
19
+ * :func:`camel_to_snake`: converts a string from CamelCase to snake_case.
20
+ * :func:`snake_to_camel`: converts a string from snake_case to CamelCase.
21
+ * :func:`norm_name`: normalizes a string to be a valid identifier (e.g., for variable-, method-, or file-names).
22
+ * :func:`norm_line_sep`: converts all line separator combinations (CRLF, CR) in a string to a single newline (LF).
23
+ * :func:`defuse`: converts special characters in string to Unicode alternatives, making it safe for use as
24
+ a URL slug, path or filename.
25
+ * :func:`dedefuse`: reverses the operation of :func:`defuse`, restoring the original string.
26
+ * :func:`force_encoding`: ensures text is in a specific encoding without raising errors, replacing characters as needed.
27
+ * :func:`to_ascii`: converts a Unicode string into its closest ASCII representation by removing accents and diacritics.
28
+ * :func:`ascii_str`: encodes a Unicode string into a reversible 7-bit ASCII representation, useful for transport
29
+ protocols like HTTP headers.
30
+ * :func:`str_ascii`: decodes a string created by :func:`ascii_str` back to its original Unicode form.
31
+ * :func:`format_given`: a replacement for `str.format_map` that formats a string but leaves placeholders intact if they
32
+ are not found in the provided mapping.
27
33
 
28
- with the help of the format string constant :data:`NOW_STR_FORMAT` and the function :func:`now_str` you can create a
29
- sortable and compact string from a timestamp.
30
34
 
35
+ system & environment
36
+ --------------------
31
37
 
32
- base helper functions
33
- ---------------------
38
+ inspect the operating system and manage environment variables.
34
39
 
35
- the function :func:`evaluate_literal` can be used as an replacement of :func:`ast.literal_eval` to retrieve
36
- basic data structure values from config, ini and .env files, while also accepting unquoted strings as a `str` type
37
- instance.
40
+ .. hint::
41
+ the :mod:`ae.core` portion is providing more OS-specific constants and helper functions, like e.g.
42
+ :func:`~ae.core.start_app_service` and :func:`~ae.core.request_app_permissions`.
38
43
 
39
- most programming languages providing a function to determine the sign of a number. the :func:`sign` functino,
40
- provided by this module/portion is filling this gap in Python.
44
+ OS information
45
+ ~~~~~~~~~~~~~~
41
46
 
42
- in order to convert and transfer Unicode character outside the 7-bit ASCII range via internet transport protocols,
43
- like http, use the helper functions :func:`ascii_str` and :func:`str_ascii`.
47
+ * :data:`os_platform`: a string identifying the operating system (e.g., 'linux', 'win32', 'android', 'ios').
48
+ * :data:`os_device_id`: a string with the ID/name of the device.
49
+ * :func:`os_host_name`: determines the operating system's host/machine name.
50
+ * :func:`os_local_ip`: determines the local IP address of the machine.
51
+ * :func:`os_user_name`: determines the current logged-in user's name.
52
+ * :func:`sys_env_dict`: returns a dictionary containing key Python runtime environment values.
53
+ * :func:`sys_env_text`: compiles a formatted text block with system environment information, useful for logging.
44
54
 
45
- :func:`now_str` creates a timestamp string with the actual UTC date and time. the :func:`utc_datetime` provides the
46
- actual UTC date and time as a datetime object.
55
+ environment variables & `.env` files
56
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
47
57
 
48
- to write more compact and readable code for the most common file I/O operations, the helper functions :func:`read_file`
49
- and :func:`write_file` are wrapping Python's built-in :func:`open` function and its context manager.
58
+ * :func:`env_str`: retrieves the string value of an OS environment variable, with an option to automatically convert the
59
+ variable name to the conventional format.
60
+ * :func:`parse_dotenv`: parses a `.env` file and returns its key-value pairs as a dictionary.
61
+ * :func:`load_env_var_defaults`: recursively searches parent directories for `.env` files and loads any undeclared
62
+ variables.
63
+ * :func:`load_dotenvs`: detects and loads all relevant `.env` files from the current working directory and optional
64
+ also from the main module's path.
50
65
 
51
- the function :func:`duplicates` returns the duplicates of an iterable type.
52
66
 
53
- in order to hide/mask secrets like credit card numbers, passwords or tokens in deeply nested data structures,
54
- before they get dumped e.g., to an app log file, the function :func:`mask_secrets` can be used.
67
+ data structure utilities
68
+ ------------------------
55
69
 
56
- :func:`norm_line_sep` is converting any combination of line separators of a string to a single new-line character.
70
+ helpers for working with lists, dictionaries, and other data structures.
57
71
 
58
- the function :func:`norm_name` converts any string into a name that can be used e.g., as a file name
59
- or as a method/attribute name.
72
+ * :func:`evaluate_literal`: replacement for :func:`ast.literal_eval` that also interprets/recognizes unquoted strings
73
+ as `str` type.
74
+ * :func:`duplicates`: returns a list of all duplicate items found in any type of iterable.
75
+ * :func:`deep_dict_update`: recursively updates a dictionary in-place with values from another dictionary.
76
+ * :func:`mask_secrets`: hides sensitive string values (e.g., passwords, API keys) in deeply nested data structures,
77
+ useful for logging.
60
78
 
61
- to normalize a file path, in order to remove `.`, `..` placeholders, to resolve symbolic links or to make it relative or
62
- absolute, call the function :func:`norm_path`.
63
79
 
64
- :func:`defuse` converts special characters of a URI/URL or a file path string, resulting in a string that can be used
65
- either as a URL slug or as a file name. use the function :func:`dedefuse` to convert this string back to the
66
- corresponding URL/URI or file path.
80
+ application & project helpers
81
+ -----------------------------
67
82
 
68
- the functions :func:`camel_to_snake` and :func:`snake_to_camel` providing name conversions of class and method names.
83
+ functions to aid in application setup, configuration, and build introspection.
69
84
 
70
- to encode Unicode strings to other codecs, the functions :func:`force_encoding` and :func:`to_ascii` can be used.
85
+ * :func:`app_name_guess`: attempts to determine the name of the currently running application from its environment.
86
+ * :func:`build_config_variable_values`: reads variable values from a `buildozer.spec` file.
87
+ * :func:`instantiate_config_parser`: returns a `ConfigParser` instance pre-configured for case-sensitive keys and
88
+ extended interpolation.
89
+ * :func:`project_main_file`: determines the absolute path to the main module file of a project package (where the
90
+ `__version__` of the app|package is defined).
91
+ * :func:`main_file_paths_parts`: returns a tuple of possible main/version file path names combinations of any project.
71
92
 
72
- the :func:`round_traditional` function gets provided by this module for traditional rounding of float values. the
73
- function signature is fully compatible with Python's :func:`round` function.
74
93
 
75
- the function :func:`instantiate_config_parser` ensures that the :class:`~configparser.ConfigParser` instance is
76
- correctly configured, e.g., to support case-sensitive config variable names and to use :class:`ExtendedInterpolation`
77
- as the interpolation argument.
94
+ modules and call stack inspection
95
+ ---------------------------------
78
96
 
79
- :func:`app_name_guess` guesses the name of o running Python application from the application environment, with the help
80
- of :func:`build_config_variable_values`, which determines config-variable-values from the build spec file of an app
81
- project.
97
+ dynamically inspect modules, execution frames, and variables on the call stack.
82
98
 
99
+ * :func:`import_module`: dynamically imports a Python module from a path without adding it to `sys.modules`.
100
+ * :func:`module_attr`: dynamically gets a reference to a module or any attribute (variable, function, class) within it.
101
+ * :func:`module_file_path`: determines the absolute file path of the module from which it is called.
102
+ * :func:`module_name`: finds the name of the first module in the call stack that is not in a predefined skip list.
103
+ * :func:`stack_frames`: a generator that yields frames from the call stack, starting at a specified depth.
104
+ * :func:`stack_var`: finds the value of a specific variable by searching up the call stack.
105
+ * :func:`stack_vars`: returns the global and local variables from a specific frame in the call stack.
106
+ * :func:`full_stack_trace`: generates a complete, detailed string representation of an exception's stack trace.
83
107
 
84
- operating system constants and helpers
85
- --------------------------------------
108
+ .. hint::
109
+ the :class:`~ae.core.AppBase` class uses these helper functions to determine the
110
+ :attr:`version <ae.core.AppBase.app_version>` and :attr:`title <ae.core.AppBase.app_title>` of an application,
111
+ if these values are not specified in the instance initializer.
86
112
 
87
- the string :data:`os_platform` provides the OS where your app is running, extending Python's :func:`sys.platform`
88
- for mobile platforms like Android and iOS.
89
113
 
90
- the functions :func:`os_host_name`, :func:`os_local_ip` and :func:`os_user_name` are determining machine and
91
- user information from the OS.
114
+ networking utilities
115
+ --------------------
92
116
 
93
- use :func:`env_str` to determine the value of an OS environment variable with automatic variable name conversion. other
94
- helper functions provided by this namespace portion to determine the values of the most important system environment
95
- variables for your application are :func:`sys_env_dict` and :func:`sys_env_text`.
117
+ * :func:`url_failure`: determines if and why a HTTP|FTP target is unavailable.
118
+ * :func:`mask_url`: hides or replaces the password/token portion of a URL for safe logging.
96
119
 
97
- to integrate system environment variables from ``.env`` files into :data:`os.environ` the helper functions
98
- :func:`parse_dotenv`, :func:`load_env_var_defaults` and :func:`load_dotenvs` are provided.
99
120
 
100
- the :mod:`ae.core` portion is providing more OS-specific constants and helper functions, like e.g.
101
- :func:`start_app_service` and :func:`request_app_permissions`.
121
+ general utilities & helpers
122
+ ---------------------------
102
123
 
103
- .. note::
104
- on import of this module, while running on Android OS, it will monkey patch the :mod:`shutil` module to allow
105
- using them on Android devices, and on the first app start requesting the permissions of your app. therefore, to
106
- prevent permission errors, the import of this module should be the first statement in the main module of your app.
124
+ a collection of miscellaneous mathematical, date/time, and other standalone helper functions.
107
125
 
126
+ mathematical
127
+ ~~~~~~~~~~~~
108
128
 
109
- types, classes and mixins
110
- -------------------------
129
+ * :func:`sign`: returns the sign of a number (-1 for negative, 0 for zero, 1 for positive).
130
+ * :func:`round_traditional`: rounds a float value using traditional rounding rules (e.g., `0.5` rounds up).
111
131
 
112
- the :class:`UnsetType` class can be used e.g., for the declaration of optional function and method parameters,
113
- allowing also ``None`` is an accepted argument value.
132
+ date & time
133
+ ~~~~~~~~~~~
134
+ * :func:`utc_datetime`: Returns the current date and time as a timezone-naive `datetime` object in UTC.
135
+ * :func:`now_str`: creates a compact, sortable timestamp string from the current UTC time.
114
136
 
115
- to extend any class with an intelligent error message handling, add the mixin :class:`ErrorMsgMixin` to it.
137
+ miscellaneous
138
+ ~~~~~~~~~~~~~
139
+ * :func:`dummy_function`: a null function that accepts any arguments and returns `None`.
116
140
 
117
- the classes :class:`UnformattedValue` and :class:`GivenFormatter` can be used to format strings with placeholders
118
- enclosed in curly brackets. the function :func:`format_given` is using them to format templates with placeholders.
119
141
 
120
-
121
- generic context manager
142
+ types, classes & mixins
122
143
  -----------------------
123
144
 
124
- the context manager :func:`in_wd` allows switching the current working directory temporarily. the following
125
- example demonstrates a typical usage, together with a temporary path, created with the help of Pythons
126
- :class:`~tempfile.TemporaryDirectory` class::
127
-
128
- with tempfile.TemporaryDirectory() as tmp_dir, in_wd(tmp_dir):
129
- # within the context the tmp_dir is set as the current working directory
130
- assert os.getcwd() == tmp_dir
131
- # current working directory set back to the original path and the temporary directory got removed
132
-
133
-
134
- call stack inspection
135
- ---------------------
136
-
137
- :func:`module_attr` dynamically determines a reference to an attribute (variable, function, class, ...) in a module.
145
+ * :class:`UnsetType`: the class for the :data:`UNSET` singleton object, useful as a sentinel value when `None` is a
146
+ valid input.
147
+ * :class:`ErrorMsgMixin`: a mixin class that provides any class with a sophisticated error message handling and
148
+ logging property.
149
+ * :class:`UnformattedValue`: a helper class for :func:`format_given` to represent a placeholder that was not found in
150
+ the formatting map.
151
+ * :class:`GivenFormatter`: a helper class for :func:`format_given` that overrides default formatting behavior to keep
152
+ missing placeholders.
138
153
 
139
- :func:`module_name`, :func:`stack_frames`, :func:`stack_var` and :func:`stack_vars` are inspecting the call stack frames
140
- to determine e.g., variable values of the callers of a function/method.
141
154
 
142
- .. hint::
143
- the :class:`AppBase` class uses these helper functions to determine the :attr:`version <AppBase.app_version>` and
144
- :attr:`title <AppBase.app_title>` of an application, if these values are not specified in the instance initializer.
145
-
146
- another useful helper function provided by this portion to inspect and debug your code is :func:`full_stack_trace`.
155
+ base constants
156
+ --------------
147
157
 
158
+ predefined constants for project structure, file conventions, and default settings.
159
+
160
+ project & file structure
161
+ ~~~~~~~~~~~~~~~~~~~~~~~~
162
+
163
+ * :data:`DOCS_FOLDER`: default name for a project's documentation folder ('docs').
164
+ * :data:`TESTS_FOLDER`: default name for a project's tests folder ('tests').
165
+ * :data:`TEMPLATES_FOLDER`: default name for a folder containing file templates ('templates').
166
+ * :data:`BUILD_CONFIG_FILE`: default name for a build configuration file ('buildozer.spec').
167
+ * :data:`DEF_PROJECT_PARENT_FOLDER`: default directory name for grouping source code projects ('src').
168
+ * :data:`PY_CACHE_FOLDER`: default name for Python's cache folder ('__pycache__').
169
+ * :data:`PY_EXT`: file extension for Python modules ('.py').
170
+ * :data:`PY_INIT`: the filename for a Python package initializer ('__init__.py').
171
+ * :data:`PY_MAIN`: the filename for a Python executable's main module ('__main__.py').
172
+ * :data:`CFG_EXT`: file extension for CFG configuration files ('.cfg').
173
+ * :data:`INI_EXT`: file extension for INI configuration files ('.ini').
174
+ * :data:`DOTENV_FILE_NAME`: default name for environment variable files ('.env').
175
+ * :data:`PACKAGE_INCLUDE_FILES_PREFIX`: prefix for files/folders to be included in setup package data (used by
176
+ :mod:`ae.updater` and :mod:`aedev.project_manager`)
177
+
178
+ formats & default settings
179
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
180
+
181
+ * :data:`DATE_ISO`: ISO format string for dates ("%Y-%m-%d").
182
+ * :data:`DATE_TIME_ISO`: ISO format string for :mod:`datetime.datetime` dates ("%Y-%m-%d %H:%M:%S.%f").
183
+ * :data:`NOW_STR_FORMAT`: the datetime format string, used e.g. by :func:`now_str` for creating timestamps.
184
+ * :data:`NAME_PARTS_SEP`: the character used as a separator in name conversions ('_').
185
+ * :data:`DEF_ENCODING`: the default encoding used for string operations ('ascii').
186
+ * :data:`DEF_ENCODE_ERRORS`: the default error handling strategy for encoding ('backslashreplace').
187
+ * :data:`SKIPPED_MODULES`: a tuple of module names to be ignored by stack inspection functions.
188
+ * :data:`UNSET`: a singleton instance of :class:`UnsetType`, used where `None` is a valid data value.
189
+
190
+
191
+ file, path & I/O operations
192
+ ---------------------------
193
+
194
+ simplify file system interactions with wrappers and context managers.
195
+
196
+ * :func:`read_file`: reads the entire content of a text or binary file into a string or bytes object.
197
+ * :func:`write_file`: writes a string or bytes object to a file, overwriting existing content.
198
+ * :func:`norm_path`: normalizes a path by expanding user home directories (`~`), resolving `.`, `..`, symbolic links,
199
+ and converting between absolute and relative paths.
200
+ * :func:`in_wd`: a context manager that temporarily switches the current working directory.
148
201
 
149
202
  os.path shortcuts
150
- -----------------
151
-
152
- the following data items are pointers to shortcut at runtime the lookup to their related functions in the
153
- Python module :mod:`os.path`:
203
+ ~~~~~~~~~~~~~~~~~
204
+
205
+ the following are direct references to functions in the :mod:`os.path` module for convenient and quicker access:
206
+
207
+ * :data:`os_path_abspath`: :func:`os.path.abspath`
208
+ * :data:`os_path_basename`: :func:`os.path.basename`
209
+ * :data:`os_path_dirname`: :func:`os.path.dirname`
210
+ * :data:`os_path_expanduser`: :func:`os.path.expanduser`
211
+ * :data:`os_path_isdir`: :func:`os.path.isdir`
212
+ * :data:`os_path_isfile`: :func:`os.path.isfile`
213
+ * :data:`os_path_join`: :func:`os.path.join`
214
+ * :data:`os_path_normpath`: :func:`os.path.normpath`
215
+ * :data:`os_path_realpath`: :func:`os.path.realpath`
216
+ * :data:`os_path_relpath`: :func:`os.path.relpath`
217
+ * :data:`os_path_sep`: :data:`os.path.sep`
218
+ * :data:`os_path_splitext`: :func:`os.path.splitext`
154
219
  """
155
220
  # pylint: disable=too-many-lines
156
221
  import datetime
@@ -162,6 +227,7 @@ import platform
162
227
  import re
163
228
  import shutil
164
229
  import socket
230
+ import ssl
165
231
  import string
166
232
  import sys
167
233
  import unicodedata
@@ -172,11 +238,14 @@ from configparser import ConfigParser, ExtendedInterpolation
172
238
  from contextlib import contextmanager
173
239
  from importlib.machinery import ModuleSpec
174
240
  from inspect import getinnerframes, getouterframes, getsourcefile
241
+ from urllib.error import HTTPError, URLError
242
+ from urllib.parse import urlparse, urlunparse
243
+ from urllib.request import urlopen
175
244
  from types import ModuleType
176
245
  from typing import Any, Callable, Generator, Iterable, MutableMapping, Optional, Union, cast
177
246
 
178
247
 
179
- __version__ = '0.3.65'
248
+ __version__ = '0.3.67'
180
249
 
181
250
 
182
251
  os_path_abspath = os.path.abspath
@@ -360,6 +429,7 @@ def deep_dict_update(data: dict, update: dict, overwrite: bool = True):
360
429
 
361
430
 
362
431
  URI_SEP_CHAR = '⫻' # U+2AFB: TRIPLE SOLIDUS BINARY RELATION
432
+ # noinspection GrazieInspection
363
433
  ASCII_UNICODE = (
364
434
  ('/', '⁄'), # U+2044: Fraction Slash; '∕' U+2215: Division Slash; '⧸' U+29F8: Big Solidus;
365
435
  # '╱' U+FF0F: Fullwidth Solidus; '╱' U+2571: Box Drawings Light Diagonal Upper Right to Lower Left
@@ -433,7 +503,7 @@ def defuse(value: str) -> str:
433
503
  in most unix variants only the slash and the ASCII 0 characters are not allowed in file names.
434
504
 
435
505
  in MS Windows are not allowed: ASCII 0..31 / | \\ : * ? ” % < > ( ). some blogs recommend also not allowing
436
- (convert) the characters # and '.
506
+ (convert) the characters `#` and `'`.
437
507
 
438
508
  only old POSIX seems to be even more restricted (only allowing alphanumeric characters plus . - and _).
439
509
 
@@ -499,13 +569,14 @@ def env_str(name: str, convert_name: bool = False) -> Optional[str]:
499
569
  return os.environ.get(name)
500
570
 
501
571
 
502
- def evaluate_literal(literal_string: str) -> Any:
572
+ def evaluate_literal(literal_string: str
573
+ ) -> Optional[Union[bool, bytes, dict, complex, float, int, list, set, str, tuple]]:
503
574
  """ evaluates a Python expression while accepting unquoted strings as str type.
504
575
 
505
576
  :param literal_string: any literal of the base types (like dict, list, set, tuple) which are recognized
506
577
  by :func:`ast.literal_eval`.
507
- :return: an instance of the data type or the specified string, even if it is not quoted with
508
- high comma characters.
578
+ :return: an instance of the data type or the specified string, even if it is not quoted with high
579
+ comma characters. `None` will be returned if the specified literal is the string "None".
509
580
  """
510
581
  try:
511
582
  return literal_eval(literal_string)
@@ -622,7 +693,11 @@ def import_module(import_name: str, path: Optional[Union[str, UnsetType]] = UNSE
622
693
 
623
694
 
624
695
  def instantiate_config_parser() -> ConfigParser:
625
- """ instantiate and prepare config file parser. """
696
+ """ instantiate and prepare config file parser.
697
+
698
+ ensures that the :class:`~configparser.ConfigParser` instance is correctly configured, e.g., to support
699
+ case-sensitive config variable names and to use :class:`ExtendedInterpolation` as the interpolation argument.
700
+ """
626
701
  cfg_parser = ConfigParser(allow_no_value=True, interpolation=ExtendedInterpolation())
627
702
  # set optionxform to have case-sensitive var names (or use 'lambda option: option')
628
703
  # mypy V 0.740 bug - see mypy issue #5062: adding pragma "type: ignore" breaks PyCharm (showing
@@ -639,6 +714,15 @@ def in_wd(new_cwd: str) -> Generator[None, None, None]:
639
714
 
640
715
  :param new_cwd: path to the directory to switch to (within the context/with block).
641
716
  an empty string gets interpreted as the current working directory.
717
+
718
+ the following example demonstrates a typical usage, together with a temporary path, created with the help of Pythons
719
+ :class:`~tempfile.TemporaryDirectory` class::
720
+
721
+ with tempfile.TemporaryDirectory() as tmp_dir, in_wd(tmp_dir):
722
+ # within the context the tmp_dir is set as the current working directory
723
+ assert os.getcwd() == tmp_dir
724
+ # here the current working directory got set back to the original path and the temporary directory got removed
725
+
642
726
  """
643
727
  cur_dir = os.getcwd()
644
728
  try:
@@ -649,14 +733,26 @@ def in_wd(new_cwd: str) -> Generator[None, None, None]:
649
733
  os.chdir(cur_dir)
650
734
 
651
735
 
652
- def load_dotenvs():
653
- """ detect and load multiple ``.env`` files in/above the current working directory and the calling module folder.
736
+ def load_dotenvs(from_module_path: bool = False):
737
+ """ detect and load not defined OS environment variables from ``.env`` files.
654
738
 
655
- .. hint:: call from the main module of project/app in order to also load ``.env`` files in/above the project folder.
739
+ :param from_module_path: pass True to load OS environment variables (that are not already loaded from ``.env``
740
+ files situated in or above the current working directory) also from/above the folder of
741
+ the first module in the call stack that gets not excluded/skipped by :func:`stack_var`.
742
+
743
+ in order to also load ``.env`` files in/above the project folder.
744
+ call this function from the main module of project/app.
745
+
746
+ .. note::
747
+ only variables that are not already defined in the OS environment variables mapping :data:`os.environ` will be
748
+ loaded/added. variables will be loaded first from the first ``.env`` file found in or above the current working
749
+ directory, while the variable values in the deeper situated files are overwriting the values defined in the
750
+ ``.env`` files situated in the above folders.
656
751
  """
657
752
  env_vars = os.environ
658
753
  load_env_var_defaults(os.getcwd(), env_vars)
659
- if file_name := stack_var('__file__'):
754
+
755
+ if from_module_path and (file_name := stack_var('__file__')):
660
756
  load_env_var_defaults(os_path_dirname(os_path_abspath(file_name)), env_vars)
661
757
 
662
758
 
@@ -731,6 +827,22 @@ def mask_secrets(data: Union[dict, Iterable], fragments: Iterable[str] = ('passw
731
827
  return data
732
828
 
733
829
 
830
+ def mask_url(url: str, replacement: str = "¿¿¿") -> str:
831
+ """ hide|replace the password/token in a URL.
832
+
833
+ :param url: URL in which an optional password|token will be searched and replaced.
834
+ :param replacement: optional replacement string, if not specified then the default value will be used.
835
+ :return: URL with the credentials masked/replaced.
836
+ """
837
+ parts = urlparse(url)
838
+ if parts.password is None:
839
+ return url
840
+ # manually split out the netloc, because using parts.hostname/,port would have to be checked for None&hostname.lower
841
+ parts = parts._replace(netloc=f"{parts.username}:{replacement}@{parts.netloc.rpartition('@')[-1]}")
842
+ # noinspection PyTypeChecker
843
+ return urlunparse(parts)
844
+
845
+
734
846
  def module_attr(import_name: str, attr_name: str = "") -> Optional[Any]:
735
847
  """ determine dynamically a reference to a module or to any attribute (variable/func/class) declared in the module.
736
848
 
@@ -788,11 +900,12 @@ def module_name(*skip_modules: str, depth: int = 0) -> Optional[str]:
788
900
 
789
901
 
790
902
  def norm_line_sep(text: str) -> str:
903
+ # noinspection GrazieInspection
791
904
  """ convert any combination of line separators in the :paramref:`~norm_line_sep.text` arg to new-line characters.
792
905
 
793
- :param text: string containing any combination of line separators ('\\\\r\\\\n' or '\\\\r').
794
- :return: normalized/converted string with only new-line ('\\\\n') line separator characters.
795
- """
906
+ :param text: string containing any combination of line separators ('\\\\r\\\\n' or '\\\\r').
907
+ :return: normalized/converted string with only new-line ('\\\\n') line separator characters.
908
+ """
796
909
  return text.replace('\r\n', '\n').replace('\r', '\n')
797
910
 
798
911
 
@@ -906,7 +1019,8 @@ def os_local_ip() -> str:
906
1019
  def _os_platform() -> str:
907
1020
  """ determine the operating system where this code is running (used to initialize the :data:`os_platform` variable).
908
1021
 
909
- :return: operating system (extension) as string:
1022
+ :return: operating system (extension) as string. extending Python's :func:`sys.platform`
1023
+ for mobile platforms like Android and iOS:
910
1024
 
911
1025
  * `'android'` for all Android systems.
912
1026
  * `'cygwin'` for MS Windows with an installed Cygwin extension.
@@ -926,7 +1040,7 @@ os_platform = _os_platform()
926
1040
  """ operating system / platform string (see :func:`_os_platform`).
927
1041
 
928
1042
  this string value gets determined for most of the operating systems with the help of Python's :func:`sys.platform`
929
- function and additionally detects the operating systems iOS and Android (not supported by Python).
1043
+ function and additionally detects the operating systems iOS and Android (currently not fully supported by Python).
930
1044
  """
931
1045
 
932
1046
 
@@ -1213,6 +1327,38 @@ def to_ascii(unicode_str: str) -> str:
1213
1327
  return "".join([c for c in nfkd_form if not unicodedata.combining(c)]).replace('ß', "ss").replace('€', "Euro")
1214
1328
 
1215
1329
 
1330
+ def url_failure(url: str, timeout: Optional[float] = None) -> str: # pylint: disable=too-many-return-statements
1331
+ """ determine if and why an FTP or HTTP[S] target is not available via a GET request.
1332
+
1333
+ :param url: URL of an target|page|file to check (not downloaded, fetching only the header).
1334
+ :param timeout: connection timeout in seconds (see :func:`urllib.request.urlopen`).
1335
+ :return: empty string if target header is available, else an error description. if an
1336
+ FTP|HTTP response error occurred then the error/status code
1337
+ will be returned in the first 3 characters.
1338
+ """
1339
+ # noinspection PyBroadException
1340
+ try:
1341
+ with urlopen(url, timeout=timeout) as response: # open connection and read header
1342
+ status = response.getcode() # no need to call response.read()
1343
+ return "" if 200 <= status < 300 else f"{status} {mask_url(url)} {response.reason=}"
1344
+
1345
+ except HTTPError as exception:
1346
+ return f"{exception.code} {mask_url(url)} raised HTTPError {exception.reason=}"
1347
+
1348
+ except URLError as exception:
1349
+ err_prefix = f"996 {mask_url(url)} raised {exception.errno=} {exception.reason=};"
1350
+ if isinstance(exception.reason, socket.gaierror):
1351
+ return f"{err_prefix} could not resolve hostname"
1352
+ if isinstance(exception.reason, socket.timeout):
1353
+ return f"{err_prefix} connection timed out after {timeout} seconds"
1354
+ if isinstance(exception.reason, ssl.SSLCertVerificationError):
1355
+ return f"{err_prefix} SSL certificate verification failed"
1356
+ return f"{err_prefix} could not reach the server"
1357
+
1358
+ except Exception: # pylint: disable=broad-exception-caught
1359
+ return f"999 {mask_url(url)} raised unexpected exception" # NOT put str(_exception) because contains password
1360
+
1361
+
1216
1362
  def utc_datetime() -> datetime.datetime:
1217
1363
  """ return the current UTC timestamp as string (to use as suffix for file and variable/attribute names).
1218
1364
 
@@ -1314,14 +1460,14 @@ class ErrorMsgMixin: # pylint: di
1314
1460
  os_device_id = os_host_name()
1315
1461
  """ user-definable id/name of the device, defaults to os_host_name() on most platforms, alternatives are:
1316
1462
 
1317
- on all platforms:
1318
- - socket.gethostname()
1319
1463
  on Android (check with adb shell 'settings get global device_name' and adb shell 'settings list global'):
1320
1464
  - Settings.Global.DEVICE_NAME (Settings.Global.getString(context.getContentResolver(), "device_name"))
1321
1465
  - android.os.Build.DEVICE/.MANUFACTURER/.BRAND/.HOST
1322
1466
  - DeviceName.getDeviceName()
1323
1467
  on MS Windows:
1324
1468
  - os.environ['COMPUTERNAME']
1469
+ on all other platforms:
1470
+ - socket.gethostname()
1325
1471
  """
1326
1472
  if os_platform == 'android': # pragma: no cover
1327
1473
  # determine Android device id because os_host_name() returns mostly 'localhost' and not the user-definable device id
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ae_base
3
- Version: 0.3.65
4
- Summary: ae namespace module portion base: basic constants, helper functions and context manager
3
+ Version: 0.3.67
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
7
7
  Author-email: aecker2@gmail.com
@@ -36,8 +36,6 @@ Requires-Dist: pytest-cov; extra == "dev"
36
36
  Requires-Dist: pytest-django; extra == "dev"
37
37
  Requires-Dist: typing; extra == "dev"
38
38
  Requires-Dist: types-setuptools; extra == "dev"
39
- Requires-Dist: wheel; extra == "dev"
40
- Requires-Dist: twine; extra == "dev"
41
39
  Provides-Extra: docs
42
40
  Provides-Extra: tests
43
41
  Requires-Dist: anybadge; extra == "tests"
@@ -51,8 +49,6 @@ Requires-Dist: pytest-cov; extra == "tests"
51
49
  Requires-Dist: pytest-django; extra == "tests"
52
50
  Requires-Dist: typing; extra == "tests"
53
51
  Requires-Dist: types-setuptools; extra == "tests"
54
- Requires-Dist: wheel; extra == "tests"
55
- Requires-Dist: twine; extra == "tests"
56
52
  Dynamic: author
57
53
  Dynamic: author-email
58
54
  Dynamic: classifier
@@ -67,19 +63,19 @@ Dynamic: provides-extra
67
63
  Dynamic: requires-python
68
64
  Dynamic: summary
69
65
 
70
- <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.96 -->
66
+ <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.96 -->
71
67
  <!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
72
- # base 0.3.65
68
+ # base 0.3.67
73
69
 
74
70
  [![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_base/develop?logo=python)](
75
71
  https://gitlab.com/ae-group/ae_base)
76
72
  [![LatestPyPIrelease](
77
- https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.64?logo=python)](
78
- https://gitlab.com/ae-group/ae_base/-/tree/release0.3.64)
73
+ https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.66?logo=python)](
74
+ https://gitlab.com/ae-group/ae_base/-/tree/release0.3.66)
79
75
  [![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](
80
76
  https://pypi.org/project/ae-base/#history)
81
77
 
82
- >ae namespace module portion base: basic constants, helper functions and context manager.
78
+ >ae namespace module portion base: basic constants, helper functions and context managers.
83
79
 
84
80
  [![Coverage](https://ae-group.gitlab.io/ae_base/coverage.svg)](
85
81
  https://ae-group.gitlab.io/ae_base/coverage/index.html)
@@ -1,5 +1,6 @@
1
1
  LICENSE.md
2
2
  README.md
3
+ pyproject.toml
3
4
  setup.py
4
5
  ae/base.py
5
6
  ae_base.egg-info/PKG-INFO
@@ -13,8 +13,6 @@ pytest-cov
13
13
  pytest-django
14
14
  typing
15
15
  types-setuptools
16
- wheel
17
- twine
18
16
 
19
17
  [docs]
20
18
 
@@ -30,5 +28,3 @@ pytest-cov
30
28
  pytest-django
31
29
  typing
32
30
  types-setuptools
33
- wheel
34
- twine
@@ -0,0 +1,4 @@
1
+ # THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.project_tpls v0.3.53
2
+ [build-system]
3
+ requires = ["setuptools>=42", "wheel"]
4
+ build-backend = "setuptools.build_meta"
@@ -1,5 +1,5 @@
1
- # THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.project_tpls V0.3.45
2
- """ setup of ae namespace module portion base: basic constants, helper functions and context manager. """
1
+ # THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.project_tpls v0.3.53
2
+ """ setup of ae namespace module portion base: basic constants, helper functions and context managers. """
3
3
  # noinspection PyUnresolvedReferences
4
4
  import sys
5
5
  print(f"SetUp {__name__=} {sys.executable=} {sys.argv=} {sys.path=}")
@@ -14,29 +14,28 @@ setup_kwargs = {
14
14
  'Programming Language :: Python', 'Programming Language :: Python :: 3',
15
15
  'Programming Language :: Python :: 3.9', 'Topic :: Software Development :: Libraries :: Python Modules',
16
16
  'Typing :: Typed'],
17
- 'description': 'ae namespace module portion base: basic constants, helper functions and context manager',
17
+ 'description': 'ae namespace module portion base: basic constants, helper functions and context managers',
18
18
  'extras_require': { 'dev': [ 'aedev_project_tpls', 'ae_ae', 'anybadge', 'coverage-badge', 'aedev_git_repo_manager', 'flake8',
19
- 'mypy', 'pylint', 'pytest', 'pytest-cov', 'pytest-django', 'typing', 'types-setuptools', 'wheel',
20
- 'twine'],
19
+ 'mypy', 'pylint', 'pytest', 'pytest-cov', 'pytest-django', 'typing', 'types-setuptools'],
21
20
  'docs': [],
22
21
  'tests': [ 'anybadge', 'coverage-badge', 'aedev_git_repo_manager', 'flake8', 'mypy', 'pylint', 'pytest',
23
- 'pytest-cov', 'pytest-django', 'typing', 'types-setuptools', 'wheel', 'twine']},
22
+ 'pytest-cov', 'pytest-django', 'typing', 'types-setuptools']},
24
23
  'install_requires': [],
25
24
  'keywords': ['configuration', 'development', 'environment', 'productivity'],
26
25
  'license': 'GPL-3.0-or-later',
27
- 'long_description': ('<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.96 -->\n'
26
+ 'long_description': ('<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae v0.3.96 -->\n'
28
27
  '<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->\n'
29
- '# base 0.3.65\n'
28
+ '# base 0.3.67\n'
30
29
  '\n'
31
30
  '[![GitLab develop](https://img.shields.io/gitlab/pipeline/ae-group/ae_base/develop?logo=python)](\n'
32
31
  ' https://gitlab.com/ae-group/ae_base)\n'
33
32
  '[![LatestPyPIrelease](\n'
34
- ' https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.64?logo=python)](\n'
35
- ' https://gitlab.com/ae-group/ae_base/-/tree/release0.3.64)\n'
33
+ ' https://img.shields.io/gitlab/pipeline/ae-group/ae_base/release0.3.66?logo=python)](\n'
34
+ ' https://gitlab.com/ae-group/ae_base/-/tree/release0.3.66)\n'
36
35
  '[![PyPIVersions](https://img.shields.io/pypi/v/ae_base)](\n'
37
36
  ' https://pypi.org/project/ae-base/#history)\n'
38
37
  '\n'
39
- '>ae namespace module portion base: basic constants, helper functions and context manager.\n'
38
+ '>ae namespace module portion base: basic constants, helper functions and context managers.\n'
40
39
  '\n'
41
40
  '[![Coverage](https://ae-group.gitlab.io/ae_base/coverage.svg)](\n'
42
41
  ' https://ae-group.gitlab.io/ae_base/coverage/index.html)\n'
@@ -108,9 +107,8 @@ setup_kwargs = {
108
107
  'Repository': 'https://gitlab.com/ae-group/ae_base',
109
108
  'Source': 'https://ae.readthedocs.io/en/latest/_modules/ae/base.html'},
110
109
  'python_requires': '>=3.9',
111
- 'setup_requires': [],
112
110
  'url': 'https://gitlab.com/ae-group/ae_base',
113
- 'version': '0.3.65',
111
+ 'version': '0.3.67',
114
112
  'zip_safe': True,
115
113
  }
116
114
 
@@ -1,19 +1,23 @@
1
1
  """ ae.base unit tests """
2
2
  import datetime
3
3
  import os
4
- import string
5
- import tempfile
6
- from unittest.mock import patch
7
-
8
4
  import pytest
9
5
  import shutil
6
+ import socket
7
+ import ssl
8
+ import string
10
9
  import sys
10
+ import tempfile
11
11
  import textwrap
12
12
 
13
13
  from collections import OrderedDict
14
14
  from configparser import ConfigParser
15
+ # noinspection PyProtectedMember
16
+ from http.client import HTTPMessage
15
17
  from types import ModuleType
16
18
  from typing import cast, Any
19
+ from unittest.mock import patch
20
+ from urllib.error import HTTPError, URLError
17
21
 
18
22
  # noinspection PyProtectedMember
19
23
  from ae.base import (
@@ -22,11 +26,10 @@ from ae.base import (
22
26
  app_name_guess, ascii_str, build_config_variable_values, camel_to_snake,
23
27
  dedefuse, deep_dict_update, defuse, dummy_function, duplicates, env_str, evaluate_literal,
24
28
  force_encoding, format_given, full_stack_trace, import_module, instantiate_config_parser, in_wd,
25
- load_env_var_defaults, load_dotenvs, main_file_paths_parts, mask_secrets, module_attr,
26
- module_file_path, module_name, norm_line_sep, norm_name, norm_path, now_str,
27
- os_host_name, os_local_ip, _os_platform, os_user_name,
28
- parse_dotenv, project_main_file, read_file, round_traditional, sign, snake_to_camel,
29
- stack_frames, stack_var, stack_vars, str_ascii, sys_env_dict, sys_env_text, to_ascii, utc_datetime, write_file,
29
+ load_env_var_defaults, load_dotenvs, main_file_paths_parts, mask_secrets, mask_url, module_attr, module_file_path,
30
+ module_name, norm_line_sep, norm_name, norm_path, now_str, os_host_name, os_local_ip, _os_platform, os_user_name,
31
+ parse_dotenv, project_main_file, read_file, round_traditional, sign, snake_to_camel, stack_frames, stack_var,
32
+ stack_vars, str_ascii, sys_env_dict, sys_env_text, to_ascii, url_failure, utc_datetime, write_file,
30
33
  ErrorMsgMixin)
31
34
 
32
35
 
@@ -543,6 +546,11 @@ class TestBaseHelpers:
543
546
  load_dotenvs()
544
547
  assert env_var_name not in os.environ
545
548
 
549
+ def test_load_dotenvs_from_module_path(self, os_env_test_env):
550
+ assert env_var_name not in os.environ
551
+ load_dotenvs(from_module_path=True)
552
+ assert env_var_name not in os.environ
553
+
546
554
  def test_load_env_var_defaults_errors(self):
547
555
  with pytest.raises(TypeError):
548
556
  # noinspection PyArgumentList
@@ -664,6 +672,19 @@ class TestBaseHelpers:
664
672
  assert dat['key1']['subKey1'][1] == untouched
665
673
  assert dat[untouched] == untouched
666
674
 
675
+ def test_mask_url(self):
676
+ assert mask_url("") == ""
677
+
678
+ password, domain, path = "toBeMaskedPassword", "any-not_existing-host_domain.zzz", "any/not/existing/url/path"
679
+
680
+ url = f"https://username:{password}@{domain}/{path}"
681
+ assert password not in mask_url(url)
682
+ assert domain in mask_url(url)
683
+ assert path in mask_url(url)
684
+
685
+ url = f"https://username@{domain}:8081/{path}"
686
+ assert mask_url(url) == url
687
+
667
688
  def test_norm_line_sep(self):
668
689
  assert norm_line_sep('a\r\nb') == 'a\nb'
669
690
  assert norm_line_sep('a\rb') == 'a\nb'
@@ -1044,6 +1065,141 @@ class TestBaseHelpers:
1044
1065
  assert to_ascii('ß') == 'ss'
1045
1066
  assert to_ascii('€') == 'Euro'
1046
1067
 
1068
+ def test_url_failure(self):
1069
+ assert not url_failure("https://gitlab.com/ae-group/ae_base")
1070
+
1071
+ assert not url_failure("https://gitlab.com/ae-group/ae_base.git")
1072
+
1073
+ assert not url_failure("https://www.google.com")
1074
+
1075
+ assert not url_failure(f"https://httpbin.org/status/200")
1076
+
1077
+ def test_url_failure_errors(self):
1078
+ assert url_failure("")
1079
+
1080
+ password, domain, path = "toBeMaskedPassword", "any-not_existing-host_domain.zzz", "any/not/existing/url/path"
1081
+ url = f"https://username:{password}@{domain}/{path}"
1082
+ err_msg = "raised exception error message"
1083
+
1084
+ ret = url_failure(url)
1085
+
1086
+ assert ret
1087
+ assert int(ret[:3]) > 0
1088
+ assert password not in ret
1089
+ assert domain in ret
1090
+ assert path in ret
1091
+
1092
+ ret = url_failure(url2 := f"https://httpbin.org/status/504")
1093
+
1094
+ assert ret
1095
+ assert int(ret[:3]) == 504
1096
+ assert ret[4:].startswith(mask_url(url2))
1097
+
1098
+ ret = url_failure(f"https://httpbin.org/delay/3", timeout=0.9)
1099
+
1100
+ assert ret
1101
+ assert int(ret[:3]) > 0
1102
+
1103
+ ret = url_failure(f"https://expired.badssl.com")
1104
+
1105
+ assert ret
1106
+ assert int(ret[:3]) > 0
1107
+
1108
+ with pytest.raises(AttributeError):
1109
+ url_failure(cast(str, 123456))
1110
+
1111
+ mocked_headers = cast(HTTPMessage, {})
1112
+
1113
+ def mock_raise_http_error404(*_args, **_kwargs):
1114
+ raise HTTPError(url=url, code=404, msg=err_msg, hdrs=mocked_headers, fp=None)
1115
+
1116
+ with patch('ae.base.urlopen', mock_raise_http_error404):
1117
+ ret = url_failure(url)
1118
+
1119
+ assert ret
1120
+ assert int(ret[:3]) > 0
1121
+ assert password not in ret
1122
+ assert domain in ret
1123
+ assert path in ret
1124
+ assert err_msg in ret
1125
+
1126
+ def mock_raise_http_error503(*_args, **_kwargs):
1127
+ raise HTTPError(url=url, code=503, msg=err_msg, hdrs=mocked_headers, fp=None)
1128
+
1129
+ with patch('ae.base.urlopen', mock_raise_http_error503):
1130
+ ret = url_failure(url)
1131
+
1132
+ assert ret
1133
+ assert int(ret[:3]) > 0
1134
+ assert password not in ret
1135
+ assert domain in ret
1136
+ assert path in ret
1137
+ assert err_msg in ret
1138
+
1139
+ def mock_raise_gai_error(*_args, **_kwargs):
1140
+ raise URLError(reason=socket.gaierror(err_msg))
1141
+
1142
+ with patch('ae.base.urlopen', mock_raise_gai_error):
1143
+ ret = url_failure(url)
1144
+
1145
+ assert ret
1146
+ assert int(ret[:3]) > 0
1147
+ assert password not in ret
1148
+ assert domain in ret
1149
+ assert path in ret
1150
+ assert err_msg in ret
1151
+
1152
+ def mock_raise_timeout(*_args, **_kwargs):
1153
+ raise URLError(reason=socket.timeout(err_msg))
1154
+
1155
+ with patch('ae.base.urlopen', mock_raise_timeout):
1156
+ ret = url_failure(url)
1157
+
1158
+ assert ret
1159
+ assert int(ret[:3]) > 0
1160
+ assert password not in ret
1161
+ assert domain in ret
1162
+ assert path in ret
1163
+ assert err_msg in ret
1164
+
1165
+ def mock_raise_ssl_error(*_args, **_kwargs):
1166
+ raise URLError(reason=ssl.SSLCertVerificationError(1, err_msg))
1167
+
1168
+ with patch('ae.base.urlopen', mock_raise_ssl_error):
1169
+ ret = url_failure(url)
1170
+
1171
+ assert ret
1172
+ assert int(ret[:3]) > 0
1173
+ assert password not in ret
1174
+ assert domain in ret
1175
+ assert path in ret
1176
+ assert err_msg in ret
1177
+
1178
+ def mock_raise_generic_url_error(*_args, **_kwargs):
1179
+ raise URLError(reason=err_msg)
1180
+
1181
+ with patch('ae.base.urlopen', mock_raise_generic_url_error):
1182
+ ret = url_failure(url)
1183
+
1184
+ assert ret
1185
+ assert int(ret[:3]) > 0
1186
+ assert password not in ret
1187
+ assert domain in ret
1188
+ assert path in ret
1189
+ assert err_msg in ret
1190
+
1191
+ def mock_raise_unexpected_error(*_args, **_kwargs):
1192
+ raise ValueError(err_msg)
1193
+
1194
+ with patch('ae.base.urlopen', mock_raise_unexpected_error):
1195
+ ret = url_failure(url)
1196
+
1197
+ assert ret
1198
+ assert int(ret[:3]) > 0
1199
+ assert password not in ret
1200
+ assert domain in ret
1201
+ assert path in ret
1202
+
1047
1203
  def test_utc_datetime(self):
1048
1204
  dt1 = utc_datetime()
1049
1205
  dt2 = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
File without changes