Skip to content

Commit 70dc039

Browse files
authored
Merge pull request #712 from hkad98/ds-credentials
Inject data source credentials from config
2 parents 5380655 + 7326f3b commit 70dc039

File tree

5 files changed

+84
-23
lines changed

5 files changed

+84
-23
lines changed

gooddata-sdk/gooddata_sdk/catalog/data_source/declarative_model/data_source.py

+34-8
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from __future__ import annotations
33

44
from pathlib import Path
5-
from typing import Any, List, Optional, Type
5+
from typing import Any, List, Optional, Type, Union
6+
from warnings import warn
67

78
import attr
89
from gooddata_api_client.model.declarative_data_source import DeclarativeDataSource
@@ -13,7 +14,7 @@
1314
from gooddata_sdk.catalog.entity import TokenCredentialsFromFile
1415
from gooddata_sdk.catalog.parameter import CatalogParameter
1516
from gooddata_sdk.catalog.permission.declarative_model.permission import CatalogDeclarativeDataSourcePermission
16-
from gooddata_sdk.utils import create_directory, read_layout_from_file, write_layout_to_file
17+
from gooddata_sdk.utils import create_directory, get_ds_credentials, read_layout_from_file, write_layout_to_file
1718

1819
BIGQUERY_TYPE = "BIGQUERY"
1920
LAYOUT_DATA_SOURCES_DIR = "data_sources"
@@ -23,10 +24,9 @@
2324
class CatalogDeclarativeDataSources(Base):
2425
data_sources: List[CatalogDeclarativeDataSource]
2526

26-
def to_api(self, credentials: Optional[dict[str, Any]] = None) -> DeclarativeDataSources:
27+
def _inject_base(self, credentials: dict[str, Any]) -> DeclarativeDataSources:
2728
data_sources = []
2829
client_class = self.client_class()
29-
credentials = credentials if credentials is not None else dict()
3030
for data_source in self.data_sources:
3131
if data_source.id in credentials:
3232
if data_source.type == BIGQUERY_TYPE:
@@ -38,6 +38,34 @@ def to_api(self, credentials: Optional[dict[str, Any]] = None) -> DeclarativeDat
3838
data_sources.append(data_source.to_api())
3939
return client_class(data_sources=data_sources)
4040

41+
def _inject_credentials_legacy(self, credentials: dict[str, Any]) -> DeclarativeDataSources:
42+
return self._inject_base(credentials)
43+
44+
def _inject_credentials_aac(self, config_file: Union[str, Path]) -> DeclarativeDataSources:
45+
ds_ids = {ds.id for ds in self.data_sources}
46+
credentials = get_ds_credentials(config_file)
47+
missing = set(credentials.keys()).difference(ds_ids)
48+
if len(missing) > 0:
49+
warn(
50+
f"The following data sources are missing credentials: {missing}.",
51+
UserWarning,
52+
stacklevel=2,
53+
)
54+
return self._inject_base(credentials)
55+
56+
def to_api(
57+
self, credentials: Optional[dict[str, Any]] = None, config_file: Optional[Union[str, Path]] = None
58+
) -> DeclarativeDataSources:
59+
client_class = self.client_class()
60+
if credentials is not None and config_file is not None:
61+
raise ValueError("Only one of credentials or config_file should be provided")
62+
if credentials is None and config_file is None:
63+
return client_class(data_sources=[data_source.to_api() for data_source in self.data_sources])
64+
if credentials is not None:
65+
return self._inject_credentials_legacy(credentials)
66+
if config_file is not None:
67+
return self._inject_credentials_aac(config_file)
68+
4169
@staticmethod
4270
def client_class() -> Type[DeclarativeDataSources]:
4371
return DeclarativeDataSources
@@ -101,9 +129,7 @@ def data_source_folder(data_sources_folder: Path, data_source_id: str) -> Path:
101129
create_directory(data_source_folder)
102130
return data_source_folder
103131

104-
def to_api(
105-
self, password: Optional[str] = None, token: Optional[str] = None, include_nested_structures: bool = True
106-
) -> DeclarativeDataSource:
132+
def to_api(self, password: Optional[str] = None, token: Optional[str] = None) -> DeclarativeDataSource:
107133
dictionary = self._get_snake_dict()
108134
if password is not None:
109135
dictionary["password"] = password
@@ -114,7 +140,7 @@ def to_api(
114140
def store_to_disk(self, data_sources_folder: Path) -> None:
115141
data_source_folder = self.data_source_folder(data_sources_folder, self.id)
116142
file_path = data_source_folder / f"{self.id}.yaml"
117-
data_source_dict = self.to_api(include_nested_structures=False).to_dict(camel_case=True)
143+
data_source_dict = self.to_api().to_dict(camel_case=True)
118144

119145
write_layout_to_file(file_path, data_source_dict)
120146

gooddata-sdk/gooddata_sdk/catalog/data_source/service.py

+27-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import functools
55
from pathlib import Path
6-
from typing import Any, List, Optional, Tuple
6+
from typing import Any, List, Optional, Tuple, Union
77

88
from gooddata_api_client.exceptions import NotFoundException
99

@@ -29,7 +29,7 @@
2929
from gooddata_sdk.catalog.entity import TokenCredentialsFromFile
3030
from gooddata_sdk.catalog.workspace.declarative_model.workspace.logical_model.ldm import CatalogDeclarativeModel
3131
from gooddata_sdk.client import GoodDataApiClient
32-
from gooddata_sdk.utils import load_all_entities_dict, read_layout_from_file
32+
from gooddata_sdk.utils import get_ds_credentials, load_all_entities_dict, read_layout_from_file
3333

3434
_PDM_DEPRECATION_MSG = "This method is going to be deprecated due to PDM removal."
3535

@@ -173,6 +173,7 @@ def put_declarative_data_sources(
173173
self,
174174
declarative_data_sources: CatalogDeclarativeDataSources,
175175
credentials_path: Optional[Path] = None,
176+
config_file: Optional[Union[str, Path]] = None,
176177
test_data_sources: bool = False,
177178
) -> None:
178179
"""Set all data sources, including their related physical data model.
@@ -182,16 +183,18 @@ def put_declarative_data_sources(
182183
Declarative Data Source object. Can be retrieved by get_declarative_data_sources.
183184
credentials_path (Optional[Path], optional):
184185
Path to the Credentials. Optional, defaults to None.
186+
config_file (Optional[Union[str, Path]], optional):
187+
Path to the config file. Defaults to None.
185188
test_data_sources (bool, optional):
186189
If True, the connection of data sources is tested. Defaults to False.
187190
188191
Returns:
189192
None
190193
"""
191194
if test_data_sources:
192-
self.test_data_sources_connection(declarative_data_sources, credentials_path)
195+
self.test_data_sources_connection(declarative_data_sources, credentials_path, config_file)
193196
credentials = self._credentials_from_file(credentials_path) if credentials_path is not None else dict()
194-
self._layout_api.put_data_sources_layout(declarative_data_sources.to_api(credentials))
197+
self._layout_api.put_data_sources_layout(declarative_data_sources.to_api(credentials, config_file))
195198

196199
def store_declarative_data_sources(self, layout_root_path: Path = Path.cwd()) -> None:
197200
"""Store data sources layouts in a directory hierarchy.
@@ -236,6 +239,7 @@ def load_and_put_declarative_data_sources(
236239
self,
237240
layout_root_path: Path = Path.cwd(),
238241
credentials_path: Optional[Path] = None,
242+
config_file: Optional[Union[str, Path]] = None,
239243
test_data_sources: bool = False,
240244
) -> None:
241245
"""Loads and sets layouts stored using `store_declarative_data_sources`.
@@ -247,15 +251,17 @@ def load_and_put_declarative_data_sources(
247251
layout_root_path (Optional[Path], optional):
248252
Path to the root of the layout directory. Defaults to Path.cwd().
249253
credentials_path (Optional[Path], optional):
250-
Path to the credentials. Defaults to Path.cwd().
254+
Path to the credentials.
255+
config_file (Optional[Union[str, Path]], optional):
256+
Path to the config file.
251257
test_data_sources (bool, optional):
252258
If True, the connection of data sources is tested. Defaults to False.
253259
254260
Returns:
255261
None
256262
"""
257263
data_sources = self.load_declarative_data_sources(layout_root_path)
258-
self.put_declarative_data_sources(data_sources, credentials_path, test_data_sources)
264+
self.put_declarative_data_sources(data_sources, credentials_path, config_file, test_data_sources)
259265

260266
@staticmethod
261267
def store_pdm_to_disk(pdm: CatalogDeclarativeTables, path: Path = Path.cwd()) -> None:
@@ -438,7 +444,10 @@ def scan_sql(self, data_source_id: str, sql_request: ScanSqlRequest) -> ScanSqlR
438444
return ScanSqlResponse.from_api(self._actions_api.scan_sql(data_source_id, sql_request.to_api()))
439445

440446
def test_data_sources_connection(
441-
self, declarative_data_sources: CatalogDeclarativeDataSources, credentials_path: Optional[Path] = None
447+
self,
448+
declarative_data_sources: CatalogDeclarativeDataSources,
449+
credentials_path: Optional[Path] = None,
450+
config_file: Optional[Union[str, Path]] = None,
442451
) -> None:
443452
"""Tests connection to declarative data sources.
444453
@@ -453,6 +462,8 @@ def test_data_sources_connection(
453462
Declarative Data Sources object
454463
credentials_path (Optional[Path], optional):
455464
Path to the credentials. Defaults to None.
465+
config_file (Optional[Union[str, Path]], optional):
466+
Path to the config file. Defaults to None.
456467
457468
Raises:
458469
ValueError:
@@ -461,7 +472,15 @@ def test_data_sources_connection(
461472
Returns:
462473
None
463474
"""
464-
credentials = self._credentials_from_file(credentials_path) if credentials_path is not None else dict()
475+
476+
credentials = dict()
477+
if credentials_path is not None and config_file is not None:
478+
raise ValueError("Only one of credentials or config_file should be provided")
479+
if credentials_path is not None:
480+
credentials = self._credentials_from_file(credentials_path)
481+
if config_file is not None:
482+
credentials = get_ds_credentials(config_file)
483+
465484
errors: dict[str, str] = dict()
466485
for declarative_data_source in declarative_data_sources.data_sources:
467486
if credentials.get(declarative_data_source.id) is not None:

gooddata-sdk/gooddata_sdk/catalog/entity.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import base64
55
import os
66
from pathlib import Path
7-
from typing import Any, ClassVar, Dict, List, Optional, Type, TypeVar
7+
from typing import Any, ClassVar, Dict, List, Optional, Type, TypeVar, Union
88

99
import attr
1010

@@ -177,7 +177,7 @@ def from_api(cls, entity: dict[str, Any]) -> TokenCredentialsFromFile:
177177
raise NotImplementedError
178178

179179
@staticmethod
180-
def token_from_file(file_path: Path) -> str:
180+
def token_from_file(file_path: Union[str, Path]) -> str:
181181
with open(file_path, "rb") as fp:
182182
return base64.b64encode(fp.read()).decode("utf-8")
183183

gooddata-sdk/gooddata_sdk/config.py

+4
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,7 @@ class AacConfig(ConfigBase):
5151
profiles: Dict[str, Profile]
5252
default_profile: str
5353
access: Dict[str, str]
54+
55+
def ds_credentials(self) -> Dict[str, str]:
56+
load_dotenv()
57+
return {k: os.environ.get(v[1:], v) for k, v in self.access.items()}

gooddata-sdk/gooddata_sdk/utils.py

+17-5
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,16 @@ def _get_profile(profile: str, content: Dict) -> Dict[str, Any]:
282282
return _create_profile_legacy(content[profile])
283283

284284

285+
def _get_config_content(path: Union[str, Path]) -> Any:
286+
path = Path(path) if isinstance(path, str) else path
287+
if not path.exists():
288+
raise ValueError(f"The file does not exist on path {path}.")
289+
content = read_layout_from_file(path)
290+
if content is None:
291+
raise ValueError(f"The file is empty {path}.")
292+
return content
293+
294+
285295
def profile_content(profile: str = "default", profiles_path: Path = PROFILES_FILE_PATH) -> dict[str, Any]:
286296
"""Get the profile content from a given file.
287297
@@ -301,14 +311,16 @@ def profile_content(profile: str = "default", profiles_path: Path = PROFILES_FIL
301311
dict[str, Any]:
302312
Profile content as a dictionary.
303313
"""
304-
if not profiles_path.exists():
305-
raise ValueError(f"There is no profiles file located for path {profiles_path}.")
306-
content = read_layout_from_file(profiles_path)
307-
if content is None:
308-
raise ValueError(f"The config file is empty {profiles_path}.")
314+
content = _get_config_content(profiles_path)
309315
return _get_profile(profile, content)
310316

311317

318+
def get_ds_credentials(config_file: Union[str, Path]) -> Dict[str, str]:
319+
content = _get_config_content(config_file)
320+
config = AacConfig.from_dict(content)
321+
return config.ds_credentials()
322+
323+
312324
def good_pandas_profile_content(
313325
profile: str = "default", profiles_path: Path = PROFILES_FILE_PATH
314326
) -> Tuple[Dict[str, Any], Dict[str, Any]]:

0 commit comments

Comments
 (0)