Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/test-dependencies.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Test Dependency Resolution

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
workflow_dispatch:

jobs:
test-dependencies:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.12'

- name: Install uv
uses: astral-sh/setup-uv@v2
with:
version: latest

- name: Install dependencies
run: |
uv sync --extra all

- name: Run with pytest
run: |
uv run pytest tests/test_dependency_resolution.py -v
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ WORKDIR /app
COPY pyproject.toml uv.lock README.md LICENSE ./
COPY lkr ./lkr
ENV UV_PROJECT_ENVIRONMENT="/usr/local/"
RUN uv sync --frozen --no-dev --no-editable
RUN uv sync --frozen --no-dev --no-editable --extra=all

CMD []
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
.PHONY: docs
.PHONY: docs test-deps

docs:
typer lkr/main.py utils docs --output lkr.md
typer lkr/main.py utils docs --output lkr.md

test-deps:
python tests/test_dependency_resolution.py
67 changes: 59 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ The `lkr` cli is a tool for interacting with Looker. It combines Looker's SDK an

## Usage

`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`.
`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`.

Alternatively, you can install `lkr` with `pip install lkr-dev-cli` and use commands directly like `lkr <command>`.
Alternatively, you can install `lkr` with `pip install lkr-dev-cli[all]` and use commands directly like `lkr <command>`.

We also have a public docker image that you can use to run `lkr` commands.

Expand All @@ -24,7 +24,7 @@ See the [prerequisites section](#oauth2-prerequisites)
Login to `lkr`

```bash
uv run --with lkr-dev-cli lkr auth login
uv run --with lkr-dev-cli[all] lkr auth login
```

- Select a new instance
Expand All @@ -37,15 +37,15 @@ You will be redirected to the Looker OAuth authorization page, click Allow. If y
If everything is successful, you will see `Successfully authenticated!`. Test it with

```bash
uv run --with lkr-dev-cli lkr auth whoami
uv run --with lkr-dev-cli[all] lkr auth whoami
```

### Using API Key

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.

```bash
uv run --with lkr-dev-cli lkr --client-id <your client id> --client-secret <your client secret> --base-url <your instance url> auth whoami
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
```


Expand Down Expand Up @@ -81,7 +81,7 @@ Built into the `lkr` is an MCP server. Right now its tools are based on helping
"mcpServers": {
"lkr-mcp": {
"command": "uv",
"args": ["run", "--with", "lkr-dev-cli", "lkr", "mcp", "run"]
"args": ["run", "--with", "lkr-dev-cli[all]", "lkr", "mcp", "run"]
},
"lkr-mcp-docker": {
"command": "docker",
Expand Down Expand Up @@ -299,12 +299,12 @@ from lkr import UserAttributeUpdater
def request_authorization(request: Request):
body = await request.json()
updater = UserAttributeUpdater.model_validate(body)
updater.get_request_authorization_for_value(request)
updater.get_request_authorization_for_value(request.headers.items())
updater.update_user_attribute_value()

@app.post("/as_body")
def as_body(request: Request, body: UserAttributeUpdater):
body.get_request_authorization_for_value(request)
body.get_request_authorization_for_value(request.headers.items())
body.update_user_attribute_value()

@app.post("/assigning_value")
Expand All @@ -324,4 +324,55 @@ def delete_user_attribute(user_attribute_name: str, email: str):
email=email,
)
updater.delete_user_attribute_value()

## Optional Dependencies

The `lkr` CLI supports optional dependencies that enable additional functionality. You can install these individually or all at once.

### Available Extras

- **`mcp`**: Enables the MCP (Model Context Protocol) server functionality
- Includes: `mcp[cli]>=1.9.2`, `duckdb>=1.2.2`
- **`embed-observability`**: Enables the observability embed monitoring features
- Includes: `fastapi>=0.115.12`, `selenium>=4.32.0`
- **`user-attribute-updater`**: Enables the user attribute updater functionality
- Includes: `fastapi>=0.115.12`

### Installing Optional Dependencies

**Install all optional dependencies:**
```bash
uv sync --extra all
```

**Install specific extras:**
```bash
# Install MCP functionality
uv sync --extra mcp

# Install observability features
uv sync --extra embed-observability

# Install user attribute updater
uv sync --extra user-attribute-updater

# Install multiple extras
uv sync --extra mcp --extra embed-observability
```

**Using pip:**
```bash
# Install all optional dependencies
pip install lkr-dev-cli[all]

# Install specific extras
pip install lkr-dev-cli[mcp,embed-observability,user-attribute-updater]
```

### What Each Extra Enables

- **`mcp`**: Use the MCP server with tools like Cursor for enhanced IDE integration
- **`embed-observability`**: Run the observability embed server for monitoring Looker dashboard performance
- **`user-attribute-updater`**: Deploy the user attribute updater service for OIDC token management

All extras are designed to work together seamlessly, and installing `all` is equivalent to installing all individual extras.
9 changes: 5 additions & 4 deletions lkr/tools/classes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from typing import Literal, Optional, Self, cast

from fastapi import Request
from looker_sdk.sdk.api40.methods import Looker40SDK
from looker_sdk.sdk.api40.models import (
UserAttributeGroupValue,
Expand Down Expand Up @@ -46,10 +45,12 @@ def check_variables(self) -> Self:
)
return self

def get_request_authorization_for_value(self, request: Request):
authorization_token = request.headers.get("Authorization")
def get_request_authorization_for_value(self, headers: list[tuple[str, str]]):
authorization_token = next(
(header for header in headers if header[0] == "Authorization"), None
)
if authorization_token:
self.value = authorization_token
self.value = authorization_token[1]
else:
logger.error("No authorization token found")
Comment on lines +48 to 55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The change from fastapi.Request to list[tuple[str, str]] for headers in get_request_authorization_for_value is a good refactoring. It improves the modularity and testability of the UserAttributeUpdater class by decoupling it from the FastAPI request object. The logic for extracting the Authorization token is correctly updated to handle the new input type.


Expand Down
2 changes: 1 addition & 1 deletion lkr/tools/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def user_attribute_updater(
@api.post("/identity_token")
def identity_token(request: Request, body: UserAttributeUpdater):
try:
body.get_request_authorization_for_value(request)
body.get_request_authorization_for_value(request.headers.items())
body.update_user_attribute_value()
raw_urls = os.getenv("LOOKER_WHITELISTED_BASE_URLS", "")
whitelisted_base_urls = (
Expand Down
19 changes: 17 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,25 @@ dependencies = [
"pydash>=8.0.5",
"structlog>=25.3.0",
"questionary>=2.1.0",
"duckdb>=1.2.2",
]

[project.optional-dependencies]
mcp = [
"mcp[cli]>=1.9.2",
"duckdb>=1.2.2"
]
embed-observability = [
"fastapi>=0.115.12",
"selenium>=4.32.0",
"selenium>=4.32.0"
]
user-attribute-updater = [
"fastapi>=0.115.12"
]
all = [
"mcp[cli]>=1.9.2",
"fastapi>=0.115.12",
"selenium>=4.32.0",
"duckdb>=1.2.2"
]

[project.scripts]
Expand Down
96 changes: 96 additions & 0 deletions tests/TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Testing Dependency Resolution

This project includes tests to ensure that all dependency combinations resolve to the same lock file, maintaining consistency in the dependency tree.

## What the tests verify

The tests ensure that these two commands produce identical lock files:

1. `uv sync --extra all`
2. `uv sync --extra [all individual extras]`

Where "all individual extras" are dynamically discovered from `pyproject.toml` (excluding the `all` extra itself).

This is important because the `all` extra should be equivalent to installing all individual extras together.

## Running the tests

### Option 1: Shell script (recommended for quick testing)

```bash
./tests/test_deps.sh
```

This script will:
- Dynamically discover all individual extras from `pyproject.toml`
- Create temporary directories
- Run both dependency resolution commands
- Compare the resulting lock files
- Clean up automatically

### Option 2: Python test (for CI/CD integration)

```bash
# Run directly
python tests/test_dependency_resolution.py

# Run with pytest
uv run pytest tests/test_dependency_resolution.py -v
```

### Option 3: Makefile target

```bash
make test-deps
```

## What the tests check

1. **Dynamic Extra Discovery**: Automatically discovers all individual extras from `pyproject.toml`
2. **Dependency Resolution Consistency**: Ensures that `--extra all` and `--extra [individual extras]` resolve to the same set of packages
3. **Configuration Validation**: Verifies that all required extras are properly defined in `pyproject.toml`
4. **Lock File Integrity**: Compares SHA256 hashes of generated lock files

## Expected output

When the tests pass, you should see:

```
🔍 Discovering individual extras...
📦 Found individual extras: mcp embed-observability user-attribute-updater
🎉 SUCCESS: All dependency combinations resolve to the same lock file!
✅ Dependency resolution is consistent
```

## Adding new extras

When you add a new extra to `pyproject.toml`, the tests will automatically:

1. Discover the new extra
2. Include it in the dependency resolution test
3. Verify that `--extra all` still includes all dependencies from the new extra

No manual test updates are required!

## Troubleshooting

If the tests fail, it usually means:

1. **Missing dependencies**: The `all` extra doesn't include all the dependencies from individual extras
2. **Version conflicts**: Different dependency combinations resolve to different versions
3. **Configuration issues**: The `pyproject.toml` file has inconsistencies

### Common fixes

1. **Update the `all` extra**: Make sure it includes all dependencies from individual extras
2. **Check for version conflicts**: Ensure that all extras use compatible versions
3. **Verify dependency definitions**: Make sure all extras are properly defined

## CI/CD Integration

The tests are automatically run in GitHub Actions on:
- Push to `main` or `develop` branches
- Pull requests to `main` or `develop` branches
- Manual workflow dispatch

See `.github/workflows/test-dependencies.yml` for the CI configuration.
Loading