oscar-python 1.3.2__py3-none-any.whl → 1.3.3__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.
- oscar_python/_oidc.py +2 -2
- oscar_python/client.py +14 -5
- {oscar_python-1.3.2.dist-info → oscar_python-1.3.3.dist-info}/METADATA +18 -22
- oscar_python-1.3.3.dist-info/RECORD +18 -0
- oscar_python-1.3.3.dist-info/top_level.txt +1 -0
- oscar_python-1.3.2.dist-info/RECORD +0 -26
- oscar_python-1.3.2.dist-info/top_level.txt +0 -3
- tests/test_client.py +0 -138
- tests/test_default_client.py +0 -61
- tests/test_oidc.py +0 -38
- tests/test_onedata.py +0 -66
- tests/test_s3.py +0 -48
- tests/test_storage.py +0 -51
- tests/test_utils.py +0 -96
- tests/test_webdav.py +0 -50
- {oscar_python-1.3.2.dist-info → oscar_python-1.3.3.dist-info}/WHEEL +0 -0
- {oscar_python-1.3.2.dist-info → oscar_python-1.3.3.dist-info}/licenses/LICENSE +0 -0
oscar_python/_oidc.py
CHANGED
|
@@ -77,14 +77,14 @@ class OIDC(object):
|
|
|
77
77
|
return True
|
|
78
78
|
|
|
79
79
|
@staticmethod
|
|
80
|
-
def refresh_access_token(refresh_token, scopes, token_endpoint):
|
|
80
|
+
def refresh_access_token(refresh_token, scopes, token_endpoint, client_id="token-portal"):
|
|
81
81
|
"""
|
|
82
82
|
Refresh the access token using the refresh token
|
|
83
83
|
"""
|
|
84
84
|
data = {
|
|
85
85
|
'grant_type': 'refresh_token',
|
|
86
86
|
'refresh_token': refresh_token,
|
|
87
|
-
'client_id':
|
|
87
|
+
'client_id': client_id,
|
|
88
88
|
'scope': ' '.join(scopes)
|
|
89
89
|
}
|
|
90
90
|
|
oscar_python/client.py
CHANGED
|
@@ -41,6 +41,7 @@ _DELETE = "delete"
|
|
|
41
41
|
# Default values for OIDC refresh token using EGI CheckIn
|
|
42
42
|
_DEFAULT_SCOPES = ['openid', 'email', 'profile', 'voperson_id', 'eduperson_entitlement']
|
|
43
43
|
_DEFAULT_TOKEN_ENDPOINT = 'https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/token'
|
|
44
|
+
_DEFAULT_CLIENT_ID = 'token-portal'
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
class Client(DefaultClient):
|
|
@@ -72,6 +73,8 @@ class Client(DefaultClient):
|
|
|
72
73
|
self.token_endpoint = options.get('token_endpoint',
|
|
73
74
|
_DEFAULT_TOKEN_ENDPOINT)
|
|
74
75
|
self.ssl = bool(options['ssl'])
|
|
76
|
+
self.client_id = options.get('client_id',
|
|
77
|
+
_DEFAULT_CLIENT_ID)
|
|
75
78
|
|
|
76
79
|
def set_auth_type(self, options):
|
|
77
80
|
if 'user' in options:
|
|
@@ -91,7 +94,8 @@ class Client(DefaultClient):
|
|
|
91
94
|
if self.refresh_token and OIDC.is_access_token_expired(self.oidc_token):
|
|
92
95
|
self.oidc_token = OIDC.refresh_access_token(self.refresh_token,
|
|
93
96
|
self.scopes,
|
|
94
|
-
self.token_endpoint
|
|
97
|
+
self.token_endpoint,
|
|
98
|
+
self.client_id)
|
|
95
99
|
return self.oidc_token
|
|
96
100
|
|
|
97
101
|
""" Creates a generic storage client to interact with the storage providers
|
|
@@ -132,9 +136,14 @@ class Client(DefaultClient):
|
|
|
132
136
|
except KeyError as err:
|
|
133
137
|
raise Exception("FDL clusterID does not match current clusterID: {0}".format(err))
|
|
134
138
|
try:
|
|
135
|
-
|
|
139
|
+
if os.path.isabs(svc["script"]):
|
|
140
|
+
script_path = svc["script"]
|
|
141
|
+
else:
|
|
142
|
+
fdl_directory = os.path.dirname(fdl_path)
|
|
143
|
+
script_path = os.path.join(fdl_directory, svc['script'])
|
|
144
|
+
with open(script_path) as s:
|
|
136
145
|
svc["script"] = s.read()
|
|
137
|
-
except IOError:
|
|
146
|
+
except IOError as e:
|
|
138
147
|
raise Exception("Couldn't read script")
|
|
139
148
|
|
|
140
149
|
# cpu parameter has to be string on the request
|
|
@@ -205,8 +214,8 @@ class Client(DefaultClient):
|
|
|
205
214
|
return utils.make_request(self, _LOGS_PATH+"/"+svc+"/"+job, _GET)
|
|
206
215
|
|
|
207
216
|
""" List a service jobs """
|
|
208
|
-
def list_jobs(self, svc):
|
|
209
|
-
return utils.make_request(self, _LOGS_PATH+"/"+svc, _GET)
|
|
217
|
+
def list_jobs(self, svc, page=""):
|
|
218
|
+
return utils.make_request(self, _LOGS_PATH+"/"+svc+"?page="+page, _GET)
|
|
210
219
|
|
|
211
220
|
""" Remove a service job """
|
|
212
221
|
def remove_job(self, svc, job):
|
|
@@ -1,37 +1,30 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
|
-
Name:
|
|
3
|
-
Version: 1.3.
|
|
4
|
-
Summary:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
Name: oscar-python
|
|
3
|
+
Version: 1.3.3
|
|
4
|
+
Summary: Python client for OSCAR clusters
|
|
5
|
+
Author: GRyCAP - I3M - UPV
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/grycap/oscar-python
|
|
8
|
+
Project-URL: Repository, https://github.com/grycap/oscar-python
|
|
9
|
+
Keywords: oscar,faas,serverless
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Requires-Dist: webdavclient3==3.14.6
|
|
14
13
|
Requires-Dist: requests
|
|
14
|
+
Requires-Dist: webdavclient3>=3.14.6
|
|
15
15
|
Requires-Dist: boto3
|
|
16
|
-
Requires-Dist: setuptools>=40.8.0
|
|
17
16
|
Requires-Dist: pyyaml
|
|
18
17
|
Requires-Dist: aiohttp
|
|
19
18
|
Requires-Dist: liboidcagent
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Dynamic: description
|
|
24
|
-
Dynamic: description-content-type
|
|
25
|
-
Dynamic: home-page
|
|
26
|
-
Dynamic: license
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
27
22
|
Dynamic: license-file
|
|
28
|
-
Dynamic: requires-dist
|
|
29
|
-
Dynamic: summary
|
|
30
23
|
|
|
31
24
|
## Python OSCAR client
|
|
32
25
|
|
|
33
|
-
[](https://github.com/grycap/oscar_python/actions/workflows/tests.yaml)
|
|
27
|
+
[](https://pypi.org/project/oscar-python/)
|
|
35
28
|
|
|
36
29
|
This package provides a client to interact with OSCAR (https://oscar.grycap.net) clusters and services. It is available on Pypi with the name [oscar-python](https://pypi.org/project/oscar-python/).
|
|
37
30
|
|
|
@@ -107,6 +100,7 @@ and `scopes`:
|
|
|
107
100
|
'refresh_token':'token',
|
|
108
101
|
'scopes': ["openid", "profile", "email"],
|
|
109
102
|
'token_endpoint': "http://issuer.com/token",
|
|
103
|
+
'client_id': "your_client_id"
|
|
110
104
|
'ssl':'True'}
|
|
111
105
|
|
|
112
106
|
client = Client(options = options_oidc_auth)
|
|
@@ -231,6 +225,8 @@ logs = client.get_job_logs("service_name", "job_id") # returns an http response
|
|
|
231
225
|
``` python
|
|
232
226
|
# get a list of jobs in a service
|
|
233
227
|
log_list = client.list_jobs("service_name") # returns an http response
|
|
228
|
+
# to get more jobs use the page parameter
|
|
229
|
+
log_list = client.list_jobs("service_name",page="token_to_next_page") # returns an http response
|
|
234
230
|
```
|
|
235
231
|
|
|
236
232
|
**remove_job**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
oscar_python/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
oscar_python/_oidc.py,sha256=0JqOuASAnyOVWKxqIWFN9jFZgYY4p-rZwa8Lq3JWfNk,2611
|
|
3
|
+
oscar_python/_utils.py,sha256=e6DJA_Vziy-3xt_-MEOg_7CM2mNIJNqCaFKD79Spug4,3707
|
|
4
|
+
oscar_python/client.py,sha256=eM9FaxEVZnqlLa_Z9Hf4Pgqf63UUNT0BqyXb2SSgrcw,8957
|
|
5
|
+
oscar_python/client_anon.py,sha256=glGC0kfb2iTTcdFhY5oBtQT7l4OriZfSkswQ3icS_YM,311
|
|
6
|
+
oscar_python/default_client.py,sha256=jjIoZ_BkIkCchlTiUoWddEz6UhCFOEVZMCYcSh-E_y8,1154
|
|
7
|
+
oscar_python/local_test.py,sha256=0i_vGn-N735pvx1JcG4GmjpNmqwXmx5LExVELtW5ECs,251
|
|
8
|
+
oscar_python/storage.py,sha256=BVQ7NHqY5OT2WBXqGTsshCX9b0FNqc0SqMoawvsWxiI,3976
|
|
9
|
+
oscar_python/_providers/_minio.py,sha256=k74Xh11Gk6xkKhIMq3jY-4Xq-jpxiZSMc2RLS6WUuq0,1267
|
|
10
|
+
oscar_python/_providers/_onedata.py,sha256=wKxSAO77ipy8Wm-sybqPo_l2BZYg0cj9B53Ebc92kFs,3469
|
|
11
|
+
oscar_python/_providers/_providers_base.py,sha256=Wmse3xheb1UQRulF2IdtJkq2w96l21anQVAA2ChI63Y,1202
|
|
12
|
+
oscar_python/_providers/_s3.py,sha256=kVK6GhkCqRDvUOzopsRFgt9ZS-KK2ugjPRqJYJG9MO8,3260
|
|
13
|
+
oscar_python/_providers/_webdav.py,sha256=Nt0w87L9K6iGLoMOgHoSfiZpjOd1eYkvuJ8Kopf2IQU,2332
|
|
14
|
+
oscar_python-1.3.3.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
|
15
|
+
oscar_python-1.3.3.dist-info/METADATA,sha256=efgJdP09L39GOldIBgrG9xnsn70XU0mn4DP_DptFhiE,9612
|
|
16
|
+
oscar_python-1.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
17
|
+
oscar_python-1.3.3.dist-info/top_level.txt,sha256=0sLOepeO4vz_8o3vBJyTRRvgJN1TFF1T3SESlIwoJQk,13
|
|
18
|
+
oscar_python-1.3.3.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
oscar_python
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
oscar_python/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
oscar_python/_oidc.py,sha256=Rhsw0ywfkZvDOElMOK8DOBfBkA2H_NmbBn5_0bQoo34,2590
|
|
3
|
-
oscar_python/_utils.py,sha256=e6DJA_Vziy-3xt_-MEOg_7CM2mNIJNqCaFKD79Spug4,3707
|
|
4
|
-
oscar_python/client.py,sha256=3W55myFdqaMZ9zQVfj3iOcw8QUVzXxvWImsMN7-RLAQ,8416
|
|
5
|
-
oscar_python/client_anon.py,sha256=glGC0kfb2iTTcdFhY5oBtQT7l4OriZfSkswQ3icS_YM,311
|
|
6
|
-
oscar_python/default_client.py,sha256=jjIoZ_BkIkCchlTiUoWddEz6UhCFOEVZMCYcSh-E_y8,1154
|
|
7
|
-
oscar_python/local_test.py,sha256=0i_vGn-N735pvx1JcG4GmjpNmqwXmx5LExVELtW5ECs,251
|
|
8
|
-
oscar_python/storage.py,sha256=BVQ7NHqY5OT2WBXqGTsshCX9b0FNqc0SqMoawvsWxiI,3976
|
|
9
|
-
oscar_python/_providers/_minio.py,sha256=k74Xh11Gk6xkKhIMq3jY-4Xq-jpxiZSMc2RLS6WUuq0,1267
|
|
10
|
-
oscar_python/_providers/_onedata.py,sha256=wKxSAO77ipy8Wm-sybqPo_l2BZYg0cj9B53Ebc92kFs,3469
|
|
11
|
-
oscar_python/_providers/_providers_base.py,sha256=Wmse3xheb1UQRulF2IdtJkq2w96l21anQVAA2ChI63Y,1202
|
|
12
|
-
oscar_python/_providers/_s3.py,sha256=kVK6GhkCqRDvUOzopsRFgt9ZS-KK2ugjPRqJYJG9MO8,3260
|
|
13
|
-
oscar_python/_providers/_webdav.py,sha256=Nt0w87L9K6iGLoMOgHoSfiZpjOd1eYkvuJ8Kopf2IQU,2332
|
|
14
|
-
oscar_python-1.3.2.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
|
15
|
-
tests/test_client.py,sha256=arhLqBVCWVfbVAKaNY3mavAz0hW16odmqY8QI8SyX5o,5097
|
|
16
|
-
tests/test_default_client.py,sha256=hXWUHS1UI-GWTtK1D6lA8xW8TpgUhzlIbP4mOhiyY-o,2606
|
|
17
|
-
tests/test_oidc.py,sha256=SBeKfXHH_Rej1I1xt6Tripy7GALPVohqnb03PMTRPYs,1641
|
|
18
|
-
tests/test_onedata.py,sha256=XwAQ6GCMHecM8ak8XMjkM5X9JTlajrvQXHsy6-eMYOM,2586
|
|
19
|
-
tests/test_s3.py,sha256=M-PsoGtc006is8dTXWVl7Q2rOJ6ZroMBE6EO49gLdXA,1882
|
|
20
|
-
tests/test_storage.py,sha256=JaD-Vs1nJVN7FH4jljvsogWaGmO2MoQn6Xza7PoyhEg,1879
|
|
21
|
-
tests/test_utils.py,sha256=RfzBpmc0HhLklLUs9ukZx7bStHlL0JBYn3q9i5mJHQM,3307
|
|
22
|
-
tests/test_webdav.py,sha256=Vv0y2CrXAt8z73lgpmC81Vib16qlguGgwTxB9JEcq14,1568
|
|
23
|
-
oscar_python-1.3.2.dist-info/METADATA,sha256=dV_lqMzrc-MhM8BkvtAs2vEZnavMqEsEfdMRDZwwgsM,9525
|
|
24
|
-
oscar_python-1.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
25
|
-
oscar_python-1.3.2.dist-info/top_level.txt,sha256=JlQqH-wbX-NZn0xlDIEZUOMjymNz_9fHfC4goTMZaPY,35
|
|
26
|
-
oscar_python-1.3.2.dist-info/RECORD,,
|
tests/test_client.py
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
import json
|
|
3
|
-
from unittest.mock import patch, mock_open
|
|
4
|
-
from oscar_python.client import Client
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@pytest.fixture
|
|
8
|
-
def options():
|
|
9
|
-
return {
|
|
10
|
-
'cluster_id': 'test_cluster',
|
|
11
|
-
'endpoint': 'http://test.endpoint',
|
|
12
|
-
'user': 'test_user',
|
|
13
|
-
'password': 'test_password',
|
|
14
|
-
'ssl': True,
|
|
15
|
-
'shortname': 'test_shortname',
|
|
16
|
-
'oidc_token': 'test_oidc_token'
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_basic_auth_client(options):
|
|
21
|
-
client = Client(options)
|
|
22
|
-
assert client.id == options['cluster_id']
|
|
23
|
-
assert client.endpoint == options['endpoint']
|
|
24
|
-
assert client.user == options['user']
|
|
25
|
-
assert client.password == options['password']
|
|
26
|
-
assert client.ssl == options['ssl']
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def test_oidc_agent_client(options):
|
|
30
|
-
del options['user']
|
|
31
|
-
client = Client(options)
|
|
32
|
-
assert client.id == options['cluster_id']
|
|
33
|
-
assert client.endpoint == options['endpoint']
|
|
34
|
-
assert client.shortname == options['shortname']
|
|
35
|
-
assert client.ssl == options['ssl']
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_oidc_client(options):
|
|
39
|
-
del options['user']
|
|
40
|
-
del options['shortname']
|
|
41
|
-
client = Client(options)
|
|
42
|
-
assert client.id == options['cluster_id']
|
|
43
|
-
assert client.endpoint == options['endpoint']
|
|
44
|
-
assert client.oidc_token == options['oidc_token']
|
|
45
|
-
assert client.ssl == options['ssl']
|
|
46
|
-
|
|
47
|
-
del options['oidc_token']
|
|
48
|
-
options['refresh_token'] = 'test_refresh_token'
|
|
49
|
-
options['scopes'] = ['openid', 'profile', 'email']
|
|
50
|
-
options['token_endpoint'] = 'test_token_endpoint'
|
|
51
|
-
client = Client(options)
|
|
52
|
-
assert client.refresh_token == options['refresh_token']
|
|
53
|
-
assert client.scopes == ['openid', 'profile', 'email']
|
|
54
|
-
assert client.token_endpoint == options['token_endpoint']
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def test_set_auth_type(options):
|
|
58
|
-
client = Client(options)
|
|
59
|
-
assert client._AUTH_TYPE == "basicauth"
|
|
60
|
-
|
|
61
|
-
del options['user']
|
|
62
|
-
client = Client(options)
|
|
63
|
-
assert client._AUTH_TYPE == "oidc-agent"
|
|
64
|
-
|
|
65
|
-
del options['shortname']
|
|
66
|
-
options['oidc_token'] = 'test_oidc_token'
|
|
67
|
-
client = Client(options)
|
|
68
|
-
assert client._AUTH_TYPE == "oidc"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def test_get_cluster_info(options):
|
|
72
|
-
client = Client(options)
|
|
73
|
-
with patch('oscar_python._utils.make_request') as mock_request:
|
|
74
|
-
client.get_cluster_info()
|
|
75
|
-
mock_request.assert_called_once_with(client, "/system/info", "get")
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def test_get_cluster_config(options):
|
|
79
|
-
client = Client(options)
|
|
80
|
-
with patch('oscar_python._utils.make_request') as mock_request:
|
|
81
|
-
client.get_cluster_config()
|
|
82
|
-
mock_request.assert_called_once_with(client, "/system/config", "get")
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def test_list_services(options):
|
|
86
|
-
client = Client(options)
|
|
87
|
-
with patch('oscar_python._utils.make_request') as mock_request:
|
|
88
|
-
client.list_services()
|
|
89
|
-
mock_request.assert_called_once_with(client, "/system/services", "get")
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def test_get_service(options):
|
|
93
|
-
client = Client(options)
|
|
94
|
-
with patch('oscar_python._utils.make_request') as mock_request:
|
|
95
|
-
client.get_service("test_service")
|
|
96
|
-
mock_request.assert_called_once_with(client, "/system/services/test_service", "get")
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def test_create_service_from_dict(options):
|
|
100
|
-
client = Client(options)
|
|
101
|
-
service_definition = {"name": "test_service"}
|
|
102
|
-
with patch('oscar_python._utils.make_request') as mock_request:
|
|
103
|
-
client.create_service(service_definition)
|
|
104
|
-
mock_request.assert_called_with(client, "/system/services", "post",
|
|
105
|
-
data=json.dumps(service_definition))
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def test_create_service_from_file(options):
|
|
109
|
-
client = Client(options)
|
|
110
|
-
service_definition = "functions:\n oscar:\n - test_cluster:\n name: test_service\n script: test_script\n cpu: 1"
|
|
111
|
-
service_file = "path/to/service.yaml"
|
|
112
|
-
with patch('os.path.isfile', return_value=True), \
|
|
113
|
-
patch('builtins.open', mock_open(read_data=service_definition)), \
|
|
114
|
-
patch('oscar_python._utils.make_request') as mock_request:
|
|
115
|
-
client.create_service(service_file)
|
|
116
|
-
assert mock_request.call_args[0][0] == client
|
|
117
|
-
assert mock_request.call_args[0][1] == "/system/services"
|
|
118
|
-
assert mock_request.call_args[0][2] == "post"
|
|
119
|
-
assert json.loads(mock_request.call_args[1]['data']) == {"name": "test_service",
|
|
120
|
-
"cpu": "1",
|
|
121
|
-
"script": service_definition}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def test_update_service_from_dict(options):
|
|
125
|
-
client = Client(options)
|
|
126
|
-
new_service = {"name": "test_service"}
|
|
127
|
-
with patch('oscar_python._utils.make_request') as mock_request:
|
|
128
|
-
mock_request.return_value.status_code = 200
|
|
129
|
-
client.update_service("test_service", new_service)
|
|
130
|
-
mock_request.assert_called_with(client, "/system/services",
|
|
131
|
-
"put", data=json.dumps(new_service))
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def test_remove_service(options):
|
|
135
|
-
client = Client(options)
|
|
136
|
-
with patch('oscar_python._utils.make_request') as mock_request:
|
|
137
|
-
client.remove_service("test_service")
|
|
138
|
-
mock_request.assert_called_once_with(client, "/system/services/test_service", "delete")
|
tests/test_default_client.py
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from unittest.mock import patch, MagicMock
|
|
3
|
-
from oscar_python.default_client import DefaultClient, _RUN_PATH, _POST, _JOB_PATH
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class TestDefaultClient(DefaultClient):
|
|
7
|
-
def _get_token(self, name):
|
|
8
|
-
return "test_token"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@pytest.fixture
|
|
12
|
-
def client():
|
|
13
|
-
return TestDefaultClient()
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@patch('oscar_python._utils.make_request')
|
|
17
|
-
@patch('oscar_python._utils.encode_input')
|
|
18
|
-
@patch('oscar_python._utils.decode_output')
|
|
19
|
-
def test_run_service_with_input_and_token(mock_decode_output, mock_encode_input, mock_make_request, client):
|
|
20
|
-
mock_response = MagicMock()
|
|
21
|
-
mock_response.text = "response_text"
|
|
22
|
-
mock_make_request.return_value = mock_response
|
|
23
|
-
mock_encode_input.return_value = "encoded_input"
|
|
24
|
-
|
|
25
|
-
response = client.run_service("test_service", input="test_input", token="test_token", output="output_file", timeout=30)
|
|
26
|
-
|
|
27
|
-
mock_encode_input.assert_called_once_with("test_input")
|
|
28
|
-
mock_make_request.assert_called_once_with(client, _RUN_PATH+"/test_service", _POST, data="encoded_input", token="test_token", timeout=30)
|
|
29
|
-
mock_decode_output.assert_called_once_with("response_text", "output_file")
|
|
30
|
-
assert response == mock_response
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@patch('oscar_python._utils.make_request')
|
|
34
|
-
@patch('oscar_python._utils.encode_input')
|
|
35
|
-
def test_run_service_with_input_no_token(mock_encode_input, mock_make_request, client):
|
|
36
|
-
mock_response = MagicMock()
|
|
37
|
-
mock_response.text = "response_text"
|
|
38
|
-
mock_make_request.return_value = mock_response
|
|
39
|
-
mock_encode_input.return_value = "encoded_input"
|
|
40
|
-
|
|
41
|
-
response = client.run_service("test_service", input="test_input")
|
|
42
|
-
|
|
43
|
-
mock_encode_input.assert_called_once_with("test_input")
|
|
44
|
-
mock_make_request.assert_called_once_with(client, _RUN_PATH+"/test_service", _POST, data="encoded_input", token="test_token", timeout=None)
|
|
45
|
-
assert response == mock_response
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@patch('oscar_python._utils.make_request')
|
|
49
|
-
def test_run_service_no_input(mock_make_request, client):
|
|
50
|
-
mock_response = MagicMock()
|
|
51
|
-
mock_response.text = "response_text"
|
|
52
|
-
mock_make_request.return_value = mock_response
|
|
53
|
-
|
|
54
|
-
response = client.run_service("test_service", input="data")
|
|
55
|
-
mock_make_request.assert_called_with(client, _RUN_PATH+"/test_service", _POST,
|
|
56
|
-
token="test_token", data=b'ZGF0YQ==', timeout=None)
|
|
57
|
-
|
|
58
|
-
response = client.run_service("test_service", input="data", async_call=True, timeout=30)
|
|
59
|
-
mock_make_request.assert_called_with(client, _JOB_PATH+"/test_service", _POST,
|
|
60
|
-
token="test_token", data=b'ZGF0YQ==', timeout=30)
|
|
61
|
-
assert response == mock_response
|
tests/test_oidc.py
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
from unittest.mock import patch, MagicMock
|
|
2
|
-
from oscar_python._oidc import OIDC
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def test_is_access_token_expired():
|
|
6
|
-
token = ("eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkYzVkNWFiNy02ZGI5LTQwNzktOTg1Yy04MGFjMDUwMTcw"
|
|
7
|
-
"NjYiLCJpc3MiOiJodHRwczpcL1wvaWFtLXRlc3QuaW5kaWdvLWRhdGFjbG91ZC5ldVwvIiwiZXhwIjoxNDY2MDkzOTE3LCJ"
|
|
8
|
-
"pYXQiOjE0NjYwOTAzMTcsImp0aSI6IjE1OTU2N2U2LTdiYzItNDUzOC1hYzNhLWJjNGU5MmE1NjlhMCJ9.eINKxJa2J--xd"
|
|
9
|
-
"GAZWIOKtx9Wi0Vz3xHzaSJWWY-UHWy044TQ5xYtt0VTvmY5Af-ngwAMGfyaqAAvNn1VEP-_fMYQZdwMqcXLsND4KkDi1ygiC"
|
|
10
|
-
"IwQ3JBz9azBT1o_oAHE5BsPsE2BjfDoVRasZxxW5UoXCmBslonYd8HK2tUVjz0")
|
|
11
|
-
assert OIDC.is_access_token_expired(token)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_refresh_access_token():
|
|
15
|
-
mock_response = MagicMock()
|
|
16
|
-
mock_response.status_code = 200
|
|
17
|
-
mock_response.json.return_value = {
|
|
18
|
-
"access_token": "new_access_token",
|
|
19
|
-
"expires_in": 3600,
|
|
20
|
-
"refresh_token": "new_refresh_token"
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
with patch("requests.post") as mock_post:
|
|
24
|
-
mock_post.return_value = mock_response
|
|
25
|
-
access_token = OIDC.refresh_access_token("old_refresh_token",
|
|
26
|
-
["openid", "profile", "email"],
|
|
27
|
-
"http://test.com/token")
|
|
28
|
-
|
|
29
|
-
assert access_token == "new_access_token"
|
|
30
|
-
mock_post.assert_called_once_with(
|
|
31
|
-
"http://test.com/token",
|
|
32
|
-
data={
|
|
33
|
-
"grant_type": "refresh_token",
|
|
34
|
-
"refresh_token": "old_refresh_token",
|
|
35
|
-
"client_id": "token-portal",
|
|
36
|
-
"scope": "openid profile email"
|
|
37
|
-
}
|
|
38
|
-
)
|
tests/test_onedata.py
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from oscar_python._providers._onedata import Onedata
|
|
3
|
-
from unittest.mock import MagicMock, patch, mock_open
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@pytest.fixture
|
|
7
|
-
def onedata():
|
|
8
|
-
credentials = {
|
|
9
|
-
"token": "token",
|
|
10
|
-
"space": "space",
|
|
11
|
-
"oneprovider_host": "oneprovider_host"
|
|
12
|
-
}
|
|
13
|
-
return Onedata(credentials)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def test_onedata_initialization(onedata):
|
|
17
|
-
assert isinstance(onedata, Onedata)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@patch('oscar_python._providers._onedata.requests')
|
|
21
|
-
def test_onedata_upload_file(mock_requests, onedata):
|
|
22
|
-
mock_response = MagicMock()
|
|
23
|
-
mock_response.status_code = 201
|
|
24
|
-
mock_requests.put.return_value = mock_response
|
|
25
|
-
with patch("builtins.open", mock_open()):
|
|
26
|
-
response = onedata.upload_file('file_path', 'remote_path')
|
|
27
|
-
assert response is None
|
|
28
|
-
mock_requests.get.assert_called_with(
|
|
29
|
-
url='https://oneprovider_host/cdmi/space//remote_path/',
|
|
30
|
-
headers={'X-CDMI-Specification-Version': '1.1.1', 'X-Auth-Token': 'token'}
|
|
31
|
-
)
|
|
32
|
-
mock_requests.put.call_args_list[0][1]['url'] == 'https://oneprovider_host/cdmi/space//remote_path/file_path'
|
|
33
|
-
mock_requests.put.call_args_list[0][1]['headers'] == {'X-Auth-Token', 'token'}
|
|
34
|
-
|
|
35
|
-
mock_requests.put.call_args_list[1][0] == 'https://oneprovider_host/cdmi/space//remote_path/file_path'
|
|
36
|
-
mock_requests.put.call_args_list[1][1]['headers'] == {'X-Auth-Token', 'token'}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@patch('oscar_python._providers._onedata.requests')
|
|
40
|
-
def test_onedata_download_file(mock_requests, onedata):
|
|
41
|
-
mock_response = MagicMock()
|
|
42
|
-
mock_response.status_code = 200
|
|
43
|
-
mock_response.content = b'content'
|
|
44
|
-
mock_requests.get.return_value = mock_response
|
|
45
|
-
with patch("builtins.open", mock_open()) as mock_file:
|
|
46
|
-
onedata.download_file('local_path', 'remote_path')
|
|
47
|
-
mock_file.assert_called_with('local_path/remote_path', 'wb')
|
|
48
|
-
mock_file().write.assert_called_with(b'content')
|
|
49
|
-
mock_requests.get.assert_called_with(
|
|
50
|
-
url='https://oneprovider_host/cdmi/space/remote_path',
|
|
51
|
-
headers={'X-Auth-Token': 'token'}
|
|
52
|
-
)
|
|
53
|
-
mock_requests.get.return_value.content == b'content'
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
@patch('oscar_python._providers._onedata.requests')
|
|
57
|
-
def test_onedata_list_files_from_path(mock_requests, onedata):
|
|
58
|
-
mock_response = MagicMock()
|
|
59
|
-
mock_response.status_code = 200
|
|
60
|
-
mock_requests.get.return_value = mock_response
|
|
61
|
-
response = onedata.list_files_from_path('path')
|
|
62
|
-
assert response == mock_response
|
|
63
|
-
mock_requests.get.assert_called_with(
|
|
64
|
-
url='https://oneprovider_host/cdmi/space/path/',
|
|
65
|
-
headers={'X-CDMI-Specification-Version': '1.1.1', 'X-Auth-Token': 'token'}
|
|
66
|
-
)
|
tests/test_s3.py
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from oscar_python._providers._s3 import S3
|
|
3
|
-
from unittest.mock import MagicMock, patch, mock_open
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@pytest.fixture
|
|
7
|
-
def s3_client():
|
|
8
|
-
credentials = {
|
|
9
|
-
"access_key": "access_key",
|
|
10
|
-
"secret_key": "secret_key",
|
|
11
|
-
"region": "region"
|
|
12
|
-
}
|
|
13
|
-
return S3(credentials)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def test_s3_initialization(s3_client):
|
|
17
|
-
assert s3_client is not None
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_s3_upload_file(s3_client):
|
|
21
|
-
s3_client.client = MagicMock(["upload_fileobj"])
|
|
22
|
-
with patch("builtins.open", mock_open(read_data="data")) as mock_file:
|
|
23
|
-
result = s3_client.upload_file('path/file.txt', 'test_bucket/folder')
|
|
24
|
-
mock_file.assert_called_once_with("path/file.txt", "rb")
|
|
25
|
-
s3_client.client.upload_fileobj.assert_called_once()
|
|
26
|
-
s3_client.client.upload_fileobj.call_args[0][1] == 'test_bucket'
|
|
27
|
-
s3_client.client.upload_fileobj.call_args[0][2] == 'folder/file.txt'
|
|
28
|
-
assert result is True
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def test_s3_download_file(s3_client):
|
|
32
|
-
s3_client.client = MagicMock(["download_fileobj"])
|
|
33
|
-
with patch("builtins.open", mock_open()) as mock_file:
|
|
34
|
-
result = s3_client.download_file('path/folder', 'test_bucket/file.txt')
|
|
35
|
-
mock_file.assert_called_once_with("path/folder/file.txt", "wb")
|
|
36
|
-
s3_client.client.download_fileobj.assert_called_once()
|
|
37
|
-
s3_client.client.download_fileobj.call_args[0][1] == 'test_bucket'
|
|
38
|
-
s3_client.client.download_fileobj.call_args[0][2] == 'folder/file.txt'
|
|
39
|
-
assert result is True
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def test_s3_list_files(s3_client):
|
|
43
|
-
s3_client.client = MagicMock(["list_objects"])
|
|
44
|
-
s3_client.client.list_objects.return_value = ['file1', 'file2']
|
|
45
|
-
result = s3_client.list_files_from_path('test_bucket/*.txt')
|
|
46
|
-
assert result == ['file1', 'file2']
|
|
47
|
-
s3_client.client.list_objects.assert_called_once_with(Bucket='test_bucket',
|
|
48
|
-
Prefix='*.txt')
|
tests/test_storage.py
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from unittest.mock import MagicMock, patch
|
|
3
|
-
from oscar_python.storage import Storage
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@pytest.fixture
|
|
7
|
-
def mock_client_obj():
|
|
8
|
-
return MagicMock()
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@pytest.fixture
|
|
12
|
-
def storage(mock_client_obj):
|
|
13
|
-
mock_response = MagicMock()
|
|
14
|
-
mock_response.text = '{"minio_provider": {"access_key": "key","secret_key": "secret", "endpoint": "http://test.endpoint", "region": "us-east-1", "verify": false}}'
|
|
15
|
-
with patch('oscar_python._utils.make_request', return_value=mock_response):
|
|
16
|
-
return Storage(mock_client_obj)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def test_get_provider_creds(storage):
|
|
20
|
-
creds = storage._get_provider_creds("minio", "default")
|
|
21
|
-
assert creds["access_key"] == "key"
|
|
22
|
-
assert creds["secret_key"] == "secret"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def test_get_client_minio(storage):
|
|
26
|
-
client = storage._get_client("minio.default")
|
|
27
|
-
assert client.__class__.__name__ == "Minio"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def test_list_files_from_path(storage):
|
|
31
|
-
with patch.object(storage, '_get_client') as mock_get_client:
|
|
32
|
-
mock_client = MagicMock()
|
|
33
|
-
mock_get_client.return_value = mock_client
|
|
34
|
-
storage.list_files_from_path("minio.default", "/path")
|
|
35
|
-
mock_client.list_files_from_path.assert_called_once_with("/path")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_upload_file(storage):
|
|
39
|
-
with patch.object(storage, '_get_client') as mock_get_client:
|
|
40
|
-
mock_client = MagicMock()
|
|
41
|
-
mock_get_client.return_value = mock_client
|
|
42
|
-
storage.upload_file("minio.default", "/local/path", "/remote/path")
|
|
43
|
-
mock_client.upload_file.assert_called_once_with("/local/path", "/remote/path")
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_download_file(storage):
|
|
47
|
-
with patch.object(storage, '_get_client') as mock_get_client:
|
|
48
|
-
mock_client = MagicMock()
|
|
49
|
-
mock_get_client.return_value = mock_client
|
|
50
|
-
storage.download_file("minio.default", "/local/path", "/remote/path")
|
|
51
|
-
mock_client.download_file.assert_called_once_with("/local/path", "/remote/path")
|
tests/test_utils.py
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
from unittest.mock import patch, MagicMock, mock_open
|
|
3
|
-
|
|
4
|
-
import oscar_python._utils as utils
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_get_headers_with_basicauth():
|
|
8
|
-
class MockClient:
|
|
9
|
-
_AUTH_TYPE = "basicauth"
|
|
10
|
-
user = "test_user"
|
|
11
|
-
password = "test_password"
|
|
12
|
-
|
|
13
|
-
c = MockClient()
|
|
14
|
-
headers = utils.get_headers(c)
|
|
15
|
-
assert headers["Authorization"].startswith("Basic ")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_get_headers_with_oidc_agent():
|
|
19
|
-
class MockClient:
|
|
20
|
-
_AUTH_TYPE = "oidc-agent"
|
|
21
|
-
shortname = "test_shortname"
|
|
22
|
-
|
|
23
|
-
with patch("liboidcagent.get_access_token", return_value="test_token"):
|
|
24
|
-
c = MockClient()
|
|
25
|
-
headers = utils.get_headers(c)
|
|
26
|
-
assert headers["Authorization"] == "Bearer test_token"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def test_get_headers_with_oidc():
|
|
30
|
-
class MockClient:
|
|
31
|
-
_AUTH_TYPE = "oidc"
|
|
32
|
-
|
|
33
|
-
def get_access_token(self):
|
|
34
|
-
return "test_oidc_token"
|
|
35
|
-
|
|
36
|
-
c = MockClient()
|
|
37
|
-
headers = utils.get_headers(c)
|
|
38
|
-
assert headers["Authorization"] == "Bearer test_oidc_token"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def test_encode_input_with_string():
|
|
42
|
-
test_data = "test_data"
|
|
43
|
-
encoded = utils.encode_input(test_data)
|
|
44
|
-
assert base64.b64encode(test_data.encode()) == encoded
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def test_decode_output_with_base64():
|
|
48
|
-
test_data = base64.b64encode(b"test_data").decode("utf-8")
|
|
49
|
-
with patch("builtins.open", mock_open()) as mock_file:
|
|
50
|
-
utils.decode_output(test_data, "test_output.txt")
|
|
51
|
-
mock_file.assert_called_once_with("test_output.txt", "w")
|
|
52
|
-
mock_file().write.assert_called_once_with("test_data")
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_decode_output_with_string():
|
|
56
|
-
test_data = base64.b64encode(b"test_data")
|
|
57
|
-
with patch("builtins.open", mock_open()) as mock_file:
|
|
58
|
-
utils.decode_output(test_data, "test_output.txt")
|
|
59
|
-
mock_file.assert_called_once_with("test_output.txt", "w")
|
|
60
|
-
mock_file().write.assert_called_once_with("test_data")
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def test_make_request_post():
|
|
64
|
-
class MockClient:
|
|
65
|
-
endpoint = "http://test.com"
|
|
66
|
-
ssl = True
|
|
67
|
-
_AUTH_TYPE = "basicauth"
|
|
68
|
-
user = "test_user"
|
|
69
|
-
password = "test_password"
|
|
70
|
-
|
|
71
|
-
c = MockClient()
|
|
72
|
-
with patch("requests.request") as mock_request:
|
|
73
|
-
mock_request.return_value.status_code = 200
|
|
74
|
-
mock_request.return_value.raise_for_status = MagicMock()
|
|
75
|
-
response = utils.make_request(c, "/test", "post", data="test_data", token="test_token")
|
|
76
|
-
assert response.status_code == 200
|
|
77
|
-
mock_request.assert_called_once_with("post", "http://test.com/test", headers={"Authorization": "Bearer test_token"}, verify=True, data="test_data", timeout=60)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def test_make_request_get():
|
|
81
|
-
class MockClient:
|
|
82
|
-
endpoint = "http://test.com"
|
|
83
|
-
ssl = True
|
|
84
|
-
_AUTH_TYPE = "basicauth"
|
|
85
|
-
user = "test_user"
|
|
86
|
-
password = "test_password"
|
|
87
|
-
|
|
88
|
-
c = MockClient()
|
|
89
|
-
with patch("requests.request") as mock_request:
|
|
90
|
-
mock_request.return_value.status_code = 200
|
|
91
|
-
mock_request.return_value.raise_for_status = MagicMock()
|
|
92
|
-
response = utils.make_request(c, "/test", "get")
|
|
93
|
-
assert response.status_code == 200
|
|
94
|
-
mock_request.assert_called_once_with("get", "http://test.com/test",
|
|
95
|
-
headers={'Authorization': 'Basic dGVzdF91c2VyOnRlc3RfcGFzc3dvcmQ='},
|
|
96
|
-
verify=True, timeout=60)
|
tests/test_webdav.py
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from oscar_python._providers._webdav import WebDav
|
|
3
|
-
from unittest.mock import patch, mock_open, MagicMock
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@pytest.fixture
|
|
7
|
-
def webdav():
|
|
8
|
-
credentials = {
|
|
9
|
-
"hostname": "hostname",
|
|
10
|
-
"login": "login",
|
|
11
|
-
"password": "password"
|
|
12
|
-
}
|
|
13
|
-
return WebDav(credentials)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def test_webdav_initialization(webdav):
|
|
17
|
-
assert isinstance(webdav, WebDav)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_webdav_upload_file(webdav):
|
|
21
|
-
webdav.client = MagicMock(["check", "mkdir", "upload_sync"])
|
|
22
|
-
webdav.client.check.return_value = False
|
|
23
|
-
|
|
24
|
-
with patch("builtins.open", mock_open()):
|
|
25
|
-
response = webdav.upload_file('local_path/file.txt', 'remote_path')
|
|
26
|
-
assert response is None
|
|
27
|
-
|
|
28
|
-
webdav.client.check.assert_called_with('remote_path')
|
|
29
|
-
webdav.client.mkdir.assert_called_with('remote_path')
|
|
30
|
-
webdav.client.upload_sync.assert_called_with('remote_path/file.txt', 'local_path/file.txt')
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_webdav_download_file(webdav):
|
|
34
|
-
webdav.client = MagicMock(["download_sync"])
|
|
35
|
-
webdav.client.download_sync.return_value = None
|
|
36
|
-
|
|
37
|
-
with patch("builtins.open", mock_open()) as mock_file:
|
|
38
|
-
webdav.download_file('local_path', 'remote_path/file.txt')
|
|
39
|
-
|
|
40
|
-
webdav.client.download_sync.assert_called_with('remote_path/file.txt', 'local_path/file.txt')
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_webdav_list_files_from_path(webdav):
|
|
44
|
-
webdav.client = MagicMock(["list"])
|
|
45
|
-
webdav.client.list.return_value = ['file1.txt', 'file2.txt']
|
|
46
|
-
|
|
47
|
-
response = webdav.list_files_from_path('path')
|
|
48
|
-
assert response == ['file1.txt', 'file2.txt']
|
|
49
|
-
|
|
50
|
-
webdav.client.list.assert_called_with('path')
|
|
File without changes
|
|
File without changes
|