lkr-dev-cli 0.0.33__tar.gz → 0.0.35__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 (47) hide show
  1. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/.github/workflows/release.yml +6 -0
  2. lkr_dev_cli-0.0.35/Makefile +26 -0
  3. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/PKG-INFO +25 -17
  4. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/README.md +4 -0
  5. lkr_dev_cli-0.0.35/codemode.md +51 -0
  6. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/auth/main.py +15 -9
  7. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/auth_service.py +19 -2
  8. lkr_dev_cli-0.0.35/lkr/codemode/__init__.py +3 -0
  9. lkr_dev_cli-0.0.35/lkr/codemode/main.py +200 -0
  10. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/logger.py +1 -1
  11. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/main.py +1 -0
  12. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/mcp/main.py +2 -0
  13. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr.md +30 -0
  14. lkr_dev_cli-0.0.35/pyproject.toml +67 -0
  15. lkr_dev_cli-0.0.35/tests/test_codemode.py +18 -0
  16. lkr_dev_cli-0.0.35/tests/test_codemode2.py +28 -0
  17. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/uv.lock +80 -37
  18. lkr_dev_cli-0.0.33/Makefile +0 -7
  19. lkr_dev_cli-0.0.33/pyproject.toml +0 -56
  20. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/.github/workflows/test-dependencies.yml +0 -0
  21. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/.gitignore +0 -0
  22. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/.python-version +0 -0
  23. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/.vscode/launch.json +0 -0
  24. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/.vscode/settings.json +0 -0
  25. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/Dockerfile +0 -0
  26. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/LICENSE +0 -0
  27. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/cloudbuild.yaml +0 -0
  28. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/__init__.py +0 -0
  29. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/auth/__init__.py +0 -0
  30. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/auth/oauth.py +0 -0
  31. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/classes.py +0 -0
  32. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/constants.py +0 -0
  33. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/custom_types.py +0 -0
  34. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/exceptions.py +0 -0
  35. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/mcp/classes.py +0 -0
  36. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/mcp/utils.py +0 -0
  37. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/observability/classes.py +0 -0
  38. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/observability/embed_container.html +0 -0
  39. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/observability/main.py +0 -0
  40. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/observability/utils.py +0 -0
  41. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/tools/classes.py +0 -0
  42. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/tools/main.py +0 -0
  43. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/tools/permission_deprecation.py +0 -0
  44. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/tests/TESTING.md +0 -0
  45. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/tests/test_dependency_resolution.py +0 -0
  46. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/tests/test_deps.sh +0 -0
  47. {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/tests/test_permission_deprecation.py +0 -0
@@ -52,6 +52,12 @@ jobs:
52
52
  ${{ env.IMAGE_NAME }}:latest \
53
53
  --project ${{ secrets.GCP_PROJECT_ID }}
54
54
 
55
+ - name: Trigger Website Deploy
56
+ run: |
57
+ curl -fSL "$DEPLOY_URL"
58
+ env:
59
+ DEPLOY_URL: ${{ secrets.DEPLOY_URL }}
60
+
55
61
  publish:
56
62
  name: python
57
63
  runs-on: ubuntu-latest
@@ -0,0 +1,26 @@
1
+ .PHONY: docs test-deps codemode-test codemode-test2 codemode-start
2
+
3
+ docs:
4
+ uv run typer lkr/main.py utils docs --output lkr.md
5
+
6
+ test-deps:
7
+ python tests/test_dependency_resolution.py
8
+
9
+ codemode-test:
10
+ uv run python tests/test_codemode.py
11
+
12
+ codemode-test2:
13
+ uv run python tests/test_codemode2.py
14
+
15
+
16
+ codemode-start:
17
+ @echo "Add this to your mcpServers config:"
18
+ @echo "{"
19
+ @echo " \"mcpServers\": {"
20
+ @echo " \"lkr_codemode\": {"
21
+ @echo " \"command\": \"uvx\","
22
+ @echo " \"args\": [\"-q\", \"--from\", \"lkr-dev-cli[codemode]\", \"lkr\", \"code-mode\", \"run\"]"
23
+ @echo " }"
24
+ @echo " }"
25
+ @echo "}"
26
+ uv run -q lkr code-mode run
@@ -1,33 +1,37 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lkr-dev-cli
3
- Version: 0.0.33
3
+ Version: 0.0.35
4
4
  Summary: lkr: a command line interface for looker
5
5
  Author: bwebs
6
6
  License-Expression: MIT
7
7
  License-File: LICENSE
8
8
  Requires-Python: >=3.12
9
- Requires-Dist: cryptography>=42.0.0
10
- Requires-Dist: looker-sdk>=25.4.0
11
- Requires-Dist: pydantic>=2.11.4
9
+ Requires-Dist: cryptography>=45.0.4
10
+ Requires-Dist: looker-sdk>=25.10.0
11
+ Requires-Dist: pydantic>=2.11.7
12
12
  Requires-Dist: pydash>=8.0.5
13
13
  Requires-Dist: questionary>=2.1.0
14
- Requires-Dist: requests>=2.31.0
15
- Requires-Dist: structlog>=25.3.0
16
- Requires-Dist: typer>=0.15.2
14
+ Requires-Dist: requests>=2.32.4
15
+ Requires-Dist: structlog>=25.4.0
16
+ Requires-Dist: typer>=0.16.0
17
17
  Provides-Extra: all
18
- Requires-Dist: duckdb>=1.2.2; extra == 'all'
19
- Requires-Dist: fastapi[standard]>=0.115.12; extra == 'all'
20
- Requires-Dist: mcp[cli]>=1.9.2; extra == 'all'
21
- Requires-Dist: selenium>=4.32.0; extra == 'all'
18
+ Requires-Dist: duckdb>=1.3.1; extra == 'all'
19
+ Requires-Dist: fastapi[standard]>=0.115.14; extra == 'all'
20
+ Requires-Dist: mcp[cli]>=1.10.1; extra == 'all'
21
+ Requires-Dist: pydantic-monty; extra == 'all'
22
+ Requires-Dist: selenium>=4.34.0; extra == 'all'
23
+ Provides-Extra: codemode
24
+ Requires-Dist: mcp[cli]>=1.10.1; extra == 'codemode'
25
+ Requires-Dist: pydantic-monty; extra == 'codemode'
22
26
  Provides-Extra: mcp
23
- Requires-Dist: duckdb>=1.2.2; extra == 'mcp'
24
- Requires-Dist: fastapi[standard]>=0.115.12; extra == 'mcp'
25
- Requires-Dist: mcp[cli]>=1.9.2; extra == 'mcp'
27
+ Requires-Dist: duckdb>=1.3.1; extra == 'mcp'
28
+ Requires-Dist: fastapi[standard]>=0.115.14; extra == 'mcp'
29
+ Requires-Dist: mcp[cli]>=1.10.1; extra == 'mcp'
26
30
  Provides-Extra: observability
27
- Requires-Dist: fastapi[standard]>=0.115.12; extra == 'observability'
28
- Requires-Dist: selenium>=4.32.0; extra == 'observability'
31
+ Requires-Dist: fastapi[standard]>=0.115.14; extra == 'observability'
32
+ Requires-Dist: selenium>=4.34.0; extra == 'observability'
29
33
  Provides-Extra: tools
30
- Requires-Dist: fastapi[standard]>=0.115.12; extra == 'tools'
34
+ Requires-Dist: fastapi[standard]>=0.115.14; extra == 'tools'
31
35
  Description-Content-Type: text/markdown
32
36
 
33
37
  # lkr cli
@@ -105,6 +109,10 @@ Go to the Looker API Explorer for Register OAuth App (https://your.looker.instan
105
109
  - This only needs to be done once per instance
106
110
 
107
111
 
112
+ ## Code-Mode
113
+
114
+ Execute Python code safely with full Looker SDK coverage within a secure sandbox environment. Constructed as an MCP tool, it dynamically inspects the Looker SDK for all public methods and injects them into the Monty sandbox safely. For detailed options, safe primitives transformations, and PKCE configurations, view the full [Code-Mode Docs](./codemode.md).
115
+
108
116
  ## MCP
109
117
  Built into the `lkr` is an MCP server. Right now its tools are based on helping you work within an IDE. To use it a tool like [Cursor](https://www.cursor.com/), add this to your mcp.json
110
118
 
@@ -73,6 +73,10 @@ Go to the Looker API Explorer for Register OAuth App (https://your.looker.instan
73
73
  - This only needs to be done once per instance
74
74
 
75
75
 
76
+ ## Code-Mode
77
+
78
+ Execute Python code safely with full Looker SDK coverage within a secure sandbox environment. Constructed as an MCP tool, it dynamically inspects the Looker SDK for all public methods and injects them into the Monty sandbox safely. For detailed options, safe primitives transformations, and PKCE configurations, view the full [Code-Mode Docs](./codemode.md).
79
+
76
80
  ## MCP
77
81
  Built into the `lkr` is an MCP server. Right now its tools are based on helping you work within an IDE. To use it a tool like [Cursor](https://www.cursor.com/), add this to your mcp.json
78
82
 
@@ -0,0 +1,51 @@
1
+ # Looker Code-Mode MCP Server
2
+
3
+ `lkr code-mode` allows you to invoke a Python-based Model Context Protocol (MCP) server. It offers an AI agent the unique capacity to batched-execute Python commands securely within the Monty sandbox against your active Looker instance!
4
+
5
+ ## How It Works
6
+
7
+ Instead of dumping hundreds of explicit tool declarations onto your AI agent (token bloat), `lkr code-mode` exposes **exactly one tool**: `run_python_code(code: str)`.
8
+
9
+ The tool instantiates Looker SDK natively, searches all bound methods, and passes them safely onto Monty's environment as global functions. When the LLM writes standard Python code (e.g., `me()`, `folder(id)`), Monty will process it correctly locally!
10
+
11
+ ### Key Features:
12
+ - **100% Tool Coverage:** Accesses all Looker SDK public operations smoothly without token limits.
13
+ - **Recursive Translation:** Complex Looker models like User, Folder, Dashboard get string-converted into dictionaries immediately before ingesting them into Monty.
14
+ - **Automatic PKCE Restarter:** Caught an invalid token? Code-Mode immediately catches `InvalidRefreshTokenError` and safely opens up your PKCE authentication browser automatically.
15
+ - **Extremely Secure:** Monty interpreter ensures isolated sandbox processing. No local filesystem accesses are exposed.
16
+
17
+ ## Continuous Usage
18
+
19
+ ### 1. Starting the Server
20
+ To immediately trigger the stdio listener, use:
21
+ ```bash
22
+ uvx -q --from lkr-dev-cli[codemode] lkr code-mode run
23
+ ```
24
+
25
+ ### 2. Client Configuration
26
+ To hook this server into Cursor or Claude Desktop natively over stdio, append the following onto your `mcpServers` configuration JSON. Make sure to pass your Looker instance credentials as environment variables (see standard API requirements in [README.md](./README.md#using-api-key)):
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "lkr_codemode": {
32
+ "command": "uvx",
33
+ "args": ["-q", "--from", "lkr-dev-cli[codemode]", "lkr", "code-mode", "run"],
34
+ "env": {
35
+ "LOOKERSDK_BASE_URL": "https://your.looker.instance",
36
+ "LOOKERSDK_CLIENT_ID": "your-client-id",
37
+ "LOOKERSDK_CLIENT_SECRET": "your-client-secret"
38
+ }
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ ### 3. Visual Inspector
45
+ To check things out on a web panel:
46
+ ```bash
47
+ npx @modelcontextprotocol/inspector uvx -q --from lkr-dev-cli[codemode] lkr code-mode run
48
+ ```
49
+
50
+ ```
51
+
@@ -191,16 +191,22 @@ def whoami(ctx: typer.Context):
191
191
  Check current authentication
192
192
  """
193
193
  auth = get_auth(ctx)
194
- sdk = auth.get_current_sdk(prompt_refresh_invalid_token=True)
195
- if not sdk:
196
- logger.error(
197
- "Not currently authenticated - use `lkr auth login` or `lkr auth switch` to authenticate"
194
+ try:
195
+ sdk = auth.get_current_sdk(prompt_refresh_invalid_token=True)
196
+ if not sdk:
197
+ logger.error(
198
+ "Not currently authenticated - use `lkr auth login` or `lkr auth switch` to authenticate"
199
+ )
200
+ raise typer.Exit(1)
201
+ user = sdk.me()
202
+ logger.info(
203
+ f"Currently authenticated as {user.first_name} {user.last_name} ({user.email}) to {sdk.auth.settings.base_url}"
198
204
  )
199
- raise typer.Exit(1)
200
- user = sdk.me()
201
- logger.info(
202
- f"Currently authenticated as {user.first_name} {user.last_name} ({user.email}) to {sdk.auth.settings.base_url}"
203
- )
205
+ except Exception as e:
206
+ if "invalid_grant" in str(e) or "token expired" in str(e):
207
+ logger.error("Your Looker OAuth session has expired. Please run 'lkr auth login' to re-authenticate.")
208
+ raise typer.Exit(1)
209
+ raise e
204
210
 
205
211
 
206
212
  @group.command()
@@ -23,7 +23,12 @@ from lkr.constants import LOOKER_API_VERSION, OAUTH_CLIENT_ID, OAUTH_REDIRECT_UR
23
23
  from lkr.custom_types import NewTokenCallback
24
24
  from lkr.logger import logger
25
25
 
26
- __all__ = ["get_auth", "ApiKeyAuthSession", "DbOAuthSession"]
26
+ __all__ = ["get_auth", "ApiKeyAuthSession", "DbOAuthSession", "is_auth_expired"]
27
+
28
+
29
+ def is_auth_expired(e: Exception) -> bool:
30
+ return "invalid_grant" in str(e) or "token expired" in str(e)
31
+
27
32
 
28
33
 
29
34
  def get_auth(ctx: Union["typer.Context", LkrCtxObj]) -> Union["SqlLiteAuth", "ApiKeyAuth"]:
@@ -465,11 +470,23 @@ class SqlLiteAuth:
465
470
  def refresh_current_token(token: Union[AccessToken, AuthToken]):
466
471
  current_auth.set_token(self.conn, new_token=token, commit=True)
467
472
 
468
- return init_oauth_sdk(
473
+ sdk = init_oauth_sdk(
469
474
  current_auth.base_url,
470
475
  new_token_callback=refresh_current_token,
471
476
  access_token=current_auth.to_access_token(),
472
477
  )
478
+ if prompt_refresh_invalid_token:
479
+ import sys
480
+ try:
481
+ sdk.auth.authenticate({})
482
+ except Exception as e:
483
+ if is_auth_expired(e):
484
+ if sys.stdin.isatty():
485
+ self._cli_confirm_refresh_token(current_auth, quiet=False)
486
+ return self.get_current_sdk(prompt_refresh_invalid_token=False)
487
+ raise e
488
+
489
+ return sdk
473
490
 
474
491
  else:
475
492
  logger.error("No current instance found, please login")
@@ -0,0 +1,3 @@
1
+ from .main import group
2
+
3
+ __all__ = ["group"]
@@ -0,0 +1,200 @@
1
+ import inspect
2
+ import io
3
+ import json
4
+ from contextlib import redirect_stdout
5
+ from typing import Optional
6
+
7
+ import typer
8
+ import pydantic_monty
9
+ from mcp.server.fastmcp import FastMCP
10
+
11
+ from lkr.auth_service import get_auth, is_auth_expired
12
+ from lkr.classes import LkrCtxObj
13
+ from lkr.logger import logger
14
+
15
+ __all__ = ["group"]
16
+
17
+ mcp = FastMCP("lkr:codemode")
18
+ group = typer.Typer()
19
+
20
+
21
+
22
+ def get_mcp_sdk(ctx: LkrCtxObj):
23
+ sdk = get_auth(ctx).get_current_sdk(prompt_refresh_invalid_token=True)
24
+ sdk.auth.settings.agent_tag += "-codemode"
25
+ return sdk
26
+
27
+
28
+
29
+ def to_primitive(obj):
30
+ seen = set()
31
+
32
+ def _to_primitive(o):
33
+ if isinstance(o, (str, int, float, bool, type(None))):
34
+ return o
35
+
36
+ obj_id = id(o)
37
+ if obj_id in seen:
38
+ return f"<Circular reference to {type(o).__name__}>"
39
+ seen.add(obj_id)
40
+
41
+ try:
42
+ if isinstance(o, list):
43
+ return [_to_primitive(item) for item in o]
44
+ elif isinstance(o, dict):
45
+ return {k: _to_primitive(v) for k, v in o.items()}
46
+ else:
47
+ try:
48
+ return _to_primitive(vars(o))
49
+ except TypeError:
50
+ return str(o)
51
+ except Exception:
52
+ return str(o)
53
+ finally:
54
+ seen.remove(obj_id)
55
+
56
+ return _to_primitive(obj)
57
+
58
+
59
+
60
+ @mcp.tool()
61
+ def run_python_code(code: str) -> str:
62
+ """
63
+ Execute Python code safely with access to all Looker SDK methods as global functions.
64
+ Capture the result.
65
+
66
+ AGENT HINTS:
67
+ - Use `dir()` and `help('method_name')` to discover available SDK methods.
68
+ - Do not instantiate an SDK; use global functions directly (e.g. `me()`).
69
+ - Returned Looker models are primitive dictionaries (use `obj["id"]`, not `obj.id`).
70
+ - Return your output (avoid using print() as it may pollute the stdio stream).
71
+ - Recursion: Use `folder_children(id)` to traverse nested folders.
72
+ """
73
+ try:
74
+ ctx = LkrCtxObj(force_oauth=False)
75
+ sdk = get_mcp_sdk(ctx)
76
+
77
+ external_funcs = {}
78
+ for name, method in inspect.getmembers(sdk, predicate=inspect.ismethod):
79
+ if not name.startswith('_'):
80
+ # Wrap in a lambda to recursively convert output to primitives
81
+ def make_wrapper(m):
82
+ def wrapper(*args, **kwargs):
83
+ res = m(*args, **kwargs)
84
+ return to_primitive(res)
85
+ return wrapper
86
+ external_funcs[name] = make_wrapper(method)
87
+
88
+ # Provide helper functions for the LLM to explore the SDK
89
+ external_funcs['dir'] = lambda: list(external_funcs.keys())
90
+
91
+ def _help(name: str) -> str:
92
+ if name in external_funcs:
93
+ if hasattr(sdk, name):
94
+ return getattr(sdk, name).__doc__ or "No docstring available."
95
+ return f"{name} is a built-in helper function."
96
+ return f"Function '{name}' not found."
97
+ external_funcs['help'] = _help
98
+
99
+ m = pydantic_monty.Monty(code)
100
+
101
+ # Redirect stdout to capture any print() statements
102
+ # and prevent them from corrupting the JSON-RPC stream
103
+ f = io.StringIO()
104
+ with redirect_stdout(f):
105
+ result = m.run(external_functions=external_funcs)
106
+
107
+ printed_output = f.getvalue()
108
+
109
+ # m.run() returns the evaluated result of the last expression (which is already a primitive)
110
+ try:
111
+ # Use JSON for nice formatting if it's a dict/list
112
+ if result is not None:
113
+ if isinstance(result, str):
114
+ output = result
115
+ else:
116
+ output = json.dumps(result, indent=2, default=str)
117
+ else:
118
+ output = ""
119
+ except Exception:
120
+ output = repr(result)
121
+
122
+ if printed_output:
123
+ return f"PRINTED OUTPUT:\n{printed_output}\nRESULT:\n{output}"
124
+ return output
125
+ except Exception as e:
126
+ logger.error(f"Error executing Monty: {e}")
127
+ if is_auth_expired(e):
128
+ return "Error: Your Looker OAuth session has expired. Please run 'lkr auth login' to re-authenticate."
129
+ return f"Error: {str(e)}"
130
+
131
+
132
+ @mcp.resource("looker://agent-hints")
133
+ def get_agent_hints() -> str:
134
+ """Crucial hints and rules for AI agents writing Python for the Looker SDK."""
135
+ return """
136
+ 1. **Global Functions**: All Looker SDK methods are global. Use `me()`, not `sdk.me()`.
137
+ 2. **Dict Access**: Return values are dictionaries, not objects. Use `user["id"]`, not `user.id`.
138
+ 3. **Discovery**: Use `dir()` and `help('method')` to explore the SDK.
139
+ 4. **No Imports**: Do not `import looker_sdk`.
140
+ 5. **Output**: Return your results instead of using `print()`.
141
+ 6. **Efficiency**: Always use the `fields` parameter (e.g., `all_dashboards(fields="id,title")`) when listing many objects to prevent timeouts.
142
+ 7. **Nested Folders**: Use `folder_children(id)` to get sub-folders.
143
+ """
144
+
145
+
146
+ @mcp.prompt("explore_looker_sdk")
147
+ def explore_looker_sdk() -> str:
148
+ """Provide examples for how to explore the Looker SDK in code mode."""
149
+ return '''
150
+ To explore the Looker SDK, you can use the injected `dir()` and `help()` helpers.
151
+ Do not use print() as it may corrupt the MCP output stream; always return the result.
152
+
153
+ Example 1: Find all dashboard-related methods
154
+ ```python
155
+ return [m for m in dir() if 'dashboard' in m.lower()]
156
+ ```
157
+
158
+ Example 2: Get the description of a specific method
159
+ ```python
160
+ return help('search_dashboards')
161
+ ```
162
+ '''
163
+
164
+
165
+ @mcp.prompt("list_personal_dashboards")
166
+ def list_personal_dashboards() -> str:
167
+ """Provide an example of how to recursively list dashboards in a user's personal folder."""
168
+ return '''
169
+ Here is a robust example of how to traverse the folder hierarchy using the Looker SDK in code mode:
170
+
171
+ ```python
172
+ def get_all_items(folder_id):
173
+ f = folder(folder_id)
174
+ items = {
175
+ "dashboards": f.get("dashboards", []),
176
+ "looks": f.get("looks", [])
177
+ }
178
+
179
+ for child in folder_children(folder_id):
180
+ child_items = get_all_items(child["id"])
181
+ items["dashboards"].extend(child_items["dashboards"])
182
+ items["looks"].extend(child_items["looks"])
183
+
184
+ return items
185
+
186
+ me_data = me()
187
+ return get_all_items(me_data["personal_folder_id"])
188
+ ```
189
+ '''
190
+
191
+
192
+ @group.command(name="run")
193
+ def run(
194
+ ctx: typer.Context,
195
+ debug: bool = typer.Option(False, help="Debug mode"),
196
+ ):
197
+ mcp.run()
198
+
199
+ if __name__ == "__main__":
200
+ mcp.run("sse")
@@ -36,7 +36,7 @@ theme = Theme(
36
36
  ) if RICH_AVAILABLE else None
37
37
 
38
38
  # Create a console for logging
39
- console = Console(theme=theme) if RICH_AVAILABLE else None
39
+ console = Console(theme=theme, stderr=True) if RICH_AVAILABLE else None
40
40
 
41
41
  # Configure the logging handler
42
42
  handler = RichHandler(
@@ -42,6 +42,7 @@ def add_optional_typer_group(app, import_path, group_name, extra_message=None):
42
42
  add_optional_typer_group(app, "lkr.mcp.main.group", "mcp")
43
43
  add_optional_typer_group(app, "lkr.observability.main.group", "observability")
44
44
  add_optional_typer_group(app, "lkr.tools.main.group", "tools")
45
+ add_optional_typer_group(app, "lkr.codemode.main.group", "code-mode")
45
46
 
46
47
  @app.callback()
47
48
  def callback(
@@ -1,4 +1,5 @@
1
1
  # server.py
2
+ import sys
2
3
  import threading
3
4
  from datetime import datetime
4
5
  from pathlib import Path
@@ -549,6 +550,7 @@ def run(
549
550
  set_log_level(LogLevel.DEBUG)
550
551
  else:
551
552
  set_log_level(LogLevel.ERROR)
553
+ sys.stdout = sys.stderr
552
554
  mcp.run()
553
555
 
554
556
 
@@ -27,6 +27,7 @@ $ lkr [OPTIONS] COMMAND [ARGS]...
27
27
  * `mcp`
28
28
  * `observability`
29
29
  * `tools`
30
+ * `code-mode`
30
31
 
31
32
  ## `lkr auth`
32
33
 
@@ -221,3 +222,32 @@ $ lkr tools schedule-download-deprecation [OPTIONS]
221
222
  * `--unfiltered`: Show all rows, including those with no missing permissions
222
223
  * `--email`: Use Email instead of Name
223
224
  * `--help`: Show this message and exit.
225
+
226
+ ## `lkr code-mode`
227
+
228
+ **Usage**:
229
+
230
+ ```console
231
+ $ lkr code-mode [OPTIONS] COMMAND [ARGS]...
232
+ ```
233
+
234
+ **Options**:
235
+
236
+ * `--help`: Show this message and exit.
237
+
238
+ **Commands**:
239
+
240
+ * `run`
241
+
242
+ ### `lkr code-mode run`
243
+
244
+ **Usage**:
245
+
246
+ ```console
247
+ $ lkr code-mode run [OPTIONS]
248
+ ```
249
+
250
+ **Options**:
251
+
252
+ * `--debug / --no-debug`: Debug mode [default: no-debug]
253
+ * `--help`: Show this message and exit.
@@ -0,0 +1,67 @@
1
+ [project]
2
+ name = "lkr-dev-cli"
3
+ version = "0.0.35"
4
+ description = "lkr: a command line interface for looker"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "bwebs"}
9
+ ]
10
+ requires-python = ">=3.12"
11
+ dependencies = [
12
+ "looker-sdk>=25.10.0",
13
+ "pydantic>=2.11.7",
14
+ "pydash>=8.0.5",
15
+ "typer>=0.16.0",
16
+ "requests>=2.32.4",
17
+ "cryptography>=45.0.4",
18
+ "structlog>=25.4.0",
19
+ "questionary>=2.1.0",
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ mcp = [
24
+ "mcp[cli]>=1.10.1",
25
+ "duckdb>=1.3.1",
26
+ "fastapi[standard]>=0.115.14"
27
+ ]
28
+ codemode = [
29
+ "pydantic-monty",
30
+ "mcp[cli]>=1.10.1"
31
+ ]
32
+ observability = [
33
+ "fastapi[standard]>=0.115.14",
34
+ "selenium>=4.34.0"
35
+ ]
36
+ tools = [
37
+ "fastapi[standard]>=0.115.14"
38
+ ]
39
+ all = [
40
+ "mcp[cli]>=1.10.1",
41
+ "fastapi[standard]>=0.115.14",
42
+ "selenium>=4.34.0",
43
+ "duckdb>=1.3.1",
44
+ "pydantic-monty"
45
+ ]
46
+
47
+ [project.scripts]
48
+ lkr = "lkr.main:app"
49
+
50
+ [build-system]
51
+ requires = ["hatchling"]
52
+ build-backend = "hatchling.build"
53
+
54
+ [dependency-groups]
55
+ dev = [
56
+ "pytest>=8.4.1",
57
+ "ruff>=0.12.1",
58
+ ]
59
+
60
+ [tool.hatch.build.targets.wheel]
61
+ packages = ["lkr"]
62
+
63
+ [[tool.uv.index]]
64
+ name = "pypi"
65
+ url = "https://pypi.org/simple"
66
+ default = true
67
+
@@ -0,0 +1,18 @@
1
+ from lkr.codemode.main import run_python_code
2
+
3
+ code = """
4
+ me_obj = me()
5
+ print("Name:", me_obj["first_name"], me_obj["last_name"])
6
+ personal_folder = folder(me_obj["personal_folder_id"])
7
+ print("Folders:")
8
+ for f in folder_children(personal_folder["id"]):
9
+ print(" - " + f["name"])
10
+ print("Dashboards:")
11
+ for d in personal_folder["dashboards"]:
12
+ print(" - " + d["title"])
13
+ print("Looks:")
14
+ for l in personal_folder["looks"]:
15
+ print(" - " + l["title"])
16
+ """
17
+
18
+ print(run_python_code(code))
@@ -0,0 +1,28 @@
1
+ from lkr.codemode.main import run_python_code
2
+
3
+ # Find me all my dashboards and looks within all my personal folder and nested folders
4
+
5
+ code = """
6
+ me_obj = me()
7
+ personal_folder_id = me_obj["personal_folder_id"]
8
+
9
+ def print_folder(folder_id, indent):
10
+ f = folder(folder_id)
11
+ print(indent + "+ Folder: " + f["name"])
12
+
13
+ if "dashboards" in f and f["dashboards"]:
14
+ for d in f["dashboards"]:
15
+ print(indent + " - Dashboard: " + d["title"])
16
+
17
+ if "looks" in f and f["looks"]:
18
+ for l in f["looks"]:
19
+ print(indent + " - Look: " + l["title"])
20
+
21
+ children = folder_children(folder_id)
22
+ for child in children:
23
+ print_folder(child["id"], indent + " ")
24
+
25
+ print_folder(personal_folder_id, "")
26
+ """
27
+
28
+ print(run_python_code(code))
@@ -397,29 +397,27 @@ name = "lkr-dev-cli"
397
397
  version = "0.0.0"
398
398
  source = { editable = "." }
399
399
  dependencies = [
400
+ { name = "cryptography" },
400
401
  { name = "looker-sdk" },
401
402
  { name = "pydantic" },
402
403
  { name = "pydash" },
404
+ { name = "questionary" },
405
+ { name = "requests" },
406
+ { name = "structlog" },
407
+ { name = "typer" },
403
408
  ]
404
409
 
405
410
  [package.optional-dependencies]
406
411
  all = [
407
- { name = "cryptography" },
408
412
  { name = "duckdb" },
409
413
  { name = "fastapi", extra = ["standard"] },
410
414
  { name = "mcp", extra = ["cli"] },
411
- { name = "questionary" },
412
- { name = "requests" },
415
+ { name = "pydantic-monty" },
413
416
  { name = "selenium" },
414
- { name = "structlog" },
415
- { name = "typer" },
416
417
  ]
417
- cli = [
418
- { name = "cryptography" },
419
- { name = "questionary" },
420
- { name = "requests" },
421
- { name = "structlog" },
422
- { name = "typer" },
418
+ codemode = [
419
+ { name = "mcp", extra = ["cli"] },
420
+ { name = "pydantic-monty" },
423
421
  ]
424
422
  mcp = [
425
423
  { name = "duckdb" },
@@ -442,36 +440,34 @@ dev = [
442
440
 
443
441
  [package.metadata]
444
442
  requires-dist = [
445
- { name = "cryptography", marker = "extra == 'all'", specifier = ">=42.0.0" },
446
- { name = "cryptography", marker = "extra == 'cli'", specifier = ">=42.0.0" },
447
- { name = "duckdb", marker = "extra == 'all'", specifier = ">=1.2.2" },
448
- { name = "duckdb", marker = "extra == 'mcp'", specifier = ">=1.2.2" },
449
- { name = "fastapi", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.115.12" },
450
- { name = "fastapi", extras = ["standard"], marker = "extra == 'mcp'", specifier = ">=0.115.12" },
451
- { name = "fastapi", extras = ["standard"], marker = "extra == 'observability'", specifier = ">=0.115.12" },
452
- { name = "fastapi", extras = ["standard"], marker = "extra == 'tools'", specifier = ">=0.115.12" },
453
- { name = "looker-sdk", specifier = ">=25.4.0" },
454
- { name = "mcp", extras = ["cli"], marker = "extra == 'all'", specifier = ">=1.9.2" },
455
- { name = "mcp", extras = ["cli"], marker = "extra == 'mcp'", specifier = ">=1.9.2" },
456
- { name = "pydantic", specifier = ">=2.11.4" },
443
+ { name = "cryptography", specifier = ">=45.0.4" },
444
+ { name = "duckdb", marker = "extra == 'all'", specifier = ">=1.3.1" },
445
+ { name = "duckdb", marker = "extra == 'mcp'", specifier = ">=1.3.1" },
446
+ { name = "fastapi", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.115.14" },
447
+ { name = "fastapi", extras = ["standard"], marker = "extra == 'mcp'", specifier = ">=0.115.14" },
448
+ { name = "fastapi", extras = ["standard"], marker = "extra == 'observability'", specifier = ">=0.115.14" },
449
+ { name = "fastapi", extras = ["standard"], marker = "extra == 'tools'", specifier = ">=0.115.14" },
450
+ { name = "looker-sdk", specifier = ">=25.10.0" },
451
+ { name = "mcp", extras = ["cli"], marker = "extra == 'all'", specifier = ">=1.10.1" },
452
+ { name = "mcp", extras = ["cli"], marker = "extra == 'codemode'", specifier = ">=1.10.1" },
453
+ { name = "mcp", extras = ["cli"], marker = "extra == 'mcp'", specifier = ">=1.10.1" },
454
+ { name = "pydantic", specifier = ">=2.11.7" },
455
+ { name = "pydantic-monty", marker = "extra == 'all'" },
456
+ { name = "pydantic-monty", marker = "extra == 'codemode'" },
457
457
  { name = "pydash", specifier = ">=8.0.5" },
458
- { name = "questionary", marker = "extra == 'all'", specifier = ">=2.1.0" },
459
- { name = "questionary", marker = "extra == 'cli'", specifier = ">=2.1.0" },
460
- { name = "requests", marker = "extra == 'all'", specifier = ">=2.31.0" },
461
- { name = "requests", marker = "extra == 'cli'", specifier = ">=2.31.0" },
462
- { name = "selenium", marker = "extra == 'all'", specifier = ">=4.32.0" },
463
- { name = "selenium", marker = "extra == 'observability'", specifier = ">=4.32.0" },
464
- { name = "structlog", marker = "extra == 'all'", specifier = ">=25.3.0" },
465
- { name = "structlog", marker = "extra == 'cli'", specifier = ">=25.3.0" },
466
- { name = "typer", marker = "extra == 'all'", specifier = ">=0.15.2" },
467
- { name = "typer", marker = "extra == 'cli'", specifier = ">=0.15.2" },
468
- ]
469
- provides-extras = ["cli", "mcp", "observability", "tools", "all"]
458
+ { name = "questionary", specifier = ">=2.1.0" },
459
+ { name = "requests", specifier = ">=2.32.4" },
460
+ { name = "selenium", marker = "extra == 'all'", specifier = ">=4.34.0" },
461
+ { name = "selenium", marker = "extra == 'observability'", specifier = ">=4.34.0" },
462
+ { name = "structlog", specifier = ">=25.4.0" },
463
+ { name = "typer", specifier = ">=0.16.0" },
464
+ ]
465
+ provides-extras = ["mcp", "codemode", "observability", "tools", "all"]
470
466
 
471
467
  [package.metadata.requires-dev]
472
468
  dev = [
473
- { name = "pytest", specifier = ">=8.3.4" },
474
- { name = "ruff", specifier = ">=0.11.7" },
469
+ { name = "pytest", specifier = ">=8.4.1" },
470
+ { name = "ruff", specifier = ">=0.12.1" },
475
471
  ]
476
472
 
477
473
  [[package]]
@@ -683,6 +679,53 @@ wheels = [
683
679
  { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 },
684
680
  ]
685
681
 
682
+ [[package]]
683
+ name = "pydantic-monty"
684
+ version = "0.0.14"
685
+ source = { registry = "https://pypi.org/simple" }
686
+ dependencies = [
687
+ { name = "typing-extensions" },
688
+ ]
689
+ sdist = { url = "https://files.pythonhosted.org/packages/45/ef/e192b10c20ca616098e694c3d890d34390aad709dcee432b1467a2187566/pydantic_monty-0.0.14.tar.gz", hash = "sha256:6c2529f307fc20f0bd97f0c12ad2d746086ac4c98b62d7dc789dd0b4fbbd0fa6", size = 981576 }
690
+ wheels = [
691
+ { url = "https://files.pythonhosted.org/packages/ec/fe/9702e8714d2a3bfa50cc72b5148513c08e1ebbaaed9e7f01d0e0075ac7f2/pydantic_monty-0.0.14-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8d6f10621888e58dab658e68be9957218164ffe8948d015fefeafc8ba9facc08", size = 7312431 },
692
+ { url = "https://files.pythonhosted.org/packages/1f/bd/22dab0664939b45748972df4ed3665a1e67d0276be48afd55b5c72aa064f/pydantic_monty-0.0.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5615e1b04bc0875d4f302965b9acfe878b4b3a3563aff30beab077daf29c74d8", size = 7307878 },
693
+ { url = "https://files.pythonhosted.org/packages/23/29/97d33ff5c95a559cf2a6d55426026de16b3b9feec6e6352545b63c0cac9d/pydantic_monty-0.0.14-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dfe535449b2a0b3257b87d66cfe9b3f4488a0d6e002424720f5b5592e88e412b", size = 7818636 },
694
+ { url = "https://files.pythonhosted.org/packages/1e/c1/7ea63a9997ddcff20240936308222c908ed410dc2eb01eeb3976a00004ec/pydantic_monty-0.0.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae9be17e9a8b4fd20c0e43b0bea9f7d83ed3e3800cde866cc490a077c17fbbbf", size = 7112021 },
695
+ { url = "https://files.pythonhosted.org/packages/8a/49/62f38c595100e483ec0aa0001f4eedc025818eed8fc892b1d34344017b80/pydantic_monty-0.0.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a282d30d0a0c309dca2d4ccf9b5258878958e49c096c6396522703a6d21c8ef", size = 7403717 },
696
+ { url = "https://files.pythonhosted.org/packages/2b/5b/ae6ca0987ee4676c34145401d417c7d375e918e73d41f5ed35a741aa85b4/pydantic_monty-0.0.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63e6339c89e33ea3d91e32d1e454277eb1f9f5687148cd97f9975ec07599ab5e", size = 7923295 },
697
+ { url = "https://files.pythonhosted.org/packages/c9/9a/58829ce58a2dc89ea02c0ac25b3b93b60fdd70a855c1dd055574585d9f7d/pydantic_monty-0.0.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa566cf8926ad12290d5fc5d75e63f87e36e683b756e868510d5542f255966f8", size = 8146698 },
698
+ { url = "https://files.pythonhosted.org/packages/13/5d/f0d95397fd9a52d8841a7971c4de18950517f89e769219911fdad1e0baa1/pydantic_monty-0.0.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a84930c1c67c0b5783adc17eb9f7aaabbbf85f0eda261eb0b39a7cab3d9b4d4", size = 7703857 },
699
+ { url = "https://files.pythonhosted.org/packages/fa/6b/dcfbe9c590d571869d8df718444347d995d91423b13c77af3f42a88e2253/pydantic_monty-0.0.14-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e042f8eaaddf2b08202766e1ac20ea2032f3ed0714ad5d64f2b4d104e4842501", size = 7292375 },
700
+ { url = "https://files.pythonhosted.org/packages/b5/5e/9c85dbfaa7c29de82e85f605ae9be7ff0d0439b04ec1fad5fd139e8c5623/pydantic_monty-0.0.14-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40d1e659fdf76a259cc4a70eaaf081556d3a34a34191a3d1fccadeb8196478dc", size = 7735590 },
701
+ { url = "https://files.pythonhosted.org/packages/74/13/da8c76d8e96070068adf947df731187c2c856b1a26faf74106601b9c29b9/pydantic_monty-0.0.14-cp312-cp312-win32.whl", hash = "sha256:cb86a22f70942118c6e1ae89304f14ba804f2460de9b33a3c4773c08ed1fce2b", size = 7189537 },
702
+ { url = "https://files.pythonhosted.org/packages/68/80/c584d2fa2c2cfcae81504435ee592c2d8ed34965061c816ce4a3ed3925b9/pydantic_monty-0.0.14-cp312-cp312-win_amd64.whl", hash = "sha256:d811554f3d46652c941cb839d07290eaebca4b771fecf149aa61d47fa77fbaf4", size = 7952530 },
703
+ { url = "https://files.pythonhosted.org/packages/01/24/279f8a2e6f7af7b05f52e37806c060078e8c4e3a81de6a57e1f2952a62cb/pydantic_monty-0.0.14-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:43298234981ad055ba80238229f1e86a6be957e9c703275f716c766a2c60f28e", size = 7311901 },
704
+ { url = "https://files.pythonhosted.org/packages/09/18/474cf9e663bdcd241b3b089144ab939b91182e86fa553dc5949d7aa55fdc/pydantic_monty-0.0.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:281e5a0367faaccbbdfc6d2335cce37068f6d1a1ec443237d26e1e21268f9e9a", size = 7307967 },
705
+ { url = "https://files.pythonhosted.org/packages/43/99/17987aeeabb3f22cbc260c0f75ca6b74de82ed6613ac56e1b231af8d2ac0/pydantic_monty-0.0.14-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c3d18ab1b5997edc1a081576518b87d08d94f02d1483a0fed648282eba304fe4", size = 7818387 },
706
+ { url = "https://files.pythonhosted.org/packages/5c/6d/7c1d5da18da51c45eac520ae6d05859c3bfad37fd548e5da09ce461081f5/pydantic_monty-0.0.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a3bbb594aa33e45589283078c7ee72aeacf66ccfd3714800b0f0b75ce5ffc1e", size = 7111299 },
707
+ { url = "https://files.pythonhosted.org/packages/02/56/90c1c354c0c5a6266625122b419aa7cdd447a907c794001c5abd234ae7f4/pydantic_monty-0.0.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f1fdf67a21d0d66fb8063d253220dc79536f6ff192058440fa30956bfdd5581b", size = 7403680 },
708
+ { url = "https://files.pythonhosted.org/packages/21/98/75f6da7405f4927c772b4a98cae7aea0cafc7247892000b5fbcd107fe16a/pydantic_monty-0.0.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec5a2305b6b38756edf7be6a67e650efcc4c56a8e93138602ad11ddb43d5036b", size = 7922586 },
709
+ { url = "https://files.pythonhosted.org/packages/0f/e9/dfff51f7cd405a7a6bbdbc707752693dd82d67d94cb4da22d42b8dbf85c3/pydantic_monty-0.0.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dfcbc3363d93dfbfc7fcacd740f2d7e652f4b22c179816795007b85a0a0ff6", size = 8146441 },
710
+ { url = "https://files.pythonhosted.org/packages/a0/e9/5a4b4e6ff73b73f116661ae4e2d7343f4e6f29940584949950a3e84bf467/pydantic_monty-0.0.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b5c5eaf7a6b4620488fe11c211f0a34c6b600857470f3dccd9097d7c52b371", size = 7703282 },
711
+ { url = "https://files.pythonhosted.org/packages/ce/4e/9c41abc7646f20c0bb90814276052939e43eb24692b125f634b6620d732a/pydantic_monty-0.0.14-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f90496ab30bc5910def0cb3c55b53b6c5a98ccffdbbc6766661552cf64579ab7", size = 7291952 },
712
+ { url = "https://files.pythonhosted.org/packages/de/34/8766903b8ea6b95173e5b2525eadf011d153e941a346af8b289d0167c8ca/pydantic_monty-0.0.14-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1f6740d535578a828ce72a664bd1f3a6e4f906f156b06c3735b9bdcdf3104f40", size = 7733595 },
713
+ { url = "https://files.pythonhosted.org/packages/36/59/af632d8f7f38224ac04eb76b49152c7c378ce0d0966144737698713d0a77/pydantic_monty-0.0.14-cp313-cp313-win32.whl", hash = "sha256:2bb211fadba4ab898d5cf8a1622d88179d5b07633d00be0ac7168913d76c1cd9", size = 7188687 },
714
+ { url = "https://files.pythonhosted.org/packages/08/6f/5da7e21c79bf93004d26b1a1fd61407bee3852d7a398e33738a9ac72a079/pydantic_monty-0.0.14-cp313-cp313-win_amd64.whl", hash = "sha256:679978dccd92551111061cfbee7f7b475447217a7656afcb3944095c0c1f0cfb", size = 7952562 },
715
+ { url = "https://files.pythonhosted.org/packages/c2/59/838b5c98ce25a1eb845c83cfd9aa70f3d22da60da5eb72b27cb1ebed5ae1/pydantic_monty-0.0.14-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8c11d75377fae113a42c7a0fec2bc31a4896f2452f283e5866f76e4dd9996950", size = 7313228 },
716
+ { url = "https://files.pythonhosted.org/packages/fd/88/7862783b3dc1de24390226ea31d5383cab011918865474681e16abede32b/pydantic_monty-0.0.14-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e8d0105672b034e32f1bb5010669e4b4d0b4412f77ceb120868d9e8836a23ee", size = 7321140 },
717
+ { url = "https://files.pythonhosted.org/packages/74/2f/a330b2fa485870eb35be655b13d18ae6e06bbefadf062a55ee96ac16d065/pydantic_monty-0.0.14-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:85b3779579eebb0425954fc4c4d33b38a007c8b432503678073787494d79f185", size = 7818570 },
718
+ { url = "https://files.pythonhosted.org/packages/5a/fd/d951afa215575c7bd43d2a6e6dc7c209d2de5be69e0966a8d8e67fdba6d2/pydantic_monty-0.0.14-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e0e493b08b9f154400c4ede998a5e41a54258a6ace7cb99b55827c354e11077", size = 7111698 },
719
+ { url = "https://files.pythonhosted.org/packages/3b/e6/c136182b6bff61b4e37b257bf893fd4f4e036fae42625d9163425cef4228/pydantic_monty-0.0.14-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6e3ed73986606c3f3ee330ff53a615dbdc0840b48960b547d0b74754376e7e7", size = 7405251 },
720
+ { url = "https://files.pythonhosted.org/packages/09/83/4e65cce2d178e61a658e0c182946e6d93277c6cb8b2428db927ee740ac31/pydantic_monty-0.0.14-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bad627a02fc0e6b69888e609915c6ffe814d7225f3effcb201bae9e0c2a6023f", size = 7923563 },
721
+ { url = "https://files.pythonhosted.org/packages/9a/7d/f5195d701098f9da604ff26d80dc86d00f6a644df6c57a91dd452ffd3dad/pydantic_monty-0.0.14-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34691bf8f558208cf513ea30bd76b0ef7821a06ba32525024fd28d1a5bf7ed4b", size = 8147677 },
722
+ { url = "https://files.pythonhosted.org/packages/bf/a4/564b603d8494d971ffebec3806fa17e51e3bec1c2849f3898bc1955f6d64/pydantic_monty-0.0.14-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18bd7e61a12fde23f3dde5561f3d8da101a33bd53621a3ccd40686dc827a98f3", size = 7715534 },
723
+ { url = "https://files.pythonhosted.org/packages/61/8e/639fb9179f39fa2e6de88b85146ab987f70e5f7cc5e8588368337537d9e4/pydantic_monty-0.0.14-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a7d0911d260347266015edde71d804fdd48f5dc5002f8e22546e8b4d5436d588", size = 7291939 },
724
+ { url = "https://files.pythonhosted.org/packages/3b/8e/950e42985689900d77f4df51213171adfcbffaf4137b322dbfd96ed3a6f9/pydantic_monty-0.0.14-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:aca052a3acf9b66fd713b44c43be4944b92c0c2eac2641dd230ea4258758bb6c", size = 7735299 },
725
+ { url = "https://files.pythonhosted.org/packages/59/40/de590213a28c769c46d78843aed8fbe9665eceabb275cf7cc414183659a6/pydantic_monty-0.0.14-cp314-cp314-win32.whl", hash = "sha256:c913b2a99fef193b3ee948885d04bb9c44035514125dc3e765167fa65d349303", size = 7191124 },
726
+ { url = "https://files.pythonhosted.org/packages/bd/a5/b5d589aa2d9d7ed1fa1c181beacaa6bdf8c13be0117f610947723236c2b2/pydantic_monty-0.0.14-cp314-cp314-win_amd64.whl", hash = "sha256:9b59d0534cca8b75a53e4386c6098116aaf969e9c4164e093696519f9382d5e9", size = 7967367 },
727
+ ]
728
+
686
729
  [[package]]
687
730
  name = "pydantic-settings"
688
731
  version = "2.10.1"
@@ -1,7 +0,0 @@
1
- .PHONY: docs test-deps
2
-
3
- docs:
4
- typer lkr/main.py utils docs --output lkr.md
5
-
6
- test-deps:
7
- python tests/test_dependency_resolution.py
@@ -1,56 +0,0 @@
1
- [project]
2
- name = "lkr-dev-cli"
3
- version = "0.0.33"
4
- description = "lkr: a command line interface for looker"
5
- readme = "README.md"
6
- license = "MIT"
7
- authors = [
8
- { name = "bwebs"}
9
- ]
10
- requires-python = ">=3.12"
11
- dependencies = [
12
- "looker-sdk>=25.4.0",
13
- "pydantic>=2.11.4",
14
- "pydash>=8.0.5",
15
- "typer>=0.15.2",
16
- "requests>=2.31.0",
17
- "cryptography>=42.0.0",
18
- "structlog>=25.3.0",
19
- "questionary>=2.1.0"
20
- ]
21
-
22
- [project.optional-dependencies]
23
- mcp = [
24
- "mcp[cli]>=1.9.2",
25
- "duckdb>=1.2.2",
26
- "fastapi[standard]>=0.115.12"
27
- ]
28
- observability = [
29
- "fastapi[standard]>=0.115.12",
30
- "selenium>=4.32.0"
31
- ]
32
- tools = [
33
- "fastapi[standard]>=0.115.12"
34
- ]
35
- all = [
36
- "mcp[cli]>=1.9.2",
37
- "fastapi[standard]>=0.115.12",
38
- "selenium>=4.32.0",
39
- "duckdb>=1.2.2"
40
- ]
41
-
42
- [project.scripts]
43
- lkr = "lkr.main:app"
44
-
45
- [build-system]
46
- requires = ["hatchling"]
47
- build-backend = "hatchling.build"
48
-
49
- [dependency-groups]
50
- dev = [
51
- "pytest>=8.3.4",
52
- "ruff>=0.11.7",
53
- ]
54
-
55
- [tool.hatch.build.targets.wheel]
56
- packages = ["lkr"]
File without changes
File without changes
File without changes