boto3-refresh-session 1.1.2__py3-none-any.whl → 1.2.0__py3-none-any.whl
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.
- boto3_refresh_session/__init__.py +2 -1
- boto3_refresh_session/ecs.py +109 -0
- boto3_refresh_session/exceptions.py +38 -0
- boto3_refresh_session/session.py +42 -38
- boto3_refresh_session/sts.py +4 -43
- {boto3_refresh_session-1.1.2.dist-info → boto3_refresh_session-1.2.0.dist-info}/METADATA +3 -2
- boto3_refresh_session-1.2.0.dist-info/RECORD +10 -0
- boto3_refresh_session/how.py +0 -44
- boto3_refresh_session-1.1.2.dist-info/RECORD +0 -9
- {boto3_refresh_session-1.1.2.dist-info → boto3_refresh_session-1.2.0.dist-info}/LICENSE +0 -0
- {boto3_refresh_session-1.1.2.dist-info → boto3_refresh_session-1.2.0.dist-info}/NOTICE +0 -0
- {boto3_refresh_session-1.1.2.dist-info → boto3_refresh_session-1.2.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,109 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
__all__ = ["ECSRefreshableSession"]
|
4
|
+
|
5
|
+
import os
|
6
|
+
|
7
|
+
import requests
|
8
|
+
|
9
|
+
from .exceptions import BRSError
|
10
|
+
from .session import BaseRefreshableSession
|
11
|
+
|
12
|
+
_ECS_CREDENTIALS_RELATIVE_URI = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"
|
13
|
+
_ECS_CREDENTIALS_FULL_URI = "AWS_CONTAINER_CREDENTIALS_FULL_URI"
|
14
|
+
_ECS_AUTHORIZATION_TOKEN = "AWS_CONTAINER_AUTHORIZATION_TOKEN"
|
15
|
+
_DEFAULT_ENDPOINT_BASE = "http://169.254.170.2"
|
16
|
+
|
17
|
+
|
18
|
+
class ECSRefreshableSession(BaseRefreshableSession, method="ecs"):
|
19
|
+
"""A boto3 session that automatically refreshes temporary AWS credentials
|
20
|
+
from the ECS container credentials metadata endpoint.
|
21
|
+
|
22
|
+
Parameters
|
23
|
+
----------
|
24
|
+
defer_refresh : bool, optional
|
25
|
+
If ``True`` then temporary credentials are not automatically refreshed until
|
26
|
+
they are explicitly needed. If ``False`` then temporary credentials refresh
|
27
|
+
immediately upon expiration. It is highly recommended that you use ``True``.
|
28
|
+
Default is ``True``.
|
29
|
+
|
30
|
+
Other Parameters
|
31
|
+
----------------
|
32
|
+
kwargs : dict
|
33
|
+
Optional keyword arguments passed to :class:`boto3.session.Session`.
|
34
|
+
"""
|
35
|
+
|
36
|
+
def __init__(self, defer_refresh: bool | None = None, **kwargs):
|
37
|
+
super().__init__(**kwargs)
|
38
|
+
|
39
|
+
self._endpoint = self._resolve_endpoint()
|
40
|
+
self._headers = self._build_headers()
|
41
|
+
self._http = self._init_http_session()
|
42
|
+
|
43
|
+
self._refresh_using(
|
44
|
+
credentials_method=self._get_credentials,
|
45
|
+
defer_refresh=defer_refresh is not False,
|
46
|
+
refresh_method="ecs-container-metadata",
|
47
|
+
)
|
48
|
+
|
49
|
+
def _resolve_endpoint(self) -> str:
|
50
|
+
uri = os.environ.get(_ECS_CREDENTIALS_FULL_URI) or os.environ.get(
|
51
|
+
_ECS_CREDENTIALS_RELATIVE_URI
|
52
|
+
)
|
53
|
+
if not uri:
|
54
|
+
raise BRSError(
|
55
|
+
"Neither AWS_CONTAINER_CREDENTIALS_FULL_URI nor "
|
56
|
+
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is set. "
|
57
|
+
"Are you running inside an ECS container?"
|
58
|
+
)
|
59
|
+
if uri.startswith("http://") or uri.startswith("https://"):
|
60
|
+
return uri
|
61
|
+
return f"{_DEFAULT_ENDPOINT_BASE}{uri}"
|
62
|
+
|
63
|
+
def _build_headers(self) -> dict[str, str]:
|
64
|
+
token = os.environ.get(_ECS_AUTHORIZATION_TOKEN)
|
65
|
+
if token:
|
66
|
+
return {"Authorization": f"Bearer {token}"}
|
67
|
+
return {}
|
68
|
+
|
69
|
+
def _init_http_session(self) -> requests.Session:
|
70
|
+
session = requests.Session()
|
71
|
+
session.headers.update(self._headers)
|
72
|
+
return session
|
73
|
+
|
74
|
+
def _get_credentials(self) -> dict[str, str]:
|
75
|
+
try:
|
76
|
+
response = self._http.get(self._endpoint, timeout=3)
|
77
|
+
response.raise_for_status()
|
78
|
+
except requests.RequestException as exc:
|
79
|
+
raise BRSError(
|
80
|
+
f"Failed to retrieve ECS credentials from {self._endpoint}"
|
81
|
+
) from exc
|
82
|
+
|
83
|
+
credentials = response.json()
|
84
|
+
required = {
|
85
|
+
"AccessKeyId",
|
86
|
+
"SecretAccessKey",
|
87
|
+
"SessionToken",
|
88
|
+
"Expiration",
|
89
|
+
}
|
90
|
+
if not required.issubset(credentials):
|
91
|
+
raise BRSError(f"Incomplete credentials received: {credentials}")
|
92
|
+
return {
|
93
|
+
"access_key": credentials.get("AccessKeyId"),
|
94
|
+
"secret_key": credentials.get("SecretAccessKey"),
|
95
|
+
"token": credentials.get("SessionToken"),
|
96
|
+
"expiry_time": credentials.get("Expiration"), # already ISO8601
|
97
|
+
}
|
98
|
+
|
99
|
+
@staticmethod
|
100
|
+
def get_identity() -> dict[str, str]:
|
101
|
+
"""Returns metadata about ECS.
|
102
|
+
|
103
|
+
Returns
|
104
|
+
-------
|
105
|
+
dict[str, str]
|
106
|
+
Dict containing metadata about ECS.
|
107
|
+
"""
|
108
|
+
|
109
|
+
return {"method": "ecs", "source": "ecs-container-metadata"}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class BRSError(Exception):
|
2
|
+
"""The base exception for boto3-refresh-session.
|
3
|
+
|
4
|
+
Parameters
|
5
|
+
----------
|
6
|
+
message : str, optional
|
7
|
+
The message to raise.
|
8
|
+
"""
|
9
|
+
|
10
|
+
def __init__(self, message: str | None = None):
|
11
|
+
self.message = "" if message is None else message
|
12
|
+
super().__init__(self.message)
|
13
|
+
|
14
|
+
def __str__(self) -> str:
|
15
|
+
return self.message
|
16
|
+
|
17
|
+
def __repr__(self) -> str:
|
18
|
+
return f"{self.__class__.__name__}({repr(self.message)})"
|
19
|
+
|
20
|
+
|
21
|
+
class BRSWarning(UserWarning):
|
22
|
+
"""The base warning for boto3-refresh-session.
|
23
|
+
|
24
|
+
Parameters
|
25
|
+
----------
|
26
|
+
message : str, optional
|
27
|
+
The message to raise.
|
28
|
+
"""
|
29
|
+
|
30
|
+
def __init__(self, message: str | None = None):
|
31
|
+
self.message = "" if message is None else message
|
32
|
+
super().__init__(self.message)
|
33
|
+
|
34
|
+
def __str__(self) -> str:
|
35
|
+
return self.message
|
36
|
+
|
37
|
+
def __repr__(self) -> str:
|
38
|
+
return f"{self.__class__.__name__}({repr(self.message)})"
|
boto3_refresh_session/session.py
CHANGED
@@ -1,44 +1,9 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
__doc__ = """
|
4
|
-
boto3_refresh_session.session
|
5
|
-
=============================
|
6
|
-
|
7
|
-
This module provides the main interface for constructing refreshable boto3 sessions.
|
8
|
-
|
9
|
-
The ``RefreshableSession`` class serves as a factory that dynamically selects the appropriate
|
10
|
-
credential refresh strategy based on the ``method`` parameter, e.g., ``sts``.
|
11
|
-
|
12
|
-
Users can interact with AWS services just like they would with a normal :class:`boto3.session.Session`,
|
13
|
-
with the added benefit of automatic credential refreshing.
|
14
|
-
|
15
|
-
Examples
|
16
|
-
--------
|
17
|
-
>>> from boto3_refresh_session import RefreshableSession
|
18
|
-
>>> session = RefreshableSession(
|
19
|
-
... assume_role_kwargs={"RoleArn": "...", "RoleSessionName": "..."},
|
20
|
-
... region_name="us-east-1"
|
21
|
-
... )
|
22
|
-
>>> s3 = session.client("s3")
|
23
|
-
>>> s3.list_buckets()
|
24
|
-
|
25
|
-
.. seealso::
|
26
|
-
:class:`boto3_refresh_session.sts.STSRefreshableSession`
|
27
|
-
|
28
|
-
Factory interface
|
29
|
-
-----------------
|
30
|
-
.. autosummary::
|
31
|
-
:toctree: generated/
|
32
|
-
:nosignatures:
|
33
|
-
|
34
|
-
RefreshableSession
|
35
|
-
"""
|
36
|
-
|
37
3
|
__all__ = ["RefreshableSession"]
|
38
4
|
|
39
5
|
from abc import ABC, abstractmethod
|
40
6
|
from typing import Any, Callable, ClassVar, Literal, get_args
|
41
|
-
from warnings import warn
|
42
7
|
|
43
8
|
from boto3.session import Session
|
44
9
|
from botocore.credentials import (
|
@@ -46,9 +11,11 @@ from botocore.credentials import (
|
|
46
11
|
RefreshableCredentials,
|
47
12
|
)
|
48
13
|
|
14
|
+
from .exceptions import BRSError, BRSWarning
|
15
|
+
|
49
16
|
#: Type alias for all currently available credential refresh methods.
|
50
|
-
Method = Literal["sts"]
|
51
|
-
RefreshMethod = Literal["sts-assume-role"]
|
17
|
+
Method = Literal["sts", "ecs"]
|
18
|
+
RefreshMethod = Literal["sts-assume-role", "ecs-container-metadata"]
|
52
19
|
|
53
20
|
|
54
21
|
class BaseRefreshableSession(ABC, Session):
|
@@ -77,7 +44,9 @@ class BaseRefreshableSession(ABC, Session):
|
|
77
44
|
|
78
45
|
# guarantees that methods are unique
|
79
46
|
if method in BaseRefreshableSession.registry:
|
80
|
-
|
47
|
+
BRSWarning(
|
48
|
+
f"Method {repr(method)} is already registered. Overwriting."
|
49
|
+
)
|
81
50
|
|
82
51
|
BaseRefreshableSession.registry[method] = cls
|
83
52
|
|
@@ -108,6 +77,34 @@ class BaseRefreshableSession(ABC, Session):
|
|
108
77
|
refresh_using=credentials_method, method=refresh_method
|
109
78
|
)
|
110
79
|
|
80
|
+
def refreshable_credentials(self) -> dict[str, str]:
|
81
|
+
"""The current temporary AWS security credentials.
|
82
|
+
|
83
|
+
Returns
|
84
|
+
-------
|
85
|
+
dict[str, str]
|
86
|
+
Temporary AWS security credentials containing:
|
87
|
+
AWS_ACCESS_KEY_ID : str
|
88
|
+
AWS access key identifier.
|
89
|
+
AWS_SECRET_ACCESS_KEY : str
|
90
|
+
AWS secret access key.
|
91
|
+
AWS_SESSION_TOKEN : str
|
92
|
+
AWS session token.
|
93
|
+
"""
|
94
|
+
|
95
|
+
creds = self.get_credentials().get_frozen_credentials()
|
96
|
+
return {
|
97
|
+
"AWS_ACCESS_KEY_ID": creds.access_key,
|
98
|
+
"AWS_SECRET_ACCESS_KEY": creds.secret_key,
|
99
|
+
"AWS_SESSION_TOKEN": creds.token,
|
100
|
+
}
|
101
|
+
|
102
|
+
@property
|
103
|
+
def credentials(self) -> dict[str, str]:
|
104
|
+
"""The current temporary AWS security credentials."""
|
105
|
+
|
106
|
+
return self.refreshable_credentials()
|
107
|
+
|
111
108
|
|
112
109
|
class RefreshableSession:
|
113
110
|
"""Factory class for constructing refreshable boto3 sessions using various authentication
|
@@ -134,11 +131,18 @@ class RefreshableSession:
|
|
134
131
|
See Also
|
135
132
|
--------
|
136
133
|
boto3_refresh_session.sts.STSRefreshableSession
|
134
|
+
boto3_refresh_session.ecs.ECSRefreshableSession
|
137
135
|
"""
|
138
136
|
|
139
137
|
def __new__(
|
140
138
|
cls, method: Method = "sts", **kwargs
|
141
139
|
) -> BaseRefreshableSession:
|
140
|
+
if method not in (methods := cls.get_available_methods()):
|
141
|
+
raise BRSError(
|
142
|
+
f"{repr(method)} is an invalid method parameter. Available methods are "
|
143
|
+
f"{', '.join(repr(meth) for meth in methods)}."
|
144
|
+
)
|
145
|
+
|
142
146
|
obj = BaseRefreshableSession.registry[method]
|
143
147
|
return obj(**kwargs)
|
144
148
|
|
boto3_refresh_session/sts.py
CHANGED
@@ -1,49 +1,10 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
__doc__ = """
|
4
|
-
boto3_refresh_session.sts
|
5
|
-
=========================
|
6
|
-
|
7
|
-
Implements the STS-based credential refresh strategy for use with
|
8
|
-
:class:`boto3_refresh_session.session.RefreshableSession`.
|
9
|
-
|
10
|
-
This module defines the :class:`STSRefreshableSession` class, which uses
|
11
|
-
IAM role assumption via STS to automatically refresh temporary credentials
|
12
|
-
in the background.
|
13
|
-
|
14
|
-
.. versionadded:: 1.1.0
|
15
|
-
|
16
|
-
Examples
|
17
|
-
--------
|
18
|
-
>>> from boto3_refresh_session import RefreshableSession
|
19
|
-
>>> session = RefreshableSession(
|
20
|
-
... method="sts",
|
21
|
-
... assume_role_kwargs={
|
22
|
-
... "RoleArn": "arn:aws:iam::123456789012:role/MyRole",
|
23
|
-
... "RoleSessionName": "my-session"
|
24
|
-
... },
|
25
|
-
... region_name="us-east-1"
|
26
|
-
... )
|
27
|
-
>>> s3 = session.client("s3")
|
28
|
-
>>> s3.list_buckets()
|
29
|
-
|
30
|
-
.. seealso::
|
31
|
-
:class:`boto3_refresh_session.session.RefreshableSession`
|
32
|
-
|
33
|
-
STS
|
34
|
-
---
|
35
|
-
|
36
|
-
.. autosummary::
|
37
|
-
:toctree: generated/
|
38
|
-
:nosignatures:
|
39
|
-
|
40
|
-
STSRefreshableSession
|
41
|
-
"""
|
42
3
|
__all__ = ["STSRefreshableSession"]
|
43
4
|
|
44
5
|
from typing import Any
|
45
|
-
from warnings import warn
|
46
6
|
|
7
|
+
from .exceptions import BRSWarning
|
47
8
|
from .session import BaseRefreshableSession
|
48
9
|
|
49
10
|
|
@@ -73,7 +34,7 @@ class STSRefreshableSession(BaseRefreshableSession, method="sts"):
|
|
73
34
|
def __init__(
|
74
35
|
self,
|
75
36
|
assume_role_kwargs: dict,
|
76
|
-
defer_refresh: bool = None,
|
37
|
+
defer_refresh: bool | None = None,
|
77
38
|
sts_client_kwargs: dict | None = None,
|
78
39
|
**kwargs,
|
79
40
|
):
|
@@ -84,8 +45,8 @@ class STSRefreshableSession(BaseRefreshableSession, method="sts"):
|
|
84
45
|
if sts_client_kwargs is not None:
|
85
46
|
# overwriting 'service_name' in case it appears in sts_client_kwargs
|
86
47
|
if "service_name" in sts_client_kwargs:
|
87
|
-
|
88
|
-
"
|
48
|
+
BRSWarning(
|
49
|
+
"'sts_client_kwargs' cannot contain values for 'service_name'. Reverting to service_name = 'sts'."
|
89
50
|
)
|
90
51
|
del sts_client_kwargs["service_name"]
|
91
52
|
self._sts_client = self.client(
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: boto3-refresh-session
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.2.0
|
4
4
|
Summary: A simple Python package for refreshing the temporary security credentials in a boto3.session.Session object automatically.
|
5
5
|
License: MIT
|
6
6
|
Keywords: boto3,botocore,aws
|
@@ -56,7 +56,7 @@ Description-Content-Type: text/markdown
|
|
56
56
|
</a>
|
57
57
|
|
58
58
|
<a href="https://pepy.tech/project/boto3-refresh-session">
|
59
|
-
<img src="https://img.shields.io/badge/downloads-
|
59
|
+
<img src="https://img.shields.io/badge/downloads-59.6K-red?logo=python&color=%23FF0000&label=Downloads" alt="Downloads"/>
|
60
60
|
</a>
|
61
61
|
|
62
62
|
<a href="https://michaelthomasletts.github.io/boto3-refresh-session/index.html">
|
@@ -77,6 +77,7 @@ Description-Content-Type: text/markdown
|
|
77
77
|
|
78
78
|
- Auto-refreshing credentials for long-lived `boto3` sessions
|
79
79
|
- Drop-in replacement for `boto3.session.Session`
|
80
|
+
- Supports automatic refresh methods for STS and ECS
|
80
81
|
- Supports `assume_role` configuration, custom STS clients, and profile / region configuration, as well as all other parameters supported by `boto3.session.Session`
|
81
82
|
- Tested, documented, and published to PyPI
|
82
83
|
|
@@ -0,0 +1,10 @@
|
|
1
|
+
boto3_refresh_session/__init__.py,sha256=hJpUIAjfhv-ByWbDCqJfw0sEESVxVnxvZ-aOnVNXu90,200
|
2
|
+
boto3_refresh_session/ecs.py,sha256=WIC5mlbcEnM1oo-QXmmtiw2mjFDn01hBfcFh67ku42A,3713
|
3
|
+
boto3_refresh_session/exceptions.py,sha256=qcFzdIuK5PZirs77H_Kb64S9QFb6cn2OJtirjvaRLiY,972
|
4
|
+
boto3_refresh_session/session.py,sha256=MtaTVdAy-Yx7x_x9SUJ0FlQs0w_8D3kHjvOS8C3bK2E,5226
|
5
|
+
boto3_refresh_session/sts.py,sha256=paIgbmn9a3cATNX-6AEGxnSGNZnX1pj4rRQmh8gQSKs,3132
|
6
|
+
boto3_refresh_session-1.2.0.dist-info/LICENSE,sha256=I3ZYTXAjbIly6bm6J-TvFTuuHwTKws4h89QaY5c5HiY,1067
|
7
|
+
boto3_refresh_session-1.2.0.dist-info/METADATA,sha256=PafbLjHvZ4mZKyVduLho8Szruoq0P4lB_2Nv0aMFTyE,7586
|
8
|
+
boto3_refresh_session-1.2.0.dist-info/NOTICE,sha256=1s8r33qbl1z0YvPB942iWgvbkP94P_e8AnROr1qXXuw,939
|
9
|
+
boto3_refresh_session-1.2.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
10
|
+
boto3_refresh_session-1.2.0.dist-info/RECORD,,
|
boto3_refresh_session/how.py
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
s = """
|
2
|
-
lbh orybat gb zr,
|
3
|
-
naq v, lbh.
|
4
|
-
|
5
|
-
jr ner abg jub jr fnl jr ner.
|
6
|
-
abg jub jr guvax jr ner.
|
7
|
-
jr ner jub bguref guvax jr ner --
|
8
|
-
jung bhe npgvbaf fnl jr ner.
|
9
|
-
|
10
|
-
fb znxr jung lbh ohvyq frysyrff.
|
11
|
-
ornhgvshy. ebohfg. cresrpg.
|
12
|
-
|
13
|
-
zrqvbpevgl yherf hf sebz qvivavgl.
|
14
|
-
gur pybfre gb cresrpgvba jr nfpraq,
|
15
|
-
gur zber fgevqrag rivy tebjf --
|
16
|
-
orpnhfr rivy pnaabg rkvfg
|
17
|
-
vs nyy vf cresrpg.
|
18
|
-
|
19
|
-
n guvat vf jung vg vf abg.
|
20
|
-
n guvat jvgu ab bgure vf abguvat.
|
21
|
-
naq abguvat
|
22
|
-
vf gur zbfg cresrpg guvat bs nyy.
|
23
|
-
|
24
|
-
ubjrire lbh hfr guvf fbsgjner,
|
25
|
-
jungrire lbh ohvyq --
|
26
|
-
znxr vg cresrpg.
|
27
|
-
qb fb sbe rirelbar ohg lbhefrys.
|
28
|
-
|
29
|
-
znl jr or sbetbggra
|
30
|
-
naq bhe npgvbaf svanyyl fvyrag.
|
31
|
-
"""
|
32
|
-
|
33
|
-
|
34
|
-
def how(text: str, shift: int = 13) -> str:
|
35
|
-
def rotate(c: str) -> str:
|
36
|
-
if 'a' <= c <= 'z':
|
37
|
-
return chr((ord(c) - ord('a') + shift) % 26 + ord('a'))
|
38
|
-
elif 'A' <= c <= 'Z':
|
39
|
-
return chr((ord(c) - ord('A') + shift) % 26 + ord('A'))
|
40
|
-
return c
|
41
|
-
return ''.join(rotate(c) for c in text)
|
42
|
-
|
43
|
-
|
44
|
-
print(how(s.strip()))
|
@@ -1,9 +0,0 @@
|
|
1
|
-
boto3_refresh_session/__init__.py,sha256=WPNU8U79usNSOl7FKoeBQrUhdaX4Yvtpwl8eJbQcTXI,161
|
2
|
-
boto3_refresh_session/how.py,sha256=zBfxfvQCzqJT8VJ8VTmUUzQUupRkSYXP8yOzvIXruUU,994
|
3
|
-
boto3_refresh_session/session.py,sha256=J3FW6nkc_s9C0gj1Xs1wKaCWQMzR6S4ncDKDqu1DYxk,4879
|
4
|
-
boto3_refresh_session/sts.py,sha256=P8lWxQLslXDeYSuvHuy0Le6CC5SIXxQ9D5DZFmwwE1Q,4057
|
5
|
-
boto3_refresh_session-1.1.2.dist-info/LICENSE,sha256=I3ZYTXAjbIly6bm6J-TvFTuuHwTKws4h89QaY5c5HiY,1067
|
6
|
-
boto3_refresh_session-1.1.2.dist-info/METADATA,sha256=25RIB9z4wwR1D3Gts08W6tNDkxH-5hW0Hz8hBX9KOYk,7533
|
7
|
-
boto3_refresh_session-1.1.2.dist-info/NOTICE,sha256=1s8r33qbl1z0YvPB942iWgvbkP94P_e8AnROr1qXXuw,939
|
8
|
-
boto3_refresh_session-1.1.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
9
|
-
boto3_refresh_session-1.1.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|