lkr-dev-cli 0.0.30__tar.gz → 0.0.32__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 (41) hide show
  1. lkr_dev_cli-0.0.32/.github/workflows/test-dependencies.yml +34 -0
  2. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/Dockerfile +1 -1
  3. lkr_dev_cli-0.0.32/Makefile +7 -0
  4. lkr_dev_cli-0.0.30/README.md → lkr_dev_cli-0.0.32/PKG-INFO +119 -12
  5. lkr_dev_cli-0.0.30/PKG-INFO → lkr_dev_cli-0.0.32/README.md +81 -34
  6. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/auth/main.py +17 -5
  7. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/auth_service.py +4 -3
  8. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/logger.py +33 -22
  9. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/main.py +25 -6
  10. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/observability/main.py +2 -0
  11. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/tools/classes.py +5 -4
  12. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/tools/main.py +4 -1
  13. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/pyproject.toml +29 -5
  14. lkr_dev_cli-0.0.32/test.py +16 -0
  15. lkr_dev_cli-0.0.32/tests/TESTING.md +96 -0
  16. lkr_dev_cli-0.0.32/tests/test_dependency_resolution.py +238 -0
  17. lkr_dev_cli-0.0.32/tests/test_deps.sh +119 -0
  18. lkr_dev_cli-0.0.32/uv.lock +1260 -0
  19. lkr_dev_cli-0.0.30/Makefile +0 -4
  20. lkr_dev_cli-0.0.30/uv.lock +0 -842
  21. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/.github/workflows/release.yml +0 -0
  22. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/.gitignore +0 -0
  23. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/.python-version +0 -0
  24. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/.vscode/launch.json +0 -0
  25. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/.vscode/settings.json +0 -0
  26. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/LICENSE +0 -0
  27. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/cloudbuild.yaml +0 -0
  28. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/__init__.py +0 -0
  29. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/auth/__init__.py +0 -0
  30. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/auth/oauth.py +0 -0
  31. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/classes.py +0 -0
  32. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/constants.py +0 -0
  33. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/custom_types.py +0 -0
  34. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/exceptions.py +0 -0
  35. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/mcp/classes.py +0 -0
  36. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/mcp/main.py +0 -0
  37. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/mcp/utils.py +0 -0
  38. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/observability/classes.py +0 -0
  39. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/observability/embed_container.html +0 -0
  40. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr/observability/utils.py +0 -0
  41. {lkr_dev_cli-0.0.30 → lkr_dev_cli-0.0.32}/lkr.md +0 -0
@@ -0,0 +1,34 @@
1
+ name: Test Dependency Resolution
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, develop ]
6
+ pull_request:
7
+ branches: [ main, develop ]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ test-dependencies:
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - name: Checkout code
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Setup Python
19
+ uses: actions/setup-python@v4
20
+ with:
21
+ python-version: '3.12'
22
+
23
+ - name: Install uv
24
+ uses: astral-sh/setup-uv@v2
25
+ with:
26
+ version: latest
27
+
28
+ - name: Install dependencies
29
+ run: |
30
+ uv sync --extra all
31
+
32
+ - name: Run with pytest
33
+ run: |
34
+ uv run pytest tests/test_dependency_resolution.py -v
@@ -9,6 +9,6 @@ WORKDIR /app
9
9
  COPY pyproject.toml uv.lock README.md LICENSE ./
10
10
  COPY lkr ./lkr
11
11
  ENV UV_PROJECT_ENVIRONMENT="/usr/local/"
12
- RUN uv sync --frozen --no-dev --no-editable
12
+ RUN uv sync --frozen --no-dev --no-editable --extra=all
13
13
 
14
14
  CMD []
@@ -0,0 +1,7 @@
1
+ .PHONY: docs test-deps
2
+
3
+ docs:
4
+ typer lkr/main.py utils docs --output lkr.md
5
+
6
+ test-deps:
7
+ python tests/test_dependency_resolution.py
@@ -1,12 +1,50 @@
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
+
1
39
  # lkr cli
2
40
 
3
41
  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
42
 
5
43
  ## Usage
6
44
 
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 `uv run --with lkr-dev-cli lkr --help`.
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 `uv run --with lkr-dev-cli[all] lkr --help`.
8
46
 
9
- Alternatively, you can install `lkr` with `pip install lkr-dev-cli` and use commands directly like `lkr <command>`.
47
+ Alternatively, you can install `lkr` with `pip install lkr-dev-cli[all]` and use commands directly like `lkr <command>`.
10
48
 
11
49
  We also have a public docker image that you can use to run `lkr` commands.
12
50
 
@@ -24,7 +62,7 @@ See the [prerequisites section](#oauth2-prerequisites)
24
62
  Login to `lkr`
25
63
 
26
64
  ```bash
27
- uv run --with lkr-dev-cli lkr auth login
65
+ uv run --with lkr-dev-cli[all] lkr auth login
28
66
  ```
29
67
 
30
68
  - Select a new instance
@@ -37,7 +75,7 @@ You will be redirected to the Looker OAuth authorization page, click Allow. If y
37
75
  If everything is successful, you will see `Successfully authenticated!`. Test it with
38
76
 
39
77
  ```bash
40
- uv run --with lkr-dev-cli lkr auth whoami
78
+ uv run --with lkr-dev-cli[all] lkr auth whoami
41
79
  ```
42
80
 
43
81
  ### Using API Key
@@ -45,7 +83,7 @@ uv run --with lkr-dev-cli lkr auth whoami
45
83
  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
84
 
47
85
  ```bash
48
- uv run --with lkr-dev-cli lkr --client-id <your client id> --client-secret <your client secret> --base-url <your instance url> auth whoami
86
+ uv run --with lkr-dev-cli[all] lkr --client-id <your client id> --client-secret <your client secret> --base-url <your instance url> auth whoami
49
87
  ```
50
88
 
51
89
 
@@ -81,7 +119,7 @@ Built into the `lkr` is an MCP server. Right now its tools are based on helping
81
119
  "mcpServers": {
82
120
  "lkr-mcp": {
83
121
  "command": "uv",
84
- "args": ["run", "--with", "lkr-dev-cli", "lkr", "mcp", "run"]
122
+ "args": ["run", "--with", "lkr-dev-cli[all]", "lkr", "mcp", "run"]
85
123
  },
86
124
  "lkr-mcp-docker": {
87
125
  "command": "docker",
@@ -192,10 +230,13 @@ This can also be used to stress test your Looker environment as it serves an API
192
230
  ## User Attribute Updater (OIDC Token)
193
231
 
194
232
  1. Create a new cloud run using the `lkr-cli` public docker image `us-central1-docker.pkg.dev/lkr-dev-production/lkr-cli/cli:latest`
195
- 2. Put in the environment variables LOOKERSDK_CLIENT_ID, LOOKERSDK_CLIENT_SECRET, LOOKERSDK_BASE_URL, LOOKER_WHITELISTED_BASE_URLS. The `LOOKER_WHITELISTED_BASE_URLS` would be the same url as the `LOOKERSDK_BASE_URL` if you are only using this for a single Looker instance. For more advanced use cases, you can set the `LOOKER_WHITELISTED_BASE_URLS` to a comma separated list of urls. The body of the request also accepts a `base_url`, `client_id`, and `client_secret` key that will override these settings.
196
- 3. Deploy the cloud run
197
- 4. Retrieve the URL of the cloud run
198
- 5. Create the user attribute
233
+ 2. Put in the environment variables LOOKERSDK_CLIENT_ID, LOOKERSDK_CLIENT_SECRET, LOOKERSDK_BASE_URL, LOOKER_WHITELISTED_BASE_URLS. The `LOOKER_WHITELISTED_BASE_URLS` would be the same url as the `LOOKERSDK_BASE_URL` if you are only using this for a single Looker instance. For more advanced use cases, you can set the `LOOKER_WHITELISTED_BASE_URLS` to a comma separated list of urls. The body of the request also accepts a `base_url`, `client_id`, and `client_secret` key that will override these settings. See example [`gcloud` command](#example-gcloud-command)
234
+ 3. For the command and arguments use:
235
+ - command: `lkr`
236
+ - args: `tools` `user-attribute-updater`
237
+ 4. Deploy the cloud run
238
+ 5. Retrieve the URL of the cloud run
239
+ 6. Create the user attribute
199
240
  - Name: cloud_run_access_token
200
241
  - Data Type: String
201
242
  - User Access: None
@@ -228,6 +269,23 @@ This can also be used to stress test your Looker environment as it serves an API
228
269
  9. Check the logs of the cloud run to see if there was a 200 response
229
270
 
230
271
 
272
+ ### Example `gcloud` command
273
+ ```bash
274
+ export REGION=<your region>
275
+ export PROJECT=<your project id>
276
+
277
+ gcloud run deploy lkr-access-token-updater \
278
+ --image us-central1-docker.pkg.dev/lkr-dev-production/lkr-cli/cli:latest \
279
+ --command lkr \
280
+ --args tools,user-attribute-updater \
281
+ --platform managed \
282
+ --region $REGION \
283
+ --project $PROJECT \
284
+ --cpu 1 \
285
+ --memory 2Gi \
286
+ --set-env-vars LOOKERSDK_CLIENT_ID=<your client id>,LOOKERSDK_CLIENT_SECRET=<your client secret>,LOOKERSDK_BASE_URL=<your instance url>,LOOKER_WHITELISTED_BASE_URLS=<your instance url>
287
+ ```
288
+
231
289
  ## UserAttributeUpdater `lkr-dev-cli`
232
290
 
233
291
  Exported from the `lkr-dev-cli` package is the `UserAttributeUpdater` pydantic class. This class has all the necessary logic to update a user attribute value.
@@ -279,12 +337,12 @@ from lkr import UserAttributeUpdater
279
337
  def request_authorization(request: Request):
280
338
  body = await request.json()
281
339
  updater = UserAttributeUpdater.model_validate(body)
282
- updater.get_request_authorization_for_value(request)
340
+ updater.get_request_authorization_for_value(request.headers.items())
283
341
  updater.update_user_attribute_value()
284
342
 
285
343
  @app.post("/as_body")
286
344
  def as_body(request: Request, body: UserAttributeUpdater):
287
- body.get_request_authorization_for_value(request)
345
+ body.get_request_authorization_for_value(request.headers.items())
288
346
  body.update_user_attribute_value()
289
347
 
290
348
  @app.post("/assigning_value")
@@ -295,4 +353,53 @@ def assigning_value(request: Request):
295
353
  )
296
354
  updater.value = request.headers.get("my_custom_header")
297
355
  updater.update_user_attribute_value()
356
+
357
+ @app.delete("/:user_attribute_name/:email")
358
+ def delete_user_attribute(user_attribute_name: str, email: str):
359
+ updater = UserAttributeUpdater(
360
+ user_attribute=user_attribute_name,
361
+ update_type="user",
362
+ email=email,
363
+ )
364
+ updater.delete_user_attribute_value()
365
+
366
+ ## Optional Dependencies
367
+
368
+ The `lkr` CLI supports optional dependencies that enable additional functionality. You can install these individually or all at once.
369
+
370
+ ### Available Extras
371
+
372
+ - **`mcp`**: Enables the MCP (Model Context Protocol) server functionality and `lkr mcp` commands
373
+ - **`observability`**: Enables the observability embed monitoring features and `lkr observability` commands
374
+ - **`tools`**: Enables the user attribute updater functionality and `lkr tools` commands
375
+
376
+ ### Installing Optional Dependencies
377
+
378
+ **Install all optional dependencies:**
379
+ ```bash
380
+ uv sync --extra all
381
+ ```
382
+
383
+ **Install specific extras:**
384
+ ```bash
385
+ # Install MCP functionality
386
+ uv sync --extra mcp
387
+
388
+ # Install observability features
389
+ uv sync --extra embed-observability
390
+
391
+ # Install user attribute updater
392
+ uv sync --extra user-attribute-updater
393
+
394
+ # Install multiple extras
395
+ uv sync --extra mcp --extra embed-observability
396
+ ```
397
+
398
+ **Using pip:**
399
+ ```bash
400
+ # Install all optional dependencies
401
+ pip install lkr-dev-cli[all]
402
+
403
+ # Install specific extras
404
+ pip install lkr-dev-cli[mcp,embed-observability,user-attribute-updater]
298
405
  ```
@@ -1,34 +1,12 @@
1
- Metadata-Version: 2.4
2
- Name: lkr-dev-cli
3
- Version: 0.0.30
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>=42.0.0
10
- Requires-Dist: duckdb>=1.2.2
11
- Requires-Dist: fastapi>=0.115.12
12
- Requires-Dist: looker-sdk>=25.4.0
13
- Requires-Dist: mcp[cli]>=1.9.2
14
- Requires-Dist: pydantic>=2.11.4
15
- Requires-Dist: pydash>=8.0.5
16
- Requires-Dist: questionary>=2.1.0
17
- Requires-Dist: requests>=2.31.0
18
- Requires-Dist: selenium>=4.32.0
19
- Requires-Dist: structlog>=25.3.0
20
- Requires-Dist: typer>=0.15.2
21
- Description-Content-Type: text/markdown
22
-
23
1
  # lkr cli
24
2
 
25
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)
26
4
 
27
5
  ## Usage
28
6
 
29
- `uv` makes everyone's life easier. Go [install it](https://docs.astral.sh/uv/getting-started/installation/). You can start using `lkr` by running `uv run --with lkr-dev-cli lkr --help`.
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 `uv run --with lkr-dev-cli[all] lkr --help`.
30
8
 
31
- Alternatively, you can install `lkr` with `pip install lkr-dev-cli` and use commands directly like `lkr <command>`.
9
+ Alternatively, you can install `lkr` with `pip install lkr-dev-cli[all]` and use commands directly like `lkr <command>`.
32
10
 
33
11
  We also have a public docker image that you can use to run `lkr` commands.
34
12
 
@@ -46,7 +24,7 @@ See the [prerequisites section](#oauth2-prerequisites)
46
24
  Login to `lkr`
47
25
 
48
26
  ```bash
49
- uv run --with lkr-dev-cli lkr auth login
27
+ uv run --with lkr-dev-cli[all] lkr auth login
50
28
  ```
51
29
 
52
30
  - Select a new instance
@@ -59,7 +37,7 @@ You will be redirected to the Looker OAuth authorization page, click Allow. If y
59
37
  If everything is successful, you will see `Successfully authenticated!`. Test it with
60
38
 
61
39
  ```bash
62
- uv run --with lkr-dev-cli lkr auth whoami
40
+ uv run --with lkr-dev-cli[all] lkr auth whoami
63
41
  ```
64
42
 
65
43
  ### Using API Key
@@ -67,7 +45,7 @@ uv run --with lkr-dev-cli lkr auth whoami
67
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.
68
46
 
69
47
  ```bash
70
- uv run --with lkr-dev-cli lkr --client-id <your client id> --client-secret <your client secret> --base-url <your instance url> auth whoami
48
+ uv run --with lkr-dev-cli[all] lkr --client-id <your client id> --client-secret <your client secret> --base-url <your instance url> auth whoami
71
49
  ```
72
50
 
73
51
 
@@ -103,7 +81,7 @@ Built into the `lkr` is an MCP server. Right now its tools are based on helping
103
81
  "mcpServers": {
104
82
  "lkr-mcp": {
105
83
  "command": "uv",
106
- "args": ["run", "--with", "lkr-dev-cli", "lkr", "mcp", "run"]
84
+ "args": ["run", "--with", "lkr-dev-cli[all]", "lkr", "mcp", "run"]
107
85
  },
108
86
  "lkr-mcp-docker": {
109
87
  "command": "docker",
@@ -214,10 +192,13 @@ This can also be used to stress test your Looker environment as it serves an API
214
192
  ## User Attribute Updater (OIDC Token)
215
193
 
216
194
  1. Create a new cloud run using the `lkr-cli` public docker image `us-central1-docker.pkg.dev/lkr-dev-production/lkr-cli/cli:latest`
217
- 2. Put in the environment variables LOOKERSDK_CLIENT_ID, LOOKERSDK_CLIENT_SECRET, LOOKERSDK_BASE_URL, LOOKER_WHITELISTED_BASE_URLS. The `LOOKER_WHITELISTED_BASE_URLS` would be the same url as the `LOOKERSDK_BASE_URL` if you are only using this for a single Looker instance. For more advanced use cases, you can set the `LOOKER_WHITELISTED_BASE_URLS` to a comma separated list of urls. The body of the request also accepts a `base_url`, `client_id`, and `client_secret` key that will override these settings.
218
- 3. Deploy the cloud run
219
- 4. Retrieve the URL of the cloud run
220
- 5. Create the user attribute
195
+ 2. Put in the environment variables LOOKERSDK_CLIENT_ID, LOOKERSDK_CLIENT_SECRET, LOOKERSDK_BASE_URL, LOOKER_WHITELISTED_BASE_URLS. The `LOOKER_WHITELISTED_BASE_URLS` would be the same url as the `LOOKERSDK_BASE_URL` if you are only using this for a single Looker instance. For more advanced use cases, you can set the `LOOKER_WHITELISTED_BASE_URLS` to a comma separated list of urls. The body of the request also accepts a `base_url`, `client_id`, and `client_secret` key that will override these settings. See example [`gcloud` command](#example-gcloud-command)
196
+ 3. For the command and arguments use:
197
+ - command: `lkr`
198
+ - args: `tools` `user-attribute-updater`
199
+ 4. Deploy the cloud run
200
+ 5. Retrieve the URL of the cloud run
201
+ 6. Create the user attribute
221
202
  - Name: cloud_run_access_token
222
203
  - Data Type: String
223
204
  - User Access: None
@@ -250,6 +231,23 @@ This can also be used to stress test your Looker environment as it serves an API
250
231
  9. Check the logs of the cloud run to see if there was a 200 response
251
232
 
252
233
 
234
+ ### Example `gcloud` command
235
+ ```bash
236
+ export REGION=<your region>
237
+ export PROJECT=<your project id>
238
+
239
+ gcloud run deploy lkr-access-token-updater \
240
+ --image us-central1-docker.pkg.dev/lkr-dev-production/lkr-cli/cli:latest \
241
+ --command lkr \
242
+ --args tools,user-attribute-updater \
243
+ --platform managed \
244
+ --region $REGION \
245
+ --project $PROJECT \
246
+ --cpu 1 \
247
+ --memory 2Gi \
248
+ --set-env-vars LOOKERSDK_CLIENT_ID=<your client id>,LOOKERSDK_CLIENT_SECRET=<your client secret>,LOOKERSDK_BASE_URL=<your instance url>,LOOKER_WHITELISTED_BASE_URLS=<your instance url>
249
+ ```
250
+
253
251
  ## UserAttributeUpdater `lkr-dev-cli`
254
252
 
255
253
  Exported from the `lkr-dev-cli` package is the `UserAttributeUpdater` pydantic class. This class has all the necessary logic to update a user attribute value.
@@ -301,12 +299,12 @@ from lkr import UserAttributeUpdater
301
299
  def request_authorization(request: Request):
302
300
  body = await request.json()
303
301
  updater = UserAttributeUpdater.model_validate(body)
304
- updater.get_request_authorization_for_value(request)
302
+ updater.get_request_authorization_for_value(request.headers.items())
305
303
  updater.update_user_attribute_value()
306
304
 
307
305
  @app.post("/as_body")
308
306
  def as_body(request: Request, body: UserAttributeUpdater):
309
- body.get_request_authorization_for_value(request)
307
+ body.get_request_authorization_for_value(request.headers.items())
310
308
  body.update_user_attribute_value()
311
309
 
312
310
  @app.post("/assigning_value")
@@ -317,4 +315,53 @@ def assigning_value(request: Request):
317
315
  )
318
316
  updater.value = request.headers.get("my_custom_header")
319
317
  updater.update_user_attribute_value()
318
+
319
+ @app.delete("/:user_attribute_name/:email")
320
+ def delete_user_attribute(user_attribute_name: str, email: str):
321
+ updater = UserAttributeUpdater(
322
+ user_attribute=user_attribute_name,
323
+ update_type="user",
324
+ email=email,
325
+ )
326
+ updater.delete_user_attribute_value()
327
+
328
+ ## Optional Dependencies
329
+
330
+ The `lkr` CLI supports optional dependencies that enable additional functionality. You can install these individually or all at once.
331
+
332
+ ### Available Extras
333
+
334
+ - **`mcp`**: Enables the MCP (Model Context Protocol) server functionality and `lkr mcp` commands
335
+ - **`observability`**: Enables the observability embed monitoring features and `lkr observability` commands
336
+ - **`tools`**: Enables the user attribute updater functionality and `lkr tools` commands
337
+
338
+ ### Installing Optional Dependencies
339
+
340
+ **Install all optional dependencies:**
341
+ ```bash
342
+ uv sync --extra all
343
+ ```
344
+
345
+ **Install specific extras:**
346
+ ```bash
347
+ # Install MCP functionality
348
+ uv sync --extra mcp
349
+
350
+ # Install observability features
351
+ uv sync --extra embed-observability
352
+
353
+ # Install user attribute updater
354
+ uv sync --extra user-attribute-updater
355
+
356
+ # Install multiple extras
357
+ uv sync --extra mcp --extra embed-observability
358
+ ```
359
+
360
+ **Using pip:**
361
+ ```bash
362
+ # Install all optional dependencies
363
+ pip install lkr-dev-cli[all]
364
+
365
+ # Install specific extras
366
+ pip install lkr-dev-cli[mcp,embed-observability,user-attribute-updater]
320
367
  ```
@@ -1,16 +1,25 @@
1
1
  import urllib.parse
2
2
  from typing import Annotated, List, Union
3
3
 
4
- import questionary
5
4
  import typer
6
5
  from looker_sdk.rtl.auth_token import AccessToken, AuthToken
7
- from rich.console import Console
8
- from rich.table import Table
9
6
 
10
7
  from lkr.auth.oauth import OAuth2PKCE
11
8
  from lkr.auth_service import get_auth
12
9
  from lkr.logger import logger
13
10
 
11
+ QUESTIONARY_AVAILABLE = True
12
+ RICH_AVAILABLE = True
13
+ try:
14
+ import questionary
15
+ except ModuleNotFoundError:
16
+ QUESTIONARY_AVAILABLE = False
17
+ try:
18
+ from rich.console import Console
19
+ from rich.table import Table
20
+ except ModuleNotFoundError:
21
+ RICH_AVAILABLE = False
22
+
14
23
  __all__ = ["group"]
15
24
 
16
25
  group = typer.Typer(name="auth", help="Authentication commands for LookML Repository")
@@ -199,7 +208,7 @@ def list(ctx: typer.Context):
199
208
  """
200
209
  List all authenticated Looker instances
201
210
  """
202
- console = Console()
211
+ console = Console() if RICH_AVAILABLE else None
203
212
  auth = get_auth(ctx)
204
213
  all_instances = auth.list_auth()
205
214
  if not all_instances:
@@ -213,7 +222,10 @@ def list(ctx: typer.Context):
213
222
  instance[1],
214
223
  "Yes" if instance[3] else "No",
215
224
  )
216
- console.print(table)
225
+ if console:
226
+ console.print(table)
227
+ else:
228
+ print(table)
217
229
 
218
230
 
219
231
  if __name__ == "__main__":
@@ -3,10 +3,11 @@ import os
3
3
  import sqlite3
4
4
  import types
5
5
  from datetime import datetime, timedelta, timezone
6
- from typing import List, Self, Tuple, Union
6
+ from typing import List, Self, Tuple, Union, TYPE_CHECKING
7
7
 
8
8
  import requests
9
- import typer
9
+ if TYPE_CHECKING:
10
+ import typer
10
11
  from looker_sdk.rtl import serialize
11
12
  from looker_sdk.rtl.api_settings import ApiSettings, SettingsConfig
12
13
  from looker_sdk.rtl.auth_session import AuthSession, CryptoHash, OAuthSession
@@ -25,7 +26,7 @@ from lkr.logger import logger
25
26
  __all__ = ["get_auth", "ApiKeyAuthSession", "DbOAuthSession"]
26
27
 
27
28
 
28
- def get_auth(ctx: typer.Context | LkrCtxObj) -> Union["SqlLiteAuth", "ApiKeyAuth"]:
29
+ def get_auth(ctx: Union["typer.Context", LkrCtxObj]) -> Union["SqlLiteAuth", "ApiKeyAuth"]:
29
30
  if isinstance(ctx, LkrCtxObj):
30
31
  lkr_ctx = ctx
31
32
  else:
@@ -1,19 +1,28 @@
1
1
  import logging
2
2
  import os
3
+ from lkr.custom_types import LogLevel
3
4
 
4
- import structlog
5
- from rich.console import Console
6
- from rich.logging import RichHandler
7
- from rich.theme import Theme
5
+ STRUCT_LOG_AVAILABLE = True
6
+ RICH_AVAILABLE = True
7
+ try:
8
+ import structlog
9
+ except ModuleNotFoundError:
10
+ STRUCT_LOG_AVAILABLE = False
11
+ try:
12
+ from rich.console import Console
13
+ from rich.logging import RichHandler
14
+ from rich.theme import Theme
15
+ except ModuleNotFoundError:
16
+ RICH_AVAILABLE = False
8
17
 
9
- from lkr.custom_types import LogLevel
10
18
 
11
- structlog.configure(
12
- processors=[
13
- structlog.processors.TimeStamper(fmt="iso"),
14
- structlog.processors.JSONRenderer(),
15
- ]
16
- )
19
+ if STRUCT_LOG_AVAILABLE:
20
+ structlog.configure(
21
+ processors=[
22
+ structlog.processors.TimeStamper(fmt="iso"),
23
+ structlog.processors.JSONRenderer(),
24
+ ]
25
+ )
17
26
 
18
27
  # Define a custom theme for our logging
19
28
  theme = Theme(
@@ -24,10 +33,10 @@ theme = Theme(
24
33
  "logging.level.error": "bold red",
25
34
  "logging.level.critical": "bold white on red",
26
35
  }
27
- )
36
+ ) if RICH_AVAILABLE else None
28
37
 
29
38
  # Create a console for logging
30
- console = Console(theme=theme)
39
+ console = Console(theme=theme) if RICH_AVAILABLE else None
31
40
 
32
41
  # Configure the logging handler
33
42
  handler = RichHandler(
@@ -37,7 +46,7 @@ handler = RichHandler(
37
46
  markup=True,
38
47
  rich_tracebacks=True,
39
48
  tracebacks_show_locals=True,
40
- )
49
+ ) if RICH_AVAILABLE else None
41
50
 
42
51
  # Get log level from environment variable, defaulting to INFO
43
52
  DEFAULT_LOG_LEVEL = "INFO"
@@ -50,25 +59,27 @@ logging.basicConfig(
50
59
  ), # Fallback to INFO if invalid level
51
60
  format="%(message)s",
52
61
  datefmt="[%X]",
53
- handlers=[handler],
62
+ handlers=[handler] if handler else [],
54
63
  )
55
64
 
56
65
  # Create a logger for the application
57
66
  logger = logging.getLogger("lkr")
58
- structured_logger = structlog.get_logger("lkr.structured")
67
+ structured_logger = structlog.get_logger("lkr.structured") if STRUCT_LOG_AVAILABLE else None
59
68
 
60
69
 
61
70
  # Configure the requests_transport logger to only show debug messages when LOG_LEVEL is DEBUG
62
- requests_logger = logging.getLogger("looker_sdk.rtl.requests_transport")
63
- if log_level != "DEBUG":
71
+ requests_logger = logging.getLogger("looker_sdk.rtl.requests_transport") if RICH_AVAILABLE else None
72
+ if log_level != "DEBUG" and requests_logger:
64
73
  requests_logger.setLevel(logging.WARNING)
65
74
 
66
75
 
67
76
  def set_log_level(level: LogLevel):
68
77
  """Set the logging level for the application."""
69
78
  logger.setLevel(getattr(logging, level.value))
70
- logging.getLogger("lkr.structured").setLevel(getattr(logging, level.value))
79
+ if structured_logger:
80
+ structured_logger.setLevel(getattr(logging, level.value))
71
81
  # Update requests_transport logger level based on the new level
72
- requests_logger.setLevel(
73
- logging.DEBUG if level == LogLevel.DEBUG else logging.WARNING
74
- )
82
+ if requests_logger:
83
+ requests_logger.setLevel(
84
+ logging.DEBUG if level == LogLevel.DEBUG else logging.WARNING
85
+ )
@@ -7,9 +7,6 @@ from lkr.auth.main import group as auth_group
7
7
  from lkr.classes import LkrCtxObj
8
8
  from lkr.custom_types import LogLevel
9
9
  from lkr.logger import logger
10
- from lkr.mcp.main import group as mcp_group
11
- from lkr.observability.main import group as observability_group
12
- from lkr.tools.main import group as tools_group
13
10
 
14
11
  app = typer.Typer(
15
12
  name="lkr",
@@ -19,10 +16,32 @@ app = typer.Typer(
19
16
  )
20
17
 
21
18
  app.add_typer(auth_group, name="auth")
22
- app.add_typer(mcp_group, name="mcp")
23
- app.add_typer(observability_group, name="observability")
24
- app.add_typer(tools_group, name="tools")
25
19
 
20
+ IMPORT_ERROR = None
21
+
22
+ def add_optional_typer_group(app, import_path, group_name, extra_message=None):
23
+ try:
24
+ module_path, attr = import_path.rsplit(".", 1)
25
+ mod = __import__(module_path, fromlist=[attr])
26
+ group = getattr(mod, attr)
27
+ app.add_typer(group, name=group_name)
28
+ except ModuleNotFoundError as import_error:
29
+ @app.command(
30
+ name=group_name,
31
+ add_help_option=False,
32
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
33
+ )
34
+ def fallback(import_error=import_error):
35
+ msg = f"{group_name} tools (dependencies not available, try installing optional dependencies: lkr-dev-cli\\[{group_name}])"
36
+ if extra_message:
37
+ msg += f" {extra_message}"
38
+ logger.error(msg)
39
+ logger.error(import_error)
40
+ raise typer.Exit(1)
41
+
42
+ add_optional_typer_group(app, "lkr.mcp.main.group", "mcp")
43
+ add_optional_typer_group(app, "lkr.observability.main.group", "observability")
44
+ add_optional_typer_group(app, "lkr.tools.main.group", "tools")
26
45
 
27
46
  @app.callback()
28
47
  def callback(
@@ -39,6 +39,8 @@ DEFAULT_PERMISSIONS = set(
39
39
 
40
40
  observability_ctx = ObservabilityCtxObj()
41
41
 
42
+ if not structured_logger:
43
+ raise Exception("Structured logger is not available")
42
44
 
43
45
  def get_embed_sdk_obj(
44
46
  dashboard_id: str = Query(...),
@@ -1,6 +1,5 @@
1
1
  from typing import Literal, Optional, Self, cast
2
2
 
3
- from fastapi import Request
4
3
  from looker_sdk.sdk.api40.methods import Looker40SDK
5
4
  from looker_sdk.sdk.api40.models import (
6
5
  UserAttributeGroupValue,
@@ -46,10 +45,12 @@ class UserAttributeUpdater(BaseModel):
46
45
  )
47
46
  return self
48
47
 
49
- def get_request_authorization_for_value(self, request: Request):
50
- authorization_token = request.headers.get("Authorization")
48
+ def get_request_authorization_for_value(self, headers: list[tuple[str, str]]):
49
+ authorization_token = next(
50
+ (header for header in headers if header[0] == "Authorization"), None
51
+ )
51
52
  if authorization_token:
52
- self.value = authorization_token
53
+ self.value = authorization_token[1]
53
54
  else:
54
55
  logger.error("No authorization token found")
55
56