Skip to content

Commit 3ae42cd

Browse files
feat: python rtl optional config ApiSettings init parameter (#996)
This PR adds an optional config_settings parameter to the python sdk init functions. It is up to the sdk user to provide a valid implementation of the ApiSettings class.
1 parent d5b0c99 commit 3ae42cd

File tree

5 files changed

+86
-19
lines changed

5 files changed

+86
-19
lines changed

Diff for: python/README.rst

+35-2
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,13 @@ Install looker_sdk using pipenv
9999
Configuring the SDK
100100
===================
101101

102-
The SDK supports configuration through a ``.ini`` file on disk as well
103-
as `setting environment variables <https://github.com/looker-open-source/sdk-codegen#environment-variable-configuration>`_ (the latter override the former).
102+
The SDK supports configuration through
103+
104+
1. an ``.ini`` file on disk
105+
2. `setting environment variables <https://github.com/looker-open-source/sdk-codegen#environment-variable-configuration>`_
106+
3. providing your own implementation of the ApiSettings class
107+
108+
. The latter override the former.
104109

105110
**Note**: The ``.ini`` configuration for the Looker SDK is a sample
106111
implementation intended to speed up the initial development of python
@@ -132,6 +137,34 @@ example file:
132137
For any ``.ini`` setting you can use an environment variable instead. It takes the form of
133138
``LOOKERSDK_<UPPERCASE-SETTING-FROM-INI>`` e.g. ``LOOKERSDK_CLIENT_SECRET``
134139

140+
A final option is to provide your own implementation of the ApiSettings class. It is easiest to subclass ``api_settings.ApiSettings`` and override the ``read_config`` function (don't forget a call to ``super().read_config()`` if appropriate, Example below). However, at a minimum your class must implement the `api_settings.PApiSettings` protocol.
141+
142+
143+
.. code-block:: python
144+
145+
import os
146+
import looker_sdk
147+
from looker_sdk import api_settings
148+
149+
class MyApiSettings(api_settings.ApiSettings):
150+
def __init__(self, *args, **kw_args):
151+
self.my_var = kw_args.pop("my_var")
152+
super().__init__(*args, **kw_args)
153+
154+
def read_config(self) -> api_settings.SettingsConfig:
155+
config = super().read_config()
156+
# See api_settings.SettingsConfig for required fields
157+
if self.my_var == "foo":
158+
config["client_id"] = os.getenv("FOO_CLIENT")
159+
config["client_secret"] = os.getenv("FOO_SECRET")
160+
else:
161+
config["client_id"] = os.getenv("BAR_CLIENT")
162+
config["client_secret"] = os.getenv("BAR_SECRET")
163+
return config
164+
165+
sdk = looker_sdk.init40(config_settings=MyApiSettings(my_var="foo"))
166+
...
167+
135168
136169
Code example
137170
============

Diff for: python/looker_sdk/__init__.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,14 @@ def _settings(
4949

5050

5151
def init31(
52-
config_file: str = "looker.ini", section: Optional[str] = None
52+
config_file: str = "looker.ini",
53+
section: Optional[str] = None,
54+
config_settings: Optional[api_settings.ApiSettings] = None,
5355
) -> methods31.Looker31SDK:
5456
"""Default dependency configuration"""
55-
settings = _settings(config_file, section)
57+
settings = (
58+
_settings(config_file, section) if config_settings is None else config_settings
59+
)
5660
settings.is_configured()
5761
transport = requests_transport.RequestsTransport.configure(settings)
5862
return methods31.Looker31SDK(
@@ -65,10 +69,14 @@ def init31(
6569

6670

6771
def init40(
68-
config_file: str = "looker.ini", section: Optional[str] = None
72+
config_file: str = "looker.ini",
73+
section: Optional[str] = None,
74+
config_settings: Optional[api_settings.ApiSettings] = None,
6975
) -> methods40.Looker40SDK:
7076
"""Default dependency configuration"""
71-
settings = _settings(config_file, section)
77+
settings = (
78+
_settings(config_file, section) if config_settings is None else config_settings
79+
)
7280
settings.is_configured()
7381
transport = requests_transport.RequestsTransport.configure(settings)
7482
return methods40.Looker40SDK(

Diff for: python/looker_sdk/rtl/api_settings.py

+36-10
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,30 @@
2626
import configparser as cp
2727
import os
2828
import sys
29-
from typing import Dict, Optional, Set
29+
from typing import Dict, Optional, Set, cast
3030
import warnings
3131

3232
from looker_sdk.rtl import transport
3333

3434
if sys.version_info >= (3, 8):
35-
from typing import Protocol
35+
from typing import Protocol, TypedDict
3636
else:
37-
from typing_extensions import Protocol
37+
from typing_extensions import Protocol, TypedDict
38+
from typing_extensions import Required
39+
40+
41+
class SettingsConfig(TypedDict, total=False):
42+
client_id: Required[str]
43+
client_secret: Required[str]
44+
base_url: str
45+
verify_ssl: str
46+
timeout: str
47+
redirect_uri: str
48+
looker_url: str
3849

3950

4051
class PApiSettings(transport.PTransportSettings, Protocol):
41-
def read_config(self) -> Dict[str, str]:
52+
def read_config(self) -> SettingsConfig:
4253
...
4354

4455

@@ -93,23 +104,27 @@ def __init__(
93104
if sdk_version:
94105
self.agent_tag += f" {sdk_version}"
95106

96-
def read_config(self) -> Dict[str, str]:
107+
def read_config(self) -> SettingsConfig:
97108
cfg_parser = cp.ConfigParser()
109+
data: SettingsConfig = {
110+
"client_id": "",
111+
"client_secret": "",
112+
}
98113
try:
99114
config_file = open(self.filename)
100115
except FileNotFoundError:
101-
data: Dict[str, str] = {}
116+
pass
102117
else:
103118
cfg_parser.read_file(config_file)
104119
config_file.close()
105120
# If section is not specified, use first section in file
106121
section = self.section or cfg_parser.sections()[0]
107122
if not cfg_parser.has_section(section):
108123
raise cp.NoSectionError(section)
109-
data = dict(cfg_parser[section])
124+
self._override_settings(data, dict(cfg_parser[section]))
110125

111126
if self.env_prefix:
112-
data.update(self._override_from_env())
127+
self._override_settings(data, self._override_from_env())
113128
return self._clean_input(data)
114129

115130
@staticmethod
@@ -122,6 +137,15 @@ def _bool(val: str) -> bool:
122137
raise TypeError
123138
return converted
124139

140+
def _override_settings(
141+
self, data: SettingsConfig, overrides: Dict[str, str]
142+
) -> SettingsConfig:
143+
# https://github.com/python/mypy/issues/6262
144+
for setting in SettingsConfig.__annotations__.keys(): # type: ignore
145+
if setting in overrides:
146+
data[setting] = overrides[setting] # type: ignore
147+
return data
148+
125149
def _override_from_env(self) -> Dict[str, str]:
126150
overrides = {}
127151
base_url = os.getenv(f"{self.env_prefix}_BASE_URL")
@@ -146,7 +170,7 @@ def _override_from_env(self) -> Dict[str, str]:
146170

147171
return overrides
148172

149-
def _clean_input(self, data: Dict[str, str]) -> Dict[str, str]:
173+
def _clean_input(self, data: SettingsConfig) -> SettingsConfig:
150174
"""Remove surrounding quotes and discard empty strings.
151175
"""
152176
cleaned = {}
@@ -157,6 +181,8 @@ def _clean_input(self, data: Dict[str, str]) -> Dict[str, str]:
157181
f"'{setting}' config setting is deprecated"
158182
)
159183
)
184+
if not isinstance(value, str):
185+
continue
160186
# Remove empty setting values
161187
if value in ['""', "''", ""]:
162188
continue
@@ -165,4 +191,4 @@ def _clean_input(self, data: Dict[str, str]) -> Dict[str, str]:
165191
cleaned[setting] = value.strip("\"'")
166192
else:
167193
cleaned[setting] = value
168-
return cleaned
194+
return cast(SettingsConfig, cleaned)

Diff for: python/looker_sdk/rtl/auth_session.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,8 @@ def __init__(
273273
# would have prefered using setattr(self, required, ...) in loop above
274274
# but mypy can't follow it
275275
self.client_id = config_data["client_id"]
276-
self.redirect_uri = config_data["redirect_uri"]
277-
self.looker_url = config_data["looker_url"]
276+
self.redirect_uri = config_data.get("redirect_uri", "")
277+
self.looker_url = config_data.get("looker_url", "")
278278
self.code_verifier = ""
279279

280280
def create_auth_code_request_url(self, scope: str, state: str) -> str:

Diff for: python/setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@
3232
VERSION = version["__version__"]
3333
REQUIRES = [
3434
"requests >= 2.22",
35+
"typing-extensions >= 4.1.1",
3536
# Python 3.6
3637
"attrs >= 18.2.0;python_version<'3.7'",
3738
"cattrs < 1.1.0;python_version<'3.7'",
3839
"python-dateutil;python_version<'3.7'",
3940
# Python 3.7+
4041
"attrs >= 20.1.0;python_version>='3.7'",
4142
"cattrs >= 1.3;python_version>='3.7'",
42-
"typing-extensions;python_version<'3.8'",
4343
]
4444

4545

0 commit comments

Comments
 (0)