ae-base 0.3.49__tar.gz → 0.3.51__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.49/ae_base.egg-info → ae_base-0.3.51}/PKG-INFO +7 -6
- {ae_base-0.3.49 → ae_base-0.3.51}/README.md +4 -4
- {ae_base-0.3.49 → ae_base-0.3.51}/ae/base.py +80 -15
- {ae_base-0.3.49 → ae_base-0.3.51/ae_base.egg-info}/PKG-INFO +7 -6
- {ae_base-0.3.49 → ae_base-0.3.51}/tests/test_base.py +38 -4
- {ae_base-0.3.49 → ae_base-0.3.51}/LICENSE.md +0 -0
- {ae_base-0.3.49 → ae_base-0.3.51}/ae_base.egg-info/SOURCES.txt +0 -0
- {ae_base-0.3.49 → ae_base-0.3.51}/ae_base.egg-info/dependency_links.txt +0 -0
- {ae_base-0.3.49 → ae_base-0.3.51}/ae_base.egg-info/requires.txt +0 -0
- {ae_base-0.3.49 → ae_base-0.3.51}/ae_base.egg-info/top_level.txt +0 -0
- {ae_base-0.3.49 → ae_base-0.3.51}/ae_base.egg-info/zip-safe +0 -0
- {ae_base-0.3.49 → ae_base-0.3.51}/setup.cfg +0 -0
- {ae_base-0.3.49 → ae_base-0.3.51}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: ae_base
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.51
|
|
4
4
|
Summary: ae namespace module portion base: basic constants, helper functions and context manager
|
|
5
5
|
Home-page: https://gitlab.com/ae-group/ae_base
|
|
6
6
|
Author: AndiEcker
|
|
@@ -61,6 +61,7 @@ Dynamic: description-content-type
|
|
|
61
61
|
Dynamic: home-page
|
|
62
62
|
Dynamic: keywords
|
|
63
63
|
Dynamic: license
|
|
64
|
+
Dynamic: license-file
|
|
64
65
|
Dynamic: project-url
|
|
65
66
|
Dynamic: provides-extra
|
|
66
67
|
Dynamic: requires-python
|
|
@@ -68,17 +69,17 @@ Dynamic: summary
|
|
|
68
69
|
|
|
69
70
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.95 -->
|
|
70
71
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
|
|
71
|
-
# base 0.3.
|
|
72
|
+
# base 0.3.51
|
|
72
73
|
|
|
73
74
|
[](
|
|
74
75
|
https://gitlab.com/ae-group/ae_base)
|
|
75
76
|
[](
|
|
78
|
+
https://gitlab.com/ae-group/ae_base/-/tree/release0.3.50)
|
|
78
79
|
[](
|
|
79
80
|
https://pypi.org/project/ae-base/#history)
|
|
80
81
|
|
|
81
|
-
>ae_base module 0.3.
|
|
82
|
+
>ae_base module 0.3.51.
|
|
82
83
|
|
|
83
84
|
[](
|
|
84
85
|
https://ae-group.gitlab.io/ae_base/coverage/index.html)
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.95 -->
|
|
2
2
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
|
|
3
|
-
# base 0.3.
|
|
3
|
+
# base 0.3.51
|
|
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.50)
|
|
10
10
|
[](
|
|
11
11
|
https://pypi.org/project/ae-base/#history)
|
|
12
12
|
|
|
13
|
-
>ae_base module 0.3.
|
|
13
|
+
>ae_base module 0.3.51.
|
|
14
14
|
|
|
15
15
|
[](
|
|
16
16
|
https://ae-group.gitlab.io/ae_base/coverage/index.html)
|
|
@@ -40,6 +40,9 @@ and :func:`write_file` are wrapping Python's built-in :func:`open` function and
|
|
|
40
40
|
|
|
41
41
|
the function :func:`duplicates` returns the duplicates of an iterable type.
|
|
42
42
|
|
|
43
|
+
in order to hide/mask secrets like credit card numbers, passwords or tokens in deeply nested data structures,
|
|
44
|
+
before they get dumped e.g. to an app log file, the function :func:`mask_secrets` can be used.
|
|
45
|
+
|
|
43
46
|
:func:`norm_line_sep` is converting any combination of line separators of a string to a single new-line character.
|
|
44
47
|
|
|
45
48
|
:func:`norm_name` converts any string into a name that can be used e.g. as file name or as method/attribute name.
|
|
@@ -157,10 +160,10 @@ from contextlib import contextmanager
|
|
|
157
160
|
from importlib.machinery import ModuleSpec
|
|
158
161
|
from inspect import getinnerframes, getouterframes, getsourcefile
|
|
159
162
|
from types import ModuleType
|
|
160
|
-
from typing import Any, Callable,
|
|
163
|
+
from typing import Any, Callable, Generator, Iterable, Optional, Union, cast
|
|
161
164
|
|
|
162
165
|
|
|
163
|
-
__version__ = '0.3.
|
|
166
|
+
__version__ = '0.3.51'
|
|
164
167
|
|
|
165
168
|
|
|
166
169
|
os_path_abspath = os.path.abspath
|
|
@@ -272,7 +275,7 @@ def app_name_guess() -> str:
|
|
|
272
275
|
return defuse(app_name)
|
|
273
276
|
|
|
274
277
|
|
|
275
|
-
def build_config_variable_values(*names_defaults:
|
|
278
|
+
def build_config_variable_values(*names_defaults: tuple[str, Any], section: str = 'app') -> tuple[Any, ...]:
|
|
276
279
|
""" determine build config variable values from the ``buildozer.spec`` file in the current directory.
|
|
277
280
|
|
|
278
281
|
:param names_defaults: tuple of tuples of build config variable names and default values.
|
|
@@ -634,7 +637,7 @@ def load_env_var_defaults(start_dir: str):
|
|
|
634
637
|
file_path = os_path_join(os_path_dirname(os_path_dirname(file_path)), DOTENV_FILE_NAME)
|
|
635
638
|
|
|
636
639
|
|
|
637
|
-
def main_file_paths_parts(portion_name: str) ->
|
|
640
|
+
def main_file_paths_parts(portion_name: str) -> tuple[tuple[str, ...], ...]:
|
|
638
641
|
""" determine tuple of supported main/version file name path part tuples.
|
|
639
642
|
|
|
640
643
|
:param portion_name: portion or package name.
|
|
@@ -650,6 +653,29 @@ def main_file_paths_parts(portion_name: str) -> Tuple[Tuple[str, ...], ...]:
|
|
|
650
653
|
)
|
|
651
654
|
|
|
652
655
|
|
|
656
|
+
def mask_secrets(data: Union[dict, Iterable], fragments: Iterable[str] = ('password', 'pwd')) -> Union[dict, Iterable]:
|
|
657
|
+
""" partially-hide secret string values like passwords/credit-card-numbers in deeply nestable data structures.
|
|
658
|
+
|
|
659
|
+
:param data: iterable deep data structure wherein its item values get masked if their related dict
|
|
660
|
+
item key contains one of the fragments specified in :paramref:`~mask_secrets.fragments`.
|
|
661
|
+
:param fragments: dict key string fragments of which the related value will be masked. each fragment has
|
|
662
|
+
to be specified in lower case! defaults to ('password', 'pwd') if not passed.
|
|
663
|
+
:return: specified data structure with the secrets masked (¡in-place!).
|
|
664
|
+
"""
|
|
665
|
+
is_dict = isinstance(data, dict)
|
|
666
|
+
|
|
667
|
+
for idx, val in tuple(data.items()) if is_dict else enumerate(data): # type: ignore # silly mypy not sees is_dict
|
|
668
|
+
val_is_str = isinstance(val, str)
|
|
669
|
+
if not val_is_str and isinstance(val, Iterable):
|
|
670
|
+
mask_secrets(val, fragments=fragments)
|
|
671
|
+
elif is_dict and val_is_str and isinstance(idx, str):
|
|
672
|
+
idx = idx.lower()
|
|
673
|
+
if any(_frag in idx for _frag in fragments):
|
|
674
|
+
data[idx] = val[:3] + "*" * 9 # type: ignore # silly mypy not sees is_dict
|
|
675
|
+
|
|
676
|
+
return data
|
|
677
|
+
|
|
678
|
+
|
|
653
679
|
def module_attr(import_name: str, attr_name: str = "") -> Optional[Any]:
|
|
654
680
|
""" determine dynamically a reference to a module or to any attribute (variable/func/class) declared in the module.
|
|
655
681
|
|
|
@@ -722,7 +748,7 @@ def norm_name(name: str, allow_num_prefix: bool = False) -> str:
|
|
|
722
748
|
:param allow_num_prefix: pass True to allow leading digits in the returned normalized name.
|
|
723
749
|
:return: cleaned/normalized/converted name string (e.g. for a variable-/method-/file-name).
|
|
724
750
|
"""
|
|
725
|
-
str_parts:
|
|
751
|
+
str_parts: list[str] = []
|
|
726
752
|
for char in name:
|
|
727
753
|
if char.isalpha() or char.isalnum() and (allow_num_prefix or str_parts):
|
|
728
754
|
str_parts.append(char)
|
|
@@ -855,13 +881,13 @@ def os_user_name() -> str:
|
|
|
855
881
|
return getpass.getuser()
|
|
856
882
|
|
|
857
883
|
|
|
858
|
-
def parse_dotenv(file_path: str) ->
|
|
884
|
+
def parse_dotenv(file_path: str) -> dict[str, str]:
|
|
859
885
|
""" parse ``.env`` file content and return environment variable names as dict keys and values as dict values.
|
|
860
886
|
|
|
861
887
|
:param file_path: string with the name/path of an existing ``.env``/:data:`DOTENV_FILE_NAME` file.
|
|
862
888
|
:return: dict with environment variable names and values
|
|
863
889
|
"""
|
|
864
|
-
env_vars:
|
|
890
|
+
env_vars: dict[str, str] = {}
|
|
865
891
|
for line in cast(str, read_file(file_path)).splitlines():
|
|
866
892
|
match = _env_line.search(line)
|
|
867
893
|
if not match:
|
|
@@ -1017,7 +1043,7 @@ def stack_var(name: str, *skip_modules: str, scope: str = '', depth: int = 1) ->
|
|
|
1017
1043
|
|
|
1018
1044
|
def stack_vars(*skip_modules: str,
|
|
1019
1045
|
find_name: str = '', min_depth: int = 1, max_depth: int = 0, scope: str = ''
|
|
1020
|
-
) ->
|
|
1046
|
+
) -> tuple[dict[str, Any], dict[str, Any], int]:
|
|
1021
1047
|
""" determine all global and local variables in a calling stack/frames.
|
|
1022
1048
|
|
|
1023
1049
|
:param skip_modules: module names to skip (def=see :data:`SKIPPED_MODULES` module constant).
|
|
@@ -1057,7 +1083,7 @@ def stack_vars(*skip_modules: str,
|
|
|
1057
1083
|
return glo.copy(), loc, depth - 1
|
|
1058
1084
|
|
|
1059
1085
|
|
|
1060
|
-
def sys_env_dict() ->
|
|
1086
|
+
def sys_env_dict() -> dict[str, Any]:
|
|
1061
1087
|
""" returns dict with python system run-time environment values.
|
|
1062
1088
|
|
|
1063
1089
|
:return: python system run-time environment values like python_ver, argv, cwd, executable,
|
|
@@ -1065,16 +1091,18 @@ def sys_env_dict() -> Dict[str, Any]:
|
|
|
1065
1091
|
|
|
1066
1092
|
.. hint:: see also https://pyinstaller.readthedocs.io/en/stable/runtime-information.html
|
|
1067
1093
|
"""
|
|
1068
|
-
sed:
|
|
1069
|
-
'
|
|
1094
|
+
sed: dict[str, Any] = {
|
|
1095
|
+
'python ver': sys.version.replace('\n', ' '),
|
|
1070
1096
|
'platform': os_platform,
|
|
1071
1097
|
'argv': sys.argv,
|
|
1072
1098
|
'executable': sys.executable,
|
|
1073
1099
|
'cwd': os.getcwd(),
|
|
1074
1100
|
'frozen': getattr(sys, 'frozen', False),
|
|
1075
|
-
'
|
|
1076
|
-
'
|
|
1101
|
+
'user name': os_user_name(),
|
|
1102
|
+
'host name': os_host_name(),
|
|
1103
|
+
'device id': os_device_id,
|
|
1077
1104
|
'app_name_guess': app_name_guess(),
|
|
1105
|
+
'os env': mask_secrets(os.environ.copy()),
|
|
1078
1106
|
}
|
|
1079
1107
|
|
|
1080
1108
|
if sed['frozen']:
|
|
@@ -1084,7 +1112,7 @@ def sys_env_dict() -> Dict[str, Any]:
|
|
|
1084
1112
|
|
|
1085
1113
|
|
|
1086
1114
|
def sys_env_text(ind_ch: str = " ", ind_len: int = 12, key_ch: str = "=", key_len: int = 15,
|
|
1087
|
-
extra_sys_env_dict: Optional[
|
|
1115
|
+
extra_sys_env_dict: Optional[dict[str, str]] = None) -> str:
|
|
1088
1116
|
""" compile formatted text block with system environment info.
|
|
1089
1117
|
|
|
1090
1118
|
:param ind_ch: indent character (defaults to " ").
|
|
@@ -1220,7 +1248,39 @@ class ErrorMsgMixin:
|
|
|
1220
1248
|
self._err_msg = ""
|
|
1221
1249
|
|
|
1222
1250
|
|
|
1223
|
-
|
|
1251
|
+
# platform-specific patches
|
|
1252
|
+
os_device_id = os_host_name()
|
|
1253
|
+
""" user-definable id/name of the device, defaults to os_host_name() on most platforms, alternatives are:
|
|
1254
|
+
|
|
1255
|
+
on all platforms:
|
|
1256
|
+
- socket.gethostname()
|
|
1257
|
+
on Android (check with adb shell 'settings get global device_name' and adb shell 'settings list global'):
|
|
1258
|
+
- Settings.Global.DEVICE_NAME (Settings.Global.getString(context.getContentResolver(), "device_name"))
|
|
1259
|
+
- android.os.Build.DEVICE/.MANUFACTURER/.BRAND/.HOST
|
|
1260
|
+
- DeviceName.getDeviceName()
|
|
1261
|
+
on MS Windows:
|
|
1262
|
+
- os.environ['COMPUTERNAME']
|
|
1263
|
+
"""
|
|
1264
|
+
if os_platform == 'android': # pragma: no cover
|
|
1265
|
+
# determine Android device id because os_host_name() returns mostly 'localhost' and not the user-definable device id
|
|
1266
|
+
from jnius import autoclass # type: ignore
|
|
1267
|
+
|
|
1268
|
+
# noinspection PyBroadException
|
|
1269
|
+
try:
|
|
1270
|
+
Settings = autoclass('android.provider.Settings$Global')
|
|
1271
|
+
PythonActivity = autoclass('org.kivy.android.PythonActivity')
|
|
1272
|
+
|
|
1273
|
+
# mActivity inherits from Context so no need to cast('android.content.Context',..) neither get app context
|
|
1274
|
+
# _Context = autoclass('android.content.Context')
|
|
1275
|
+
# context = cast('android.content.Context', PythonActivity.mActivity)
|
|
1276
|
+
# context = PythonActivity.mActivity.getApplicationContext()
|
|
1277
|
+
context = PythonActivity.mActivity
|
|
1278
|
+
if _dev_id := Settings.getString(context.getContentResolver(), 'device_name'):
|
|
1279
|
+
os_device_id = defuse(_dev_id)
|
|
1280
|
+
|
|
1281
|
+
except Exception:
|
|
1282
|
+
pass
|
|
1283
|
+
|
|
1224
1284
|
# monkey patch the :func:`shutil.copystat` and :func:`shutil.copymode` helper functions, which are crashing on
|
|
1225
1285
|
# 'android' (see # `<https://bugs.python.org/issue28141>`__ and `<https://bugs.python.org/issue32073>`__). these
|
|
1226
1286
|
# functions are used by shutil.copy2/copy/copytree/move to copy OS-specific file attributes.
|
|
@@ -1229,3 +1289,8 @@ if os_platform == 'android': # pragma: no cov
|
|
|
1229
1289
|
# on the destination root directory.
|
|
1230
1290
|
shutil.copymode = dummy_function
|
|
1231
1291
|
shutil.copystat = dummy_function
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
elif os_platform in ('win32', 'cygwin'): # pragma: no cover
|
|
1295
|
+
if _dev_id := os.environ.get('COMPUTERNAME'):
|
|
1296
|
+
os_device_id = defuse(_dev_id)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: ae_base
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.51
|
|
4
4
|
Summary: ae namespace module portion base: basic constants, helper functions and context manager
|
|
5
5
|
Home-page: https://gitlab.com/ae-group/ae_base
|
|
6
6
|
Author: AndiEcker
|
|
@@ -61,6 +61,7 @@ Dynamic: description-content-type
|
|
|
61
61
|
Dynamic: home-page
|
|
62
62
|
Dynamic: keywords
|
|
63
63
|
Dynamic: license
|
|
64
|
+
Dynamic: license-file
|
|
64
65
|
Dynamic: project-url
|
|
65
66
|
Dynamic: provides-extra
|
|
66
67
|
Dynamic: requires-python
|
|
@@ -68,17 +69,17 @@ Dynamic: summary
|
|
|
68
69
|
|
|
69
70
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project ae.ae V0.3.95 -->
|
|
70
71
|
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.tpl_namespace_root V0.3.14 -->
|
|
71
|
-
# base 0.3.
|
|
72
|
+
# base 0.3.51
|
|
72
73
|
|
|
73
74
|
[](
|
|
74
75
|
https://gitlab.com/ae-group/ae_base)
|
|
75
76
|
[](
|
|
78
|
+
https://gitlab.com/ae-group/ae_base/-/tree/release0.3.50)
|
|
78
79
|
[](
|
|
79
80
|
https://pypi.org/project/ae-base/#history)
|
|
80
81
|
|
|
81
|
-
>ae_base module 0.3.
|
|
82
|
+
>ae_base module 0.3.51.
|
|
82
83
|
|
|
83
84
|
[](
|
|
84
85
|
https://ae-group.gitlab.io/ae_base/coverage/index.html)
|
|
@@ -22,8 +22,9 @@ from ae.base import (
|
|
|
22
22
|
URI_SEP_CHAR, app_name_guess, build_config_variable_values, camel_to_snake, deep_dict_update, dummy_function,
|
|
23
23
|
duplicates, env_str,
|
|
24
24
|
dedefuse, 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,
|
|
26
|
-
norm_line_sep, norm_name, norm_path, now_str,
|
|
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,
|
|
27
28
|
parse_dotenv, project_main_file, read_file, round_traditional, snake_to_camel, stack_frames, stack_var, stack_vars,
|
|
28
29
|
sys_env_dict, sys_env_text, to_ascii, defuse, utc_datetime, write_file, ErrorMsgMixin)
|
|
29
30
|
|
|
@@ -506,6 +507,39 @@ class TestBaseHelpers:
|
|
|
506
507
|
assert ('main', PY_INIT) in main_file_paths_parts("")
|
|
507
508
|
assert (por_name, PY_INIT) in main_file_paths_parts(por_name)
|
|
508
509
|
|
|
510
|
+
def test_mask_secrets(self):
|
|
511
|
+
assert mask_secrets({}) == {}
|
|
512
|
+
assert mask_secrets([]) == []
|
|
513
|
+
assert mask_secrets(tuple()) == ()
|
|
514
|
+
assert mask_secrets("") == ""
|
|
515
|
+
|
|
516
|
+
assert mask_secrets({'password': "secret"}) == {'password': "sec*********"}
|
|
517
|
+
assert mask_secrets([{'pwd': "secret"}, "any"]) == [{'pwd': "sec*********"}, "any"]
|
|
518
|
+
|
|
519
|
+
assert mask_secrets({'secret': "secret"}, fragments=('token', 'secret')) == {'secret': "sec*********"}
|
|
520
|
+
assert mask_secrets({'_token': "secret"}, fragments=('token', 'secret')) == {'_token': "sec*********"}
|
|
521
|
+
assert mask_secrets({'_token': "secret"}, fragments=('TOKEN', 'secret')) == {'_token': "secret"}
|
|
522
|
+
|
|
523
|
+
untouched = 'untouched_pw_p_a_s_s_word'
|
|
524
|
+
dat = {'key1':
|
|
525
|
+
{'subkey1':
|
|
526
|
+
(
|
|
527
|
+
{'host_Pwd': "secret"},
|
|
528
|
+
untouched,
|
|
529
|
+
),
|
|
530
|
+
'passWord___': "secRet",
|
|
531
|
+
},
|
|
532
|
+
'any_PASSWORD_to_hide': "Se",
|
|
533
|
+
untouched: untouched,
|
|
534
|
+
}
|
|
535
|
+
assert mask_secrets(dat) is dat
|
|
536
|
+
assert dat['key1']['subkey1'][0]['host_pwd'] == "sec*********"
|
|
537
|
+
assert dat['key1']['password___'] == "sec*********"
|
|
538
|
+
assert dat['any_password_to_hide'] == "Se*********"
|
|
539
|
+
|
|
540
|
+
assert dat['key1']['subkey1'][1] == untouched
|
|
541
|
+
assert dat[untouched] == untouched
|
|
542
|
+
|
|
509
543
|
def test_norm_line_sep(self):
|
|
510
544
|
assert norm_line_sep('a\r\nb') == 'a\nb'
|
|
511
545
|
assert norm_line_sep('a\rb') == 'a\nb'
|
|
@@ -792,7 +826,7 @@ class TestBaseHelpers:
|
|
|
792
826
|
assert snake_to_camel("@special/chars!") == "@special/chars!"
|
|
793
827
|
|
|
794
828
|
def test_sys_env_dict(self):
|
|
795
|
-
assert sys_env_dict().get('
|
|
829
|
+
assert sys_env_dict().get('python ver')
|
|
796
830
|
assert sys_env_dict().get('cwd')
|
|
797
831
|
assert sys_env_dict().get('frozen') is False
|
|
798
832
|
|
|
@@ -805,7 +839,7 @@ class TestBaseHelpers:
|
|
|
805
839
|
|
|
806
840
|
def test_sys_env_text(self):
|
|
807
841
|
assert isinstance(sys_env_text(), str)
|
|
808
|
-
assert '
|
|
842
|
+
assert 'python ver' in sys_env_text()
|
|
809
843
|
ret = sys_env_text(extra_sys_env_dict=dict(test_add='TstAdd'))
|
|
810
844
|
assert 'test_add' in ret
|
|
811
845
|
assert 'TstAdd' in ret
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|