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.
@@ -0,0 +1,3 @@
1
+ __all__ = ["__version__"]
2
+
3
+ __version__ = "0.1.0"
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)}