lkr-dev-cli 0.0.47__tar.gz → 0.0.51__tar.gz
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.
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/PKG-INFO +1 -1
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/codemode.md +3 -0
- {lkr_dev_cli-0.0.47/lkr_dev_cli → lkr_dev_cli-0.0.51/lkr}/__init__.py +3 -3
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/auth/main.py +7 -6
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/auth/oauth.py +10 -13
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/auth_service.py +40 -36
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/classes.py +2 -2
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/download_swagger.py +3 -2
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/help.py +11 -10
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/main.py +30 -11
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/readme.py +1 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/type.py +4 -2
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/custom_types.py +2 -2
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/extended_sdk_methods/classes.py +21 -21
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/extended_sdk_methods/main.py +32 -32
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/logger.py +1 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/main.py +3 -3
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/mcp/classes.py +2 -2
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/mcp/main.py +37 -38
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/mcp/utils.py +6 -5
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/observability/classes.py +12 -11
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/observability/main.py +30 -32
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/observability/utils.py +8 -7
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/tools/classes.py +24 -24
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/tools/lookml.py +20 -20
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/tools/main.py +10 -10
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/tools/permission_deprecation.py +14 -12
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr.md +22 -0
- {lkr_dev_cli-0.0.47/lkr → lkr_dev_cli-0.0.51/lkr_dev_cli}/__init__.py +3 -3
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/pyproject.toml +1 -1
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_codemode.py +8 -6
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_dependency_resolution.py +21 -7
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_extended_sdk_methods.py +2 -1
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_init_sdk.py +11 -8
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_oauth_account.py +9 -3
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_permission_deprecation.py +4 -1
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_version.py +1 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/uv.lock +542 -505
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/.github/workflows/ci.yml +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/.github/workflows/release.yml +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/.github/workflows/website.yml +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/.gitignore +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/.python-version +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/.vscode/launch.json +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/.vscode/settings.json +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/Dockerfile +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/LICENSE +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/Makefile +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/README.md +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/cloudbuild.yaml +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/auth/__init__.py +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/LOCAL.md +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/__init__.py +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/constant.py +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/examples.py +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/swagger.json +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/constants.py +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/exceptions.py +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/extended_sdk_methods/__init__.py +7 -7
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/observability/embed_container.html +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/TESTING.md +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_deps.sh +0 -0
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_lookml_tools.py +1 -1
- {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/ty.toml +0 -0
|
@@ -25,6 +25,9 @@ uvx lkr-dev-cli code-mode sandbox --code="return me()"
|
|
|
25
25
|
|
|
26
26
|
# Execute Python script file
|
|
27
27
|
uvx lkr-dev-cli code-mode sandbox --file=./path/to/script.py
|
|
28
|
+
|
|
29
|
+
# Execute Python script file with injected variables (--var / -v)
|
|
30
|
+
uvx lkr-dev-cli code-mode sandbox --file=./path/to/script.py --var project=my_project --var connection=my_connection
|
|
28
31
|
```
|
|
29
32
|
|
|
30
33
|
### 2. Starting the Server
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from lkr.tools.classes import UserAttributeUpdater
|
|
2
|
-
from lkr.extended_sdk_methods import ExtendedLooker40SDK
|
|
3
1
|
from lkr.auth_service import init_sdk
|
|
2
|
+
from lkr.extended_sdk_methods import ExtendedLooker40SDK
|
|
3
|
+
from lkr.tools.classes import UserAttributeUpdater
|
|
4
4
|
|
|
5
|
-
__all__ = ["
|
|
5
|
+
__all__ = ["ExtendedLooker40SDK", "UserAttributeUpdater", "init_sdk"]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import builtins
|
|
1
2
|
import urllib.parse
|
|
2
|
-
from typing import Annotated
|
|
3
|
+
from typing import Annotated
|
|
3
4
|
|
|
4
5
|
import typer
|
|
5
6
|
from looker_sdk.rtl.auth_token import AccessToken, AuthToken
|
|
@@ -82,7 +83,7 @@ def login(
|
|
|
82
83
|
logger.error(f"Instance '{instance_name}' not found")
|
|
83
84
|
raise typer.Exit(1)
|
|
84
85
|
else:
|
|
85
|
-
options:
|
|
86
|
+
options: builtins.list[questionary.Choice] = []
|
|
86
87
|
max_name_length = 0
|
|
87
88
|
for name, url, current, up in all_instances:
|
|
88
89
|
max_name_length = max(max_name_length, len(name))
|
|
@@ -139,7 +140,7 @@ def login(
|
|
|
139
140
|
"Instance name must be set before adding auth."
|
|
140
141
|
)
|
|
141
142
|
|
|
142
|
-
def auth_callback(token:
|
|
143
|
+
def auth_callback(token: AuthToken | AccessToken):
|
|
143
144
|
auth.add_auth(instance_name, origin, token, use_production)
|
|
144
145
|
|
|
145
146
|
oauth = OAuth2PKCE(
|
|
@@ -166,9 +167,9 @@ def login(
|
|
|
166
167
|
else:
|
|
167
168
|
logger.error("Failed to exchange authorization code for tokens")
|
|
168
169
|
raise typer.Exit(1)
|
|
169
|
-
except Exception as e:
|
|
170
|
+
except Exception as e: # noqa: BLE001
|
|
170
171
|
logger.error(
|
|
171
|
-
f"Failed to exchange authorization code for tokens: {
|
|
172
|
+
f"Failed to exchange authorization code for tokens: {e!s}"
|
|
172
173
|
)
|
|
173
174
|
raise typer.Exit(1)
|
|
174
175
|
else:
|
|
@@ -249,7 +250,7 @@ def whoami(ctx: typer.Context):
|
|
|
249
250
|
if "invalid_grant" in str(e) or "token expired" in str(e):
|
|
250
251
|
logger.error("Your Looker OAuth session has expired. Please run 'lkr auth login' to re-authenticate.")
|
|
251
252
|
raise typer.Exit(1)
|
|
252
|
-
raise
|
|
253
|
+
raise
|
|
253
254
|
|
|
254
255
|
|
|
255
256
|
@group.command()
|
|
@@ -8,7 +8,7 @@ import threading
|
|
|
8
8
|
import time
|
|
9
9
|
import urllib.parse
|
|
10
10
|
import webbrowser
|
|
11
|
-
from typing import
|
|
11
|
+
from typing import TypedDict, cast
|
|
12
12
|
from urllib.parse import parse_qs
|
|
13
13
|
|
|
14
14
|
from lkr.custom_types import NewTokenCallback
|
|
@@ -24,19 +24,21 @@ def kill_process_on_port(port: int, retries: int = 5, delay: float = 1) -> None:
|
|
|
24
24
|
sock.bind(("localhost", port))
|
|
25
25
|
sock.close()
|
|
26
26
|
return # Port is free, no need to kill anything
|
|
27
|
-
except
|
|
27
|
+
except OSError:
|
|
28
28
|
# Port is in use, try to kill the process
|
|
29
29
|
if os.name == "posix": # macOS/Linux
|
|
30
30
|
subprocess.run(
|
|
31
31
|
f"lsof -ti tcp:{port} | xargs kill -9",
|
|
32
32
|
shell=True,
|
|
33
33
|
capture_output=True,
|
|
34
|
+
check=False,
|
|
34
35
|
)
|
|
35
36
|
elif os.name == "nt": # Windows
|
|
36
37
|
subprocess.run(
|
|
37
38
|
f'for /f "tokens=5" %a in (\'netstat -aon ^| find ":{port}"\') do taskkill /F /PID %a',
|
|
38
39
|
shell=True,
|
|
39
40
|
capture_output=True,
|
|
41
|
+
check=False,
|
|
40
42
|
)
|
|
41
43
|
# After killing, wait for the port to be free
|
|
42
44
|
for _ in range(retries):
|
|
@@ -46,7 +48,7 @@ def kill_process_on_port(port: int, retries: int = 5, delay: float = 1) -> None:
|
|
|
46
48
|
sock.bind(("localhost", port))
|
|
47
49
|
sock.close()
|
|
48
50
|
return
|
|
49
|
-
except
|
|
51
|
+
except OSError:
|
|
50
52
|
time.sleep(delay)
|
|
51
53
|
raise RuntimeError(f"Port {port} is still in use after killing process.")
|
|
52
54
|
|
|
@@ -91,9 +93,7 @@ class OAuthCallbackHandler(http.server.BaseHTTPRequestHandler):
|
|
|
91
93
|
f"Looker OAuth returned error: {error} - {error_description}"
|
|
92
94
|
)
|
|
93
95
|
self.wfile.write(
|
|
94
|
-
f"OAuth Authentication Failed: {error} - {error_description}. You can close this window.".encode(
|
|
95
|
-
"utf-8"
|
|
96
|
-
)
|
|
96
|
+
f"OAuth Authentication Failed: {error} - {error_description}. You can close this window.".encode()
|
|
97
97
|
)
|
|
98
98
|
threading.Thread(target=server.shutdown).start()
|
|
99
99
|
else:
|
|
@@ -107,7 +107,6 @@ class OAuthCallbackHandler(http.server.BaseHTTPRequestHandler):
|
|
|
107
107
|
|
|
108
108
|
def log_message(self, format, *args):
|
|
109
109
|
"""Suppress logging of requests"""
|
|
110
|
-
pass
|
|
111
110
|
|
|
112
111
|
|
|
113
112
|
class OAuthCallbackServer(socketserver.TCPServer):
|
|
@@ -119,8 +118,8 @@ class OAuthCallbackServer(socketserver.TCPServer):
|
|
|
119
118
|
|
|
120
119
|
|
|
121
120
|
class LoginResponse(TypedDict):
|
|
122
|
-
auth_code:
|
|
123
|
-
code_verifier:
|
|
121
|
+
auth_code: str | None
|
|
122
|
+
code_verifier: str | None
|
|
124
123
|
|
|
125
124
|
|
|
126
125
|
class OAuth2PKCE:
|
|
@@ -132,7 +131,7 @@ class OAuth2PKCE:
|
|
|
132
131
|
):
|
|
133
132
|
from lkr.auth_service import DbOAuthSession
|
|
134
133
|
|
|
135
|
-
self.auth_code:
|
|
134
|
+
self.auth_code: str | None = None
|
|
136
135
|
self.state = secrets.token_urlsafe(16)
|
|
137
136
|
self.new_token_callback: NewTokenCallback = new_token_callback
|
|
138
137
|
self.auth_session: DbOAuthSession | None = None
|
|
@@ -181,9 +180,7 @@ class OAuth2PKCE:
|
|
|
181
180
|
else:
|
|
182
181
|
raise
|
|
183
182
|
if not server_created:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
logging.error(
|
|
183
|
+
logger.error(
|
|
187
184
|
f"Failed to bind to port {self.port} after waiting: {last_exception}"
|
|
188
185
|
)
|
|
189
186
|
raise RuntimeError(
|
|
@@ -2,8 +2,8 @@ import json
|
|
|
2
2
|
import os
|
|
3
3
|
import sqlite3
|
|
4
4
|
import types
|
|
5
|
-
from datetime import datetime, timedelta
|
|
6
|
-
from typing import
|
|
5
|
+
from datetime import UTC, datetime, timedelta
|
|
6
|
+
from typing import Self, Union
|
|
7
7
|
|
|
8
8
|
import requests
|
|
9
9
|
import typer
|
|
@@ -13,21 +13,21 @@ from looker_sdk.rtl.auth_session import AuthSession, CryptoHash, OAuthSession
|
|
|
13
13
|
from looker_sdk.rtl.auth_token import AccessToken, AuthToken
|
|
14
14
|
from looker_sdk.rtl.requests_transport import RequestsTransport
|
|
15
15
|
from looker_sdk.rtl.transport import LOOKER_API_ID, HttpMethod
|
|
16
|
-
from lkr.extended_sdk_methods import ExtendedLooker40SDK as Looker40SDK
|
|
17
16
|
from pydantic import BaseModel, Field, computed_field
|
|
18
17
|
from pydash import get
|
|
19
18
|
|
|
20
19
|
from lkr.classes import LkrCtxObj, LookerApiKey
|
|
21
20
|
from lkr.constants import LOOKER_API_VERSION, OAUTH_CLIENT_ID
|
|
22
21
|
from lkr.custom_types import NewTokenCallback
|
|
22
|
+
from lkr.extended_sdk_methods import ExtendedLooker40SDK as Looker40SDK
|
|
23
23
|
from lkr.logger import logger
|
|
24
24
|
|
|
25
25
|
__all__ = [
|
|
26
|
-
"get_auth",
|
|
27
26
|
"ApiKeyAuthSession",
|
|
28
27
|
"DbOAuthSession",
|
|
29
|
-
"
|
|
28
|
+
"get_auth",
|
|
30
29
|
"init_sdk",
|
|
30
|
+
"is_auth_expired",
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
|
|
@@ -138,7 +138,7 @@ class DbOAuthSession(OAuthSession):
|
|
|
138
138
|
if not self.use_production:
|
|
139
139
|
try:
|
|
140
140
|
self._switch_to_dev_mode()
|
|
141
|
-
except Exception as e:
|
|
141
|
+
except Exception as e: # noqa: BLE001
|
|
142
142
|
logger.error(f"Failed to switch to development mode: {e}")
|
|
143
143
|
raise typer.Exit(1)
|
|
144
144
|
self.new_token_callback(self.token)
|
|
@@ -223,9 +223,7 @@ def monkey_patch_prepare_request(session: requests.Session):
|
|
|
223
223
|
x.headers.pop(LOOKER_API_ID)
|
|
224
224
|
return x
|
|
225
225
|
|
|
226
|
-
|
|
227
|
-
session, "prepare_request", types.MethodType(prepare_request, session)
|
|
228
|
-
)
|
|
226
|
+
session.prepare_request = types.MethodType(prepare_request, session) # type: ignore
|
|
229
227
|
|
|
230
228
|
|
|
231
229
|
class MonkeyPatchTransport(RequestsTransport):
|
|
@@ -269,7 +267,7 @@ class CurrentAuth(BaseModel):
|
|
|
269
267
|
refresh_token: str
|
|
270
268
|
refresh_expires_at: str = Field(
|
|
271
269
|
default_factory=lambda: (
|
|
272
|
-
datetime.now(
|
|
270
|
+
datetime.now(UTC) + timedelta(days=30)
|
|
273
271
|
).isoformat()
|
|
274
272
|
)
|
|
275
273
|
token_type: str
|
|
@@ -283,23 +281,23 @@ class CurrentAuth(BaseModel):
|
|
|
283
281
|
if not self.refresh_expires_at:
|
|
284
282
|
return False
|
|
285
283
|
return datetime.fromisoformat(self.refresh_expires_at).replace(
|
|
286
|
-
tzinfo=
|
|
287
|
-
) > (datetime.now(
|
|
284
|
+
tzinfo=UTC
|
|
285
|
+
) > (datetime.now(UTC) + timedelta(hours=24))
|
|
288
286
|
|
|
289
287
|
@computed_field
|
|
290
288
|
@property
|
|
291
289
|
def expires_at(self) -> str:
|
|
292
290
|
return (
|
|
293
|
-
datetime.now(
|
|
291
|
+
datetime.now(UTC) + timedelta(seconds=self.expires_in)
|
|
294
292
|
).isoformat()
|
|
295
293
|
|
|
296
|
-
def __add__(self, other:
|
|
294
|
+
def __add__(self, other: AccessToken | AuthToken) -> Self:
|
|
297
295
|
self.access_token = other.access_token or ""
|
|
298
296
|
self.refresh_token = other.refresh_token or ""
|
|
299
297
|
self.token_type = other.token_type or ""
|
|
300
298
|
self.expires_in = other.expires_in or 0
|
|
301
299
|
self.refresh_expires_at = (
|
|
302
|
-
datetime.now(
|
|
300
|
+
datetime.now(UTC) + timedelta(days=30)
|
|
303
301
|
).isoformat()
|
|
304
302
|
return self
|
|
305
303
|
|
|
@@ -307,7 +305,7 @@ class CurrentAuth(BaseModel):
|
|
|
307
305
|
def from_access_token(
|
|
308
306
|
cls,
|
|
309
307
|
*,
|
|
310
|
-
access_token:
|
|
308
|
+
access_token: AccessToken | AuthToken,
|
|
311
309
|
instance_name: str,
|
|
312
310
|
base_url: str,
|
|
313
311
|
use_production: bool,
|
|
@@ -327,8 +325,8 @@ class CurrentAuth(BaseModel):
|
|
|
327
325
|
def from_db_row(cls, row: sqlite3.Row) -> "CurrentAuth":
|
|
328
326
|
expires_at_dt = datetime.fromisoformat(row["expires_at"])
|
|
329
327
|
if expires_at_dt.tzinfo is None:
|
|
330
|
-
expires_at_dt = expires_at_dt.replace(tzinfo=
|
|
331
|
-
now_utc = datetime.now(
|
|
328
|
+
expires_at_dt = expires_at_dt.replace(tzinfo=UTC)
|
|
329
|
+
now_utc = datetime.now(UTC)
|
|
332
330
|
expires_in = int((expires_at_dt - now_utc).total_seconds())
|
|
333
331
|
return cls(
|
|
334
332
|
instance_name=row["instance_name"],
|
|
@@ -364,22 +362,29 @@ class CurrentAuth(BaseModel):
|
|
|
364
362
|
self,
|
|
365
363
|
connection: sqlite3.Connection,
|
|
366
364
|
*,
|
|
367
|
-
new_token:
|
|
365
|
+
new_token: AccessToken | AuthToken | None = None,
|
|
368
366
|
commit: bool = True,
|
|
369
367
|
):
|
|
370
368
|
expires_at = (
|
|
371
|
-
datetime.now(
|
|
369
|
+
datetime.now(UTC)
|
|
372
370
|
+ timedelta(seconds=(new_token.expires_in or 0) if new_token else 0)
|
|
373
371
|
).isoformat()
|
|
374
372
|
refresh_expires_at = (
|
|
375
373
|
datetime.fromisoformat(expires_at) + timedelta(days=30)
|
|
376
374
|
).isoformat()
|
|
377
375
|
if self.from_db and new_token:
|
|
376
|
+
if new_token.refresh_token:
|
|
377
|
+
self.refresh_token = new_token.refresh_token
|
|
378
|
+
self.refresh_expires_at = refresh_expires_at
|
|
379
|
+
self.access_token = new_token.access_token or self.access_token
|
|
380
|
+
self.token_type = new_token.token_type or self.token_type
|
|
378
381
|
connection.execute(
|
|
379
|
-
"UPDATE auth SET access_token = ?, token_type = ?, expires_at = ? WHERE instance_name = ?",
|
|
382
|
+
"UPDATE auth SET access_token = ?, refresh_token = ?, refresh_expires_at = ?, token_type = ?, expires_at = ? WHERE instance_name = ?",
|
|
380
383
|
(
|
|
381
|
-
|
|
382
|
-
|
|
384
|
+
self.access_token,
|
|
385
|
+
self.refresh_token,
|
|
386
|
+
self.refresh_expires_at,
|
|
387
|
+
self.token_type,
|
|
383
388
|
expires_at,
|
|
384
389
|
self.instance_name,
|
|
385
390
|
),
|
|
@@ -443,7 +448,7 @@ class SqlLiteAuth:
|
|
|
443
448
|
self,
|
|
444
449
|
instance_name: str,
|
|
445
450
|
base_url: str,
|
|
446
|
-
access_token:
|
|
451
|
+
access_token: AuthToken | AccessToken,
|
|
447
452
|
use_production: bool,
|
|
448
453
|
):
|
|
449
454
|
self.conn.execute("UPDATE auth SET current_instance = 0")
|
|
@@ -498,7 +503,7 @@ class SqlLiteAuth:
|
|
|
498
503
|
else:
|
|
499
504
|
raise InvalidRefreshTokenError(current_auth.instance_name)
|
|
500
505
|
|
|
501
|
-
def refresh_current_token(token:
|
|
506
|
+
def refresh_current_token(token: AccessToken | AuthToken):
|
|
502
507
|
current_auth.set_token(self.conn, new_token=token, commit=True)
|
|
503
508
|
|
|
504
509
|
sdk = init_oauth_sdk(
|
|
@@ -513,13 +518,12 @@ class SqlLiteAuth:
|
|
|
513
518
|
try:
|
|
514
519
|
sdk.auth.authenticate({})
|
|
515
520
|
except Exception as e:
|
|
516
|
-
if is_auth_expired(e):
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
raise e
|
|
521
|
+
if is_auth_expired(e) and sys.stdin.isatty():
|
|
522
|
+
self._cli_confirm_refresh_token(current_auth, quiet=False)
|
|
523
|
+
return self.get_current_sdk(
|
|
524
|
+
prompt_refresh_invalid_token=False
|
|
525
|
+
)
|
|
526
|
+
raise
|
|
523
527
|
|
|
524
528
|
return sdk
|
|
525
529
|
|
|
@@ -531,7 +535,7 @@ class SqlLiteAuth:
|
|
|
531
535
|
self.conn.execute("DELETE FROM auth WHERE instance_name = ?", (instance_name,))
|
|
532
536
|
self.conn.commit()
|
|
533
537
|
|
|
534
|
-
def list_auth(self) ->
|
|
538
|
+
def list_auth(self) -> list[tuple[str, str, bool, bool]]:
|
|
535
539
|
cursor = self.conn.execute(
|
|
536
540
|
"SELECT instance_name, base_url, current_instance, use_production FROM auth ORDER BY instance_name ASC"
|
|
537
541
|
)
|
|
@@ -562,7 +566,7 @@ class SqlLiteAuth:
|
|
|
562
566
|
)
|
|
563
567
|
if confirmed:
|
|
564
568
|
|
|
565
|
-
def add_auth(token:
|
|
569
|
+
def add_auth(token: AccessToken | AuthToken):
|
|
566
570
|
current_auth + token
|
|
567
571
|
current_auth.update_refresh_expires_at(self.conn, commit=False)
|
|
568
572
|
current_auth.set_token(self.conn, commit=True, new_token=token)
|
|
@@ -600,7 +604,7 @@ class ApiKeyAuth:
|
|
|
600
604
|
self,
|
|
601
605
|
instance_name: str,
|
|
602
606
|
base_url: str,
|
|
603
|
-
access_token:
|
|
607
|
+
access_token: AuthToken | AccessToken,
|
|
604
608
|
use_production: bool,
|
|
605
609
|
):
|
|
606
610
|
raise NotImplementedError("ApiKeyAuth does not support adding auth")
|
|
@@ -608,7 +612,7 @@ class ApiKeyAuth:
|
|
|
608
612
|
def delete_auth(self, instance_name: str):
|
|
609
613
|
raise NotImplementedError("ApiKeyAuth does not support deleting auth")
|
|
610
614
|
|
|
611
|
-
def list_auth(self) ->
|
|
615
|
+
def list_auth(self) -> list[tuple[str, str, bool, bool]]:
|
|
612
616
|
raise NotImplementedError("ApiKeyAuth does not support listing auth")
|
|
613
617
|
|
|
614
618
|
def set_current_instance(self, instance_name: str):
|
|
@@ -21,7 +21,7 @@ class LookerApiKey(BaseModel):
|
|
|
21
21
|
base_url=os.environ.get("LOOKERSDK_BASE_URL"), # type: ignore
|
|
22
22
|
verify_ssl=verify_ssl,
|
|
23
23
|
)
|
|
24
|
-
except Exception:
|
|
24
|
+
except Exception: # noqa: BLE001
|
|
25
25
|
return None
|
|
26
26
|
|
|
27
27
|
|
|
@@ -38,6 +38,6 @@ class LkrCtxObj(BaseModel):
|
|
|
38
38
|
return "api_key" if self.api_key else "oauth"
|
|
39
39
|
|
|
40
40
|
def __init__(self, api_key: LookerApiKey | None = None, *args, **kwargs):
|
|
41
|
-
super().__init__(api_key=api_key,
|
|
41
|
+
super().__init__(*args, api_key=api_key, **kwargs)
|
|
42
42
|
if not self.api_key:
|
|
43
43
|
self.api_key = LookerApiKey.from_env()
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import re
|
|
2
1
|
import inspect
|
|
3
2
|
import json
|
|
4
3
|
import os
|
|
4
|
+
import re
|
|
5
|
+
|
|
5
6
|
from lkr.codemode.constant import EXCLUDED_FUNCS
|
|
6
7
|
|
|
7
8
|
_operation_map = None
|
|
@@ -22,8 +23,8 @@ def _get_operation_map():
|
|
|
22
23
|
with open(swagger_path, 'r') as f:
|
|
23
24
|
swagger = json.load(f)
|
|
24
25
|
paths = swagger.get('paths', {})
|
|
25
|
-
for
|
|
26
|
-
for
|
|
26
|
+
for path_item in paths.values():
|
|
27
|
+
for op in path_item.values():
|
|
27
28
|
if isinstance(op, dict) and 'operationId' in op:
|
|
28
29
|
op_id = op['operationId']
|
|
29
30
|
summary = op.get('summary', '')
|
|
@@ -95,7 +96,7 @@ def _get_matches(query: str, external_funcs: dict, sdk) -> list:
|
|
|
95
96
|
params = list(sig.parameters.keys())
|
|
96
97
|
if any(pattern.search(p) for p in params):
|
|
97
98
|
hit_in_params = True
|
|
98
|
-
except Exception:
|
|
99
|
+
except Exception: # noqa: BLE001, S110
|
|
99
100
|
pass
|
|
100
101
|
|
|
101
102
|
# Search in return type fields (Outputs)
|
|
@@ -111,7 +112,7 @@ def _get_matches(query: str, external_funcs: dict, sdk) -> list:
|
|
|
111
112
|
if matching_fields:
|
|
112
113
|
hit_in_output = True
|
|
113
114
|
output_fields = matching_fields
|
|
114
|
-
except Exception:
|
|
115
|
+
except Exception: # noqa: BLE001, S110
|
|
115
116
|
pass
|
|
116
117
|
|
|
117
118
|
if hit_in_name or matching_lines or hit_in_params or hit_in_output:
|
|
@@ -133,11 +134,11 @@ def _get_type_matches(query: str) -> list:
|
|
|
133
134
|
pattern = re.compile(re.escape(query), re.IGNORECASE)
|
|
134
135
|
|
|
135
136
|
try:
|
|
136
|
-
from lkr.codemode.type import
|
|
137
|
+
from lkr.codemode.type import _get_ext_definitions, _get_swagger_data
|
|
137
138
|
swagger = _get_swagger_data()
|
|
138
139
|
definitions = dict(swagger.get('definitions', {}))
|
|
139
140
|
definitions.update(_get_ext_definitions())
|
|
140
|
-
except Exception:
|
|
141
|
+
except Exception: # noqa: BLE001
|
|
141
142
|
return []
|
|
142
143
|
|
|
143
144
|
matches = []
|
|
@@ -217,7 +218,7 @@ def search_with_lookups(query: str, external_funcs: dict, sdk) -> list:
|
|
|
217
218
|
try:
|
|
218
219
|
from lkr.codemode.type import lookup_type
|
|
219
220
|
results.append(lookup_type(m['name']))
|
|
220
|
-
except Exception:
|
|
221
|
+
except Exception: # noqa: BLE001, S110
|
|
221
222
|
pass
|
|
222
223
|
return results
|
|
223
224
|
|
|
@@ -230,7 +231,7 @@ def lookup_function(name: str, external_funcs: dict, sdk) -> str:
|
|
|
230
231
|
res = lookup_type(name)
|
|
231
232
|
if not res.startswith("Type '"):
|
|
232
233
|
return res
|
|
233
|
-
except Exception:
|
|
234
|
+
except Exception: # noqa: BLE001, S110
|
|
234
235
|
pass
|
|
235
236
|
return f"Function or type '{name}' not found."
|
|
236
237
|
|
|
@@ -279,5 +280,5 @@ Inputs:
|
|
|
279
280
|
Outputs:
|
|
280
281
|
{outputs_str}
|
|
281
282
|
"""
|
|
282
|
-
except Exception as e:
|
|
283
|
+
except Exception as e: # noqa: BLE001
|
|
283
284
|
return f"Function: {name}\n\nDocstring:\n{doc}\n\n(Could not inspect signature: {e})"
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import ast
|
|
2
2
|
import inspect
|
|
3
3
|
import json
|
|
4
|
+
import keyword
|
|
4
5
|
import os
|
|
5
6
|
import re
|
|
6
7
|
import sys
|
|
7
8
|
import tempfile
|
|
8
9
|
from contextlib import contextmanager
|
|
9
10
|
|
|
10
|
-
import typer
|
|
11
11
|
import pydantic_monty
|
|
12
|
+
import typer
|
|
12
13
|
from mcp.server.fastmcp import FastMCP
|
|
13
14
|
|
|
14
15
|
from lkr.auth_service import get_auth, is_auth_expired
|
|
15
16
|
from lkr.classes import LkrCtxObj
|
|
17
|
+
from lkr.codemode.constant import EXCLUDED_FUNCS
|
|
16
18
|
from lkr.codemode.examples import EXAMPLES
|
|
17
|
-
from lkr.codemode.help import
|
|
19
|
+
from lkr.codemode.help import lookup_function, search_help, search_with_lookups
|
|
18
20
|
from lkr.codemode.readme import get_readme
|
|
19
|
-
from lkr.codemode.constant import EXCLUDED_FUNCS
|
|
20
21
|
from lkr.codemode.type import lookup_type
|
|
21
22
|
from lkr.logger import logger
|
|
22
23
|
|
|
@@ -87,7 +88,7 @@ def to_primitive(obj):
|
|
|
87
88
|
return _to_primitive(vars(o))
|
|
88
89
|
except TypeError:
|
|
89
90
|
return str(o)
|
|
90
|
-
except Exception:
|
|
91
|
+
except Exception: # noqa: BLE001
|
|
91
92
|
return str(o)
|
|
92
93
|
finally:
|
|
93
94
|
seen.remove(obj_id)
|
|
@@ -113,7 +114,6 @@ def run_python_code(code: str, dev_mode: bool = False) -> str:
|
|
|
113
114
|
- Dev Mode: Set `dev_mode=True` to ensure you are in development mode before running code.
|
|
114
115
|
"""
|
|
115
116
|
try:
|
|
116
|
-
global ctx_lkr
|
|
117
117
|
ctx = (
|
|
118
118
|
LkrCtxObj(
|
|
119
119
|
force_oauth=ctx_lkr.force_oauth,
|
|
@@ -172,7 +172,7 @@ def run_python_code(code: str, dev_mode: bool = False) -> str:
|
|
|
172
172
|
tree = SDKAttributeRewriter().visit(tree)
|
|
173
173
|
ast.fix_missing_locations(tree)
|
|
174
174
|
code = ast.unparse(tree)
|
|
175
|
-
except Exception:
|
|
175
|
+
except Exception: # noqa: BLE001
|
|
176
176
|
# Fallback to regex if parsing fails
|
|
177
177
|
code = re.sub(r"\bsdk\.([a-zA-Z_][a-zA-Z0-9_]*)\b", r"\1", code)
|
|
178
178
|
|
|
@@ -195,7 +195,7 @@ def run_python_code(code: str, dev_mode: bool = False) -> str:
|
|
|
195
195
|
output = json.dumps(result, indent=2, default=str)
|
|
196
196
|
else:
|
|
197
197
|
output = ""
|
|
198
|
-
except Exception:
|
|
198
|
+
except Exception: # noqa: BLE001
|
|
199
199
|
output = repr(result)
|
|
200
200
|
|
|
201
201
|
if printed_output:
|
|
@@ -204,11 +204,11 @@ def run_python_code(code: str, dev_mode: bool = False) -> str:
|
|
|
204
204
|
"result": result
|
|
205
205
|
}, indent=2, default=str)
|
|
206
206
|
return output
|
|
207
|
-
except Exception as e:
|
|
207
|
+
except Exception as e: # noqa: BLE001
|
|
208
208
|
logger.error(f"Error executing Monty: {e}")
|
|
209
209
|
if is_auth_expired(e):
|
|
210
210
|
return "Error: Your Looker OAuth session has expired. Please run 'lkr auth login' to re-authenticate."
|
|
211
|
-
return f"Error: {
|
|
211
|
+
return f"Error: {e!s}"
|
|
212
212
|
|
|
213
213
|
|
|
214
214
|
@group.command(name="sandbox")
|
|
@@ -223,6 +223,9 @@ def sandbox(
|
|
|
223
223
|
dev_mode: bool = typer.Option(
|
|
224
224
|
False, "--dev-mode", help="Run in dev mode"
|
|
225
225
|
),
|
|
226
|
+
var: list[str] | None = typer.Option( # noqa: B008
|
|
227
|
+
None, "--var", "-v", help="Inject variable as key=value pair (e.g. -v project=my_project)"
|
|
228
|
+
),
|
|
226
229
|
):
|
|
227
230
|
if not code and not file:
|
|
228
231
|
logger.error("Must specify either --code or --file")
|
|
@@ -236,11 +239,27 @@ def sandbox(
|
|
|
236
239
|
try:
|
|
237
240
|
with open(file, "r", encoding="utf-8") as f:
|
|
238
241
|
code_to_run = f.read()
|
|
239
|
-
except Exception as e:
|
|
242
|
+
except Exception as e: # noqa: BLE001
|
|
240
243
|
logger.error(f"Failed to read file {file}: {e}")
|
|
241
244
|
raise typer.Exit(1)
|
|
242
|
-
|
|
245
|
+
elif code:
|
|
243
246
|
code_to_run = code
|
|
247
|
+
else:
|
|
248
|
+
raise typer.Exit(1)
|
|
249
|
+
|
|
250
|
+
if var:
|
|
251
|
+
var_defs = []
|
|
252
|
+
for v_str in var:
|
|
253
|
+
if "=" not in v_str:
|
|
254
|
+
logger.error(f"Invalid variable format: '{v_str}'. Must be key=value.")
|
|
255
|
+
raise typer.Exit(1)
|
|
256
|
+
k, v = v_str.split("=", 1)
|
|
257
|
+
k = k.strip()
|
|
258
|
+
if not k.isidentifier() or keyword.iskeyword(k):
|
|
259
|
+
logger.error(f"Invalid variable name: '{k}'. Must be a valid Python identifier and not a keyword.")
|
|
260
|
+
raise typer.Exit(1)
|
|
261
|
+
var_defs.append(f"{k} = {json.dumps(v)}")
|
|
262
|
+
code_to_run = "\n".join(var_defs) + "\n" + code_to_run
|
|
244
263
|
|
|
245
264
|
global ctx_lkr
|
|
246
265
|
ctx_lkr = (
|
|
@@ -13,6 +13,7 @@ RULES & HINTS:
|
|
|
13
13
|
6. **Efficiency**: Always use the `fields` parameter (e.g., `all_dashboards(fields="id,title")`) when listing many objects to prevent timeouts.
|
|
14
14
|
7. **Nested Folders**: Use `folder_children(id)` to get sub-folders.
|
|
15
15
|
8. **Search with Lookups**: If `help('pattern')` returns too many results and you want to see full details for all of them at once, use `search_with_lookups('pattern')`.
|
|
16
|
+
9. **Injected CLI Variables**: When using `sandbox` with `--var key=value` flags, those variables are directly available in your code as top-level string constants.
|
|
16
17
|
|
|
17
18
|
EXAMPLES:
|
|
18
19
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
|
+
|
|
4
5
|
from pydantic import BaseModel
|
|
6
|
+
|
|
5
7
|
import lkr.extended_sdk_methods.classes as ext_classes
|
|
6
8
|
|
|
7
9
|
_swagger_data = None
|
|
@@ -23,7 +25,7 @@ def _get_swagger_data():
|
|
|
23
25
|
try:
|
|
24
26
|
with open(swagger_path, 'r') as f:
|
|
25
27
|
_swagger_data = json.load(f)
|
|
26
|
-
except Exception:
|
|
28
|
+
except Exception: # noqa: BLE001
|
|
27
29
|
_swagger_data = {}
|
|
28
30
|
|
|
29
31
|
return _swagger_data
|
|
@@ -63,7 +65,7 @@ def _get_ext_definitions() -> dict:
|
|
|
63
65
|
prop_type = t.__name__
|
|
64
66
|
elif t is bool:
|
|
65
67
|
prop_type = "boolean"
|
|
66
|
-
elif hasattr(t, '__origin__') and
|
|
68
|
+
elif hasattr(t, '__origin__') and t.__origin__ in (list, set):
|
|
67
69
|
is_array = True
|
|
68
70
|
sub_args = getattr(t, '__args__', ())
|
|
69
71
|
for sub_t in sub_args:
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
1
2
|
from enum import Enum
|
|
2
|
-
from typing import Callable, Union
|
|
3
3
|
|
|
4
4
|
from looker_sdk.rtl.auth_token import AccessToken, AuthToken
|
|
5
5
|
|
|
6
|
-
NewTokenCallback = Callable[[
|
|
6
|
+
NewTokenCallback = Callable[[AuthToken | AccessToken], None]
|
|
7
7
|
|
|
8
8
|
class LogLevel(str, Enum):
|
|
9
9
|
DEBUG = "DEBUG"
|