oscar-python 1.2.1__tar.gz → 1.3.1__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.
- {oscar_python-1.2.1 → oscar_python-1.3.1}/PKG-INFO +44 -3
- {oscar_python-1.2.1 → oscar_python-1.3.1}/README.md +40 -2
- oscar_python-1.3.1/oscar_python/_oidc.py +96 -0
- {oscar_python-1.2.1 → oscar_python-1.3.1}/oscar_python/_providers/_minio.py +4 -3
- {oscar_python-1.2.1 → oscar_python-1.3.1}/oscar_python/_providers/_onedata.py +16 -13
- {oscar_python-1.2.1 → oscar_python-1.3.1}/oscar_python/_providers/_providers_base.py +4 -3
- {oscar_python-1.2.1 → oscar_python-1.3.1}/oscar_python/_providers/_s3.py +15 -13
- {oscar_python-1.2.1 → oscar_python-1.3.1}/oscar_python/_providers/_webdav.py +7 -7
- {oscar_python-1.2.1 → oscar_python-1.3.1}/oscar_python/_utils.py +27 -21
- {oscar_python-1.2.1 → oscar_python-1.3.1}/oscar_python/client.py +55 -34
- {oscar_python-1.2.1 → oscar_python-1.3.1}/oscar_python/client_anon.py +3 -2
- oscar_python-1.3.1/oscar_python/default_client.py +39 -0
- {oscar_python-1.2.1 → oscar_python-1.3.1}/oscar_python/local_test.py +1 -1
- {oscar_python-1.2.1 → oscar_python-1.3.1}/oscar_python/storage.py +34 -18
- oscar_python-1.3.1/oscar_python/test_v2.py +206 -0
- {oscar_python-1.2.1 → oscar_python-1.3.1}/oscar_python.egg-info/PKG-INFO +44 -3
- {oscar_python-1.2.1 → oscar_python-1.3.1}/oscar_python.egg-info/SOURCES.txt +11 -1
- {oscar_python-1.2.1 → oscar_python-1.3.1}/oscar_python.egg-info/requires.txt +5 -4
- {oscar_python-1.2.1 → oscar_python-1.3.1}/oscar_python.egg-info/top_level.txt +3 -0
- {oscar_python-1.2.1 → oscar_python-1.3.1}/setup.py +3 -2
- oscar_python-1.3.1/tests/test_client.py +138 -0
- oscar_python-1.3.1/tests/test_default_client.py +61 -0
- oscar_python-1.3.1/tests/test_oidc.py +38 -0
- oscar_python-1.3.1/tests/test_onedata.py +66 -0
- oscar_python-1.3.1/tests/test_s3.py +48 -0
- oscar_python-1.3.1/tests/test_storage.py +51 -0
- oscar_python-1.3.1/tests/test_utils.py +96 -0
- oscar_python-1.3.1/tests/test_webdav.py +50 -0
- oscar_python-1.2.1/oscar_python/default_client.py +0 -35
- {oscar_python-1.2.1 → oscar_python-1.3.1}/LICENSE +0 -0
- {oscar_python-1.2.1 → oscar_python-1.3.1}/oscar_python/__init__.py +0 -0
- {oscar_python-1.2.1 → oscar_python-1.3.1}/oscar_python.egg-info/dependency_links.txt +0 -0
- {oscar_python-1.2.1 → oscar_python-1.3.1}/oscar_python.egg-info/not-zip-safe +0 -0
- {oscar_python-1.2.1 → oscar_python-1.3.1}/setup.cfg +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: oscar_python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.1
|
|
4
4
|
Summary: OSCAR API for python
|
|
5
5
|
Home-page: https://github.com/grycap/oscar_python
|
|
6
6
|
Author: GRyCAP - Universitat Politecnica de Valencia
|
|
7
7
|
Author-email: calarcon@i3m.upv.es
|
|
8
8
|
License: Apache 2.0
|
|
9
|
+
Platform: UNKNOWN
|
|
9
10
|
Classifier: Programming Language :: Python :: 3
|
|
10
11
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
12
|
Description-Content-Type: text/markdown
|
|
@@ -13,7 +14,7 @@ License-File: LICENSE
|
|
|
13
14
|
|
|
14
15
|
## Python OSCAR client
|
|
15
16
|
|
|
16
|
-
[](https://github.com/grycap/oscar_python/actions/workflows/main.yaml)
|
|
17
18
|

|
|
18
19
|
|
|
19
20
|
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/).
|
|
@@ -68,6 +69,33 @@ If you already have a valid token, you can use the parameter `oidc_token` instea
|
|
|
68
69
|
```
|
|
69
70
|
An example of using a generated token is if you want to use EGI Notebooks. Since you can't use oidc-agent on the Notebook, you can make use of the generated token that EGI provides on path `/var/run/secrets/egi.eu/access_token`.
|
|
70
71
|
|
|
72
|
+
If you have a valid refresh token (long live token), you can use the parameter `refresh_token` instead.
|
|
73
|
+
|
|
74
|
+
``` python
|
|
75
|
+
options_oidc_auth = {'cluster_id':'cluster-id',
|
|
76
|
+
'endpoint':'https://cluster-endpoint',
|
|
77
|
+
'refresh_token':'token',
|
|
78
|
+
'ssl':'True'}
|
|
79
|
+
|
|
80
|
+
client = Client(options = options_oidc_auth)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
You can get a refresh token from EGI Check-In using the [Token Portal](https://aai.egi.eu/token).
|
|
84
|
+
|
|
85
|
+
In case of using other OIDC provider you must provide two additional parameters `token_endpoint`
|
|
86
|
+
and `scopes`:
|
|
87
|
+
|
|
88
|
+
``` python
|
|
89
|
+
options_oidc_auth = {'cluster_id':'cluster-id',
|
|
90
|
+
'endpoint':'https://cluster-endpoint',
|
|
91
|
+
'refresh_token':'token',
|
|
92
|
+
'scopes': ["openid", "profile", "email"],
|
|
93
|
+
'token_endpoint': "http://issuer.com/token",
|
|
94
|
+
'ssl':'True'}
|
|
95
|
+
|
|
96
|
+
client = Client(options = options_oidc_auth)
|
|
97
|
+
```
|
|
98
|
+
|
|
71
99
|
### Sample usage
|
|
72
100
|
|
|
73
101
|
- Sample code that creates a client and gets information about the cluster
|
|
@@ -170,6 +198,9 @@ response = client.remove_service("service_name") # returns an http response
|
|
|
170
198
|
``` python
|
|
171
199
|
# make a synchronous execution
|
|
172
200
|
response = client.run_service("service_name", input="input", output="out.png", timeout=100) # returns an http response
|
|
201
|
+
|
|
202
|
+
# make an asynchronous execution
|
|
203
|
+
response = client.run_service("service_name", input="input", async_call=True) # returns an http response
|
|
173
204
|
```
|
|
174
205
|
|
|
175
206
|
#### Logs methods
|
|
@@ -200,11 +231,19 @@ response = client.remove_all_jobs("service_name") # returns an http response
|
|
|
200
231
|
|
|
201
232
|
#### Storage usage
|
|
202
233
|
|
|
203
|
-
You can create a storage object to operate over the different storage providers defined on a service with the method `create_storage_client
|
|
234
|
+
You can create a storage object to operate over the different storage providers defined on a service with the method `create_storage_client`. This constructor returns a storage object with methos to interact with the storage providers.
|
|
235
|
+
|
|
236
|
+
The default constructor, seen as follows, will create a provider to interact with the default MinIO instance through the user's credentials.
|
|
237
|
+
|
|
238
|
+
``` python
|
|
239
|
+
storage_service = client.create_storage_client() # returns a storage object
|
|
240
|
+
```
|
|
241
|
+
Additionally, if you need to interact with specific storage providers defined on a service, the constructor accepts a `svc` parameter where you can state the service name from which to search for additional credentials.
|
|
204
242
|
|
|
205
243
|
``` python
|
|
206
244
|
storage_service = client.create_storage_client("service_name") # returns a storage object
|
|
207
245
|
```
|
|
246
|
+
|
|
208
247
|
> _Note_ : The `storage_provider` parameter on the storage methods follows the format: `["storage_provider_type"].["storage_provider_name"]` where `storage_provider_type` is one of the suported storage providers (minIO, S3, Onedata or webdav) and `storage_provider_name` is the identifier _(ex: minio.default)_
|
|
209
248
|
|
|
210
249
|
**list_files_from_path**
|
|
@@ -227,3 +266,5 @@ response = storage_service.upload_file("storage_provider", "local_path", "remote
|
|
|
227
266
|
# download a file from a remote path to a local path
|
|
228
267
|
response = storage_service.download_file("storage_provider", "local_path", "remote_path")
|
|
229
268
|
```
|
|
269
|
+
|
|
270
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Python OSCAR client
|
|
2
2
|
|
|
3
|
-
[](https://github.com/grycap/oscar_python/actions/workflows/main.yaml)
|
|
4
4
|

|
|
5
5
|
|
|
6
6
|
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/).
|
|
@@ -55,6 +55,33 @@ If you already have a valid token, you can use the parameter `oidc_token` instea
|
|
|
55
55
|
```
|
|
56
56
|
An example of using a generated token is if you want to use EGI Notebooks. Since you can't use oidc-agent on the Notebook, you can make use of the generated token that EGI provides on path `/var/run/secrets/egi.eu/access_token`.
|
|
57
57
|
|
|
58
|
+
If you have a valid refresh token (long live token), you can use the parameter `refresh_token` instead.
|
|
59
|
+
|
|
60
|
+
``` python
|
|
61
|
+
options_oidc_auth = {'cluster_id':'cluster-id',
|
|
62
|
+
'endpoint':'https://cluster-endpoint',
|
|
63
|
+
'refresh_token':'token',
|
|
64
|
+
'ssl':'True'}
|
|
65
|
+
|
|
66
|
+
client = Client(options = options_oidc_auth)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
You can get a refresh token from EGI Check-In using the [Token Portal](https://aai.egi.eu/token).
|
|
70
|
+
|
|
71
|
+
In case of using other OIDC provider you must provide two additional parameters `token_endpoint`
|
|
72
|
+
and `scopes`:
|
|
73
|
+
|
|
74
|
+
``` python
|
|
75
|
+
options_oidc_auth = {'cluster_id':'cluster-id',
|
|
76
|
+
'endpoint':'https://cluster-endpoint',
|
|
77
|
+
'refresh_token':'token',
|
|
78
|
+
'scopes': ["openid", "profile", "email"],
|
|
79
|
+
'token_endpoint': "http://issuer.com/token",
|
|
80
|
+
'ssl':'True'}
|
|
81
|
+
|
|
82
|
+
client = Client(options = options_oidc_auth)
|
|
83
|
+
```
|
|
84
|
+
|
|
58
85
|
### Sample usage
|
|
59
86
|
|
|
60
87
|
- Sample code that creates a client and gets information about the cluster
|
|
@@ -157,6 +184,9 @@ response = client.remove_service("service_name") # returns an http response
|
|
|
157
184
|
``` python
|
|
158
185
|
# make a synchronous execution
|
|
159
186
|
response = client.run_service("service_name", input="input", output="out.png", timeout=100) # returns an http response
|
|
187
|
+
|
|
188
|
+
# make an asynchronous execution
|
|
189
|
+
response = client.run_service("service_name", input="input", async_call=True) # returns an http response
|
|
160
190
|
```
|
|
161
191
|
|
|
162
192
|
#### Logs methods
|
|
@@ -187,11 +217,19 @@ response = client.remove_all_jobs("service_name") # returns an http response
|
|
|
187
217
|
|
|
188
218
|
#### Storage usage
|
|
189
219
|
|
|
190
|
-
You can create a storage object to operate over the different storage providers defined on a service with the method `create_storage_client
|
|
220
|
+
You can create a storage object to operate over the different storage providers defined on a service with the method `create_storage_client`. This constructor returns a storage object with methos to interact with the storage providers.
|
|
221
|
+
|
|
222
|
+
The default constructor, seen as follows, will create a provider to interact with the default MinIO instance through the user's credentials.
|
|
223
|
+
|
|
224
|
+
``` python
|
|
225
|
+
storage_service = client.create_storage_client() # returns a storage object
|
|
226
|
+
```
|
|
227
|
+
Additionally, if you need to interact with specific storage providers defined on a service, the constructor accepts a `svc` parameter where you can state the service name from which to search for additional credentials.
|
|
191
228
|
|
|
192
229
|
``` python
|
|
193
230
|
storage_service = client.create_storage_client("service_name") # returns a storage object
|
|
194
231
|
```
|
|
232
|
+
|
|
195
233
|
> _Note_ : The `storage_provider` parameter on the storage methods follows the format: `["storage_provider_type"].["storage_provider_name"]` where `storage_provider_type` is one of the suported storage providers (minIO, S3, Onedata or webdav) and `storage_provider_name` is the identifier _(ex: minio.default)_
|
|
196
234
|
|
|
197
235
|
**list_files_from_path**
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Class to manage OIDC JWT tokens
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
import base64
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OIDC(object):
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def _b64d(b):
|
|
15
|
+
"""Decode some base64-encoded bytes.
|
|
16
|
+
|
|
17
|
+
Raises Exception if the string contains invalid characters or padding.
|
|
18
|
+
|
|
19
|
+
:param b: bytes
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
cb = b.rstrip(b"=") # shouldn't but there you are
|
|
23
|
+
|
|
24
|
+
# Python's base64 functions ignore invalid characters, so we need to
|
|
25
|
+
# check for them explicitly.
|
|
26
|
+
b64_re = re.compile(b"^[A-Za-z0-9_-]*$")
|
|
27
|
+
if not b64_re.match(cb):
|
|
28
|
+
raise Exception(cb, "base64-encoded data contains illegal characters")
|
|
29
|
+
|
|
30
|
+
if cb == b:
|
|
31
|
+
b = OIDC._add_padding(b)
|
|
32
|
+
|
|
33
|
+
return base64.urlsafe_b64decode(b)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def _add_padding(b):
|
|
37
|
+
# add padding chars
|
|
38
|
+
m = len(b) % 4
|
|
39
|
+
if m == 1:
|
|
40
|
+
# NOTE: for some reason b64decode raises *TypeError* if the
|
|
41
|
+
# padding is incorrect.
|
|
42
|
+
raise Exception(b, "incorrect padding")
|
|
43
|
+
elif m == 2:
|
|
44
|
+
b += b"=="
|
|
45
|
+
elif m == 3:
|
|
46
|
+
b += b"="
|
|
47
|
+
return b
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def get_info(token):
|
|
51
|
+
"""
|
|
52
|
+
Unpacks a JWT into its parts and base64 decodes the parts
|
|
53
|
+
individually, returning the part 1 json decoded, where the
|
|
54
|
+
token info is stored.
|
|
55
|
+
|
|
56
|
+
:param token: The JWT token
|
|
57
|
+
"""
|
|
58
|
+
part = tuple(token.encode("utf-8").split(b"."))
|
|
59
|
+
part = [OIDC._b64d(p) for p in part]
|
|
60
|
+
return json.loads(part[1].decode("utf-8"))
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def is_access_token_expired(token):
|
|
64
|
+
"""
|
|
65
|
+
Check if the current access token is expired
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
decoded_token = OIDC.get_info(token)
|
|
69
|
+
now = int(time.time())
|
|
70
|
+
expires = int(decoded_token['exp'])
|
|
71
|
+
validity = expires - now
|
|
72
|
+
if validity < 0:
|
|
73
|
+
return True
|
|
74
|
+
else:
|
|
75
|
+
return False
|
|
76
|
+
except Exception:
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def refresh_access_token(refresh_token, scopes, token_endpoint):
|
|
81
|
+
"""
|
|
82
|
+
Refresh the access token using the refresh token
|
|
83
|
+
"""
|
|
84
|
+
data = {
|
|
85
|
+
'grant_type': 'refresh_token',
|
|
86
|
+
'refresh_token': refresh_token,
|
|
87
|
+
'client_id': 'token-portal',
|
|
88
|
+
'scope': ' '.join(scopes)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
response = requests.post(token_endpoint, data=data)
|
|
92
|
+
|
|
93
|
+
if response.status_code == 200:
|
|
94
|
+
return response.json()['access_token']
|
|
95
|
+
else:
|
|
96
|
+
return None
|
|
@@ -10,15 +10,16 @@
|
|
|
10
10
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
|
-
# limitations under the License.
|
|
13
|
+
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
import boto3
|
|
16
16
|
from oscar_python._providers._s3 import S3
|
|
17
17
|
|
|
18
18
|
_DEFAULT_MINIO_ENDPOINT = 'http://minio-service.minio:9000'
|
|
19
19
|
|
|
20
|
+
|
|
20
21
|
class Minio(S3):
|
|
21
|
-
|
|
22
|
+
|
|
22
23
|
def _get_client(self, c):
|
|
23
24
|
"""Return Minio client with user configuration."""
|
|
24
25
|
if c["endpoint"] == '':
|
|
@@ -30,4 +31,4 @@ class Minio(S3):
|
|
|
30
31
|
region_name=c["region"],
|
|
31
32
|
verify=c["verify"],
|
|
32
33
|
aws_access_key_id=c["access_key"],
|
|
33
|
-
aws_secret_access_key=c["secret_key"])
|
|
34
|
+
aws_secret_access_key=c["secret_key"])
|
|
@@ -10,29 +10,30 @@
|
|
|
10
10
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
|
-
# limitations under the License.
|
|
13
|
+
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
from oscar_python._providers._providers_base import StorageProvider
|
|
16
16
|
import requests
|
|
17
17
|
import json
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
# TODO fix error returns
|
|
20
21
|
class Onedata(StorageProvider):
|
|
21
|
-
|
|
22
|
+
|
|
22
23
|
_CDMI_PATH = '/cdmi'
|
|
23
24
|
_CDMI_VERSION_HEADER = {'X-CDMI-Specification-Version': '1.1.1'}
|
|
24
25
|
|
|
25
26
|
def __init__(self, credentials) -> None:
|
|
26
27
|
super().__init__()
|
|
27
28
|
self._get_client(credentials)
|
|
28
|
-
|
|
29
|
+
|
|
29
30
|
def _get_client(self, c):
|
|
30
31
|
self.oneprovider_space = c["space"]
|
|
31
32
|
self.oneprovider_host = c["oneprovider_host"]
|
|
32
33
|
self.headers = {'X-Auth-Token': c["token"]}
|
|
33
34
|
self.url = (f'https://{self.oneprovider_host}{self._CDMI_PATH}/'
|
|
34
35
|
f'{self.oneprovider_space}/')
|
|
35
|
-
|
|
36
|
+
|
|
36
37
|
def upload_file(self, local_path, remote_path):
|
|
37
38
|
file_name = local_path.split('/')[-1]
|
|
38
39
|
if not self._folder_exists(remote_path):
|
|
@@ -57,12 +58,13 @@ class Onedata(StorageProvider):
|
|
|
57
58
|
file_name = remote_path.split('/')[-1]
|
|
58
59
|
url = self.url+remote_path
|
|
59
60
|
res = requests.get(url=url, headers=self.headers)
|
|
60
|
-
if res.status_code == 200:
|
|
61
|
+
if res.status_code == 200:
|
|
62
|
+
print("Saving file to {0}/{1}".format(local_path, file_name))
|
|
61
63
|
self._save_file(local_path+"/"+file_name, res.content, mode='wb')
|
|
62
|
-
|
|
64
|
+
|
|
63
65
|
def list_files_from_path(self, path):
|
|
64
66
|
headers = {**self._CDMI_VERSION_HEADER, **self.headers}
|
|
65
|
-
return requests.get(url
|
|
67
|
+
return requests.get(url=self.url+path+"/", headers=headers)
|
|
66
68
|
|
|
67
69
|
def _save_file(self, path, content, mode='w'):
|
|
68
70
|
with open(path, mode) as fwc:
|
|
@@ -72,12 +74,13 @@ class Onedata(StorageProvider):
|
|
|
72
74
|
|
|
73
75
|
def _folder_exists(self, folder_name):
|
|
74
76
|
headers = {**self._CDMI_VERSION_HEADER, **self.headers}
|
|
75
|
-
response = requests.get(url
|
|
77
|
+
response = requests.get(url=self.url+folder_name+"/", headers=headers)
|
|
76
78
|
if response.status_code == 200:
|
|
77
79
|
return True
|
|
78
80
|
return False
|
|
79
|
-
|
|
81
|
+
|
|
80
82
|
def _create_folder(self, folder_name):
|
|
81
|
-
response = requests.put(url
|
|
82
|
-
if response.status_code
|
|
83
|
-
return
|
|
83
|
+
response = requests.put(url=self.url+folder_name+"/", headers=self.headers)
|
|
84
|
+
if response.status_code == 201:
|
|
85
|
+
return True
|
|
86
|
+
return False
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
|
-
# limitations under the License.
|
|
13
|
+
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
import abc
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
class StorageProvider(abc.ABC):
|
|
18
19
|
@abc.abstractmethod
|
|
19
20
|
def download_file(self, local_path, remote_path):
|
|
@@ -22,11 +23,11 @@ class StorageProvider(abc.ABC):
|
|
|
22
23
|
@abc.abstractmethod
|
|
23
24
|
def upload_file(self, local_path, remote_path):
|
|
24
25
|
"""Generic method to be implemented by all the storage providers."""
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
@abc.abstractmethod
|
|
27
28
|
def list_files_from_path(self, path):
|
|
28
29
|
"""Generic method to be implemented by all the storage providers."""
|
|
29
30
|
|
|
30
31
|
@abc.abstractmethod
|
|
31
32
|
def _get_client(self):
|
|
32
|
-
"""Generic method to be implemented by all the storage providers."""
|
|
33
|
+
"""Generic method to be implemented by all the storage providers."""
|
|
@@ -10,19 +10,19 @@
|
|
|
10
10
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
|
-
# limitations under the License.
|
|
13
|
+
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
from aiohttp import ClientError
|
|
16
|
-
import logging
|
|
17
16
|
import boto3
|
|
18
17
|
from oscar_python._providers._providers_base import StorageProvider
|
|
19
18
|
|
|
19
|
+
|
|
20
20
|
class S3(StorageProvider):
|
|
21
21
|
def __init__(self, credentials) -> None:
|
|
22
22
|
super().__init__()
|
|
23
23
|
self.client = self._get_client(credentials)
|
|
24
24
|
self.resource = self._get_resource(credentials)
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
def _get_client(self, c):
|
|
27
27
|
"""Returns S3 client with default configuration."""
|
|
28
28
|
if c is None:
|
|
@@ -42,39 +42,41 @@ class S3(StorageProvider):
|
|
|
42
42
|
if region == '':
|
|
43
43
|
region = None
|
|
44
44
|
return boto3.resource('s3',
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
region_name=region,
|
|
46
|
+
aws_access_key_id=c["access_key"],
|
|
47
|
+
aws_secret_access_key=c["secret_key"])
|
|
48
48
|
|
|
49
49
|
def list_files_from_path(self, path):
|
|
50
50
|
prefix = ""
|
|
51
51
|
path_split = path.split('/')
|
|
52
52
|
bucket = path_split[0]
|
|
53
53
|
if len(path_split) > 1:
|
|
54
|
-
prefix = path.split('/',2)[1]
|
|
55
|
-
print("Reading content from path:",path)
|
|
54
|
+
prefix = path.split('/', 2)[1]
|
|
55
|
+
print("Reading content from path:", path)
|
|
56
56
|
return self.client.list_objects(Bucket=bucket, Prefix=prefix)
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
def upload_file(self, local_path, remote_path):
|
|
59
59
|
bucket_name = remote_path.split('/')[0]
|
|
60
|
-
file_key = remote_path.split('/',1)[1]
|
|
60
|
+
file_key = remote_path.split('/', 1)[1]
|
|
61
61
|
file_name = local_path.split('/')[-1]
|
|
62
62
|
print("Uploading to bucket '{0}' with key '{1}'".format(bucket_name,file_key))
|
|
63
63
|
with open(local_path, 'rb') as data:
|
|
64
64
|
try:
|
|
65
|
-
self.client.upload_fileobj(data, bucket_name, file_key+"/"+file_name)
|
|
65
|
+
self.client.upload_fileobj(data, bucket_name, file_key + "/" + file_name)
|
|
66
|
+
return True
|
|
66
67
|
except ClientError as err:
|
|
67
68
|
print("Error uploading file: ", err)
|
|
68
69
|
return False
|
|
69
70
|
|
|
70
71
|
def download_file(self, local_path, remote_path):
|
|
71
72
|
bucket_name = remote_path.split('/')[0]
|
|
72
|
-
file_key = remote_path.split('/',1)[1]
|
|
73
|
+
file_key = remote_path.split('/', 1)[1]
|
|
73
74
|
file_path = local_path+"/"+remote_path.split('/')[-1]
|
|
74
75
|
print("Downloading from bucket '{0}' to path '{1}' with key '{2}'".format(bucket_name, file_path, file_key))
|
|
75
76
|
with open(file_path, 'wb') as data:
|
|
76
77
|
try:
|
|
77
78
|
self.client.download_fileobj(bucket_name, file_key, data)
|
|
79
|
+
return True
|
|
78
80
|
except ClientError as err:
|
|
79
81
|
print("Error downloading file: ", err)
|
|
80
|
-
return False
|
|
82
|
+
return False
|
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
|
-
# limitations under the License.
|
|
13
|
+
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
from oscar_python._providers._providers_base import StorageProvider
|
|
16
16
|
from webdav3.client import Client
|
|
17
17
|
from webdav3.exceptions import WebDavException
|
|
18
18
|
|
|
19
|
+
|
|
19
20
|
class WebDav(StorageProvider):
|
|
20
21
|
def __init__(self, credentials) -> None:
|
|
21
22
|
self.client = self._get_client(credentials)
|
|
@@ -26,9 +27,9 @@ class WebDav(StorageProvider):
|
|
|
26
27
|
if 'https://' not in hostname:
|
|
27
28
|
hostname = 'https://'+hostname
|
|
28
29
|
options = {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
'webdav_hostname': hostname,
|
|
31
|
+
'webdav_login': c["login"],
|
|
32
|
+
'webdav_password': c["password"]
|
|
32
33
|
}
|
|
33
34
|
return Client(options=options)
|
|
34
35
|
|
|
@@ -45,13 +46,12 @@ class WebDav(StorageProvider):
|
|
|
45
46
|
except WebDavException as exception:
|
|
46
47
|
print("error uploading file to path '{0}': {1}".format(remote_path+"/"+file_name, exception))
|
|
47
48
|
|
|
48
|
-
|
|
49
49
|
def download_file(self, local_path, remote_path):
|
|
50
50
|
file_name = remote_path.split('/')[-1]
|
|
51
51
|
try:
|
|
52
52
|
self.client.download_sync(remote_path, local_path+"/"+file_name)
|
|
53
53
|
except WebDavException as exception:
|
|
54
54
|
print("error downloading file from path '{0}': {1}".format(remote_path, exception))
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
def list_files_from_path(self, path):
|
|
57
|
-
return self.client.list(path)
|
|
57
|
+
return self.client.list(path)
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
|
-
# limitations under the License.
|
|
13
|
+
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
import base64
|
|
16
16
|
import os
|
|
@@ -18,18 +18,19 @@ import requests
|
|
|
18
18
|
import liboidcagent as agent
|
|
19
19
|
_DEFAULT_TIMEOUT = 60
|
|
20
20
|
|
|
21
|
-
""" Generic http request """
|
|
22
|
-
def make_request(c , path, method, **kwargs):
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
def make_request(c, path, method, **kwargs):
|
|
23
|
+
""" Generic http request """
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
headers = get_headers(c)
|
|
26
|
+
|
|
27
|
+
if "timeout" in kwargs.keys() and kwargs["timeout"]:
|
|
27
28
|
timeout = kwargs["timeout"]
|
|
28
|
-
else:
|
|
29
|
+
else:
|
|
29
30
|
timeout = _DEFAULT_TIMEOUT
|
|
30
31
|
|
|
31
32
|
url = c.endpoint+path
|
|
32
|
-
|
|
33
|
+
|
|
33
34
|
if method in ["post", "put"]:
|
|
34
35
|
if "token" in kwargs.keys() and kwargs["token"]:
|
|
35
36
|
headers = get_headers_with_token(kwargs["token"])
|
|
@@ -40,37 +41,42 @@ def make_request(c , path, method, **kwargs):
|
|
|
40
41
|
|
|
41
42
|
if "handle" in kwargs.keys() and kwargs["handle"] == False:
|
|
42
43
|
return result
|
|
43
|
-
|
|
44
|
+
|
|
44
45
|
result.raise_for_status()
|
|
45
46
|
return result
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
|
|
48
49
|
def get_headers(c):
|
|
50
|
+
""" Function to generate headers with basic authentication or OIDC """
|
|
49
51
|
if c._AUTH_TYPE == "basicauth":
|
|
50
|
-
usr_pass_as_bytes = bytes(c.user+":"+c.password,"utf-8")
|
|
52
|
+
usr_pass_as_bytes = bytes(c.user + ":" + c.password, "utf-8")
|
|
51
53
|
usr_pass_base_64 = base64.b64encode(usr_pass_as_bytes).decode("utf-8")
|
|
52
|
-
return {"Authorization": "Basic "+ usr_pass_base_64}
|
|
54
|
+
return {"Authorization": "Basic " + usr_pass_base_64}
|
|
53
55
|
if c._AUTH_TYPE == "oidc-agent":
|
|
54
56
|
token = agent.get_access_token(c.shortname)
|
|
55
57
|
return get_headers_with_token(token)
|
|
56
58
|
if c._AUTH_TYPE == "oidc":
|
|
57
|
-
return get_headers_with_token(c.
|
|
59
|
+
return get_headers_with_token(c.get_access_token())
|
|
60
|
+
|
|
58
61
|
|
|
59
|
-
""" Function to generate headers with token auth """
|
|
60
62
|
def get_headers_with_token(token):
|
|
61
|
-
|
|
63
|
+
""" Function to generate headers with token auth """
|
|
64
|
+
return {"Authorization": "Bearer " + str(token)}
|
|
65
|
+
|
|
62
66
|
|
|
63
67
|
def write_text_file(content, file_path):
|
|
64
68
|
with open(file_path, 'w') as f:
|
|
65
69
|
f.write(content)
|
|
66
70
|
|
|
71
|
+
|
|
67
72
|
def isBase64(st):
|
|
68
73
|
try:
|
|
69
74
|
base64.b64decode(st)
|
|
70
75
|
return True
|
|
71
|
-
except:
|
|
76
|
+
except Exception:
|
|
72
77
|
return False
|
|
73
78
|
|
|
79
|
+
|
|
74
80
|
def decode_b64(b64_str, file_out):
|
|
75
81
|
file_extension = os.path.splitext(file_out)[1]
|
|
76
82
|
try:
|
|
@@ -80,7 +86,7 @@ def decode_b64(b64_str, file_out):
|
|
|
80
86
|
decode = 'w'
|
|
81
87
|
decoded_data = decoded_data.decode("utf-8")
|
|
82
88
|
else:
|
|
83
|
-
decode = 'wb'
|
|
89
|
+
decode = 'wb'
|
|
84
90
|
|
|
85
91
|
with open(file_out, decode) as f:
|
|
86
92
|
f.write(decoded_data)
|
|
@@ -90,6 +96,7 @@ def decode_b64(b64_str, file_out):
|
|
|
90
96
|
except OSError:
|
|
91
97
|
print('Error decoding output: Failed to write decoded data to file.')
|
|
92
98
|
|
|
99
|
+
|
|
93
100
|
def encode_input(data):
|
|
94
101
|
if os.path.isfile(data):
|
|
95
102
|
try:
|
|
@@ -103,12 +110,11 @@ def encode_input(data):
|
|
|
103
110
|
message_bytes = data.encode('ascii')
|
|
104
111
|
return base64.b64encode(message_bytes)
|
|
105
112
|
|
|
113
|
+
|
|
106
114
|
def decode_output(output, file_path):
|
|
107
|
-
if
|
|
115
|
+
if isBase64(output):
|
|
108
116
|
decode_b64(output, file_path)
|
|
109
117
|
return
|
|
110
|
-
if
|
|
111
|
-
write_text_file(output,file_path)
|
|
118
|
+
if isinstance(output, str):
|
|
119
|
+
write_text_file(output, file_path)
|
|
112
120
|
return
|
|
113
|
-
|
|
114
|
-
|