oscar-python 1.3.0__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.
Files changed (33) hide show
  1. {oscar_python-1.3.0 → oscar_python-1.3.1}/PKG-INFO +42 -20
  2. {oscar_python-1.3.0 → oscar_python-1.3.1}/README.md +37 -2
  3. oscar_python-1.3.1/oscar_python/_oidc.py +96 -0
  4. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python/_utils.py +1 -1
  5. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python/client.py +28 -13
  6. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python/storage.py +21 -5
  7. oscar_python-1.3.1/oscar_python/test_v2.py +206 -0
  8. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python.egg-info/PKG-INFO +43 -21
  9. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python.egg-info/SOURCES.txt +3 -0
  10. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python.egg-info/top_level.txt +2 -0
  11. {oscar_python-1.3.0 → oscar_python-1.3.1}/tests/test_client.py +9 -0
  12. oscar_python-1.3.1/tests/test_oidc.py +38 -0
  13. {oscar_python-1.3.0 → oscar_python-1.3.1}/tests/test_storage.py +2 -2
  14. {oscar_python-1.3.0 → oscar_python-1.3.1}/tests/test_utils.py +3 -1
  15. {oscar_python-1.3.0 → oscar_python-1.3.1}/LICENSE +0 -0
  16. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python/__init__.py +0 -0
  17. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python/_providers/_minio.py +0 -0
  18. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python/_providers/_onedata.py +0 -0
  19. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python/_providers/_providers_base.py +0 -0
  20. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python/_providers/_s3.py +0 -0
  21. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python/_providers/_webdav.py +0 -0
  22. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python/client_anon.py +0 -0
  23. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python/default_client.py +0 -0
  24. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python/local_test.py +0 -0
  25. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python.egg-info/dependency_links.txt +0 -0
  26. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python.egg-info/not-zip-safe +0 -0
  27. {oscar_python-1.3.0 → oscar_python-1.3.1}/oscar_python.egg-info/requires.txt +5 -5
  28. {oscar_python-1.3.0 → oscar_python-1.3.1}/setup.cfg +0 -0
  29. {oscar_python-1.3.0 → oscar_python-1.3.1}/setup.py +0 -0
  30. {oscar_python-1.3.0 → oscar_python-1.3.1}/tests/test_default_client.py +0 -0
  31. {oscar_python-1.3.0 → oscar_python-1.3.1}/tests/test_onedata.py +0 -0
  32. {oscar_python-1.3.0 → oscar_python-1.3.1}/tests/test_s3.py +0 -0
  33. {oscar_python-1.3.0 → oscar_python-1.3.1}/tests/test_webdav.py +0 -0
@@ -1,35 +1,20 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.1
2
2
  Name: oscar_python
3
- Version: 1.3.0
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
12
13
  License-File: LICENSE
13
- Requires-Dist: webdavclient3==3.14.6
14
- Requires-Dist: requests
15
- Requires-Dist: boto3
16
- Requires-Dist: setuptools>=40.8.0
17
- Requires-Dist: pyyaml
18
- Requires-Dist: aiohttp
19
- Requires-Dist: liboidcagent
20
- Dynamic: author
21
- Dynamic: author-email
22
- Dynamic: classifier
23
- Dynamic: description
24
- Dynamic: description-content-type
25
- Dynamic: home-page
26
- Dynamic: license
27
- Dynamic: requires-dist
28
- Dynamic: summary
29
14
 
30
15
  ## Python OSCAR client
31
16
 
32
- [![Build](https://github.com/grycap/oscar_python/actions/workflows/main.yaml/badge.svg)](https://github.com/grycap/oscar_python/actions/workflows/main.yaml)
17
+ [![Build](https://github.com/grycap/oscar_python/actions/workflows/release.yaml/badge.svg)](https://github.com/grycap/oscar_python/actions/workflows/main.yaml)
33
18
  ![PyPI](https://img.shields.io/pypi/v/oscar_python)
34
19
 
35
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/).
@@ -84,6 +69,33 @@ If you already have a valid token, you can use the parameter `oidc_token` instea
84
69
  ```
85
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`.
86
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
+
87
99
  ### Sample usage
88
100
 
89
101
  - Sample code that creates a client and gets information about the cluster
@@ -219,11 +231,19 @@ response = client.remove_all_jobs("service_name") # returns an http response
219
231
 
220
232
  #### Storage usage
221
233
 
222
- You can create a storage object to operate over the different storage providers defined on a service with the method `create_storage_client` as follows:
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.
223
242
 
224
243
  ``` python
225
244
  storage_service = client.create_storage_client("service_name") # returns a storage object
226
245
  ```
246
+
227
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)_
228
248
 
229
249
  **list_files_from_path**
@@ -246,3 +266,5 @@ response = storage_service.upload_file("storage_provider", "local_path", "remote
246
266
  # download a file from a remote path to a local path
247
267
  response = storage_service.download_file("storage_provider", "local_path", "remote_path")
248
268
  ```
269
+
270
+
@@ -1,6 +1,6 @@
1
1
  ## Python OSCAR client
2
2
 
3
- [![Build](https://github.com/grycap/oscar_python/actions/workflows/main.yaml/badge.svg)](https://github.com/grycap/oscar_python/actions/workflows/main.yaml)
3
+ [![Build](https://github.com/grycap/oscar_python/actions/workflows/release.yaml/badge.svg)](https://github.com/grycap/oscar_python/actions/workflows/main.yaml)
4
4
  ![PyPI](https://img.shields.io/pypi/v/oscar_python)
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
@@ -190,11 +217,19 @@ response = client.remove_all_jobs("service_name") # returns an http response
190
217
 
191
218
  #### Storage usage
192
219
 
193
- You can create a storage object to operate over the different storage providers defined on a service with the method `create_storage_client` as follows:
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.
194
228
 
195
229
  ``` python
196
230
  storage_service = client.create_storage_client("service_name") # returns a storage object
197
231
  ```
232
+
198
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)_
199
234
 
200
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
@@ -56,7 +56,7 @@ def get_headers(c):
56
56
  token = agent.get_access_token(c.shortname)
57
57
  return get_headers_with_token(token)
58
58
  if c._AUTH_TYPE == "oidc":
59
- return get_headers_with_token(c.oidc_token)
59
+ return get_headers_with_token(c.get_access_token())
60
60
 
61
61
 
62
62
  def get_headers_with_token(token):
@@ -18,6 +18,7 @@ import os
18
18
  import yaml
19
19
  import liboidcagent as agent
20
20
  import oscar_python._utils as utils
21
+ from oscar_python._oidc import OIDC
21
22
  from oscar_python.default_client import DefaultClient
22
23
  from oscar_python.storage import Storage
23
24
 
@@ -37,10 +38,16 @@ _POST = "post"
37
38
  _PUT = "put"
38
39
  _DELETE = "delete"
39
40
 
41
+ # Default values for OIDC refresh token using EGI CheckIn
42
+ _DEFAULT_SCOPES = ['openid', 'email', 'profile', 'voperson_id', 'eduperson_entitlement']
43
+ _DEFAULT_TOKEN_ENDPOINT = 'https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/token'
44
+
40
45
 
41
46
  class Client(DefaultClient):
42
47
  # Cluster info
43
48
  def __init__(self, options) -> None:
49
+ self.id = options['cluster_id']
50
+ self.endpoint = options['endpoint']
44
51
  self.set_auth_type(options)
45
52
  if self._AUTH_TYPE == 'basicauth':
46
53
  self.basic_auth_client(options)
@@ -50,22 +57,20 @@ class Client(DefaultClient):
50
57
  self.oidc_client(options)
51
58
 
52
59
  def basic_auth_client(self, options):
53
- self.id = options['cluster_id']
54
- self.endpoint = options['endpoint']
55
60
  self.user = options['user']
56
61
  self.password = options['password']
57
62
  self.ssl = bool(options['ssl'])
58
63
 
59
64
  def oidc_agent_client(self, options):
60
- self.id = options['cluster_id']
61
- self.endpoint = options['endpoint']
62
65
  self.shortname = options['shortname']
63
66
  self.ssl = bool(options['ssl'])
64
67
 
65
68
  def oidc_client(self, options):
66
- self.id = options['cluster_id']
67
- self.endpoint = options['endpoint']
68
- self.oidc_token = options['oidc_token']
69
+ self.oidc_token = options.get('oidc_token')
70
+ self.refresh_token = options.get('refresh_token')
71
+ self.scopes = options.get('scopes', _DEFAULT_SCOPES)
72
+ self.token_endpoint = options.get('token_endpoint',
73
+ _DEFAULT_TOKEN_ENDPOINT)
69
74
  self.ssl = bool(options['ssl'])
70
75
 
71
76
  def set_auth_type(self, options):
@@ -77,18 +82,28 @@ class Client(DefaultClient):
77
82
  agent.get_access_token(options['shortname'])
78
83
  except agent.OidcAgentError as e:
79
84
  print("ERROR oidc-agent: {}".format(e))
80
- elif 'oidc_token' in options:
85
+ elif 'oidc_token' in options or 'refresh_token' in options:
81
86
  self._AUTH_TYPE = "oidc"
82
87
  else:
83
88
  raise ValueError("Unrecognized authentication credentials in options")
84
89
 
90
+ def get_access_token(self):
91
+ if self.refresh_token and OIDC.is_access_token_expired(self.oidc_token):
92
+ self.oidc_token = OIDC.refresh_access_token(self.refresh_token,
93
+ self.scopes,
94
+ self.token_endpoint)
95
+ return self.oidc_token
96
+
85
97
  """ Creates a generic storage client to interact with the storage providers
86
98
  defined on a specific service of the refered OSCAR cluster """
87
- def create_storage_client(self, svc):
88
- return Storage(
89
- client_obj=self,
90
- svc_name=svc)
91
-
99
+ def create_storage_client(self, svc=None):
100
+ if svc != None:
101
+ return Storage(
102
+ client_obj=self, svc_name=svc)
103
+ else:
104
+ return Storage(
105
+ client_obj=self)
106
+
92
107
  """ Function to get cluster info """
93
108
  def get_cluster_info(self):
94
109
  return utils.make_request(self, _INFO_PATH, _GET)
@@ -23,21 +23,37 @@ _MINIO = "minio"
23
23
  _S3 = "s3"
24
24
  _ONE_DATA = "onedata"
25
25
  _WEBDAV = "webdav"
26
+
27
+ _GET = "get"
28
+ _CONFIG_PATH = "/system/config"
26
29
  _SVC_PATH = "/system/services"
27
30
 
28
31
 
29
32
  # TODO check returns from functions
30
33
  class Storage:
31
- def __init__(self, client_obj, svc_name) -> None:
34
+ def __init__(self, client_obj, svc_name = None) -> None:
32
35
  self.client_obj = client_obj
33
- self.svc_name = svc_name
34
- self._store_providers()
36
+ self.storage_providers = {}
37
+ if svc_name != None:
38
+ self.svc_name = svc_name
39
+ self._store_provider_from_service()
40
+ self._store_default_minio_provider()
35
41
 
36
42
  """ Function to store all the providers of the service """
37
- def _store_providers(self):
38
- svc = utils.make_request(self.client_obj, _SVC_PATH + "/" + self.svc_name, "get")
43
+ def _store_provider_from_service(self):
44
+ svc = utils.make_request(self.client_obj, _SVC_PATH + "/" + self.svc_name, _GET)
39
45
  self.storage_providers = json.loads(svc.text)["storage_providers"]
40
46
 
47
+ """ Function to store the user credentials for the default MinIO provider """
48
+ def _store_default_minio_provider(self):
49
+ config = utils.make_request(self.client_obj, _CONFIG_PATH + "/" , _GET)
50
+ if _MINIO in self.storage_providers:
51
+ self.storage_providers[_MINIO]["default"] = json.loads(config.text)["minio_provider"]
52
+ else:
53
+ default = {"default": json.loads(config.text)["minio_provider"]}
54
+ self.storage_providers[_MINIO] = default
55
+
56
+
41
57
  """ Function to retreive credentials of a specific storage provider """
42
58
  def _get_provider_creds(self, provider, provider_name):
43
59
  return self.storage_providers[provider][provider_name]
@@ -0,0 +1,206 @@
1
+ import json
2
+ import base64
3
+ from client import Client
4
+ #from client import Client
5
+ from storage import Storage
6
+ from client_anon import AnonymousClient
7
+
8
+
9
+ def get_svc_test(client):
10
+ response = client.get_service("cowsay")
11
+ print(response)
12
+
13
+ def cowsay_test_anon(client):
14
+ text = {"message": "blyat"}
15
+ tk = '38919156059e55805f66b81f74e8f6b59e0ba9e1f987900ed0a6eb0eb0aed82e'
16
+ response = client.run_service("cowsay", token=tk, input=json.dumps(text))
17
+ print(response.text)
18
+
19
+ def cowsay_test_text(client):
20
+ text = "blyat"
21
+ response = client.run_service("cowsay", input=text)
22
+ print(response.text)
23
+
24
+ def plants_test_anon(client):
25
+ tk = "201d9c9c252a47d70e61dacbf02353074921ddd4a39aaef16440949b604c2e26"
26
+ image_in = "/home/calarcon/Pictures/plant2.jpg"
27
+ response = client.run_service("ai4eosc-service", token= tk, input=image_in, timeout=400)
28
+ print(response)
29
+
30
+ def text_to_speech_test(client):
31
+ file_in = "/home/caterina/Documentos/text-to-speech-coqui/text.txt"
32
+ file_out = "/home/caterina/Documentos/text-to-speech-coqui/test-python-run-out.mp3"
33
+ response = client.run_service("text-to-speech-coqui", input="si vose volve a desir uwu", output="test-tes-test.mp3", timeout=100)
34
+ #print(response.text)
35
+
36
+ def plants_sync_test(client):
37
+ image_in = "/home/caterina/Documentos/egi_conference_2023/plants_example/images/cerezos.jpg"
38
+ response = client.run_service("plant-classification", input=image_in, timeout=400)
39
+
40
+
41
+ def grayify_test(client):
42
+ response = client.run_service("grayify", input="/home/caterina/Documentos/imagemagick_example/images")
43
+
44
+ def create_service_test(client):
45
+ path = "/home/caterina/Documentos/imagemagick_example/imagemagick.yaml"
46
+ return client.create_service(path)
47
+
48
+ def json_definition_example():
49
+ return {
50
+ "name": "grayify",
51
+ "memory": "1Gi",
52
+ "cpu": "1.0",
53
+ "total_memory": "1Gi",
54
+ "total_cpu": "1.0",
55
+ "log_level": "CRITICAL",
56
+ "image": "ghcr.io/grycap/imagemagick",
57
+ "alpine": True,
58
+ "script": "#!/bin/bash \necho \'SCRIPT: Invoked Image Grayifier. File available in $INPUT_FILE_PATH\' \nFILE_NAME=`basename \'$INPUT_FILE_PATH\'` \nOUTPUT_FILE=\'$TMP_OUTPUT_DIR/$FILE_NAME\' \necho \'SCRIPT: Converting input image file $INPUT_FILE_PATH to grayscale to output file $OUTPUT_FILE\' \nconvert \'$INPUT_FILE_PATH\' -type Grayscale \'$OUTPUT_FILE\'",
59
+ "image_pull_secrets": [
60
+ ],
61
+ "input": [
62
+ {
63
+ "storage_provider": "minio.default",
64
+ "path": "grayify/input",
65
+ "suffix": [
66
+ ""
67
+ ],
68
+ "prefix": [
69
+ ""
70
+ ]
71
+ }
72
+ ],
73
+ "output": [
74
+ {
75
+ "storage_provider": "minio.default",
76
+ "path": "grayify/output",
77
+ "suffix": [
78
+ ""
79
+ ],
80
+ "prefix": [
81
+ ""
82
+ ]
83
+ },
84
+ {
85
+ "storage_provider": "webdav.dcache",
86
+ "path": "/Users/calarcon/grayify",
87
+ "suffix": [
88
+ ""
89
+ ],
90
+ "prefix": [
91
+ ""
92
+ ]
93
+ }
94
+ ],
95
+ "storage_providers": {
96
+ "minio": {
97
+ "default": {
98
+ "endpoint": "http://console.minio.wizardly-lamport0.im.grycap.net",
99
+ "region": "us-west-1",
100
+ "secret_key": "a1b2c3d4",
101
+ "access_key": "minio",
102
+ "verify": True
103
+ }
104
+ },
105
+ "webdav": {
106
+ "dcache": {
107
+ "hostname": "prometheus.desy.de",
108
+ "login": "calarcon",
109
+ "password": "v5vsYhd2"
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ def ai4eosc_test(client):
116
+ service = {
117
+ "log_level": "CRITICAL",
118
+ "memory": "1Gi",
119
+ "cluster_id": "oscar-cluster",
120
+ "name": "test2",
121
+ "cpu": "1.0",
122
+ "vo": "vo.ai4eosc.eu",
123
+ "image": "deephdc/deep-oc-plants-classification-tf",
124
+ "alpine": False,
125
+ "script": "#!/bin/bash",
126
+ "allowed_users" : []
127
+ }
128
+ res = client.create_service(service)
129
+ print(res.text)
130
+
131
+ def get_service_base_definition():
132
+ """
133
+ Base parameters of an OSCAR service
134
+
135
+ """
136
+ return {
137
+
138
+ "log_level": "CRITICAL",
139
+ "alpine": False,
140
+ "script": "#!/bin/bash \nFILE_NAME=`basename $INPUT_FILE_PATH` \
141
+ \nOUTPUT_FILE=\"$TMP_OUTPUT_DIR/$FILE_NAME\"\
142
+ \necho \"SCRIPT: Invoked deepaas-predict command. File available in $INPUT_FILE_PATH.\" \
143
+ \deepaas-predict -i $INPUT_FILE_PATH -o $OUTPUT_FILE"
144
+ }
145
+
146
+ def main():
147
+
148
+ #client.remove_service(svc_name)
149
+ #local_client = Client("oscar-test","http://localhost", "oscar", "MTY5Yjc0", False)
150
+
151
+ # Options for basic auth login
152
+ options_basic_auth = {'cluster_id':'oscar-egi-cluster',
153
+ 'endpoint':'https://funny-kalam8.im.grycap.net',
154
+ 'user':'oscar',
155
+ 'password':'oscar123',
156
+ 'ssl':'True'}
157
+
158
+ options_egi = {'cluster_id':'oscar-cluster',
159
+ 'endpoint':'https://oscar.test.fedcloud.eu/',
160
+ 'shortname':'calarcon',
161
+ 'ssl':'True'}
162
+
163
+ SERVICE_NAME = "grayify"
164
+ token = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJQVVlPaXJBM1ktZF9kR3BkajRpSkRIdzR6SGE4SVktYmhaZGFFajByamJVIn0.eyJleHAiOjE3MTY4MDMwMDksImlhdCI6MTcxNjc5OTQwOSwiYXV0aF90aW1lIjoxNzE2Nzk1NDgzLCJqdGkiOiI0OTBmNDdlNy00NTc2LTRjODYtYWUyMy00ZjIxNDUzZDEzMjMiLCJpc3MiOiJodHRwczovL2FhaS5lZ2kuZXUvYXV0aC9yZWFsbXMvZWdpIiwic3ViIjoiNjJiYjExYjQwMzk4ZjczNzc4YjY2ZjM0NGQyODIyNDJkZWJiOGVlM2ViYjEwNjcxN2ExMjNjYTIxMzE2MjkyNkBlZ2kuZXUiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJpbS1kYXNoYm9hcmQiLCJzZXNzaW9uX3N0YXRlIjoiMjhiMjU5OGUtYmQ2YS00Y2UzLWI4MDUtNDgyNWRmNGQ2ZjM0Iiwic2NvcGUiOiJvcGVuaWQgb2ZmbGluZV9hY2Nlc3MgZWR1cGVyc29uX2VudGl0bGVtZW50IHByb2ZpbGUgZW1haWwiLCJzaWQiOiIyOGIyNTk4ZS1iZDZhLTRjZTMtYjgwNS00ODI1ZGY0ZDZmMzQiLCJhdXRoZW50aWNhdGluZ19hdXRob3JpdHkiOiJodHRwczovL3d3dy5yZWRpcmlzLmVzL3Npci91cHZpZHAifQ.aZib2Rypqy-XGjPn4ycmEA4u1nlNUb45zz4ry8yZevAdna3lNzEIvxsfwYpjgAvmiDEzrJMYRNAm2652EgvCPUK4bnn9qJuzPhEfSpRbam_jDxmG9MiX6XlKSIO-UqlJQLuDxAAAGuxhdvfqffaT9rZ10042w6-0qTvSiXtNOvFkusc4iVHp8eBcr469txMFvEcWoV8uPXtTL6bl8rbEQMEo2KT1p9twzVpL1WQNIa0XrVa4pfIS3V_IDCKjcDl8PmV3N7KKKkIiQEFg13FS6TSLEs2peszi8eqSAXX2Lq27eSxl0pPH3ZTcrA7vI5dyTKSi7qoK7lsKtHZJOvOCvw"
165
+ options_egi_oidc = {'cluster_id':'oscar-egi-oidc',
166
+ 'endpoint':'https://inference.cloud.ai4eosc.eu',
167
+ 'oidc_token': token,
168
+ 'ssl':'True'}
169
+
170
+ #client = Client("oscar-cluster","https://sleepy-brown8.im.grycap.net", "oscar", "oscar123", True)
171
+ anon_client = AnonymousClient({
172
+ 'cluster_id':'oscar-cluster',
173
+ 'endpoint':'https://funny-kalam8.im.grycap.net',
174
+ 'ssl':'true'
175
+ })
176
+
177
+ #get_svc_test(anon_client)
178
+
179
+ try:
180
+ client = Client(options = options_egi)
181
+ print("Client created!!")
182
+ except:
183
+ print("Error creating OSCAR client!!!!11")
184
+
185
+ # res = client.run_service(SERVICE_NAME, input="/home/calarcon/Pictures/cat.jpg", output=".")
186
+ # print(res)
187
+ storage = client.create_storage_client()
188
+ print(storage.storage_providers)
189
+ print("------------------------")
190
+ storage = client.create_storage_client("grayify")
191
+ print(storage.storage_providers)
192
+ #cowsay_test_text(client)
193
+ #get_svc_test(client)
194
+ #grayify_storage = client.create_storage_client(svc_name)
195
+ #stablediff_storage = client.create_storage_client("stable-diffusion")
196
+
197
+ # Download files test
198
+ #res = stablediff_storage.download_file("minio.default", "/home/caterina/Documentos/oscar_python_package", "stable-diffusion/out/text1.zip")
199
+ #res = grayify_storage.download_file("minio.default", "/home/caterina/Documentos/oscar_python_package", "gray/output/image1.jpg")
200
+
201
+ # Upload files test
202
+ #res = stablediff_storage.upload_file("minio.default", "/home/caterina/Documentos/txt_to_img/text1.txt", "stable-diffusion/in")
203
+ #res = grayify_storage.upload_file("minio.default", "/home/caterina/Documentos/imagemagick_example/images/image2.jpeg", "gray/input")
204
+
205
+ if __name__ == "__main__":
206
+ main()
@@ -1,35 +1,20 @@
1
- Metadata-Version: 2.2
2
- Name: oscar_python
3
- Version: 1.3.0
1
+ Metadata-Version: 2.1
2
+ Name: oscar-python
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
12
13
  License-File: LICENSE
13
- Requires-Dist: webdavclient3==3.14.6
14
- Requires-Dist: requests
15
- Requires-Dist: boto3
16
- Requires-Dist: setuptools>=40.8.0
17
- Requires-Dist: pyyaml
18
- Requires-Dist: aiohttp
19
- Requires-Dist: liboidcagent
20
- Dynamic: author
21
- Dynamic: author-email
22
- Dynamic: classifier
23
- Dynamic: description
24
- Dynamic: description-content-type
25
- Dynamic: home-page
26
- Dynamic: license
27
- Dynamic: requires-dist
28
- Dynamic: summary
29
14
 
30
15
  ## Python OSCAR client
31
16
 
32
- [![Build](https://github.com/grycap/oscar_python/actions/workflows/main.yaml/badge.svg)](https://github.com/grycap/oscar_python/actions/workflows/main.yaml)
17
+ [![Build](https://github.com/grycap/oscar_python/actions/workflows/release.yaml/badge.svg)](https://github.com/grycap/oscar_python/actions/workflows/main.yaml)
33
18
  ![PyPI](https://img.shields.io/pypi/v/oscar_python)
34
19
 
35
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/).
@@ -84,6 +69,33 @@ If you already have a valid token, you can use the parameter `oidc_token` instea
84
69
  ```
85
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`.
86
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
+
87
99
  ### Sample usage
88
100
 
89
101
  - Sample code that creates a client and gets information about the cluster
@@ -219,11 +231,19 @@ response = client.remove_all_jobs("service_name") # returns an http response
219
231
 
220
232
  #### Storage usage
221
233
 
222
- You can create a storage object to operate over the different storage providers defined on a service with the method `create_storage_client` as follows:
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.
223
242
 
224
243
  ``` python
225
244
  storage_service = client.create_storage_client("service_name") # returns a storage object
226
245
  ```
246
+
227
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)_
228
248
 
229
249
  **list_files_from_path**
@@ -246,3 +266,5 @@ response = storage_service.upload_file("storage_provider", "local_path", "remote
246
266
  # download a file from a remote path to a local path
247
267
  response = storage_service.download_file("storage_provider", "local_path", "remote_path")
248
268
  ```
269
+
270
+
@@ -2,12 +2,14 @@ LICENSE
2
2
  README.md
3
3
  setup.py
4
4
  oscar_python/__init__.py
5
+ oscar_python/_oidc.py
5
6
  oscar_python/_utils.py
6
7
  oscar_python/client.py
7
8
  oscar_python/client_anon.py
8
9
  oscar_python/default_client.py
9
10
  oscar_python/local_test.py
10
11
  oscar_python/storage.py
12
+ oscar_python/test_v2.py
11
13
  oscar_python.egg-info/PKG-INFO
12
14
  oscar_python.egg-info/SOURCES.txt
13
15
  oscar_python.egg-info/dependency_links.txt
@@ -21,6 +23,7 @@ oscar_python/_providers/_s3.py
21
23
  oscar_python/_providers/_webdav.py
22
24
  tests/test_client.py
23
25
  tests/test_default_client.py
26
+ tests/test_oidc.py
24
27
  tests/test_onedata.py
25
28
  tests/test_s3.py
26
29
  tests/test_storage.py
@@ -1,3 +1,5 @@
1
+ build
2
+ dist
1
3
  jupyter_example
2
4
  oscar_python
3
5
  tests
@@ -44,6 +44,15 @@ def test_oidc_client(options):
44
44
  assert client.oidc_token == options['oidc_token']
45
45
  assert client.ssl == options['ssl']
46
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
+
47
56
 
48
57
  def test_set_auth_type(options):
49
58
  client = Client(options)
@@ -0,0 +1,38 @@
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
+ )
@@ -11,9 +11,9 @@ def mock_client_obj():
11
11
  @pytest.fixture
12
12
  def storage(mock_client_obj):
13
13
  mock_response = MagicMock()
14
- mock_response.text = '{"storage_providers": {"minio": {"default": {"access_key": "key","secret_key": "secret", "endpoint": "http://test.endpoint", "region": "us-east-1", "verify": false}}}}'
14
+ mock_response.text = '{"minio_provider": {"access_key": "key","secret_key": "secret", "endpoint": "http://test.endpoint", "region": "us-east-1", "verify": false}}'
15
15
  with patch('oscar_python._utils.make_request', return_value=mock_response):
16
- return Storage(mock_client_obj, "test_service")
16
+ return Storage(mock_client_obj)
17
17
 
18
18
 
19
19
  def test_get_provider_creds(storage):
@@ -29,7 +29,9 @@ def test_get_headers_with_oidc_agent():
29
29
  def test_get_headers_with_oidc():
30
30
  class MockClient:
31
31
  _AUTH_TYPE = "oidc"
32
- oidc_token = "test_oidc_token"
32
+
33
+ def get_access_token(self):
34
+ return "test_oidc_token"
33
35
 
34
36
  c = MockClient()
35
37
  headers = utils.get_headers(c)
File without changes
@@ -1,7 +1,7 @@
1
- webdavclient3==3.14.6
2
- requests
3
- boto3
4
- setuptools>=40.8.0
5
- pyyaml
6
1
  aiohttp
2
+ boto3
7
3
  liboidcagent
4
+ pyyaml
5
+ requests
6
+ setuptools>=40.8.0
7
+ webdavclient3==3.14.6
File without changes
File without changes