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.
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/.github/workflows/release.yml +6 -0
- lkr_dev_cli-0.0.35/Makefile +26 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/PKG-INFO +25 -17
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/README.md +4 -0
- lkr_dev_cli-0.0.35/codemode.md +51 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/auth/main.py +15 -9
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/auth_service.py +19 -2
- lkr_dev_cli-0.0.35/lkr/codemode/__init__.py +3 -0
- lkr_dev_cli-0.0.35/lkr/codemode/main.py +200 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/logger.py +1 -1
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/main.py +1 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/mcp/main.py +2 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr.md +30 -0
- lkr_dev_cli-0.0.35/pyproject.toml +67 -0
- lkr_dev_cli-0.0.35/tests/test_codemode.py +18 -0
- lkr_dev_cli-0.0.35/tests/test_codemode2.py +28 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/uv.lock +80 -37
- lkr_dev_cli-0.0.33/Makefile +0 -7
- lkr_dev_cli-0.0.33/pyproject.toml +0 -56
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/.github/workflows/test-dependencies.yml +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/.gitignore +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/.python-version +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/.vscode/launch.json +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/.vscode/settings.json +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/Dockerfile +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/LICENSE +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/cloudbuild.yaml +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/__init__.py +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/auth/__init__.py +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/auth/oauth.py +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/classes.py +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/constants.py +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/custom_types.py +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/exceptions.py +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/mcp/classes.py +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/mcp/utils.py +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/observability/classes.py +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/observability/embed_container.html +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/observability/main.py +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/observability/utils.py +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/tools/classes.py +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/tools/main.py +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/lkr/tools/permission_deprecation.py +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/tests/TESTING.md +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/tests/test_dependency_resolution.py +0 -0
- {lkr_dev_cli-0.0.33 → lkr_dev_cli-0.0.35}/tests/test_deps.sh +0 -0
- {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.
|
|
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>=
|
|
10
|
-
Requires-Dist: looker-sdk>=25.
|
|
11
|
-
Requires-Dist: pydantic>=2.11.
|
|
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.
|
|
15
|
-
Requires-Dist: structlog>=25.
|
|
16
|
-
Requires-Dist: typer>=0.
|
|
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.
|
|
19
|
-
Requires-Dist: fastapi[standard]>=0.115.
|
|
20
|
-
Requires-Dist: mcp[cli]>=1.
|
|
21
|
-
Requires-Dist:
|
|
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.
|
|
24
|
-
Requires-Dist: fastapi[standard]>=0.115.
|
|
25
|
-
Requires-Dist: mcp[cli]>=1.
|
|
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.
|
|
28
|
-
Requires-Dist: selenium>=4.
|
|
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.
|
|
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
|
-
|
|
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,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 = "
|
|
412
|
-
{ name = "requests" },
|
|
415
|
+
{ name = "pydantic-monty" },
|
|
413
416
|
{ name = "selenium" },
|
|
414
|
-
{ name = "structlog" },
|
|
415
|
-
{ name = "typer" },
|
|
416
417
|
]
|
|
417
|
-
|
|
418
|
-
{ name = "
|
|
419
|
-
{ name = "
|
|
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",
|
|
446
|
-
{ name = "
|
|
447
|
-
{ name = "duckdb", marker = "extra == '
|
|
448
|
-
{ name = "
|
|
449
|
-
{ name = "fastapi", extras = ["standard"], marker = "extra == '
|
|
450
|
-
{ name = "fastapi", extras = ["standard"], marker = "extra == '
|
|
451
|
-
{ name = "fastapi", extras = ["standard"], marker = "extra == '
|
|
452
|
-
{ name = "
|
|
453
|
-
{ name = "
|
|
454
|
-
{ name = "mcp", extras = ["cli"], marker = "extra == '
|
|
455
|
-
{ name = "mcp", extras = ["cli"], marker = "extra == 'mcp'", specifier = ">=1.
|
|
456
|
-
{ name = "pydantic", specifier = ">=2.11.
|
|
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",
|
|
459
|
-
{ name = "
|
|
460
|
-
{ name = "
|
|
461
|
-
{ name = "
|
|
462
|
-
{ name = "
|
|
463
|
-
{ name = "
|
|
464
|
-
|
|
465
|
-
|
|
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.
|
|
474
|
-
{ name = "ruff", specifier = ">=0.
|
|
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"
|
lkr_dev_cli-0.0.33/Makefile
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|