Skip to content

Commit 9ed9cb5

Browse files
authored
Merge branch 'main' into ODSC-59115/mlforecast_add_data
2 parents a979c97 + debfe43 commit 9ed9cb5

File tree

7 files changed

+163
-15
lines changed

7 files changed

+163
-15
lines changed

ads/opctl/operator/common/operator_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class InputData(DataClassSerializable):
2828
limit: int = None
2929
sql: str = None
3030
table_name: str = None
31+
vault_secret_id: str = None
3132

3233

3334
@dataclass(repr=True)

ads/opctl/operator/lowcode/anomaly/schema.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ spec:
7878
limit:
7979
required: false
8080
type: integer
81+
vault_secret_id:
82+
required: false
83+
type: string
8184

8285
validation_data:
8386
required: false
@@ -130,6 +133,9 @@ spec:
130133
limit:
131134
required: false
132135
type: integer
136+
vault_secret_id:
137+
required: false
138+
type: string
133139

134140
datetime_column:
135141
type: dict

ads/opctl/operator/lowcode/common/utils.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import argparse
88
import logging
99
import os
10+
import shutil
1011
import sys
12+
import tempfile
1113
import time
1214
from string import Template
1315
from typing import Any, Dict, List, Tuple
@@ -28,6 +30,7 @@
2830
)
2931
from ads.opctl.operator.common.operator_config import OutputDirectory
3032
from ads.common.object_storage_details import ObjectStorageDetails
33+
from ads.secrets import ADBSecretKeeper
3134

3235

3336
def call_pandas_fsspec(pd_fn, filename, storage_options, **kwargs):
@@ -53,10 +56,12 @@ def load_data(data_spec, storage_options=None, **kwargs):
5356
sql = data_spec.sql
5457
table_name = data_spec.table_name
5558
limit = data_spec.limit
56-
59+
vault_secret_id = data_spec.vault_secret_id
5760
storage_options = storage_options or (
5861
default_signer() if ObjectStorageDetails.is_oci_path(filename) else {}
5962
)
63+
if vault_secret_id is not None and connect_args is None:
64+
connect_args = dict()
6065

6166
if filename is not None:
6267
if not format:
@@ -76,15 +81,32 @@ def load_data(data_spec, storage_options=None, **kwargs):
7681
f"The format {format} is not currently supported for reading data. Please reformat the data source: {filename} ."
7782
)
7883
elif connect_args is not None:
79-
con = oracledb.connect(**connect_args)
80-
if table_name is not None:
81-
data = pd.read_sql_table(table_name, con)
82-
elif sql is not None:
83-
data = pd.read_sql(sql, con)
84-
else:
85-
raise InvalidParameterError(
86-
f"Database `connect_args` provided without sql query or table name. Please specify either `sql` or `table_name`."
87-
)
84+
with tempfile.TemporaryDirectory() as temp_dir:
85+
if vault_secret_id is not None:
86+
try:
87+
with ADBSecretKeeper.load_secret(vault_secret_id, wallet_dir=temp_dir) as adwsecret:
88+
if 'wallet_location' in adwsecret and 'wallet_location' not in connect_args:
89+
shutil.unpack_archive(adwsecret["wallet_location"], temp_dir)
90+
connect_args['wallet_location'] = temp_dir
91+
if 'user_name' in adwsecret and 'user' not in connect_args:
92+
connect_args['user'] = adwsecret['user_name']
93+
if 'password' in adwsecret and 'password' not in connect_args:
94+
connect_args['password'] = adwsecret['password']
95+
if 'service_name' in adwsecret and 'service_name' not in connect_args:
96+
connect_args['service_name'] = adwsecret['service_name']
97+
98+
except Exception as e:
99+
raise Exception(f"Could not retrieve database credentials from vault {vault_secret_id}: {e}")
100+
101+
con = oracledb.connect(**connect_args)
102+
if table_name is not None:
103+
data = pd.read_sql(f"SELECT * FROM {table_name}", con)
104+
elif sql is not None:
105+
data = pd.read_sql(sql, con)
106+
else:
107+
raise InvalidParameterError(
108+
f"Database `connect_args` provided without sql query or table name. Please specify either `sql` or `table_name`."
109+
)
88110
else:
89111
raise InvalidParameterError(
90112
f"No filename/url provided, and no connect_args provided. Please specify one of these if you want to read data from a file or a database respectively."

ads/opctl/operator/lowcode/forecast/schema.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ spec:
7878
limit:
7979
required: false
8080
type: integer
81+
vault_secret_id:
82+
required: false
83+
type: string
8184

8285
additional_data:
8386
required: false
@@ -130,6 +133,9 @@ spec:
130133
limit:
131134
required: false
132135
type: integer
136+
vault_secret_id:
137+
required: false
138+
type: string
133139

134140
test_data:
135141
required: false
@@ -181,6 +187,9 @@ spec:
181187
limit:
182188
required: false
183189
type: integer
190+
vault_secret_id:
191+
required: false
192+
type: string
184193
type: dict
185194

186195
output_directory:

docs/source/release_notes.rst

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,38 @@
22
Release Notes
33
=============
44

5+
2.11.14
6+
-------
7+
Release date: June 27, 2024
8+
9+
* Added compatibility with Python ``3.11``.
10+
* Fixed the bug in model deployment tail logging.
11+
512
2.11.13
6-
------
13+
-------
714
Release date: June 18, 2024
815

916
* Update langchain dependencies.
1017
* Support adding and removing artifact in a multi-model setting for model created by reference.
1118

1219

1320
2.11.12
14-
------
21+
-------
1522
Release date: June 13, 2024
1623

1724
* Fixed bugs and introduced enhancements following our recent release.
1825

1926

2027
2.11.11
21-
------
28+
-------
2229
Release date: June 11, 2024
2330

2431
* Fixed the bug that led to timeout when loading config files during jupyterlab load.
2532
* Fixed bugs and introduced enhancements following our recent release.
2633

2734

2835
2.11.10
29-
------
36+
-------
3037
Release date: June 5, 2024
3138

3239
* Support for Bring Your Own Model (BYOM) via AI Quick Actions.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ build-backend = "flit_core.buildapi"
2121

2222
# Required
2323
name = "oracle_ads" # the install (PyPI) name; name for local build in [tool.flit.module] section below
24-
version = "2.11.13"
24+
version = "2.11.14"
2525

2626
# Optional
2727
description = "Oracle Accelerated Data Science SDK"
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#!/usr/bin/env python
2+
from typing import Union
3+
4+
# Copyright (c) 2024 Oracle and/or its affiliates.
5+
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
6+
import pytest
7+
from ads.opctl.operator.lowcode.common.utils import (
8+
load_data,
9+
)
10+
from ads.opctl.operator.common.operator_config import InputData
11+
from unittest.mock import patch, Mock, MagicMock
12+
import unittest
13+
import pandas as pd
14+
15+
mock_secret = {
16+
'user_name': 'mock_user',
17+
'password': 'mock_password',
18+
'service_name': 'mock_service_name'
19+
}
20+
21+
mock_connect_args = {
22+
'user': 'mock_user',
23+
'password': 'mock_password',
24+
'service_name': 'mock_service_name',
25+
'dsn': 'mock_dsn'
26+
}
27+
28+
# Mock data for testing
29+
mock_data = pd.DataFrame({
30+
'id': [1, 2, 3],
31+
'name': ['Alice', 'Bob', 'Charlie']
32+
})
33+
34+
mock_db_connection = MagicMock()
35+
36+
load_secret_err_msg = "Vault exception message"
37+
db_connect_err_msg = "Mocked DB connection error"
38+
39+
40+
def mock_oracledb_connect_failure(*args, **kwargs):
41+
raise Exception(db_connect_err_msg)
42+
43+
44+
def mock_oracledb_connect(**kwargs):
45+
assert kwargs == mock_connect_args, f"Expected connect_args {mock_connect_args}, but got {kwargs}"
46+
return mock_db_connection
47+
48+
49+
class MockADBSecretKeeper:
50+
@staticmethod
51+
def __enter__(*args, **kwargs):
52+
return mock_secret
53+
54+
@staticmethod
55+
def __exit__(*args, **kwargs):
56+
pass
57+
58+
@staticmethod
59+
def load_secret(vault_secret_id, wallet_dir):
60+
return MockADBSecretKeeper()
61+
62+
@staticmethod
63+
def load_secret_fail(*args, **kwargs):
64+
raise Exception(load_secret_err_msg)
65+
66+
67+
class TestDataLoad(unittest.TestCase):
68+
def setUp(self):
69+
self.data_spec = Mock(spec=InputData)
70+
self.data_spec.connect_args = {
71+
'dsn': 'mock_dsn'
72+
}
73+
self.data_spec.vault_secret_id = 'mock_secret_id'
74+
self.data_spec.table_name = 'mock_table_name'
75+
self.data_spec.url = None
76+
self.data_spec.format = None
77+
self.data_spec.columns = None
78+
self.data_spec.limit = None
79+
80+
def testLoadSecretAndDBConnection(self):
81+
with patch('ads.secrets.ADBSecretKeeper.load_secret', side_effect=MockADBSecretKeeper.load_secret):
82+
with patch('oracledb.connect', side_effect=mock_oracledb_connect):
83+
with patch('pandas.read_sql', return_value=mock_data) as mock_read_sql:
84+
data = load_data(self.data_spec)
85+
mock_read_sql.assert_called_once_with(f"SELECT * FROM {self.data_spec.table_name}",
86+
mock_db_connection)
87+
pd.testing.assert_frame_equal(data, mock_data)
88+
89+
def testLoadVaultFailure(self):
90+
with patch('ads.secrets.ADBSecretKeeper.load_secret', side_effect=MockADBSecretKeeper.load_secret_fail):
91+
with pytest.raises(Exception) as e:
92+
load_data(self.data_spec)
93+
94+
expected_msg = f"Could not retrieve database credentials from vault {self.data_spec.vault_secret_id}: {load_secret_err_msg}"
95+
assert str(e.value) == expected_msg, f"Expected exception message '{expected_msg}', but got '{str(e)}'"
96+
97+
def testDBConnectionFailure(self):
98+
with patch('ads.secrets.ADBSecretKeeper.load_secret', side_effect=MockADBSecretKeeper.load_secret):
99+
with patch('oracledb.connect', side_effect=mock_oracledb_connect_failure):
100+
with pytest.raises(Exception) as e:
101+
load_data(self.data_spec)
102+
103+
assert str(e.value) == db_connect_err_msg , f"Expected exception message '{db_connect_err_msg }', but got '{str(e)}'"

0 commit comments

Comments
 (0)