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.
Files changed (64) hide show
  1. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/PKG-INFO +1 -1
  2. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/codemode.md +3 -0
  3. {lkr_dev_cli-0.0.47/lkr_dev_cli → lkr_dev_cli-0.0.51/lkr}/__init__.py +3 -3
  4. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/auth/main.py +7 -6
  5. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/auth/oauth.py +10 -13
  6. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/auth_service.py +40 -36
  7. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/classes.py +2 -2
  8. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/download_swagger.py +3 -2
  9. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/help.py +11 -10
  10. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/main.py +30 -11
  11. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/readme.py +1 -0
  12. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/type.py +4 -2
  13. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/custom_types.py +2 -2
  14. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/extended_sdk_methods/classes.py +21 -21
  15. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/extended_sdk_methods/main.py +32 -32
  16. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/logger.py +1 -0
  17. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/main.py +3 -3
  18. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/mcp/classes.py +2 -2
  19. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/mcp/main.py +37 -38
  20. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/mcp/utils.py +6 -5
  21. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/observability/classes.py +12 -11
  22. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/observability/main.py +30 -32
  23. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/observability/utils.py +8 -7
  24. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/tools/classes.py +24 -24
  25. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/tools/lookml.py +20 -20
  26. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/tools/main.py +10 -10
  27. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/tools/permission_deprecation.py +14 -12
  28. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr.md +22 -0
  29. {lkr_dev_cli-0.0.47/lkr → lkr_dev_cli-0.0.51/lkr_dev_cli}/__init__.py +3 -3
  30. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/pyproject.toml +1 -1
  31. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_codemode.py +8 -6
  32. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_dependency_resolution.py +21 -7
  33. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_extended_sdk_methods.py +2 -1
  34. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_init_sdk.py +11 -8
  35. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_oauth_account.py +9 -3
  36. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_permission_deprecation.py +4 -1
  37. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_version.py +1 -0
  38. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/uv.lock +542 -505
  39. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/.github/workflows/ci.yml +0 -0
  40. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/.github/workflows/release.yml +0 -0
  41. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/.github/workflows/website.yml +0 -0
  42. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/.gitignore +0 -0
  43. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/.python-version +0 -0
  44. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/.vscode/launch.json +0 -0
  45. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/.vscode/settings.json +0 -0
  46. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/Dockerfile +0 -0
  47. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/LICENSE +0 -0
  48. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/Makefile +0 -0
  49. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/README.md +0 -0
  50. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/cloudbuild.yaml +0 -0
  51. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/auth/__init__.py +0 -0
  52. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/LOCAL.md +0 -0
  53. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/__init__.py +0 -0
  54. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/constant.py +0 -0
  55. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/examples.py +0 -0
  56. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/codemode/swagger.json +0 -0
  57. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/constants.py +0 -0
  58. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/exceptions.py +0 -0
  59. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/extended_sdk_methods/__init__.py +7 -7
  60. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/lkr/observability/embed_container.html +0 -0
  61. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/TESTING.md +0 -0
  62. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_deps.sh +0 -0
  63. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/tests/test_lookml_tools.py +1 -1
  64. {lkr_dev_cli-0.0.47 → lkr_dev_cli-0.0.51}/ty.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lkr-dev-cli
3
- Version: 0.0.47
3
+ Version: 0.0.51
4
4
  Summary: lkr: a command line interface for looker
5
5
  Author: bwebs
6
6
  License-Expression: MIT
@@ -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__ = ["UserAttributeUpdater", "ExtendedLooker40SDK", "init_sdk"]
5
+ __all__ = ["ExtendedLooker40SDK", "UserAttributeUpdater", "init_sdk"]
@@ -1,5 +1,6 @@
1
+ import builtins
1
2
  import urllib.parse
2
- from typing import Annotated, List, Union
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: List[questionary.Choice] = []
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: Union[AuthToken, AccessToken]):
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: {str(e)}"
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 e
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 Optional, TypedDict, cast
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 socket.error:
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 socket.error:
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: Optional[str]
123
- code_verifier: Optional[str]
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: Optional[str] = None
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
- import logging
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, timezone
6
- from typing import List, Self, Tuple, Union
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
- "is_auth_expired",
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
- setattr(
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(timezone.utc) + timedelta(days=30)
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=timezone.utc
287
- ) > (datetime.now(timezone.utc) + timedelta(hours=24))
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(timezone.utc) + timedelta(seconds=self.expires_in)
291
+ datetime.now(UTC) + timedelta(seconds=self.expires_in)
294
292
  ).isoformat()
295
293
 
296
- def __add__(self, other: Union[AccessToken, AuthToken]) -> Self:
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(timezone.utc) + timedelta(days=30)
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: Union[AccessToken, AuthToken],
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=timezone.utc)
331
- now_utc = datetime.now(timezone.utc)
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: Union[AccessToken, AuthToken] | None = None,
365
+ new_token: AccessToken | AuthToken | None = None,
368
366
  commit: bool = True,
369
367
  ):
370
368
  expires_at = (
371
- datetime.now(timezone.utc)
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
- new_token.access_token,
382
- new_token.token_type,
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: Union[AuthToken, AccessToken],
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: Union[AccessToken, AuthToken]):
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
- if sys.stdin.isatty():
518
- self._cli_confirm_refresh_token(current_auth, quiet=False)
519
- return self.get_current_sdk(
520
- prompt_refresh_invalid_token=False
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) -> List[Tuple[str, str, bool, bool]]:
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: Union[AccessToken, AuthToken]):
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: Union[AuthToken, AccessToken],
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) -> List[Tuple[str, str, bool, bool]]:
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, *args, **kwargs)
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 os
2
- import requests
3
1
  import json
2
+ import os
4
3
  import sys
4
+
5
+ import requests
5
6
  from dotenv import load_dotenv
6
7
 
7
8
 
@@ -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 path, path_item in paths.items():
26
- for method, op in path_item.items():
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 _get_swagger_data, _get_ext_definitions
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 search_help, lookup_function, search_with_lookups
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: {str(e)}"
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
- else:
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 getattr(t, '__origin__') in (list, set):
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[[Union[AuthToken, AccessToken]], None]
6
+ NewTokenCallback = Callable[[AuthToken | AccessToken], None]
7
7
 
8
8
  class LogLevel(str, Enum):
9
9
  DEBUG = "DEBUG"