asa-api-client 0.3.0__tar.gz → 0.4.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.
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/PKG-INFO +1 -1
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/__init__.py +1 -1
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/cli/analyze.py +11 -1
- asa_api_client-0.4.0/asa_api_client/cli/popularity.py +258 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/cli/workbook.py +24 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/client.py +21 -2
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/ad_groups.py +11 -58
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/budget_orders.py +6 -39
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/campaigns.py +12 -81
- asa_api_client-0.4.0/asa_api_client/v1/models/shared.py +99 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/creatives.py +55 -12
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/pyproject.toml +1 -1
- asa_api_client-0.4.0/tests/unit/cli/test_popularity.py +434 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/cli/test_workbook.py +56 -1
- asa_api_client-0.4.0/tests/unit/test_deprecation.py +44 -0
- asa_api_client-0.4.0/tests/unit/test_version.py +17 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_creatives_assets.py +26 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/uv.lock +1 -1
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/.github/dependabot.yml +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/.github/workflows/ci.yml +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/.github/workflows/docs.yml +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/.github/workflows/publish.yml +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/.gitignore +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/.pre-commit-config.yaml +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/LICENSE +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/README.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/auth.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/cli/__init__.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/cli/dates.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/cli/fetch.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/cli/metrics.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/cli/v1_adapter.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/cli/v1_smoke.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/exceptions.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/logging.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/models/__init__.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/models/acls.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/models/ad_groups.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/models/ads.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/models/apps.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/models/base.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/models/budget_orders.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/models/campaigns.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/models/countries.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/models/geo.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/models/keywords.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/models/product_pages.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/models/reports.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/resources/__init__.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/resources/acls.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/resources/ad_groups.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/resources/ads.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/resources/apps.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/resources/base.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/resources/budget_orders.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/resources/campaigns.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/resources/countries.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/resources/custom_reports.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/resources/geo.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/resources/keywords.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/resources/product_pages.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/resources/reports.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/settings.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/utils/__init__.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/__init__.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/client.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/__init__.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/ad_accounts.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/ads.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/apps.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/base.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/brands.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/bulk.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/change_history.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/creatives.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/geo.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/insights.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/keywords.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/product_pages.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/recommendations.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/reports.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/models/suggestions.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/query.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/__init__.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/ad_accounts.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/ad_groups.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/ads.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/apps.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/base.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/brands.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/budget_orders.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/bulk.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/campaigns.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/change_history.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/geo.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/insights.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/keywords.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/product_pages.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/recommendations.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/reports.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/asa_api_client/v1/resources/suggestions.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/CNAME +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/api/client.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/api/exceptions.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/api/models.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/getting-started/authentication.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/getting-started/installation.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/getting-started/quickstart.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/guide/ad-groups.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/guide/async.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/guide/campaigns.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/guide/cli.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/guide/keywords.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/guide/platform-api-v1.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/guide/reports.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/index.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/superpowers/plans/2026-08-05-analyze-cli.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/superpowers/plans/2026-08-16-apple-ads-platform-api-v1.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/superpowers/specs/2026-08-05-analyze-cli-design.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/docs/superpowers/specs/2026-08-16-apple-ads-platform-api-v1-design.md +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/mkdocs.yml +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/__init__.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/__init__.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/cli/__init__.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/cli/conftest.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/cli/test_cli.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/cli/test_dates.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/cli/test_entry.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/cli/test_fetch.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/cli/test_metrics.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/cli/test_v1_adapter.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/cli/test_v1_smoke.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/test_models.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/__init__.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/conftest.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_ad_accounts.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_ad_groups.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_ads.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_apps.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_brands.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_budget_orders.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_bulk.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_campaigns.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_change_history.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_client.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_exceptions.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_geo.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_insights.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_keywords.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_models_base.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_product_pages.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_query.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_recommendations.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_reports.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_resource_base.py +0 -0
- {asa_api_client-0.3.0 → asa_api_client-0.4.0}/tests/unit/v1/test_suggestions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: asa-api-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A modern Python client for the Apple Search Ads API with full type safety and async support
|
|
5
5
|
Project-URL: Homepage, https://github.com/SamPetherbridge/asa-api-client
|
|
6
6
|
Project-URL: Documentation, https://asa-api-client.peth.au
|
|
@@ -12,7 +12,7 @@ from rich.console import Console
|
|
|
12
12
|
from rich.progress import BarColumn, Progress, TaskID, TextColumn
|
|
13
13
|
from xlsxwriter.exceptions import XlsxWriterException
|
|
14
14
|
|
|
15
|
-
from asa_api_client.cli import dates, fetch, metrics, v1_smoke
|
|
15
|
+
from asa_api_client.cli import dates, fetch, metrics, popularity, v1_smoke
|
|
16
16
|
from asa_api_client.cli.v1_adapter import V1FetchAdapter
|
|
17
17
|
from asa_api_client.cli.workbook import SummaryData, write_workbook
|
|
18
18
|
from asa_api_client.client import AppleSearchAdsClient
|
|
@@ -210,6 +210,14 @@ def analyze(
|
|
|
210
210
|
wasted=metrics.wasted_spend(analysis["keywords"]),
|
|
211
211
|
)
|
|
212
212
|
|
|
213
|
+
# The popularity sheet is enrichment via the v1 insights API; it
|
|
214
|
+
# must never break (or fail) an otherwise successful analyze run.
|
|
215
|
+
try:
|
|
216
|
+
popularity_frame, popularity_notes = popularity.build_popularity(result)
|
|
217
|
+
except Exception as exc: # nothing from this sheet may abort the workbook
|
|
218
|
+
popularity_frame, popularity_notes = pd.DataFrame(), []
|
|
219
|
+
_err.print(f"[yellow]Warning:[/yellow] Search Popularity sheet skipped: {exc}")
|
|
220
|
+
|
|
213
221
|
out_path = output or Path(f"asa-analysis-{file_scope}-{today:%Y-%m-%d}.xlsx")
|
|
214
222
|
notes = {key: lv.notes for key, lv in result.levels.items()}
|
|
215
223
|
try:
|
|
@@ -220,6 +228,8 @@ def analyze(
|
|
|
220
228
|
daily=daily_frames,
|
|
221
229
|
notes=notes,
|
|
222
230
|
currency_format=fmt,
|
|
231
|
+
popularity=popularity_frame,
|
|
232
|
+
popularity_notes=popularity_notes,
|
|
223
233
|
)
|
|
224
234
|
except (OSError, XlsxWriterException) as exc:
|
|
225
235
|
raise _fail(f"Could not write {out_path}: {exc}") from exc
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Search Popularity enrichment for the analyze command.
|
|
2
|
+
|
|
3
|
+
Matches the organization's keywords and search terms against Apple's
|
|
4
|
+
search-term-popularity insights (Apple Ads Platform API v1) to show how
|
|
5
|
+
much App Store search volume each term actually carries.
|
|
6
|
+
|
|
7
|
+
Live-API behavior this module works around (documented in
|
|
8
|
+
:mod:`asa_api_client.v1.resources.insights`):
|
|
9
|
+
|
|
10
|
+
- Any ``filters`` in the request make the API return zero rows, so the
|
|
11
|
+
query is always unfiltered and matching happens client-side.
|
|
12
|
+
- Weekly windows must start on a Sunday, so the query targets the most
|
|
13
|
+
recent complete Sun-Sat week.
|
|
14
|
+
- Data is published with roughly a week's lag, so an empty latest week
|
|
15
|
+
falls back exactly one week and retries once.
|
|
16
|
+
|
|
17
|
+
Failures never propagate: any API or configuration problem degrades to
|
|
18
|
+
an empty frame plus an explanatory note.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
from datetime import UTC, date, datetime, timedelta
|
|
24
|
+
|
|
25
|
+
import pandas as pd
|
|
26
|
+
|
|
27
|
+
from asa_api_client.cli import fetch
|
|
28
|
+
from asa_api_client.exceptions import AppleSearchAdsError
|
|
29
|
+
from asa_api_client.v1.client import AppleAdsClient
|
|
30
|
+
from asa_api_client.v1.models.insights import (
|
|
31
|
+
SearchTermPopularityGranularity,
|
|
32
|
+
SearchTermPopularityQueryRequest,
|
|
33
|
+
SearchTermPopularityRow,
|
|
34
|
+
SearchTermPopularityTimeRange,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
POPULARITY_COLUMNS: list[str] = [
|
|
38
|
+
"search_term",
|
|
39
|
+
"source",
|
|
40
|
+
"country_or_region",
|
|
41
|
+
"genre",
|
|
42
|
+
"rank_in_genre",
|
|
43
|
+
"search_popularity_1_to_100",
|
|
44
|
+
"search_popularity_1_to_5",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
_FIELDS = (
|
|
48
|
+
"rankInGenre",
|
|
49
|
+
"searchPopularityInGenre",
|
|
50
|
+
"searchPopularity1to100",
|
|
51
|
+
"searchPopularity1to5",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
_NO_MATCH_NOTE = (
|
|
55
|
+
"No account keywords or search terms meet Apple's ≥500 searches/week popularity floor."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _SkipPopularity(Exception):
|
|
60
|
+
"""The sheet cannot be built; the message is the explanatory note."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def latest_complete_week(today: date) -> tuple[date, date]:
|
|
64
|
+
"""Find the most recent complete Sun-Sat week ending before today.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
today: The current date.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The inclusive ``(sunday, saturday)`` pair of the latest Sun-Sat
|
|
71
|
+
week whose Saturday is at least one day in the past.
|
|
72
|
+
"""
|
|
73
|
+
reference = today - timedelta(days=1)
|
|
74
|
+
saturday = reference - timedelta(days=(reference.weekday() - 5) % 7)
|
|
75
|
+
return saturday - timedelta(days=6), saturday
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _level_terms(result: fetch.FetchResult, level: str, column: str) -> set[str]:
|
|
79
|
+
"""Collect lowercased distinct term texts from one level frame.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
result: The fetched analyze data.
|
|
83
|
+
level: The level key (e.g. ``"keywords"``).
|
|
84
|
+
column: The term column on that level's daily frame.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
The distinct non-empty terms, lowercased. Empty when the level,
|
|
88
|
+
its frame, or the column is missing.
|
|
89
|
+
"""
|
|
90
|
+
data = result.levels.get(level)
|
|
91
|
+
if data is None or data.daily.empty or column not in data.daily.columns:
|
|
92
|
+
return set()
|
|
93
|
+
return {str(value).lower() for value in data.daily[column].dropna() if str(value)}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _resolve_account(client: AppleAdsClient) -> str | None:
|
|
97
|
+
"""Ensure the client has an ad account, auto-selecting from ACLs.
|
|
98
|
+
|
|
99
|
+
Prefers the ACL ad account whose ID equals ``ASA_ORG_ID`` (orgs and
|
|
100
|
+
their primary ad accounts commonly share an ID); otherwise a single
|
|
101
|
+
accessible account is used.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
client: The v1 API client.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
A note describing the auto-selection, or None when the client
|
|
108
|
+
already had an account configured.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
_SkipPopularity: When no account can be selected unambiguously.
|
|
112
|
+
"""
|
|
113
|
+
if client.ad_account_id is not None:
|
|
114
|
+
return None
|
|
115
|
+
accounts = [
|
|
116
|
+
acl.ad_account
|
|
117
|
+
for acl in client.acls.list()
|
|
118
|
+
if acl.ad_account is not None and acl.ad_account.id is not None
|
|
119
|
+
]
|
|
120
|
+
org_id = os.environ.get("ASA_ORG_ID")
|
|
121
|
+
selected = next((a for a in accounts if str(a.id) == org_id), None) if org_id else None
|
|
122
|
+
if selected is None and len(accounts) == 1:
|
|
123
|
+
selected = accounts[0]
|
|
124
|
+
if selected is None:
|
|
125
|
+
raise _SkipPopularity(
|
|
126
|
+
"Search Popularity skipped: ASA_AD_ACCOUNT_ID is not set and the "
|
|
127
|
+
f"ad account could not be auto-selected from {len(accounts)} ACL entries."
|
|
128
|
+
)
|
|
129
|
+
client.ad_account_id = str(selected.id)
|
|
130
|
+
return f"Ad account {selected.id} ({selected.name}) auto-selected for popularity data."
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _week_rows(
|
|
134
|
+
client: AppleAdsClient, sunday: date, saturday: date
|
|
135
|
+
) -> list[SearchTermPopularityRow]:
|
|
136
|
+
"""Query one Sun-Sat week of popularity data, unfiltered.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
client: The v1 API client.
|
|
140
|
+
sunday: The week's start (must be a Sunday).
|
|
141
|
+
saturday: The week's end.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Every popularity row for the week across all storefronts.
|
|
145
|
+
"""
|
|
146
|
+
request = SearchTermPopularityQueryRequest(
|
|
147
|
+
fields=list(_FIELDS),
|
|
148
|
+
time_range=SearchTermPopularityTimeRange(
|
|
149
|
+
start=sunday,
|
|
150
|
+
end=saturday,
|
|
151
|
+
granularity=SearchTermPopularityGranularity.WEEKLY_SUN_SAT,
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
return list(client.insights.query_search_term_popularity(request))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _match(
|
|
158
|
+
rows: list[SearchTermPopularityRow], keyword_terms: set[str], search_terms: set[str]
|
|
159
|
+
) -> pd.DataFrame:
|
|
160
|
+
"""Keep the popularity rows whose term the org bids on or matched.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
rows: All popularity rows for the queried week.
|
|
164
|
+
keyword_terms: Lowercased keyword texts from the keywords level.
|
|
165
|
+
search_terms: Lowercased texts from the search-terms level.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
The matched rows with :data:`POPULARITY_COLUMNS`, sorted by
|
|
169
|
+
overall popularity (desc) then in-genre rank (asc).
|
|
170
|
+
"""
|
|
171
|
+
records: list[dict[str, object]] = []
|
|
172
|
+
for row in rows:
|
|
173
|
+
term = (row.search_term or "").lower()
|
|
174
|
+
in_keywords = term in keyword_terms
|
|
175
|
+
in_search_terms = term in search_terms
|
|
176
|
+
if not term or not (in_keywords or in_search_terms):
|
|
177
|
+
continue
|
|
178
|
+
if in_keywords and in_search_terms:
|
|
179
|
+
source = "Both"
|
|
180
|
+
else:
|
|
181
|
+
source = "Keywords" if in_keywords else "Search Terms"
|
|
182
|
+
records.append(
|
|
183
|
+
{
|
|
184
|
+
"search_term": row.search_term,
|
|
185
|
+
"source": source,
|
|
186
|
+
"country_or_region": row.country_or_region,
|
|
187
|
+
"genre": row.genre,
|
|
188
|
+
"rank_in_genre": row.rank_in_genre,
|
|
189
|
+
"search_popularity_1_to_100": row.search_popularity_1_to_100,
|
|
190
|
+
"search_popularity_1_to_5": row.search_popularity_1_to_5,
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
frame = pd.DataFrame(records, columns=POPULARITY_COLUMNS)
|
|
194
|
+
if frame.empty:
|
|
195
|
+
return frame
|
|
196
|
+
return frame.sort_values(
|
|
197
|
+
by=["search_popularity_1_to_100", "rank_in_genre"],
|
|
198
|
+
ascending=[False, True],
|
|
199
|
+
na_position="last",
|
|
200
|
+
kind="mergesort",
|
|
201
|
+
).reset_index(drop=True)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def build_popularity(
|
|
205
|
+
result: fetch.FetchResult,
|
|
206
|
+
*,
|
|
207
|
+
client_factory: Callable[[], AppleAdsClient] | None = None,
|
|
208
|
+
today: date | None = None,
|
|
209
|
+
) -> tuple[pd.DataFrame, list[str]]:
|
|
210
|
+
"""Build the Search Popularity frame for an analyze run.
|
|
211
|
+
|
|
212
|
+
Collects the org's keyword and search-term texts from ``result``,
|
|
213
|
+
queries one week of search-term popularity via the v1 API (falling
|
|
214
|
+
back one week when the latest week has no data yet), and keeps the
|
|
215
|
+
rows matching the org's terms across all storefronts.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
result: The fetched analyze data (term sources).
|
|
219
|
+
client_factory: Zero-argument v1 client factory; defaults to
|
|
220
|
+
:meth:`AppleAdsClient.from_env`. The client is closed before
|
|
221
|
+
returning.
|
|
222
|
+
today: The current date; defaults to today (UTC).
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Tuple of (popularity frame with :data:`POPULARITY_COLUMNS`,
|
|
226
|
+
notes for the sheet). On any API or configuration failure the
|
|
227
|
+
frame is empty and the notes explain why — this function never
|
|
228
|
+
raises for those errors.
|
|
229
|
+
"""
|
|
230
|
+
keyword_terms = _level_terms(result, "keywords", "keyword")
|
|
231
|
+
search_terms = _level_terms(result, "search_terms", "search_term_text")
|
|
232
|
+
if not (keyword_terms or search_terms):
|
|
233
|
+
return pd.DataFrame(columns=POPULARITY_COLUMNS), [
|
|
234
|
+
"No keywords or search terms in scope; popularity lookup skipped."
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
factory = client_factory or AppleAdsClient.from_env
|
|
238
|
+
try:
|
|
239
|
+
with factory() as client:
|
|
240
|
+
account_note = _resolve_account(client)
|
|
241
|
+
sunday, saturday = latest_complete_week(today or datetime.now(tz=UTC).date())
|
|
242
|
+
rows = _week_rows(client, sunday, saturday)
|
|
243
|
+
if not rows:
|
|
244
|
+
sunday -= timedelta(days=7)
|
|
245
|
+
saturday -= timedelta(days=7)
|
|
246
|
+
rows = _week_rows(client, sunday, saturday)
|
|
247
|
+
except _SkipPopularity as exc:
|
|
248
|
+
return pd.DataFrame(columns=POPULARITY_COLUMNS), [str(exc)]
|
|
249
|
+
except AppleSearchAdsError as exc:
|
|
250
|
+
return pd.DataFrame(columns=POPULARITY_COLUMNS), [f"Search Popularity unavailable: {exc}"]
|
|
251
|
+
|
|
252
|
+
notes = [f"Popularity week: {sunday} – {saturday}"] # noqa: RUF001
|
|
253
|
+
if account_note is not None:
|
|
254
|
+
notes.append(account_note)
|
|
255
|
+
frame = _match(rows, keyword_terms, search_terms)
|
|
256
|
+
if frame.empty:
|
|
257
|
+
notes.append(_NO_MATCH_NOTE)
|
|
258
|
+
return frame, notes
|
|
@@ -90,6 +90,17 @@ ANALYSIS_COLUMNS: dict[str, list[tuple[str, str, str]]] = {
|
|
|
90
90
|
],
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
POPULARITY_SHEET = "Search Popularity"
|
|
94
|
+
_POPULARITY_SPEC: list[tuple[str, str, str]] = [
|
|
95
|
+
("search_term", "Search Term", "text"),
|
|
96
|
+
("source", "Source", "text"),
|
|
97
|
+
("country_or_region", "Country/Region", "text"),
|
|
98
|
+
("genre", "Genre", "text"),
|
|
99
|
+
("rank_in_genre", "Rank in Genre", "int"),
|
|
100
|
+
("search_popularity_1_to_100", "Popularity (1-100)", "int"),
|
|
101
|
+
("search_popularity_1_to_5", "Popularity (1-5)", "int"),
|
|
102
|
+
]
|
|
103
|
+
|
|
93
104
|
_COLUMN_WIDTHS = {"text": 28, "id": 12, "int": 12, "currency": 12, "percent": 9}
|
|
94
105
|
_TAB_GREY = "#9CA3AF"
|
|
95
106
|
|
|
@@ -406,6 +417,8 @@ def write_workbook(
|
|
|
406
417
|
daily: dict[str, pd.DataFrame],
|
|
407
418
|
notes: dict[str, list[str]],
|
|
408
419
|
currency_format: str = "$#,##0.00",
|
|
420
|
+
popularity: pd.DataFrame | None = None,
|
|
421
|
+
popularity_notes: list[str] | None = None,
|
|
409
422
|
) -> None:
|
|
410
423
|
"""Write the full analysis workbook to ``path``.
|
|
411
424
|
|
|
@@ -416,6 +429,10 @@ def write_workbook(
|
|
|
416
429
|
daily: Normalized daily frames keyed by level key.
|
|
417
430
|
notes: Per-level notes rendered above the daily tables.
|
|
418
431
|
currency_format: Excel number format for money cells.
|
|
432
|
+
popularity: Optional Search Popularity frame; the sheet is
|
|
433
|
+
written when the frame has rows or ``popularity_notes`` has
|
|
434
|
+
entries, and omitted entirely when ``popularity`` is None.
|
|
435
|
+
popularity_notes: Notes rendered above the popularity table.
|
|
419
436
|
"""
|
|
420
437
|
book = xlsxwriter.Workbook(str(path), {"nan_inf_to_errors": True})
|
|
421
438
|
try:
|
|
@@ -431,6 +448,13 @@ def write_workbook(
|
|
|
431
448
|
fmts,
|
|
432
449
|
cpa_benchmark,
|
|
433
450
|
)
|
|
451
|
+
if popularity is not None and (not popularity.empty or popularity_notes):
|
|
452
|
+
ws = book.add_worksheet(POPULARITY_SHEET)
|
|
453
|
+
start = 0
|
|
454
|
+
for note in popularity_notes or []:
|
|
455
|
+
ws.write_string(start, 0, note, fmts["note"])
|
|
456
|
+
start += 1
|
|
457
|
+
_write_table(ws, popularity, _POPULARITY_SPEC, fmts, start_row=start)
|
|
434
458
|
for key, label in LEVELS:
|
|
435
459
|
ws = book.add_worksheet(f"Daily · {label}")
|
|
436
460
|
ws.set_tab_color(_TAB_GREY)
|
|
@@ -4,6 +4,7 @@ This module provides the AppleSearchAdsClient class, which is the
|
|
|
4
4
|
primary interface for interacting with the Apple Search Ads API.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import warnings
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from types import TracebackType
|
|
9
10
|
from typing import Self
|
|
@@ -29,9 +30,15 @@ DEFAULT_BASE_URL = "https://api.searchads.apple.com/api/v5"
|
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
class AppleSearchAdsClient:
|
|
32
|
-
"""Client for
|
|
33
|
+
"""Client for the legacy Apple Search Ads Campaign Management API v5.
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
.. deprecated:: 0.4.0
|
|
36
|
+
Apple sunsets the Campaign Management API v5 on **2027-01-26**.
|
|
37
|
+
Migrate to :class:`~asa_api_client.AppleAdsClient` (Platform API
|
|
38
|
+
v1) — it uses the same credentials. Constructing this client
|
|
39
|
+
emits a ``DeprecationWarning``.
|
|
40
|
+
|
|
41
|
+
This is the v5 entry point for the library. It provides access
|
|
35
42
|
to all API resources through a structured, resource-based interface.
|
|
36
43
|
|
|
37
44
|
The client supports both synchronous and asynchronous operations.
|
|
@@ -125,7 +132,19 @@ class AppleSearchAdsClient:
|
|
|
125
132
|
|
|
126
133
|
Raises:
|
|
127
134
|
ConfigurationError: If credentials are invalid or missing.
|
|
135
|
+
|
|
136
|
+
Warns:
|
|
137
|
+
DeprecationWarning: The Campaign Management API v5 sunsets
|
|
138
|
+
on 2027-01-26; migrate to ``AppleAdsClient``.
|
|
128
139
|
"""
|
|
140
|
+
warnings.warn(
|
|
141
|
+
"AppleSearchAdsClient targets the Campaign Management API v5, "
|
|
142
|
+
"which Apple sunsets on 2027-01-26. Migrate to AppleAdsClient "
|
|
143
|
+
"(Apple Ads Platform API v1) — same credentials; see "
|
|
144
|
+
"https://asa-api-client.peth.au/guide/platform-api-v1/",
|
|
145
|
+
DeprecationWarning,
|
|
146
|
+
stacklevel=2,
|
|
147
|
+
)
|
|
129
148
|
self.org_id = org_id
|
|
130
149
|
self._base_url = base_url.rstrip("/")
|
|
131
150
|
self._timeout = timeout
|
|
@@ -14,7 +14,17 @@ from enum import StrEnum
|
|
|
14
14
|
|
|
15
15
|
from pydantic import Field
|
|
16
16
|
|
|
17
|
-
from asa_api_client.v1.models.base import
|
|
17
|
+
from asa_api_client.v1.models.base import V1Model
|
|
18
|
+
from asa_api_client.v1.models.shared import (
|
|
19
|
+
BidStrategy,
|
|
20
|
+
TargetingData,
|
|
21
|
+
)
|
|
22
|
+
from asa_api_client.v1.models.shared import (
|
|
23
|
+
BidStrategyGoal as BidStrategyGoal,
|
|
24
|
+
)
|
|
25
|
+
from asa_api_client.v1.models.shared import (
|
|
26
|
+
BidStrategyType as BidStrategyType,
|
|
27
|
+
)
|
|
18
28
|
|
|
19
29
|
|
|
20
30
|
class PricingModel(StrEnum):
|
|
@@ -90,44 +100,6 @@ class AdGroupSystemLimitedStatusReason(StrEnum):
|
|
|
90
100
|
ADS_LIMITED = "ADS_LIMITED"
|
|
91
101
|
|
|
92
102
|
|
|
93
|
-
class BidStrategyType(StrEnum):
|
|
94
|
-
"""Bidding strategy applied to the ad group's auctions."""
|
|
95
|
-
|
|
96
|
-
MANUAL_CPT = "MANUAL_CPT"
|
|
97
|
-
MANUAL_CPM = "MANUAL_CPM"
|
|
98
|
-
MAX_CONVERSIONS = "MAX_CONVERSIONS"
|
|
99
|
-
MAX_ENGAGEMENTS = "MAX_ENGAGEMENTS"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
class BidStrategyGoal(StrEnum):
|
|
103
|
-
"""Optimization goal paired with a bid strategy type.
|
|
104
|
-
|
|
105
|
-
Documented pairings: ``MANUAL_CPT`` ↔ ``TAP``, ``MANUAL_CPM`` ↔
|
|
106
|
-
``IMPRESSION``, ``MAX_CONVERSIONS`` ↔ ``INSTALL``,
|
|
107
|
-
``MAX_ENGAGEMENTS`` ↔ ``TAP``.
|
|
108
|
-
"""
|
|
109
|
-
|
|
110
|
-
IMPRESSION = "IMPRESSION"
|
|
111
|
-
INSTALL = "INSTALL"
|
|
112
|
-
TAP = "TAP"
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
class TargetingData(V1Model):
|
|
116
|
-
"""Include/exclude value lists for one targeting dimension.
|
|
117
|
-
|
|
118
|
-
All values are strings, even numeric IDs and ages (e.g. ``"18"``,
|
|
119
|
-
``"123456789"``). Only ``appCategory`` and ``appDownloader``
|
|
120
|
-
support ``exclude``; other dimensions are include-only.
|
|
121
|
-
|
|
122
|
-
Attributes:
|
|
123
|
-
include: Values to restrict delivery to.
|
|
124
|
-
exclude: Values to block delivery to.
|
|
125
|
-
"""
|
|
126
|
-
|
|
127
|
-
include: list[str] | None = None
|
|
128
|
-
exclude: list[str] | None = None
|
|
129
|
-
|
|
130
|
-
|
|
131
103
|
class AdGroupTargeting(V1Model):
|
|
132
104
|
"""Targeting dimensions for an ad group.
|
|
133
105
|
|
|
@@ -168,25 +140,6 @@ class AdGroupTargeting(V1Model):
|
|
|
168
140
|
location_group: TargetingData | None = Field(default=None, alias="locationGroup")
|
|
169
141
|
|
|
170
142
|
|
|
171
|
-
class BidStrategy(V1Model):
|
|
172
|
-
"""Bid strategy for an ad group.
|
|
173
|
-
|
|
174
|
-
``bid_strategy_type`` and ``bid_strategy_goal`` must always be
|
|
175
|
-
sent together as a documented pairing; omitting one or mismatching
|
|
176
|
-
them returns an error.
|
|
177
|
-
|
|
178
|
-
Attributes:
|
|
179
|
-
bid_strategy_type: The bidding strategy type.
|
|
180
|
-
bid_strategy_goal: The optimization goal matching the type.
|
|
181
|
-
bid: For ``MANUAL_CPT`` the max amount paid per tap; for
|
|
182
|
-
automated strategies an optional per-auction ceiling.
|
|
183
|
-
"""
|
|
184
|
-
|
|
185
|
-
bid_strategy_type: BidStrategyType | None = Field(default=None, alias="bidStrategyType")
|
|
186
|
-
bid_strategy_goal: BidStrategyGoal | None = Field(default=None, alias="bidStrategyGoal")
|
|
187
|
-
bid: Money | None = None
|
|
188
|
-
|
|
189
|
-
|
|
190
143
|
class AdGroup(V1Model):
|
|
191
144
|
"""An ad group as returned by the API.
|
|
192
145
|
|
|
@@ -13,6 +13,12 @@ from typing import Annotated
|
|
|
13
13
|
from pydantic import Field, field_serializer
|
|
14
14
|
|
|
15
15
|
from asa_api_client.v1.models.base import Money, V1Model
|
|
16
|
+
from asa_api_client.v1.models.shared import (
|
|
17
|
+
InvoiceDetail,
|
|
18
|
+
)
|
|
19
|
+
from asa_api_client.v1.models.shared import (
|
|
20
|
+
SharedBudgetAssignment as SharedBudgetAssignment,
|
|
21
|
+
)
|
|
16
22
|
|
|
17
23
|
# The API requires yyyy-MM-dd'T'HH:mm:ss.SSS timestamps in UTC with no
|
|
18
24
|
# timezone suffix.
|
|
@@ -91,32 +97,6 @@ class BudgetSystemStatusReason(StrEnum):
|
|
|
91
97
|
SCHEDULE_PENDING = "SCHEDULE_PENDING"
|
|
92
98
|
|
|
93
99
|
|
|
94
|
-
class InvoiceDetail(V1Model):
|
|
95
|
-
"""Billing details for Line of Credit (LOC) accounts.
|
|
96
|
-
|
|
97
|
-
Embedded in budget order (and campaign) objects. On budget order
|
|
98
|
-
creation the API requires ``name``, ``primary_buyer_name``,
|
|
99
|
-
``primary_buyer_email``, and ``billing_email``; all fields are
|
|
100
|
-
modeled optional because responses may omit any of them.
|
|
101
|
-
|
|
102
|
-
Attributes:
|
|
103
|
-
name: Billing contact name (required on budget-order create).
|
|
104
|
-
client_name: Identifies the advertiser or product. Nullable.
|
|
105
|
-
primary_buyer_name: Primary buyer's name (required on create).
|
|
106
|
-
primary_buyer_email: Primary buyer's email address (required
|
|
107
|
-
on create).
|
|
108
|
-
order_number: Purchase order (PO) number. Nullable.
|
|
109
|
-
billing_email: Billing email address (required on create).
|
|
110
|
-
"""
|
|
111
|
-
|
|
112
|
-
name: str | None = None
|
|
113
|
-
client_name: str | None = Field(default=None, alias="clientName")
|
|
114
|
-
primary_buyer_name: str | None = Field(default=None, alias="primaryBuyerName")
|
|
115
|
-
primary_buyer_email: str | None = Field(default=None, alias="primaryBuyerEmail")
|
|
116
|
-
order_number: str | None = Field(default=None, alias="orderNumber")
|
|
117
|
-
billing_email: str | None = Field(default=None, alias="billingEmail")
|
|
118
|
-
|
|
119
|
-
|
|
120
100
|
class InvoiceDetailUpdate(V1Model):
|
|
121
101
|
"""Partial update of a budget order's invoice details.
|
|
122
102
|
|
|
@@ -244,19 +224,6 @@ class SharedBudgetUpdate(V1Model):
|
|
|
244
224
|
return _format_timestamp(value)
|
|
245
225
|
|
|
246
226
|
|
|
247
|
-
class SharedBudgetAssignment(V1Model):
|
|
248
|
-
"""Read-only link between a campaign and a budget order.
|
|
249
|
-
|
|
250
|
-
Returned only inside a Campaign's ``sharedBudgets`` array; NOT
|
|
251
|
-
independently addressable via any endpoint.
|
|
252
|
-
|
|
253
|
-
Attributes:
|
|
254
|
-
budget_id: The budget order the campaign is assigned to.
|
|
255
|
-
"""
|
|
256
|
-
|
|
257
|
-
budget_id: int | None = Field(default=None, alias="budgetId")
|
|
258
|
-
|
|
259
|
-
|
|
260
227
|
class SharedBudgetAssignmentCreate(V1Model):
|
|
261
228
|
"""Assignment entry embedded in a CampaignCreate ``sharedBudgets`` array.
|
|
262
229
|
|
|
@@ -13,6 +13,18 @@ from enum import StrEnum
|
|
|
13
13
|
from pydantic import Field, field_serializer
|
|
14
14
|
|
|
15
15
|
from asa_api_client.v1.models.base import Money, V1Model
|
|
16
|
+
from asa_api_client.v1.models.shared import (
|
|
17
|
+
BidStrategy,
|
|
18
|
+
InvoiceDetail,
|
|
19
|
+
SharedBudgetAssignment,
|
|
20
|
+
TargetingData,
|
|
21
|
+
)
|
|
22
|
+
from asa_api_client.v1.models.shared import (
|
|
23
|
+
BidStrategyGoal as BidStrategyGoal,
|
|
24
|
+
)
|
|
25
|
+
from asa_api_client.v1.models.shared import (
|
|
26
|
+
BidStrategyType as BidStrategyType,
|
|
27
|
+
)
|
|
16
28
|
|
|
17
29
|
|
|
18
30
|
class BillingEvent(StrEnum):
|
|
@@ -161,23 +173,6 @@ class SupplyPlacement(StrEnum):
|
|
|
161
173
|
MAPS_SEARCH_HOME = "MAPS_SEARCH_HOME"
|
|
162
174
|
|
|
163
175
|
|
|
164
|
-
class BidStrategyType(StrEnum):
|
|
165
|
-
"""How bids are set in auctions."""
|
|
166
|
-
|
|
167
|
-
MANUAL_CPT = "MANUAL_CPT"
|
|
168
|
-
MANUAL_CPM = "MANUAL_CPM"
|
|
169
|
-
MAX_CONVERSIONS = "MAX_CONVERSIONS"
|
|
170
|
-
MAX_ENGAGEMENTS = "MAX_ENGAGEMENTS"
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
class BidStrategyGoal(StrEnum):
|
|
174
|
-
"""The outcome a bid strategy optimizes for."""
|
|
175
|
-
|
|
176
|
-
IMPRESSION = "IMPRESSION"
|
|
177
|
-
INSTALL = "INSTALL"
|
|
178
|
-
TAP = "TAP"
|
|
179
|
-
|
|
180
|
-
|
|
181
176
|
class RegulationType(StrEnum):
|
|
182
177
|
"""Regulatory disclosure types."""
|
|
183
178
|
|
|
@@ -198,21 +193,6 @@ class RegulationResponseValue(StrEnum):
|
|
|
198
193
|
NOT_ANSWERED = "NOT_ANSWERED"
|
|
199
194
|
|
|
200
195
|
|
|
201
|
-
class TargetingData(V1Model):
|
|
202
|
-
"""A targeting dimension's include/exclude value sets.
|
|
203
|
-
|
|
204
|
-
Campaign-level targeting is include-only: ``exclude`` is unsupported
|
|
205
|
-
for every campaign targeting dimension (unlike ad-group targeting).
|
|
206
|
-
|
|
207
|
-
Attributes:
|
|
208
|
-
include: Values to target.
|
|
209
|
-
exclude: Values to exclude (ad-group targeting only).
|
|
210
|
-
"""
|
|
211
|
-
|
|
212
|
-
include: list[str] | None = None
|
|
213
|
-
exclude: list[str] | None = None
|
|
214
|
-
|
|
215
|
-
|
|
216
196
|
class CampaignTargeting(V1Model):
|
|
217
197
|
"""Campaign-level targeting: supply source, placements, markets.
|
|
218
198
|
|
|
@@ -237,55 +217,6 @@ class DailyBudget(V1Model):
|
|
|
237
217
|
value: Money | None = None
|
|
238
218
|
|
|
239
219
|
|
|
240
|
-
class BidStrategy(V1Model):
|
|
241
|
-
"""How a campaign competes in auctions.
|
|
242
|
-
|
|
243
|
-
``bid_strategy_type`` and ``bid_strategy_goal`` must both be
|
|
244
|
-
supplied together and match an allowed pairing on create and update.
|
|
245
|
-
|
|
246
|
-
Attributes:
|
|
247
|
-
bid_strategy_type: The bidding type (manual or automated).
|
|
248
|
-
bid_strategy_goal: The outcome the strategy optimizes for.
|
|
249
|
-
bid: The bid amount for manual strategies.
|
|
250
|
-
"""
|
|
251
|
-
|
|
252
|
-
bid_strategy_type: BidStrategyType | None = Field(default=None, alias="bidStrategyType")
|
|
253
|
-
bid_strategy_goal: BidStrategyGoal | None = Field(default=None, alias="bidStrategyGoal")
|
|
254
|
-
bid: Money | None = None
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
class SharedBudgetAssignment(V1Model):
|
|
258
|
-
"""One budget-order assignment in a campaign's ``sharedBudgets``.
|
|
259
|
-
|
|
260
|
-
Attributes:
|
|
261
|
-
budget_id: The assigned budget order's ID.
|
|
262
|
-
"""
|
|
263
|
-
|
|
264
|
-
budget_id: int | None = Field(default=None, alias="budgetId")
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
class InvoiceDetail(V1Model):
|
|
268
|
-
"""Invoice billing details for Line of Credit (LOC) accounts.
|
|
269
|
-
|
|
270
|
-
``primary_buyer_name``, ``primary_buyer_email``, and
|
|
271
|
-
``billing_email`` are required by the API when creating a campaign
|
|
272
|
-
on an LOC account.
|
|
273
|
-
|
|
274
|
-
Attributes:
|
|
275
|
-
client_name: Identifies the advertiser or product.
|
|
276
|
-
primary_buyer_name: The primary buyer's name.
|
|
277
|
-
primary_buyer_email: The primary buyer's email address.
|
|
278
|
-
order_number: Purchase order number.
|
|
279
|
-
billing_email: The billing email address.
|
|
280
|
-
"""
|
|
281
|
-
|
|
282
|
-
client_name: str | None = Field(default=None, alias="clientName")
|
|
283
|
-
primary_buyer_name: str | None = Field(default=None, alias="primaryBuyerName")
|
|
284
|
-
primary_buyer_email: str | None = Field(default=None, alias="primaryBuyerEmail")
|
|
285
|
-
order_number: str | None = Field(default=None, alias="orderNumber")
|
|
286
|
-
billing_email: str | None = Field(default=None, alias="billingEmail")
|
|
287
|
-
|
|
288
|
-
|
|
289
220
|
class RegulationResponse(V1Model):
|
|
290
221
|
"""A regulatory consent acknowledgment on a campaign.
|
|
291
222
|
|