-
Notifications
You must be signed in to change notification settings - Fork 196
feat: python rtl optional config ApiSettings init parameter #996
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: python rtl optional config ApiSettings init parameter #996
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fantastic! besides my suggested readme change I have one more request to update this code
- ideally some typing foo that specifies the return dict must have
client_id: str
andclient_secret: str
keys but may have any other keys as well - if that's not easily available then at least comment (or docstring?) on that method declaration indicating that those keys are necessary. PEP665 would have been good but we ned a solution that supports at least 3.7 (and ideally 3.6)
python/README.rst
Outdated
@@ -132,6 +137,35 @@ example file: | |||
For any ``.ini`` setting you can use an environment variable instead. It takes the form of | |||
``LOOKERSDK_<UPPERCASE-SETTING-FROM-INI>`` e.g. ``LOOKERSDK_CLIENT_SECRET`` | |||
|
|||
A final option is to provide your own implementation of the ApiSettings class. In particular you may want to override the ``read_config`` function. Example: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A final option is to provide your own implementation of the ApiSettings class. In particular you may want to override the ``read_config`` function. Example: | |
A final option is to provide your own implementation of the ApiSettings class. It is easiest to subclass ``api_settings.ApiSettings`` and override ``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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated comments and added type for read_config
from typing_extensions import Required | ||
|
||
class SettingsConfig(TypedDict, total=False): | ||
client_id: Required[str] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool! I'm approving action runs so we can see how this goes on all the python versions we support
|
||
class PApiSettings(transport.PTransportSettings, Protocol): | ||
def read_config(self) -> Dict[str, str]: | ||
def read_config(self) -> SettingsConfig: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added the type here and my editor was happy with it. Adding it to read_config
would probably require further code changes, probably not worth it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's see how the mypy and unittest runs go. If TypeDict.Required
is available on all our supported python versions then it actually would be nice (actually probably required) to update the default implementation's type signature
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You'll need to sign the Google CLA to get this in (see the google/cla check that's failing and click details for instructions on how to sign it)
from typing_extensions import Required | ||
|
||
class SettingsConfig(TypedDict, total=False): | ||
client_id: Required[str] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool! I'm approving action runs so we can see how this goes on all the python versions we support
class SettingsConfig(TypedDict, total=False): | ||
client_id: Required[str] | ||
client_secret: Required[str] | ||
base_url: Required[str] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
base_url
is technically not required. It's required to be an instance property on the settings object and the default implementation gets it from read_config
but that's not required. I know it's confusing - honestly the only reason read_config
exists is so that our default implementation doesn't have to store client_id
or client_secret
in python memory for security purposes. Otherwise they'd just be required instance properties too and there would be no read_config
|
||
class PApiSettings(transport.PTransportSettings, Protocol): | ||
def read_config(self) -> Dict[str, str]: | ||
def read_config(self) -> SettingsConfig: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's see how the mypy and unittest runs go. If TypeDict.Required
is available on all our supported python versions then it actually would be nice (actually probably required) to update the default implementation's type signature
yup, mypy run failed with |
If #1005 passes CI then I think it would be great if you could fix all the mypy errors so that we can use |
if sys.version_info >= (3, 8): | ||
from typing import Protocol | ||
from typing import Protocol, TypedDict | ||
else: | ||
from typing_extensions import Protocol | ||
from typing_extensions import Protocol, TypedDict | ||
|
||
from typing_extensions import Required |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that we're using typing_extensions
for Required
and it's not available till 3.11(?) we need to update this and setup.py (and we should specify the minimum version of typing_extensions to use)
if sys.version_info >= (3, 8):
from typing import Protocol
else:
from typing_extensions import Protocol, TypedDict
if sys.version_info >= (3, 11):
from typing import Required
else:
from typing_extensions import Required
starting to get a little unwieldy - I've seen other libraries consolidate into a _compat.py
library to do all this. But for now I think we're ok leaving it inline here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried with 3.11.0a4 but Required
wasn't in there.
So I've
- kept the import coming from
typing_extensions
- updated setup.py to require typing-extensions >= 4.1.1 regardless of the python version
I think that is the best way? Note we still can't use 3.11 anyway because of #944 (I pulled Required from typing_extensions running 3.11 and got the error referenced in that issue).
provided some relevant guidance inline I appreciate your effort on this! |
@@ -122,6 +135,21 @@ def _bool(val: str) -> bool: | |||
raise TypeError | |||
return converted | |||
|
|||
def _override_settings( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can avoid this with some cast
s, but I couldn't get anything else to infer the correct types without explicitly using the SettingsConfig keys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm actually I see now there is some use of cast
elsewhere. Personally I like the cast
solution better, less code changes and personally I feel the code is more future-proof, this way we could easily miss keys we want.
I'll put up a version with that so you can compare
cfg_parser = cp.ConfigParser() | ||
data: SettingsConfig = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the user doesn't end up settings these they'll run into this exception
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good!
I don't know why I always have to approve your check runs. thought that was a one-time thing. anyway, getting closer but mypy
still failed:
looker_sdk/rtl/auth_session.py:276: error: Expression has type "Any"
looker_sdk/rtl/auth_session.py:276: error: TypedDict "SettingsConfig" has no key "redirect_uri"
looker_sdk/rtl/auth_session.py:277: error: Expression has type "Any"
looker_sdk/rtl/auth_session.py:277: error: TypedDict "SettingsConfig" has no key "looker_url"
looker_sdk/rtl/auth_session.py:286: error: Expression type contains "Any" (has type "Tuple[str, Any]")
looker_sdk/rtl/auth_session.py:286: error: Expression has type "Any"
looker_sdk/rtl/auth_session.py:292: error: Expression has type "Any"
looker_sdk/rtl/auth_session.py:294: error: Expression has type "Any"
looker_sdk/rtl/auth_session.py:338: error: Expression has type "Any"
looker_sdk/rtl/auth_session.py:349: error: Expression has type "Any"
points out some inconsistencies we have - added "OAuthSession" support that needs redirect_uri
and looker_url
so we should add those to both SettingConfig
and PApiSettings
as optional.
you should be able to check it locally (might need to install requests-types) with:
pipenv run mypy looker_sdk/
@@ -122,6 +135,21 @@ def _bool(val: str) -> bool: | |||
raise TypeError | |||
return converted | |||
|
|||
def _override_settings( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
Ah whoops ok I'll take a look at it. I was actually running mypy locally but there were quite a few complaints about |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok added redirect_uri
and looker_url
to SettingsConfig. Simplified _override_settings
to just loop over SettingsConfig's keys (we could do the same thing in _override_from_env if you want?). Reverted _clean_input to original impl.
@@ -146,7 +170,7 @@ def _override_from_env(self) -> Dict[str, str]: | |||
|
|||
return overrides | |||
|
|||
def _clean_input(self, data: Dict[str, str]) -> Dict[str, str]: | |||
def _clean_input(self, data: SettingsConfig) -> SettingsConfig: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverted back to more or less the original version here with a cast
at the end.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
works for me!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work! really appreciate it.
mypy and unittests are passing. our integration tests won't run/pass because they use a secret that doesn't transfer to forked repos. So I'm gonna update this branch and then override the failed integration test checks and merge it in. If, for some reason, our integration tests fail I'll forward fix them
@@ -146,7 +170,7 @@ def _override_from_env(self) -> Dict[str, str]: | |||
|
|||
return overrides | |||
|
|||
def _clean_input(self, data: Dict[str, str]) -> Dict[str, str]: | |||
def _clean_input(self, data: SettingsConfig) -> SettingsConfig: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
works for me!
Supersedes #972
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 theApiSettings
class.