looker-sdk 24.18.0__py3-none-any.whl → 24.20.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,55 @@
1
+ import os
2
+ import pytest # type: ignore
3
+ from urllib.parse import urlparse
4
+
5
+ from looker_sdk.sdk.api40 import methods as mtds
6
+ from looker_sdk.sdk.api40 import models as ml
7
+
8
+ NETRC_LOCATION = os.path.expanduser("~/.netrc")
9
+
10
+
11
+ @pytest.fixture()
12
+ def sdk(sdk40) -> mtds.Looker40SDK:
13
+ return sdk40
14
+
15
+
16
+ def can_create_netrc_file():
17
+ """Check if netrc can be created in home directory."""
18
+ can = False
19
+ if NETRC_LOCATION.startswith("~") or os.path.exists(NETRC_LOCATION):
20
+ can = False
21
+ else:
22
+ can = True
23
+ return can
24
+
25
+
26
+ @pytest.fixture()
27
+ def create_netrc_file(sdk: mtds.Looker40SDK):
28
+ """Create a sample netrc meant to cause conflicts with the looker.ini file"""
29
+ host = urlparse(sdk.auth.settings.base_url).netloc.split(":")[0]
30
+ netrc_contents = (
31
+ f"machine {host}"
32
+ f"\n login netrc_client_id"
33
+ f"\n password netrc_client_secret"
34
+ )
35
+
36
+ with open(NETRC_LOCATION, "w") as netrc_file:
37
+ netrc_file.write(netrc_contents)
38
+
39
+ yield
40
+
41
+ os.remove(NETRC_LOCATION)
42
+
43
+
44
+ @pytest.mark.skipif(
45
+ not can_create_netrc_file(),
46
+ reason="netrc file cannot be created because it already exists or $HOME is undefined", # noqa: B950
47
+ )
48
+ @pytest.mark.usefixtures("create_netrc_file")
49
+ def test_netrc_does_not_override_ini_creds(sdk: mtds.Looker40SDK):
50
+ """The requests library overrides HTTP authorization headers if the auth= parameter
51
+ is not specified, resulting in an authentication error. This test makes sure this
52
+ does not happen when netrc files are found on the system.
53
+ """
54
+ me = sdk.me()
55
+ assert isinstance(me, ml.User)
tests/rtl/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """I exist so that mypy.ini "[mypy-tests.*]" config works.
2
+ """
@@ -0,0 +1,216 @@
1
+ # The MIT License (MIT)
2
+ #
3
+ # Copyright (c) 2019 Looker Data Sciences, Inc.
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ import datetime
24
+ import json
25
+ from typing import MutableMapping, Optional, Union
26
+
27
+ import pytest # type: ignore
28
+
29
+ from looker_sdk import error
30
+ from looker_sdk.rtl import auth_session
31
+ from looker_sdk.rtl import api_settings
32
+ from looker_sdk.rtl import api_methods
33
+ from looker_sdk.rtl import requests_transport
34
+ from looker_sdk.rtl import serialize
35
+ from looker_sdk.rtl import transport
36
+ from looker_sdk.sdk import constants
37
+ from looker_sdk.sdk.api40 import models
38
+
39
+
40
+ @pytest.fixture(scope="module")
41
+ def api() -> api_methods.APIMethods:
42
+ settings = api_settings.ApiSettings(
43
+ filename="../looker.ini", env_prefix=constants.environment_prefix
44
+ )
45
+ transport = requests_transport.RequestsTransport.configure(settings)
46
+ auth = auth_session.AuthSession(settings, transport, serialize.deserialize40, "4.0")
47
+ return api_methods.APIMethods(
48
+ auth, serialize.deserialize40, serialize.serialize40, transport, "4.0"
49
+ )
50
+
51
+
52
+ @pytest.mark.parametrize(
53
+ "test_query_params, expected",
54
+ [
55
+ ({"a": None}, {}),
56
+ ({"a": True}, {"a": "true"}),
57
+ ({"a": "text"}, {"a": "text"}),
58
+ ({"a": 1}, {"a": "1"}),
59
+ ({"a": [1, 2, 3]}, {"a": "[1, 2, 3]"}),
60
+ ({"a": models.DelimSequence([1, 2, 3])}, {"a": "1,2,3"}),
61
+ ({"a": models.DelimSequence(["a", "b", "c"])}, {"a": "a,b,c"}),
62
+ (
63
+ {
64
+ "a": models.DelimSequence(
65
+ ["a", "b", "c"], prefix="<", suffix=">", separator="|"
66
+ )
67
+ },
68
+ {"a": "<a|b|c>"},
69
+ ),
70
+ ({"a": ["x", "xy", "xyz"]}, {"a": '["x", "xy", "xyz"]'}),
71
+ ({"a": datetime.datetime(2019, 8, 14, 8, 4, 2)}, {"a": "2019-08-14T08:04Z"}),
72
+ ],
73
+ )
74
+ def test_convert_query_params(
75
+ api: api_methods.APIMethods,
76
+ test_query_params: api_methods.TQueryParams,
77
+ expected: MutableMapping[str, str],
78
+ ):
79
+ actual = api._convert_query_params(test_query_params)
80
+ assert actual == expected
81
+
82
+
83
+ @pytest.mark.parametrize(
84
+ "test_body, expected",
85
+ [
86
+ ("some body text", b"some body text"),
87
+ ("", b""),
88
+ ([1, 2, 3], b"[1, 2, 3]"),
89
+ (["a", "b", "c"], b'["a", "b", "c"]'),
90
+ ({"foo": "bar"}, b'{"foo": "bar"}'),
91
+ (None, None),
92
+ (models.WriteApiSession(workspace_id="dev"), b'{"workspace_id": "dev"}'),
93
+ (
94
+ [
95
+ models.WriteApiSession(workspace_id="dev"),
96
+ models.WriteApiSession(workspace_id="dev"),
97
+ ],
98
+ b'[{"workspace_id": "dev"}, {"workspace_id": "dev"}]',
99
+ ),
100
+ ],
101
+ )
102
+ def test_get_serialized(
103
+ api: api_methods.APIMethods, test_body: api_methods.TBody, expected: Optional[bytes]
104
+ ):
105
+ actual = api._get_serialized(test_body)
106
+ assert actual == expected
107
+
108
+
109
+ @pytest.mark.parametrize(
110
+ "test_response, test_structure, expected",
111
+ [
112
+ (
113
+ transport.Response(
114
+ ok=True,
115
+ value=bytes(range(0, 10)),
116
+ response_mode=transport.ResponseMode.BINARY,
117
+ ),
118
+ Union[str, bytes],
119
+ bytes(range(0, 10)),
120
+ ),
121
+ (
122
+ transport.Response(
123
+ ok=True,
124
+ value=b"some response text",
125
+ response_mode=transport.ResponseMode.STRING,
126
+ ),
127
+ Union[str, bytes],
128
+ "some response text",
129
+ ),
130
+ (
131
+ transport.Response(
132
+ ok=True,
133
+ value=bytes("ئ", encoding="arabic"),
134
+ response_mode=transport.ResponseMode.STRING,
135
+ encoding="arabic",
136
+ ),
137
+ Union[str, bytes],
138
+ "ئ",
139
+ ),
140
+ (
141
+ transport.Response(
142
+ ok=True, value=b"", response_mode=transport.ResponseMode.STRING
143
+ ),
144
+ None,
145
+ None,
146
+ ),
147
+ (
148
+ transport.Response(
149
+ ok=True,
150
+ value=bytes(
151
+ json.dumps(
152
+ {
153
+ "current_version": {
154
+ "full_version": "6.18.4",
155
+ "status": "fully functional",
156
+ "swagger_url": None,
157
+ "version": None,
158
+ },
159
+ "looker_release_version": "6.18",
160
+ "supported_versions": None,
161
+ }
162
+ ),
163
+ encoding="utf-8",
164
+ ),
165
+ response_mode=transport.ResponseMode.STRING,
166
+ ),
167
+ models.ApiVersion,
168
+ models.ApiVersion(
169
+ looker_release_version="6.18",
170
+ current_version=models.ApiVersionElement(
171
+ version=None,
172
+ full_version="6.18.4",
173
+ status="fully functional",
174
+ swagger_url=None,
175
+ ),
176
+ supported_versions=None,
177
+ ),
178
+ ),
179
+ ],
180
+ )
181
+ def test_return(
182
+ api: api_methods.APIMethods,
183
+ test_response: transport.Response,
184
+ test_structure: api_methods.TStructure,
185
+ expected: api_methods.TReturn,
186
+ ):
187
+ actual = api._return(test_response, test_structure)
188
+ assert actual == expected
189
+
190
+
191
+ def test_return_raises_an_SDKError_for_bad_responses(api):
192
+ with pytest.raises(error.SDKError) as exc:
193
+ api._return(
194
+ transport.Response(
195
+ ok=False,
196
+ value=b"some error message",
197
+ response_mode=transport.ResponseMode.STRING,
198
+ ),
199
+ str,
200
+ )
201
+ assert "some error message" in str(exc.value)
202
+
203
+
204
+ @pytest.mark.parametrize(
205
+ "method_path, expected_url",
206
+ [
207
+ ("/user", "/api/4.0/user"),
208
+ ("user", "/api/4.0/user"),
209
+ ("/user/1", "/api/4.0/user/1"),
210
+ ("user/1", "/api/4.0/user/1"),
211
+ ],
212
+ )
213
+ def test_api_versioned_url_is_built_properly(
214
+ api: api_methods.APIMethods, method_path: str, expected_url: str
215
+ ):
216
+ assert api._path(method_path).endswith(expected_url)
@@ -0,0 +1,252 @@
1
+ # The MIT License (MIT)
2
+ #
3
+ # Copyright (c) 2019 Looker Data Sciences, Inc.
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ import configparser
24
+
25
+ import pytest # type: ignore
26
+
27
+ from looker_sdk import error
28
+ from looker_sdk.rtl import api_settings
29
+
30
+
31
+ @pytest.fixture
32
+ def config_file(monkeypatch, tmpdir_factory):
33
+ """Creates a sample looker.ini file and returns its path"""
34
+ monkeypatch.delenv("LOOKERSDK_BASE_URL", raising=False)
35
+ monkeypatch.delenv("LOOKERSDK_VERIFY_SSL", raising=False)
36
+ monkeypatch.delenv("LOOKERSDK_CLIENT_ID", raising=False)
37
+ monkeypatch.delenv("LOOKERSDK_CLIENT_SECRET", raising=False)
38
+ filename = tmpdir_factory.mktemp("settings").join("looker.ini")
39
+ filename.write(
40
+ """
41
+ [Looker]
42
+ # Base URL for API. Do not include /api/* in the url
43
+ base_url=https://host1.looker.com:19999
44
+ # API client id
45
+ client_id=your_API_client_id
46
+ # API client secret
47
+ client_secret=your_API_client_secret
48
+ # Set to false if testing locally against self-signed certs. Otherwise leave True
49
+ verify_ssl=True
50
+
51
+ [OLD_API]
52
+ base_url=https://host2.looker.com:19999
53
+ client_id=your_API_client_id
54
+ client_secret=your_API_client_secret
55
+ verify_ssl=
56
+
57
+ [BARE_MINIMUM]
58
+ base_url=https://host3.looker.com:19999/
59
+
60
+ [BARE]
61
+ # Empty section
62
+
63
+ [BARE_MIN_NO_VALUES]
64
+ base_url=""
65
+
66
+ [QUOTED_CONFIG_VARS]
67
+ base_url="https://host4.looker.com:19999"
68
+ verify_ssl='false'
69
+ """
70
+ )
71
+ return filename
72
+
73
+
74
+ def test_settings_defaults_to_looker_section(config_file):
75
+ """ApiSettings should retrieve settings from default (Looker) section
76
+ if section is not specified during instantiation.
77
+ """
78
+ settings = api_settings.ApiSettings(filename=config_file)
79
+ assert settings.base_url == "https://host1.looker.com:19999"
80
+ # API credentials are not set as attributes in ApiSettings
81
+ data = vars(settings)
82
+ assert "client_id" not in data
83
+ assert "client_secret" not in data
84
+
85
+
86
+ @pytest.mark.parametrize(
87
+ "test_section, expected_url",
88
+ [
89
+ ("Looker", "https://host1.looker.com:19999"),
90
+ ("OLD_API", "https://host2.looker.com:19999"),
91
+ ],
92
+ ids=["section=Looker", "section=OLD_API"],
93
+ )
94
+ def test_it_retrieves_section_by_name(config_file, test_section, expected_url):
95
+ """ApiSettings should return settings of specified section."""
96
+ settings = api_settings.ApiSettings(filename=config_file, section=test_section)
97
+ assert settings.base_url == expected_url
98
+ assert settings.verify_ssl
99
+ data = vars(settings)
100
+ assert "client_id" not in data
101
+ assert "client_secret" not in data
102
+
103
+
104
+ def test_it_assigns_defaults_to_empty_settings(config_file):
105
+ """ApiSettings assigns defaults to optional settings that are empty in the
106
+ config file.
107
+ """
108
+ settings = api_settings.ApiSettings(filename=config_file, section="BARE_MINIMUM")
109
+ assert settings.base_url == "https://host3.looker.com:19999/"
110
+ assert settings.verify_ssl
111
+ data = vars(settings)
112
+ assert "client_id" not in data
113
+ assert "client_secret" not in data
114
+
115
+
116
+ def test_it_fails_with_a_bad_section_name(config_file):
117
+ """ApiSettings should raise NoSectionError section is not found."""
118
+ with pytest.raises(configparser.NoSectionError) as exc_info:
119
+ api_settings.ApiSettings(filename=config_file, section="NotAGoodLookForYou")
120
+ assert exc_info.match("NotAGoodLookForYou")
121
+
122
+
123
+ def test_it_fails_with_a_bad_file_path():
124
+ """ApiSettings should raise an error non-default ini path doesn't exist."""
125
+ api_settings.ApiSettings() # defaulting to _DEFAULT_INIS[0], no error
126
+ api_settings.ApiSettings(
127
+ filename="looker.ini"
128
+ ) # specifying _DEFAULT_INIS[0], no error
129
+ with pytest.raises(FileNotFoundError) as exc_info:
130
+ api_settings.ApiSettings(filename="/no/such/file.ini")
131
+ assert exc_info.match("file.ini")
132
+
133
+
134
+ @pytest.mark.parametrize(
135
+ "test_section",
136
+ [
137
+ pytest.param("BARE", id="Empty config file"),
138
+ pytest.param("BARE_MINIMUM", id="Overriding with env variables"),
139
+ ],
140
+ )
141
+ def test_settings_from_env_variables_override_config_file(
142
+ monkeypatch, config_file, test_section
143
+ ):
144
+ """ApiSettings should read settings defined as env variables."""
145
+ monkeypatch.setenv("LOOKERSDK_BASE_URL", "https://host1.looker.com:19999")
146
+ monkeypatch.setenv("LOOKERSDK_VERIFY_SSL", "0")
147
+ monkeypatch.setenv("LOOKERSDK_CLIENT_ID", "id123")
148
+ monkeypatch.setenv("LOOKERSDK_CLIENT_SECRET", "secret123")
149
+
150
+ settings = api_settings.ApiSettings(
151
+ filename=config_file, section=test_section, env_prefix="LOOKERSDK"
152
+ )
153
+ assert settings.base_url == "https://host1.looker.com:19999"
154
+ assert not settings.verify_ssl
155
+ # API credentials are still not set as attributes when read from env variables
156
+ data = vars(settings)
157
+ assert "client_id" not in data
158
+ assert "client_secret" not in data
159
+
160
+
161
+ @pytest.mark.parametrize(
162
+ "test_value, expected",
163
+ [
164
+ ("yes", True),
165
+ ("y", True),
166
+ ("true", True),
167
+ ("t", True),
168
+ ("1", True),
169
+ ("", True),
170
+ ("no", False),
171
+ ("n", False),
172
+ ("f", False),
173
+ ("0", False),
174
+ ],
175
+ )
176
+ def test_env_verify_ssl_maps_properly(monkeypatch, config_file, test_value, expected):
177
+ """ApiSettings should map the various values that VERIFY_SSL can take to True/False
178
+ accordingly.
179
+ """
180
+ monkeypatch.setenv("LOOKERSDK_VERIFY_SSL", test_value)
181
+ settings = api_settings.ApiSettings(
182
+ filename=config_file, section="BARE_MINIMUM", env_prefix="LOOKERSDK"
183
+ )
184
+ assert settings.verify_ssl == expected
185
+
186
+
187
+ def test_configure_with_no_file(monkeypatch):
188
+ """ApiSettings should be instantiated if required parameters all exist in env
189
+ variables.
190
+ """
191
+ monkeypatch.setenv("LOOKERSDK_BASE_URL", "https://host1.looker.com:19999")
192
+ monkeypatch.setenv("LOOKERSDK_CLIENT_ID", "id123")
193
+ monkeypatch.setenv("LOOKERSDK_CLIENT_SECRET", "secret123")
194
+
195
+ settings = api_settings.ApiSettings(
196
+ filename="", env_prefix="LOOKERSDK",
197
+ ) # explicitly setting config_file to falsey
198
+ assert settings.base_url == "https://host1.looker.com:19999"
199
+ data = vars(settings)
200
+ assert "client_id" not in data
201
+ assert "client_secret" not in data
202
+
203
+
204
+ @pytest.mark.parametrize(
205
+ "test_section",
206
+ [
207
+ pytest.param("BARE", id="Empty config file"),
208
+ pytest.param("BARE_MIN_NO_VALUES", id="Required settings are empty strings"),
209
+ ],
210
+ )
211
+ def test_it_fails_if_required_settings_are_not_found(config_file, test_section):
212
+ """ApiSettings should throw an error if required settings are not found."""
213
+ with pytest.raises(error.SDKError):
214
+ api_settings.ApiSettings(
215
+ filename=config_file, section=test_section
216
+ ).is_configured()
217
+
218
+
219
+ def test_it_fails_when_env_variables_are_defined_but_empty(config_file, monkeypatch):
220
+ """ApiSettings should throw an error if required settings are passed as empty
221
+ env variables.
222
+ """
223
+ monkeypatch.setenv("LOOKERSDK_BASE_URL", "")
224
+
225
+ with pytest.raises(error.SDKError):
226
+ api_settings.ApiSettings(
227
+ filename=config_file, section="BARE", env_prefix="LOOKERSDK"
228
+ ).is_configured()
229
+
230
+
231
+ def test_it_unquotes_quoted_config_file_vars(config_file):
232
+ """ApiSettings should strip quotes from config file variables."""
233
+ settings = api_settings.ApiSettings(
234
+ filename=config_file, section="QUOTED_CONFIG_VARS"
235
+ )
236
+ assert settings.base_url == "https://host4.looker.com:19999"
237
+ assert settings.verify_ssl is False
238
+
239
+
240
+ def test_it_unquotes_quoted_env_var_values(monkeypatch):
241
+ """ApiSettings should strip quotes from env variable values."""
242
+ monkeypatch.setenv("LOOKERSDK_BASE_URL", "'https://host1.looker.com:19999'")
243
+ monkeypatch.setenv("LOOKERSDK_TIMEOUT", "100")
244
+ monkeypatch.setenv("LOOKERSDK_VERIFY_SSL", '"false"')
245
+
246
+ settings = api_settings.ApiSettings(
247
+ env_prefix="LOOKERSDK"
248
+ ) # _DEFAULT_INIS[0] absence doesn't raise
249
+
250
+ assert settings.base_url == "https://host1.looker.com:19999"
251
+ assert settings.verify_ssl is False
252
+ assert settings.timeout == 100