scrapebadger-cli 0.15.0__tar.gz → 0.16.0__tar.gz

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.
Files changed (49) hide show
  1. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/PKG-INFO +1 -1
  2. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/pyproject.toml +1 -1
  3. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/depop.py +1 -1
  4. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/google.py +1 -1
  5. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/tiktok.py +264 -13
  6. scrapebadger_cli-0.16.0/tests/test_tiktok_pagination.py +36 -0
  7. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/.gitignore +0 -0
  8. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/README.md +0 -0
  9. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/__init__.py +0 -0
  10. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/client.py +0 -0
  11. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/__init__.py +0 -0
  12. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/airbnb.py +0 -0
  13. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/amazon.py +0 -0
  14. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/apartments.py +0 -0
  15. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/app_store.py +0 -0
  16. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/auth.py +0 -0
  17. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/baidu.py +0 -0
  18. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/bing.py +0 -0
  19. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/booking.py +0 -0
  20. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/chatgpt.py +0 -0
  21. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/duckduckgo.py +0 -0
  22. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/ebay.py +0 -0
  23. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/facebook.py +0 -0
  24. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/gemini.py +0 -0
  25. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/google_play.py +0 -0
  26. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/idealista.py +0 -0
  27. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/immobiliare.py +0 -0
  28. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/instagram.py +0 -0
  29. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/leboncoin.py +0 -0
  30. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/linkedin.py +0 -0
  31. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/loopnet.py +0 -0
  32. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/perplexity.py +0 -0
  33. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/realtor.py +0 -0
  34. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/reddit.py +0 -0
  35. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/redfin.py +0 -0
  36. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/stream.py +0 -0
  37. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/twitter.py +0 -0
  38. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/vinted.py +0 -0
  39. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/walmart.py +0 -0
  40. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/web.py +0 -0
  41. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/yahoo.py +0 -0
  42. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/yandex.py +0 -0
  43. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/youtube.py +0 -0
  44. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/commands/zillow.py +0 -0
  45. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/config.py +0 -0
  46. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/main.py +0 -0
  47. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/src/scrapebadger_cli/output.py +0 -0
  48. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/tests/test_vinted_image_search.py +0 -0
  49. {scrapebadger_cli-0.15.0 → scrapebadger_cli-0.16.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: scrapebadger-cli
3
- Version: 0.15.0
3
+ Version: 0.16.0
4
4
  Summary: ScrapeBadger CLI — Twitter, Vinted, Google, and Web Scraping from the command line
5
5
  Author: ScrapeBadger Team
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scrapebadger-cli"
3
- version = "0.15.0"
3
+ version = "0.16.0"
4
4
  description = "ScrapeBadger CLI — Twitter, Vinted, Google, and Web Scraping from the command line"
5
5
  requires-python = ">=3.10"
6
6
  license = {text = "MIT"}
@@ -32,7 +32,7 @@ def search(
32
32
  sort: str | None = typer.Option(
33
33
  None,
34
34
  "--sort",
35
- help="relevance, newest, priceAscending, priceDescending, popularity",
35
+ help="relevance, priceAscending, priceDescending",
36
36
  ),
37
37
  fmt: str = FMT,
38
38
  fields: str | None = FIELDS,
@@ -881,7 +881,7 @@ def finance(
881
881
  fmt: str = FMT,
882
882
  fields: str | None = FIELDS,
883
883
  ) -> None:
884
- """Google Finance quote — via mKsvE RPC."""
884
+ """Google Finance quote."""
885
885
  data = api_get("/v1/google/finance/quote", {"q": q, "hl": hl, "gl": gl})
886
886
  render(data, fmt, fields)
887
887
 
@@ -31,10 +31,16 @@ def user_videos(
31
31
  count: int = COUNT,
32
32
  fmt: str = FMT,
33
33
  fields: str | None = FIELDS,
34
+ cursor: str | None = typer.Option(
35
+ None, "--cursor", help="Continuation cursor from the previous page"
36
+ ),
34
37
  ) -> None:
35
38
  """Get a user's posted videos."""
36
39
  render(
37
- api_get(f"/v1/tiktok/users/{username}/videos", {"region": region, "count": count}),
40
+ api_get(
41
+ f"/v1/tiktok/users/{username}/videos",
42
+ {**({"cursor": cursor} if cursor else {}), "region": region, "count": count},
43
+ ),
38
44
  fmt,
39
45
  fields,
40
46
  )
@@ -47,10 +53,16 @@ def followers(
47
53
  count: int = COUNT,
48
54
  fmt: str = FMT,
49
55
  fields: str | None = FIELDS,
56
+ cursor: str | None = typer.Option(
57
+ None, "--cursor", help="Continuation cursor from the previous page"
58
+ ),
50
59
  ) -> None:
51
60
  """Get a user's followers (best-effort)."""
52
61
  render(
53
- api_get(f"/v1/tiktok/users/{username}/followers", {"region": region, "count": count}),
62
+ api_get(
63
+ f"/v1/tiktok/users/{username}/followers",
64
+ {**({"cursor": cursor} if cursor else {}), "region": region, "count": count},
65
+ ),
54
66
  fmt,
55
67
  fields,
56
68
  )
@@ -63,10 +75,16 @@ def following(
63
75
  count: int = COUNT,
64
76
  fmt: str = FMT,
65
77
  fields: str | None = FIELDS,
78
+ cursor: str | None = typer.Option(
79
+ None, "--cursor", help="Continuation cursor from the previous page"
80
+ ),
66
81
  ) -> None:
67
82
  """Get accounts a user follows (best-effort)."""
68
83
  render(
69
- api_get(f"/v1/tiktok/users/{username}/following", {"region": region, "count": count}),
84
+ api_get(
85
+ f"/v1/tiktok/users/{username}/following",
86
+ {**({"cursor": cursor} if cursor else {}), "region": region, "count": count},
87
+ ),
70
88
  fmt,
71
89
  fields,
72
90
  )
@@ -98,12 +116,20 @@ def comments(
98
116
  username: str | None = typer.Option(None, "--username", "-u"),
99
117
  fmt: str = FMT,
100
118
  fields: str | None = FIELDS,
119
+ cursor: str | None = typer.Option(
120
+ None, "--cursor", help="Continuation cursor from the previous page"
121
+ ),
101
122
  ) -> None:
102
123
  """Get top-level comments on a video."""
103
124
  render(
104
125
  api_get(
105
126
  f"/v1/tiktok/videos/{video_id}/comments",
106
- {"region": region, "count": count, "username": username},
127
+ {
128
+ **({"cursor": cursor} if cursor else {}),
129
+ "region": region,
130
+ "count": count,
131
+ "username": username,
132
+ },
107
133
  ),
108
134
  fmt,
109
135
  fields,
@@ -118,12 +144,20 @@ def related(
118
144
  username: str | None = typer.Option(None, "--username", "-u"),
119
145
  fmt: str = FMT,
120
146
  fields: str | None = FIELDS,
147
+ cursor: str | None = typer.Option(
148
+ None, "--cursor", help="Continuation cursor from the previous page"
149
+ ),
121
150
  ) -> None:
122
151
  """Get related videos for a video."""
123
152
  render(
124
153
  api_get(
125
154
  f"/v1/tiktok/videos/{video_id}/related",
126
- {"region": region, "count": count, "username": username},
155
+ {
156
+ **({"cursor": cursor} if cursor else {}),
157
+ "region": region,
158
+ "count": count,
159
+ "username": username,
160
+ },
127
161
  ),
128
162
  fmt,
129
163
  fields,
@@ -177,10 +211,16 @@ def hashtag_videos(
177
211
  count: int = COUNT,
178
212
  fmt: str = FMT,
179
213
  fields: str | None = FIELDS,
214
+ cursor: str | None = typer.Option(
215
+ None, "--cursor", help="Continuation cursor from the previous page"
216
+ ),
180
217
  ) -> None:
181
218
  """Get videos tagged with a hashtag."""
182
219
  render(
183
- api_get(f"/v1/tiktok/hashtags/{name}/videos", {"region": region, "count": count}),
220
+ api_get(
221
+ f"/v1/tiktok/hashtags/{name}/videos",
222
+ {**({"cursor": cursor} if cursor else {}), "region": region, "count": count},
223
+ ),
184
224
  fmt,
185
225
  fields,
186
226
  )
@@ -204,10 +244,16 @@ def music_videos(
204
244
  count: int = COUNT,
205
245
  fmt: str = FMT,
206
246
  fields: str | None = FIELDS,
247
+ cursor: str | None = typer.Option(
248
+ None, "--cursor", help="Continuation cursor from the previous page"
249
+ ),
207
250
  ) -> None:
208
251
  """Get videos using a sound."""
209
252
  render(
210
- api_get(f"/v1/tiktok/music/{music_id}/videos", {"region": region, "count": count}),
253
+ api_get(
254
+ f"/v1/tiktok/music/{music_id}/videos",
255
+ {**({"cursor": cursor} if cursor else {}), "region": region, "count": count},
256
+ ),
211
257
  fmt,
212
258
  fields,
213
259
  )
@@ -221,24 +267,41 @@ def search(
221
267
  count: int = typer.Option(20, "--count", "-c"),
222
268
  fmt: str = FMT,
223
269
  fields: str | None = FIELDS,
270
+ cursor: str | None = typer.Option(
271
+ None, "--cursor", help="Continuation cursor from the previous page"
272
+ ),
224
273
  ) -> None:
225
274
  """Search TikTok (general/users/videos/hashtags)."""
226
275
  path = "/v1/tiktok/search" if kind == "general" else f"/v1/tiktok/search/{kind}"
227
- render(api_get(path, {"query": query, "region": region, "count": count}), fmt, fields)
276
+ render(
277
+ api_get(
278
+ path,
279
+ {
280
+ "query": query,
281
+ "region": region,
282
+ "count": count,
283
+ **({"cursor": cursor} if cursor else {}),
284
+ },
285
+ ),
286
+ fmt,
287
+ fields,
288
+ )
228
289
 
229
290
 
230
291
  @app.command()
231
292
  def trending(
232
293
  kind: str = typer.Option("videos", "--kind", "-k", help="videos | hashtags | songs"),
233
294
  region: str = REGION,
234
- period: int = typer.Option(7, "--period", help="Trend window in days (7/30/120)"),
295
+ period: int | None = typer.Option(
296
+ None, "--period", help="Historical windows unavailable; omit"
297
+ ),
235
298
  count: int = typer.Option(20, "--count", "-c"),
236
299
  fmt: str = FMT,
237
300
  fields: str | None = FIELDS,
238
301
  ) -> None:
239
302
  """Get trending data (videos/hashtags/songs)."""
240
303
  params = {"region": region, "count": count}
241
- if kind in ("hashtags", "songs"):
304
+ if kind in ("hashtags", "songs") and period is not None:
242
305
  params["period"] = period
243
306
  render(api_get(f"/v1/tiktok/trending/{kind}", params), fmt, fields)
244
307
 
@@ -305,7 +368,12 @@ def ad(
305
368
  render(api_get(f"/v1/tiktok/ads/{ad_id}", {"region": region}), fmt, fields)
306
369
 
307
370
 
308
- SHOP_REGION = typer.Option("US", "--region", "-r", help="Market: US, GB, ID")
371
+ SHOP_REGION = typer.Option(
372
+ "US",
373
+ "--region",
374
+ "-r",
375
+ help="Market: US, GB, ID, JP, MX, SG, MY, PH, TH, VN (coverage varies by endpoint)",
376
+ )
309
377
 
310
378
 
311
379
  @app.command(name="shop-categories")
@@ -324,17 +392,103 @@ def shop_categories(
324
392
  render(api_get(path, {"region": region}), fmt, fields)
325
393
 
326
394
 
395
+ @app.command(name="shop-category-products")
396
+ def shop_category_products(
397
+ category_id: str = typer.Argument(..., help="Numeric category id"),
398
+ region: str = SHOP_REGION,
399
+ count: int = typer.Option(20, "--count", "-c", min=1, max=40),
400
+ exclude_product_ids: list[str] = typer.Option(
401
+ [], "--exclude-product-id", help="Repeat for every next_exclude_product_ids value"
402
+ ),
403
+ fmt: str = FMT,
404
+ fields: str | None = FIELDS,
405
+ ) -> None:
406
+ """Page through a category's product recommendations."""
407
+ render(
408
+ api_get(
409
+ f"/v1/tiktok/shop/categories/{category_id}/products",
410
+ {
411
+ "region": region,
412
+ "count": count,
413
+ "exclude_product_ids": exclude_product_ids,
414
+ },
415
+ ),
416
+ fmt,
417
+ fields,
418
+ )
419
+
420
+
421
+ @app.command(name="shop-ranking")
422
+ def shop_ranking(
423
+ rank_id: str = typer.Argument(..., help="Native category/theme ranking id"),
424
+ region: str = SHOP_REGION,
425
+ rank_type: int = typer.Option(1, "--rank-type", min=1, max=3),
426
+ cursor: int = typer.Option(0, "--cursor", min=0),
427
+ count: int = typer.Option(20, "--count", "-c", min=1, max=20),
428
+ fmt: str = FMT,
429
+ fields: str | None = FIELDS,
430
+ ) -> None:
431
+ """Native Shop ranking; ID/PH/TH/VN require regional guest appliance profiles."""
432
+ render(
433
+ api_get(
434
+ f"/v1/tiktok/shop/rankings/{rank_id}",
435
+ {
436
+ "region": region,
437
+ "rank_type": rank_type,
438
+ "cursor": cursor,
439
+ "count": count,
440
+ },
441
+ ),
442
+ fmt,
443
+ fields,
444
+ )
445
+
446
+
447
+ @app.command(name="shop-bestsellers")
448
+ def shop_bestsellers(
449
+ region: str = SHOP_REGION,
450
+ category_id: str | None = typer.Option(None, "--category-id", help="Limit to one category"),
451
+ pages: int = typer.Option(2, "--pages", min=1, max=4),
452
+ limit: int = typer.Option(20, "--limit", "-l", min=1, max=100),
453
+ fmt: str = FMT,
454
+ fields: str | None = FIELDS,
455
+ ) -> None:
456
+ """Sales-ranked best-selling products; works for every market (incl. SG/MY/JP)."""
457
+ render(
458
+ api_get(
459
+ "/v1/tiktok/shop/bestsellers",
460
+ {
461
+ "region": region,
462
+ "category_id": category_id,
463
+ "pages": pages,
464
+ "limit": limit,
465
+ },
466
+ ),
467
+ fmt,
468
+ fields,
469
+ )
470
+
471
+
327
472
  @app.command(name="shop-search")
328
473
  def shop_search(
329
474
  query: str = typer.Argument(..., help="Keyword, e.g. 'wireless earbuds'"),
330
475
  region: str = SHOP_REGION,
476
+ page_token: str | None = typer.Option(None, "--page-token", help="Native ID/JP continuation"),
331
477
  offset: int = typer.Option(0, "--offset", help="Pass back pagination.next_offset (US)"),
332
478
  fmt: str = FMT,
333
479
  fields: str | None = FIELDS,
334
480
  ) -> None:
335
481
  """Search TikTok Shop products by keyword (30 per page, offset pagination in the US)."""
336
482
  render(
337
- api_get("/v1/tiktok/shop/search", {"q": query, "region": region, "offset": offset}),
483
+ api_get(
484
+ "/v1/tiktok/shop/search",
485
+ {
486
+ "q": query,
487
+ "region": region,
488
+ "offset": offset,
489
+ "page_token": page_token,
490
+ },
491
+ ),
338
492
  fmt,
339
493
  fields,
340
494
  )
@@ -384,7 +538,7 @@ def shop_reviews(
384
538
  fmt: str = FMT,
385
539
  fields: str | None = FIELDS,
386
540
  ) -> None:
387
- """Paginated TikTok Shop product reviews with the rating breakdown (US)."""
541
+ """Paginated product reviews (US; Tokopedia source for ID)."""
388
542
  params: dict = {
389
543
  "region": region,
390
544
  "page": page,
@@ -405,3 +559,100 @@ def regions(
405
559
  ) -> None:
406
560
  """List supported TikTok content regions."""
407
561
  render(api_get("/v1/tiktok/regions"), fmt, fields)
562
+
563
+
564
+ @app.command(name="shop-mall")
565
+ def shop_mall(
566
+ region: str = SHOP_REGION,
567
+ tab_id: int = typer.Option(0, "--tab-id", min=0),
568
+ page_token: str | None = typer.Option(None, "--page-token"),
569
+ fmt: str = FMT,
570
+ fields: str | None = FIELDS,
571
+ ) -> None:
572
+ """Regional mall navigation and feed; pass back the 15-minute page token."""
573
+ render(
574
+ api_get(
575
+ "/v1/tiktok/shop/mall",
576
+ {
577
+ "region": region,
578
+ "tab_id": tab_id,
579
+ "page_token": page_token,
580
+ },
581
+ ),
582
+ fmt,
583
+ fields,
584
+ )
585
+
586
+
587
+ @app.command()
588
+ def liked(
589
+ username: str = typer.Argument(..., help="TikTok handle"),
590
+ region: str = REGION,
591
+ count: int = COUNT,
592
+ fmt: str = FMT,
593
+ fields: str | None = FIELDS,
594
+ cursor: str | None = typer.Option(
595
+ None, "--cursor", help="Continuation cursor from the previous page"
596
+ ),
597
+ ) -> None:
598
+ """Get accounts a user follows (best-effort)."""
599
+ render(
600
+ api_get(
601
+ f"/v1/tiktok/users/{username}/liked",
602
+ {**({"cursor": cursor} if cursor else {}), "region": region, "count": count},
603
+ ),
604
+ fmt,
605
+ fields,
606
+ )
607
+
608
+
609
+ @app.command()
610
+ def reposts(
611
+ username: str = typer.Argument(..., help="TikTok handle"),
612
+ region: str = REGION,
613
+ count: int = COUNT,
614
+ fmt: str = FMT,
615
+ fields: str | None = FIELDS,
616
+ cursor: str | None = typer.Option(
617
+ None, "--cursor", help="Continuation cursor from the previous page"
618
+ ),
619
+ ) -> None:
620
+ """Get accounts a user follows (best-effort)."""
621
+ render(
622
+ api_get(
623
+ f"/v1/tiktok/users/{username}/reposts",
624
+ {**({"cursor": cursor} if cursor else {}), "region": region, "count": count},
625
+ ),
626
+ fmt,
627
+ fields,
628
+ )
629
+
630
+
631
+ @app.command()
632
+ def replies(
633
+ comment_id: str = typer.Argument(..., help="Comment id"),
634
+ video_id: str = typer.Argument(..., help="Numeric video id"),
635
+ region: str = REGION,
636
+ count: int = typer.Option(20, "--count", "-c"),
637
+ username: str | None = typer.Option(None, "--username", "-u"),
638
+ fmt: str = FMT,
639
+ fields: str | None = FIELDS,
640
+ cursor: str | None = typer.Option(
641
+ None, "--cursor", help="Continuation cursor from the previous page"
642
+ ),
643
+ ) -> None:
644
+ """Get replies to a public comment."""
645
+ render(
646
+ api_get(
647
+ f"/v1/tiktok/comments/{comment_id}/replies",
648
+ {
649
+ **({"cursor": cursor} if cursor else {}),
650
+ "region": region,
651
+ "video_id": video_id,
652
+ "count": count,
653
+ "username": username,
654
+ },
655
+ ),
656
+ fmt,
657
+ fields,
658
+ )
@@ -0,0 +1,36 @@
1
+ from unittest.mock import patch
2
+
3
+ import pytest
4
+
5
+ CliRunner = pytest.importorskip("typer.testing").CliRunner
6
+ app = pytest.importorskip(
7
+ "scrapebadger_cli.commands.tiktok", reason="CLI is tested with its own installed package"
8
+ ).app
9
+
10
+
11
+ @pytest.mark.parametrize(
12
+ "args,path",
13
+ [
14
+ (["followers", "tiktok"], "/users/tiktok/followers"),
15
+ (["following", "tiktok"], "/users/tiktok/following"),
16
+ (["liked", "tiktok"], "/users/tiktok/liked"),
17
+ (["reposts", "tiktok"], "/users/tiktok/reposts"),
18
+ (["replies", "123", "456"], "/comments/123/replies"),
19
+ (["search", "news", "--kind", "users"], "/search/users"),
20
+ ],
21
+ )
22
+ def test_continuations_are_forwarded(args, path):
23
+ with patch("scrapebadger_cli.commands.tiktok.api_get", return_value={}) as call:
24
+ result = CliRunner().invoke(app, args + ["--cursor", "tw1_test"])
25
+ assert result.exit_code == 0, result.output
26
+ assert call.call_args.args[0] == "/v1/tiktok" + path
27
+ assert call.call_args.args[1]["cursor"] == "tw1_test"
28
+ if args[0] == "replies":
29
+ assert call.call_args.args[1]["video_id"] == "456"
30
+
31
+
32
+ def test_trends_do_not_invent_a_historical_window():
33
+ with patch("scrapebadger_cli.commands.tiktok.api_get", return_value={}) as call:
34
+ result = CliRunner().invoke(app, ["trending", "--kind", "songs"])
35
+ assert result.exit_code == 0, result.output
36
+ assert "period" not in call.call_args.args[1]