lkr-dev-cli 0.0.32__tar.gz → 0.0.34__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.32 → lkr_dev_cli-0.0.34}/.github/workflows/release.yml +6 -0
- lkr_dev_cli-0.0.34/Makefile +26 -0
- lkr_dev_cli-0.0.32/README.md → lkr_dev_cli-0.0.34/PKG-INFO +69 -5
- lkr_dev_cli-0.0.32/PKG-INFO → lkr_dev_cli-0.0.34/README.md +33 -43
- lkr_dev_cli-0.0.34/codemode.md +51 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/auth/main.py +15 -9
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/auth_service.py +19 -2
- lkr_dev_cli-0.0.34/lkr/codemode/__init__.py +3 -0
- lkr_dev_cli-0.0.34/lkr/codemode/main.py +104 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/main.py +1 -0
- lkr_dev_cli-0.0.34/lkr/tools/main.py +232 -0
- lkr_dev_cli-0.0.34/lkr/tools/permission_deprecation.py +193 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr.md +51 -0
- lkr_dev_cli-0.0.34/pyproject.toml +67 -0
- lkr_dev_cli-0.0.34/tests/test_codemode.py +18 -0
- lkr_dev_cli-0.0.34/tests/test_codemode2.py +26 -0
- lkr_dev_cli-0.0.34/tests/test_permission_deprecation.py +176 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/uv.lock +80 -37
- lkr_dev_cli-0.0.32/Makefile +0 -7
- lkr_dev_cli-0.0.32/lkr/tools/main.py +0 -87
- lkr_dev_cli-0.0.32/pyproject.toml +0 -64
- lkr_dev_cli-0.0.32/test.py +0 -16
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/.github/workflows/test-dependencies.yml +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/.gitignore +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/.python-version +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/.vscode/launch.json +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/.vscode/settings.json +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/Dockerfile +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/LICENSE +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/cloudbuild.yaml +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/__init__.py +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/auth/__init__.py +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/auth/oauth.py +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/classes.py +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/constants.py +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/custom_types.py +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/exceptions.py +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/logger.py +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/mcp/classes.py +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/mcp/main.py +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/mcp/utils.py +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/observability/classes.py +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/observability/embed_container.html +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/observability/main.py +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/observability/utils.py +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/lkr/tools/classes.py +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/tests/TESTING.md +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/tests/test_dependency_resolution.py +0 -0
- {lkr_dev_cli-0.0.32 → lkr_dev_cli-0.0.34}/tests/test_deps.sh +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\": [\"--from\", \"lkr-dev-cli[codemode]\", \"lkr\", \"code-mode\", \"run\"]"
|
|
23
|
+
@echo " }"
|
|
24
|
+
@echo " }"
|
|
25
|
+
@echo "}"
|
|
26
|
+
uv run lkr code-mode run
|
|
@@ -1,10 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lkr-dev-cli
|
|
3
|
+
Version: 0.0.34
|
|
4
|
+
Summary: lkr: a command line interface for looker
|
|
5
|
+
Author: bwebs
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: cryptography>=45.0.4
|
|
10
|
+
Requires-Dist: looker-sdk>=25.10.0
|
|
11
|
+
Requires-Dist: pydantic>=2.11.7
|
|
12
|
+
Requires-Dist: pydash>=8.0.5
|
|
13
|
+
Requires-Dist: questionary>=2.1.0
|
|
14
|
+
Requires-Dist: requests>=2.32.4
|
|
15
|
+
Requires-Dist: structlog>=25.4.0
|
|
16
|
+
Requires-Dist: typer>=0.16.0
|
|
17
|
+
Provides-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'
|
|
26
|
+
Provides-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'
|
|
30
|
+
Provides-Extra: observability
|
|
31
|
+
Requires-Dist: fastapi[standard]>=0.115.14; extra == 'observability'
|
|
32
|
+
Requires-Dist: selenium>=4.34.0; extra == 'observability'
|
|
33
|
+
Provides-Extra: tools
|
|
34
|
+
Requires-Dist: fastapi[standard]>=0.115.14; extra == 'tools'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
1
37
|
# lkr cli
|
|
2
38
|
|
|
3
39
|
The `lkr` cli is a tool for interacting with Looker. It combines Looker's SDK and customer logic to interact with Looker in meaninful ways. For a full list of commands, see the full [cli docs](./lkr.md)
|
|
4
40
|
|
|
5
41
|
## Usage
|
|
6
42
|
|
|
7
|
-
`uv` makes everyone's life easier. Go [install it](https://docs.astral.sh/uv/getting-started/installation/). You can start using `lkr` by running `
|
|
43
|
+
`uv` makes everyone's life easier. Go [install it](https://docs.astral.sh/uv/getting-started/installation/). You can start using `lkr` by running `uvx --from lkr-dev-cli[all] lkr --help`.
|
|
8
44
|
|
|
9
45
|
Alternatively, you can install `lkr` with `pip install lkr-dev-cli[all]` and use commands directly like `lkr <command>`.
|
|
10
46
|
|
|
@@ -24,7 +60,7 @@ See the [prerequisites section](#oauth2-prerequisites)
|
|
|
24
60
|
Login to `lkr`
|
|
25
61
|
|
|
26
62
|
```bash
|
|
27
|
-
|
|
63
|
+
uvx --from lkr-dev-cli[all] lkr auth login
|
|
28
64
|
```
|
|
29
65
|
|
|
30
66
|
- Select a new instance
|
|
@@ -32,12 +68,12 @@ uv run --with lkr-dev-cli[all] lkr auth login
|
|
|
32
68
|
- Choose whether you want this login to use production or development mode
|
|
33
69
|
- Give it a name
|
|
34
70
|
|
|
35
|
-
You will be redirected to the Looker OAuth authorization page, click Allow. If you do not see an allow button, the [prerequisites](#prerequisites) were not done properly.
|
|
71
|
+
You will be redirected to the Looker OAuth authorization page, click Allow. If you do not see an allow button, the [prerequisites](#oauth2-prerequisites) were not done properly.
|
|
36
72
|
|
|
37
73
|
If everything is successful, you will see `Successfully authenticated!`. Test it with
|
|
38
74
|
|
|
39
75
|
```bash
|
|
40
|
-
|
|
76
|
+
uvx --from lkr-dev-cli[all] lkr auth whoami
|
|
41
77
|
```
|
|
42
78
|
|
|
43
79
|
### Using API Key
|
|
@@ -45,7 +81,7 @@ uv run --with lkr-dev-cli[all] lkr auth whoami
|
|
|
45
81
|
If you provide environment variables for `LOOKERSDK_CLIENT_ID`, `LOOKERSDK_CLIENT_SECRET`, and `LOOKERSDK_BASE_URL`, `lkr` will use the API key to authenticate and the commands. We also support command line arguments to pass in the client id, client secret, and base url.
|
|
46
82
|
|
|
47
83
|
```bash
|
|
48
|
-
|
|
84
|
+
uvx --from lkr-dev-cli[all] lkr --client-id <your client id> --client-secret <your client secret> --base-url <your instance url> auth whoami
|
|
49
85
|
```
|
|
50
86
|
|
|
51
87
|
|
|
@@ -73,6 +109,10 @@ Go to the Looker API Explorer for Register OAuth App (https://your.looker.instan
|
|
|
73
109
|
- This only needs to be done once per instance
|
|
74
110
|
|
|
75
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
|
+
|
|
76
116
|
## MCP
|
|
77
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
|
|
78
118
|
|
|
@@ -325,6 +365,30 @@ def delete_user_attribute(user_attribute_name: str, email: str):
|
|
|
325
365
|
)
|
|
326
366
|
updater.delete_user_attribute_value()
|
|
327
367
|
|
|
368
|
+
## Permission Deprecation Tool
|
|
369
|
+
|
|
370
|
+
The `schedule-download-deprecation` tool helps Looker admins ensure that users do not lose access to models they already have when Looker moves towards more granular model-specific permissions for scheduling and downloading.
|
|
371
|
+
|
|
372
|
+
### How it helps
|
|
373
|
+
Currently, some permissions in Looker can be granted instance-wide. In the future, these permissions may need to be explicitly granted at the model level (via Model Sets). This tool audits all active users and identifies those who:
|
|
374
|
+
- Have "target permissions" (like `download_with_limit`, `schedule_look_emails`, etc.) instance-wide.
|
|
375
|
+
- Do **not** have those same permissions for specific models they otherwise have access to.
|
|
376
|
+
|
|
377
|
+
By running this tool, an admin can proactively identify and fix permission gaps before any deprecation takes effect, ensuring a seamless experience for end-users.
|
|
378
|
+
|
|
379
|
+
### Usage
|
|
380
|
+
This command should be run by a **Looker Admin**.
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
uvx --from lkr-dev-cli[all] lkr tools schedule-download-deprecation
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Options:
|
|
387
|
+
- `--csv`: Export the results to a CSV file for easier analysis of large instances.
|
|
388
|
+
- `--unfiltered`: Show all users, including those who have all required permissions across all models.
|
|
389
|
+
- `--model-offset`: Slice the table output to show different sets of models (the table shows 5 models at a time).
|
|
390
|
+
|
|
391
|
+
|
|
328
392
|
## Optional Dependencies
|
|
329
393
|
|
|
330
394
|
The `lkr` CLI supports optional dependencies that enable additional functionality. You can install these individually or all at once.
|
|
@@ -1,48 +1,10 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: lkr-dev-cli
|
|
3
|
-
Version: 0.0.32
|
|
4
|
-
Summary: lkr: a command line interface for looker
|
|
5
|
-
Author: bwebs
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
License-File: LICENSE
|
|
8
|
-
Requires-Python: >=3.12
|
|
9
|
-
Requires-Dist: looker-sdk>=25.4.0
|
|
10
|
-
Requires-Dist: pydantic>=2.11.4
|
|
11
|
-
Requires-Dist: pydash>=8.0.5
|
|
12
|
-
Provides-Extra: all
|
|
13
|
-
Requires-Dist: cryptography>=42.0.0; extra == 'all'
|
|
14
|
-
Requires-Dist: duckdb>=1.2.2; extra == 'all'
|
|
15
|
-
Requires-Dist: fastapi[standard]>=0.115.12; extra == 'all'
|
|
16
|
-
Requires-Dist: mcp[cli]>=1.9.2; extra == 'all'
|
|
17
|
-
Requires-Dist: questionary>=2.1.0; extra == 'all'
|
|
18
|
-
Requires-Dist: requests>=2.31.0; extra == 'all'
|
|
19
|
-
Requires-Dist: selenium>=4.32.0; extra == 'all'
|
|
20
|
-
Requires-Dist: structlog>=25.3.0; extra == 'all'
|
|
21
|
-
Requires-Dist: typer>=0.15.2; extra == 'all'
|
|
22
|
-
Provides-Extra: cli
|
|
23
|
-
Requires-Dist: cryptography>=42.0.0; extra == 'cli'
|
|
24
|
-
Requires-Dist: questionary>=2.1.0; extra == 'cli'
|
|
25
|
-
Requires-Dist: requests>=2.31.0; extra == 'cli'
|
|
26
|
-
Requires-Dist: structlog>=25.3.0; extra == 'cli'
|
|
27
|
-
Requires-Dist: typer>=0.15.2; extra == 'cli'
|
|
28
|
-
Provides-Extra: mcp
|
|
29
|
-
Requires-Dist: duckdb>=1.2.2; extra == 'mcp'
|
|
30
|
-
Requires-Dist: fastapi[standard]>=0.115.12; extra == 'mcp'
|
|
31
|
-
Requires-Dist: mcp[cli]>=1.9.2; extra == 'mcp'
|
|
32
|
-
Provides-Extra: observability
|
|
33
|
-
Requires-Dist: fastapi[standard]>=0.115.12; extra == 'observability'
|
|
34
|
-
Requires-Dist: selenium>=4.32.0; extra == 'observability'
|
|
35
|
-
Provides-Extra: tools
|
|
36
|
-
Requires-Dist: fastapi[standard]>=0.115.12; extra == 'tools'
|
|
37
|
-
Description-Content-Type: text/markdown
|
|
38
|
-
|
|
39
1
|
# lkr cli
|
|
40
2
|
|
|
41
3
|
The `lkr` cli is a tool for interacting with Looker. It combines Looker's SDK and customer logic to interact with Looker in meaninful ways. For a full list of commands, see the full [cli docs](./lkr.md)
|
|
42
4
|
|
|
43
5
|
## Usage
|
|
44
6
|
|
|
45
|
-
`uv` makes everyone's life easier. Go [install it](https://docs.astral.sh/uv/getting-started/installation/). You can start using `lkr` by running `
|
|
7
|
+
`uv` makes everyone's life easier. Go [install it](https://docs.astral.sh/uv/getting-started/installation/). You can start using `lkr` by running `uvx --from lkr-dev-cli[all] lkr --help`.
|
|
46
8
|
|
|
47
9
|
Alternatively, you can install `lkr` with `pip install lkr-dev-cli[all]` and use commands directly like `lkr <command>`.
|
|
48
10
|
|
|
@@ -62,7 +24,7 @@ See the [prerequisites section](#oauth2-prerequisites)
|
|
|
62
24
|
Login to `lkr`
|
|
63
25
|
|
|
64
26
|
```bash
|
|
65
|
-
|
|
27
|
+
uvx --from lkr-dev-cli[all] lkr auth login
|
|
66
28
|
```
|
|
67
29
|
|
|
68
30
|
- Select a new instance
|
|
@@ -70,12 +32,12 @@ uv run --with lkr-dev-cli[all] lkr auth login
|
|
|
70
32
|
- Choose whether you want this login to use production or development mode
|
|
71
33
|
- Give it a name
|
|
72
34
|
|
|
73
|
-
You will be redirected to the Looker OAuth authorization page, click Allow. If you do not see an allow button, the [prerequisites](#prerequisites) were not done properly.
|
|
35
|
+
You will be redirected to the Looker OAuth authorization page, click Allow. If you do not see an allow button, the [prerequisites](#oauth2-prerequisites) were not done properly.
|
|
74
36
|
|
|
75
37
|
If everything is successful, you will see `Successfully authenticated!`. Test it with
|
|
76
38
|
|
|
77
39
|
```bash
|
|
78
|
-
|
|
40
|
+
uvx --from lkr-dev-cli[all] lkr auth whoami
|
|
79
41
|
```
|
|
80
42
|
|
|
81
43
|
### Using API Key
|
|
@@ -83,7 +45,7 @@ uv run --with lkr-dev-cli[all] lkr auth whoami
|
|
|
83
45
|
If you provide environment variables for `LOOKERSDK_CLIENT_ID`, `LOOKERSDK_CLIENT_SECRET`, and `LOOKERSDK_BASE_URL`, `lkr` will use the API key to authenticate and the commands. We also support command line arguments to pass in the client id, client secret, and base url.
|
|
84
46
|
|
|
85
47
|
```bash
|
|
86
|
-
|
|
48
|
+
uvx --from lkr-dev-cli[all] lkr --client-id <your client id> --client-secret <your client secret> --base-url <your instance url> auth whoami
|
|
87
49
|
```
|
|
88
50
|
|
|
89
51
|
|
|
@@ -111,6 +73,10 @@ Go to the Looker API Explorer for Register OAuth App (https://your.looker.instan
|
|
|
111
73
|
- This only needs to be done once per instance
|
|
112
74
|
|
|
113
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
|
+
|
|
114
80
|
## MCP
|
|
115
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
|
|
116
82
|
|
|
@@ -363,6 +329,30 @@ def delete_user_attribute(user_attribute_name: str, email: str):
|
|
|
363
329
|
)
|
|
364
330
|
updater.delete_user_attribute_value()
|
|
365
331
|
|
|
332
|
+
## Permission Deprecation Tool
|
|
333
|
+
|
|
334
|
+
The `schedule-download-deprecation` tool helps Looker admins ensure that users do not lose access to models they already have when Looker moves towards more granular model-specific permissions for scheduling and downloading.
|
|
335
|
+
|
|
336
|
+
### How it helps
|
|
337
|
+
Currently, some permissions in Looker can be granted instance-wide. In the future, these permissions may need to be explicitly granted at the model level (via Model Sets). This tool audits all active users and identifies those who:
|
|
338
|
+
- Have "target permissions" (like `download_with_limit`, `schedule_look_emails`, etc.) instance-wide.
|
|
339
|
+
- Do **not** have those same permissions for specific models they otherwise have access to.
|
|
340
|
+
|
|
341
|
+
By running this tool, an admin can proactively identify and fix permission gaps before any deprecation takes effect, ensuring a seamless experience for end-users.
|
|
342
|
+
|
|
343
|
+
### Usage
|
|
344
|
+
This command should be run by a **Looker Admin**.
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
uvx --from lkr-dev-cli[all] lkr tools schedule-download-deprecation
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Options:
|
|
351
|
+
- `--csv`: Export the results to a CSV file for easier analysis of large instances.
|
|
352
|
+
- `--unfiltered`: Show all users, including those who have all required permissions across all models.
|
|
353
|
+
- `--model-offset`: Slice the table output to show different sets of models (the table shows 5 models at a time).
|
|
354
|
+
|
|
355
|
+
|
|
366
356
|
## Optional Dependencies
|
|
367
357
|
|
|
368
358
|
The `lkr` CLI supports optional dependencies that enable additional functionality. You can install these individually or all at once.
|
|
@@ -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 --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
|
+
"looker-codemode": {
|
|
32
|
+
"command": "uvx",
|
|
33
|
+
"args": ["--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 --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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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,104 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
import pydantic_monty
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
|
|
8
|
+
from lkr.auth_service import get_auth, is_auth_expired
|
|
9
|
+
from lkr.classes import LkrCtxObj
|
|
10
|
+
from lkr.logger import logger
|
|
11
|
+
|
|
12
|
+
__all__ = ["group"]
|
|
13
|
+
|
|
14
|
+
mcp = FastMCP("lkr:codemode")
|
|
15
|
+
group = typer.Typer()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_mcp_sdk(ctx: LkrCtxObj):
|
|
20
|
+
sdk = get_auth(ctx).get_current_sdk(prompt_refresh_invalid_token=True)
|
|
21
|
+
sdk.auth.settings.agent_tag += "-codemode"
|
|
22
|
+
return sdk
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def to_primitive(obj):
|
|
27
|
+
seen = set()
|
|
28
|
+
|
|
29
|
+
def _to_primitive(o):
|
|
30
|
+
if isinstance(o, (str, int, float, bool, type(None))):
|
|
31
|
+
return o
|
|
32
|
+
|
|
33
|
+
obj_id = id(o)
|
|
34
|
+
if obj_id in seen:
|
|
35
|
+
return f"<Circular reference to {type(o).__name__}>"
|
|
36
|
+
seen.add(obj_id)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
if isinstance(o, list):
|
|
40
|
+
return [_to_primitive(item) for item in o]
|
|
41
|
+
elif isinstance(o, dict):
|
|
42
|
+
return {k: _to_primitive(v) for k, v in o.items()}
|
|
43
|
+
else:
|
|
44
|
+
try:
|
|
45
|
+
return _to_primitive(vars(o))
|
|
46
|
+
except TypeError:
|
|
47
|
+
return str(o)
|
|
48
|
+
except Exception:
|
|
49
|
+
return str(o)
|
|
50
|
+
finally:
|
|
51
|
+
seen.remove(obj_id)
|
|
52
|
+
|
|
53
|
+
return _to_primitive(obj)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@mcp.tool()
|
|
58
|
+
def run_python_code(code: str) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Execute Python code safely with access to all Looker SDK methods as global functions.
|
|
61
|
+
Capture the result and any print outputs.
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
ctx = LkrCtxObj(force_oauth=False)
|
|
65
|
+
sdk = get_mcp_sdk(ctx)
|
|
66
|
+
|
|
67
|
+
external_funcs = {}
|
|
68
|
+
for name, method in inspect.getmembers(sdk, predicate=inspect.ismethod):
|
|
69
|
+
if not name.startswith('_'):
|
|
70
|
+
# Wrap in a lambda to recursively convert output to primitives
|
|
71
|
+
def make_wrapper(m):
|
|
72
|
+
def wrapper(*args, **kwargs):
|
|
73
|
+
res = m(*args, **kwargs)
|
|
74
|
+
return to_primitive(res)
|
|
75
|
+
return wrapper
|
|
76
|
+
external_funcs[name] = make_wrapper(method)
|
|
77
|
+
|
|
78
|
+
m = pydantic_monty.Monty(code)
|
|
79
|
+
result = m.run(external_functions=external_funcs)
|
|
80
|
+
|
|
81
|
+
# Monty run returns a MontyComplete or None
|
|
82
|
+
output = str(getattr(result, "output", "")) if result is not None else ""
|
|
83
|
+
|
|
84
|
+
# Try to append captured stdout if available on the object
|
|
85
|
+
stdout = getattr(result, "stdout", None) if result is not None else None
|
|
86
|
+
if stdout:
|
|
87
|
+
return f"Output:\n{output}\nStdout:\n{stdout}"
|
|
88
|
+
return output
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.error(f"Error executing Monty: {e}")
|
|
91
|
+
if is_auth_expired(e):
|
|
92
|
+
return "Error: Your Looker OAuth session has expired. Please run 'lkr auth login' to re-authenticate."
|
|
93
|
+
return f"Error: {str(e)}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@group.command(name="run")
|
|
97
|
+
def run(
|
|
98
|
+
ctx: typer.Context,
|
|
99
|
+
debug: bool = typer.Option(False, help="Debug mode"),
|
|
100
|
+
):
|
|
101
|
+
mcp.run()
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
mcp.run("sse")
|
|
@@ -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(
|