sfq 0.0.30__tar.gz → 0.0.32__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.
- {sfq-0.0.30 → sfq-0.0.32}/.github/workflows/publish.yml +2 -0
- {sfq-0.0.30 → sfq-0.0.32}/PKG-INFO +1 -1
- {sfq-0.0.30 → sfq-0.0.32}/pyproject.toml +1 -1
- {sfq-0.0.30 → sfq-0.0.32}/src/sfq/__init__.py +48 -11
- sfq-0.0.32/tests/test_debug_cleanup.py +148 -0
- sfq-0.0.32/tests/test_open_frontdoor.py +51 -0
- {sfq-0.0.30 → sfq-0.0.32}/uv.lock +1 -1
- sfq-0.0.30/tests/test_debug_cleanup.py +0 -67
- {sfq-0.0.30 → sfq-0.0.32}/.gitignore +0 -0
- {sfq-0.0.30 → sfq-0.0.32}/.python-version +0 -0
- {sfq-0.0.30 → sfq-0.0.32}/README.md +0 -0
- {sfq-0.0.30 → sfq-0.0.32}/src/sfq/_cometd.py +0 -0
- {sfq-0.0.30 → sfq-0.0.32}/src/sfq/py.typed +0 -0
- {sfq-0.0.30 → sfq-0.0.32}/tests/test_cdelete.py +0 -0
- {sfq-0.0.30 → sfq-0.0.32}/tests/test_cquery.py +0 -0
- {sfq-0.0.30 → sfq-0.0.32}/tests/test_create.py +0 -0
- {sfq-0.0.30 → sfq-0.0.32}/tests/test_cupdate.py +0 -0
- {sfq-0.0.30 → sfq-0.0.32}/tests/test_limits_api.py +0 -0
- {sfq-0.0.30 → sfq-0.0.32}/tests/test_log_trace_redact.py +0 -0
- {sfq-0.0.30 → sfq-0.0.32}/tests/test_query.py +0 -0
@@ -78,6 +78,8 @@ jobs:
|
|
78
78
|
sed -i -E "s/(user_agent: str = \"sfq\/)[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/__init__.py
|
79
79
|
sed -i -E "s/(default is \"sfq\/)[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/__init__.py
|
80
80
|
|
81
|
+
echo "Updating src/sfq/__init__.py __version__ to $VERSION"
|
82
|
+
sed -i -E "s/(self.__version__ = \")[0-9]+\.[0-9]+\.[0-9]+(\")/\1$VERSION\2/" src/sfq/__init__.py
|
81
83
|
- name: Run tests
|
82
84
|
run: pytest --verbose --strict-config
|
83
85
|
env:
|
@@ -10,6 +10,7 @@ import os
|
|
10
10
|
import re
|
11
11
|
import time
|
12
12
|
import warnings
|
13
|
+
import webbrowser
|
13
14
|
import xml.etree.ElementTree as ET
|
14
15
|
from collections import OrderedDict
|
15
16
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
@@ -99,7 +100,7 @@ class SFAuth:
|
|
99
100
|
access_token: Optional[str] = None,
|
100
101
|
token_expiration_time: Optional[float] = None,
|
101
102
|
token_lifetime: int = 15 * 60,
|
102
|
-
user_agent: str = "sfq/0.0.
|
103
|
+
user_agent: str = "sfq/0.0.32",
|
103
104
|
sforce_client: str = "_auto",
|
104
105
|
proxy: str = "_auto",
|
105
106
|
) -> None:
|
@@ -169,6 +170,16 @@ class SFAuth:
|
|
169
170
|
* 📖 [Salesforce OAuth Flows Documentation](https://help.salesforce.com/s/articleView?id=xcloud.remoteaccess_oauth_flows.htm&type=5)
|
170
171
|
"""
|
171
172
|
|
173
|
+
self.__version__ = "0.0.32"
|
174
|
+
"""
|
175
|
+
### `__version__`
|
176
|
+
|
177
|
+
**The version of the sfq library.**
|
178
|
+
- Schema: `MAJOR.MINOR.PATCH`
|
179
|
+
- Used for debugging and compatibility checks
|
180
|
+
- Updated to reflect the current library version via CI/CD automation
|
181
|
+
"""
|
182
|
+
|
172
183
|
self.api_version = api_version
|
173
184
|
"""
|
174
185
|
|
@@ -997,9 +1008,10 @@ class SFAuth:
|
|
997
1008
|
|
998
1009
|
return combined_response or None
|
999
1010
|
|
1000
|
-
def _gen_soap_envelope(self, header: str, body: str) -> str:
|
1001
|
-
"""Generates a full SOAP envelope with all required namespaces for Salesforce
|
1002
|
-
|
1011
|
+
def _gen_soap_envelope(self, header: str, body: str, type: str) -> str:
|
1012
|
+
"""Generates a full SOAP envelope with all required namespaces for Salesforce API."""
|
1013
|
+
if type == "enterprise":
|
1014
|
+
return (
|
1003
1015
|
'<?xml version="1.0" encoding="UTF-8"?>'
|
1004
1016
|
"<soapenv:Envelope "
|
1005
1017
|
'xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" '
|
@@ -1010,8 +1022,24 @@ class SFAuth:
|
|
1010
1022
|
f"{header}{body}"
|
1011
1023
|
"</soapenv:Envelope>"
|
1012
1024
|
)
|
1025
|
+
elif type == "tooling":
|
1026
|
+
return (
|
1027
|
+
'<?xml version="1.0" encoding="UTF-8"?>'
|
1028
|
+
"<soapenv:Envelope "
|
1029
|
+
'xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" '
|
1030
|
+
'xmlns:xsd="http://www.w3.org/2001/XMLSchema" '
|
1031
|
+
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
|
1032
|
+
'xmlns="urn:tooling.soap.sforce.com" '
|
1033
|
+
'xmlns:mns="urn:metadata.tooling.soap.sforce.com" '
|
1034
|
+
'xmlns:sf="urn:sobject.tooling.soap.sforce.com">'
|
1035
|
+
f"{header}{body}"
|
1036
|
+
"</soapenv:Envelope>"
|
1037
|
+
)
|
1038
|
+
raise ValueError(
|
1039
|
+
f"Unsupported API type: {type}. Must be 'enterprise' or 'tooling'."
|
1040
|
+
)
|
1013
1041
|
|
1014
|
-
def _gen_soap_header(self):
|
1042
|
+
def _gen_soap_header(self) -> str:
|
1015
1043
|
"""This function generates the header for the SOAP request."""
|
1016
1044
|
headers = self._get_common_headers()
|
1017
1045
|
session_id = headers["Authorization"].split(" ")[1]
|
@@ -1099,7 +1127,7 @@ class SFAuth:
|
|
1099
1127
|
insert_list: List[Dict[str, Any]],
|
1100
1128
|
batch_size: int = 200,
|
1101
1129
|
max_workers: int = None,
|
1102
|
-
api_type: Literal["enterprise", "tooling"
|
1130
|
+
api_type: Literal["enterprise", "tooling"] = "enterprise",
|
1103
1131
|
) -> Optional[Dict[str, Any]]:
|
1104
1132
|
"""
|
1105
1133
|
Execute the Insert API to insert multiple records via SOAP calls.
|
@@ -1116,14 +1144,13 @@ class SFAuth:
|
|
1116
1144
|
endpoint += f"c/{self.api_version}"
|
1117
1145
|
elif api_type == "tooling":
|
1118
1146
|
endpoint += f"T/{self.api_version}"
|
1119
|
-
elif api_type == "metadata":
|
1120
|
-
endpoint += f"m/{self.api_version}"
|
1121
1147
|
else:
|
1122
1148
|
logger.error(
|
1123
|
-
"Invalid API type: %s. Must be one of: 'enterprise', 'tooling'
|
1149
|
+
"Invalid API type: %s. Must be one of: 'enterprise', 'tooling'.",
|
1124
1150
|
api_type,
|
1125
1151
|
)
|
1126
1152
|
return None
|
1153
|
+
endpoint = endpoint.replace('/v', '/') # handle API versioning in the endpoint
|
1127
1154
|
|
1128
1155
|
if isinstance(insert_list, dict):
|
1129
1156
|
insert_list = [insert_list]
|
@@ -1136,7 +1163,7 @@ class SFAuth:
|
|
1136
1163
|
def insert_chunk(chunk: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
1137
1164
|
header = self._gen_soap_header()
|
1138
1165
|
body = self._gen_soap_body(sobject=sobject, method="create", data=chunk)
|
1139
|
-
envelope = self._gen_soap_envelope(header, body)
|
1166
|
+
envelope = self._gen_soap_envelope(header=header, body=body, type=api_type)
|
1140
1167
|
soap_headers = self._get_common_headers().copy()
|
1141
1168
|
soap_headers["Content-Type"] = "text/xml; charset=UTF-8"
|
1142
1169
|
soap_headers["SOAPAction"] = '""'
|
@@ -1197,4 +1224,14 @@ class SFAuth:
|
|
1197
1224
|
Perform cleanup operations for Apex debug logs.
|
1198
1225
|
"""
|
1199
1226
|
if apex_logs:
|
1200
|
-
self._debug_cleanup_apex_logs()
|
1227
|
+
self._debug_cleanup_apex_logs()
|
1228
|
+
|
1229
|
+
def open_frontdoor(self) -> None:
|
1230
|
+
"""
|
1231
|
+
This function opens the Salesforce Frontdoor URL in the default web browser.
|
1232
|
+
"""
|
1233
|
+
if not self.access_token:
|
1234
|
+
self._get_common_headers()
|
1235
|
+
sid = quote(self.access_token, safe="")
|
1236
|
+
frontdoor_url = f"{self.instance_url}/secur/frontdoor.jsp?sid={sid}"
|
1237
|
+
webbrowser.open(frontdoor_url)
|
@@ -0,0 +1,148 @@
|
|
1
|
+
import http.client
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
import sys
|
5
|
+
from datetime import datetime, timedelta, timezone
|
6
|
+
from pathlib import Path
|
7
|
+
from time import sleep
|
8
|
+
from urllib.parse import quote
|
9
|
+
|
10
|
+
import pytest
|
11
|
+
|
12
|
+
# --- Setup local import path ---
|
13
|
+
project_root = Path(__file__).resolve().parents[1]
|
14
|
+
src_path = project_root / "src"
|
15
|
+
sys.path.insert(0, str(src_path))
|
16
|
+
from sfq import SFAuth # noqa: E402
|
17
|
+
|
18
|
+
|
19
|
+
@pytest.fixture(scope="module")
|
20
|
+
def sf_instance():
|
21
|
+
required_env_vars = [
|
22
|
+
"SF_INSTANCE_URL",
|
23
|
+
"SF_CLIENT_ID",
|
24
|
+
"SF_CLIENT_SECRET",
|
25
|
+
"SF_REFRESH_TOKEN",
|
26
|
+
]
|
27
|
+
|
28
|
+
missing_vars = [var for var in required_env_vars if not os.getenv(var)]
|
29
|
+
if missing_vars:
|
30
|
+
pytest.fail(f"Missing required env vars: {', '.join(missing_vars)}")
|
31
|
+
|
32
|
+
sf = SFAuth(
|
33
|
+
instance_url=os.getenv("SF_INSTANCE_URL"),
|
34
|
+
client_id=os.getenv("SF_CLIENT_ID"),
|
35
|
+
client_secret=os.getenv("SF_CLIENT_SECRET"),
|
36
|
+
refresh_token=os.getenv("SF_REFRESH_TOKEN"),
|
37
|
+
)
|
38
|
+
return sf
|
39
|
+
|
40
|
+
|
41
|
+
def test_debug_cleanup(sf_instance, already_executed: bool = False):
|
42
|
+
"""
|
43
|
+
Test the debug_cleanup method of SFAuth.
|
44
|
+
This test ensures the method deletes Apex logs as expected.
|
45
|
+
"""
|
46
|
+
# Check if any Apex logs already exist
|
47
|
+
apex_logs = sf_instance.query("SELECT Id FROM ApexLog LIMIT 1")
|
48
|
+
if apex_logs.get("records"):
|
49
|
+
sf_instance.debug_cleanup(apex_logs=True)
|
50
|
+
apex_logs_after = sf_instance.query("SELECT Id FROM ApexLog LIMIT 1")
|
51
|
+
assert len(apex_logs_after.get("records", [])) == 0, (
|
52
|
+
"Apex logs were not cleaned up successfully."
|
53
|
+
)
|
54
|
+
return
|
55
|
+
|
56
|
+
# No Apex logs yet; create one via anonymous Apex
|
57
|
+
traceflag_query = sf_instance.tooling_query(
|
58
|
+
f"SELECT Id FROM TraceFlag WHERE TracedEntityId = '{sf_instance.user_id}' LIMIT 1"
|
59
|
+
)
|
60
|
+
records = traceflag_query.get("records", [])
|
61
|
+
traceflag_id = records[0].get("Id") if records else None
|
62
|
+
|
63
|
+
if not traceflag_id:
|
64
|
+
debuglevel_query = sf_instance.tooling_query(
|
65
|
+
"SELECT Id FROM DebugLevel WHERE DeveloperName = 'SFDC_DevConsole' LIMIT 1"
|
66
|
+
)
|
67
|
+
debuglevel_id = debuglevel_query.get("records", [{}])[0].get("Id")
|
68
|
+
|
69
|
+
if not debuglevel_id:
|
70
|
+
pytest.fail(
|
71
|
+
"DebugLevel 'SFDC_DevConsole' not found. Please create it in your Salesforce org."
|
72
|
+
)
|
73
|
+
|
74
|
+
traceflag_payload = {
|
75
|
+
"DebugLevelId": debuglevel_id,
|
76
|
+
"LogType": "USER_DEBUG",
|
77
|
+
"TracedEntityId": sf_instance.user_id,
|
78
|
+
"StartDate": datetime.now(timezone.utc).isoformat(),
|
79
|
+
"ExpirationDate": (
|
80
|
+
datetime.now(timezone.utc) + timedelta(minutes=5)
|
81
|
+
).isoformat(),
|
82
|
+
}
|
83
|
+
resp = sf_instance._create(
|
84
|
+
sobject="TraceFlag", insert_list=[traceflag_payload], api_type="tooling"
|
85
|
+
)
|
86
|
+
with open("debug_payload.json", "w") as f:
|
87
|
+
f.write(json.dumps(resp, indent=2))
|
88
|
+
with open("debug_payload.json", "w") as f:
|
89
|
+
f.write(json.dumps(traceflag_payload, indent=2))
|
90
|
+
|
91
|
+
traceflag_query = sf_instance.tooling_query(
|
92
|
+
f"SELECT Id FROM TraceFlag WHERE TracedEntityId = '{sf_instance.user_id}' LIMIT 1"
|
93
|
+
)
|
94
|
+
records = traceflag_query.get("records", [])
|
95
|
+
traceflag_id = records[0].get("Id") if records else None
|
96
|
+
|
97
|
+
if not traceflag_id:
|
98
|
+
pytest.fail("Failed to create TraceFlag.")
|
99
|
+
|
100
|
+
else:
|
101
|
+
# Update the existing TraceFlag's dates
|
102
|
+
starttime = datetime.now(timezone.utc).isoformat()
|
103
|
+
endtime = (datetime.now(timezone.utc) + timedelta(minutes=5)).isoformat()
|
104
|
+
payload = json.dumps({"StartDate": starttime, "ExpirationDate": endtime})
|
105
|
+
|
106
|
+
conn = http.client.HTTPSConnection(
|
107
|
+
sf_instance.instance_url.replace("https://", "")
|
108
|
+
)
|
109
|
+
conn.request(
|
110
|
+
"PATCH",
|
111
|
+
f"/services/data/v64.0/tooling/sobjects/TraceFlag/{traceflag_id}",
|
112
|
+
body=payload,
|
113
|
+
headers={
|
114
|
+
"Authorization": f"Bearer {sf_instance.access_token}",
|
115
|
+
"Content-Type": "application/json",
|
116
|
+
},
|
117
|
+
)
|
118
|
+
response = conn.getresponse()
|
119
|
+
resp_body = response.read().decode()
|
120
|
+
|
121
|
+
if response.status not in (200, 204):
|
122
|
+
pytest.fail(
|
123
|
+
f"Failed to update TraceFlag: {response.reason} | Body: {resp_body}"
|
124
|
+
)
|
125
|
+
|
126
|
+
# Now generate an Apex log
|
127
|
+
anonymous_body = f"System.debug('Hello from {sf_instance.user_agent}! :)');"
|
128
|
+
encoded_body = quote(anonymous_body, safe="")
|
129
|
+
conn = http.client.HTTPSConnection(sf_instance.instance_url.replace("https://", ""))
|
130
|
+
conn.request(
|
131
|
+
"GET",
|
132
|
+
f"/services/data/v64.0/tooling/executeAnonymous/?anonymousBody={encoded_body}",
|
133
|
+
headers={
|
134
|
+
"Authorization": f"Bearer {sf_instance.access_token}",
|
135
|
+
"Content-Type": "application/json",
|
136
|
+
},
|
137
|
+
)
|
138
|
+
response = conn.getresponse()
|
139
|
+
if response.status != 200:
|
140
|
+
pytest.fail(f"Failed to execute anonymous Apex: {response.reason}")
|
141
|
+
|
142
|
+
if already_executed:
|
143
|
+
pytest.fail(
|
144
|
+
"ApexLog creation failed or already attempted. Skipping recursion to avoid infinite loop."
|
145
|
+
)
|
146
|
+
|
147
|
+
sleep(1) # Race condition mitigation
|
148
|
+
return test_debug_cleanup(sf_instance, already_executed=True)
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import os
|
2
|
+
import re
|
3
|
+
import sys
|
4
|
+
from pathlib import Path
|
5
|
+
from unittest.mock import patch
|
6
|
+
from urllib.parse import quote
|
7
|
+
|
8
|
+
import pytest
|
9
|
+
from pytest import fail
|
10
|
+
|
11
|
+
# --- Setup local import path ---
|
12
|
+
project_root = Path(__file__).resolve().parents[1]
|
13
|
+
src_path = project_root / "src"
|
14
|
+
sys.path.insert(0, str(src_path))
|
15
|
+
from sfq import SFAuth # noqa: E402
|
16
|
+
|
17
|
+
|
18
|
+
@pytest.fixture(scope="module")
|
19
|
+
def sf_instance():
|
20
|
+
required_env_vars = [
|
21
|
+
"SF_INSTANCE_URL",
|
22
|
+
"SF_CLIENT_ID",
|
23
|
+
"SF_CLIENT_SECRET",
|
24
|
+
"SF_REFRESH_TOKEN",
|
25
|
+
]
|
26
|
+
|
27
|
+
missing_vars = [var for var in required_env_vars if not os.getenv(var)]
|
28
|
+
if missing_vars:
|
29
|
+
pytest.fail(f"Missing required env vars: {', '.join(missing_vars)}")
|
30
|
+
|
31
|
+
return SFAuth(
|
32
|
+
instance_url=os.getenv("SF_INSTANCE_URL"),
|
33
|
+
client_id=os.getenv("SF_CLIENT_ID"),
|
34
|
+
client_secret=os.getenv("SF_CLIENT_SECRET"),
|
35
|
+
refresh_token=os.getenv("SF_REFRESH_TOKEN"),
|
36
|
+
)
|
37
|
+
|
38
|
+
|
39
|
+
def test_open_frontdoor(sf_instance):
|
40
|
+
with patch("webbrowser.open") as mock_open:
|
41
|
+
try:
|
42
|
+
sf_instance.open_frontdoor()
|
43
|
+
sid = quote(sf_instance.access_token, safe="")
|
44
|
+
expected_url = f"{sf_instance.instance_url}/secur/frontdoor.jsp?sid={sid}"
|
45
|
+
mock_open.assert_called_once_with(expected_url)
|
46
|
+
except AssertionError as e:
|
47
|
+
msg = str(e)
|
48
|
+
if sf_instance.access_token and sf_instance.access_token in msg:
|
49
|
+
msg = msg.replace(sf_instance.access_token, "[REDACTED]")
|
50
|
+
msg = re.sub(r"([?&]sid=)[^&\s]+", r"\1[REDACTED]", msg)
|
51
|
+
fail(f"Assertion failed: {msg}")
|
@@ -1,67 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
import sys
|
3
|
-
from pathlib import Path
|
4
|
-
from datetime import datetime, timedelta
|
5
|
-
|
6
|
-
import pytest
|
7
|
-
|
8
|
-
# --- Setup local import path ---
|
9
|
-
project_root = Path(__file__).resolve().parents[1]
|
10
|
-
src_path = project_root / "src"
|
11
|
-
sys.path.insert(0, str(src_path))
|
12
|
-
from sfq import SFAuth # noqa: E402
|
13
|
-
|
14
|
-
|
15
|
-
@pytest.fixture(scope="module")
|
16
|
-
def sf_instance():
|
17
|
-
required_env_vars = [
|
18
|
-
"SF_INSTANCE_URL",
|
19
|
-
"SF_CLIENT_ID",
|
20
|
-
"SF_CLIENT_SECRET",
|
21
|
-
"SF_REFRESH_TOKEN",
|
22
|
-
]
|
23
|
-
|
24
|
-
missing_vars = [var for var in required_env_vars if not os.getenv(var)]
|
25
|
-
if missing_vars:
|
26
|
-
pytest.fail(f"Missing required env vars: {', '.join(missing_vars)}")
|
27
|
-
|
28
|
-
sf = SFAuth(
|
29
|
-
instance_url=os.getenv("SF_INSTANCE_URL"),
|
30
|
-
client_id=os.getenv("SF_CLIENT_ID"),
|
31
|
-
client_secret=os.getenv("SF_CLIENT_SECRET"),
|
32
|
-
refresh_token=os.getenv("SF_REFRESH_TOKEN"),
|
33
|
-
)
|
34
|
-
return sf
|
35
|
-
|
36
|
-
def test_debug_cleanup(sf_instance, already_executed: bool = False):
|
37
|
-
"""
|
38
|
-
Test the debug_cleanup method of SFAuth.
|
39
|
-
This test will check if the method can be called without errors.
|
40
|
-
"""
|
41
|
-
apex_logs = sf_instance.query("SELECT Id FROM ApexLog LIMIT 1")
|
42
|
-
apex_log_count = len(apex_logs.get("records", []))
|
43
|
-
if apex_log_count == 0:
|
44
|
-
# OK, so we need to create an Apex log to test cleanup
|
45
|
-
# To do this, let's execute a simple Apex anonymous block
|
46
|
-
import http.client
|
47
|
-
conn = http.client.HTTPSConnection(sf_instance.instance_url.replace("https://", ""))
|
48
|
-
conn.request(
|
49
|
-
"GET",
|
50
|
-
'/services/data/v64.0/tooling/executeAnonymous/?anonymousBody=Long%20currentUnixTime%20%3D%20DateTime.now().getTime()%20%2F%201000%3B',
|
51
|
-
headers={
|
52
|
-
"Authorization": f"Bearer {sf_instance.access_token}",
|
53
|
-
"Content-Type": "application/json"
|
54
|
-
}
|
55
|
-
)
|
56
|
-
response = conn.getresponse()
|
57
|
-
if response.status != http.client.OK:
|
58
|
-
pytest.fail(f"Failed to create Apex logs: {response.reason}")
|
59
|
-
if already_executed:
|
60
|
-
pytest.fail("ApexLog creation failed, cannot evaluate Apex log test.")
|
61
|
-
return test_debug_cleanup(sf_instance, already_executed=True)
|
62
|
-
|
63
|
-
sf_instance.debug_cleanup(apex_logs=True)
|
64
|
-
|
65
|
-
apex_logs = sf_instance.query("SELECT Id FROM ApexLog LIMIT 1")
|
66
|
-
apex_log_count = len(apex_logs.get("records", []))
|
67
|
-
assert apex_log_count == 0, "Apex logs were not cleaned up successfully."
|
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
|