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.
Files changed (54) hide show
  1. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/Makefile +4 -4
  2. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/PKG-INFO +2 -1
  3. lkr_dev_cli-0.0.37/lkr/codemode/constant.py +1 -0
  4. lkr_dev_cli-0.0.37/lkr/codemode/download_swagger.py +49 -0
  5. lkr_dev_cli-0.0.37/lkr/codemode/examples.py +24 -0
  6. lkr_dev_cli-0.0.37/lkr/codemode/help.py +210 -0
  7. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/codemode/main.py +28 -66
  8. lkr_dev_cli-0.0.37/lkr/codemode/readme.py +58 -0
  9. lkr_dev_cli-0.0.37/lkr/codemode/swagger.json +50246 -0
  10. lkr_dev_cli-0.0.37/lkr/codemode/type.py +92 -0
  11. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/pyproject.toml +2 -1
  12. lkr_dev_cli-0.0.37/tests/test_codemode.py +136 -0
  13. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/uv.lock +2 -0
  14. lkr_dev_cli-0.0.36/tests/test_codemode.py +0 -18
  15. lkr_dev_cli-0.0.36/tests/test_codemode2.py +0 -28
  16. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/.github/workflows/release.yml +0 -0
  17. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/.github/workflows/test-dependencies.yml +0 -0
  18. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/.gitignore +0 -0
  19. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/.python-version +0 -0
  20. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/.vscode/launch.json +0 -0
  21. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/.vscode/settings.json +0 -0
  22. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/Dockerfile +0 -0
  23. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/LICENSE +0 -0
  24. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/README.md +0 -0
  25. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/cloudbuild.yaml +0 -0
  26. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/codemode.md +0 -0
  27. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/__init__.py +0 -0
  28. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/auth/__init__.py +0 -0
  29. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/auth/main.py +0 -0
  30. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/auth/oauth.py +0 -0
  31. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/auth_service.py +0 -0
  32. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/classes.py +0 -0
  33. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/codemode/LOCAL.md +0 -0
  34. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/codemode/__init__.py +0 -0
  35. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/constants.py +0 -0
  36. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/custom_types.py +0 -0
  37. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/exceptions.py +0 -0
  38. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/logger.py +0 -0
  39. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/main.py +0 -0
  40. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/mcp/classes.py +0 -0
  41. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/mcp/main.py +0 -0
  42. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/mcp/utils.py +0 -0
  43. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/observability/classes.py +0 -0
  44. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/observability/embed_container.html +0 -0
  45. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/observability/main.py +0 -0
  46. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/observability/utils.py +0 -0
  47. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/tools/classes.py +0 -0
  48. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/tools/main.py +0 -0
  49. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr/tools/permission_deprecation.py +0 -0
  50. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/lkr.md +0 -0
  51. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/tests/TESTING.md +0 -0
  52. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/tests/test_dependency_resolution.py +0 -0
  53. {lkr_dev_cli-0.0.36 → lkr_dev_cli-0.0.37}/tests/test_deps.sh +0 -0
  54. {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-test2 codemode-start
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 python tests/test_codemode.py
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.36
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
- - Use `dir()` and `help('method_name')` to discover available SDK methods.
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
- 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
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
+ """