Skip to content

Commit

Permalink
Merge pull request #3 from grycap/dev-calarcon
Browse files Browse the repository at this point in the history
Added output parameter to sync calls
Bump version 1.0.4
  • Loading branch information
catttam authored May 5, 2023
2 parents 4834ee7 + e5e7c37 commit 9eea68b
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 37 deletions.
34 changes: 10 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
## Python OSCAR API
## Python OSCAR client

[![Build](https://github.com/grycap/oscar_python/actions/workflows/main.yaml/badge.svg)](https://github.com/grycap/oscar_python/actions/workflows/main.yaml)
![PyPI](https://img.shields.io/pypi/v/oscar_python)

This package provides an API 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/).
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/).

### Contents
- [Python OSCAR API](#python-oscar-api)
- [Python OSCAR client](#python-oscar-client)
- [Contents](#contents)
- [Sample usage](#sample-usage)
- [API methods](#api-methods)
- [Client methods](#client-methods)
- [Cluster methods](#cluster-methods)
- [Service methods](#service-methods)
- [Logs methods](#logs-methods)
Expand Down Expand Up @@ -41,14 +41,14 @@ client = Client("cluster-id","https://cluster-endpoint", "username", "password",

try:
client.create_service("/absolute_path/cowsay.yaml")
res = client.run_service("cowsay", '{"message": "Hi there"}')
if res.status_code == 200:
print(res.text)
response = client.run_service("cowsay", input = '{"message": "Hi there"}')
if response.status_code == 200:
print(response.text)
except Exception as err:
print("Failed with: ", err)
```

### API methods
### Client methods

#### Cluster methods

Expand Down Expand Up @@ -100,25 +100,11 @@ response = client.remove_service("service_name") # returns an http response

**run_service**

The `input` parameter may not be passed if the function doesn't require input.
*`input`, `output` and `timeout` are optional parameters.*

``` python
# make a synchronous execution
response = client.run_service("service_name", input="input") # returns an http response
```

A service could fail if the input is an image and the input data is not well encoded.
To run a service with an image. It is necessary to read the file as binary and encode the content into base64.

``` python
import base64

with open('path/to/image', 'rb') as binary_file:
binary_file_data = binary_file.read()
base64_encoded_data = base64.b64encode(binary_file_data)
base64_message = base64_encoded_data.decode('utf-8')

print(base64_message)
response = client.run_service("service_name", input="input", output="out.png", timeout=100) # returns an http response
```

#### Logs methods
Expand Down
72 changes: 66 additions & 6 deletions oscar_python/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,33 @@
# limitations under the License.

import base64
import json
import os
import requests
_DEFAULT_TIMEOUT = 30

""" Generic http request """
def make_request(c , path, method, **kwargs):

if "timeout" in kwargs.keys() and kwargs["timeout"]:
timeout = kwargs["timeout"]
print("timeout set to: ", timeout)
else:
timeout = _DEFAULT_TIMEOUT

url = c.endpoint+path
headers = get_headers(c)
headers = get_headers(c)
if method in ["post", "put"]:
if "token" in kwargs.keys() and kwargs["token"]:
headers = get_headers_with_token(kwargs["token"])
if "data" in kwargs.keys() and kwargs["data"]:
result = requests.request(method, url, headers=headers, verify=c.ssl, data=kwargs["data"])
result = requests.request(method, url, headers=headers, verify=c.ssl, data=kwargs["data"], timeout=timeout)
else:
result = requests.request(method, url, headers=headers, verify=c.ssl)
result = requests.request(method, url, headers=headers, verify=c.ssl, timeout=timeout)

if "handle" in kwargs.keys() and kwargs["handle"] == False:
return result

result.raise_for_status()
return result

Expand All @@ -43,5 +53,55 @@ def get_headers(c):
def get_headers_with_token(token):
return {"Authorization": "Bearer "+ str(token)}

def raise_http_errors(response):
response.raise_for_status()
def write_text_file(content, file_path):
with open(file_path, 'w') as f:
f.write(content)

def isBase64(st):
try:
base64.b64decode(st)
return True
except:
return False

def decode_b64(b64_str, file_out):
file_extension = os.path.splitext(file_out)[1]
try:
decoded_data = base64.b64decode(b64_str)

if file_extension in [".txt", ".json"]:
decode = 'w'
decoded_data = decoded_data.decode("utf-8")
else:
decode = 'wb'

with open(file_out, decode) as f:
f.write(decoded_data)

except ValueError:
print('Error decoding output: Invalid base64 string.')
except OSError:
print('Error decoding output: Failed to write decoded data to file.')

def encode_input(data):
if os.path.isfile(data):
try:
with open(data, 'rb') as file:
return base64.b64encode(file.read())
except FileNotFoundError:
print('Error encoding input: File {0} not found.'.format(data))
except OSError:
print('Error encoding input: Failed to read file.')
else:
message_bytes = data.encode('ascii')
return base64.b64encode(message_bytes)

def decode_output(output, file_path):
if(isBase64(output)):
decode_b64(output, file_path)
return
if(isinstance(output,str)):
write_text_file(output,file_path)
return


29 changes: 23 additions & 6 deletions oscar_python/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import json
import yaml
import oscar_python._utils as utils
Expand All @@ -34,6 +35,7 @@
_POST = "post"
_PUT = "put"
_DELETE = "delete"
_DEFAULT_TIMEOUT = 30

class Client:
#Cluster info
Expand Down Expand Up @@ -86,7 +88,7 @@ def _apply_service(self, fdl_path, method):
with open(svc["script"]) as s:
svc["script"] = s.read()
except IOError as err:
raise("Bouldn't read script")
raise("Couldn't read script")

# cpu parameter has to be string on the request
if type(svc["cpu"]) is int or type(svc["cpu"]) is float: svc["cpu"]= str(svc["cpu"])
Expand All @@ -110,13 +112,28 @@ def update_service(self, name, fdl_path):
def remove_service(self, name):
return utils.make_request(self, _SVC_PATH+"/"+name, _DELETE)

""" Run a synchronous execution """
def run_service(self, name, input=""):
token = self._get_token(name)
if input: return utils.make_request(self, _RUN_PATH+"/"+name, _POST, data=input, token=token)
""" Run a synchronous execution.
If an output is provided the result is decoded onto the file.
In both cases the function returns the HTTP response."""
def run_service(self, name, **kwargs):
if "input" in kwargs.keys() and kwargs["input"]:
exec_input = kwargs["input"]
token = self._get_token(name)

send_data = utils.encode_input(exec_input)

if "timeout" in kwargs.keys() and kwargs["timeout"]:
response = utils.make_request(self, _RUN_PATH+"/"+name, _POST, data=send_data, token=token, timeout=kwargs["timeout"])
else:
response = utils.make_request(self, _RUN_PATH+"/"+name, _POST, data=send_data, token=token)

if "output" in kwargs.keys() and kwargs["output"]:
utils.decode_output(response.text, kwargs["output"])
return response

return utils.make_request(self, _RUN_PATH+"/"+name, _POST, token=token)

""" Run an asynchronous execution (not usable at the moment). """
""" Run an asynchronous execution (unable at the moment). """
#TODO
""" def _run_job(self, name, input_path =""):
pass
Expand Down
2 changes: 1 addition & 1 deletion version.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
"""Stores the package version."""


__version__ = '1.0.3'
__version__ = '1.0.4'

0 comments on commit 9eea68b

Please sign in to comment.