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.
- {ae_base-0.3.69/ae_base.egg-info → ae_base-0.3.71}/PKG-INFO +6 -6
- {ae_base-0.3.69 → ae_base-0.3.71}/README.md +5 -5
- {ae_base-0.3.69 → ae_base-0.3.71}/ae/base.py +44 -14
- {ae_base-0.3.69 → ae_base-0.3.71/ae_base.egg-info}/PKG-INFO +6 -6
- {ae_base-0.3.69 → ae_base-0.3.71}/setup.py +6 -6
- {ae_base-0.3.69 → ae_base-0.3.71}/tests/test_base.py +74 -21
- {ae_base-0.3.69 → ae_base-0.3.71}/LICENSE.md +0 -0
- {ae_base-0.3.69 → ae_base-0.3.71}/ae_base.egg-info/SOURCES.txt +0 -0
- {ae_base-0.3.69 → ae_base-0.3.71}/ae_base.egg-info/dependency_links.txt +0 -0
- {ae_base-0.3.69 → ae_base-0.3.71}/ae_base.egg-info/requires.txt +0 -0
- {ae_base-0.3.69 → ae_base-0.3.71}/ae_base.egg-info/top_level.txt +0 -0
- {ae_base-0.3.69 → ae_base-0.3.71}/ae_base.egg-info/zip-safe +0 -0
- {ae_base-0.3.69 → ae_base-0.3.71}/pyproject.toml +0 -0
- {ae_base-0.3.69 → ae_base-0.3.71}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ae_base
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.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.
|
|
67
|
-
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.
|
|
68
|
-
# base 0.3.
|
|
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
|
[](
|
|
71
71
|
https://gitlab.com/ae-group/ae_base)
|
|
72
72
|
[](
|
|
74
|
+
https://gitlab.com/ae-group/ae_base/-/tree/release0.3.71)
|
|
75
75
|
[](
|
|
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.
|
|
2
|
-
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.
|
|
3
|
-
# base 0.3.
|
|
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
|
[](
|
|
6
6
|
https://gitlab.com/ae-group/ae_base)
|
|
7
7
|
[](
|
|
9
|
+
https://gitlab.com/ae-group/ae_base/-/tree/release0.3.71)
|
|
10
10
|
[](
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1365
|
+
err_msg = f" {mask_url(url)} raised {exception.errno=} {exception.reason=};"
|
|
1339
1366
|
if isinstance(exception.reason, socket.gaierror):
|
|
1340
|
-
return f"{
|
|
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"{
|
|
1345
|
-
|
|
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:
|
|
1348
|
-
return f"
|
|
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.
|
|
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.
|
|
67
|
-
<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.
|
|
68
|
-
# base 0.3.
|
|
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
|
[](
|
|
71
71
|
https://gitlab.com/ae-group/ae_base)
|
|
72
72
|
[](
|
|
74
|
+
https://gitlab.com/ae-group/ae_base/-/tree/release0.3.71)
|
|
75
75
|
[](
|
|
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.
|
|
27
|
-
'<!-- THIS FILE IS EXCLUSIVELY MAINTAINED by the project aedev.
|
|
28
|
-
'# base 0.3.
|
|
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
|
'[](\n'
|
|
31
31
|
' https://gitlab.com/ae-group/ae_base)\n'
|
|
32
32
|
'[](\n'
|
|
34
|
+
' https://gitlab.com/ae-group/ae_base/-/tree/release0.3.71)\n'
|
|
35
35
|
'[](\n'
|
|
36
36
|
' https://pypi.org/project/ae-base/#history)\n'
|
|
37
37
|
'\n'
|
|
@@ -108,7 +108,7 @@ setup_kwargs = {
|
|
|
108
108
|
'Source': 'https://ae.readthedocs.io/en/latest/_modules/ae/base.html'},
|
|
109
109
|
'python_requires': '>=3.9',
|
|
110
110
|
'url': 'https://gitlab.com/ae-group/ae_base',
|
|
111
|
-
'version': '0.3.
|
|
111
|
+
'version': '0.3.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
|
|
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://
|
|
1106
|
+
assert not url_failure("https://gitlab.com/ae-group/ae_base", git_repo=True)
|
|
1076
1107
|
|
|
1077
|
-
assert not url_failure(
|
|
1108
|
+
assert not url_failure("https://www.google.com")
|
|
1078
1109
|
|
|
1079
|
-
|
|
1080
|
-
assert
|
|
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(
|
|
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(
|
|
1134
|
+
ret = url_failure(url)
|
|
1106
1135
|
|
|
1107
1136
|
assert ret
|
|
1108
1137
|
assert int(ret[:3]) > 0
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|