lkr-dev-cli 0.0.40__tar.gz → 0.0.42__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 (63) hide show
  1. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/PKG-INFO +45 -3
  2. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/README.md +44 -2
  3. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/codemode.md +10 -1
  4. lkr_dev_cli-0.0.42/lkr/__init__.py +5 -0
  5. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/auth_service.py +62 -1
  6. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/classes.py +4 -0
  7. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/codemode/main.py +44 -10
  8. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/extended_sdk_methods/__init__.py +2 -0
  9. lkr_dev_cli-0.0.42/lkr/tools/lookml.py +386 -0
  10. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/tools/main.py +3 -0
  11. lkr_dev_cli-0.0.42/lkr_dev_cli/__init__.py +5 -0
  12. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/pyproject.toml +2 -2
  13. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/tests/test_codemode.py +35 -0
  14. lkr_dev_cli-0.0.42/tests/test_init_sdk.py +167 -0
  15. lkr_dev_cli-0.0.42/tests/test_lookml_tools.py +268 -0
  16. lkr_dev_cli-0.0.40/lkr/__init__.py +0 -3
  17. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/.github/workflows/ci.yml +0 -0
  18. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/.github/workflows/release.yml +0 -0
  19. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/.gitignore +0 -0
  20. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/.python-version +0 -0
  21. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/.vscode/launch.json +0 -0
  22. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/.vscode/settings.json +0 -0
  23. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/Dockerfile +0 -0
  24. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/LICENSE +0 -0
  25. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/Makefile +0 -0
  26. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/cloudbuild.yaml +0 -0
  27. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/auth/__init__.py +0 -0
  28. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/auth/main.py +0 -0
  29. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/auth/oauth.py +0 -0
  30. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/codemode/LOCAL.md +0 -0
  31. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/codemode/__init__.py +0 -0
  32. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/codemode/constant.py +0 -0
  33. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/codemode/download_swagger.py +0 -0
  34. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/codemode/examples.py +0 -0
  35. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/codemode/help.py +0 -0
  36. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/codemode/readme.py +0 -0
  37. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/codemode/swagger.json +0 -0
  38. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/codemode/type.py +0 -0
  39. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/constants.py +0 -0
  40. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/custom_types.py +0 -0
  41. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/exceptions.py +0 -0
  42. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/extended_sdk_methods/classes.py +0 -0
  43. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/extended_sdk_methods/main.py +0 -0
  44. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/logger.py +0 -0
  45. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/main.py +0 -0
  46. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/mcp/classes.py +0 -0
  47. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/mcp/main.py +0 -0
  48. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/mcp/utils.py +0 -0
  49. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/observability/classes.py +0 -0
  50. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/observability/embed_container.html +0 -0
  51. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/observability/main.py +0 -0
  52. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/observability/utils.py +0 -0
  53. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/tools/classes.py +0 -0
  54. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr/tools/permission_deprecation.py +0 -0
  55. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/lkr.md +0 -0
  56. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/tests/TESTING.md +0 -0
  57. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/tests/test_dependency_resolution.py +0 -0
  58. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/tests/test_deps.sh +0 -0
  59. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/tests/test_extended_sdk_methods.py +0 -0
  60. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/tests/test_oauth_account.py +0 -0
  61. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/tests/test_permission_deprecation.py +0 -0
  62. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/ty.toml +0 -0
  63. {lkr_dev_cli-0.0.40 → lkr_dev_cli-0.0.42}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lkr-dev-cli
3
- Version: 0.0.40
3
+ Version: 0.0.42
4
4
  Summary: lkr: a command line interface for looker
5
5
  Author: bwebs
6
6
  License-Expression: MIT
@@ -288,9 +288,51 @@ gcloud run deploy lkr-access-token-updater \
288
288
  --set-env-vars LOOKERSDK_CLIENT_ID=<your client id>,LOOKERSDK_CLIENT_SECRET=<your client secret>,LOOKERSDK_BASE_URL=<your instance url>,LOOKER_WHITELISTED_BASE_URLS=<your instance url>
289
289
  ```
290
290
 
291
- ## UserAttributeUpdater `lkr-dev-cli`
291
+ ## Python SDK & Library Usage
292
292
 
293
- Exported from the `lkr-dev-cli` package is the `UserAttributeUpdater` pydantic class. This class has all the necessary logic to update a user attribute value.
293
+ When installing `lkr-dev-cli` in your Python environment, you can import tools and pre-authenticated Looker SDK clients directly into your scripts, apps, or Jupyter notebooks.
294
+
295
+ ### Package Import Aliases (`lkr` vs `lkr_dev_cli`)
296
+ To maximize convenience and match user intuition, both `lkr` and `lkr_dev_cli` are available as identical top-level Python packages. You can use them completely interchangeably:
297
+
298
+ ```python
299
+ from lkr import init_sdk, UserAttributeUpdater
300
+ # works exactly identical to:
301
+ from lkr_dev_cli import init_sdk, UserAttributeUpdater
302
+ ```
303
+
304
+ ### Initializing the Looker SDK (`init_sdk`)
305
+ The `init_sdk()` function initializes and returns an authenticated `ExtendedLooker40SDK` instance. It automatically handles credential discovery and authentication:
306
+
307
+ ```python
308
+ from lkr_dev_cli import init_sdk
309
+
310
+ # Option 1: Explicit API Authentication
311
+ sdk = init_sdk(
312
+ base_url="https://acme.cloud.looker.com",
313
+ client_id="your-client-id",
314
+ client_secret="your-client-secret",
315
+ verify_ssl=True, # optional, defaults to True
316
+ )
317
+
318
+ # Option 2: Environment Variable API Authentication
319
+ # Automatically uses LOOKERSDK_BASE_URL, LOOKERSDK_CLIENT_ID, LOOKERSDK_CLIENT_SECRET
320
+ sdk = init_sdk()
321
+
322
+ # Option 3: Database (SQLite) OAuth Login Profile
323
+ # Uses your saved active session from running `lkr auth login`
324
+ sdk = init_sdk() # Picks the active current_instance = 1
325
+ # Or pick a specific named login profile:
326
+ sdk = init_sdk(instance_name="my_dev_instance")
327
+
328
+ # Make an API call
329
+ me = sdk.me()
330
+ print(f"Hello, {me.display_name}")
331
+ ```
332
+
333
+ ### UserAttributeUpdater
334
+
335
+ Exported from the package is the `UserAttributeUpdater` pydantic class. This class has all the necessary logic to update a user attribute value.
294
336
 
295
337
  It supports the following operations:
296
338
  - Updating a default value
@@ -252,9 +252,51 @@ gcloud run deploy lkr-access-token-updater \
252
252
  --set-env-vars LOOKERSDK_CLIENT_ID=<your client id>,LOOKERSDK_CLIENT_SECRET=<your client secret>,LOOKERSDK_BASE_URL=<your instance url>,LOOKER_WHITELISTED_BASE_URLS=<your instance url>
253
253
  ```
254
254
 
255
- ## UserAttributeUpdater `lkr-dev-cli`
255
+ ## Python SDK & Library Usage
256
256
 
257
- Exported from the `lkr-dev-cli` package is the `UserAttributeUpdater` pydantic class. This class has all the necessary logic to update a user attribute value.
257
+ When installing `lkr-dev-cli` in your Python environment, you can import tools and pre-authenticated Looker SDK clients directly into your scripts, apps, or Jupyter notebooks.
258
+
259
+ ### Package Import Aliases (`lkr` vs `lkr_dev_cli`)
260
+ To maximize convenience and match user intuition, both `lkr` and `lkr_dev_cli` are available as identical top-level Python packages. You can use them completely interchangeably:
261
+
262
+ ```python
263
+ from lkr import init_sdk, UserAttributeUpdater
264
+ # works exactly identical to:
265
+ from lkr_dev_cli import init_sdk, UserAttributeUpdater
266
+ ```
267
+
268
+ ### Initializing the Looker SDK (`init_sdk`)
269
+ The `init_sdk()` function initializes and returns an authenticated `ExtendedLooker40SDK` instance. It automatically handles credential discovery and authentication:
270
+
271
+ ```python
272
+ from lkr_dev_cli import init_sdk
273
+
274
+ # Option 1: Explicit API Authentication
275
+ sdk = init_sdk(
276
+ base_url="https://acme.cloud.looker.com",
277
+ client_id="your-client-id",
278
+ client_secret="your-client-secret",
279
+ verify_ssl=True, # optional, defaults to True
280
+ )
281
+
282
+ # Option 2: Environment Variable API Authentication
283
+ # Automatically uses LOOKERSDK_BASE_URL, LOOKERSDK_CLIENT_ID, LOOKERSDK_CLIENT_SECRET
284
+ sdk = init_sdk()
285
+
286
+ # Option 3: Database (SQLite) OAuth Login Profile
287
+ # Uses your saved active session from running `lkr auth login`
288
+ sdk = init_sdk() # Picks the active current_instance = 1
289
+ # Or pick a specific named login profile:
290
+ sdk = init_sdk(instance_name="my_dev_instance")
291
+
292
+ # Make an API call
293
+ me = sdk.me()
294
+ print(f"Hello, {me.display_name}")
295
+ ```
296
+
297
+ ### UserAttributeUpdater
298
+
299
+ Exported from the package is the `UserAttributeUpdater` pydantic class. This class has all the necessary logic to update a user attribute value.
258
300
 
259
301
  It supports the following operations:
260
302
  - Updating a default value
@@ -47,5 +47,14 @@ To check things out on a web panel:
47
47
  npx @modelcontextprotocol/inspector uvx -q --from lkr-dev-cli[codemode] lkr code-mode run
48
48
  ```
49
49
 
50
- ```
50
+ ## Architectural & Troubleshooting Notes
51
+
52
+ ### The stdio Stream Corruption Trap
53
+ When running an MCP server over standard input/output (`stdio`) transport, JSON-RPC packets are transmitted directly through the process's `stdout`.
54
+
55
+ If Python code executed within the server (such as tool execution or third-party libraries) writes raw text directly to `sys.stdout` via standard `print()` calls, that raw text corrupts the JSON-RPC response stream. This causes fatal JSON parsing errors on the client (e.g., `invalid character 'C' looking for beginning of value`) and immediately closes the connection.
56
+
57
+ To safeguard against this:
58
+ 1. `run_python_code` automatically wraps all Monty execution in a `redirect_stdout` block.
59
+ 2. If any `print()` output is captured, it is safely packaged and returned as part of a valid JSON structure rather than polluting raw `stdout`.
51
60
 
@@ -0,0 +1,5 @@
1
+ from lkr.tools.classes import UserAttributeUpdater
2
+ from lkr.extended_sdk_methods import ExtendedLooker40SDK
3
+ from lkr.auth_service import init_sdk
4
+
5
+ __all__ = ["UserAttributeUpdater", "ExtendedLooker40SDK", "init_sdk"]
@@ -22,7 +22,13 @@ from lkr.constants import LOOKER_API_VERSION, OAUTH_CLIENT_ID
22
22
  from lkr.custom_types import NewTokenCallback
23
23
  from lkr.logger import logger
24
24
 
25
- __all__ = ["get_auth", "ApiKeyAuthSession", "DbOAuthSession", "is_auth_expired"]
25
+ __all__ = [
26
+ "get_auth",
27
+ "ApiKeyAuthSession",
28
+ "DbOAuthSession",
29
+ "is_auth_expired",
30
+ "init_sdk",
31
+ ]
26
32
 
27
33
 
28
34
  def is_auth_expired(e: Exception) -> bool:
@@ -57,6 +63,7 @@ class ApiKeyApiSettings(ApiSettings):
57
63
  base_url=self.api_key.base_url,
58
64
  client_id=self.api_key.client_id,
59
65
  client_secret=self.api_key.client_secret,
66
+ verify_ssl=str(self.api_key.verify_ssl).lower(),
60
67
  )
61
68
 
62
69
 
@@ -624,3 +631,57 @@ class ApiKeyAuth:
624
631
  raise NotImplementedError(
625
632
  "ApiKeyAuth does not support getting current instance"
626
633
  )
634
+
635
+
636
+ def init_sdk(
637
+ base_url: str | None = None,
638
+ client_id: str | None = None,
639
+ client_secret: str | None = None,
640
+ verify_ssl: bool | None = None,
641
+ use_production: bool = True,
642
+ instance_name: str | None = None,
643
+ ) -> Looker40SDK:
644
+ resolved_base_url = base_url or os.environ.get("LOOKERSDK_BASE_URL")
645
+ resolved_client_id = client_id or os.environ.get("LOOKERSDK_CLIENT_ID")
646
+ resolved_client_secret = (
647
+ client_secret or os.environ.get("LOOKERSDK_CLIENT_SECRET")
648
+ )
649
+
650
+ resolved_verify_ssl: bool
651
+ if verify_ssl is not None:
652
+ resolved_verify_ssl = verify_ssl
653
+ else:
654
+ if "LOOKERSDK_VERIFY_SSL" in os.environ:
655
+ verify_ssl_str = os.environ.get("LOOKERSDK_VERIFY_SSL", "true").lower()
656
+ resolved_verify_ssl = verify_ssl_str not in ("false", "0", "f")
657
+ else:
658
+ resolved_verify_ssl = True
659
+
660
+ if resolved_base_url or resolved_client_id or resolved_client_secret:
661
+ if not (
662
+ resolved_base_url and resolved_client_id and resolved_client_secret
663
+ ):
664
+ missing = []
665
+ if not resolved_base_url:
666
+ missing.append("base_url")
667
+ if not resolved_client_id:
668
+ missing.append("client_id")
669
+ if not resolved_client_secret:
670
+ missing.append("client_secret")
671
+ raise ValueError(
672
+ f"Incomplete API key configuration. Missing: {', '.join(missing)}"
673
+ )
674
+ api_key = LookerApiKey(
675
+ client_id=resolved_client_id,
676
+ client_secret=resolved_client_secret,
677
+ base_url=resolved_base_url,
678
+ verify_ssl=resolved_verify_ssl,
679
+ )
680
+ return init_api_key_sdk(api_key, use_production=use_production)
681
+
682
+ ctx = LkrCtxObj(
683
+ force_oauth=True, oauth_account=instance_name, use_production=use_production
684
+ )
685
+ sql_auth = SqlLiteAuth(ctx)
686
+ return sql_auth.get_current_sdk()
687
+
@@ -8,14 +8,18 @@ class LookerApiKey(BaseModel):
8
8
  client_id: str = Field(..., min_length=1)
9
9
  client_secret: str = Field(..., min_length=1)
10
10
  base_url: str = Field(..., min_length=1)
11
+ verify_ssl: bool = True
11
12
 
12
13
  @classmethod
13
14
  def from_env(cls):
14
15
  try:
16
+ verify_ssl_str = os.environ.get("LOOKERSDK_VERIFY_SSL", "true").lower()
17
+ verify_ssl = verify_ssl_str not in ("false", "0", "f")
15
18
  return cls(
16
19
  client_id=os.environ.get("LOOKERSDK_CLIENT_ID"), # type: ignore
17
20
  client_secret=os.environ.get("LOOKERSDK_CLIENT_SECRET"), # type: ignore
18
21
  base_url=os.environ.get("LOOKERSDK_BASE_URL"), # type: ignore
22
+ verify_ssl=verify_ssl,
19
23
  )
20
24
  except Exception:
21
25
  return None
@@ -1,7 +1,9 @@
1
1
  import inspect
2
- import io
3
2
  import json
4
- from contextlib import redirect_stdout
3
+ import os
4
+ import sys
5
+ import tempfile
6
+ from contextlib import contextmanager
5
7
 
6
8
  import typer
7
9
  import pydantic_monty
@@ -21,8 +23,38 @@ __all__ = ["group"]
21
23
  mcp = FastMCP("lkr:codemode")
22
24
  group = typer.Typer()
23
25
  ctx_lkr: LkrCtxObj | None = None
24
-
25
-
26
+ class OSCapture:
27
+ def __init__(self):
28
+ self.output = ""
29
+
30
+ @contextmanager
31
+ def capture_os_stdout():
32
+ cap = OSCapture()
33
+ fd, temp_path = tempfile.mkstemp()
34
+
35
+ if sys.stdout and hasattr(sys.stdout, "flush"):
36
+ sys.stdout.flush()
37
+
38
+ original_stdout = sys.stdout
39
+ original_stdout_fd = os.dup(1)
40
+ try:
41
+ os.dup2(fd, 1)
42
+ with os.fdopen(os.dup(1), "w") as f:
43
+ sys.stdout = f
44
+ yield cap
45
+ f.flush()
46
+ finally:
47
+ sys.stdout = original_stdout
48
+ os.dup2(original_stdout_fd, 1)
49
+ os.close(original_stdout_fd)
50
+ os.close(fd)
51
+
52
+ try:
53
+ with open(temp_path, "r") as f:
54
+ cap.output = f.read()
55
+ finally:
56
+ if os.path.exists(temp_path):
57
+ os.remove(temp_path)
26
58
 
27
59
  def get_mcp_sdk(ctx: LkrCtxObj):
28
60
  sdk = get_auth(ctx).get_current_sdk(prompt_refresh_invalid_token=True)
@@ -126,13 +158,12 @@ def run_python_code(code: str, dev_mode: bool = False) -> str:
126
158
 
127
159
  m = pydantic_monty.Monty(code)
128
160
 
129
- # Redirect stdout to capture any print() statements
130
- # and prevent them from corrupting the JSON-RPC stream
131
- f = io.StringIO()
132
- with redirect_stdout(f):
161
+ # Use low-level OS stdout capture to ensure any print() statements
162
+ # in Rust or sub-interpreters don't corrupt the JSON-RPC stream
163
+ with capture_os_stdout() as cap:
133
164
  result = m.run(external_functions=external_funcs)
134
165
 
135
- printed_output = f.getvalue()
166
+ printed_output = cap.output
136
167
 
137
168
  # m.run() returns the evaluated result of the last expression (which is already a primitive)
138
169
  try:
@@ -148,7 +179,10 @@ def run_python_code(code: str, dev_mode: bool = False) -> str:
148
179
  output = repr(result)
149
180
 
150
181
  if printed_output:
151
- return f"PRINTED OUTPUT:\n{printed_output}\nRESULT:\n{output}"
182
+ return json.dumps({
183
+ "stdout": printed_output,
184
+ "result": result
185
+ }, indent=2, default=str)
152
186
  return output
153
187
  except Exception as e:
154
188
  logger.error(f"Error executing Monty: {e}")
@@ -8,6 +8,7 @@ from lkr.extended_sdk_methods.classes import (
8
8
  ProjectGeneratorColumn,
9
9
  ProjectGeneratorTable,
10
10
  SelectedTable,
11
+ ProjectCommitRequest,
11
12
  )
12
13
  from lkr.extended_sdk_methods.main import ExtendedLooker40SDK
13
14
 
@@ -22,4 +23,5 @@ __all__ = [
22
23
  "GenerateLookMLParameters",
23
24
  "SelectedTable",
24
25
  "GenerateLookMLWithNewFilesResponse",
26
+ "ProjectCommitRequest",
25
27
  ]