Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
cfenv==0.5.3
requests==2.25.1
typing-extensions==4.0.0
ai-api-client-sdk==2.4.0
# Pinning aenum to <=3.1.12 because versions >3.1.12 omit the 'NoneType'
# attribute on PyPy3 (see https://github.com/ethanfurman/aenum/issues/32).
aenum==3.1.12
Empty file.
62 changes: 62 additions & 0 deletions sap/aibus/dar/client/aiapi/dar_ai_api_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Base client for DAR AI API."""

from typing import Callable
from typing import Optional

from ai_api_client_sdk.ai_api_v2_client import AIAPIV2Client
from sap.aibus.dar.client.aiapi.dar_ai_api_file_upload_client import (
DARAIAPIFileUploadClient,
)


class DARAIAPIClient(AIAPIV2Client):
"""Base client for DAR AI API."""

# pylint: disable=too-many-arguments
def __init__(
self,
base_url: str,
auth_url: Optional[str] = None,
client_id: Optional[str] = None,
client_secret: Optional[str] = None,
cert_str: Optional[str] = None,
key_str: Optional[str] = None,
cert_file_path: Optional[str] = None,
key_file_path: Optional[str] = None,
token_creator: Optional[Callable[[], str]] = None,
):
"""
Initialize the DARAIAPIClient.

:param base_url: The base URL of the DAR AI API.
:param auth_url: The URL of the authorization endpoint, defaults to None
:param client_id: The client id to be used for authorization,
defaults to None
:param client_secret: The client secret to be used for authorization,
defaults to None
:param cert_str: The certificate file content, needs to be provided alongside
the key_str parameter, defaults to None
:param key_str: The key file content, needs to be provided alongside
the cert_str parameter, defaults to None
:param cert_file_path: The path to the certificate file, needs to be provided
alongside the key_file_path parameter, defaults to None
:param key_file_path: The path to the key file, needs to be provided alongside
the cert_file_path parameter, defaults to None
:param token_creator: The function which returns the Bearer token,
when called, defaults to None.
"""
super().__init__(
base_url=base_url,
auth_url=auth_url,
client_id=client_id,
client_secret=client_secret,
cert_str=cert_str,
key_str=key_str,
cert_file_path=cert_file_path,
key_file_path=key_file_path,
token_creator=token_creator,
)

self.file_upload_client = DARAIAPIFileUploadClient(
base_url=base_url, get_token=self.rest_client.get_token
)
104 changes: 104 additions & 0 deletions sap/aibus/dar/client/aiapi/dar_ai_api_file_upload_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""Client for DAR File Upload AI API."""

import urllib.parse
from typing import Any, Callable

import requests
from requests import Response


class DARAIAPIFileUploadClient:
"""Client for DAR File Upload AI API.

This client provides methods to upload and delete files on the DAR service,
handling authentication and request preparation internally.
"""

def __init__(self, base_url: str, get_token: Callable[[], str]):
"""Initialize the DARFileUploadAIAPIClient.

:param base_url: The base URL of the DAR AI API.
:param get_token: A callable to fetch the authorization token.
"""
self.base_url = base_url + "/files"
self.get_token = get_token

def delete_file(self, remote_path: str) -> Response:
"""Delete file under defined remote path.

:param remote_path: The path to the file to delete on the server.

:returns: The HTTP response object from the API call.
"""
url = self.base_url + remote_path

return self._send("DELETE", url)

def put_file(
self, local_path: str, remote_path: str, overwrite: bool = False
) -> Response:
"""Upload file to the defined remote path.

:param local_path: The local path of the file to upload.
:param remote_path: The destination path for the file on the server.
:param overwrite: Whether to overwrite the file if it already exists,
defaults to False.

:returns: The HTTP response object from the API call.
"""
url = self.base_url + remote_path
headers = {
# Content-Type MUST be application/octet-stream, even when uploading
# e.g. CSV files
"Content-Type": "application/octet-stream",
}
params = {"overwrite": overwrite}
with open(local_path, "rb") as file:
return self._send(
method="PUT", url=url, headers=headers, params=params, data=file
)

def get_file_from_url(self, url: str) -> Response:
"""Download file under defined url.

:param url: The url to the file that needs to be downloaded

:returns: The downloaded file on the server from the url.
"""
return self._send("GET", url)

def _send( # pylint: disable=too-many-arguments
self,
method: str,
url: str,
headers: dict = None,
data: Any = None,
params: dict = None,
) -> Response:
"""Send an HTTP request using the `requests` library.

:param method: The HTTP method (e.g., 'GET', 'POST', 'PUT', 'DELETE').
:param url: The full URL for the request.
:param headers: The headers for the request,defaults to None.
:param data: The data payload for the request (e.g., file data),
defaults to None.
:param params: The query parameters for the URL,defaults to None.

:returns: The HTTP response object from the API call.
"""
session = requests.Session()
auth_headers = {
"Authorization": self.get_token(),
}
if headers:
auth_headers.update(headers)

req = requests.Request(
method=method, url=url, headers=auth_headers, data=data, params=params
)
prep = req.prepare()
prep.url = url
if params:
prep.url += "?" + urllib.parse.urlencode(params)
response = session.send(prep, verify=True)
return response
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ def get_long_version():
"requests~=2.20",
"typing-extensions~=4.0",
"cfenv~=0.5",
"ai-api-client-sdk~=2.4",
"aenum==3.1.12",
],
packages=find_packages(exclude=["tests"]),
include_package_data=True,
Expand Down
28 changes: 28 additions & 0 deletions tests/sap/aibus/dar/client/test_dar_ai_api_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from sap.aibus.dar.client.aiapi.dar_ai_api_client import DARAIAPIClient
import unittest

from sap.aibus.dar.client.aiapi.dar_ai_api_file_upload_client import (
DARAIAPIFileUploadClient,
)

BASE_URL = "https://dar-dummy.cfapps.sap.hana.ondemand.com/model-manager/v2/lm"
AUTH_URL = "https://dummy.authentication.sap.hana.ondemand.com/oauth/token"


class TestDARAIAPIClient(unittest.TestCase):
base_url = BASE_URL
auth_url = AUTH_URL
client_id = "a-client-id"
client_secret = "a-client-secret"
token = "1234567890"

def test_constructor(self):
"""Test the basic constructor."""
client = DARAIAPIClient(
base_url=self.base_url,
auth_url=self.auth_url,
client_id=self.client_id,
client_secret=self.client_secret,
)
assert client.base_url == self.base_url
assert isinstance(client.file_upload_client, DARAIAPIFileUploadClient)
Loading