adloop 0.27.0__tar.gz → 0.27.1__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 (71) hide show
  1. {adloop-0.27.0 → adloop-0.27.1}/PKG-INFO +6 -5
  2. {adloop-0.27.0 → adloop-0.27.1}/README.md +5 -4
  3. {adloop-0.27.0 → adloop-0.27.1}/pyproject.toml +1 -1
  4. {adloop-0.27.0 → adloop-0.27.1}/pyproject.toml.orig +1 -1
  5. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/cli.py +136 -58
  6. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/__init__.py +0 -0
  7. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/__main__.py +0 -0
  8. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/_mcp_patches.py +0 -0
  9. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ads/__init__.py +0 -0
  10. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ads/ai_max.py +0 -0
  11. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ads/brands.py +0 -0
  12. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ads/client.py +0 -0
  13. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ads/conversion_actions.py +0 -0
  14. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ads/currency.py +0 -0
  15. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ads/custom_conversion_goals.py +0 -0
  16. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ads/enums.py +0 -0
  17. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ads/forecast.py +0 -0
  18. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ads/gaql.py +0 -0
  19. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ads/image_fetch.py +0 -0
  20. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ads/pmax.py +0 -0
  21. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ads/read.py +0 -0
  22. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ads/shopping.py +0 -0
  23. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ads/validate_only.py +0 -0
  24. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ads/write.py +0 -0
  25. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/auth.py +0 -0
  26. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/config.py +0 -0
  27. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/crossref.py +0 -0
  28. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/diagnostics.py +0 -0
  29. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ga4/__init__.py +0 -0
  30. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ga4/client.py +0 -0
  31. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ga4/reports.py +0 -0
  32. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ga4/tracking.py +0 -0
  33. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/ga4/write.py +0 -0
  34. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/gsc/__init__.py +0 -0
  35. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/gsc/client.py +0 -0
  36. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/gsc/reports.py +0 -0
  37. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/gtm/__init__.py +0 -0
  38. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/gtm/client.py +0 -0
  39. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/gtm/entities.py +0 -0
  40. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/gtm/read.py +0 -0
  41. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/gtm/write.py +0 -0
  42. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/merchant/__init__.py +0 -0
  43. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/merchant/client.py +0 -0
  44. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/merchant/read.py +0 -0
  45. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/pagespeed.py +0 -0
  46. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/reddit/__init__.py +0 -0
  47. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/reddit/auth.py +0 -0
  48. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/reddit/client.py +0 -0
  49. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/reddit/read.py +0 -0
  50. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/reddit/schedule.py +0 -0
  51. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/reddit/write.py +0 -0
  52. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/rules/__init__.py +0 -0
  53. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/rules/adloop.md +0 -0
  54. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/rules/commands/analyze-performance.md +0 -0
  55. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/rules/commands/budget-plan.md +0 -0
  56. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/rules/commands/create-ad.md +0 -0
  57. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/rules/commands/create-campaign.md +0 -0
  58. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
  59. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/rules/commands/optimize-campaign.md +0 -0
  60. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/rules_install.py +0 -0
  61. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/runtime.py +0 -0
  62. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/safety/__init__.py +0 -0
  63. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/safety/approval.py +0 -0
  64. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/safety/approval_card.html +0 -0
  65. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/safety/audit.py +0 -0
  66. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/safety/guards.py +0 -0
  67. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/safety/preview.py +0 -0
  68. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/server.py +0 -0
  69. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/tracking.py +0 -0
  70. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/upload_card.html +0 -0
  71. {adloop-0.27.0 → adloop-0.27.1}/src/adloop/uploads.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: adloop
3
- Version: 0.27.0
3
+ Version: 0.27.1
4
4
  Summary: The AI command center for Google Ads, Reddit Ads, GA4, and tracking code.
5
5
  Keywords: mcp,google-ads,reddit-ads,google-analytics,ga4,cursor,marketing
6
6
  Author: Daniel Klose
@@ -404,7 +404,7 @@ uv run adloop init
404
404
 
405
405
  ### What `adloop init` does
406
406
 
407
- The wizard walks you through:
407
+ The wizard first asks whether to set up Google. Answer no to run AdLoop on Reddit Ads alone: it skips steps 1 to 6 and goes straight to the Reddit step. Otherwise it walks you through:
408
408
 
409
409
  1. **Google Cloud setup** — creates a project, enables the three APIs, generates an OAuth client (see [Custom Google Cloud Project Setup](#custom-google-cloud-project-setup) below for the exact steps the wizard refers you to)
410
410
  2. **Google Ads API access** — applied for on your Cloud project's [Google Ads API Overview page](https://console.cloud.google.com/google/ads-apis/overview); the legacy developer-token prompt can be left empty
@@ -412,9 +412,10 @@ The wizard walks you through:
412
412
  4. **OAuth sign-in** — opens a browser to sign in with Google (or prints a URL for headless servers)
413
413
  5. **Auto-discovers your accounts** — finds your GA4 properties and Ads accounts automatically
414
414
  6. **Optional services** — pin a GTM container, a Search Console property (both auto-discovered too), and a PageSpeed API key; skip any of them with Enter
415
- 7. **Safety defaults** — budget cap and dry-run preference
416
- 8. **Toolsets** — optionally expose only part of the tool catalog to your AI client (see [Toolsets](#toolsets--trim-the-context-footprint))
417
- 9. **Editor config snippets** — prints MCP configuration for both Cursor and Claude Code, including your toolset selection
415
+ 7. **Reddit Ads** — optional next to Google, required without it: your own Reddit developer app, sign-in, and the default ad account
416
+ 8. **Safety defaults** — budget cap and dry-run preference
417
+ 9. **Toolsets** — optionally expose only part of the tool catalog to your AI client (see [Toolsets](#toolsets--trim-the-context-footprint))
418
+ 10. **Editor config snippets** — prints MCP configuration for both Cursor and Claude Code, including your toolset selection
418
419
 
419
420
  ### Requirements
420
421
 
@@ -376,7 +376,7 @@ uv run adloop init
376
376
 
377
377
  ### What `adloop init` does
378
378
 
379
- The wizard walks you through:
379
+ The wizard first asks whether to set up Google. Answer no to run AdLoop on Reddit Ads alone: it skips steps 1 to 6 and goes straight to the Reddit step. Otherwise it walks you through:
380
380
 
381
381
  1. **Google Cloud setup** — creates a project, enables the three APIs, generates an OAuth client (see [Custom Google Cloud Project Setup](#custom-google-cloud-project-setup) below for the exact steps the wizard refers you to)
382
382
  2. **Google Ads API access** — applied for on your Cloud project's [Google Ads API Overview page](https://console.cloud.google.com/google/ads-apis/overview); the legacy developer-token prompt can be left empty
@@ -384,9 +384,10 @@ The wizard walks you through:
384
384
  4. **OAuth sign-in** — opens a browser to sign in with Google (or prints a URL for headless servers)
385
385
  5. **Auto-discovers your accounts** — finds your GA4 properties and Ads accounts automatically
386
386
  6. **Optional services** — pin a GTM container, a Search Console property (both auto-discovered too), and a PageSpeed API key; skip any of them with Enter
387
- 7. **Safety defaults** — budget cap and dry-run preference
388
- 8. **Toolsets** — optionally expose only part of the tool catalog to your AI client (see [Toolsets](#toolsets--trim-the-context-footprint))
389
- 9. **Editor config snippets** — prints MCP configuration for both Cursor and Claude Code, including your toolset selection
387
+ 7. **Reddit Ads** — optional next to Google, required without it: your own Reddit developer app, sign-in, and the default ad account
388
+ 8. **Safety defaults** — budget cap and dry-run preference
389
+ 9. **Toolsets** — optionally expose only part of the tool catalog to your AI client (see [Toolsets](#toolsets--trim-the-context-footprint))
390
+ 10. **Editor config snippets** — prints MCP configuration for both Cursor and Claude Code, including your toolset selection
390
391
 
391
392
  ### Requirements
392
393
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adloop"
3
- version = "0.27.0"
3
+ version = "0.27.1"
4
4
  description = "The AI command center for Google Ads, Reddit Ads, GA4, and tracking code."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adloop"
3
- version = "0.27.0"
3
+ version = "0.27.1"
4
4
  description = "The AI command center for Google Ads, Reddit Ads, GA4, and tracking code."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -414,7 +414,7 @@ def _wizard_gsc_step(oauth_ok: bool, _existing) -> str:
414
414
  )
415
415
 
416
416
 
417
- def _wizard_reddit_step(_existing) -> dict[str, str]:
417
+ def _wizard_reddit_step(_existing, required: bool = False) -> dict[str, str]:
418
418
  """Optionally connect Reddit Ads: own developer app, own OAuth, own token file.
419
419
 
420
420
  Returns the ``reddit:`` values for the config. Reddit's redirect URI
@@ -430,7 +430,8 @@ def _wizard_reddit_step(_existing) -> dict[str, str]:
430
430
  "ad_account_id": _existing("reddit", "ad_account_id"),
431
431
  "username": _existing("reddit", "username"),
432
432
  }
433
- if not _prompt_bool("Connect Reddit Ads? (optional)", default=bool(existing_id)):
433
+ # Without Google, Reddit is the platform being set up: no opt-out.
434
+ if not required and not _prompt_bool("Connect Reddit Ads? (optional)", default=bool(existing_id)):
434
435
  return values
435
436
 
436
437
  _print()
@@ -589,6 +590,70 @@ def run_init_wizard() -> None:
589
590
  return str(existing_config[section].get(key, fallback))
590
591
  return fallback
591
592
 
593
+ # Both platforms are optional; a workspace can run on Reddit alone.
594
+ _print(" AdLoop connects Google (Ads, Analytics, Tag Manager, Search Console)")
595
+ _print(" and Reddit Ads. Set up one or both.")
596
+ _print()
597
+ use_google = _prompt_bool(
598
+ "Set up Google?", default=_existing("google", "credentials_path") != "" or existing_config is None
599
+ )
600
+ credentials_path = project_id = developer_token = login_customer_id = ""
601
+ step_num = 0
602
+ if use_google:
603
+ step_num = _wizard_google_steps(_existing)
604
+ credentials_path, project_id, developer_token, login_customer_id = _WIZARD_GOOGLE
605
+
606
+ # OAuth + auto-discovery
607
+ step_num += 1
608
+ _step_header(step_num, "Authorization & Account Discovery" if use_google else "Writing a first config")
609
+
610
+ # Write a temporary config for OAuth + discovery. If the wizard is
611
+ # interrupted after this point, _cleanup_on_cancel restores the original.
612
+ _ADLOOP_DIR.mkdir(parents=True, exist_ok=True)
613
+ temp_config_yaml = _generate_config_yaml(
614
+ project_id=project_id,
615
+ credentials_path=credentials_path,
616
+ property_id="",
617
+ developer_token=developer_token,
618
+ customer_id="",
619
+ login_customer_id=login_customer_id,
620
+ max_daily_budget=50.0,
621
+ require_dry_run=True,
622
+ )
623
+ _CONFIG_PATH.write_text(temp_config_yaml)
624
+
625
+ # Everything below uses the temp config for OAuth and discovery.
626
+ # If the wizard is interrupted, restore the original config (or remove
627
+ # the temp) so we never leave a half-baked config on disk.
628
+ try:
629
+ _run_wizard_post_config(
630
+ credentials_path=credentials_path,
631
+ project_id=project_id,
632
+ developer_token=developer_token,
633
+ login_customer_id=login_customer_id,
634
+ step_num=step_num,
635
+ _existing=_existing,
636
+ use_google=use_google,
637
+ )
638
+ except KeyboardInterrupt:
639
+ if _original_config_backup is not None:
640
+ _CONFIG_PATH.write_text(_original_config_backup)
641
+ elif _CONFIG_PATH.exists():
642
+ _CONFIG_PATH.unlink()
643
+ raise
644
+
645
+
646
+ _WIZARD_GOOGLE: tuple[str, str, str, str] = ("", "", "", "")
647
+
648
+
649
+ def _wizard_google_steps(_existing) -> int:
650
+ """The Google project, credentials and API access steps; returns the last step number.
651
+
652
+ The collected values land in ``_WIZARD_GOOGLE`` (credentials path,
653
+ project id, developer token, login customer id).
654
+ """
655
+ global _WIZARD_GOOGLE
656
+
592
657
  # Step 1: Google Cloud project
593
658
  #
594
659
  # AdLoop no longer ships OAuth credentials. Every install brings its
@@ -641,44 +706,8 @@ def run_init_wizard() -> None:
641
706
  default=_existing("ads", "login_customer_id"),
642
707
  required=False,
643
708
  )
644
-
645
- # OAuth + auto-discovery
646
- step_num += 1
647
- _step_header(step_num, "Authorization & Account Discovery")
648
-
649
- # Write a temporary config for OAuth + discovery. If the wizard is
650
- # interrupted after this point, _cleanup_on_cancel restores the original.
651
- _ADLOOP_DIR.mkdir(parents=True, exist_ok=True)
652
- temp_config_yaml = _generate_config_yaml(
653
- project_id=project_id,
654
- credentials_path=credentials_path,
655
- property_id="",
656
- developer_token=developer_token,
657
- customer_id="",
658
- login_customer_id=login_customer_id,
659
- max_daily_budget=50.0,
660
- require_dry_run=True,
661
- )
662
- _CONFIG_PATH.write_text(temp_config_yaml)
663
-
664
- # Everything below uses the temp config for OAuth and discovery.
665
- # If the wizard is interrupted, restore the original config (or remove
666
- # the temp) so we never leave a half-baked config on disk.
667
- try:
668
- _run_wizard_post_config(
669
- credentials_path=credentials_path,
670
- project_id=project_id,
671
- developer_token=developer_token,
672
- login_customer_id=login_customer_id,
673
- step_num=step_num,
674
- _existing=_existing,
675
- )
676
- except KeyboardInterrupt:
677
- if _original_config_backup is not None:
678
- _CONFIG_PATH.write_text(_original_config_backup)
679
- elif _CONFIG_PATH.exists():
680
- _CONFIG_PATH.unlink()
681
- raise
709
+ _WIZARD_GOOGLE = (credentials_path, project_id, developer_token, login_customer_id)
710
+ return step_num
682
711
 
683
712
 
684
713
  def _run_wizard_post_config(
@@ -689,10 +718,9 @@ def _run_wizard_post_config(
689
718
  login_customer_id: str,
690
719
  step_num: int,
691
720
  _existing: object,
721
+ use_google: bool = True,
692
722
  ) -> None:
693
723
  """Run the wizard steps after the temp config has been written."""
694
- from adloop.config import load_config
695
-
696
724
  # Optional: copy custom credentials to ~/.adloop/
697
725
  if credentials_path:
698
726
  creds_expanded = Path(credentials_path).expanduser()
@@ -706,6 +734,54 @@ def _run_wizard_post_config(
706
734
  shutil.copy2(creds_expanded, adloop_creds)
707
735
  _print(f" ✓ Credentials copied to {adloop_creds}")
708
736
 
737
+ oauth_ok = False
738
+ property_id = ""
739
+ customer_id = ""
740
+ gtm_account_id = gtm_container_id = gsc_site_url = ""
741
+ if use_google:
742
+ oauth_ok, property_id, customer_id, step_num = _wizard_google_discovery(step_num, _existing)
743
+
744
+ # Optional service pins: GTM / Search Console / PageSpeed. All three
745
+ # work without config (explicit args or discovery per call) — a pin
746
+ # just saves the model a lookup and locks tools to the right property.
747
+ step_num += 1
748
+ if use_google:
749
+ _step_header(step_num, "Optional Services (GTM, Search Console, PageSpeed)")
750
+ gtm_account_id, gtm_container_id = _wizard_gtm_step(oauth_ok, _existing)
751
+ gsc_site_url = _wizard_gsc_step(oauth_ok, _existing)
752
+ else:
753
+ _step_header(step_num, "PageSpeed (optional)")
754
+ pagespeed_api_key = _prompt(
755
+ "PageSpeed Insights API key (optional, Enter to skip)",
756
+ default=_existing("pagespeed", "api_key"), # type: ignore[operator]
757
+ required=False,
758
+ )
759
+
760
+ # Reddit Ads: a second ad platform with its own OAuth app and token file.
761
+ step_num += 1
762
+ _step_header(step_num, "Reddit Ads" if not use_google else "Reddit Ads (optional)")
763
+ reddit_values = _wizard_reddit_step(_existing, required=not use_google)
764
+ _wizard_finish(
765
+ step_num=step_num,
766
+ _existing=_existing,
767
+ credentials_path=credentials_path,
768
+ project_id=project_id,
769
+ developer_token=developer_token,
770
+ login_customer_id=login_customer_id,
771
+ property_id=property_id,
772
+ customer_id=customer_id,
773
+ gtm_account_id=gtm_account_id,
774
+ gtm_container_id=gtm_container_id,
775
+ gsc_site_url=gsc_site_url,
776
+ pagespeed_api_key=pagespeed_api_key,
777
+ reddit_values=reddit_values,
778
+ )
779
+
780
+
781
+ def _wizard_google_discovery(step_num: int, _existing) -> tuple[bool, str, str, int]:
782
+ """Google sign-in plus GA4 and Ads account discovery: (oauth_ok, property_id, customer_id, step)."""
783
+ from adloop.config import load_config
784
+
709
785
  _print(" Signing in with Google (this may open a browser)...")
710
786
  _print()
711
787
  oauth_ok = False
@@ -797,24 +873,26 @@ def _run_wizard_post_config(
797
873
  default=_existing("ads", "customer_id"), # type: ignore[operator]
798
874
  )
799
875
 
800
- # Optional service pins: GTM / Search Console / PageSpeed. All three
801
- # work without config (explicit args or discovery per call) — a pin
802
- # just saves the model a lookup and locks tools to the right property.
803
- step_num += 1
804
- _step_header(step_num, "Optional Services (GTM, Search Console, PageSpeed)")
805
- gtm_account_id, gtm_container_id = _wizard_gtm_step(oauth_ok, _existing)
806
- gsc_site_url = _wizard_gsc_step(oauth_ok, _existing)
807
- pagespeed_api_key = _prompt(
808
- "PageSpeed Insights API key (optional, Enter to skip)",
809
- default=_existing("pagespeed", "api_key"), # type: ignore[operator]
810
- required=False,
811
- )
876
+ return oauth_ok, property_id, customer_id, step_num
812
877
 
813
- # Reddit Ads: a second ad platform with its own OAuth app and token file.
814
- step_num += 1
815
- _step_header(step_num, "Reddit Ads (optional)")
816
- reddit_values = _wizard_reddit_step(_existing)
817
878
 
879
+ def _wizard_finish(
880
+ *,
881
+ step_num: int,
882
+ _existing,
883
+ credentials_path: str,
884
+ project_id: str,
885
+ developer_token: str,
886
+ login_customer_id: str,
887
+ property_id: str,
888
+ customer_id: str,
889
+ gtm_account_id: str,
890
+ gtm_container_id: str,
891
+ gsc_site_url: str,
892
+ pagespeed_api_key: str,
893
+ reddit_values: dict,
894
+ ) -> None:
895
+ """Safety defaults, the final config and the client snippets."""
818
896
  # Safety defaults
819
897
  step_num += 1
820
898
  _step_header(step_num, "Safety Defaults")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes