@champpaba/gslide 0.1.2 → 0.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@champpaba/gslide",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "CLI tool for automating Google Slides 'Help me visualize' feature via browser automation",
5
5
  "bin": {
6
6
  "gslide": "./bin/gslide.mjs"
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gslide"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  description = "CLI tool for automating Google Slides 'Help me visualize' feature via browser automation"
5
5
  requires-python = ">=3.10"
6
6
  dependencies = [
@@ -1,3 +1,4 @@
1
- """gslide — CLI for automating Google Slides 'Help me visualize' feature."""
1
+ """gslide — CLI for automating Google Slides 'Help me visualise' feature."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.4"
4
+ __all__ = ["__version__"]
@@ -7,33 +7,25 @@ from typing import Any
7
7
 
8
8
  import click
9
9
 
10
-
11
10
  SLIDES_URL = "https://docs.google.com/presentation/"
12
11
 
13
12
 
14
13
  def get_storage_path() -> Path:
15
- """Return the path to the storage state file."""
16
14
  return Path.home() / ".gslide" / "storage_state.json"
17
15
 
18
16
 
19
17
  def is_logged_in() -> bool:
20
- """Check if a storage state file exists."""
21
18
  return get_storage_path().exists()
22
19
 
23
20
 
24
21
  def save_storage_state(data: dict[str, Any]) -> None:
25
- """Save storage state data to disk, creating directories as needed."""
26
22
  path = get_storage_path()
27
23
  path.parent.mkdir(parents=True, exist_ok=True)
28
24
  path.write_text(json.dumps(data, indent=2))
29
25
 
30
26
 
31
27
  def delete_storage_state() -> None:
32
- """Delete the storage state file. No error if missing."""
33
- try:
34
- get_storage_path().unlink()
35
- except FileNotFoundError:
36
- pass
28
+ get_storage_path().unlink(missing_ok=True)
37
29
 
38
30
 
39
31
  def login() -> None:
@@ -3,7 +3,7 @@
3
3
  from pathlib import Path
4
4
  from typing import Any
5
5
 
6
- from playwright.sync_api import sync_playwright, BrowserContext, Playwright
6
+ from playwright.sync_api import BrowserContext, Playwright, sync_playwright
7
7
 
8
8
 
9
9
  class BrowserSession:
@@ -32,7 +32,6 @@ class BrowserSession:
32
32
  self._context.browser.close()
33
33
  if self._pw:
34
34
  self._pw.stop()
35
- return None
36
35
 
37
36
 
38
37
  def save_session(context: BrowserContext, path: Path) -> None:
package/src/gslide/cli.py CHANGED
@@ -13,47 +13,35 @@ def cli() -> None:
13
13
 
14
14
  @cli.command()
15
15
  def update() -> None:
16
- """Check for updates and self-update via npm."""
16
+ """Update gslide to the latest version."""
17
17
  import subprocess
18
18
 
19
19
  from gslide import __version__
20
20
 
21
- click.echo(f"Current version: v{__version__}")
22
- click.echo("Checking for updates...")
23
-
24
- try:
25
- result = subprocess.run(
26
- ["npm", "view", "@champpaba/gslide", "version", "--registry", "https://registry.npmjs.org"],
27
- capture_output=True,
28
- text=True,
29
- timeout=15,
30
- )
31
- latest = result.stdout.strip()
32
- except (subprocess.TimeoutExpired, FileNotFoundError):
33
- click.echo("Could not check npm registry.", err=True)
34
- sys.exit(1)
35
-
36
- if not latest:
37
- click.echo("Could not determine latest version.", err=True)
38
- sys.exit(1)
39
-
40
- if latest == __version__:
41
- click.echo(f"Already up to date (v{__version__}).")
42
- return
43
-
44
- click.echo(f"Latest version: v{latest}")
21
+ click.echo(f"Current: v{__version__}")
45
22
  click.echo("Updating...")
46
23
 
47
24
  proc = subprocess.run(
48
- ["npm", "update", "-g", "@champpaba/gslide"],
25
+ ["npm", "install", "-g", "@champpaba/gslide@latest"],
49
26
  timeout=120,
50
27
  )
51
- if proc.returncode == 0:
52
- click.echo(f"Updated to v{latest}.")
53
- else:
28
+ if proc.returncode != 0:
54
29
  click.echo("Update failed.", err=True)
55
30
  sys.exit(1)
56
31
 
32
+ # Check new version
33
+ result = subprocess.run(
34
+ ["node", "-e", "console.log(require(require('path').join(require('child_process').execSync('npm prefix -g').toString().trim(),'lib/node_modules/@champpaba/gslide/package.json')).version)"],
35
+ capture_output=True,
36
+ text=True,
37
+ )
38
+ new_ver = result.stdout.strip() if result.returncode == 0 else "unknown"
39
+
40
+ if new_ver == __version__:
41
+ click.echo(f"Already up to date (v{__version__}).")
42
+ else:
43
+ click.echo(f"Updated: v{__version__} -> v{new_ver}")
44
+
57
45
 
58
46
  # --- Auth commands ---
59
47
 
package/src/gslide/gen.py CHANGED
@@ -20,7 +20,6 @@ from gslide.auth import get_storage_path
20
20
  from gslide.browser import BrowserSession
21
21
  from gslide.prompts import PromptsData
22
22
 
23
-
24
23
  PRESENTATION_URL = "https://docs.google.com/presentation/d/{id}/edit"
25
24
  DEFAULT_TIMEOUT_MS = 120_000 # 120s — generation can take 30-60s
26
25
  PANEL_LOAD_TIMEOUT_MS = 10_000
@@ -53,8 +52,8 @@ def navigate_to_presentation(page: Page, presentation_id: str) -> None:
53
52
  page.wait_for_selector(
54
53
  '[aria-label="filmstrip"]', timeout=SLIDES_LOAD_TIMEOUT_MS
55
54
  )
56
- except PwTimeout:
57
- raise GenerationError("Google Slides UI did not load in time")
55
+ except PwTimeout as e:
56
+ raise GenerationError("Google Slides UI did not load in time") from e
58
57
 
59
58
 
60
59
  def open_panel(page: Page) -> None:
@@ -71,11 +70,7 @@ def open_panel(page: Page) -> None:
71
70
 
72
71
 
73
72
  def _reopen_panel(page: Page, retries: int = 2) -> None:
74
- """Reopen the HMV panel after insert overlay is dismissed.
75
-
76
- After inserting a slide, the panel may close. This clicks the HMV icon
77
- and waits for tabs to reappear, with retry logic.
78
- """
73
+ """Reopen the HMV panel after insert overlay is dismissed."""
79
74
  for attempt in range(retries + 1):
80
75
  try:
81
76
  hmv = page.locator('div[aria-label="Help me visualize"]')
@@ -97,7 +92,6 @@ def select_tab(page: Page, tab_name: str) -> None:
97
92
 
98
93
 
99
94
  def _find_visible_textarea(page: Page) -> object:
100
- """Find the first visible textarea in the panel."""
101
95
  textareas = page.locator("textarea")
102
96
  for i in range(textareas.count()):
103
97
  ta = textareas.nth(i)
@@ -179,12 +173,7 @@ def fill_and_create(
179
173
 
180
174
 
181
175
  def _click_preview_image(page: Page) -> None:
182
- """Click the generated preview image to open the insert overlay.
183
-
184
- Validated flow: after generation, the preview appears as an img hosted on
185
- googleusercontent.com. Clicking it opens a fullscreen preview with
186
- "Insert on new slide" button.
187
- """
176
+ """Click the generated preview image to open the insert overlay."""
188
177
  preview = page.locator('img[src*="googleusercontent.com"]')
189
178
  for i in range(preview.count()):
190
179
  try:
@@ -203,46 +192,40 @@ def _click_preview_image(page: Page) -> None:
203
192
  raise GenerationError("Generated preview image not found")
204
193
 
205
194
 
206
- def insert_infographic(page: Page) -> None:
207
- """Click preview 'Insert on new slide' for infographic tab."""
208
- _click_preview_image(page)
209
-
210
- btn = page.get_by_text("Insert on new slide")
195
+ def _click_insert_button(page: Page, text: str) -> None:
196
+ """Click an insert button by text, raising GenerationError if not found."""
197
+ btn = page.get_by_text(text)
211
198
  if btn.count() > 0 and btn.first.is_visible():
212
199
  btn.first.click()
213
200
  else:
214
- raise GenerationError("'Insert on new slide' button not found after clicking preview")
201
+ raise GenerationError(f"'{text}' button not found after clicking preview")
202
+
203
+
204
+ def insert_infographic(page: Page) -> None:
205
+ """Click preview then 'Insert on new slide' for infographic tab."""
206
+ _click_preview_image(page)
207
+ _click_insert_button(page, "Insert on new slide")
215
208
  _wait_for_new_slide(page)
216
209
 
217
210
 
218
211
  def insert_slide(page: Page) -> None:
219
- """Click preview 'Insert on new slide' for slide tab."""
212
+ """Click preview then 'Insert on new slide' for slide tab."""
220
213
  _click_preview_image(page)
221
-
222
- # Slide tab uses same "Insert on new slide" button after clicking preview
223
- btn = page.get_by_text("Insert on new slide")
224
- if btn.count() > 0 and btn.first.is_visible():
225
- btn.first.click()
226
- else:
227
- raise GenerationError("'Insert on new slide' button not found for slide tab")
214
+ _click_insert_button(page, "Insert on new slide")
228
215
  _wait_for_new_slide(page)
229
216
 
230
217
 
231
218
  def insert_image(page: Page, insert_as: str = "image") -> None:
232
- """Click preview insert as image or background."""
219
+ """Click preview then insert as image or background."""
233
220
  _click_preview_image(page)
234
221
 
235
- # Image tab may show different insert options via dropdown
236
- if insert_as == "background":
237
- option_text = "Insert as background"
238
- else:
239
- option_text = "Insert as image"
222
+ option_text = "Insert as background" if insert_as == "background" else "Insert as image"
240
223
 
241
224
  btn = page.get_by_text(option_text)
242
225
  if btn.count() > 0 and btn.first.is_visible():
243
226
  btn.first.click()
244
227
  else:
245
- # Fallback: try "Insert on new slide" or dropdown
228
+ # Fallback: try "Insert on new slide"
246
229
  fallback = page.get_by_text("Insert on new slide")
247
230
  if fallback.count() > 0 and fallback.first.is_visible():
248
231
  fallback.first.click()
@@ -252,15 +235,12 @@ def insert_image(page: Page, insert_as: str = "image") -> None:
252
235
  page.wait_for_timeout(2000)
253
236
 
254
237
 
255
-
256
238
  def check_url(page: Page, presentation_id: str) -> None:
257
- """Verify browser URL still contains the target presentation ID."""
258
239
  if presentation_id not in page.url:
259
240
  raise GenerationError("Browser navigated away from target presentation")
260
241
 
261
242
 
262
243
  def _wait_for_new_slide(page: Page) -> None:
263
- """Wait for new slide to appear in filmstrip."""
264
244
  page.wait_for_timeout(2000)
265
245
 
266
246
 
@@ -274,6 +254,15 @@ _INSERT_FN = {
274
254
  }
275
255
 
276
256
 
257
+ def _require_login() -> "Path":
258
+ """Return storage path or exit if not logged in."""
259
+ storage_path = get_storage_path()
260
+ if not storage_path.exists():
261
+ click.echo("Not logged in. Run: gslide auth login", err=True)
262
+ sys.exit(1)
263
+ return storage_path
264
+
265
+
277
266
  def gen_single(
278
267
  presentation_id: str,
279
268
  tab: str,
@@ -284,11 +273,7 @@ def gen_single(
284
273
  insert_as: str = "image",
285
274
  ) -> None:
286
275
  """Generate a single slide/infographic/image via browser automation."""
287
- storage_path = get_storage_path()
288
- if not storage_path.exists():
289
- click.echo("Not logged in. Run: gslide auth login", err=True)
290
- sys.exit(1)
291
-
276
+ storage_path = _require_login()
292
277
  timeout_ms = timeout * 1000
293
278
 
294
279
  with BrowserSession(storage_state=storage_path) as context:
@@ -315,7 +300,6 @@ def gen_single(
315
300
 
316
301
  except GenerationError as e:
317
302
  click.echo(f"Error: {e}", err=True)
318
- # Take screenshot for debugging
319
303
  try:
320
304
  page.screenshot(path="/tmp/gslide_error.png")
321
305
  click.echo("Debug screenshot saved to /tmp/gslide_error.png")
@@ -344,11 +328,7 @@ def gen_batch(
344
328
  timeout: int = 120,
345
329
  ) -> None:
346
330
  """Generate all slides from prompts data in a single browser session."""
347
- storage_path = get_storage_path()
348
- if not storage_path.exists():
349
- click.echo("Not logged in. Run: gslide auth login", err=True)
350
- sys.exit(1)
351
-
331
+ storage_path = _require_login()
352
332
  timeout_ms = timeout * 1000
353
333
  total_slides = len(prompts_data.slides)
354
334
  total_images = len(prompts_data.images)
@@ -397,7 +377,7 @@ def gen_batch(
397
377
  page.wait_for_timeout(500)
398
378
  _reopen_panel(page)
399
379
 
400
- except (GenerationError, PwTimeout, Exception) as e:
380
+ except Exception as e:
401
381
  errors.append((i, slide.tab, str(e)))
402
382
  click.echo(f" {label} FAILED: {e}")
403
383
  if not continue_on_error:
@@ -412,7 +392,7 @@ def gen_batch(
412
392
  check_url(page, prompts_data.presentation_id)
413
393
  _navigate_to_slide(page, img.target_slide)
414
394
 
415
- if "image" != current_tab:
395
+ if current_tab != "image":
416
396
  select_tab(page, "Image")
417
397
  current_tab = "image"
418
398
 
@@ -423,13 +403,12 @@ def gen_batch(
423
403
  elapsed = time.monotonic() - t0
424
404
  click.echo(f" {label} done ({elapsed:.1f}s)")
425
405
 
426
- except (GenerationError, PwTimeout, Exception) as e:
406
+ except Exception as e:
427
407
  errors.append((total_slides + i, "image", str(e)))
428
408
  click.echo(f" {label} FAILED: {e}")
429
409
  if not continue_on_error:
430
410
  break
431
411
 
432
-
433
412
  # Summary
434
413
  total_elapsed = time.monotonic() - start_time
435
414
  total_ops = total_slides + total_images
@@ -33,9 +33,7 @@ class PromptsData:
33
33
 
34
34
 
35
35
  def load_prompts(path: Path) -> PromptsData:
36
- """Load and validate a prompts.json file."""
37
- with open(path) as f:
38
- raw = json.load(f)
36
+ raw = json.loads(path.read_text())
39
37
 
40
38
  if not isinstance(raw, dict):
41
39
  raise ValidationError("Prompts file must be a JSON object")