lkr-dev-cli 0.0.39__tar.gz → 0.0.41__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 (58) hide show
  1. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/PKG-INFO +1 -1
  2. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/codemode.md +10 -1
  3. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/codemode/main.py +44 -10
  4. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/extended_sdk_methods/classes.py +12 -0
  5. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/extended_sdk_methods/main.py +35 -0
  6. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/pyproject.toml +1 -1
  7. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/tests/test_codemode.py +58 -1
  8. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/tests/test_extended_sdk_methods.py +55 -0
  9. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/.github/workflows/ci.yml +0 -0
  10. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/.github/workflows/release.yml +0 -0
  11. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/.gitignore +0 -0
  12. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/.python-version +0 -0
  13. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/.vscode/launch.json +0 -0
  14. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/.vscode/settings.json +0 -0
  15. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/Dockerfile +0 -0
  16. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/LICENSE +0 -0
  17. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/Makefile +0 -0
  18. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/README.md +0 -0
  19. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/cloudbuild.yaml +0 -0
  20. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/__init__.py +0 -0
  21. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/auth/__init__.py +0 -0
  22. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/auth/main.py +0 -0
  23. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/auth/oauth.py +0 -0
  24. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/auth_service.py +0 -0
  25. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/classes.py +0 -0
  26. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/codemode/LOCAL.md +0 -0
  27. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/codemode/__init__.py +0 -0
  28. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/codemode/constant.py +0 -0
  29. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/codemode/download_swagger.py +0 -0
  30. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/codemode/examples.py +0 -0
  31. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/codemode/help.py +0 -0
  32. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/codemode/readme.py +0 -0
  33. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/codemode/swagger.json +0 -0
  34. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/codemode/type.py +0 -0
  35. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/constants.py +0 -0
  36. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/custom_types.py +0 -0
  37. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/exceptions.py +0 -0
  38. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/extended_sdk_methods/__init__.py +0 -0
  39. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/logger.py +0 -0
  40. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/main.py +0 -0
  41. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/mcp/classes.py +0 -0
  42. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/mcp/main.py +0 -0
  43. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/mcp/utils.py +0 -0
  44. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/observability/classes.py +0 -0
  45. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/observability/embed_container.html +0 -0
  46. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/observability/main.py +0 -0
  47. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/observability/utils.py +0 -0
  48. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/tools/classes.py +0 -0
  49. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/tools/main.py +0 -0
  50. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr/tools/permission_deprecation.py +0 -0
  51. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/lkr.md +0 -0
  52. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/tests/TESTING.md +0 -0
  53. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/tests/test_dependency_resolution.py +0 -0
  54. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/tests/test_deps.sh +0 -0
  55. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/tests/test_oauth_account.py +0 -0
  56. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/tests/test_permission_deprecation.py +0 -0
  57. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/ty.toml +0 -0
  58. {lkr_dev_cli-0.0.39 → lkr_dev_cli-0.0.41}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lkr-dev-cli
3
- Version: 0.0.39
3
+ Version: 0.0.41
4
4
  Summary: lkr: a command line interface for looker
5
5
  Author: bwebs
6
6
  License-Expression: MIT
@@ -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
 
@@ -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}")
@@ -13,6 +13,7 @@ __all__ = [
13
13
  "GenerateLookMLParameters",
14
14
  "SelectedTable",
15
15
  "GenerateLookMLWithNewFilesResponse",
16
+ "ProjectCommitRequest",
16
17
  ]
17
18
 
18
19
 
@@ -84,3 +85,14 @@ class GenerateLookMLWithNewFilesResponse(BaseModel):
84
85
 
85
86
  generate_lookml: str = Field(..., description="The API response from generating LookML, typically an empty string on success.")
86
87
  new_files: list[Any] = Field(..., description="List of newly created files.")
88
+
89
+
90
+ class ProjectCommitRequest(BaseModel):
91
+ """Request body for LookML project commit."""
92
+
93
+ files: Optional[list[str]] = Field(
94
+ None,
95
+ description="List of files to commit. If omitted or passed as None, Looker automatically stages and commits all modified, newly added, and deleted LookML files in the project's current development workspace.",
96
+ )
97
+ message: Optional[str] = Field(None, description="Commit message")
98
+ amend: Optional[bool] = Field(None, description="Amend the last commit")
@@ -13,6 +13,7 @@ from lkr.extended_sdk_methods.classes import (
13
13
  ProjectGeneratorColumn,
14
14
  ProjectGeneratorTable,
15
15
  SelectedTable,
16
+ ProjectCommitRequest,
16
17
  )
17
18
 
18
19
  __all__ = [
@@ -26,6 +27,7 @@ __all__ = [
26
27
  "GenerateLookMLParameters",
27
28
  "SelectedTable",
28
29
  "GenerateLookMLWithNewFilesResponse",
30
+ "ProjectCommitRequest",
29
31
  ]
30
32
 
31
33
 
@@ -310,3 +312,36 @@ class ExtendedLooker40SDK(Looker40SDK):
310
312
  return GenerateLookMLWithNewFilesResponse(
311
313
  generate_lookml=response, new_files=new_files
312
314
  )
315
+
316
+ def commit(
317
+ self,
318
+ project_id: str,
319
+ body: Optional[Union[ProjectCommitRequest, dict, Any]] = None,
320
+ transport_options: Optional[transport.TransportOptions] = None,
321
+ ) -> str:
322
+ """Commit changes to the project's git repository.
323
+
324
+ If you do not specify an array of files in the body (or pass None),
325
+ Looker automatically stages and commits all modified, newly added,
326
+ and deleted LookML files in the project's current development workspace.
327
+
328
+ Args:
329
+ project_id: Id of project.
330
+ body: Optional commit parameters.
331
+ transport_options: Optional transport options.
332
+
333
+ Returns:
334
+ str: API response.
335
+ """
336
+ project_id = self.encode_path_param(project_id)
337
+ path = f"/projects/{project_id}/commit"
338
+ request_body = self._prepare_body(body)
339
+ return cast(
340
+ str,
341
+ self.post(
342
+ path=path,
343
+ structure=str,
344
+ body=request_body,
345
+ transport_options=transport_options,
346
+ ),
347
+ )
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lkr-dev-cli"
3
- version = "0.0.39"
3
+ version = "0.0.41"
4
4
  description = "lkr: a command line interface for looker"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,4 +1,5 @@
1
1
  from typing import Any, cast
2
+ import json
2
3
  import pytest
3
4
  from unittest.mock import patch, MagicMock
4
5
  from looker_sdk.rtl.auth_session import AuthSession
@@ -198,7 +199,7 @@ return "\\n".join(res)
198
199
 
199
200
  def test_extended_sdk_methods_present():
200
201
  code = """
201
- methods = ['all_project_files', 'get_file_content', 'create_file', 'update_file', 'delete_file', 'create_project_directory', 'delete_project_directory', 'generate_lookml', 'generate_lookml_with_new_files']
202
+ methods = ['all_project_files', 'get_file_content', 'create_file', 'update_file', 'delete_file', 'create_project_directory', 'delete_project_directory', 'generate_lookml', 'generate_lookml_with_new_files', 'commit']
202
203
  for m in methods:
203
204
  if m not in dir():
204
205
  return "Missing " + m
@@ -225,4 +226,60 @@ return lookup('ProjectGeneratorTable')
225
226
  assert "primary_key: string" in result_lookup
226
227
 
227
228
 
229
+ def test_commit_help_and_lookup():
230
+ code_help = """
231
+ return help('commit')
232
+ """
233
+ result = run_python_code(code_help)
234
+ assert "Function: commit" in result or "Type: ProjectCommitRequest" in result
235
+
236
+ code_lookup = """
237
+ return lookup('commit')
238
+ """
239
+ result_lookup = run_python_code(code_lookup)
240
+ assert "Function: commit" in result_lookup
241
+ assert "Looker automatically stages and commits all modified" in result_lookup
242
+
243
+ code_lookup_model = """
244
+ return lookup('ProjectCommitRequest')
245
+ """
246
+ result_model = run_python_code(code_lookup_model)
247
+ assert "Type: ProjectCommitRequest" in result_model
248
+ assert "Looker automatically stages and commits" in result_model
249
+
228
250
 
251
+ def test_captured_print_json():
252
+ code = """
253
+ print("Hello from stdout")
254
+ return {"status": "success"}
255
+ """
256
+ result_raw = run_python_code(code)
257
+ data = json.loads(result_raw)
258
+ assert data["stdout"] == "Hello from stdout\n"
259
+ assert data["result"] == {"status": "success"}
260
+
261
+ def test_captured_print_json_many():
262
+ code = """
263
+ for i in range(10):
264
+ print(i)
265
+ return {"status": "success"}
266
+ """
267
+ result_raw = run_python_code(code)
268
+ data = json.loads(result_raw)
269
+ assert data["stdout"] == "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n"
270
+ assert data["result"] == {"status": "success"}
271
+
272
+
273
+ def test_parent_runtime_not_polluted(capfd):
274
+ code = """
275
+ print("This should be trapped and not leak to parent stdout")
276
+ print("Also checking second print")
277
+ return {"status": "clean"}
278
+ """
279
+ result_raw = run_python_code(code)
280
+ data = json.loads(result_raw)
281
+ assert "This should be trapped" in data["stdout"]
282
+ assert "Also checking" in data["stdout"]
283
+
284
+ out, err = capfd.readouterr()
285
+ assert out == ""
@@ -8,6 +8,7 @@ from lkr.extended_sdk_methods.main import (
8
8
  ProjectGeneratorColumn,
9
9
  ProjectGeneratorTable,
10
10
  SelectedTable,
11
+ ProjectCommitRequest,
11
12
  )
12
13
 
13
14
 
@@ -186,3 +187,57 @@ def test_generate_lookml_with_new_files():
186
187
  assert isinstance(result, GenerateLookMLWithNewFilesResponse)
187
188
  assert result.generate_lookml == "generated"
188
189
  assert result.new_files == [FakeFile("c")]
190
+
191
+
192
+ def test_project_commit_request_model():
193
+ req = ProjectCommitRequest(files=["a.lkml"], message="fix bug", amend=False)
194
+ assert req.files == ["a.lkml"]
195
+ assert req.message == "fix bug"
196
+ assert req.amend is False
197
+
198
+
199
+ def test_commit():
200
+ mock_auth = MagicMock()
201
+ mock_auth.settings.base_url = "https://example.looker.com"
202
+ sdk = ExtendedLooker40SDK(
203
+ auth=mock_auth,
204
+ deserialize=MagicMock(),
205
+ serialize=MagicMock(),
206
+ transport=MagicMock(),
207
+ api_version="4.0",
208
+ )
209
+
210
+ with patch.object(sdk, "post") as mock_post:
211
+ mock_post.return_value = "commit hash or project"
212
+
213
+ req = ProjectCommitRequest(files=["model.lkml"], message="initial commit")
214
+ res = sdk.commit(project_id="test_proj", body=req)
215
+
216
+ assert res == "commit hash or project"
217
+ mock_post.assert_called_once()
218
+ _, kwargs = mock_post.call_args
219
+ assert kwargs["path"] == "/projects/test_proj/commit"
220
+ assert kwargs["body"] == req.model_dump()
221
+
222
+
223
+ def test_commit_no_body():
224
+ mock_auth = MagicMock()
225
+ mock_auth.settings.base_url = "https://example.looker.com"
226
+ sdk = ExtendedLooker40SDK(
227
+ auth=mock_auth,
228
+ deserialize=MagicMock(),
229
+ serialize=MagicMock(),
230
+ transport=MagicMock(),
231
+ api_version="4.0",
232
+ )
233
+
234
+ with patch.object(sdk, "post") as mock_post:
235
+ mock_post.return_value = "commit success"
236
+
237
+ res = sdk.commit(project_id="test_proj")
238
+
239
+ assert res == "commit success"
240
+ mock_post.assert_called_once()
241
+ _, kwargs = mock_post.call_args
242
+ assert kwargs["path"] == "/projects/test_proj/commit"
243
+ assert kwargs["body"] is None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes