python3-commons 0.16.0__tar.gz → 0.16.2__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.
- {python3_commons-0.16.0 → python3_commons-0.16.2}/PKG-INFO +1 -1
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/db/__init__.py +13 -13
- python3_commons-0.16.2/src/python3_commons/log/formatters.py +125 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/object_storage.py +4 -4
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons.egg-info/PKG-INFO +1 -1
- {python3_commons-0.16.0 → python3_commons-0.16.2}/uv.lock +3 -3
- python3_commons-0.16.0/src/python3_commons/log/formatters.py +0 -35
- {python3_commons-0.16.0 → python3_commons-0.16.2}/.coveragerc +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/.devcontainer/Dockerfile +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/.devcontainer/devcontainer.json +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/.devcontainer/docker-compose.yml +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/.env_template +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/.github/workflows/checks.yml +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/.github/workflows/python-publish.yaml +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/.github/workflows/release-on-tag-push.yml +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/.gitignore +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/.pre-commit-config.yaml +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/.python-version +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/AUTHORS.rst +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/CHANGELOG.rst +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/LICENSE +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/README.md +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/README.rst +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/docs/Makefile +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/docs/_static/.gitignore +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/docs/authors.rst +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/docs/changelog.rst +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/docs/conf.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/docs/index.rst +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/docs/license.rst +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/pyproject.toml +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/setup.cfg +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/__init__.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/api_client.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/async_functools.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/audit.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/auth.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/cache.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/conf.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/db/helpers.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/db/models/__init__.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/db/models/auth.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/db/models/common.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/db/models/rbac.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/db/models/users.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/exceptions.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/fs.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/generators.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/helpers.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/log/__init__.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/log/filters.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/permissions.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/serializers/__init__.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/serializers/common.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/serializers/json.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/serializers/msgpack.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/serializers/msgspec.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons.egg-info/SOURCES.txt +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons.egg-info/dependency_links.txt +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons.egg-info/requires.txt +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons.egg-info/top_level.txt +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/tests/__init__.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/tests/integration/__init__.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/tests/integration/test_cache.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/tests/integration/test_osc.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/tests/unit/__init__.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/tests/unit/conftest.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/tests/unit/log/__init__.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/tests/unit/log/test_formatters.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/tests/unit/test_async_functools.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/tests/unit/test_audit.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/tests/unit/test_helpers.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/tests/unit/test_msgpack.py +0 -0
- {python3_commons-0.16.0 → python3_commons-0.16.2}/tests/unit/test_msgspec.py +0 -0
|
@@ -17,28 +17,28 @@ Base = declarative_base(metadata=metadata)
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class AsyncSessionManager:
|
|
20
|
-
def __init__(self,
|
|
21
|
-
self.
|
|
20
|
+
def __init__(self, db_configs: Mapping[str, DBSettings]) -> None:
|
|
21
|
+
self.db_configs: Mapping[str, DBSettings] = db_configs
|
|
22
22
|
self.engines: dict[str, AsyncEngine] = {}
|
|
23
23
|
self.session_makers: dict = {}
|
|
24
24
|
|
|
25
|
-
def
|
|
25
|
+
def get_db_config(self, name: str) -> DBSettings:
|
|
26
26
|
try:
|
|
27
|
-
return self.
|
|
27
|
+
return self.db_configs[name]
|
|
28
28
|
except KeyError:
|
|
29
29
|
logger.exception('Missing database settings: %s', name)
|
|
30
30
|
|
|
31
31
|
raise
|
|
32
32
|
|
|
33
|
-
def
|
|
34
|
-
|
|
33
|
+
def async_engine_from_db_config(self, name):
|
|
34
|
+
db_config = self.get_db_config(name)
|
|
35
35
|
configuration = {
|
|
36
|
-
'url': str(
|
|
37
|
-
'echo':
|
|
38
|
-
'pool_size':
|
|
39
|
-
'max_overflow':
|
|
40
|
-
'pool_timeout':
|
|
41
|
-
'pool_recycle':
|
|
36
|
+
'url': str(db_config.dsn),
|
|
37
|
+
'echo': db_config.echo,
|
|
38
|
+
'pool_size': db_config.pool_size,
|
|
39
|
+
'max_overflow': db_config.max_overflow,
|
|
40
|
+
'pool_timeout': db_config.pool_timeout,
|
|
41
|
+
'pool_recycle': db_config.pool_recycle,
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
return async_engine_from_config(configuration, prefix='')
|
|
@@ -48,7 +48,7 @@ class AsyncSessionManager:
|
|
|
48
48
|
engine = self.engines[name]
|
|
49
49
|
except KeyError:
|
|
50
50
|
logger.debug('Creating engine: %s', name)
|
|
51
|
-
engine = self.
|
|
51
|
+
engine = self.async_engine_from_db_config(name)
|
|
52
52
|
self.engines[name] = engine
|
|
53
53
|
|
|
54
54
|
return engine
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import traceback
|
|
3
|
+
from contextvars import ContextVar
|
|
4
|
+
from datetime import UTC, datetime, date
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from typing import Any, Final, Callable
|
|
7
|
+
|
|
8
|
+
import msgspec
|
|
9
|
+
|
|
10
|
+
correlation_id: ContextVar[str | None] = ContextVar('correlation_id', default=None)
|
|
11
|
+
|
|
12
|
+
_DEFAULT_MAX_TB_CHARS: Final[int] = 8_000
|
|
13
|
+
_STD_LOG_FIELDS: Final[frozenset[str]] = frozenset(
|
|
14
|
+
{
|
|
15
|
+
'msg',
|
|
16
|
+
'args',
|
|
17
|
+
'levelname',
|
|
18
|
+
'levelno',
|
|
19
|
+
'pathname',
|
|
20
|
+
'filename',
|
|
21
|
+
'module',
|
|
22
|
+
'exc_info',
|
|
23
|
+
'exc_text',
|
|
24
|
+
'stack_info',
|
|
25
|
+
'lineno',
|
|
26
|
+
'funcName',
|
|
27
|
+
'created',
|
|
28
|
+
'msecs',
|
|
29
|
+
'relativeCreated',
|
|
30
|
+
'thread',
|
|
31
|
+
'threadName',
|
|
32
|
+
'process',
|
|
33
|
+
'processName',
|
|
34
|
+
'name',
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Keep normalization minimal and branch-based (unchanged)
|
|
40
|
+
def _normalize(v: Any) -> Any:
|
|
41
|
+
if isinstance(v, datetime | date):
|
|
42
|
+
return v.isoformat()
|
|
43
|
+
|
|
44
|
+
if isinstance(v, bytes):
|
|
45
|
+
return v.decode('utf-8', errors='replace')
|
|
46
|
+
|
|
47
|
+
if isinstance(v, Decimal):
|
|
48
|
+
return str(v)
|
|
49
|
+
|
|
50
|
+
if isinstance(v, Exception):
|
|
51
|
+
return str(v)
|
|
52
|
+
|
|
53
|
+
return v
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class JSONFormatter(logging.Formatter):
|
|
57
|
+
__slots__ = ('_get_correlation_id', '_max_tb_chars', '_encoder')
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
get_correlation_id: Callable[[], str | None] = lambda: correlation_id.get(),
|
|
63
|
+
max_exc_tb_chars: int = _DEFAULT_MAX_TB_CHARS,
|
|
64
|
+
**kwargs: Any,
|
|
65
|
+
) -> None:
|
|
66
|
+
super().__init__(**kwargs)
|
|
67
|
+
|
|
68
|
+
self._get_correlation_id = get_correlation_id
|
|
69
|
+
self._max_tb_chars = max_exc_tb_chars
|
|
70
|
+
self._encoder = msgspec.json.Encoder()
|
|
71
|
+
|
|
72
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
73
|
+
# -------------------------
|
|
74
|
+
# message (fast path)
|
|
75
|
+
# -------------------------
|
|
76
|
+
try:
|
|
77
|
+
message = record.getMessage()
|
|
78
|
+
except Exception:
|
|
79
|
+
message = str(record.msg)
|
|
80
|
+
|
|
81
|
+
# -------------------------
|
|
82
|
+
# timestamp (no Formatter overhead)
|
|
83
|
+
# -------------------------
|
|
84
|
+
timestamp = datetime.fromtimestamp(record.created, UTC).isoformat().replace('+00:00', 'Z')
|
|
85
|
+
|
|
86
|
+
# -------------------------
|
|
87
|
+
# base log object (plain dict)
|
|
88
|
+
# -------------------------
|
|
89
|
+
log: dict[str, Any] = {
|
|
90
|
+
'message': message,
|
|
91
|
+
'level': record.levelname,
|
|
92
|
+
'logger': record.name,
|
|
93
|
+
'timestamp': timestamp,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (corr_id := self._get_correlation_id()) is not None:
|
|
97
|
+
log['correlation_id'] = corr_id
|
|
98
|
+
|
|
99
|
+
if (exc_info := record.exc_info) and exc_info[0] is not None:
|
|
100
|
+
exc_type, exc_value, exc_tb = exc_info
|
|
101
|
+
|
|
102
|
+
log['exc_type'] = f'{exc_type.__module__}.{exc_type.__qualname__}'
|
|
103
|
+
log['exc_value'] = str(exc_value)
|
|
104
|
+
|
|
105
|
+
tb = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)).rstrip()
|
|
106
|
+
cap = self._max_tb_chars
|
|
107
|
+
|
|
108
|
+
if cap and len(tb) > cap:
|
|
109
|
+
tb = tb[:cap] + '\n... <truncated>'
|
|
110
|
+
|
|
111
|
+
log['exc_traceback'] = tb
|
|
112
|
+
|
|
113
|
+
record_dict = record.__dict__
|
|
114
|
+
std_log_fields = _STD_LOG_FIELDS
|
|
115
|
+
|
|
116
|
+
if len(record_dict) > len(std_log_fields):
|
|
117
|
+
normalize = _normalize
|
|
118
|
+
out_set = log.__setitem__
|
|
119
|
+
|
|
120
|
+
for k, v in record_dict.items():
|
|
121
|
+
if k[0] == '_' or k in std_log_fields:
|
|
122
|
+
continue
|
|
123
|
+
out_set(k, normalize(v))
|
|
124
|
+
|
|
125
|
+
return self._encoder.encode(log).decode('utf-8')
|
|
@@ -135,7 +135,7 @@ async def list_objects(bucket_name: str, prefix: str, *, recursive: bool = True)
|
|
|
135
135
|
|
|
136
136
|
|
|
137
137
|
async def get_object_streams(
|
|
138
|
-
|
|
138
|
+
bucket_name: str, path: str, *, recursive: bool = True
|
|
139
139
|
) -> AsyncGenerator[tuple[str, datetime, StreamingBody]]:
|
|
140
140
|
async for obj in list_objects(bucket_name, path, recursive=recursive):
|
|
141
141
|
object_name = obj['Key']
|
|
@@ -146,7 +146,7 @@ async def get_object_streams(
|
|
|
146
146
|
|
|
147
147
|
|
|
148
148
|
async def get_objects(
|
|
149
|
-
|
|
149
|
+
bucket_name: str, path: str, *, recursive: bool = True
|
|
150
150
|
) -> AsyncGenerator[tuple[str, datetime, bytes]]:
|
|
151
151
|
async for object_name, last_modified, stream in get_object_streams(bucket_name, path, recursive=recursive):
|
|
152
152
|
data = await stream.read()
|
|
@@ -168,7 +168,7 @@ async def remove_object(bucket_name: str, object_name: str) -> None:
|
|
|
168
168
|
|
|
169
169
|
|
|
170
170
|
async def remove_objects(
|
|
171
|
-
|
|
171
|
+
bucket_name: str, prefix: str | None = None, object_names: Iterable[str] | None = None
|
|
172
172
|
) -> Sequence[Mapping] | None:
|
|
173
173
|
storage = ObjectStorage(s3_settings)
|
|
174
174
|
|
|
@@ -191,7 +191,7 @@ async def remove_objects(
|
|
|
191
191
|
chunk_size = 1000
|
|
192
192
|
|
|
193
193
|
for i in range(0, len(objects_to_delete), chunk_size):
|
|
194
|
-
chunk = objects_to_delete[i: i + chunk_size]
|
|
194
|
+
chunk = objects_to_delete[i : i + chunk_size]
|
|
195
195
|
|
|
196
196
|
response = await s3_client.delete_objects(Bucket=bucket_name, Delete={'Objects': chunk})
|
|
197
197
|
|
|
@@ -715,7 +715,7 @@ wheels = [
|
|
|
715
715
|
|
|
716
716
|
[[package]]
|
|
717
717
|
name = "pre-commit"
|
|
718
|
-
version = "4.
|
|
718
|
+
version = "4.6.0"
|
|
719
719
|
source = { registry = "https://pypi.org/simple" }
|
|
720
720
|
dependencies = [
|
|
721
721
|
{ name = "cfgv" },
|
|
@@ -724,9 +724,9 @@ dependencies = [
|
|
|
724
724
|
{ name = "pyyaml" },
|
|
725
725
|
{ name = "virtualenv" },
|
|
726
726
|
]
|
|
727
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
727
|
+
sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" }
|
|
728
728
|
wheels = [
|
|
729
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
729
|
+
{ url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" },
|
|
730
730
|
]
|
|
731
731
|
|
|
732
732
|
[[package]]
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import logging
|
|
3
|
-
import traceback
|
|
4
|
-
from contextvars import ContextVar
|
|
5
|
-
|
|
6
|
-
from python3_commons.serializers.json import CustomJSONEncoder
|
|
7
|
-
|
|
8
|
-
correlation_id: ContextVar[str | None] = ContextVar('correlation_id', default=None)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class JSONFormatter(logging.Formatter):
|
|
12
|
-
@staticmethod
|
|
13
|
-
def format_exception(exc_info: logging._SysExcInfoType) -> str:
|
|
14
|
-
return ''.join(traceback.format_exception(*exc_info))
|
|
15
|
-
|
|
16
|
-
def format(self, record: logging.LogRecord) -> str:
|
|
17
|
-
if corr_id := correlation_id.get():
|
|
18
|
-
record.correlation_id = corr_id
|
|
19
|
-
|
|
20
|
-
try:
|
|
21
|
-
record.message = record.getMessage()
|
|
22
|
-
except TypeError:
|
|
23
|
-
record.message = str(record.msg)
|
|
24
|
-
|
|
25
|
-
if record.exc_info:
|
|
26
|
-
record.exc_text = self.format_exception(record.exc_info)
|
|
27
|
-
else:
|
|
28
|
-
record.exc_text = None
|
|
29
|
-
|
|
30
|
-
record_dict = record.__dict__
|
|
31
|
-
|
|
32
|
-
del record_dict['args']
|
|
33
|
-
del record_dict['msg']
|
|
34
|
-
|
|
35
|
-
return json.dumps(record_dict, cls=CustomJSONEncoder)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/serializers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/serializers/msgpack.py
RENAMED
|
File without changes
|
{python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons/serializers/msgspec.py
RENAMED
|
File without changes
|
|
File without changes
|
{python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{python3_commons-0.16.0 → python3_commons-0.16.2}/src/python3_commons.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|