pydiagral 1.5.0b1__tar.gz → 1.5.0b2__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.
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/CHANGELOG.md +7 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/PKG-INFO +1 -1
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/example_code.py +75 -39
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/src/pydiagral/api.py +23 -22
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/src/pydiagral/models.py +24 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.devcontainer/Dockerfile.dev +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.devcontainer/devcontainer.json +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.github/FUNDING.yml +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.github/copilot-commit-message-instructions.md +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.github/dependabot.yml +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.github/labels.yml +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.github/workflows/labeler.yml +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.github/workflows/lint.yaml +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.github/workflows/lock.yml +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.github/workflows/pytest.yaml +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.github/workflows/release_and_doc.yaml +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.github/workflows/semantic-prs.yml +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.gitignore +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.releaserc +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.vscode/extensions.json +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/.vscode/settings.json +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/LICENSE +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/README.md +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/docs/api.md +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/docs/exceptions.md +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/docs/how-to-find-diagral-serial.png +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/docs/index.md +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/docs/models.md +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/docs/pydiagral-Logo.png +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/mkdocs.yml +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/pyproject.toml +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/src/pydiagral/__init__.py +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/src/pydiagral/constants.py +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/src/pydiagral/exceptions.py +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/src/pydiagral/utils.py +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/tests/__init__.py +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/tests/data/configuration_sample.json +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/tests/data/system_details_sample.json +0 -0
- {pydiagral-1.5.0b1 → pydiagral-1.5.0b2}/tests/test_pydiagral_api.py +0 -0
@@ -1,3 +1,10 @@
|
|
1
|
+
# [1.5.0-beta.2](https://github.com/mguyard/pydiagral/compare/v1.5.0-beta.1...v1.5.0-beta.2) (2025-02-28)
|
2
|
+
|
3
|
+
|
4
|
+
### Features
|
5
|
+
|
6
|
+
* **api:** 🚀 Add connection testing functionality with result handling ([a07bbfc](https://github.com/mguyard/pydiagral/commit/a07bbfc22a1f41418dad0347394cadcab31685e8))
|
7
|
+
|
1
8
|
# [1.5.0-beta.1](https://github.com/mguyard/pydiagral/compare/v1.4.0...v1.5.0-beta.1) (2025-02-28)
|
2
9
|
|
3
10
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pydiagral
|
3
|
-
Version: 1.5.
|
3
|
+
Version: 1.5.0b2
|
4
4
|
Summary: A Python library for interacting with Diagral systems
|
5
5
|
Project-URL: Homepage, https://github.com/mguyard/pydiagral
|
6
6
|
Project-URL: Documentation, https://github.com/mguyard/pydiagral
|
@@ -9,6 +9,7 @@ from pydiagral.models import (
|
|
9
9
|
ApiKeyWithSecret,
|
10
10
|
SystemDetails,
|
11
11
|
SystemStatus,
|
12
|
+
TryConnectResult,
|
12
13
|
Webhook,
|
13
14
|
)
|
14
15
|
from src.pydiagral import DiagralAPI, DiagralAPIError
|
@@ -22,7 +23,7 @@ logging.basicConfig(
|
|
22
23
|
level=getattr(logging, log_level, logging.INFO),
|
23
24
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
24
25
|
)
|
25
|
-
|
26
|
+
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
26
27
|
|
27
28
|
# Connection information (replace with your own data)
|
28
29
|
# For this, create a .env file in the root of the project with the following content:
|
@@ -47,9 +48,11 @@ WEBHOOK_URL: str | None = os.getenv("WEBHOOK_URL") # Optional
|
|
47
48
|
# Switch to True the actions you want to test
|
48
49
|
TRY_CONNECTION_WITH_KEYS = False
|
49
50
|
TRY_CONNECTION_WITHOUT_KEYS = False
|
50
|
-
|
51
|
-
|
52
|
-
|
51
|
+
TRY_CONNECTION_WITH_KEYS_EPHEMERAL = False
|
52
|
+
TRY_CONNECTION_WITHOUT_KEYS_EPHEMERAL = False
|
53
|
+
APIKEY_CREATION = False
|
54
|
+
APIKEY_DELETION = False
|
55
|
+
LIST_GROUPS = False
|
53
56
|
LIST_SYSTEM_DETAILS = False
|
54
57
|
LIST_SYSTEM_STATUS = False
|
55
58
|
LIST_SYSTEM_CONFIGURATION = False
|
@@ -62,12 +65,16 @@ TEST_PARTIAL_ACTIONS = False
|
|
62
65
|
|
63
66
|
######################################## DON'T MODIFY BELOW THIS LINE ########################################
|
64
67
|
|
65
|
-
if
|
68
|
+
if (
|
69
|
+
APIKEY_CREATION
|
70
|
+
or TRY_CONNECTION_WITHOUT_KEYS
|
71
|
+
or TRY_CONNECTION_WITHOUT_KEYS_EPHEMERAL
|
72
|
+
):
|
66
73
|
API_KEY = None
|
67
74
|
SECRET_KEY = None
|
68
75
|
|
69
76
|
|
70
|
-
async def test_diagral_api() -> None: # noqa: D103
|
77
|
+
async def test_diagral_api() -> None: # noqa: D103,C901
|
71
78
|
try:
|
72
79
|
# Initialization of the DiagralAPI object
|
73
80
|
diagral = DiagralAPI(
|
@@ -78,47 +85,76 @@ async def test_diagral_api() -> None: # noqa: D103
|
|
78
85
|
secret_key=SECRET_KEY,
|
79
86
|
pincode=PIN_CODE,
|
80
87
|
)
|
81
|
-
|
88
|
+
_LOGGER.info("Initialisation de DiagralAPI réussie")
|
82
89
|
|
83
90
|
# Connection to the Diagral API
|
84
91
|
async with diagral as alarm:
|
85
|
-
|
92
|
+
_LOGGER.info("Connection to the Diagral API successful")
|
86
93
|
if APIKEY_CREATION and (not API_KEY or not SECRET_KEY):
|
87
94
|
await alarm.login() # Login to the API
|
88
95
|
# await asyncio.sleep(3700) # Wait for the access token to expire
|
89
96
|
api_keys: ApiKeyWithSecret = (
|
90
97
|
await alarm.set_apikey()
|
91
98
|
) # Create a new API key
|
92
|
-
|
93
|
-
|
99
|
+
_LOGGER.info("API Key: %s", api_keys.api_key) # Display the API key
|
100
|
+
_LOGGER.info(
|
94
101
|
"Secret Key: %s", api_keys.secret_key
|
95
102
|
) # Display the secret key
|
96
103
|
await (
|
97
104
|
alarm.validate_apikey()
|
98
105
|
) # Validate the API key - Optional as already done in set_apikey
|
99
106
|
|
107
|
+
if TRY_CONNECTION_WITH_KEYS:
|
108
|
+
connection: TryConnectResult = await alarm.try_connection(
|
109
|
+
ephemeral=False
|
110
|
+
) # Try to connect to the API with keys
|
111
|
+
if connection.result:
|
112
|
+
_LOGGER.info("Connection (with provided keys) successful")
|
113
|
+
else:
|
114
|
+
_LOGGER.error("Connection (with provided keys) failed")
|
115
|
+
|
100
116
|
if TRY_CONNECTION_WITHOUT_KEYS:
|
101
|
-
|
117
|
+
connection: TryConnectResult = await alarm.try_connection(
|
118
|
+
ephemeral=False
|
119
|
+
) # Try to connect to the API with keys
|
120
|
+
if connection.result:
|
121
|
+
_LOGGER.info("Connection (with provided keys) successful")
|
122
|
+
_LOGGER.info("Generated keys are : %s", connection.keys)
|
123
|
+
_LOGGER.info("Running cleanup by deleting the generated keys")
|
124
|
+
await alarm.delete_apikey(connection.keys.api_key) # For cleanup
|
125
|
+
else:
|
126
|
+
_LOGGER.error("Connection (with provided keys) failed")
|
127
|
+
|
128
|
+
if TRY_CONNECTION_WITH_KEYS_EPHEMERAL:
|
129
|
+
connection: TryConnectResult = await alarm.try_connection(
|
102
130
|
ephemeral=True
|
103
131
|
) # Try to connect to the API without keys
|
104
|
-
if result:
|
105
|
-
|
132
|
+
if connection.result:
|
133
|
+
if connection.keys is not None:
|
134
|
+
_LOGGER.warning(
|
135
|
+
"Keys was returned. Not a normal behavior as keys was provided"
|
136
|
+
)
|
137
|
+
_LOGGER.info("Connection (without provided keys) successful")
|
106
138
|
else:
|
107
|
-
|
139
|
+
_LOGGER.error("Connection (without provided keys) failed")
|
108
140
|
|
109
|
-
if
|
110
|
-
|
111
|
-
ephemeral=
|
112
|
-
) # Try to connect to the API
|
113
|
-
if result:
|
114
|
-
|
141
|
+
if TRY_CONNECTION_WITHOUT_KEYS_EPHEMERAL:
|
142
|
+
connection: TryConnectResult = await alarm.try_connection(
|
143
|
+
ephemeral=True
|
144
|
+
) # Try to connect to the API without keys
|
145
|
+
if connection.result:
|
146
|
+
if connection.keys is not None:
|
147
|
+
_LOGGER.warning(
|
148
|
+
"Keys was returned. Not a normal behavior as keys was provided"
|
149
|
+
)
|
150
|
+
_LOGGER.info("Connection (without provided keys) successful")
|
115
151
|
else:
|
116
|
-
|
152
|
+
_LOGGER.error("Connection (without provided keys) failed")
|
117
153
|
|
118
154
|
if LIST_GROUPS:
|
119
155
|
await alarm.get_devices_info()
|
120
156
|
for group in alarm.alarm_configuration.groups:
|
121
|
-
|
157
|
+
_LOGGER.info(
|
122
158
|
"Group %i: %s",
|
123
159
|
group.index,
|
124
160
|
group.name,
|
@@ -128,80 +164,80 @@ async def test_diagral_api() -> None: # noqa: D103
|
|
128
164
|
system_details: SystemDetails = (
|
129
165
|
await alarm.get_system_details()
|
130
166
|
) # Get the system details
|
131
|
-
|
167
|
+
_LOGGER.info("System Details: %s", system_details)
|
132
168
|
|
133
169
|
if LIST_SYSTEM_STATUS:
|
134
170
|
system_status: SystemStatus = (
|
135
171
|
await alarm.get_system_status()
|
136
172
|
) # Get the system status
|
137
|
-
|
173
|
+
_LOGGER.info("System Status: %s", system_status)
|
138
174
|
|
139
175
|
if LIST_SYSTEM_CONFIGURATION:
|
140
176
|
if not alarm.alarm_configuration:
|
141
177
|
await alarm.get_configuration() # Get the configuration
|
142
|
-
|
178
|
+
_LOGGER.info(
|
143
179
|
"System Configuration: %s",
|
144
180
|
alarm.alarm_configuration.grp_marche_partielle2,
|
145
181
|
)
|
146
182
|
|
147
183
|
if WEHBOOK_REGISTRATION and WEBHOOK_URL:
|
148
|
-
|
184
|
+
_LOGGER.info("-----> WEBHOOK <-----")
|
149
185
|
webhook_register_output: Webhook | None = await alarm.register_webhook(
|
150
186
|
webhook_url=WEBHOOK_URL,
|
151
187
|
subscribe_to_anomaly=True,
|
152
188
|
subscribe_to_alert=True,
|
153
189
|
subscribe_to_state=True,
|
154
190
|
)
|
155
|
-
|
191
|
+
_LOGGER.info("Webhook Register Output: %s", webhook_register_output)
|
156
192
|
webhook_update_output: Webhook | None = await alarm.update_webhook(
|
157
193
|
webhook_url=WEBHOOK_URL,
|
158
194
|
subscribe_to_anomaly=True,
|
159
195
|
subscribe_to_alert=True,
|
160
196
|
subscribe_to_state=True,
|
161
197
|
)
|
162
|
-
|
198
|
+
_LOGGER.info("Webhook Update Output: %s", webhook_update_output)
|
163
199
|
if WEBHOOK_DELETION:
|
164
200
|
await alarm.delete_webhook()
|
165
201
|
webhook_sub: Webhook = await alarm.get_webhook()
|
166
202
|
|
167
|
-
|
203
|
+
_LOGGER.info("Webhook Subscription: %s", webhook_sub)
|
168
204
|
|
169
205
|
if GET_ANOMALIES:
|
170
|
-
|
206
|
+
_LOGGER.info("-----> ANOMALIES <-----")
|
171
207
|
anomalies: Anomalies | dict = await alarm.get_anomalies()
|
172
|
-
|
208
|
+
_LOGGER.info("Anomalies: %s", anomalies)
|
173
209
|
|
174
210
|
if TEST_FULL_ACTIONS:
|
175
211
|
start_result: SystemStatus = (
|
176
212
|
await alarm.start_system()
|
177
213
|
) # Start the system
|
178
|
-
|
214
|
+
_LOGGER.info("Start System with result: %s", start_result)
|
179
215
|
await asyncio.sleep(30) # Wait for 30 seconds
|
180
216
|
stop_result: SystemStatus = await alarm.stop_system() # Stop the system
|
181
|
-
|
217
|
+
_LOGGER.info("Stop System with result: %s", stop_result)
|
182
218
|
|
183
219
|
if TEST_PRESENCE:
|
184
220
|
presence_result: SystemStatus = (
|
185
221
|
await alarm.presence()
|
186
222
|
) # Activate the presence mode
|
187
|
-
|
223
|
+
_LOGGER.info("Presence with result: %s", presence_result)
|
188
224
|
await asyncio.sleep(30) # Wait for 30 seconds
|
189
225
|
stop_result = await alarm.stop_system() # Stop the system
|
190
|
-
|
226
|
+
_LOGGER.info("Stop System with result: %s", stop_result)
|
191
227
|
|
192
228
|
if TEST_PARTIAL_ACTIONS:
|
193
229
|
activategroup_result: SystemStatus = await alarm.activate_group(
|
194
230
|
groups=[1, 2]
|
195
231
|
)
|
196
|
-
|
232
|
+
_LOGGER.info("Activate Group with result: %s", activategroup_result)
|
197
233
|
await asyncio.sleep(30) # Wait for 30 seconds
|
198
234
|
disablegroup_result: SystemStatus = await alarm.disable_group(
|
199
235
|
groups=[2]
|
200
236
|
)
|
201
|
-
|
237
|
+
_LOGGER.info("Disable Group with result: %s", disablegroup_result)
|
202
238
|
await asyncio.sleep(30) # Wait for 30 seconds
|
203
239
|
stop_result = await alarm.stop_system()
|
204
|
-
|
240
|
+
_LOGGER.info("Stop System with result: %s", stop_result)
|
205
241
|
|
206
242
|
if (
|
207
243
|
APIKEY_DELETION and APIKEY_CREATION
|
@@ -209,7 +245,7 @@ async def test_diagral_api() -> None: # noqa: D103
|
|
209
245
|
await alarm.delete_apikey() # Delete the API key
|
210
246
|
|
211
247
|
except DiagralAPIError as e:
|
212
|
-
|
248
|
+
_LOGGER.error("Erreur : %s", e)
|
213
249
|
|
214
250
|
|
215
251
|
if __name__ == "__main__":
|
@@ -36,6 +36,7 @@ from .models import (
|
|
36
36
|
Rudes,
|
37
37
|
SystemDetails,
|
38
38
|
SystemStatus,
|
39
|
+
TryConnectResult,
|
39
40
|
Webhook,
|
40
41
|
)
|
41
42
|
from .utils import generate_hmac_signature
|
@@ -400,48 +401,48 @@ class DiagralAPI:
|
|
400
401
|
self.__secret_key = None
|
401
402
|
|
402
403
|
async def try_connection(self, ephemeral: bool = True) -> bool:
|
403
|
-
"""
|
404
|
+
"""Test connection with the Diagral system.
|
404
405
|
|
405
|
-
This method
|
406
|
-
|
407
|
-
2. If not, generating temporary API keys
|
408
|
-
3. Validating the connection by checking system status
|
409
|
-
4. Optionally cleaning up temporary keys if requested
|
406
|
+
This method tests the connection by either using provided API credentials or generating
|
407
|
+
temporary ones. It validates the connection by checking the system status.
|
410
408
|
|
411
409
|
Args:
|
412
|
-
ephemeral (bool, optional): If True
|
413
|
-
|
410
|
+
ephemeral (bool, optional): If True, temporary API keys will be deleted after
|
411
|
+
connection test. Defaults to True.
|
414
412
|
|
415
413
|
Returns:
|
416
|
-
|
414
|
+
TryConnectResult: Object containing connection test results and optionally API keys
|
415
|
+
if non-ephemeral temporary keys were generated.
|
417
416
|
|
418
417
|
Raises:
|
419
|
-
DiagralAPIError: If connection fails or system status check fails
|
418
|
+
DiagralAPIError: If connection attempt fails or system status check fails.
|
419
|
+
|
420
|
+
Note:
|
421
|
+
If API credentials are not provided during client initialization, temporary
|
422
|
+
keys will be generated (if ephemeral) for the connection test. These keys will be:
|
423
|
+
- Deleted after the test if ephemeral=True
|
424
|
+
- Returned in the result if ephemeral=False
|
420
425
|
|
421
426
|
"""
|
422
427
|
|
428
|
+
result: TryConnectResult = TryConnectResult()
|
423
429
|
api_keys_provided = bool(self.__apikey and self.__secret_key)
|
424
|
-
_LOGGER.warning("API keys provided: %s", api_keys_provided)
|
425
430
|
try:
|
431
|
+
# If API keys are not provided, generate temporary keys
|
426
432
|
if not api_keys_provided:
|
427
433
|
api_key_response: ApiKeyWithSecret = await self.set_apikey()
|
428
|
-
_LOGGER.debug(
|
429
|
-
"TEST CONNECTION - Successfully created temporary API key : %s",
|
430
|
-
api_key_response,
|
431
|
-
)
|
432
|
-
if await self.validate_apikey(apikey=api_key_response.api_key):
|
433
|
-
_LOGGER.debug(
|
434
|
-
"TEST CONNECTION - Successfully validated temporary API key"
|
435
|
-
)
|
436
|
-
self.__apikey: str = api_key_response.api_key
|
437
|
-
self.__secret_key: str = api_key_response.secret_key
|
438
434
|
|
435
|
+
# Retrieve system status to validate connection
|
439
436
|
await self.get_system_status()
|
437
|
+
# If connection is successful, clean up temporary keys if requested (ephemeral)
|
440
438
|
if ephemeral and not api_keys_provided:
|
441
439
|
await self.delete_apikey(apikey=self.__apikey)
|
440
|
+
elif not ephemeral and not api_keys_provided:
|
441
|
+
result.keys = api_key_response
|
442
442
|
except DiagralAPIError as e:
|
443
443
|
raise DiagralAPIError(f"Failed to connect to the system: {e}") from e
|
444
|
-
|
444
|
+
result.result = True
|
445
|
+
return result
|
445
446
|
|
446
447
|
async def get_configuration(self) -> None:
|
447
448
|
"""Asynchronously retrieve the configuration of the Diagral system.
|
@@ -355,6 +355,30 @@ class ApiKeys(CamelCaseModel):
|
|
355
355
|
)
|
356
356
|
|
357
357
|
|
358
|
+
@dataclass
|
359
|
+
class TryConnectResult(CamelCaseModel):
|
360
|
+
"""A class representing the result of an API connection attempt.
|
361
|
+
|
362
|
+
This class is used to store the result of an API connection attempt
|
363
|
+
and the associated API keys if the connection was successful.
|
364
|
+
|
365
|
+
Attributes:
|
366
|
+
result (bool | None): Whether the connection attempt was successful. Defaults to False.
|
367
|
+
keys (ApiKeyWithSecret | None): The API keys associated with the successful connection. Defaults to None.
|
368
|
+
|
369
|
+
Example:
|
370
|
+
>>> result = TryConnectResult(result=True, keys=api_key_obj)
|
371
|
+
>>> print(result.result)
|
372
|
+
True
|
373
|
+
>>> print(result.keys)
|
374
|
+
ApiKeyWithSecret(api_key='abc123', api_secret='xyz789')
|
375
|
+
|
376
|
+
"""
|
377
|
+
|
378
|
+
result: bool | None = False
|
379
|
+
keys: ApiKeyWithSecret | None = None
|
380
|
+
|
381
|
+
|
358
382
|
#####################################
|
359
383
|
# Data models for alarm configuration
|
360
384
|
#####################################
|
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
|