cmsdials 1.5.0__tar.gz → 1.7.0__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.
- {cmsdials-1.5.0 → cmsdials-1.7.0}/PKG-INFO +27 -6
- {cmsdials-1.5.0 → cmsdials-1.7.0}/README.md +24 -2
- cmsdials-1.7.0/cmsdials/__init__.py +5 -0
- cmsdials-1.7.0/cmsdials/_version.py +1 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/auth/bearer.py +1 -1
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/auth/client.py +3 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/oms_proxy/client.py +4 -1
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/utils/api_client.py +19 -7
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/utils/base_model.py +1 -1
- {cmsdials-1.5.0 → cmsdials-1.7.0}/pyproject.toml +20 -6
- cmsdials-1.5.0/cmsdials/__init__.py +0 -4
- {cmsdials-1.5.0 → cmsdials-1.7.0}/LICENSE +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/auth/__init__.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/auth/_base.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/auth/exceptions.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/auth/models.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/auth/secret_key.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/__init__.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/dataset_index/__init__.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/dataset_index/client.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/dataset_index/models.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/file_index/__init__.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/file_index/client.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/file_index/models.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/h1d/__init__.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/h1d/client.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/h1d/models.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/h2d/__init__.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/h2d/client.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/h2d/models.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/lumisection/__init__.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/lumisection/client.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/lumisection/models.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/mes/__init__.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/mes/client.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/mes/models.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/ml_bad_lumisection/__init__.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/ml_bad_lumisection/client.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/ml_bad_lumisection/models.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/ml_models_index/__init__.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/ml_models_index/client.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/ml_models_index/models.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/oms_proxy/__init__.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/oms_proxy/models.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/run/__init__.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/run/client.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/clients/run/models.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/cmsdials.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/filters.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/utils/__init__.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/utils/_json.py +0 -0
- {cmsdials-1.5.0 → cmsdials-1.7.0}/cmsdials/utils/logger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cmsdials
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: The Python api client interface to DIALS service
|
|
5
5
|
Home-page: https://github.com/cms-DQM/dials-py
|
|
6
6
|
Author: Gabriel Moreira
|
|
@@ -18,14 +18,17 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
18
18
|
Provides-Extra: pandas
|
|
19
19
|
Provides-Extra: tqdm
|
|
20
20
|
Requires-Dist: pandas (>=1.5.3) ; extra == "pandas"
|
|
21
|
-
Requires-Dist: pydantic (>=
|
|
21
|
+
Requires-Dist: pydantic (>=2.8.2,<3.0.0)
|
|
22
22
|
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
|
23
23
|
Requires-Dist: tqdm (>=4.66.1) ; extra == "tqdm"
|
|
24
|
-
Requires-Dist:
|
|
25
|
-
Requires-Dist: urllib3 (>=2.2.3,<3.0.0)
|
|
24
|
+
Requires-Dist: urllib3 (>=1.26.16,<2.0.0)
|
|
26
25
|
Project-URL: Repository, https://github.com/cms-DQM/dials-py
|
|
27
26
|
Description-Content-Type: text/markdown
|
|
28
27
|
|
|
28
|
+

|
|
29
|
+

|
|
30
|
+
[](https://badge.fury.io/py/cmsdials)
|
|
31
|
+
|
|
29
32
|
# dials-py
|
|
30
33
|
|
|
31
34
|
The Python api client interface to DIALS service.
|
|
@@ -322,7 +325,7 @@ dials.h2d.list_all(LumisectionHistogram2DFilters(me__regex="EEOT digi occupancy
|
|
|
322
325
|
The repository has some tests written to make sure DIALS responses are compatible with pydantic metaclasses, you can use pytest to run all tests but you need to specify a secret key to authenticate non-interactively against DIALS api:
|
|
323
326
|
|
|
324
327
|
```bash
|
|
325
|
-
SECRET_KEY=... pytest tests
|
|
328
|
+
SECRET_KEY=... pytest tests --cov=cmsdials --cov-report=xml --cov-report=term --cov-report=html
|
|
326
329
|
```
|
|
327
330
|
|
|
328
331
|
The secret key is an api client enabled secret key and can be obtained from the applications portal, any api client secret key whitelisted in DIALS can be used. The interactive authentication flow should be tested manually, an example for this can be found in [this line](/tests/integration/test_auth_client.py#L33).
|
|
@@ -330,6 +333,24 @@ The secret key is an api client enabled secret key and can be obtained from the
|
|
|
330
333
|
If testing against a local version of DIALS you need to specify the BASE_URL:
|
|
331
334
|
|
|
332
335
|
```bash
|
|
333
|
-
SECRET_KEY=...
|
|
336
|
+
SECRET_KEY=... BASE_URL=http://localhost:8000 pytest tests --cov=cmsdials --cov-report=xml --cov-report=term --cov-report=html
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Tox
|
|
340
|
+
|
|
341
|
+
Tox is pre-configured in `pyproject.toml`, so you can run the following to test against multiple python versions locally:
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
SECRET_KEY=... poetry run tox
|
|
334
345
|
```
|
|
335
346
|
|
|
347
|
+
**[asdf](https://asdf-vm.com/) users**
|
|
348
|
+
|
|
349
|
+
tox requires multiple versions of Python to be installed. Using `asdf`, you have multiple versions installed, but they aren’t normally exposed to the current shell. You can use the following command to expose multiple versions of Python in the current directory:
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
asdf set python 3.12.9 3.11.10 3.10.13 3.9.19
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
This will use `3.12.9` by default (if you just run `python`), but it will also put `python3.11`, `python3.10` and `python3.9` symlinks in your path so you can run those too (which is exactly what tox is looking for).
|
|
356
|
+
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+
[](https://badge.fury.io/py/cmsdials)
|
|
4
|
+
|
|
1
5
|
# dials-py
|
|
2
6
|
|
|
3
7
|
The Python api client interface to DIALS service.
|
|
@@ -294,7 +298,7 @@ dials.h2d.list_all(LumisectionHistogram2DFilters(me__regex="EEOT digi occupancy
|
|
|
294
298
|
The repository has some tests written to make sure DIALS responses are compatible with pydantic metaclasses, you can use pytest to run all tests but you need to specify a secret key to authenticate non-interactively against DIALS api:
|
|
295
299
|
|
|
296
300
|
```bash
|
|
297
|
-
SECRET_KEY=... pytest tests
|
|
301
|
+
SECRET_KEY=... pytest tests --cov=cmsdials --cov-report=xml --cov-report=term --cov-report=html
|
|
298
302
|
```
|
|
299
303
|
|
|
300
304
|
The secret key is an api client enabled secret key and can be obtained from the applications portal, any api client secret key whitelisted in DIALS can be used. The interactive authentication flow should be tested manually, an example for this can be found in [this line](/tests/integration/test_auth_client.py#L33).
|
|
@@ -302,5 +306,23 @@ The secret key is an api client enabled secret key and can be obtained from the
|
|
|
302
306
|
If testing against a local version of DIALS you need to specify the BASE_URL:
|
|
303
307
|
|
|
304
308
|
```bash
|
|
305
|
-
SECRET_KEY=...
|
|
309
|
+
SECRET_KEY=... BASE_URL=http://localhost:8000 pytest tests --cov=cmsdials --cov-report=xml --cov-report=term --cov-report=html
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Tox
|
|
313
|
+
|
|
314
|
+
Tox is pre-configured in `pyproject.toml`, so you can run the following to test against multiple python versions locally:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
SECRET_KEY=... poetry run tox
|
|
306
318
|
```
|
|
319
|
+
|
|
320
|
+
**[asdf](https://asdf-vm.com/) users**
|
|
321
|
+
|
|
322
|
+
tox requires multiple versions of Python to be installed. Using `asdf`, you have multiple versions installed, but they aren’t normally exposed to the current shell. You can use the following command to expose multiple versions of Python in the current directory:
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
asdf set python 3.12.9 3.11.10 3.10.13 3.9.19
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
This will use `3.12.9` by default (if you just run `python`), but it will also put `python3.11`, `python3.10` and `python3.9` symlinks in your path so you can run those too (which is exactly what tox is looking for).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.7.0"
|
|
@@ -126,7 +126,7 @@ class Credentials(BaseCredentials):
|
|
|
126
126
|
def cache_credentials(self) -> None:
|
|
127
127
|
fpath = self._handle_creds_filepath(self.cache_dir)
|
|
128
128
|
with open(fpath, "w") as filebuffer:
|
|
129
|
-
json.dump(self._token.
|
|
129
|
+
json.dump(self._token.model_dump(), filebuffer, cls=TokenEncoder)
|
|
130
130
|
|
|
131
131
|
def refresh(self) -> None:
|
|
132
132
|
token: Token = self.client.refresh_token(self.token_type, self.token, self.refresh_token)
|
|
@@ -25,6 +25,7 @@ class AuthClient(BaseAPIClient):
|
|
|
25
25
|
url=endpoint_url,
|
|
26
26
|
headers={
|
|
27
27
|
"accept": "application/json",
|
|
28
|
+
"User-Agent": self._build_user_agent()
|
|
28
29
|
},
|
|
29
30
|
timeout=self.default_timeout,
|
|
30
31
|
)
|
|
@@ -39,6 +40,7 @@ class AuthClient(BaseAPIClient):
|
|
|
39
40
|
headers={
|
|
40
41
|
"accept": "application/json",
|
|
41
42
|
"Content-Type": "application/json",
|
|
43
|
+
"User-Agent": self._build_user_agent()
|
|
42
44
|
},
|
|
43
45
|
json={"device_code": device_code},
|
|
44
46
|
timeout=self.default_timeout,
|
|
@@ -65,6 +67,7 @@ class AuthClient(BaseAPIClient):
|
|
|
65
67
|
"accept": "application/json",
|
|
66
68
|
"Content-Type": "application/json",
|
|
67
69
|
"Authorization": f"{token_type} {access_token}",
|
|
70
|
+
"User-Agent": self._build_user_agent()
|
|
68
71
|
},
|
|
69
72
|
json={"refresh_token": refresh_token},
|
|
70
73
|
timeout=self.default_timeout,
|
|
@@ -23,7 +23,10 @@ class OMSProxyClient(BaseAPIClient):
|
|
|
23
23
|
self.creds = creds
|
|
24
24
|
|
|
25
25
|
def _build_headers(self) -> dict:
|
|
26
|
-
base = {
|
|
26
|
+
base = {
|
|
27
|
+
"accept": "application/json",
|
|
28
|
+
"User-Agent": self._build_user_agent()
|
|
29
|
+
}
|
|
27
30
|
self.creds.before_request(base)
|
|
28
31
|
return base
|
|
29
32
|
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
from importlib import util as importlib_util
|
|
2
3
|
from traceback import format_exc
|
|
3
4
|
from typing import Optional
|
|
4
|
-
from urllib.parse import parse_qs, urlparse
|
|
5
5
|
from warnings import warn
|
|
6
6
|
|
|
7
|
+
import requests
|
|
8
|
+
from pydantic import AnyUrl
|
|
7
9
|
from requests import Response, Session
|
|
8
10
|
from requests.adapters import DEFAULT_RETRIES, HTTPAdapter
|
|
9
11
|
from requests.exceptions import HTTPError
|
|
10
12
|
|
|
13
|
+
from .._version import __version__
|
|
11
14
|
from ..auth._base import BaseCredentials
|
|
12
15
|
from ..utils.logger import logger
|
|
13
16
|
|
|
@@ -51,6 +54,12 @@ class BaseAPIClient:
|
|
|
51
54
|
ret = s.get(*args, **kwargs)
|
|
52
55
|
return ret
|
|
53
56
|
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _build_user_agent():
|
|
59
|
+
py_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
60
|
+
req_version = requests.__version__
|
|
61
|
+
return f"dials-py/{__version__} (python/{py_version}, python-requests/{req_version})"
|
|
62
|
+
|
|
54
63
|
@property
|
|
55
64
|
def api_url(self):
|
|
56
65
|
return self.base_url + self.route + self.version
|
|
@@ -75,7 +84,10 @@ class BaseAuthorizedAPIClient(BaseAPIClient):
|
|
|
75
84
|
self.workspace = workspace
|
|
76
85
|
|
|
77
86
|
def _build_headers(self) -> dict:
|
|
78
|
-
base = {
|
|
87
|
+
base = {
|
|
88
|
+
"accept": "application/json",
|
|
89
|
+
"User-Agent": self._build_user_agent()
|
|
90
|
+
}
|
|
79
91
|
if self.workspace is not None:
|
|
80
92
|
base["Workspace"] = self.workspace
|
|
81
93
|
self.creds.before_request(base)
|
|
@@ -138,13 +150,13 @@ class BaseAuthorizedAPIClient(BaseAPIClient):
|
|
|
138
150
|
keep_failed: bool = False,
|
|
139
151
|
resume_from=None,
|
|
140
152
|
):
|
|
141
|
-
next_string: Optional[
|
|
153
|
+
next_string: Optional[AnyUrl] = None
|
|
142
154
|
results = []
|
|
143
155
|
is_last_page = False
|
|
144
156
|
|
|
145
157
|
if resume_from is not None:
|
|
146
158
|
results = resume_from.results
|
|
147
|
-
next_string = resume_from.next
|
|
159
|
+
next_string: AnyUrl = resume_from.next
|
|
148
160
|
if next_string is None and len(results):
|
|
149
161
|
warn(
|
|
150
162
|
"resume_from.next is None while resume_from.result is not empty, doing nothing.",
|
|
@@ -160,8 +172,8 @@ class BaseAuthorizedAPIClient(BaseAPIClient):
|
|
|
160
172
|
progress = tqdm(desc="Progress", total=1)
|
|
161
173
|
|
|
162
174
|
while is_last_page is False:
|
|
163
|
-
next_token =
|
|
164
|
-
curr_filters = self.filter_class(**filters.
|
|
175
|
+
next_token = dict(next_string.query_params()).get("next_token") if next_string else None
|
|
176
|
+
curr_filters = self.filter_class(**filters.model_dump())
|
|
165
177
|
curr_filters.next_token = next_token
|
|
166
178
|
try:
|
|
167
179
|
response = self.list(curr_filters, retries=retries)
|
|
@@ -186,7 +198,7 @@ class BaseAuthorizedAPIClient(BaseAPIClient):
|
|
|
186
198
|
exc_formatted=exc_formatted,
|
|
187
199
|
)
|
|
188
200
|
results.extend(response.results)
|
|
189
|
-
next_string = response.next
|
|
201
|
+
next_string: AnyUrl = response.next
|
|
190
202
|
is_last_page = next_string is None
|
|
191
203
|
total_pages += 1
|
|
192
204
|
max_pages_reached = max_pages and total_pages >= max_pages
|
|
@@ -16,7 +16,7 @@ class OBaseModel(BaseModel):
|
|
|
16
16
|
def cleandict(self):
|
|
17
17
|
return {
|
|
18
18
|
key: ",".join(str(v) for v in value) if isinstance(value, (list, tuple)) else value
|
|
19
|
-
for key, value in self.
|
|
19
|
+
for key, value in self.model_dump().items()
|
|
20
20
|
if value is not None
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "cmsdials"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.7.0"
|
|
4
4
|
description = "The Python api client interface to DIALS service"
|
|
5
5
|
authors = ["Gabriel Moreira <gabrielmscampos@gmail.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -19,17 +19,18 @@ packages = [
|
|
|
19
19
|
[tool.poetry.dependencies]
|
|
20
20
|
python = "^3.9"
|
|
21
21
|
requests = "^2.31.0"
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
urllib3 = "^1.26.16"
|
|
23
|
+
pydantic = "^2.8.2"
|
|
24
24
|
pandas = {version = ">=1.5.3", optional = true}
|
|
25
25
|
tqdm = {version = ">=4.66.1", optional = true}
|
|
26
|
-
urllib3 = "^2.2.3"
|
|
27
26
|
|
|
28
27
|
[tool.poetry.group.dev.dependencies]
|
|
29
28
|
pre-commit = "^3.6.2"
|
|
30
29
|
ruff = "^0.4.2"
|
|
31
30
|
notebook = "^7.1.3"
|
|
32
31
|
pytest = "^8.2.0"
|
|
32
|
+
pytest-cov = "^6.0.0"
|
|
33
|
+
tox = "^4.24.1"
|
|
33
34
|
|
|
34
35
|
[tool.poetry.extras]
|
|
35
36
|
pandas = ["pandas"]
|
|
@@ -130,6 +131,19 @@ lines-after-imports = 2
|
|
|
130
131
|
"**/{tests,docs}/*" = ["E402", "F403", "F405", "S", "N802"]
|
|
131
132
|
"**/*test*.py" = ["E402", "F403", "F405", "S", "N802"]
|
|
132
133
|
|
|
134
|
+
[tool.poetry-dynamic-versioning]
|
|
135
|
+
enable = false
|
|
136
|
+
|
|
137
|
+
[tool.tox]
|
|
138
|
+
requires = ["tox>=4.24"]
|
|
139
|
+
env_list = ["py39", "py310", "py311", "py312"]
|
|
140
|
+
|
|
141
|
+
[tool.tox.env_run_base]
|
|
142
|
+
description = "Run test under {base_python}"
|
|
143
|
+
commands = [["pytest", "tests"]]
|
|
144
|
+
passenv = ["SECRET_KEY", "BASE_URL"]
|
|
145
|
+
allowlist_externals = ["pytest"]
|
|
146
|
+
|
|
133
147
|
[build-system]
|
|
134
|
-
requires = ["poetry-core"]
|
|
135
|
-
build-backend = "
|
|
148
|
+
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
|
|
149
|
+
build-backend = "poetry_dynamic_versioning.backend"
|
|
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
|