superset-cli 0.1.0__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.
- superset_cli/__init__.py +3 -0
- superset_cli/auth.py +243 -0
- superset_cli/cli.py +2282 -0
- superset_cli/client.py +652 -0
- superset_cli/config.py +41 -0
- superset_cli/main.py +9 -0
- superset_cli/models.py +20 -0
- superset_cli-0.1.0.dist-info/METADATA +288 -0
- superset_cli-0.1.0.dist-info/RECORD +12 -0
- superset_cli-0.1.0.dist-info/WHEEL +4 -0
- superset_cli-0.1.0.dist-info/entry_points.txt +2 -0
- superset_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
superset_cli/__init__.py
ADDED
superset_cli/auth.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from http.cookiejar import Cookie, CookieJar
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Callable
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
import browser_cookie3
|
|
13
|
+
|
|
14
|
+
from superset_cli.client import load_storage_state
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
SUPPORTED_BROWSERS: tuple[str, ...] = (
|
|
18
|
+
"auto",
|
|
19
|
+
"chrome",
|
|
20
|
+
"edge",
|
|
21
|
+
"brave",
|
|
22
|
+
"firefox",
|
|
23
|
+
"zen",
|
|
24
|
+
"safari",
|
|
25
|
+
)
|
|
26
|
+
DEFAULT_BROWSER = "auto"
|
|
27
|
+
_AUTO_ORDER: tuple[str, ...] = ("chrome", "edge", "brave", "firefox", "zen", "safari")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NoCookiesFoundError(Exception):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_instance_dir(*, state_dir: Path, instance_name: str) -> Path:
|
|
35
|
+
return state_dir / instance_name
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_storage_state_path(*, state_dir: Path, instance_name: str) -> Path:
|
|
39
|
+
return get_instance_dir(state_dir=state_dir, instance_name=instance_name) / "storage-state.json"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_auth_status(*, storage_state_path: Path) -> dict:
|
|
43
|
+
if not storage_state_path.exists():
|
|
44
|
+
return {
|
|
45
|
+
"authenticated": False,
|
|
46
|
+
"storage_state_path": str(storage_state_path),
|
|
47
|
+
"cookie_count": 0,
|
|
48
|
+
"earliest_cookie_expiry": None,
|
|
49
|
+
"expired": False,
|
|
50
|
+
"session_only": True,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
state = load_storage_state(storage_state_path)
|
|
54
|
+
positive_expiries = [c.expires for c in state.cookies if c.expires is not None and c.expires > 0]
|
|
55
|
+
earliest = min(positive_expiries) if positive_expiries else None
|
|
56
|
+
expired = earliest is not None and earliest < time.time()
|
|
57
|
+
return {
|
|
58
|
+
"authenticated": True,
|
|
59
|
+
"storage_state_path": str(storage_state_path),
|
|
60
|
+
"cookie_count": len(state.cookies),
|
|
61
|
+
"earliest_cookie_expiry": earliest,
|
|
62
|
+
"expired": expired,
|
|
63
|
+
"session_only": earliest is None,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def format_expiry(value: float | None) -> str:
|
|
68
|
+
if value is None:
|
|
69
|
+
return "session-only"
|
|
70
|
+
return datetime.fromtimestamp(value, tz=timezone.utc).isoformat()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def remove_auth_state(*, state_dir: Path, instance_name: str) -> Path:
|
|
74
|
+
instance_dir = get_instance_dir(state_dir=state_dir, instance_name=instance_name)
|
|
75
|
+
shutil.rmtree(instance_dir, ignore_errors=True)
|
|
76
|
+
return instance_dir
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _zen_profile_roots() -> list[Path]:
|
|
80
|
+
if sys.platform == "darwin":
|
|
81
|
+
return [Path.home() / "Library" / "Application Support" / "zen" / "Profiles"]
|
|
82
|
+
if sys.platform.startswith("linux"):
|
|
83
|
+
return [Path.home() / ".zen", Path.home() / ".var/app/io.github.zen_browser.zen/.zen"]
|
|
84
|
+
if sys.platform == "win32":
|
|
85
|
+
appdata = os.environ.get("APPDATA")
|
|
86
|
+
return [Path(appdata) / "zen" / "Profiles"] if appdata else []
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _resolve_zen_cookies_path() -> Path | None:
|
|
91
|
+
candidates: list[Path] = []
|
|
92
|
+
for root in _zen_profile_roots():
|
|
93
|
+
if not root.exists():
|
|
94
|
+
continue
|
|
95
|
+
candidates.extend(root.glob("*/cookies.sqlite"))
|
|
96
|
+
if not candidates:
|
|
97
|
+
return None
|
|
98
|
+
return max(candidates, key=lambda p: p.stat().st_mtime)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _load_zen_cookies(domain_name: str = "") -> CookieJar:
|
|
102
|
+
path = _resolve_zen_cookies_path()
|
|
103
|
+
if path is None:
|
|
104
|
+
raise FileNotFoundError(
|
|
105
|
+
"Could not locate Zen Browser cookies.sqlite. Expected to find a Firefox-format "
|
|
106
|
+
"profile under your Zen Browser data directory."
|
|
107
|
+
)
|
|
108
|
+
return browser_cookie3.firefox(cookie_file=str(path), domain_name=domain_name)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
_LOADERS: dict[str, Callable[[str], CookieJar]] = {
|
|
112
|
+
"chrome": lambda d: browser_cookie3.chrome(domain_name=d),
|
|
113
|
+
"edge": lambda d: browser_cookie3.edge(domain_name=d),
|
|
114
|
+
"brave": lambda d: browser_cookie3.brave(domain_name=d),
|
|
115
|
+
"firefox": lambda d: browser_cookie3.firefox(domain_name=d),
|
|
116
|
+
"zen": lambda d: _load_zen_cookies(domain_name=d),
|
|
117
|
+
"safari": lambda d: browser_cookie3.safari(domain_name=d),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _hostname_of(base_url: str) -> str:
|
|
122
|
+
host = urlparse(base_url).hostname
|
|
123
|
+
if not host:
|
|
124
|
+
raise ValueError(f"base_url has no hostname: {base_url}")
|
|
125
|
+
return host
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _cookie_matches_host(cookie_domain: str, host: str) -> bool:
|
|
129
|
+
cd = cookie_domain.lstrip(".")
|
|
130
|
+
if not cd:
|
|
131
|
+
return False
|
|
132
|
+
return host == cd or host.endswith("." + cd)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _serialise_cookie(c: Cookie) -> dict:
|
|
136
|
+
return {
|
|
137
|
+
"name": c.name,
|
|
138
|
+
"value": c.value or "",
|
|
139
|
+
"domain": c.domain,
|
|
140
|
+
"path": c.path or "/",
|
|
141
|
+
"expires": float(c.expires) if c.expires is not None else None,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _filter_cookies(jar: CookieJar, host: str) -> list[Cookie]:
|
|
146
|
+
return [c for c in jar if _cookie_matches_host(c.domain, host)]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _try_loader(name: str, host: str) -> list[Cookie]:
|
|
150
|
+
loader = _LOADERS[name]
|
|
151
|
+
jar = loader(host)
|
|
152
|
+
return _filter_cookies(jar, host)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _format_loader_errors(errors: dict[str, Exception]) -> str:
|
|
156
|
+
if not errors:
|
|
157
|
+
return ""
|
|
158
|
+
parts = [f"{name}: {exc}" for name, exc in errors.items()]
|
|
159
|
+
return " Loader errors — " + "; ".join(parts) + "."
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _write_storage_state(storage_state_path: Path, cookies: list[Cookie]) -> None:
|
|
163
|
+
storage_state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
payload = {
|
|
165
|
+
"cookies": [_serialise_cookie(c) for c in cookies],
|
|
166
|
+
"origins": [],
|
|
167
|
+
}
|
|
168
|
+
storage_state_path.write_text(json.dumps(payload, separators=(",", ":")))
|
|
169
|
+
try:
|
|
170
|
+
os.chmod(storage_state_path, 0o600)
|
|
171
|
+
except OSError:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def import_browser_cookies(
|
|
176
|
+
*,
|
|
177
|
+
base_url: str,
|
|
178
|
+
storage_state_path: Path,
|
|
179
|
+
browser: str = DEFAULT_BROWSER,
|
|
180
|
+
validate: Callable[[Path], bool] | None = None,
|
|
181
|
+
) -> dict:
|
|
182
|
+
if browser not in SUPPORTED_BROWSERS:
|
|
183
|
+
raise ValueError(
|
|
184
|
+
f"Unsupported browser '{browser}'. Choose one of: {', '.join(SUPPORTED_BROWSERS)}."
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
host = _hostname_of(base_url)
|
|
188
|
+
|
|
189
|
+
def accepted(cookies: list[Cookie]) -> bool:
|
|
190
|
+
_write_storage_state(storage_state_path, cookies)
|
|
191
|
+
return validate is None or validate(storage_state_path)
|
|
192
|
+
|
|
193
|
+
if browser == "auto":
|
|
194
|
+
errors: dict[str, Exception] = {}
|
|
195
|
+
rejected: list[str] = []
|
|
196
|
+
picked: tuple[str, list[Cookie]] | None = None
|
|
197
|
+
for name in _AUTO_ORDER:
|
|
198
|
+
try:
|
|
199
|
+
cookies = _try_loader(name, host)
|
|
200
|
+
except Exception as exc:
|
|
201
|
+
errors[name] = exc
|
|
202
|
+
continue
|
|
203
|
+
if cookies and accepted(cookies):
|
|
204
|
+
picked = (name, cookies)
|
|
205
|
+
break
|
|
206
|
+
if cookies:
|
|
207
|
+
rejected.append(name)
|
|
208
|
+
if picked is None:
|
|
209
|
+
if rejected:
|
|
210
|
+
shutil.rmtree(storage_state_path.parent, ignore_errors=True)
|
|
211
|
+
raise NoCookiesFoundError(
|
|
212
|
+
f"Superset rejected cookies from: {', '.join(rejected)}. "
|
|
213
|
+
f"Sign in to {base_url} in another browser, then re-run."
|
|
214
|
+
)
|
|
215
|
+
raise NoCookiesFoundError(
|
|
216
|
+
f"No Superset session found for {host} in any supported browser. "
|
|
217
|
+
f"Sign in to {base_url} in your browser first, then re-run."
|
|
218
|
+
f"{_format_loader_errors(errors)}"
|
|
219
|
+
)
|
|
220
|
+
browser_used, cookies = picked
|
|
221
|
+
else:
|
|
222
|
+
try:
|
|
223
|
+
cookies = _try_loader(browser, host)
|
|
224
|
+
except Exception as exc:
|
|
225
|
+
raise NoCookiesFoundError(
|
|
226
|
+
f"No Superset session found for {host} in {browser}. "
|
|
227
|
+
f"Sign in to {base_url} in {browser} first, then re-run. "
|
|
228
|
+
f"Loader error — {browser}: {exc}."
|
|
229
|
+
) from exc
|
|
230
|
+
if not cookies:
|
|
231
|
+
raise NoCookiesFoundError(
|
|
232
|
+
f"No Superset session found for {host} in {browser}. "
|
|
233
|
+
f"Sign in to {base_url} in {browser} first, then re-run."
|
|
234
|
+
)
|
|
235
|
+
if not accepted(cookies):
|
|
236
|
+
shutil.rmtree(storage_state_path.parent, ignore_errors=True)
|
|
237
|
+
raise NoCookiesFoundError(
|
|
238
|
+
f"A cookie was found in {browser}, but Superset rejected it. "
|
|
239
|
+
"The imported auth state was removed."
|
|
240
|
+
)
|
|
241
|
+
browser_used = browser
|
|
242
|
+
|
|
243
|
+
return {"browser_used": browser_used, "cookie_count": len(cookies)}
|