@geoly-ai/social-hub-cli 0.3.46 → 0.3.48
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.
- package/CHANGELOG.md +25 -0
- package/dist/cmd-manifest.json +4 -2
- package/dist/register-subreddit-assets.d.ts.map +1 -1
- package/dist/register-subreddit-assets.js +43 -0
- package/dist/register-subreddit-assets.js.map +1 -1
- package/dist/register-subreddit-assets.test.js +4 -1
- package/dist/register-subreddit-assets.test.js.map +1 -1
- package/package.json +3 -3
- package/skills/integrity.json +15 -14
- package/skills/manifest.json +1 -1
- package/skills/reddit-content-rewrite/SKILL.md +1 -1
- package/skills/reddit-content-rewrite/prompts.md +2 -2
- package/skills/reddit-content-rewrite/sop-reference.md +18 -123
- package/skills/reddit-ext-post-audit/references/ext-post-audit.md +6 -7
- package/skills/reddit-matrix.lock.json +3 -3
- package/skills/reddit-voc-volume/SKILL.md +13 -9
- package/skills/reddit-voc-volume/references/arctic-shift.md +1 -1
- package/skills/reddit-voc-volume/requirements.txt +1 -4
- package/skills/reddit-voc-volume/scripts/reddit_voc_volume.py +22 -234
- package/skills/reddit-voc-volume/scripts/setup_env.sh +3 -40
- package/skills/reddit-voc-volume/services/firecrawl/examples/o1_web_crawler/o1_web_crawler.py +152 -152
- package/skills/reddit-voc-volume/tests/test_links_fallback.py +7 -7
- package/skills/reddit-voc-volume/tests/test_no_direct_reddit.py +315 -0
|
@@ -15,7 +15,7 @@ Pushshift 停摆后的接续者。**无需 auth**,免费公开服务,作者
|
|
|
15
15
|
v2.12.26 修掉正文截断后,baseline 卡的长度维度只接受未截断全文。但多数网络
|
|
16
16
|
环境下 reddit.com 的 `.json` 直采已被拦截(实测本机:`curl (56) CONNECT tunnel
|
|
17
17
|
failed 403`,去掉代理直连亦为 `403 Blocked`)。Arctic Shift 不走 reddit.com,
|
|
18
|
-
不需要 cookie
|
|
18
|
+
不需要 cookie 或指纹伪装,且**正文不截断**。
|
|
19
19
|
|
|
20
20
|
## 覆盖范围(实测,非文档转述)
|
|
21
21
|
|
|
@@ -154,7 +154,19 @@ DEFAULT_UA = (
|
|
|
154
154
|
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 reddit-voc-volume/1.0"
|
|
155
155
|
)
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
|
|
158
|
+
class _DeprecatedViaCurl(argparse.Action):
|
|
159
|
+
"""`--via-curl` 自 v2.12.28 起无效果;保留一个发布周期,仅向 stderr 提示,下一版移除。"""
|
|
160
|
+
|
|
161
|
+
def __init__(self, option_strings: Any, dest: str, **kwargs: Any) -> None:
|
|
162
|
+
kwargs["nargs"] = 0
|
|
163
|
+
kwargs.setdefault("default", False)
|
|
164
|
+
kwargs.setdefault("help", "DEPRECATED:已无效果(v2.12.28 起不直连 reddit),下一版移除")
|
|
165
|
+
super().__init__(option_strings, dest, **kwargs)
|
|
166
|
+
|
|
167
|
+
def __call__(self, parser: Any, namespace: Any, values: Any, option_string: Any = None) -> None:
|
|
168
|
+
print("DEPRECATED: --via-curl 已无效果(v2.12.28 起不直连 reddit)", file=sys.stderr)
|
|
169
|
+
setattr(namespace, self.dest, True)
|
|
158
170
|
|
|
159
171
|
|
|
160
172
|
def utc_now_iso() -> str:
|
|
@@ -177,22 +189,6 @@ def sleep_with_jitter(base_sec: float) -> float:
|
|
|
177
189
|
return jitter
|
|
178
190
|
|
|
179
191
|
|
|
180
|
-
def _looks_like_html(text: str) -> bool:
|
|
181
|
-
s = (text or "").lstrip()
|
|
182
|
-
return s.startswith("<") or "<html" in s[:200].lower() or "theme-beta" in s[:500]
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _loads_json_or_raise(text: str, *, url: str) -> Any:
|
|
186
|
-
if _looks_like_html(text):
|
|
187
|
-
raise ValueError(
|
|
188
|
-
"reddit_returned_html_not_json (likely 403/block; try proxy or browser fallback)"
|
|
189
|
-
)
|
|
190
|
-
try:
|
|
191
|
-
return json.loads(text)
|
|
192
|
-
except json.JSONDecodeError as e:
|
|
193
|
-
raise ValueError(f"invalid_json: {e}") from e
|
|
194
|
-
|
|
195
|
-
|
|
196
192
|
def _loads_json_response(text: str) -> Any:
|
|
197
193
|
try:
|
|
198
194
|
return json.loads(text)
|
|
@@ -243,114 +239,11 @@ def resolve_proxy_url() -> str:
|
|
|
243
239
|
return _macos_system_proxy_url()
|
|
244
240
|
|
|
245
241
|
|
|
246
|
-
def _proxies_dict() -> dict[str, str]:
|
|
247
|
-
p = resolve_proxy_url()
|
|
248
|
-
return {"http": p, "https": p} if p else {}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def _curl_proxy_args() -> list[str]:
|
|
252
|
-
proxy = resolve_proxy_url()
|
|
253
|
-
return ["--proxy", proxy] if proxy else []
|
|
254
|
-
|
|
255
|
-
|
|
256
242
|
def _env_truthy(name: str) -> bool:
|
|
257
243
|
raw = (os.environ.get(name) or "").strip().lower()
|
|
258
244
|
return raw in ("1", "true", "yes", "on")
|
|
259
245
|
|
|
260
246
|
|
|
261
|
-
def _use_browser_cookies() -> bool:
|
|
262
|
-
raw = (os.environ.get("REDDIT_USE_BROWSER_COOKIES") or "1").strip().lower()
|
|
263
|
-
return raw not in ("0", "false", "no", "off")
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
class CookieStore:
|
|
267
|
-
"""Load reddit.com cookies once per process and reuse for all requests."""
|
|
268
|
-
|
|
269
|
-
_instance: CookieStore | None = None
|
|
270
|
-
|
|
271
|
-
def __init__(self) -> None:
|
|
272
|
-
self._cookies: dict[str, str] = {}
|
|
273
|
-
self.source: str | None = None
|
|
274
|
-
self.loaded = False
|
|
275
|
-
|
|
276
|
-
@classmethod
|
|
277
|
-
def get(cls) -> CookieStore:
|
|
278
|
-
if cls._instance is None:
|
|
279
|
-
cls._instance = cls()
|
|
280
|
-
cls._instance.load_once()
|
|
281
|
-
return cls._instance
|
|
282
|
-
|
|
283
|
-
def load_once(self) -> dict[str, str]:
|
|
284
|
-
if self.loaded:
|
|
285
|
-
return self._cookies
|
|
286
|
-
self.loaded = True
|
|
287
|
-
raw = (os.environ.get("REDDIT_COOKIES_JSON") or "").strip()
|
|
288
|
-
if raw:
|
|
289
|
-
try:
|
|
290
|
-
obj = json.loads(raw)
|
|
291
|
-
if isinstance(obj, dict):
|
|
292
|
-
self._cookies = {str(k): str(v) for k, v in obj.items() if k and v is not None}
|
|
293
|
-
self.source = "env_json"
|
|
294
|
-
return self._cookies
|
|
295
|
-
except json.JSONDecodeError:
|
|
296
|
-
pass
|
|
297
|
-
if not _use_browser_cookies():
|
|
298
|
-
self.source = None
|
|
299
|
-
return self._cookies
|
|
300
|
-
try:
|
|
301
|
-
import browser_cookie3 # type: ignore
|
|
302
|
-
except ImportError:
|
|
303
|
-
self.source = None
|
|
304
|
-
return self._cookies
|
|
305
|
-
for name in ("chrome", "safari", "firefox"):
|
|
306
|
-
fn = getattr(browser_cookie3, name, None)
|
|
307
|
-
if not callable(fn):
|
|
308
|
-
continue
|
|
309
|
-
try:
|
|
310
|
-
jar = fn(domain_name="reddit.com")
|
|
311
|
-
cookies = {c.name: c.value for c in jar if c.name and c.value}
|
|
312
|
-
if cookies:
|
|
313
|
-
self._cookies = cookies
|
|
314
|
-
self.source = "browser"
|
|
315
|
-
# Share with child processes in same sync job (e.g. feishu sync)
|
|
316
|
-
os.environ.setdefault("REDDIT_COOKIES_JSON", json.dumps(cookies))
|
|
317
|
-
return self._cookies
|
|
318
|
-
except Exception:
|
|
319
|
-
continue
|
|
320
|
-
self.source = None
|
|
321
|
-
return self._cookies
|
|
322
|
-
|
|
323
|
-
@property
|
|
324
|
-
def cookies(self) -> dict[str, str]:
|
|
325
|
-
return self._cookies
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
def _load_browser_cookies() -> dict[str, str]:
|
|
329
|
-
return CookieStore.get().cookies
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
def _browser_headers(user_agent: str) -> dict[str, str]:
|
|
333
|
-
return {
|
|
334
|
-
"User-Agent": user_agent,
|
|
335
|
-
"Accept": "application/json, text/plain, */*",
|
|
336
|
-
"Accept-Language": "en-US,en;q=0.9",
|
|
337
|
-
"Referer": "https://www.reddit.com/",
|
|
338
|
-
"Origin": "https://www.reddit.com",
|
|
339
|
-
"Sec-Fetch-Dest": "empty",
|
|
340
|
-
"Sec-Fetch-Mode": "cors",
|
|
341
|
-
"Sec-Fetch-Site": "same-origin",
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
def _curl_cffi_available() -> bool:
|
|
346
|
-
try:
|
|
347
|
-
import curl_cffi # noqa: F401
|
|
348
|
-
|
|
349
|
-
return True
|
|
350
|
-
except ImportError:
|
|
351
|
-
return False
|
|
352
|
-
|
|
353
|
-
|
|
354
247
|
class RequestTracker:
|
|
355
248
|
def __init__(self) -> None:
|
|
356
249
|
self.count = 0
|
|
@@ -358,9 +251,6 @@ class RequestTracker:
|
|
|
358
251
|
self.rate_limited = False
|
|
359
252
|
self.truncated = False
|
|
360
253
|
self.last_fetch_mode = "none"
|
|
361
|
-
self.used_cookies = False
|
|
362
|
-
self.cookie_source: str | None = None
|
|
363
|
-
self.cookie_cache_reused = False
|
|
364
254
|
|
|
365
255
|
def bump(self) -> None:
|
|
366
256
|
self.count += 1
|
|
@@ -368,108 +258,6 @@ class RequestTracker:
|
|
|
368
258
|
self.truncated = True
|
|
369
259
|
raise RuntimeError("max_http_requests_exceeded")
|
|
370
260
|
|
|
371
|
-
def record_identity(self, mode: str, store: CookieStore) -> None:
|
|
372
|
-
self.last_fetch_mode = mode
|
|
373
|
-
if store.cookies:
|
|
374
|
-
self.used_cookies = True
|
|
375
|
-
self.cookie_source = store.source
|
|
376
|
-
if store.loaded and self.count > 1:
|
|
377
|
-
self.cookie_cache_reused = True
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
class RedditHttpClient:
|
|
381
|
-
"""Reuse cookies + curl_cffi session within a single CLI run."""
|
|
382
|
-
|
|
383
|
-
def __init__(self, user_agent: str, *, force_curl: bool = False) -> None:
|
|
384
|
-
self.user_agent = user_agent
|
|
385
|
-
self.force_curl = force_curl
|
|
386
|
-
self.cookie_store = CookieStore.get()
|
|
387
|
-
self._cffi_session: Any = None
|
|
388
|
-
self._warmed = False
|
|
389
|
-
|
|
390
|
-
def _ensure_cffi_session(self) -> Any:
|
|
391
|
-
if self._cffi_session is None:
|
|
392
|
-
from curl_cffi import requests as cffi_requests
|
|
393
|
-
|
|
394
|
-
self._cffi_session = cffi_requests.Session(impersonate=IMPERSONATE)
|
|
395
|
-
if not self._warmed:
|
|
396
|
-
headers = _browser_headers(self.user_agent)
|
|
397
|
-
proxies = _proxies_dict()
|
|
398
|
-
cookies = self.cookie_store.cookies
|
|
399
|
-
try:
|
|
400
|
-
self._cffi_session.get(
|
|
401
|
-
"https://www.reddit.com/",
|
|
402
|
-
headers=headers,
|
|
403
|
-
proxies=proxies or None,
|
|
404
|
-
cookies=cookies or None,
|
|
405
|
-
timeout=30.0,
|
|
406
|
-
)
|
|
407
|
-
except Exception:
|
|
408
|
-
pass
|
|
409
|
-
self._warmed = True
|
|
410
|
-
return self._cffi_session
|
|
411
|
-
|
|
412
|
-
def fetch_via_curl_cffi(self, url: str, timeout: float = 30.0) -> tuple[Any, str]:
|
|
413
|
-
session = self._ensure_cffi_session()
|
|
414
|
-
headers = _browser_headers(self.user_agent)
|
|
415
|
-
proxies = _proxies_dict()
|
|
416
|
-
cookies = self.cookie_store.cookies
|
|
417
|
-
resp = session.get(
|
|
418
|
-
url,
|
|
419
|
-
headers=headers,
|
|
420
|
-
proxies=proxies or None,
|
|
421
|
-
cookies=cookies or None,
|
|
422
|
-
timeout=timeout,
|
|
423
|
-
)
|
|
424
|
-
if resp.status_code == 429:
|
|
425
|
-
raise RuntimeError("http_status:429")
|
|
426
|
-
if resp.status_code == 403:
|
|
427
|
-
raise RuntimeError("http_status:403")
|
|
428
|
-
if resp.status_code >= 400:
|
|
429
|
-
raise RuntimeError(f"http_status:{resp.status_code}")
|
|
430
|
-
return _loads_json_or_raise(resp.text, url=url), "curl_cffi"
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
def http_get_json_via_curl(url: str, user_agent: str, timeout: int = 30) -> tuple[Any, str]:
|
|
434
|
-
curl = shutil.which("curl")
|
|
435
|
-
if not curl:
|
|
436
|
-
raise RuntimeError("curl not found in PATH")
|
|
437
|
-
cmd = [
|
|
438
|
-
curl,
|
|
439
|
-
"-sS",
|
|
440
|
-
"-L",
|
|
441
|
-
"--max-time",
|
|
442
|
-
str(int(timeout)),
|
|
443
|
-
"-A",
|
|
444
|
-
user_agent,
|
|
445
|
-
"-H",
|
|
446
|
-
"Accept: application/json, text/plain, */*",
|
|
447
|
-
"-H",
|
|
448
|
-
"Accept-Language: en-US,en;q=0.9",
|
|
449
|
-
"-H",
|
|
450
|
-
"Referer: https://www.reddit.com/",
|
|
451
|
-
*_curl_proxy_args(),
|
|
452
|
-
url,
|
|
453
|
-
]
|
|
454
|
-
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 5)
|
|
455
|
-
if proc.returncode != 0:
|
|
456
|
-
err = (proc.stderr or proc.stdout or "curl failed").strip()
|
|
457
|
-
if "429" in err:
|
|
458
|
-
raise RuntimeError("http_status:429")
|
|
459
|
-
raise RuntimeError(err)
|
|
460
|
-
return _loads_json_or_raise(proc.stdout, url=url), "curl"
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
def http_get_json_urllib(url: str, user_agent: str, timeout: float = 25.0) -> tuple[Any, str]:
|
|
464
|
-
req = urllib.request.Request(url, headers=_browser_headers(user_agent), method="GET")
|
|
465
|
-
try:
|
|
466
|
-
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
467
|
-
charset = resp.headers.get_content_charset() or "utf-8"
|
|
468
|
-
raw = resp.read().decode(charset)
|
|
469
|
-
except urllib.error.HTTPError as e:
|
|
470
|
-
raise RuntimeError(f"http_status:{e.code}") from e
|
|
471
|
-
return _loads_json_or_raise(raw, url=url), "urllib"
|
|
472
|
-
|
|
473
261
|
|
|
474
262
|
def normalize_firecrawl_base_url(base_url: str | None = None) -> str:
|
|
475
263
|
raw = (base_url or os.environ.get("FIRECRAWL_BASE_URL") or DEFAULT_FIRECRAWL_BASE_URL).strip()
|
|
@@ -4693,10 +4481,10 @@ def build_output(
|
|
|
4693
4481
|
"request_identity": {
|
|
4694
4482
|
"user_agent_kind": "chrome",
|
|
4695
4483
|
"fetch_mode": tracker.last_fetch_mode,
|
|
4696
|
-
"used_cookies":
|
|
4697
|
-
"cookie_source":
|
|
4698
|
-
"cookie_cache_reused":
|
|
4699
|
-
"impersonation":
|
|
4484
|
+
"used_cookies": False,
|
|
4485
|
+
"cookie_source": None,
|
|
4486
|
+
"cookie_cache_reused": False,
|
|
4487
|
+
"impersonation": None,
|
|
4700
4488
|
"proxy_used": bool(proxy),
|
|
4701
4489
|
},
|
|
4702
4490
|
"notes": notes,
|
|
@@ -4833,7 +4621,7 @@ def main() -> int:
|
|
|
4833
4621
|
pk.add_argument("--max-pages", type=int, default=DEFAULT_MAX_PAGES)
|
|
4834
4622
|
pk.add_argument("--sleep-sec", type=float, default=DEFAULT_SLEEP_SEC)
|
|
4835
4623
|
pk.add_argument("--user-agent", default=DEFAULT_UA)
|
|
4836
|
-
pk.add_argument("--via-curl", action=
|
|
4624
|
+
pk.add_argument("--via-curl", action=_DeprecatedViaCurl)
|
|
4837
4625
|
pk.add_argument("--pretty", action="store_true")
|
|
4838
4626
|
add_hub_voc_options(pk)
|
|
4839
4627
|
|
|
@@ -4846,7 +4634,7 @@ def main() -> int:
|
|
|
4846
4634
|
ps.add_argument("--max-pages", type=int, default=DEFAULT_MAX_PAGES)
|
|
4847
4635
|
ps.add_argument("--sleep-sec", type=float, default=DEFAULT_SLEEP_SEC)
|
|
4848
4636
|
ps.add_argument("--user-agent", default=DEFAULT_UA)
|
|
4849
|
-
ps.add_argument("--via-curl", action=
|
|
4637
|
+
ps.add_argument("--via-curl", action=_DeprecatedViaCurl)
|
|
4850
4638
|
ps.add_argument("--pretty", action="store_true")
|
|
4851
4639
|
add_hub_voc_options(ps)
|
|
4852
4640
|
|
|
@@ -4855,7 +4643,7 @@ def main() -> int:
|
|
|
4855
4643
|
pst.add_argument("--limit", type=int, default=10, help="Hot listing size per subreddit before filtering stickied posts")
|
|
4856
4644
|
pst.add_argument("--sleep-sec", type=float, default=DEFAULT_SLEEP_SEC)
|
|
4857
4645
|
pst.add_argument("--user-agent", default=DEFAULT_UA)
|
|
4858
|
-
pst.add_argument("--via-curl", action=
|
|
4646
|
+
pst.add_argument("--via-curl", action=_DeprecatedViaCurl)
|
|
4859
4647
|
pst.add_argument("--pretty", action="store_true")
|
|
4860
4648
|
add_hub_voc_options(pst)
|
|
4861
4649
|
|
|
@@ -4922,7 +4710,7 @@ def main() -> int:
|
|
|
4922
4710
|
pl.add_argument("--file", "-f", help="File with one URL per line")
|
|
4923
4711
|
pl.add_argument("--sleep-sec", type=float, default=DEFAULT_SLEEP_SEC)
|
|
4924
4712
|
pl.add_argument("--user-agent", default=DEFAULT_UA)
|
|
4925
|
-
pl.add_argument("--via-curl", action=
|
|
4713
|
+
pl.add_argument("--via-curl", action=_DeprecatedViaCurl)
|
|
4926
4714
|
pl.add_argument(
|
|
4927
4715
|
"--fallback",
|
|
4928
4716
|
choices=["none", "firecrawl", "social-hub", "auto"],
|
|
@@ -5363,7 +5151,7 @@ def main() -> int:
|
|
|
5363
5151
|
errors = list(acquisition.get("errors") or [])
|
|
5364
5152
|
if acquisition.get("should_local"):
|
|
5365
5153
|
# Emergency Acquisition 缺省走 Arctic Shift 归档 API。
|
|
5366
|
-
# 相对 .json
|
|
5154
|
+
# 相对 .json 直采:正文不截断、不受 Reddit 反爬拦截;
|
|
5367
5155
|
# 代价是 score 有约 1/3 为陈旧值(已逐条标 score_status),且无 live 语义。
|
|
5368
5156
|
items = []
|
|
5369
5157
|
for idx, sub in enumerate(subreddits or [None]):
|
|
@@ -36,44 +36,8 @@ find_python() {
|
|
|
36
36
|
return 1
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
echo " curl: $(command -v curl)"
|
|
42
|
-
return 0
|
|
43
|
-
fi
|
|
44
|
-
echo " curl: not found — attempting install..."
|
|
45
|
-
if [[ "$SKIP_BREW" != 1 ]] && command -v brew >/dev/null 2>&1; then
|
|
46
|
-
brew install curl || true
|
|
47
|
-
fi
|
|
48
|
-
if [[ "$SKIP_APT" != 1 ]] && command -v apt-get >/dev/null 2>&1; then
|
|
49
|
-
if [[ "$ASSUME_YES" == 1 ]]; then
|
|
50
|
-
sudo apt-get update -qq && sudo apt-get install -y curl || true
|
|
51
|
-
else
|
|
52
|
-
echo " Run: sudo apt-get install -y curl (or re-run with --yes if sudo is OK)"
|
|
53
|
-
fi
|
|
54
|
-
fi
|
|
55
|
-
if command -v curl >/dev/null 2>&1; then
|
|
56
|
-
echo " curl: installed at $(command -v curl)"
|
|
57
|
-
return 0
|
|
58
|
-
fi
|
|
59
|
-
echo " curl: still missing (urllib/curl_cffi may still work; install curl manually for fallback)"
|
|
60
|
-
return 0
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
ensure_pip_packages() {
|
|
64
|
-
local py="$1"
|
|
65
|
-
echo " pip packages: checking curl_cffi, browser_cookie3..."
|
|
66
|
-
if "$py" -c "import curl_cffi, browser_cookie3" 2>/dev/null; then
|
|
67
|
-
echo " pip packages: OK"
|
|
68
|
-
return 0
|
|
69
|
-
fi
|
|
70
|
-
echo " pip packages: installing from ${REQ}..."
|
|
71
|
-
"$py" -m pip install --upgrade pip >/dev/null 2>&1 || true
|
|
72
|
-
if ! "$py" -m pip install -r "$REQ"; then
|
|
73
|
-
echo " pip install failed — try: $py -m pip install -r $REQ" >&2
|
|
74
|
-
return 1
|
|
75
|
-
fi
|
|
76
|
-
echo " pip packages: installed"
|
|
39
|
+
report_pip_packages() {
|
|
40
|
+
echo " pip packages: none required (stdlib only; see ${REQ})"
|
|
77
41
|
}
|
|
78
42
|
|
|
79
43
|
main() {
|
|
@@ -84,8 +48,7 @@ main() {
|
|
|
84
48
|
exit 1
|
|
85
49
|
fi
|
|
86
50
|
echo " python: $PYTHON ($("$PYTHON" --version 2>&1))"
|
|
87
|
-
|
|
88
|
-
ensure_pip_packages "$PYTHON"
|
|
51
|
+
report_pip_packages
|
|
89
52
|
echo "=== setup complete ==="
|
|
90
53
|
}
|
|
91
54
|
|