adloop 0.16.0__tar.gz → 0.16.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 (60) hide show
  1. {adloop-0.16.0 → adloop-0.16.1}/PKG-INFO +1 -1
  2. {adloop-0.16.0 → adloop-0.16.1}/pyproject.toml +1 -1
  3. {adloop-0.16.0 → adloop-0.16.1}/pyproject.toml.orig +1 -1
  4. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/ads/validate_only.py +41 -14
  5. {adloop-0.16.0 → adloop-0.16.1}/README.md +0 -0
  6. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/__init__.py +0 -0
  7. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/__main__.py +0 -0
  8. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/_mcp_patches.py +0 -0
  9. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/ads/__init__.py +0 -0
  10. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/ads/client.py +0 -0
  11. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/ads/conversion_actions.py +0 -0
  12. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/ads/currency.py +0 -0
  13. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/ads/enums.py +0 -0
  14. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/ads/forecast.py +0 -0
  15. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/ads/gaql.py +0 -0
  16. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/ads/pmax.py +0 -0
  17. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/ads/read.py +0 -0
  18. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/ads/write.py +0 -0
  19. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/auth.py +0 -0
  20. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/cli.py +0 -0
  21. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/config.py +0 -0
  22. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/crossref.py +0 -0
  23. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/diagnostics.py +0 -0
  24. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/ga4/__init__.py +0 -0
  25. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/ga4/client.py +0 -0
  26. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/ga4/reports.py +0 -0
  27. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/ga4/tracking.py +0 -0
  28. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/ga4/write.py +0 -0
  29. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/gsc/__init__.py +0 -0
  30. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/gsc/client.py +0 -0
  31. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/gsc/reports.py +0 -0
  32. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/gtm/__init__.py +0 -0
  33. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/gtm/client.py +0 -0
  34. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/gtm/read.py +0 -0
  35. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/merchant/__init__.py +0 -0
  36. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/merchant/client.py +0 -0
  37. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/merchant/read.py +0 -0
  38. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/pagespeed.py +0 -0
  39. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/reddit/__init__.py +0 -0
  40. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/reddit/auth.py +0 -0
  41. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/reddit/client.py +0 -0
  42. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/reddit/read.py +0 -0
  43. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/reddit/schedule.py +0 -0
  44. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/reddit/write.py +0 -0
  45. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/rules/__init__.py +0 -0
  46. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/rules/adloop.md +0 -0
  47. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/rules/commands/analyze-performance.md +0 -0
  48. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/rules/commands/budget-plan.md +0 -0
  49. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/rules/commands/create-ad.md +0 -0
  50. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/rules/commands/create-campaign.md +0 -0
  51. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
  52. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/rules/commands/optimize-campaign.md +0 -0
  53. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/rules_install.py +0 -0
  54. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/runtime.py +0 -0
  55. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/safety/__init__.py +0 -0
  56. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/safety/audit.py +0 -0
  57. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/safety/guards.py +0 -0
  58. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/safety/preview.py +0 -0
  59. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/server.py +0 -0
  60. {adloop-0.16.0 → adloop-0.16.1}/src/adloop/tracking.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: adloop
3
- Version: 0.16.0
3
+ Version: 0.16.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adloop"
3
- version = "0.16.0"
3
+ version = "0.16.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.16.0"
3
+ version = "0.16.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 = [
@@ -10,6 +10,11 @@ Validate-only responses carry no results, so each validated call answers the
10
10
  apply code with placeholder resource names. A later call that references one
11
11
  (a plan whose second step uses what the first would have created) cannot be
12
12
  validated meaningfully; it is skipped and counted instead of sent.
13
+
14
+ The wrapper fails closed: only path helpers, reads, ``mutate*`` and
15
+ ``upload*`` calls (both sent validate-only) reach the real service. Any other
16
+ method raises, so a write path added later cannot slip past a dry run and
17
+ change an account for real.
13
18
  """
14
19
 
15
20
  from __future__ import annotations
@@ -18,7 +23,8 @@ from types import SimpleNamespace
18
23
 
19
24
  PLACEHOLDER = "adloop-validate-only"
20
25
 
21
- _RESPONSE_FIELD_FOR_SERVICE = "campaign_result"
26
+ # Methods that never change an account and may run for real in a dry run.
27
+ _READ_METHODS = frozenset({"search", "search_stream"})
22
28
 
23
29
 
24
30
  class ValidateOnlyFailure(Exception):
@@ -52,9 +58,14 @@ class _ValidateOnlyService:
52
58
  self._service = service
53
59
 
54
60
  def __getattr__(self, attr: str) -> object:
61
+ if attr in _READ_METHODS or attr.endswith("_path") or attr.startswith("parse_"):
62
+ return getattr(self._service, attr)
63
+ if not (attr.startswith("mutate") or attr.startswith("upload")):
64
+ raise ValidateOnlyFailure(
65
+ f"{self._name}.{attr} has no validate-only mode, so a dry run "
66
+ "cannot check it without changing the account. Refusing."
67
+ )
55
68
  target = getattr(self._service, attr)
56
- if not attr.startswith("mutate"):
57
- return target
58
69
 
59
70
  def validate(request: object = None, **kwargs: object) -> object:
60
71
  if request is None:
@@ -78,35 +89,51 @@ class _ValidateOnlyService:
78
89
  def _build_request(self, method: str, kwargs: dict) -> object:
79
90
  request = self._owner._client.get_type(_request_type(self._name, method))
80
91
  for key, value in kwargs.items():
81
- if key in ("operations", "mutate_operations"):
92
+ if key in _LIST_FIELDS:
82
93
  getattr(request, key).extend(value)
83
94
  else:
84
95
  setattr(request, key, value)
85
96
  return request
86
97
 
87
98
 
99
+ _LIST_FIELDS = ("operations", "mutate_operations", "conversions")
100
+
101
+
88
102
  def _request_type(service_name: str, method: str) -> str:
89
- """``mutate_ad_group_criteria`` -> ``MutateAdGroupCriteriaRequest``."""
103
+ """``mutate_ad_group_criteria`` -> ``MutateAdGroupCriteriaRequest``,
104
+ ``upload_click_conversions`` -> ``UploadClickConversionsRequest``."""
90
105
  if method == "mutate":
91
106
  return "Mutate" + service_name.removesuffix("Service") + "Request"
92
- words = method.removeprefix("mutate_").split("_")
93
- return "Mutate" + "".join(word.capitalize() for word in words) + "Request"
107
+ verb, _, rest = method.partition("_")
108
+ return verb.capitalize() + "".join(word.capitalize() for word in rest.split("_")) + "Request"
94
109
 
95
110
 
96
111
  def _operations_of(request: object) -> object:
97
- if hasattr(request, "mutate_operations"):
98
- return request.mutate_operations
99
- return getattr(request, "operations", [])
112
+ for field in _LIST_FIELDS:
113
+ if hasattr(request, field):
114
+ return getattr(request, field)
115
+ return []
116
+
117
+
118
+ class _PlaceholderResult:
119
+ """A MutateOperationResponse stand-in: every ``*_result`` field
120
+ (``asset_result``, ``campaign_asset_result``, ...) carries the placeholder,
121
+ whichever one the apply code reads."""
122
+
123
+ def __init__(self, name: str) -> None:
124
+ self._name = name
125
+
126
+ def __getattr__(self, attr: str) -> object:
127
+ if attr.endswith("_result"):
128
+ return SimpleNamespace(resource_name=self._name)
129
+ raise AttributeError(attr)
100
130
 
101
131
 
102
132
  def _placeholder_response(customer_id: str, count: int, googleads_mutate: bool) -> object:
103
133
  names = [f"customers/{customer_id}/{PLACEHOLDER}/{i}" for i in range(count)]
104
134
  if googleads_mutate:
105
135
  return SimpleNamespace(
106
- mutate_operation_responses=[
107
- SimpleNamespace(**{_RESPONSE_FIELD_FOR_SERVICE: SimpleNamespace(resource_name=n)})
108
- for n in names
109
- ],
136
+ mutate_operation_responses=[_PlaceholderResult(n) for n in names],
110
137
  partial_failure_error=None,
111
138
  )
112
139
  return SimpleNamespace(
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