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.
Files changed (36) hide show
  1. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/.github/dependabot.yml +1 -1
  2. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/.github/workflows/ci.yml +2 -2
  3. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/.github/workflows/publish.yml +1 -1
  4. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/PKG-INFO +21 -3
  5. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/README.md +16 -0
  6. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/__init__.py +6 -1
  7. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/ad_groups.py +2 -4
  8. asa_api_cli-0.4.0/asa_api_cli/ads.py +345 -0
  9. asa_api_cli-0.4.0/asa_api_cli/apps.py +81 -0
  10. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/auth.py +77 -5
  11. asa_api_cli-0.4.0/asa_api_cli/budget_orders.py +114 -0
  12. asa_api_cli-0.4.0/asa_api_cli/countries.py +84 -0
  13. asa_api_cli-0.4.0/asa_api_cli/geo.py +92 -0
  14. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/impression_share.py +12 -3
  15. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/keywords.py +4 -4
  16. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/main.py +57 -2
  17. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/optimize.py +500 -24
  18. asa_api_cli-0.4.0/asa_api_cli/product_pages.py +155 -0
  19. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/reports.py +6 -6
  20. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/utils.py +11 -3
  21. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/pyproject.toml +6 -2
  22. asa_api_cli-0.4.0/tests/test_cli.py +273 -0
  23. asa_api_cli-0.4.0/uv.lock +2514 -0
  24. asa_api_cli-0.2.0/tests/test_cli.py +0 -39
  25. asa_api_cli-0.2.0/uv.lock +0 -3129
  26. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  27. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  28. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  29. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/.gitignore +0 -0
  30. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/.pre-commit-config.yaml +0 -0
  31. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/LICENSE +0 -0
  32. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/brand.py +0 -0
  33. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/campaigns.py +0 -0
  34. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/languages.py +0 -0
  35. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/asa_api_cli/translate.py +0 -0
  36. {asa_api_cli-0.2.0 → asa_api_cli-0.4.0}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  version: 2
2
2
  updates:
3
- - package-ecosystem: "pip"
3
+ - package-ecosystem: "uv"
4
4
  directory: "/"
5
5
  schedule:
6
6
  interval: "weekly"
@@ -14,7 +14,7 @@ jobs:
14
14
  python-version: ["3.13"]
15
15
 
16
16
  steps:
17
- - uses: actions/checkout@v6
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@v5
43
+ uses: codecov/codecov-action@v7
44
44
  with:
45
45
  file: ./coverage.xml
46
46
  fail_ci_if_error: false
@@ -12,7 +12,7 @@ jobs:
12
12
  id-token: write # Required for trusted publishing
13
13
 
14
14
  steps:
15
- - uses: actions/checkout@v6
15
+ - uses: actions/checkout@v7
16
16
 
17
17
  - name: Install uv
18
18
  uses: astral-sh/setup-uv@v7
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: asa-api-cli
3
- Version: 0.2.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.5
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
- __version__ = "0.2.0"
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(conditions=[{"field": "status", "operator": "EQUALS", "values": ["ENABLED"]}])
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