lkr-dev-cli 0.0.29__tar.gz → 0.0.30__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 (34) hide show
  1. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/PKG-INFO +111 -3
  2. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/README.md +110 -2
  3. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/main.py +4 -1
  4. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/tools/classes.py +36 -45
  5. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/tools/main.py +1 -2
  6. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr.md +1 -1
  7. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/pyproject.toml +1 -1
  8. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/.github/workflows/release.yml +0 -0
  9. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/.gitignore +0 -0
  10. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/.python-version +0 -0
  11. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/.vscode/launch.json +0 -0
  12. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/.vscode/settings.json +0 -0
  13. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/Dockerfile +0 -0
  14. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/LICENSE +0 -0
  15. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/Makefile +0 -0
  16. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/cloudbuild.yaml +0 -0
  17. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/__init__.py +0 -0
  18. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/auth/__init__.py +0 -0
  19. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/auth/main.py +0 -0
  20. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/auth/oauth.py +0 -0
  21. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/auth_service.py +0 -0
  22. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/classes.py +0 -0
  23. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/constants.py +0 -0
  24. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/custom_types.py +0 -0
  25. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/exceptions.py +0 -0
  26. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/logger.py +0 -0
  27. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/mcp/classes.py +0 -0
  28. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/mcp/main.py +0 -0
  29. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/mcp/utils.py +0 -0
  30. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/observability/classes.py +0 -0
  31. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/observability/embed_container.html +0 -0
  32. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/observability/main.py +0 -0
  33. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/lkr/observability/utils.py +0 -0
  34. {lkr_dev_cli-0.0.29 → lkr_dev_cli-0.0.30}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lkr-dev-cli
3
- Version: 0.0.29
3
+ Version: 0.0.30
4
4
  Summary: lkr: a command line interface for looker
5
5
  Author: bwebs
6
6
  License-Expression: MIT
@@ -129,7 +129,7 @@ The observability command provides tools for monitoring and interacting with Loo
129
129
  - `GET /health`: Launches a headless browser to simulate embedding a dashboard, waits for a completion indicator, and logs the process for health checking. This endpoint accepts query parameters to help login users with custom attributes.
130
130
 
131
131
  > [!IMPORTANT]
132
- > Make sure you add the http://host:port to your domain allowlist in Admin Embed. [docs](https://cloud.google.com/looker/docs/embedded-javascript-events#adding_the_embed_domain_to_the_allowlist) Unless overridden, the default would be http://0.0.0.0:8080. These can also set via cli arguments. E.g., `lkr observability embed --host localhost --port 7777` or by setting the environment variables `HOST` and `PORT`. You can check the embed_domain by sending a request to the `/settings` endpoint.
132
+ > Make sure you add the `http://host:port` to your domain allowlist in Admin Embed. [docs](https://cloud.google.com/looker/docs/embedded-javascript-events#adding_the_embed_domain_to_the_allowlist) Unless overridden, the default would be http://0.0.0.0:8080. These can also set via cli arguments. E.g., `lkr observability embed --host localhost --port 7777` or by setting the environment variables `HOST` and `PORT`. You can check the embed_domain by sending a request to the `/settings` endpoint.
133
133
 
134
134
 
135
135
  For example:
@@ -209,4 +209,112 @@ gcloud monitoring uptime create lkr-observability-health-check \
209
209
  ```
210
210
 
211
211
  ### Alternative Usage
212
- This can also be used to stress test your Looker environment as it serves an API that logs into a Looker embedded dashboard and runs queries like a user would within Chromium. If you wrote a script to repeatedly call this API with different parameters, you could use it to stress test your Looker environment and/or your database.
212
+ This can also be used to stress test your Looker environment as it serves an API that logs into a Looker embedded dashboard and runs queries like a user would within Chromium. If you wrote a script to repeatedly call this API with different parameters, you could use it to stress test your Looker environment and/or your database.
213
+
214
+ ## User Attribute Updater (OIDC Token)
215
+
216
+ 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
221
+ - Name: cloud_run_access_token
222
+ - Data Type: String
223
+ - User Access: None
224
+ - Hide values: Yes
225
+ - Domain Allowlist: The URL of the cloud run from step 4. Looker will only allow the user attribute to be set if the request is going to this URL
226
+
227
+ > [!NOTE]
228
+ > The user attribute name can be anything you want. Typically you will be using this with an extension, so you should follow the naming convention of the type of user attribute you will be using with the extension. I would recommend using a `scoped user attributes` for this. See [Extension User Attributes](https://www.npmjs.com/package/@looker/extension-sdk#user-attributes). If you are using a `global_user_attribute`, then you can just use the name of it like `cloud_run_access_token`.
229
+
230
+ 6. Create a new cloud scheduler
231
+ - cron: `0 * * * *`
232
+ - Target Type: Cloud Run
233
+ - URL: The URL of the cloud run from step 4 with a path of `/identity_token`. E.g. `https://your-cloud-run-url.com/identity_token`
234
+ - HTTP Method: POST
235
+ - Headers: `Content-Type: application/json`
236
+ - Body: Use the user attribute_name from step 5, or use the user_attribute_id found in the Looker URL after you created it or are editing it
237
+
238
+ ```json
239
+ {
240
+ "user_attribute": "cloud_run_access_token",
241
+ "update_type": "default"
242
+ }
243
+ ```
244
+ - Auth Header: OIDC Token
245
+ - Service Account: Choose the service account you want to use to run the cloud scheduler
246
+ - Audience: The URL of the cloud run
247
+ - Max Retries: >0
248
+ 7. Make sure the Service Account has the `Cloud Run Invoker` role
249
+ 8. Navigate the the cloud scheduler page, select the one you just created, and click Force Run
250
+ 9. Check the logs of the cloud run to see if there was a 200 response
251
+
252
+
253
+ ## UserAttributeUpdater `lkr-dev-cli`
254
+
255
+ 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.
256
+
257
+ It supports the following operations:
258
+ - Updating a default value
259
+ - Updating a group value
260
+ - Updating a user value
261
+ - Deleting a default value
262
+ - Deleting a group value
263
+ - Deleting a user value
264
+
265
+ It can also support looking up looker ids. It will lookup the following if the id is not provided:
266
+ - user_attribute_id by the name
267
+ - user_id by the email or external_user_id
268
+ - group_id by the name
269
+
270
+
271
+ ### Example Usage
272
+
273
+ ```python
274
+ from lkr import UserAttributeUpdater
275
+
276
+ # without credentials
277
+ updater = UserAttributeUpdater(
278
+ user_attribute="cloud_run_access_token",
279
+ update_type="default",
280
+ value="123",
281
+ )
282
+
283
+
284
+ # with credentials
285
+ updater = UserAttributeUpdater(
286
+ user_attribute="cloud_run_access_token",
287
+ update_type="default",
288
+ value="123",
289
+ base_url="https://your-looker-instance.com",
290
+ client_id="your-client-id",
291
+ client_secret="your-client-secret",
292
+ )
293
+
294
+ updater.update_user_attribute_value()
295
+
296
+ # Getting authorization header from a FastAPI request
297
+ from fastapi import Request
298
+ from lkr import UserAttributeUpdater
299
+
300
+ @app.post("/request_authorization")
301
+ def request_authorization(request: Request):
302
+ body = await request.json()
303
+ updater = UserAttributeUpdater.model_validate(body)
304
+ updater.get_request_authorization_for_value(request)
305
+ updater.update_user_attribute_value()
306
+
307
+ @app.post("/as_body")
308
+ def as_body(request: Request, body: UserAttributeUpdater):
309
+ body.get_request_authorization_for_value(request)
310
+ body.update_user_attribute_value()
311
+
312
+ @app.post("/assigning_value")
313
+ def assigning_value(request: Request):
314
+ updater = UserAttributeUpdater(
315
+ user_attribute="cloud_run_access_token",
316
+ update_type="default"
317
+ )
318
+ updater.value = request.headers.get("my_custom_header")
319
+ updater.update_user_attribute_value()
320
+ ```
@@ -107,7 +107,7 @@ The observability command provides tools for monitoring and interacting with Loo
107
107
  - `GET /health`: Launches a headless browser to simulate embedding a dashboard, waits for a completion indicator, and logs the process for health checking. This endpoint accepts query parameters to help login users with custom attributes.
108
108
 
109
109
  > [!IMPORTANT]
110
- > Make sure you add the http://host:port to your domain allowlist in Admin Embed. [docs](https://cloud.google.com/looker/docs/embedded-javascript-events#adding_the_embed_domain_to_the_allowlist) Unless overridden, the default would be http://0.0.0.0:8080. These can also set via cli arguments. E.g., `lkr observability embed --host localhost --port 7777` or by setting the environment variables `HOST` and `PORT`. You can check the embed_domain by sending a request to the `/settings` endpoint.
110
+ > Make sure you add the `http://host:port` to your domain allowlist in Admin Embed. [docs](https://cloud.google.com/looker/docs/embedded-javascript-events#adding_the_embed_domain_to_the_allowlist) Unless overridden, the default would be http://0.0.0.0:8080. These can also set via cli arguments. E.g., `lkr observability embed --host localhost --port 7777` or by setting the environment variables `HOST` and `PORT`. You can check the embed_domain by sending a request to the `/settings` endpoint.
111
111
 
112
112
 
113
113
  For example:
@@ -187,4 +187,112 @@ gcloud monitoring uptime create lkr-observability-health-check \
187
187
  ```
188
188
 
189
189
  ### Alternative Usage
190
- This can also be used to stress test your Looker environment as it serves an API that logs into a Looker embedded dashboard and runs queries like a user would within Chromium. If you wrote a script to repeatedly call this API with different parameters, you could use it to stress test your Looker environment and/or your database.
190
+ This can also be used to stress test your Looker environment as it serves an API that logs into a Looker embedded dashboard and runs queries like a user would within Chromium. If you wrote a script to repeatedly call this API with different parameters, you could use it to stress test your Looker environment and/or your database.
191
+
192
+ ## User Attribute Updater (OIDC Token)
193
+
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`
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
199
+ - Name: cloud_run_access_token
200
+ - Data Type: String
201
+ - User Access: None
202
+ - Hide values: Yes
203
+ - Domain Allowlist: The URL of the cloud run from step 4. Looker will only allow the user attribute to be set if the request is going to this URL
204
+
205
+ > [!NOTE]
206
+ > The user attribute name can be anything you want. Typically you will be using this with an extension, so you should follow the naming convention of the type of user attribute you will be using with the extension. I would recommend using a `scoped user attributes` for this. See [Extension User Attributes](https://www.npmjs.com/package/@looker/extension-sdk#user-attributes). If you are using a `global_user_attribute`, then you can just use the name of it like `cloud_run_access_token`.
207
+
208
+ 6. Create a new cloud scheduler
209
+ - cron: `0 * * * *`
210
+ - Target Type: Cloud Run
211
+ - URL: The URL of the cloud run from step 4 with a path of `/identity_token`. E.g. `https://your-cloud-run-url.com/identity_token`
212
+ - HTTP Method: POST
213
+ - Headers: `Content-Type: application/json`
214
+ - Body: Use the user attribute_name from step 5, or use the user_attribute_id found in the Looker URL after you created it or are editing it
215
+
216
+ ```json
217
+ {
218
+ "user_attribute": "cloud_run_access_token",
219
+ "update_type": "default"
220
+ }
221
+ ```
222
+ - Auth Header: OIDC Token
223
+ - Service Account: Choose the service account you want to use to run the cloud scheduler
224
+ - Audience: The URL of the cloud run
225
+ - Max Retries: >0
226
+ 7. Make sure the Service Account has the `Cloud Run Invoker` role
227
+ 8. Navigate the the cloud scheduler page, select the one you just created, and click Force Run
228
+ 9. Check the logs of the cloud run to see if there was a 200 response
229
+
230
+
231
+ ## UserAttributeUpdater `lkr-dev-cli`
232
+
233
+ 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.
234
+
235
+ It supports the following operations:
236
+ - Updating a default value
237
+ - Updating a group value
238
+ - Updating a user value
239
+ - Deleting a default value
240
+ - Deleting a group value
241
+ - Deleting a user value
242
+
243
+ It can also support looking up looker ids. It will lookup the following if the id is not provided:
244
+ - user_attribute_id by the name
245
+ - user_id by the email or external_user_id
246
+ - group_id by the name
247
+
248
+
249
+ ### Example Usage
250
+
251
+ ```python
252
+ from lkr import UserAttributeUpdater
253
+
254
+ # without credentials
255
+ updater = UserAttributeUpdater(
256
+ user_attribute="cloud_run_access_token",
257
+ update_type="default",
258
+ value="123",
259
+ )
260
+
261
+
262
+ # with credentials
263
+ updater = UserAttributeUpdater(
264
+ user_attribute="cloud_run_access_token",
265
+ update_type="default",
266
+ value="123",
267
+ base_url="https://your-looker-instance.com",
268
+ client_id="your-client-id",
269
+ client_secret="your-client-secret",
270
+ )
271
+
272
+ updater.update_user_attribute_value()
273
+
274
+ # Getting authorization header from a FastAPI request
275
+ from fastapi import Request
276
+ from lkr import UserAttributeUpdater
277
+
278
+ @app.post("/request_authorization")
279
+ def request_authorization(request: Request):
280
+ body = await request.json()
281
+ updater = UserAttributeUpdater.model_validate(body)
282
+ updater.get_request_authorization_for_value(request)
283
+ updater.update_user_attribute_value()
284
+
285
+ @app.post("/as_body")
286
+ def as_body(request: Request, body: UserAttributeUpdater):
287
+ body.get_request_authorization_for_value(request)
288
+ body.update_user_attribute_value()
289
+
290
+ @app.post("/assigning_value")
291
+ def assigning_value(request: Request):
292
+ updater = UserAttributeUpdater(
293
+ user_attribute="cloud_run_access_token",
294
+ update_type="default"
295
+ )
296
+ updater.value = request.headers.get("my_custom_header")
297
+ updater.update_user_attribute_value()
298
+ ```
@@ -12,7 +12,10 @@ from lkr.observability.main import group as observability_group
12
12
  from lkr.tools.main import group as tools_group
13
13
 
14
14
  app = typer.Typer(
15
- name="lkr", help="LookML Repository CLI", add_completion=True, no_args_is_help=True
15
+ name="lkr",
16
+ help="A CLI for Looker with helpful tools",
17
+ add_completion=True,
18
+ no_args_is_help=True,
16
19
  )
17
20
 
18
21
  app.add_typer(auth_group, name="auth")
@@ -68,6 +68,17 @@ class UserAttributeUpdater(BaseModel):
68
68
  return None
69
69
  return init_api_key_sdk(api_key, True)
70
70
 
71
+ def _get_looker_user_id(self, sdk: Looker40SDK) -> str | None:
72
+ if self.looker_user_id:
73
+ return self.looker_user_id
74
+ elif self.email:
75
+ user = sdk.user_for_credential("email", self.email)
76
+ return user.id if user else None
77
+ elif self.external_user_id:
78
+ user = sdk.user_for_credential("embed", self.external_user_id)
79
+ return user.id if user else None
80
+ return None
81
+
71
82
  def _get_group_id(self, sdk: Looker40SDK) -> str | None:
72
83
  if self.group_id:
73
84
  return self.group_id
@@ -107,25 +118,24 @@ class UserAttributeUpdater(BaseModel):
107
118
  else:
108
119
  raise ValueError("Group not found")
109
120
  elif self.update_type == "default":
110
- sdk.delete_user_attribute(user_attribute_id)
121
+ user_attribute = sdk.user_attribute(user_attribute_id, "name,label,type")
122
+ sdk.update_user_attribute(
123
+ user_attribute_id,
124
+ WriteUserAttribute(
125
+ default_value=None,
126
+ name=user_attribute.name,
127
+ label=user_attribute.label,
128
+ type=user_attribute.type,
129
+ ),
130
+ )
111
131
  elif self.update_type == "user":
112
- if self.looker_user_id:
113
- sdk.delete_user_attribute_user_value(
114
- user_id=self.looker_user_id,
115
- user_attribute_id=user_attribute_id,
116
- )
117
- elif self.external_user_id:
118
- sdk.delete_user_attribute_user_value(
119
- user_id=self.external_user_id,
120
- user_attribute_id=user_attribute_id,
121
- )
122
- elif self.email:
123
- sdk.delete_user_attribute_user_value(
124
- user_id=self.email,
125
- user_attribute_id=user_attribute_id,
126
- )
127
- else:
132
+ looker_user_id = self._get_looker_user_id(sdk)
133
+ if not looker_user_id:
128
134
  raise ValueError("User not found")
135
+ sdk.delete_user_attribute_user_value(
136
+ user_id=looker_user_id,
137
+ user_attribute_id=user_attribute_id,
138
+ )
129
139
 
130
140
  def update_user_attribute_value(self):
131
141
  if not self.value:
@@ -170,35 +180,16 @@ class UserAttributeUpdater(BaseModel):
170
180
  ),
171
181
  )
172
182
  elif self.update_type == "user":
173
-
174
- def set_user_attribute_user_value(user_id: str):
175
- sdk.set_user_attribute_user_value(
176
- user_id=user_id,
177
- user_attribute_id=user_attribute_id,
178
- body=WriteUserAttributeWithValue(
179
- value=self.value,
180
- ),
181
- )
182
-
183
- if self.looker_user_id:
184
- set_user_attribute_user_value(self.looker_user_id)
185
-
186
- elif self.external_user_id:
187
- user = sdk.user_for_credential("embed", self.external_user_id)
188
- if not (user and user.id):
189
- raise ValueError("User not found")
190
- set_user_attribute_user_value(user.id)
191
-
192
- elif self.email:
193
- user = sdk.user_for_credential("email", self.email)
194
- if not (user and user.id):
195
- raise ValueError("User not found")
196
- set_user_attribute_user_value(user.id)
197
-
198
- else:
183
+ looker_user_id = self._get_looker_user_id(sdk)
184
+ if not looker_user_id:
199
185
  raise ValueError("User not found")
200
- else:
201
- raise ValueError("Invalid update_type")
186
+ sdk.set_user_attribute_user_value(
187
+ user_id=looker_user_id,
188
+ user_attribute_id=user_attribute_id,
189
+ body=WriteUserAttributeWithValue(
190
+ value=self.value,
191
+ ),
192
+ )
202
193
 
203
194
 
204
195
  class AttributeUpdaterResponse(BaseModel):
@@ -14,9 +14,8 @@ group = typer.Typer()
14
14
 
15
15
  @group.command()
16
16
  def user_attribute_updater(
17
- ctx: typer.Context,
18
17
  host: str = typer.Option(default="127.0.0.1", envvar="HOST"),
19
- port: int = typer.Option(default=8080, envvar="x`"),
18
+ port: int = typer.Option(default=8080, envvar="PORT"),
20
19
  ):
21
20
  api = FastAPI()
22
21
 
@@ -1,6 +1,6 @@
1
1
  # `lkr`
2
2
 
3
- LookML Repository CLI
3
+ A CLI for Looker with helpful tools
4
4
 
5
5
  **Usage**:
6
6
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lkr-dev-cli"
3
- version = "0.0.29"
3
+ version = "0.0.30"
4
4
  description = "lkr: a command line interface for looker"
5
5
  readme = "README.md"
6
6
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes