asa-api-cli 0.2.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_cli-0.2.0 → asa_api_cli-0.4.0}/.github/dependabot.yml +1 -1
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/.github/workflows/ci.yml +2 -2
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/.github/workflows/publish.yml +1 -1
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/PKG-INFO +21 -3
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/README.md +16 -0
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/__init__.py +6 -1
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/ad_groups.py +2 -4
- asa_api_cli-0.4.0/asa_api_cli/ads.py +345 -0
- asa_api_cli-0.4.0/asa_api_cli/apps.py +81 -0
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/auth.py +77 -5
- asa_api_cli-0.4.0/asa_api_cli/budget_orders.py +114 -0
- asa_api_cli-0.4.0/asa_api_cli/countries.py +84 -0
- asa_api_cli-0.4.0/asa_api_cli/geo.py +92 -0
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/impression_share.py +12 -3
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/keywords.py +4 -4
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/main.py +57 -2
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/optimize.py +500 -24
- asa_api_cli-0.4.0/asa_api_cli/product_pages.py +155 -0
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/reports.py +6 -6
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/utils.py +11 -3
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/pyproject.toml +6 -2
- asa_api_cli-0.4.0/tests/test_cli.py +273 -0
- asa_api_cli-0.4.0/uv.lock +2514 -0
- asa_api_cli-0.2.0/tests/test_cli.py +0 -39
- asa_api_cli-0.2.0/uv.lock +0 -3129
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/.gitignore +0 -0
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/.pre-commit-config.yaml +0 -0
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/LICENSE +0 -0
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/brand.py +0 -0
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/campaigns.py +0 -0
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/languages.py +0 -0
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/translate.py +0 -0
- {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/tests/__init__.py +0 -0
|
@@ -14,7 +14,7 @@ jobs:
|
|
|
14
14
|
python-version: ["3.13"]
|
|
15
15
|
|
|
16
16
|
steps:
|
|
17
|
-
- uses: actions/checkout@
|
|
17
|
+
- uses: actions/checkout@v7
|
|
18
18
|
|
|
19
19
|
- name: Install uv
|
|
20
20
|
uses: astral-sh/setup-uv@v7
|
|
@@ -40,7 +40,7 @@ jobs:
|
|
|
40
40
|
run: uv run pytest --cov=asa_api_cli --cov-report=xml
|
|
41
41
|
|
|
42
42
|
- name: Upload coverage to Codecov
|
|
43
|
-
uses: codecov/codecov-action@
|
|
43
|
+
uses: codecov/codecov-action@v7
|
|
44
44
|
with:
|
|
45
45
|
file: ./coverage.xml
|
|
46
46
|
fail_ci_if_error: false
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: asa-api-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Command-line interface for Apple Search Ads campaign management
|
|
5
5
|
Project-URL: Homepage, https://github.com/SamPetherbridge/asa-api-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/SamPetherbridge/asa-api-cli
|
|
@@ -18,11 +18,13 @@ Classifier: Programming Language :: Python :: 3
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Typing :: Typed
|
|
20
20
|
Requires-Python: >=3.13
|
|
21
|
-
Requires-Dist: asa-api-client>=0.1
|
|
21
|
+
Requires-Dist: asa-api-client>=0.6.1
|
|
22
22
|
Requires-Dist: pydantic-ai>=0.1.0
|
|
23
23
|
Requires-Dist: pydantic-settings>=2.0.0
|
|
24
24
|
Requires-Dist: rich>=13.9.0
|
|
25
25
|
Requires-Dist: typer>=0.13.0
|
|
26
|
+
Provides-Extra: analyze
|
|
27
|
+
Requires-Dist: asa-api-client[cli]>=0.6.1; extra == 'analyze'
|
|
26
28
|
Provides-Extra: dev
|
|
27
29
|
Requires-Dist: mypy>=1.13.0; extra == 'dev'
|
|
28
30
|
Requires-Dist: pre-commit>=4.0.0; extra == 'dev'
|
|
@@ -49,12 +51,18 @@ Using [uv](https://docs.astral.sh/uv/) (recommended):
|
|
|
49
51
|
|
|
50
52
|
```bash
|
|
51
53
|
uv tool install asa-api-cli
|
|
54
|
+
|
|
55
|
+
# Include Excel workbook analysis support (pandas + xlsxwriter)
|
|
56
|
+
uv tool install "asa-api-cli[analyze]"
|
|
52
57
|
```
|
|
53
58
|
|
|
54
59
|
Using pip:
|
|
55
60
|
|
|
56
61
|
```bash
|
|
57
62
|
pip install asa-api-cli
|
|
63
|
+
|
|
64
|
+
# Or with Excel workbook analysis support
|
|
65
|
+
pip install "asa-api-cli[analyze]"
|
|
58
66
|
```
|
|
59
67
|
|
|
60
68
|
Using pipx:
|
|
@@ -131,6 +139,16 @@ asa reports keywords 123456789 --start 2024-01-01 --end 2024-01-31
|
|
|
131
139
|
asa reports campaigns --start 2024-01-01 --end 2024-01-31 --output report.csv
|
|
132
140
|
```
|
|
133
141
|
|
|
142
|
+
### Analysis Workbook & Platform API v1 Smoke Test
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Generate an Excel performance analysis workbook (requires [analyze] extra)
|
|
146
|
+
asa analyze --period 30d --output asa-30d.xlsx
|
|
147
|
+
|
|
148
|
+
# Run a read-only smoke test against the Apple Ads Platform API v1
|
|
149
|
+
asa v1-smoke
|
|
150
|
+
```
|
|
151
|
+
|
|
134
152
|
### Brand Campaigns
|
|
135
153
|
|
|
136
154
|
Create brand protection campaigns for your app:
|
|
@@ -15,12 +15,18 @@ Using [uv](https://docs.astral.sh/uv/) (recommended):
|
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
uv tool install asa-api-cli
|
|
18
|
+
|
|
19
|
+
# Include Excel workbook analysis support (pandas + xlsxwriter)
|
|
20
|
+
uv tool install "asa-api-cli[analyze]"
|
|
18
21
|
```
|
|
19
22
|
|
|
20
23
|
Using pip:
|
|
21
24
|
|
|
22
25
|
```bash
|
|
23
26
|
pip install asa-api-cli
|
|
27
|
+
|
|
28
|
+
# Or with Excel workbook analysis support
|
|
29
|
+
pip install "asa-api-cli[analyze]"
|
|
24
30
|
```
|
|
25
31
|
|
|
26
32
|
Using pipx:
|
|
@@ -97,6 +103,16 @@ asa reports keywords 123456789 --start 2024-01-01 --end 2024-01-31
|
|
|
97
103
|
asa reports campaigns --start 2024-01-01 --end 2024-01-31 --output report.csv
|
|
98
104
|
```
|
|
99
105
|
|
|
106
|
+
### Analysis Workbook & Platform API v1 Smoke Test
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Generate an Excel performance analysis workbook (requires [analyze] extra)
|
|
110
|
+
asa analyze --period 30d --output asa-30d.xlsx
|
|
111
|
+
|
|
112
|
+
# Run a read-only smoke test against the Apple Ads Platform API v1
|
|
113
|
+
asa v1-smoke
|
|
114
|
+
```
|
|
115
|
+
|
|
100
116
|
### Brand Campaigns
|
|
101
117
|
|
|
102
118
|
Create brand protection campaigns for your app:
|
|
@@ -8,8 +8,13 @@ Usage:
|
|
|
8
8
|
asa reports campaigns --start 2024-01-01 --end 2024-01-31
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
12
|
+
|
|
11
13
|
from asa_api_cli.main import app
|
|
12
14
|
|
|
13
15
|
__all__ = ["app"]
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
try:
|
|
18
|
+
__version__ = version("asa-api-cli")
|
|
19
|
+
except PackageNotFoundError: # pragma: no cover - only when running from a non-installed checkout
|
|
20
|
+
__version__ = "0.0.0"
|
|
@@ -661,7 +661,7 @@ def review_negatives(
|
|
|
661
661
|
# Step 2: Fetch all ad groups
|
|
662
662
|
with spinner("Fetching ad groups..."):
|
|
663
663
|
ad_groups_resp = client.campaigns(campaign_id).ad_groups.find(
|
|
664
|
-
Selector(
|
|
664
|
+
Selector().where("status", "==", "ENABLED")
|
|
665
665
|
)
|
|
666
666
|
ad_groups = ad_groups_resp
|
|
667
667
|
|
|
@@ -887,9 +887,7 @@ def review_skag(
|
|
|
887
887
|
|
|
888
888
|
# Step 2: Fetch all ad groups
|
|
889
889
|
with spinner("Fetching ad groups..."):
|
|
890
|
-
ad_groups = client.campaigns(campaign_id).ad_groups.find(
|
|
891
|
-
Selector(conditions=[{"field": "status", "operator": "EQUALS", "values": ["ENABLED"]}])
|
|
892
|
-
)
|
|
890
|
+
ad_groups = client.campaigns(campaign_id).ad_groups.find(Selector().where("status", "==", "ENABLED"))
|
|
893
891
|
|
|
894
892
|
if not ad_groups:
|
|
895
893
|
print_warning("No enabled ad groups found in this campaign")
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""Ad (creative) CLI commands."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from asa_api_client.exceptions import AppleSearchAdsError
|
|
7
|
+
from asa_api_client.models import AdCreate, AdStatus, AdUpdate, CreativeType
|
|
8
|
+
|
|
9
|
+
from asa_api_cli.utils import (
|
|
10
|
+
EXIT_ERROR,
|
|
11
|
+
EXIT_USAGE,
|
|
12
|
+
OutputFormat,
|
|
13
|
+
confirm_action,
|
|
14
|
+
enum_value,
|
|
15
|
+
get_client,
|
|
16
|
+
handle_api_error,
|
|
17
|
+
output_data,
|
|
18
|
+
print_error,
|
|
19
|
+
print_info,
|
|
20
|
+
print_result_panel,
|
|
21
|
+
print_success,
|
|
22
|
+
print_warning,
|
|
23
|
+
spinner,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
app = typer.Typer(help="View and manage ads (creatives) within ad groups")
|
|
27
|
+
|
|
28
|
+
AD_COLUMNS = ["id", "name", "creative_type", "status", "serving_status", "creative_id"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _validate_create(creative_type: CreativeType, product_page_id: str | None) -> None:
|
|
32
|
+
"""Validate the creative-type / product-page combination before any API call.
|
|
33
|
+
|
|
34
|
+
Raises typer.Exit(EXIT_USAGE) with a friendly message on an invalid combination.
|
|
35
|
+
"""
|
|
36
|
+
if creative_type == CreativeType.CREATIVE_SET:
|
|
37
|
+
print_error(
|
|
38
|
+
"Unsupported creative type",
|
|
39
|
+
"CREATIVE_SET ads require selecting creative set assets, which this command "
|
|
40
|
+
"does not support yet. Use CUSTOM_PRODUCT_PAGE or DEFAULT_PRODUCT_PAGE.",
|
|
41
|
+
)
|
|
42
|
+
raise typer.Exit(EXIT_USAGE)
|
|
43
|
+
if creative_type == CreativeType.CUSTOM_PRODUCT_PAGE and not product_page_id:
|
|
44
|
+
print_error(
|
|
45
|
+
"Missing product page",
|
|
46
|
+
"CUSTOM_PRODUCT_PAGE ads require --product-page <id>. Find ids with 'asa product-pages list'.",
|
|
47
|
+
)
|
|
48
|
+
raise typer.Exit(EXIT_USAGE)
|
|
49
|
+
if creative_type == CreativeType.DEFAULT_PRODUCT_PAGE and product_page_id:
|
|
50
|
+
print_error(
|
|
51
|
+
"Unexpected product page",
|
|
52
|
+
"DEFAULT_PRODUCT_PAGE ads must not specify --product-page.",
|
|
53
|
+
)
|
|
54
|
+
raise typer.Exit(EXIT_USAGE)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command("list")
|
|
58
|
+
def list_ads(
|
|
59
|
+
campaign_id: Annotated[
|
|
60
|
+
int,
|
|
61
|
+
typer.Option("--campaign", "-c", help="Campaign ID"),
|
|
62
|
+
],
|
|
63
|
+
ad_group_id: Annotated[
|
|
64
|
+
int,
|
|
65
|
+
typer.Option("--ad-group", "-a", help="Ad group ID"),
|
|
66
|
+
],
|
|
67
|
+
limit: Annotated[
|
|
68
|
+
int,
|
|
69
|
+
typer.Option("--limit", "-l", help="Maximum number of results"),
|
|
70
|
+
] = 100,
|
|
71
|
+
format: Annotated[
|
|
72
|
+
OutputFormat,
|
|
73
|
+
typer.Option("--format", "-f", help="Output format"),
|
|
74
|
+
] = OutputFormat.TABLE,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""List ads in an ad group.
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
asa ads list --campaign 123 --ad-group 456
|
|
80
|
+
asa ads list -c 123 -a 456 --format json
|
|
81
|
+
"""
|
|
82
|
+
client = get_client()
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
with client:
|
|
86
|
+
with spinner("Fetching ads..."):
|
|
87
|
+
result = client.campaigns(campaign_id).ad_groups(ad_group_id).ads.list(limit=limit)
|
|
88
|
+
|
|
89
|
+
if not result.data:
|
|
90
|
+
print_warning("No ads found")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
rows: list[dict[str, Any]] = [
|
|
94
|
+
{
|
|
95
|
+
"id": ad.id,
|
|
96
|
+
"name": ad.name,
|
|
97
|
+
"creative_type": enum_value(ad.creative_type),
|
|
98
|
+
"status": enum_value(ad.status),
|
|
99
|
+
"serving_status": enum_value(ad.serving_status),
|
|
100
|
+
"creative_id": ad.creative_id or "-",
|
|
101
|
+
}
|
|
102
|
+
for ad in result.data
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
output_data(rows, AD_COLUMNS, format, title=f"Ads in ad group {ad_group_id}")
|
|
106
|
+
except AppleSearchAdsError as e:
|
|
107
|
+
handle_api_error(e)
|
|
108
|
+
raise typer.Exit(EXIT_ERROR) from None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.command("get")
|
|
112
|
+
def get_ad(
|
|
113
|
+
ad_id: Annotated[
|
|
114
|
+
int,
|
|
115
|
+
typer.Argument(help="Ad ID"),
|
|
116
|
+
],
|
|
117
|
+
campaign_id: Annotated[
|
|
118
|
+
int,
|
|
119
|
+
typer.Option("--campaign", "-c", help="Campaign ID"),
|
|
120
|
+
],
|
|
121
|
+
ad_group_id: Annotated[
|
|
122
|
+
int,
|
|
123
|
+
typer.Option("--ad-group", "-a", help="Ad group ID"),
|
|
124
|
+
],
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Show details for a single ad.
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
asa ads get 789 --campaign 123 --ad-group 456
|
|
130
|
+
"""
|
|
131
|
+
client = get_client()
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
with client:
|
|
135
|
+
with spinner("Fetching ad..."):
|
|
136
|
+
ad = client.campaigns(campaign_id).ad_groups(ad_group_id).ads.get(ad_id)
|
|
137
|
+
|
|
138
|
+
print_result_panel(
|
|
139
|
+
f"Ad {ad.id}",
|
|
140
|
+
{
|
|
141
|
+
"Name": ad.name,
|
|
142
|
+
"Creative type": enum_value(ad.creative_type),
|
|
143
|
+
"Status": enum_value(ad.status),
|
|
144
|
+
"Serving status": enum_value(ad.serving_status),
|
|
145
|
+
"Creative ID": str(ad.creative_id) if ad.creative_id else "-",
|
|
146
|
+
"Campaign ID": str(ad.campaign_id),
|
|
147
|
+
"Ad group ID": str(ad.ad_group_id),
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
except AppleSearchAdsError as e:
|
|
151
|
+
handle_api_error(e)
|
|
152
|
+
raise typer.Exit(EXIT_ERROR) from None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@app.command("create")
|
|
156
|
+
def create_ad(
|
|
157
|
+
name: Annotated[
|
|
158
|
+
str,
|
|
159
|
+
typer.Option("--name", "-n", help="Ad name"),
|
|
160
|
+
],
|
|
161
|
+
campaign_id: Annotated[
|
|
162
|
+
int,
|
|
163
|
+
typer.Option("--campaign", "-c", help="Campaign ID"),
|
|
164
|
+
],
|
|
165
|
+
ad_group_id: Annotated[
|
|
166
|
+
int,
|
|
167
|
+
typer.Option("--ad-group", "-a", help="Ad group ID"),
|
|
168
|
+
],
|
|
169
|
+
creative_type: Annotated[
|
|
170
|
+
CreativeType,
|
|
171
|
+
typer.Option("--creative-type", "-t", help="Creative type"),
|
|
172
|
+
] = CreativeType.CUSTOM_PRODUCT_PAGE,
|
|
173
|
+
product_page_id: Annotated[
|
|
174
|
+
str | None,
|
|
175
|
+
typer.Option("--product-page", "-p", help="Custom product page ID (required for CUSTOM_PRODUCT_PAGE)"),
|
|
176
|
+
] = None,
|
|
177
|
+
status: Annotated[
|
|
178
|
+
AdStatus,
|
|
179
|
+
typer.Option("--status", "-s", help="Initial status"),
|
|
180
|
+
] = AdStatus.ENABLED,
|
|
181
|
+
dry_run: Annotated[
|
|
182
|
+
bool,
|
|
183
|
+
typer.Option("--dry-run", help="Preview the ad payload without creating it"),
|
|
184
|
+
] = False,
|
|
185
|
+
yes: Annotated[
|
|
186
|
+
bool,
|
|
187
|
+
typer.Option("--yes", "-y", help="Skip the confirmation prompt"),
|
|
188
|
+
] = False,
|
|
189
|
+
) -> None:
|
|
190
|
+
"""Create an ad in an ad group.
|
|
191
|
+
|
|
192
|
+
CUSTOM_PRODUCT_PAGE ads need a --product-page id (see 'asa product-pages list').
|
|
193
|
+
DEFAULT_PRODUCT_PAGE ads use the app's default page and take no product page.
|
|
194
|
+
|
|
195
|
+
Examples:
|
|
196
|
+
asa ads create -c 123 -a 456 -n "Holiday CPP" -p <product-page-id>
|
|
197
|
+
asa ads create -c 123 -a 456 -n "Default ad" -t DEFAULT_PRODUCT_PAGE
|
|
198
|
+
asa ads create -c 123 -a 456 -n "Preview" -p <id> --dry-run
|
|
199
|
+
"""
|
|
200
|
+
_validate_create(creative_type, product_page_id)
|
|
201
|
+
|
|
202
|
+
payload = AdCreate(
|
|
203
|
+
name=name,
|
|
204
|
+
creative_type=creative_type,
|
|
205
|
+
status=status,
|
|
206
|
+
product_page_id=product_page_id,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
preview = {
|
|
210
|
+
"Name": name,
|
|
211
|
+
"Creative type": enum_value(creative_type),
|
|
212
|
+
"Status": enum_value(status),
|
|
213
|
+
"Product page": product_page_id or "-",
|
|
214
|
+
"Campaign ID": str(campaign_id),
|
|
215
|
+
"Ad group ID": str(ad_group_id),
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if dry_run:
|
|
219
|
+
print_info("Dry run - no ad created")
|
|
220
|
+
print_result_panel("Ad payload", preview)
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
if not yes and not confirm_action(f"Create ad '{name}' in ad group {ad_group_id}?", default=True):
|
|
224
|
+
print_info("Cancelled")
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
client = get_client()
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
with client:
|
|
231
|
+
with spinner("Creating ad..."):
|
|
232
|
+
ad = client.campaigns(campaign_id).ad_groups(ad_group_id).ads.create(payload)
|
|
233
|
+
|
|
234
|
+
print_success(f"Created ad {ad.id}: {ad.name}")
|
|
235
|
+
print_result_panel(
|
|
236
|
+
f"Ad {ad.id}",
|
|
237
|
+
{
|
|
238
|
+
"Name": ad.name,
|
|
239
|
+
"Creative type": enum_value(ad.creative_type),
|
|
240
|
+
"Status": enum_value(ad.status),
|
|
241
|
+
"Serving status": enum_value(ad.serving_status),
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
except AppleSearchAdsError as e:
|
|
245
|
+
handle_api_error(e)
|
|
246
|
+
raise typer.Exit(EXIT_ERROR) from None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@app.command("update")
|
|
250
|
+
def update_ad(
|
|
251
|
+
ad_id: Annotated[
|
|
252
|
+
int,
|
|
253
|
+
typer.Argument(help="Ad ID"),
|
|
254
|
+
],
|
|
255
|
+
campaign_id: Annotated[
|
|
256
|
+
int,
|
|
257
|
+
typer.Option("--campaign", "-c", help="Campaign ID"),
|
|
258
|
+
],
|
|
259
|
+
ad_group_id: Annotated[
|
|
260
|
+
int,
|
|
261
|
+
typer.Option("--ad-group", "-a", help="Ad group ID"),
|
|
262
|
+
],
|
|
263
|
+
name: Annotated[
|
|
264
|
+
str | None,
|
|
265
|
+
typer.Option("--name", "-n", help="New ad name"),
|
|
266
|
+
] = None,
|
|
267
|
+
status: Annotated[
|
|
268
|
+
AdStatus | None,
|
|
269
|
+
typer.Option("--status", "-s", help="New status (ENABLED or PAUSED)"),
|
|
270
|
+
] = None,
|
|
271
|
+
) -> None:
|
|
272
|
+
"""Update an ad's name and/or status.
|
|
273
|
+
|
|
274
|
+
At least one of --name or --status must be provided.
|
|
275
|
+
|
|
276
|
+
Examples:
|
|
277
|
+
asa ads update 789 -c 123 -a 456 --status PAUSED
|
|
278
|
+
asa ads update 789 -c 123 -a 456 --name "Renamed ad"
|
|
279
|
+
"""
|
|
280
|
+
if name is None and status is None:
|
|
281
|
+
print_error("Nothing to update", "Provide at least one of --name or --status.")
|
|
282
|
+
raise typer.Exit(EXIT_USAGE)
|
|
283
|
+
|
|
284
|
+
payload = AdUpdate(name=name, status=status)
|
|
285
|
+
|
|
286
|
+
client = get_client()
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
with client:
|
|
290
|
+
with spinner("Updating ad..."):
|
|
291
|
+
ad = client.campaigns(campaign_id).ad_groups(ad_group_id).ads.update(ad_id, payload)
|
|
292
|
+
|
|
293
|
+
print_success(f"Updated ad {ad.id}")
|
|
294
|
+
print_result_panel(
|
|
295
|
+
f"Ad {ad.id}",
|
|
296
|
+
{
|
|
297
|
+
"Name": ad.name,
|
|
298
|
+
"Status": enum_value(ad.status),
|
|
299
|
+
"Serving status": enum_value(ad.serving_status),
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
except AppleSearchAdsError as e:
|
|
303
|
+
handle_api_error(e)
|
|
304
|
+
raise typer.Exit(EXIT_ERROR) from None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@app.command("delete")
|
|
308
|
+
def delete_ad(
|
|
309
|
+
ad_id: Annotated[
|
|
310
|
+
int,
|
|
311
|
+
typer.Argument(help="Ad ID"),
|
|
312
|
+
],
|
|
313
|
+
campaign_id: Annotated[
|
|
314
|
+
int,
|
|
315
|
+
typer.Option("--campaign", "-c", help="Campaign ID"),
|
|
316
|
+
],
|
|
317
|
+
ad_group_id: Annotated[
|
|
318
|
+
int,
|
|
319
|
+
typer.Option("--ad-group", "-a", help="Ad group ID"),
|
|
320
|
+
],
|
|
321
|
+
yes: Annotated[
|
|
322
|
+
bool,
|
|
323
|
+
typer.Option("--yes", "-y", help="Skip the confirmation prompt"),
|
|
324
|
+
] = False,
|
|
325
|
+
) -> None:
|
|
326
|
+
"""Delete an ad.
|
|
327
|
+
|
|
328
|
+
Example:
|
|
329
|
+
asa ads delete 789 -c 123 -a 456
|
|
330
|
+
"""
|
|
331
|
+
if not yes and not confirm_action(f"Delete ad {ad_id}? This cannot be undone.", default=False):
|
|
332
|
+
print_info("Cancelled")
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
client = get_client()
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
with client:
|
|
339
|
+
with spinner("Deleting ad..."):
|
|
340
|
+
client.campaigns(campaign_id).ad_groups(ad_group_id).ads.delete(ad_id)
|
|
341
|
+
|
|
342
|
+
print_success(f"Deleted ad {ad_id}")
|
|
343
|
+
except AppleSearchAdsError as e:
|
|
344
|
+
handle_api_error(e)
|
|
345
|
+
raise typer.Exit(EXIT_ERROR) from None
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""App lookup CLI commands."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from asa_api_client.exceptions import AppleSearchAdsError
|
|
7
|
+
|
|
8
|
+
from asa_api_cli.utils import (
|
|
9
|
+
EXIT_ERROR,
|
|
10
|
+
OutputFormat,
|
|
11
|
+
get_client,
|
|
12
|
+
handle_api_error,
|
|
13
|
+
output_data,
|
|
14
|
+
print_warning,
|
|
15
|
+
spinner,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(help="Search the App Store for advertisable apps")
|
|
19
|
+
|
|
20
|
+
APP_COLUMNS = ["adam_id", "app_name", "developer_name", "countries"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command("search")
|
|
24
|
+
def search_apps(
|
|
25
|
+
query: Annotated[
|
|
26
|
+
str,
|
|
27
|
+
typer.Argument(help="Search term (app name or keyword)"),
|
|
28
|
+
],
|
|
29
|
+
own: Annotated[
|
|
30
|
+
bool,
|
|
31
|
+
typer.Option("--own", help="Only return apps owned by your org"),
|
|
32
|
+
] = False,
|
|
33
|
+
limit: Annotated[
|
|
34
|
+
int,
|
|
35
|
+
typer.Option("--limit", "-l", help="Maximum number of results"),
|
|
36
|
+
] = 50,
|
|
37
|
+
format: Annotated[
|
|
38
|
+
OutputFormat,
|
|
39
|
+
typer.Option("--format", "-f", help="Output format"),
|
|
40
|
+
] = OutputFormat.TABLE,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Search for iOS apps eligible for advertising.
|
|
43
|
+
|
|
44
|
+
Returns each app's adamId — the identifier you need to create campaigns.
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
asa apps search "weather"
|
|
48
|
+
asa apps search "my app" --own
|
|
49
|
+
asa apps search photo --limit 10 --format json
|
|
50
|
+
"""
|
|
51
|
+
client = get_client()
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
with client:
|
|
55
|
+
with spinner("Searching App Store..."):
|
|
56
|
+
results = client.apps.search(query=query, return_own_apps=own, limit=limit)
|
|
57
|
+
|
|
58
|
+
if not results:
|
|
59
|
+
print_warning("No apps found")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
rows: list[dict[str, Any]] = [
|
|
63
|
+
{
|
|
64
|
+
"adam_id": app_info.adam_id,
|
|
65
|
+
"app_name": app_info.app_name,
|
|
66
|
+
"developer_name": app_info.developer_name or "-",
|
|
67
|
+
"countries": ", ".join(app_info.country_or_region_codes or []) or "-",
|
|
68
|
+
}
|
|
69
|
+
for app_info in results
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
output_data(
|
|
73
|
+
rows,
|
|
74
|
+
APP_COLUMNS,
|
|
75
|
+
format,
|
|
76
|
+
title=f"App search: {query}",
|
|
77
|
+
column_labels={"adam_id": "adam ID"},
|
|
78
|
+
)
|
|
79
|
+
except AppleSearchAdsError as e:
|
|
80
|
+
handle_api_error(e)
|
|
81
|
+
raise typer.Exit(EXIT_ERROR) from None
|