lkr-dev-cli 0.0.22__py3-none-any.whl → 0.0.24__py3-none-any.whl
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/auth/main.py +35 -21
- lkr/auth/oauth.py +81 -33
- lkr/auth_service.py +71 -17
- lkr/classes.py +7 -6
- lkr/{logging.py → logger.py} +17 -10
- lkr/main.py +16 -6
- lkr/mcp/main.py +2 -2
- lkr/observability/classes.py +1 -9
- lkr/observability/embed_container.html +8 -3
- lkr/observability/main.py +30 -6
- lkr/{embed/observability → observability}/utils.py +2 -27
- {lkr_dev_cli-0.0.22.dist-info → lkr_dev_cli-0.0.24.dist-info}/METADATA +1 -1
- lkr_dev_cli-0.0.24.dist-info/RECORD +21 -0
- lkr_dev_cli-0.0.22.dist-info/RECORD +0 -21
- /lkr/{types.py → custom_types.py} +0 -0
- {lkr_dev_cli-0.0.22.dist-info → lkr_dev_cli-0.0.24.dist-info}/WHEEL +0 -0
- {lkr_dev_cli-0.0.22.dist-info → lkr_dev_cli-0.0.24.dist-info}/entry_points.txt +0 -0
- {lkr_dev_cli-0.0.22.dist-info → lkr_dev_cli-0.0.24.dist-info}/licenses/LICENSE +0 -0
lkr/auth/main.py
CHANGED
@@ -9,27 +9,31 @@ from rich.table import Table
|
|
9
9
|
|
10
10
|
from lkr.auth.oauth import OAuth2PKCE
|
11
11
|
from lkr.auth_service import get_auth
|
12
|
-
from lkr.
|
12
|
+
from lkr.logger import logger
|
13
13
|
|
14
14
|
__all__ = ["group"]
|
15
15
|
|
16
16
|
group = typer.Typer(name="auth", help="Authentication commands for LookML Repository")
|
17
17
|
|
18
|
+
|
18
19
|
@group.callback()
|
19
20
|
def callback(ctx: typer.Context):
|
20
21
|
if ctx.invoked_subcommand == "whoami":
|
21
22
|
return
|
22
|
-
if ctx.obj[
|
23
|
+
if ctx.obj["ctx_lkr"].use_sdk == "api_key":
|
23
24
|
logger.error("API key authentication is not supported for auth commands")
|
24
25
|
raise typer.Exit(1)
|
25
26
|
|
27
|
+
|
26
28
|
@group.command()
|
27
29
|
def login(
|
28
30
|
ctx: typer.Context,
|
29
31
|
instance_name: Annotated[
|
30
32
|
str | None,
|
31
33
|
typer.Option(
|
32
|
-
"-I",
|
34
|
+
"-I",
|
35
|
+
"--instance-name",
|
36
|
+
help="Name of the Looker instance to login or switch to",
|
33
37
|
),
|
34
38
|
] = None,
|
35
39
|
):
|
@@ -38,7 +42,7 @@ def login(
|
|
38
42
|
"""
|
39
43
|
auth = get_auth(ctx)
|
40
44
|
all_instances = auth.list_auth()
|
41
|
-
|
45
|
+
|
42
46
|
def do_switch(instance_name: str):
|
43
47
|
auth.set_current_instance(instance_name)
|
44
48
|
sdk = auth.get_current_sdk()
|
@@ -61,17 +65,19 @@ def login(
|
|
61
65
|
else:
|
62
66
|
options: List[questionary.Choice] = []
|
63
67
|
max_name_length = 0
|
64
|
-
for
|
68
|
+
for name, url, current, up in all_instances:
|
65
69
|
max_name_length = max(max_name_length, len(name))
|
66
70
|
options = [
|
67
|
-
questionary.Choice(
|
71
|
+
questionary.Choice(
|
72
|
+
title=f"{name:{max_name_length}} ({url})", value=name, checked=current
|
73
|
+
)
|
68
74
|
for name, url, current, up in all_instances
|
69
75
|
]
|
70
|
-
options.append(
|
76
|
+
options.append(
|
77
|
+
questionary.Choice(title="+ Add new instance", value="_add_new_instance")
|
78
|
+
)
|
71
79
|
picked = questionary.select(
|
72
|
-
"Select instance to login/switch to",
|
73
|
-
choices=options,
|
74
|
-
pointer=">"
|
80
|
+
"Select instance to login/switch to", choices=options, pointer=">"
|
75
81
|
).ask()
|
76
82
|
if picked != "_add_new_instance":
|
77
83
|
do_switch(picked)
|
@@ -88,17 +94,19 @@ def login(
|
|
88
94
|
)
|
89
95
|
use_production = typer.confirm("Use production mode?", default=False)
|
90
96
|
instance_name = typer.prompt(
|
91
|
-
"Enter a name for this Looker instance",
|
97
|
+
"Enter a name for this Looker instance",
|
98
|
+
default=f"{'dev' if not use_production else 'prod'}-{parsed_url.netloc}",
|
92
99
|
)
|
93
100
|
# Ensure instance_name is str, not None
|
94
|
-
assert instance_name is not None,
|
101
|
+
assert instance_name is not None, (
|
102
|
+
"Instance name must be set before adding auth."
|
103
|
+
)
|
95
104
|
|
96
105
|
def auth_callback(token: Union[AuthToken, AccessToken]):
|
97
106
|
auth.add_auth(instance_name, origin, token, use_production)
|
98
|
-
|
107
|
+
|
99
108
|
oauth = OAuth2PKCE(
|
100
|
-
new_token_callback=auth_callback,
|
101
|
-
use_production=use_production
|
109
|
+
new_token_callback=auth_callback, use_production=use_production
|
102
110
|
)
|
103
111
|
logger.info(f"Opening browser for authentication at {origin + '/auth'}...")
|
104
112
|
login_response = oauth.initiate_login(origin)
|
@@ -114,13 +122,16 @@ def login(
|
|
114
122
|
logger.error("Failed to exchange authorization code for tokens")
|
115
123
|
raise typer.Exit(1)
|
116
124
|
except Exception as e:
|
117
|
-
logger.error(
|
125
|
+
logger.error(
|
126
|
+
f"Failed to exchange authorization code for tokens: {str(e)}"
|
127
|
+
)
|
118
128
|
raise typer.Exit(1)
|
119
129
|
else:
|
120
130
|
logger.error("Failed to receive authorization code")
|
121
131
|
raise typer.Exit(1)
|
122
132
|
do_switch(instance_name)
|
123
133
|
|
134
|
+
|
124
135
|
@group.command()
|
125
136
|
def logout(
|
126
137
|
ctx: typer.Context,
|
@@ -132,9 +143,7 @@ def logout(
|
|
132
143
|
] = None,
|
133
144
|
all: Annotated[
|
134
145
|
bool,
|
135
|
-
typer.Option(
|
136
|
-
"--all", help="Logout from all instances"
|
137
|
-
),
|
146
|
+
typer.Option("--all", help="Logout from all instances"),
|
138
147
|
] = False,
|
139
148
|
):
|
140
149
|
"""
|
@@ -156,7 +165,7 @@ def logout(
|
|
156
165
|
logger.info("Logout cancelled")
|
157
166
|
raise typer.Exit()
|
158
167
|
|
159
|
-
if instance_name:
|
168
|
+
if instance_name:
|
160
169
|
logger.info(f"Logging out from instance: {instance_name}")
|
161
170
|
auth.delete_auth(instance_name=instance_name)
|
162
171
|
else:
|
@@ -198,7 +207,12 @@ def list(ctx: typer.Context):
|
|
198
207
|
raise typer.Exit(1)
|
199
208
|
table = Table(" ", "Instance", "URL", "Production")
|
200
209
|
for instance in all_instances:
|
201
|
-
table.add_row(
|
210
|
+
table.add_row(
|
211
|
+
"*" if instance[2] else " ",
|
212
|
+
instance[0],
|
213
|
+
instance[1],
|
214
|
+
"Yes" if instance[3] else "No",
|
215
|
+
)
|
202
216
|
console.print(table)
|
203
217
|
|
204
218
|
|
lkr/auth/oauth.py
CHANGED
@@ -10,7 +10,7 @@ import webbrowser
|
|
10
10
|
from typing import Optional, TypedDict
|
11
11
|
from urllib.parse import parse_qs
|
12
12
|
|
13
|
-
from lkr.
|
13
|
+
from lkr.custom_types import NewTokenCallback
|
14
14
|
|
15
15
|
|
16
16
|
def kill_process_on_port(port: int, retries: int = 5, delay: float = 1) -> None:
|
@@ -19,21 +19,23 @@ def kill_process_on_port(port: int, retries: int = 5, delay: float = 1) -> None:
|
|
19
19
|
# Try to create a socket binding to check if port is in use
|
20
20
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
21
21
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
22
|
-
sock.bind((
|
22
|
+
sock.bind(("localhost", port))
|
23
23
|
sock.close()
|
24
24
|
return # Port is free, no need to kill anything
|
25
25
|
except socket.error:
|
26
26
|
# Port is in use, try to kill the process
|
27
|
-
if os.name ==
|
28
|
-
os.system(f
|
29
|
-
elif os.name ==
|
30
|
-
os.system(
|
27
|
+
if os.name == "posix": # macOS/Linux
|
28
|
+
os.system(f"lsof -ti tcp:{port} | xargs kill -9 2>/dev/null")
|
29
|
+
elif os.name == "nt": # Windows
|
30
|
+
os.system(
|
31
|
+
f'for /f "tokens=5" %a in (\'netstat -aon ^| find ":{port}"\') do taskkill /F /PID %a 2>nul'
|
32
|
+
)
|
31
33
|
# After killing, wait for the port to be free
|
32
34
|
for _ in range(retries):
|
33
35
|
try:
|
34
36
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
35
37
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
36
|
-
sock.bind((
|
38
|
+
sock.bind(("localhost", port))
|
37
39
|
sock.close()
|
38
40
|
return
|
39
41
|
except socket.error:
|
@@ -45,19 +47,21 @@ class OAuthCallbackHandler(http.server.BaseHTTPRequestHandler):
|
|
45
47
|
def do_GET(self):
|
46
48
|
"""Handle the callback from OAuth authorization"""
|
47
49
|
self.send_response(200)
|
48
|
-
self.send_header(
|
50
|
+
self.send_header("Content-type", "text/html")
|
49
51
|
self.end_headers()
|
50
|
-
|
52
|
+
|
51
53
|
# Parse the authorization code from query parameters
|
52
54
|
query_components = parse_qs(urllib.parse.urlparse(self.path).query)
|
53
|
-
|
55
|
+
|
54
56
|
# Store the code in the server instance
|
55
|
-
if
|
56
|
-
self.server.auth_code = query_components[
|
57
|
-
|
57
|
+
if "code" in query_components:
|
58
|
+
self.server.auth_code = query_components["code"][0] # type: ignore
|
59
|
+
|
58
60
|
# Display a success message to the user
|
59
|
-
self.wfile.write(
|
60
|
-
|
61
|
+
self.wfile.write(
|
62
|
+
b"Successfully authenticated to Looker OAuth! You can close this window."
|
63
|
+
)
|
64
|
+
|
61
65
|
# Shutdown the server
|
62
66
|
threading.Thread(target=self.server.shutdown).start()
|
63
67
|
|
@@ -65,18 +69,22 @@ class OAuthCallbackHandler(http.server.BaseHTTPRequestHandler):
|
|
65
69
|
"""Suppress logging of requests"""
|
66
70
|
pass
|
67
71
|
|
72
|
+
|
68
73
|
class OAuthCallbackServer(socketserver.TCPServer):
|
69
74
|
def __init__(self, server_address):
|
70
75
|
super().__init__(server_address, OAuthCallbackHandler)
|
71
76
|
self.auth_code: str | None = None
|
72
77
|
|
78
|
+
|
73
79
|
class LoginResponse(TypedDict):
|
74
80
|
auth_code: Optional[str]
|
75
81
|
code_verifier: Optional[str]
|
76
82
|
|
83
|
+
|
77
84
|
class OAuth2PKCE:
|
78
85
|
def __init__(self, new_token_callback: NewTokenCallback, use_production: bool):
|
79
86
|
from lkr.auth_service import DbOAuthSession
|
87
|
+
|
80
88
|
self.auth_code: Optional[str] = None
|
81
89
|
self.state = secrets.token_urlsafe(16)
|
82
90
|
self.new_token_callback: NewTokenCallback = new_token_callback
|
@@ -85,7 +93,7 @@ class OAuth2PKCE:
|
|
85
93
|
self.server: OAuthCallbackServer | None = None
|
86
94
|
self.port: int = 8000
|
87
95
|
self.use_production: bool = use_production
|
88
|
-
|
96
|
+
|
89
97
|
def cleanup(self):
|
90
98
|
"""Clean up the server and its thread."""
|
91
99
|
if self.server:
|
@@ -100,51 +108,91 @@ class OAuth2PKCE:
|
|
100
108
|
"""
|
101
109
|
Initiates the OAuth2 PKCE login flow by opening the browser with the authorization URL
|
102
110
|
and starting a local server to catch the callback.
|
103
|
-
|
111
|
+
|
104
112
|
Returns:
|
105
113
|
Optional[str]: The authorization code if successful, None otherwise
|
106
114
|
"""
|
107
115
|
from lkr.auth_service import get_auth_session
|
108
|
-
|
116
|
+
|
109
117
|
# Kill any process using port 8000
|
110
118
|
kill_process_on_port(self.port)
|
111
|
-
|
112
|
-
#
|
113
|
-
|
114
|
-
|
119
|
+
|
120
|
+
# Wait until the port is actually free and we can bind the real server (up to 20 seconds)
|
121
|
+
server_created = False
|
122
|
+
last_exception = None
|
123
|
+
for _ in range(20): # 20 x 1s = 20s
|
124
|
+
try:
|
125
|
+
self.server = OAuthCallbackServer(("localhost", self.port))
|
126
|
+
server_created = True
|
127
|
+
break
|
128
|
+
except OSError as e:
|
129
|
+
if getattr(e, "errno", None) == 48 or "Address already in use" in str(
|
130
|
+
e
|
131
|
+
):
|
132
|
+
last_exception = e
|
133
|
+
time.sleep(1)
|
134
|
+
else:
|
135
|
+
raise
|
136
|
+
if not server_created:
|
137
|
+
import logging
|
138
|
+
|
139
|
+
logging.error(
|
140
|
+
f"Failed to bind to port {self.port} after waiting: {last_exception}"
|
141
|
+
)
|
142
|
+
raise RuntimeError(
|
143
|
+
f"Failed to bind to port {self.port} after waiting: {last_exception}"
|
144
|
+
)
|
145
|
+
|
115
146
|
# Start the server in a separate thread
|
147
|
+
if self.server is None:
|
148
|
+
raise RuntimeError("Internal error: server was not created successfully.")
|
116
149
|
self.server_thread = threading.Thread(target=self.server.serve_forever)
|
117
150
|
self.server_thread.daemon = True
|
118
151
|
self.server_thread.start()
|
119
152
|
|
120
153
|
# Construct and open the OAuth URL
|
121
|
-
self.auth_session = get_auth_session(
|
122
|
-
|
123
|
-
|
154
|
+
self.auth_session = get_auth_session(
|
155
|
+
base_url, self.new_token_callback, use_production=self.use_production
|
156
|
+
)
|
157
|
+
oauth_url = self.auth_session.create_auth_code_request_url(
|
158
|
+
"cors_api", self.state
|
159
|
+
)
|
160
|
+
|
124
161
|
webbrowser.open(oauth_url)
|
125
|
-
|
162
|
+
|
126
163
|
# Wait for the callback
|
127
164
|
self.server_thread.join()
|
128
|
-
|
165
|
+
|
129
166
|
# Get the authorization code
|
130
|
-
|
167
|
+
if self.server is None:
|
168
|
+
raise RuntimeError("Internal error: server was not created successfully.")
|
169
|
+
return LoginResponse(
|
170
|
+
auth_code=self.server.auth_code,
|
171
|
+
code_verifier=self.auth_session.code_verifier,
|
172
|
+
)
|
131
173
|
|
132
174
|
def exchange_code_for_token(self):
|
133
175
|
"""
|
134
176
|
Exchange the authorization code for access and refresh tokens.
|
135
|
-
|
177
|
+
|
136
178
|
Args:
|
137
179
|
base_url: The base URL of the Looker instance
|
138
180
|
client_id: The OAuth client ID
|
139
|
-
|
181
|
+
|
140
182
|
Returns:
|
141
183
|
Dict containing access_token, refresh_token, token_type, and expires_in
|
142
184
|
"""
|
143
185
|
if not self.auth_code:
|
144
|
-
raise ValueError(
|
186
|
+
raise ValueError(
|
187
|
+
"No authorization code available. Must call initiate_login first."
|
188
|
+
)
|
145
189
|
if not self.auth_session:
|
146
|
-
raise ValueError(
|
147
|
-
|
190
|
+
raise ValueError(
|
191
|
+
"No auth session available. Must call initiate_login first."
|
192
|
+
)
|
193
|
+
self.auth_session.redeem_auth_code(
|
194
|
+
self.auth_code, self.auth_session.code_verifier
|
195
|
+
)
|
148
196
|
self.cleanup()
|
149
197
|
return self.auth_session.token
|
150
198
|
|
lkr/auth_service.py
CHANGED
@@ -1,26 +1,28 @@
|
|
1
1
|
import json
|
2
2
|
import os
|
3
3
|
import sqlite3
|
4
|
+
import types
|
4
5
|
from datetime import datetime, timedelta, timezone
|
5
6
|
from typing import List, Self, Tuple, Union
|
6
7
|
|
8
|
+
import requests
|
7
9
|
import typer
|
8
10
|
from looker_sdk.rtl import serialize
|
9
11
|
from looker_sdk.rtl.api_settings import ApiSettings, SettingsConfig
|
10
12
|
from looker_sdk.rtl.auth_session import AuthSession, CryptoHash, OAuthSession
|
11
13
|
from looker_sdk.rtl.auth_token import AccessToken, AuthToken
|
12
14
|
from looker_sdk.rtl.requests_transport import RequestsTransport
|
13
|
-
from looker_sdk.rtl.transport import HttpMethod
|
15
|
+
from looker_sdk.rtl.transport import LOOKER_API_ID, HttpMethod
|
14
16
|
from looker_sdk.sdk.api40.methods import Looker40SDK
|
15
17
|
from pydantic import BaseModel, Field, computed_field
|
16
18
|
from pydash import get
|
17
19
|
|
18
20
|
from lkr.classes import LkrCtxObj, LookerApiKey
|
19
21
|
from lkr.constants import LOOKER_API_VERSION, OAUTH_CLIENT_ID, OAUTH_REDIRECT_URI
|
20
|
-
from lkr.
|
21
|
-
from lkr.
|
22
|
+
from lkr.custom_types import NewTokenCallback
|
23
|
+
from lkr.logger import logger
|
22
24
|
|
23
|
-
__all__ = ["get_auth"]
|
25
|
+
__all__ = ["get_auth", "ApiKeyAuthSession", "DbOAuthSession"]
|
24
26
|
|
25
27
|
|
26
28
|
def get_auth(ctx: typer.Context | LkrCtxObj) -> Union["SqlLiteAuth", "ApiKeyAuth"]:
|
@@ -33,7 +35,7 @@ def get_auth(ctx: typer.Context | LkrCtxObj) -> Union["SqlLiteAuth", "ApiKeyAuth
|
|
33
35
|
raise typer.Exit(1)
|
34
36
|
if lkr_ctx.use_sdk == "api_key" and lkr_ctx.api_key:
|
35
37
|
logger.info("Using API key authentication")
|
36
|
-
return ApiKeyAuth(lkr_ctx.api_key)
|
38
|
+
return ApiKeyAuth(lkr_ctx.api_key, use_production=lkr_ctx.use_production)
|
37
39
|
else:
|
38
40
|
return SqlLiteAuth(lkr_ctx)
|
39
41
|
|
@@ -43,7 +45,6 @@ class ApiKeyApiSettings(ApiSettings):
|
|
43
45
|
self.api_key = api_key
|
44
46
|
super().__init__()
|
45
47
|
self.agent_tag = "lkr-cli-api-key"
|
46
|
-
self.headers = {"X-Looker-AppId": "lkr-cli"}
|
47
48
|
|
48
49
|
def read_config(self) -> SettingsConfig:
|
49
50
|
return SettingsConfig(
|
@@ -58,7 +59,6 @@ class OAuthApiSettings(ApiSettings):
|
|
58
59
|
self.base_url = base_url
|
59
60
|
super().__init__()
|
60
61
|
self.agent_tag = "lkr-cli-oauth"
|
61
|
-
self.headers = {"X-Looker-AppId": "lkr-cli"}
|
62
62
|
|
63
63
|
def read_config(self) -> SettingsConfig:
|
64
64
|
return SettingsConfig(
|
@@ -70,6 +70,36 @@ class OAuthApiSettings(ApiSettings):
|
|
70
70
|
)
|
71
71
|
|
72
72
|
|
73
|
+
class ApiKeyAuthSession(AuthSession):
|
74
|
+
def __init__(self, *args, use_production: bool, **kwargs):
|
75
|
+
super().__init__(*args, **kwargs)
|
76
|
+
self.use_production = use_production
|
77
|
+
|
78
|
+
def _login(self, *args, **kwargs):
|
79
|
+
super()._login(*args, **kwargs)
|
80
|
+
if not self.use_production:
|
81
|
+
self._switch_to_dev_mode()
|
82
|
+
|
83
|
+
def _switch_to_dev_mode(self):
|
84
|
+
logger.debug("Switching to dev mode")
|
85
|
+
config = self.settings.read_config()
|
86
|
+
if "base_url" in config:
|
87
|
+
url = f"{config['base_url']}/api/{LOOKER_API_VERSION}/session"
|
88
|
+
return self.transport.request(
|
89
|
+
method=HttpMethod.PATCH,
|
90
|
+
path=url,
|
91
|
+
body=json.dumps({"workspace_id": "dev"}).encode("utf-8"),
|
92
|
+
transport_options={
|
93
|
+
"headers": {
|
94
|
+
"Content-Type": "application/json",
|
95
|
+
"Authorization": f"Bearer {self.token.access_token}",
|
96
|
+
}
|
97
|
+
},
|
98
|
+
)
|
99
|
+
else:
|
100
|
+
raise ValueError("Base URL not found in settings")
|
101
|
+
|
102
|
+
|
73
103
|
class DbOAuthSession(OAuthSession):
|
74
104
|
def __init__(
|
75
105
|
self,
|
@@ -121,7 +151,7 @@ def get_auth_session(
|
|
121
151
|
access_token: AccessToken | None = None,
|
122
152
|
) -> DbOAuthSession:
|
123
153
|
settings = OAuthApiSettings(base_url)
|
124
|
-
transport =
|
154
|
+
transport = MonkeyPatchTransport.configure(settings)
|
125
155
|
auth = DbOAuthSession(
|
126
156
|
settings=settings,
|
127
157
|
transport=transport,
|
@@ -137,18 +167,19 @@ def get_auth_session(
|
|
137
167
|
return auth
|
138
168
|
|
139
169
|
|
140
|
-
def init_api_key_sdk(api_key: LookerApiKey) -> Looker40SDK:
|
170
|
+
def init_api_key_sdk(api_key: LookerApiKey, use_production: bool) -> Looker40SDK:
|
141
171
|
from looker_sdk.rtl import serialize
|
142
172
|
|
143
173
|
settings = ApiKeyApiSettings(api_key)
|
144
174
|
settings.is_configured()
|
145
175
|
transport = RequestsTransport.configure(settings)
|
146
176
|
return Looker40SDK(
|
147
|
-
auth=
|
177
|
+
auth=ApiKeyAuthSession(
|
148
178
|
settings,
|
149
179
|
transport,
|
150
180
|
serialize.deserialize40, # type: ignore
|
151
181
|
LOOKER_API_VERSION,
|
182
|
+
use_production=use_production,
|
152
183
|
),
|
153
184
|
deserialize=serialize.deserialize40, # type: ignore
|
154
185
|
serialize=serialize.serialize40, # type: ignore
|
@@ -157,6 +188,29 @@ def init_api_key_sdk(api_key: LookerApiKey) -> Looker40SDK:
|
|
157
188
|
)
|
158
189
|
|
159
190
|
|
191
|
+
# monkey patch to remove the LOOKER_API_ID header when exchanging the code for a token
|
192
|
+
def monkey_patch_prepare_request(session: requests.Session):
|
193
|
+
original_prepare_request = session.prepare_request
|
194
|
+
|
195
|
+
def prepare_request(self, request, *args, **kwargs):
|
196
|
+
x = original_prepare_request(request, *args, **kwargs)
|
197
|
+
if (
|
198
|
+
x.headers.get(LOOKER_API_ID)
|
199
|
+
and x.path_url.endswith("/api/token")
|
200
|
+
and request.method == "POST"
|
201
|
+
):
|
202
|
+
x.headers.pop(LOOKER_API_ID)
|
203
|
+
return x
|
204
|
+
|
205
|
+
session.prepare_request = types.MethodType(prepare_request, session)
|
206
|
+
|
207
|
+
|
208
|
+
class MonkeyPatchTransport(RequestsTransport):
|
209
|
+
def __init__(self, *args, **kwargs):
|
210
|
+
super().__init__(*args, **kwargs)
|
211
|
+
monkey_patch_prepare_request(self.session)
|
212
|
+
|
213
|
+
|
160
214
|
def init_oauth_sdk(
|
161
215
|
base_url: str,
|
162
216
|
new_token_callback: NewTokenCallback,
|
@@ -167,7 +221,8 @@ def init_oauth_sdk(
|
|
167
221
|
"""Default dependency configuration"""
|
168
222
|
settings = OAuthApiSettings(base_url)
|
169
223
|
settings.is_configured()
|
170
|
-
transport =
|
224
|
+
transport = MonkeyPatchTransport.configure(settings)
|
225
|
+
|
171
226
|
auth = get_auth_session(
|
172
227
|
base_url,
|
173
228
|
new_token_callback,
|
@@ -395,9 +450,7 @@ class SqlLiteAuth:
|
|
395
450
|
return current_auth.instance_name
|
396
451
|
return None
|
397
452
|
|
398
|
-
def get_current_sdk(
|
399
|
-
self, prompt_refresh_invalid_token: bool = False
|
400
|
-
) -> Looker40SDK:
|
453
|
+
def get_current_sdk(self, prompt_refresh_invalid_token: bool = True) -> Looker40SDK:
|
401
454
|
current_auth = self._get_current_auth()
|
402
455
|
if current_auth:
|
403
456
|
if not current_auth.valid_refresh_token:
|
@@ -471,7 +524,7 @@ class SqlLiteAuth:
|
|
471
524
|
if not token:
|
472
525
|
raise InvalidRefreshTokenError(current_auth.instance_name)
|
473
526
|
else:
|
474
|
-
from lkr.
|
527
|
+
from lkr.logger import logger
|
475
528
|
|
476
529
|
logger.info(
|
477
530
|
f"Successfully refreshed token for {current_auth.instance_name}"
|
@@ -480,8 +533,9 @@ class SqlLiteAuth:
|
|
480
533
|
|
481
534
|
|
482
535
|
class ApiKeyAuth:
|
483
|
-
def __init__(self, api_key: LookerApiKey):
|
536
|
+
def __init__(self, api_key: LookerApiKey, use_production: bool):
|
484
537
|
self.api_key = api_key
|
538
|
+
self.use_production = use_production
|
485
539
|
|
486
540
|
def __enter__(self):
|
487
541
|
return self
|
@@ -518,7 +572,7 @@ class ApiKeyAuth:
|
|
518
572
|
)
|
519
573
|
|
520
574
|
def get_current_sdk(self, **kwargs) -> Looker40SDK:
|
521
|
-
return init_api_key_sdk(self.api_key)
|
575
|
+
return init_api_key_sdk(self.api_key, self.use_production)
|
522
576
|
|
523
577
|
def get_current_instance(self) -> str | None:
|
524
578
|
raise NotImplementedError(
|
lkr/classes.py
CHANGED
@@ -13,24 +13,25 @@ class LookerApiKey(BaseModel):
|
|
13
13
|
def from_env(cls):
|
14
14
|
try:
|
15
15
|
return cls(
|
16
|
-
client_id=os.environ.get("LOOKERSDK_CLIENT_ID"),
|
17
|
-
client_secret=os.environ.get("LOOKERSDK_CLIENT_SECRET"),
|
18
|
-
base_url=os.environ.get("LOOKERSDK_BASE_URL"),
|
16
|
+
client_id=os.environ.get("LOOKERSDK_CLIENT_ID"), # type: ignore
|
17
|
+
client_secret=os.environ.get("LOOKERSDK_CLIENT_SECRET"), # type: ignore
|
18
|
+
base_url=os.environ.get("LOOKERSDK_BASE_URL"), # type: ignore
|
19
19
|
)
|
20
20
|
except Exception:
|
21
21
|
return None
|
22
22
|
|
23
|
-
|
23
|
+
|
24
24
|
class LkrCtxObj(BaseModel):
|
25
25
|
api_key: LookerApiKey | None
|
26
26
|
force_oauth: bool = False
|
27
|
-
|
27
|
+
use_production: bool = True
|
28
|
+
|
28
29
|
@property
|
29
30
|
def use_sdk(self) -> Literal["oauth", "api_key"]:
|
30
31
|
if self.force_oauth:
|
31
32
|
return "oauth"
|
32
33
|
return "api_key" if self.api_key else "oauth"
|
33
|
-
|
34
|
+
|
34
35
|
def __init__(self, api_key: LookerApiKey | None = None, *args, **kwargs):
|
35
36
|
super().__init__(api_key=api_key, *args, **kwargs)
|
36
37
|
if not self.api_key:
|
lkr/{logging.py → logger.py}
RENAMED
@@ -6,16 +6,18 @@ from rich.console import Console
|
|
6
6
|
from rich.logging import RichHandler
|
7
7
|
from rich.theme import Theme
|
8
8
|
|
9
|
-
from lkr.
|
9
|
+
from lkr.custom_types import LogLevel
|
10
10
|
|
11
11
|
# Define a custom theme for our logging
|
12
|
-
theme = Theme(
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
12
|
+
theme = Theme(
|
13
|
+
{
|
14
|
+
"logging.level.debug": "dim blue",
|
15
|
+
"logging.level.info": "bold green",
|
16
|
+
"logging.level.warning": "bold yellow",
|
17
|
+
"logging.level.error": "bold red",
|
18
|
+
"logging.level.critical": "bold white on red",
|
19
|
+
}
|
20
|
+
)
|
19
21
|
|
20
22
|
# Create a console for logging
|
21
23
|
console = Console(theme=theme)
|
@@ -36,7 +38,9 @@ log_level = os.getenv("LOG_LEVEL", DEFAULT_LOG_LEVEL).upper()
|
|
36
38
|
|
37
39
|
# Configure the root logger
|
38
40
|
logging.basicConfig(
|
39
|
-
level=getattr(
|
41
|
+
level=getattr(
|
42
|
+
logging, log_level, logging.INFO
|
43
|
+
), # Fallback to INFO if invalid level
|
40
44
|
format="%(message)s",
|
41
45
|
datefmt="[%X]",
|
42
46
|
handlers=[handler],
|
@@ -51,9 +55,12 @@ requests_logger = logging.getLogger("looker_sdk.rtl.requests_transport")
|
|
51
55
|
if log_level != "DEBUG":
|
52
56
|
requests_logger.setLevel(logging.WARNING)
|
53
57
|
|
58
|
+
|
54
59
|
def set_log_level(level: LogLevel):
|
55
60
|
"""Set the logging level for the application."""
|
56
61
|
logger.setLevel(getattr(logging, level.value))
|
57
62
|
logging.getLogger("lkr.structured").setLevel(getattr(logging, level.value))
|
58
63
|
# Update requests_transport logger level based on the new level
|
59
|
-
requests_logger.setLevel(
|
64
|
+
requests_logger.setLevel(
|
65
|
+
logging.DEBUG if level == LogLevel.DEBUG else logging.WARNING
|
66
|
+
)
|
lkr/main.py
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
import os
|
2
|
-
from typing import Annotated
|
2
|
+
from typing import Annotated, Optional
|
3
3
|
|
4
4
|
import typer
|
5
5
|
|
6
6
|
from lkr.auth.main import group as auth_group
|
7
7
|
from lkr.classes import LkrCtxObj
|
8
|
-
from lkr.
|
8
|
+
from lkr.custom_types import LogLevel
|
9
|
+
from lkr.logger import logger
|
9
10
|
from lkr.mcp.main import group as mcp_group
|
10
11
|
from lkr.observability.main import group as observability_group
|
11
|
-
from lkr.types import LogLevel
|
12
12
|
|
13
13
|
app = typer.Typer(
|
14
14
|
name="lkr", help="LookML Repository CLI", add_completion=True, no_args_is_help=True
|
@@ -30,6 +30,7 @@ def callback(
|
|
30
30
|
log_level: Annotated[LogLevel | None, typer.Option(envvar="LOG_LEVEL")] = None,
|
31
31
|
quiet: Annotated[bool, typer.Option("--quiet")] = False,
|
32
32
|
force_oauth: Annotated[bool, typer.Option("--force-oauth")] = False,
|
33
|
+
dev: Annotated[Optional[bool], typer.Option("--dev")] = None,
|
33
34
|
):
|
34
35
|
if client_id:
|
35
36
|
os.environ["LOOKERSDK_CLIENT_ID"] = client_id
|
@@ -43,13 +44,22 @@ def callback(
|
|
43
44
|
# Initialize ctx.obj as a dictionary if it's None
|
44
45
|
if ctx.obj is None:
|
45
46
|
ctx.obj = {}
|
46
|
-
|
47
|
+
|
48
|
+
ctx_obj = LkrCtxObj(
|
49
|
+
force_oauth=force_oauth,
|
50
|
+
use_production=not dev if dev is not None else True,
|
51
|
+
)
|
52
|
+
ctx.obj["ctx_lkr"] = ctx_obj
|
53
|
+
# if the user passes --dev, but lkrCtxObj.use_sdk is oauth, then we need to log a warning saying we're ignoring the --dev flag
|
54
|
+
if dev and ctx_obj.use_sdk == "oauth":
|
55
|
+
logger.warning("Ignoring --dev flag because OAuth token tracks dev/prod mode.")
|
56
|
+
|
47
57
|
if log_level:
|
48
|
-
from lkr.
|
58
|
+
from lkr.logger import set_log_level
|
49
59
|
|
50
60
|
set_log_level(log_level)
|
51
61
|
if quiet:
|
52
|
-
from lkr.
|
62
|
+
from lkr.logger import set_log_level
|
53
63
|
|
54
64
|
set_log_level(LogLevel.ERROR)
|
55
65
|
|
lkr/mcp/main.py
CHANGED
@@ -17,7 +17,7 @@ from pydash import get
|
|
17
17
|
|
18
18
|
from lkr.auth_service import get_auth
|
19
19
|
from lkr.classes import LkrCtxObj
|
20
|
-
from lkr.
|
20
|
+
from lkr.logger import logger
|
21
21
|
|
22
22
|
__all__ = ["group"]
|
23
23
|
|
@@ -101,7 +101,7 @@ class SpectaclesRequest(BaseModel):
|
|
101
101
|
|
102
102
|
|
103
103
|
def get_mcp_sdk(ctx: LkrCtxObj | typer.Context):
|
104
|
-
sdk = get_auth(ctx).get_current_sdk()
|
104
|
+
sdk = get_auth(ctx).get_current_sdk(prompt_refresh_invalid_token=False)
|
105
105
|
sdk.auth.settings.agent_tag += "-mcp"
|
106
106
|
return sdk
|
107
107
|
|
lkr/observability/classes.py
CHANGED
@@ -8,7 +8,7 @@ from looker_sdk.sdk.api40.methods import LookerSDK
|
|
8
8
|
from looker_sdk.sdk.api40.models import EmbedSsoParams
|
9
9
|
from pydantic import BaseModel, ConfigDict, Field
|
10
10
|
|
11
|
-
from lkr.
|
11
|
+
from lkr.logger import structured_logger
|
12
12
|
|
13
13
|
DEFAULT_PERMISSIONS = [
|
14
14
|
"access_data",
|
@@ -84,14 +84,6 @@ class LogEvent(BaseModel):
|
|
84
84
|
dashboard_id: str
|
85
85
|
|
86
86
|
|
87
|
-
class EventCollector:
|
88
|
-
def __init__(self):
|
89
|
-
self.events = []
|
90
|
-
|
91
|
-
def log_event(self, event: dict[str, Any], event_type: str):
|
92
|
-
self.events.append(LogEvent(**event, event_type=event_type))
|
93
|
-
|
94
|
-
|
95
87
|
class ObservabilityCtxObj(BaseModel):
|
96
88
|
event_prefix: str = Field(
|
97
89
|
default="lkr-observability", description="The prefix of the event", min_length=1
|
@@ -32,10 +32,12 @@
|
|
32
32
|
const urlParams = new URLSearchParams(window.location.search);
|
33
33
|
const iframeUrl = urlParams.get('iframe_url');
|
34
34
|
const origin = new URL(iframeUrl).origin;
|
35
|
-
console.log(origin)
|
36
|
-
console.log(iframeUrl)
|
37
35
|
const sessionId = urlParams.get('session_id');
|
36
|
+
const debug = urlParams.get('debug');
|
38
37
|
// Set the iframe source
|
38
|
+
if (debug) {
|
39
|
+
console.log({debug, iframeUrl, origin, sessionId})
|
40
|
+
}
|
39
41
|
document.getElementById('looker-iframe').src = iframeUrl;
|
40
42
|
|
41
43
|
// Track which events we've received
|
@@ -55,7 +57,7 @@
|
|
55
57
|
return;
|
56
58
|
}
|
57
59
|
const {type, ...data} = JSON.parse(event.data)
|
58
|
-
if (!trackedEvents.has(type)) {
|
60
|
+
if (!trackedEvents.has(type) && !debug) {
|
59
61
|
return;
|
60
62
|
}
|
61
63
|
if (type) {
|
@@ -65,6 +67,9 @@
|
|
65
67
|
event_data: data,
|
66
68
|
timestamp: now.toISOString(),
|
67
69
|
};
|
70
|
+
if (debug) {
|
71
|
+
console.log(eventData)
|
72
|
+
}
|
68
73
|
// Send event data to the server
|
69
74
|
fetch(`/log_event?session_id=${sessionId}`, {
|
70
75
|
method: 'POST',
|
lkr/observability/main.py
CHANGED
@@ -17,7 +17,7 @@ from selenium.webdriver.remote.webdriver import WebDriver
|
|
17
17
|
from selenium.webdriver.support import expected_conditions as EC
|
18
18
|
from selenium.webdriver.support.ui import WebDriverWait
|
19
19
|
|
20
|
-
from lkr.
|
20
|
+
from lkr.logger import structured_logger
|
21
21
|
from lkr.observability.classes import (
|
22
22
|
EmbedSDKObj,
|
23
23
|
IframeRequestEvent,
|
@@ -158,14 +158,30 @@ def health_check(
|
|
158
158
|
params.model_dump(mode="json"), "health_check_start", session_id
|
159
159
|
)
|
160
160
|
chrome_options = Options()
|
161
|
+
chrome_options.set_capability("goog:loggingPrefs", {"browser": "ALL"})
|
161
162
|
chrome_options.add_argument("--headless=new")
|
162
163
|
chrome_options.add_argument("--no-sandbox")
|
164
|
+
chrome_options.add_argument("--disable-dev-shm-usage")
|
165
|
+
chrome_options.add_argument("--disable-gpu")
|
166
|
+
chrome_options.add_argument("--enable-logging")
|
167
|
+
chrome_options.add_argument("--v=1")
|
168
|
+
|
169
|
+
chrome_options.add_experimental_option(
|
170
|
+
"prefs",
|
171
|
+
{
|
172
|
+
"profile.default_content_settings.cookies": 1,
|
173
|
+
"profile.cookie_controls_mode": 0,
|
174
|
+
},
|
175
|
+
)
|
163
176
|
driver = webdriver.Chrome(options=chrome_options)
|
164
177
|
url = observability_ctx.sdk.create_sso_embed_url(
|
165
178
|
body=params.to_embed_sso_params(
|
166
179
|
observability_ctx.origin, observability_ctx.base_url or ""
|
167
180
|
)
|
168
181
|
)
|
182
|
+
observability_ctx.log_event(
|
183
|
+
{"sso_url": url.url}, "create_sso_embed_url", session_id
|
184
|
+
)
|
169
185
|
|
170
186
|
if not (url and url.url):
|
171
187
|
raise HTTPException(status_code=500, detail="No URL found")
|
@@ -175,11 +191,16 @@ def health_check(
|
|
175
191
|
else:
|
176
192
|
quoted_url = quote(url.url, safe="")
|
177
193
|
embed_url = f"{observability_ctx.origin}/?iframe_url={quoted_url}&session_id={session_id}"
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
194
|
+
driver.get(embed_url)
|
195
|
+
observability_ctx.log_event(
|
196
|
+
{"url": embed_url}, "chromium_driver_get", session_id
|
197
|
+
)
|
198
|
+
WebDriverWait(driver, observability_ctx.timeout).until(
|
199
|
+
EC.presence_of_element_located((By.ID, "completion-indicator"))
|
200
|
+
)
|
201
|
+
observability_ctx.log_event(
|
202
|
+
{"session_id": session_id}, "chromium_driver_get_complete", session_id
|
203
|
+
)
|
183
204
|
|
184
205
|
except TimeoutException:
|
185
206
|
observability_ctx.log_event(
|
@@ -209,6 +230,9 @@ def health_check(
|
|
209
230
|
session_id,
|
210
231
|
)
|
211
232
|
finally:
|
233
|
+
observability_ctx.log_event(
|
234
|
+
{"session_id": session_id}, "health_check_complete", session_id
|
235
|
+
)
|
212
236
|
if driver:
|
213
237
|
driver.quit()
|
214
238
|
if not redirect:
|
@@ -3,10 +3,9 @@ import re
|
|
3
3
|
from datetime import datetime, timezone
|
4
4
|
from typing import Dict, List, Tuple
|
5
5
|
|
6
|
-
from pydantic import BaseModel, Field
|
7
6
|
from pydash import set_
|
8
7
|
|
9
|
-
from lkr.
|
8
|
+
from lkr.logger import logger
|
10
9
|
|
11
10
|
MAX_SESSION_LENGTH = 2592000
|
12
11
|
|
@@ -75,30 +74,6 @@ def format_attributes(
|
|
75
74
|
def now():
|
76
75
|
return datetime.now(timezone.utc)
|
77
76
|
|
77
|
+
|
78
78
|
def ms_diff(start: datetime):
|
79
79
|
return int((now() - start).total_seconds() * 1000)
|
80
|
-
|
81
|
-
class LogEventResponse(BaseModel):
|
82
|
-
timestamp: datetime = Field(default_factory=now)
|
83
|
-
event_name: str
|
84
|
-
event_prefix: str
|
85
|
-
|
86
|
-
def make_previous(self):
|
87
|
-
return dict(
|
88
|
-
previous_event_name=self.event_name,
|
89
|
-
previous_event_timestamp=self.timestamp,
|
90
|
-
previous_event_duration_ms=ms_diff(self.timestamp),
|
91
|
-
)
|
92
|
-
|
93
|
-
def log_event(event_name: str, prefix: str, **kwargs):
|
94
|
-
if "timestamp" not in kwargs:
|
95
|
-
kwargs["timestamp"] = now().isoformat()
|
96
|
-
logger.info(
|
97
|
-
f"{prefix}:{event_name}",
|
98
|
-
**kwargs
|
99
|
-
)
|
100
|
-
return LogEventResponse(
|
101
|
-
timestamp=datetime.fromisoformat(kwargs["timestamp"]),
|
102
|
-
event_name=event_name,
|
103
|
-
event_prefix=prefix
|
104
|
-
)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
lkr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
lkr/auth_service.py,sha256=NIGlSVtzS3ajpWYx2gTJVDknuF_KxwYsJEj88Hc05dQ,19887
|
3
|
+
lkr/classes.py,sha256=f2TJOXFta0s8LJLEXOqPdWPLg-EIIntUSDS6gDOon7M,1163
|
4
|
+
lkr/constants.py,sha256=DdCfsV6q8wgs2iHpIQeb6oDP_2XejusEHyPvCbaM3yY,108
|
5
|
+
lkr/custom_types.py,sha256=feJ-W2U61PJTiotMLuZJqxrotA53er95kO1O30mooy4,323
|
6
|
+
lkr/exceptions.py,sha256=M_aR4YaCZtY4wyxhcoqJCVkxVu9z3Wwo5KgSDyOoEnI,210
|
7
|
+
lkr/logger.py,sha256=wLEVluF-2lVMPJ228D7O4-shHaydEGXmn47j567nUgw,1887
|
8
|
+
lkr/main.py,sha256=wpnSiCpIScEi-jmrLuwcSJrMX3tMkuaYNwlvNkVf5N8,2375
|
9
|
+
lkr/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
lkr/auth/main.py,sha256=7tWGPWokzbBnrX1enZ9YP4rdDJqYBlGfuYe0Wg-fXT4,7532
|
11
|
+
lkr/auth/oauth.py,sha256=n2yAcccdBZaloGVtFRTwCPBfh1cvVYNbXLsFCxmWc5M,7207
|
12
|
+
lkr/mcp/main.py,sha256=fz7Bvbkj1CTD185lGwNdMYP7bOTDadWL1xdD4JZW8qQ,22886
|
13
|
+
lkr/observability/classes.py,sha256=LgGuUnY-J1csPrlAKnw4PPOqOfbvaOx2cxENlQgJYcE,5816
|
14
|
+
lkr/observability/embed_container.html,sha256=IcDG-QVsYYNGQGrkDrx9OMZ2Pmo4C8oAjRHddFQ7Tlw,2939
|
15
|
+
lkr/observability/main.py,sha256=XbejIdqhNNUMqHVezb5EnLaJ32dO9-Bt0o5d8lc0kyw,9544
|
16
|
+
lkr/observability/utils.py,sha256=UpaBrp_ufaXLoz4p3xG3K6lHKBpP9wBhvP8rDmeGoWg,2148
|
17
|
+
lkr_dev_cli-0.0.24.dist-info/METADATA,sha256=PKOjKBiQqRlg4nfYzcFjbjYHSzaRNCslIlBiBGaaZPw,10663
|
18
|
+
lkr_dev_cli-0.0.24.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
19
|
+
lkr_dev_cli-0.0.24.dist-info/entry_points.txt,sha256=nn2sFMGDpwUVE61ZUpbDPnQZkW7Gc08nV-tyLGo8q34,37
|
20
|
+
lkr_dev_cli-0.0.24.dist-info/licenses/LICENSE,sha256=hKnCOORW1JRE_M2vStz8dblS5u1iR-2VpqS9xagKNa0,1063
|
21
|
+
lkr_dev_cli-0.0.24.dist-info/RECORD,,
|
@@ -1,21 +0,0 @@
|
|
1
|
-
lkr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
lkr/auth_service.py,sha256=_enIsYxpL1IILtBfNG4YAA_H9cqwnwcvrGMyfmzgH7U,17815
|
3
|
-
lkr/classes.py,sha256=0NKttc5kAhW_omuxwnN9Jy6oQnRKgI7b2c7HZUfI0lc,1144
|
4
|
-
lkr/constants.py,sha256=DdCfsV6q8wgs2iHpIQeb6oDP_2XejusEHyPvCbaM3yY,108
|
5
|
-
lkr/exceptions.py,sha256=M_aR4YaCZtY4wyxhcoqJCVkxVu9z3Wwo5KgSDyOoEnI,210
|
6
|
-
lkr/logging.py,sha256=bBGdxkVrUidRTJAvqGHyQ5KS6IJGYDCoool5tTB_JxM,1822
|
7
|
-
lkr/main.py,sha256=5m036LAS4PTLIzpWW5ohxIs2FGYtVf-OwYnTmtrpnxU,1933
|
8
|
-
lkr/types.py,sha256=feJ-W2U61PJTiotMLuZJqxrotA53er95kO1O30mooy4,323
|
9
|
-
lkr/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
-
lkr/auth/main.py,sha256=ebGp-NdgULodHaDrLKspnGSFf_sOkV2Vanil1NbOJKY,7382
|
11
|
-
lkr/auth/oauth.py,sha256=-QCb7YGIqmuTiy6SZcUTbVZrDo7itOpUUOK1VuvfMqM,5987
|
12
|
-
lkr/embed/observability/utils.py,sha256=Hv3g60cI03cQwyEe8QV7bUMssE6pyrZKnDyl9rxm5b8,2915
|
13
|
-
lkr/mcp/main.py,sha256=39VO0_bdscrCW7BC3kdlgjfIWaNigpT38HZ74_Drm-A,22853
|
14
|
-
lkr/observability/classes.py,sha256=5kQnHxa3pgUartrbccuG0yMClfoFqYCXY8iCTJTZvgk,6025
|
15
|
-
lkr/observability/embed_container.html,sha256=BMzV_jfBhcussUExf0KccOmOTAO3UEV7SuZ4lznQ0no,2758
|
16
|
-
lkr/observability/main.py,sha256=6xAcaW4ruxGZDiGHNTi0oIaQnI6HhKxsjCZ5jq_ldsE,8557
|
17
|
-
lkr_dev_cli-0.0.22.dist-info/METADATA,sha256=Yp81tfskRQC4vKRCecxXGmmBK7eiT5Djf_fBzj9VFyo,10663
|
18
|
-
lkr_dev_cli-0.0.22.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
19
|
-
lkr_dev_cli-0.0.22.dist-info/entry_points.txt,sha256=nn2sFMGDpwUVE61ZUpbDPnQZkW7Gc08nV-tyLGo8q34,37
|
20
|
-
lkr_dev_cli-0.0.22.dist-info/licenses/LICENSE,sha256=hKnCOORW1JRE_M2vStz8dblS5u1iR-2VpqS9xagKNa0,1063
|
21
|
-
lkr_dev_cli-0.0.22.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|