lkr-dev-cli 0.0.36__tar.gz → 0.0.37__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/Makefile +4 -4
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/PKG-INFO +2 -1
- lkr_dev_cli-0.0.37/lkr/codemode/constant.py +1 -0
- lkr_dev_cli-0.0.37/lkr/codemode/download_swagger.py +49 -0
- lkr_dev_cli-0.0.37/lkr/codemode/examples.py +24 -0
- lkr_dev_cli-0.0.37/lkr/codemode/help.py +210 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/codemode/main.py +28 -66
- lkr_dev_cli-0.0.37/lkr/codemode/readme.py +58 -0
- lkr_dev_cli-0.0.37/lkr/codemode/swagger.json +50246 -0
- lkr_dev_cli-0.0.37/lkr/codemode/type.py +92 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/pyproject.toml +2 -1
- lkr_dev_cli-0.0.37/tests/test_codemode.py +136 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/uv.lock +2 -0
- lkr_dev_cli-0.0.36/tests/test_codemode.py +0 -18
- lkr_dev_cli-0.0.36/tests/test_codemode2.py +0 -28
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/.github/workflows/release.yml +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/.github/workflows/test-dependencies.yml +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/.gitignore +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/.python-version +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/.vscode/launch.json +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/.vscode/settings.json +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/Dockerfile +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/LICENSE +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/README.md +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/cloudbuild.yaml +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/codemode.md +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/__init__.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/auth/__init__.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/auth/main.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/auth/oauth.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/auth_service.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/classes.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/codemode/LOCAL.md +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/codemode/__init__.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/constants.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/custom_types.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/exceptions.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/logger.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/main.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/mcp/classes.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/mcp/main.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/mcp/utils.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/observability/classes.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/observability/embed_container.html +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/observability/main.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/observability/utils.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/tools/classes.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/tools/main.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/tools/permission_deprecation.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr.md +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/tests/TESTING.md +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/tests/test_dependency_resolution.py +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/tests/test_deps.sh +0 -0
- {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/tests/test_permission_deprecation.py +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.PHONY: docs test-deps codemode-test codemode-
|
|
1
|
+
.PHONY: docs test-deps codemode-test codemode-start
|
|
2
2
|
|
|
3
3
|
docs:
|
|
4
4
|
uv run typer lkr/main.py utils docs --output lkr.md
|
|
@@ -7,11 +7,11 @@ test-deps:
|
|
|
7
7
|
python tests/test_dependency_resolution.py
|
|
8
8
|
|
|
9
9
|
codemode-test:
|
|
10
|
-
uv run
|
|
10
|
+
uv run pytest tests/test_codemode.py
|
|
11
11
|
|
|
12
|
-
codemode-test2:
|
|
13
|
-
uv run python tests/test_codemode2.py
|
|
14
12
|
|
|
13
|
+
download-swagger:
|
|
14
|
+
uv run python lkr/codemode/download_swagger.py
|
|
15
15
|
|
|
16
16
|
codemode-start:
|
|
17
17
|
@echo "Add this to your mcpServers config:"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lkr-dev-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.37
|
|
4
4
|
Summary: lkr: a command line interface for looker
|
|
5
5
|
Author: bwebs
|
|
6
6
|
License-Expression: MIT
|
|
@@ -10,6 +10,7 @@ Requires-Dist: cryptography>=45.0.4
|
|
|
10
10
|
Requires-Dist: looker-sdk>=25.10.0
|
|
11
11
|
Requires-Dist: pydantic>=2.11.7
|
|
12
12
|
Requires-Dist: pydash>=8.0.5
|
|
13
|
+
Requires-Dist: python-dotenv>=1.1.1
|
|
13
14
|
Requires-Dist: questionary>=2.1.0
|
|
14
15
|
Requires-Dist: requests>=2.32.4
|
|
15
16
|
Requires-Dist: structlog>=25.4.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
EXCLUDED_FUNCS = ['dir', 'help', 'examples', 'readme', 'lookup', 'sdk', 'search_with_lookups', 'lookup_type']
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def download_swagger():
|
|
9
|
+
base_url = os.environ.get("LOOKERSDK_BASE_URL")
|
|
10
|
+
|
|
11
|
+
if not base_url:
|
|
12
|
+
# Try to load from .env in project root
|
|
13
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
14
|
+
env_path = os.path.join(current_dir, "..", "..", ".env")
|
|
15
|
+
if os.path.exists(env_path):
|
|
16
|
+
print(f"Loading environment from {env_path}")
|
|
17
|
+
load_dotenv(env_path)
|
|
18
|
+
base_url = os.environ.get("LOOKERSDK_BASE_URL")
|
|
19
|
+
|
|
20
|
+
if not base_url:
|
|
21
|
+
print("Error: LOOKERSDK_BASE_URL environment variable not set.", file=sys.stderr)
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
url = f"{base_url.rstrip('/')}/api/4.0/swagger.json"
|
|
25
|
+
output_dir = os.path.dirname(os.path.abspath(__file__))
|
|
26
|
+
output_path = os.path.join(output_dir, "swagger.json")
|
|
27
|
+
|
|
28
|
+
print(f"Downloading swagger.json from {url} ...")
|
|
29
|
+
try:
|
|
30
|
+
response = requests.get(url, timeout=10)
|
|
31
|
+
response.raise_for_status()
|
|
32
|
+
|
|
33
|
+
# Verify it's JSON
|
|
34
|
+
try:
|
|
35
|
+
swagger_data = response.json()
|
|
36
|
+
except ValueError:
|
|
37
|
+
print("Error: Response is not valid JSON.", file=sys.stderr)
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
with open(output_path, "w") as f:
|
|
41
|
+
json.dump(swagger_data, f, indent=2)
|
|
42
|
+
|
|
43
|
+
print(f"Saved swagger.json to {output_path}")
|
|
44
|
+
except requests.RequestException as e:
|
|
45
|
+
print(f"Error downloading swagger.json: {e}", file=sys.stderr)
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
download_swagger()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
EXAMPLES = [
|
|
2
|
+
[
|
|
3
|
+
"Find all dashboard-related methods",
|
|
4
|
+
"return [m for m in dir() if 'dashboard' in m.lower()]"
|
|
5
|
+
],
|
|
6
|
+
[
|
|
7
|
+
"Get the description of a specific method",
|
|
8
|
+
"return help('search_dashboards')"
|
|
9
|
+
],
|
|
10
|
+
[
|
|
11
|
+
"List personal dashboards",
|
|
12
|
+
"""def get_all_items(folder_id):
|
|
13
|
+
f = folder(folder_id)
|
|
14
|
+
items = {"dashboards": f.get("dashboards", []), "looks": f.get("looks", [])}
|
|
15
|
+
for child in folder_children(folder_id):
|
|
16
|
+
child_items = get_all_items(child["id"])
|
|
17
|
+
items["dashboards"].extend(child_items["dashboards"])
|
|
18
|
+
items["looks"].extend(child_items["looks"])
|
|
19
|
+
return items
|
|
20
|
+
|
|
21
|
+
me_data = me()
|
|
22
|
+
return get_all_items(me_data["personal_folder_id"])"""
|
|
23
|
+
]
|
|
24
|
+
]
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import inspect
|
|
3
|
+
import fnmatch
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from lkr.codemode.constant import EXCLUDED_FUNCS
|
|
7
|
+
|
|
8
|
+
_operation_map = None
|
|
9
|
+
|
|
10
|
+
def _get_operation_map():
|
|
11
|
+
global _operation_map
|
|
12
|
+
if _operation_map is not None:
|
|
13
|
+
return _operation_map
|
|
14
|
+
|
|
15
|
+
_operation_map = {}
|
|
16
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
17
|
+
swagger_path = os.path.join(current_dir, 'swagger.json')
|
|
18
|
+
|
|
19
|
+
if not os.path.exists(swagger_path):
|
|
20
|
+
return _operation_map
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
with open(swagger_path, 'r') as f:
|
|
24
|
+
swagger = json.load(f)
|
|
25
|
+
paths = swagger.get('paths', {})
|
|
26
|
+
for path, path_item in paths.items():
|
|
27
|
+
for method, op in path_item.items():
|
|
28
|
+
if isinstance(op, dict) and 'operationId' in op:
|
|
29
|
+
op_id = op['operationId']
|
|
30
|
+
summary = op.get('summary', '')
|
|
31
|
+
description = op.get('description', '')
|
|
32
|
+
if description.startswith('###'):
|
|
33
|
+
description = description.lstrip('#').strip()
|
|
34
|
+
_operation_map[op_id] = {
|
|
35
|
+
'summary': summary,
|
|
36
|
+
'description': description
|
|
37
|
+
}
|
|
38
|
+
except (OSError, json.JSONDecodeError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
return _operation_map
|
|
42
|
+
|
|
43
|
+
def _get_enhanced_doc(name: str, method) -> str:
|
|
44
|
+
doc = method.__doc__ or ""
|
|
45
|
+
op_map = _get_operation_map()
|
|
46
|
+
if name in op_map:
|
|
47
|
+
op_info = op_map[name]
|
|
48
|
+
summary = op_info['summary']
|
|
49
|
+
description = op_info['description']
|
|
50
|
+
|
|
51
|
+
parts = []
|
|
52
|
+
if summary:
|
|
53
|
+
parts.append(summary)
|
|
54
|
+
if description:
|
|
55
|
+
parts.append(description)
|
|
56
|
+
|
|
57
|
+
if parts:
|
|
58
|
+
return "\n".join(parts)
|
|
59
|
+
|
|
60
|
+
return doc
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _get_matches(query: str, external_funcs: dict, sdk) -> list:
|
|
64
|
+
"""Helper to get matching functions with hit details."""
|
|
65
|
+
escaped_query = re.escape(query).replace(r'\*', '.*').replace(r'\?', '.')
|
|
66
|
+
try:
|
|
67
|
+
pattern = re.compile(escaped_query, re.IGNORECASE)
|
|
68
|
+
except re.error:
|
|
69
|
+
pattern = re.compile(re.escape(query), re.IGNORECASE)
|
|
70
|
+
|
|
71
|
+
matches = []
|
|
72
|
+
for name in external_funcs:
|
|
73
|
+
if name in EXCLUDED_FUNCS:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
doc = ""
|
|
77
|
+
method = None
|
|
78
|
+
if hasattr(sdk, name):
|
|
79
|
+
method = getattr(sdk, name)
|
|
80
|
+
doc = _get_enhanced_doc(name, method)
|
|
81
|
+
|
|
82
|
+
hit_in_name = bool(pattern.search(name))
|
|
83
|
+
|
|
84
|
+
# Search in docstring lines
|
|
85
|
+
matching_lines = []
|
|
86
|
+
if doc:
|
|
87
|
+
for line in doc.split('\n'):
|
|
88
|
+
if pattern.search(line):
|
|
89
|
+
matching_lines.append(line.strip())
|
|
90
|
+
|
|
91
|
+
# Search in parameters (Inputs)
|
|
92
|
+
hit_in_params = False
|
|
93
|
+
if method:
|
|
94
|
+
try:
|
|
95
|
+
sig = inspect.signature(method)
|
|
96
|
+
params = list(sig.parameters.keys())
|
|
97
|
+
if any(pattern.search(p) for p in params):
|
|
98
|
+
hit_in_params = True
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
# Search in return type fields (Outputs)
|
|
103
|
+
hit_in_output = False
|
|
104
|
+
output_fields = []
|
|
105
|
+
if method:
|
|
106
|
+
try:
|
|
107
|
+
sig = inspect.signature(method)
|
|
108
|
+
return_type = sig.return_annotation
|
|
109
|
+
if return_type and hasattr(return_type, '__annotations__'):
|
|
110
|
+
fields = list(return_type.__annotations__.keys())
|
|
111
|
+
matching_fields = [f for f in fields if pattern.search(f)]
|
|
112
|
+
if matching_fields:
|
|
113
|
+
hit_in_output = True
|
|
114
|
+
output_fields = matching_fields
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
if hit_in_name or matching_lines or hit_in_params or hit_in_output:
|
|
119
|
+
matches.append({
|
|
120
|
+
'name': name,
|
|
121
|
+
'hit_in_name': hit_in_name,
|
|
122
|
+
'matching_lines': matching_lines,
|
|
123
|
+
'hit_in_params': hit_in_params,
|
|
124
|
+
'output_fields': output_fields
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
return matches
|
|
128
|
+
|
|
129
|
+
def search_help(query: str, external_funcs: dict, sdk) -> str:
|
|
130
|
+
"""Search for functions and return a summary string with snippets."""
|
|
131
|
+
matches = _get_matches(query, external_funcs, sdk)
|
|
132
|
+
if not matches:
|
|
133
|
+
return f"No matches found for '{query}'."
|
|
134
|
+
|
|
135
|
+
lines = []
|
|
136
|
+
for m in matches:
|
|
137
|
+
hit_info = []
|
|
138
|
+
if m['hit_in_name']:
|
|
139
|
+
hit_info.append("Name match")
|
|
140
|
+
if m['matching_lines']:
|
|
141
|
+
hit_info.append(f"Doc hit: \"{m['matching_lines'][0]}\"")
|
|
142
|
+
if m['hit_in_params']:
|
|
143
|
+
hit_info.append("Input param match")
|
|
144
|
+
if m['output_fields']:
|
|
145
|
+
hit_info.append(f"Output field match: {', '.join(m['output_fields'][:2])}")
|
|
146
|
+
|
|
147
|
+
lines.append(f"- {m['name']} ({' | '.join(hit_info)})")
|
|
148
|
+
|
|
149
|
+
return f"Matches found for '{query}':\n" + "\n".join(lines)
|
|
150
|
+
|
|
151
|
+
def search_with_lookups(query: str, external_funcs: dict, sdk) -> list:
|
|
152
|
+
"""Search for functions and return the array of lookups for matches."""
|
|
153
|
+
matches = _get_matches(query, external_funcs, sdk)
|
|
154
|
+
results = []
|
|
155
|
+
for m in matches:
|
|
156
|
+
results.append(lookup_function(m['name'], external_funcs, sdk))
|
|
157
|
+
return results
|
|
158
|
+
|
|
159
|
+
def lookup_function(name: str, external_funcs: dict, sdk) -> str:
|
|
160
|
+
"""Look up the exact name of a function and return its docstring, inputs, and outputs."""
|
|
161
|
+
if name not in external_funcs:
|
|
162
|
+
return f"Function '{name}' not found."
|
|
163
|
+
|
|
164
|
+
if not hasattr(sdk, name):
|
|
165
|
+
return f"{name} is a built-in helper function."
|
|
166
|
+
|
|
167
|
+
method = getattr(sdk, name)
|
|
168
|
+
doc = _get_enhanced_doc(name, method) or "No docstring available."
|
|
169
|
+
|
|
170
|
+
def _get_type_str(t):
|
|
171
|
+
if t == inspect._empty:
|
|
172
|
+
return "Any"
|
|
173
|
+
if hasattr(t, '__name__'):
|
|
174
|
+
return t.__name__
|
|
175
|
+
return str(t)
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
sig = inspect.signature(method)
|
|
179
|
+
|
|
180
|
+
# Inputs
|
|
181
|
+
inputs = []
|
|
182
|
+
for param_name, param in sig.parameters.items():
|
|
183
|
+
inputs.append(f"- {param_name}: {_get_type_str(param.annotation)}")
|
|
184
|
+
inputs_str = "\n".join(inputs) if inputs else "None"
|
|
185
|
+
|
|
186
|
+
# Outputs
|
|
187
|
+
return_type = sig.return_annotation
|
|
188
|
+
outputs_str = f"Return Type: {_get_type_str(return_type)}"
|
|
189
|
+
|
|
190
|
+
if return_type and hasattr(return_type, '__annotations__'):
|
|
191
|
+
fields = []
|
|
192
|
+
for field_name, field_type in return_type.__annotations__.items():
|
|
193
|
+
fields.append(f" - {field_name}: {_get_type_str(field_type)}")
|
|
194
|
+
if fields:
|
|
195
|
+
outputs_str += "\nFields:\n" + "\n".join(fields)
|
|
196
|
+
|
|
197
|
+
return f"""
|
|
198
|
+
Function: {name}
|
|
199
|
+
|
|
200
|
+
Docstring:
|
|
201
|
+
{doc}
|
|
202
|
+
|
|
203
|
+
Inputs:
|
|
204
|
+
{inputs_str}
|
|
205
|
+
|
|
206
|
+
Outputs:
|
|
207
|
+
{outputs_str}
|
|
208
|
+
"""
|
|
209
|
+
except Exception as e:
|
|
210
|
+
return f"Function: {name}\n\nDocstring:\n{doc}\n\n(Could not inspect signature: {e})"
|
|
@@ -10,6 +10,11 @@ from mcp.server.fastmcp import FastMCP
|
|
|
10
10
|
|
|
11
11
|
from lkr.auth_service import get_auth, is_auth_expired
|
|
12
12
|
from lkr.classes import LkrCtxObj
|
|
13
|
+
from lkr.codemode.examples import EXAMPLES
|
|
14
|
+
from lkr.codemode.help import search_help, lookup_function, search_with_lookups
|
|
15
|
+
from lkr.codemode.readme import get_readme
|
|
16
|
+
from lkr.codemode.constant import EXCLUDED_FUNCS
|
|
17
|
+
from lkr.codemode.type import lookup_type
|
|
13
18
|
from lkr.logger import logger
|
|
14
19
|
|
|
15
20
|
__all__ = ["group"]
|
|
@@ -58,20 +63,26 @@ def to_primitive(obj):
|
|
|
58
63
|
|
|
59
64
|
|
|
60
65
|
@mcp.tool()
|
|
61
|
-
def run_python_code(code: str) -> str:
|
|
66
|
+
def run_python_code(code: str, dev_mode: bool = False) -> str:
|
|
62
67
|
"""
|
|
63
68
|
Execute Python code safely with access to all Looker SDK methods as global functions.
|
|
64
69
|
Capture the result.
|
|
65
70
|
|
|
66
71
|
AGENT HINTS:
|
|
67
|
-
-
|
|
72
|
+
- CRITICAL: Call `readme()` first if you haven't already to see full instructions and examples.
|
|
73
|
+
- Use `dir()`, `help('pattern')`, `lookup('method_name')`, `lookup_type('TypeName')`, and `examples()` to discover available SDK methods, types, and patterns.
|
|
68
74
|
- Do not instantiate an SDK; use global functions directly (e.g. `me()`).
|
|
75
|
+
- You can also use `sdk.method_name()` (e.g. `sdk.me()`) if preferred.
|
|
69
76
|
- Returned Looker models are primitive dictionaries (use `obj["id"]`, not `obj.id`).
|
|
70
77
|
- Return your output (avoid using print() as it may pollute the stdio stream).
|
|
71
78
|
- Recursion: Use `folder_children(id)` to traverse nested folders.
|
|
79
|
+
- Dev Mode: Set `dev_mode=True` to ensure you are in development mode before running code.
|
|
72
80
|
"""
|
|
73
81
|
try:
|
|
74
82
|
ctx = LkrCtxObj(force_oauth=False)
|
|
83
|
+
|
|
84
|
+
if dev_mode:
|
|
85
|
+
ctx.use_production = False
|
|
75
86
|
sdk = get_mcp_sdk(ctx)
|
|
76
87
|
|
|
77
88
|
external_funcs = {}
|
|
@@ -88,13 +99,21 @@ def run_python_code(code: str) -> str:
|
|
|
88
99
|
# Provide helper functions for the LLM to explore the SDK
|
|
89
100
|
external_funcs['dir'] = lambda: list(external_funcs.keys())
|
|
90
101
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
external_funcs['
|
|
102
|
+
external_funcs['help'] = lambda query: search_help(query, external_funcs, sdk)
|
|
103
|
+
external_funcs['lookup'] = lambda name: lookup_function(name, external_funcs, sdk)
|
|
104
|
+
external_funcs['search_with_lookups'] = lambda query: search_with_lookups(query, external_funcs, sdk)
|
|
105
|
+
external_funcs['lookup_type'] = lookup_type
|
|
106
|
+
|
|
107
|
+
external_funcs['examples'] = lambda: EXAMPLES
|
|
108
|
+
external_funcs['readme'] = get_readme
|
|
109
|
+
|
|
110
|
+
# Add sdk object to support sdk.method_name
|
|
111
|
+
class SDK:
|
|
112
|
+
pass
|
|
113
|
+
for name, func in external_funcs.items():
|
|
114
|
+
if name not in EXCLUDED_FUNCS:
|
|
115
|
+
setattr(SDK, name, staticmethod(func))
|
|
116
|
+
external_funcs['sdk'] = SDK
|
|
98
117
|
|
|
99
118
|
m = pydantic_monty.Monty(code)
|
|
100
119
|
|
|
@@ -129,64 +148,7 @@ def run_python_code(code: str) -> str:
|
|
|
129
148
|
return f"Error: {str(e)}"
|
|
130
149
|
|
|
131
150
|
|
|
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
151
|
|
|
186
|
-
me_data = me()
|
|
187
|
-
return get_all_items(me_data["personal_folder_id"])
|
|
188
|
-
```
|
|
189
|
-
'''
|
|
190
152
|
|
|
191
153
|
|
|
192
154
|
@group.command(name="run")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
def get_readme() -> str:
|
|
2
|
+
return """
|
|
3
|
+
Looker Code Mode - Instructions & Examples
|
|
4
|
+
|
|
5
|
+
IMPORTANT: If you have not already run this function, read these instructions carefully before writing any code.
|
|
6
|
+
|
|
7
|
+
RULES & HINTS:
|
|
8
|
+
1. **SDK Access**: All Looker SDK methods are available as global functions (e.g., `me()`) or via the `sdk` object (e.g., `sdk.me()`).
|
|
9
|
+
2. **Dict Access**: Return values are dictionaries, not objects. Use `user["id"]`, not `user.id`.
|
|
10
|
+
3. **Discovery**: Use `dir()`, `help('pattern')`, `lookup('method_name')`, `lookup_type('TypeName')`, and `examples()` to explore the SDK.
|
|
11
|
+
4. **No Imports**: Do not `import looker_sdk`.
|
|
12
|
+
5. **Output**: Return your results instead of using `print()`.
|
|
13
|
+
6. **Efficiency**: Always use the `fields` parameter (e.g., `all_dashboards(fields="id,title")`) when listing many objects to prevent timeouts.
|
|
14
|
+
7. **Nested Folders**: Use `folder_children(id)` to get sub-folders.
|
|
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
|
+
|
|
17
|
+
EXAMPLES:
|
|
18
|
+
|
|
19
|
+
Example 1: Find all dashboard-related methods (wildcard search)
|
|
20
|
+
```python
|
|
21
|
+
return help('dashboard')
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Example 2: Get the description of a specific method
|
|
25
|
+
```python
|
|
26
|
+
return lookup('search_dashboards')
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Example 3: Lookup a type definition
|
|
30
|
+
```python
|
|
31
|
+
return lookup_type('User')
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Example 4: Search and return full details for all matches
|
|
35
|
+
```python
|
|
36
|
+
return search_with_lookups('board_item')
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Example 5: List personal dashboards (Recursive traversal)
|
|
40
|
+
```python
|
|
41
|
+
def get_all_items(folder_id):
|
|
42
|
+
f = folder(folder_id)
|
|
43
|
+
items = {
|
|
44
|
+
"dashboards": f.get("dashboards", []),
|
|
45
|
+
"looks": f.get("looks", [])
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for child in folder_children(folder_id):
|
|
49
|
+
child_items = get_all_items(child["id"])
|
|
50
|
+
items["dashboards"].extend(child_items["dashboards"])
|
|
51
|
+
items["looks"].extend(child_items["looks"])
|
|
52
|
+
|
|
53
|
+
return items
|
|
54
|
+
|
|
55
|
+
me_data = me()
|
|
56
|
+
return get_all_items(me_data["personal_folder_id"])
|
|
57
|
+
```
|
|
58
|
+
"""
|