asa-api-cli 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.
Files changed (36) hide show
  1. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/.github/dependabot.yml +1 -1
  2. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/.github/workflows/ci.yml +1 -1
  3. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/.github/workflows/publish.yml +1 -1
  4. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/PKG-INFO +21 -3
  5. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/README.md +16 -0
  6. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/ad_groups.py +2 -4
  7. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/auth.py +20 -4
  8. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/impression_share.py +12 -3
  9. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/keywords.py +4 -4
  10. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/main.py +39 -1
  11. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/optimize.py +27 -21
  12. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/product_pages.py +20 -8
  13. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/utils.py +11 -3
  14. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/pyproject.toml +6 -2
  15. asa_api_cli-0.4.0/tests/test_cli.py +273 -0
  16. asa_api_cli-0.4.0/uv.lock +2514 -0
  17. asa_api_cli-0.3.0/tests/test_cli.py +0 -139
  18. asa_api_cli-0.3.0/uv.lock +0 -3098
  19. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  20. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  21. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  22. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/.gitignore +0 -0
  23. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/.pre-commit-config.yaml +0 -0
  24. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/LICENSE +0 -0
  25. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/__init__.py +0 -0
  26. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/ads.py +0 -0
  27. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/apps.py +0 -0
  28. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/brand.py +0 -0
  29. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/budget_orders.py +0 -0
  30. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/campaigns.py +0 -0
  31. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/countries.py +0 -0
  32. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/geo.py +0 -0
  33. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/languages.py +0 -0
  34. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/reports.py +0 -0
  35. {asa_api_cli-0.3.0 → asa_api_cli-0.4.0}/asa_api_cli/translate.py +0 -0
  36. {asa_api_cli-0.3.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
@@ -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.3.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.2.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:
@@ -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")
@@ -67,7 +67,16 @@ def test_auth(
67
67
  table.add_column("Setting", style="label")
68
68
  table.add_column("Status")
69
69
 
70
- errors_by_field = {err["loc"][0]: err["msg"] for err in e.errors()}
70
+ errors_by_field: dict[str, str] = {}
71
+ model_errors: list[str] = []
72
+ for err in e.errors():
73
+ if err["loc"]:
74
+ errors_by_field[str(err["loc"][0])] = err["msg"]
75
+ else:
76
+ model_errors.append(err["msg"])
77
+ if "private_key" in err["msg"]:
78
+ errors_by_field.setdefault("private_key", err["msg"])
79
+ errors_by_field.setdefault("private_key_path", err["msg"])
71
80
 
72
81
  for field in ["client_id", "team_id", "key_id", "org_id", "private_key", "private_key_path"]:
73
82
  if field in errors_by_field:
@@ -77,7 +86,11 @@ def test_auth(
77
86
 
78
87
  error_console.print(table)
79
88
  error_console.print()
80
- print_error("Configuration Error", "Missing or invalid settings")
89
+ print_error(
90
+ "Configuration Error",
91
+ "Missing or invalid settings",
92
+ details="\n".join(model_errors) if model_errors else None,
93
+ )
81
94
  raise typer.Exit(EXIT_USAGE) from None
82
95
 
83
96
  # Display loaded configuration
@@ -200,9 +213,12 @@ def show_config(
200
213
  except ValidationError as e:
201
214
  print_error("Configuration Error", "Could not load settings")
202
215
  for err in e.errors():
203
- field = err["loc"][0]
204
216
  msg = err["msg"]
205
- error_console.print(f" [error]ASA_{str(field).upper()}:[/error] {msg}")
217
+ if err["loc"]:
218
+ field = err["loc"][0]
219
+ error_console.print(f" [error]ASA_{str(field).upper()}:[/error] {msg}")
220
+ else:
221
+ error_console.print(f" [error]Configuration:[/error] {msg}")
206
222
  raise typer.Exit(EXIT_USAGE) from None
207
223
 
208
224
 
@@ -224,9 +224,12 @@ def analyze_impression_share(
224
224
  poll_interval=3.0,
225
225
  timeout=120.0,
226
226
  )
227
- except Exception as e:
227
+ except AppleSearchAdsError as e:
228
228
  handle_api_error(e)
229
229
  return
230
+ except (TimeoutError, RuntimeError) as e:
231
+ print_error("Report Error", str(e))
232
+ return
230
233
 
231
234
  if not report.row:
232
235
  print_warning("No impression share data available for the selected period")
@@ -359,9 +362,12 @@ def generate_share_report(
359
362
  poll_interval=3.0,
360
363
  timeout=120.0,
361
364
  )
362
- except Exception as e:
365
+ except AppleSearchAdsError as e:
363
366
  handle_api_error(e)
364
367
  return
368
+ except (TimeoutError, RuntimeError) as e:
369
+ print_error("Report Error", str(e))
370
+ return
365
371
 
366
372
  if not report.row:
367
373
  print_warning("No data available")
@@ -444,9 +450,12 @@ def share_summary(
444
450
  poll_interval=3.0,
445
451
  timeout=120.0,
446
452
  )
447
- except Exception as e:
453
+ except AppleSearchAdsError as e:
448
454
  handle_api_error(e)
449
455
  return
456
+ except (TimeoutError, RuntimeError) as e:
457
+ print_error("Report Error", str(e))
458
+ return
450
459
 
451
460
  if not report.row:
452
461
  print_warning("No data available")
@@ -221,7 +221,7 @@ def add_keyword(
221
221
  {
222
222
  "ID": str(keyword.id),
223
223
  "Text": keyword.text,
224
- "Match Type": keyword.match_type.value,
224
+ "Match Type": enum_value(keyword.match_type),
225
225
  },
226
226
  )
227
227
  print_hint(f"asa keywords list --campaign {campaign_id} --ad-group {ad_group_id}")
@@ -454,8 +454,8 @@ def list_negatives(
454
454
  {
455
455
  "id": n.id,
456
456
  "text": n.text,
457
- "match_type": n.match_type.value,
458
- "status": n.status.value,
457
+ "match_type": enum_value(n.match_type),
458
+ "status": enum_value(n.status),
459
459
  }
460
460
  for n in negatives
461
461
  ]
@@ -519,7 +519,7 @@ def add_negative(
519
519
  {
520
520
  "Level": level.title(),
521
521
  "Text": negative.text,
522
- "Match Type": negative.match_type.value,
522
+ "Match Type": enum_value(negative.match_type),
523
523
  },
524
524
  )
525
525
  hint_args = f"--campaign {campaign_id}"
@@ -3,6 +3,7 @@
3
3
  from typing import Annotated
4
4
 
5
5
  import typer
6
+ from asa_api_client.cli import v1_smoke
6
7
 
7
8
  from asa_api_cli import (
8
9
  ad_groups,
@@ -21,7 +22,7 @@ from asa_api_cli import (
21
22
  reports,
22
23
  translate,
23
24
  )
24
- from asa_api_cli.utils import cli_state, error_console, init_consoles
25
+ from asa_api_cli.utils import EXIT_ERROR, cli_state, error_console, init_consoles, print_error
25
26
 
26
27
  app = typer.Typer(
27
28
  name="asa",
@@ -29,6 +30,38 @@ app = typer.Typer(
29
30
  rich_markup_mode="rich",
30
31
  )
31
32
 
33
+
34
+ def _register_analyze_command(target_app: typer.Typer) -> None:
35
+ """Register ``asa analyze`` from asa-api-client, or a clean install-hint fallback."""
36
+ try:
37
+ from asa_api_client.cli.analyze import analyze as analyze_command
38
+ except ImportError:
39
+
40
+ @target_app.command(
41
+ "analyze",
42
+ help="Generate an Excel performance analysis workbook (requires [analyze] extra)",
43
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
44
+ )
45
+ def _analyze_missing_extra() -> None:
46
+ """Generate an Excel performance analysis workbook.
47
+
48
+ Requires optional dependencies (pandas and xlsxwriter).
49
+ """
50
+ print_error(
51
+ "Missing Optional Dependencies",
52
+ "The 'analyze' command requires pandas and xlsxwriter.",
53
+ details=('Install with: pip install "asa-api-cli[analyze]" or uv tool install "asa-api-cli[analyze]"'),
54
+ )
55
+ raise typer.Exit(EXIT_ERROR)
56
+
57
+ return
58
+
59
+ target_app.command(
60
+ "analyze",
61
+ help="Generate an Excel performance analysis workbook",
62
+ )(analyze_command)
63
+
64
+
32
65
  # Register sub-commands
33
66
  app.add_typer(auth.app, name="auth", help="Authentication commands")
34
67
  app.add_typer(brand.app, name="brand", help="Create brand protection campaigns")
@@ -45,6 +78,11 @@ app.add_typer(reports.app, name="reports", help="Generate reports")
45
78
  app.add_typer(optimize.app, name="optimize", help="Optimization tools")
46
79
  app.add_typer(impression_share.app, name="impression-share", help="Impression share analysis")
47
80
  app.add_typer(translate.app, name="translate", help="Translate keywords to multiple languages")
81
+ _register_analyze_command(app)
82
+ app.command(
83
+ "v1-smoke",
84
+ help="Run a read-only smoke test against the Apple Ads Platform API v1",
85
+ )(v1_smoke.command)
48
86
 
49
87
 
50
88
  def version_callback(value: bool) -> None:
@@ -1675,6 +1675,8 @@ def analyze_cpa_cap_impact(
1675
1675
  currency = perf_30d.get("currency", "USD")
1676
1676
 
1677
1677
  # Parse CPA goal
1678
+ if ag.cpa_goal is None:
1679
+ continue
1678
1680
  cpa_goal_amount = ag.cpa_goal.amount
1679
1681
  if hasattr(cpa_goal_amount, "amount"):
1680
1682
  # Money object
@@ -1885,24 +1887,24 @@ def analyze_cpa_cap_impact(
1885
1887
  console.print()
1886
1888
 
1887
1889
  changes_made = 0
1888
- for i, ag in enumerate(problem_groups, 1):
1890
+ for i, analysis in enumerate(problem_groups, 1):
1889
1891
  console.rule(f"[bold]{i}/{len(problem_groups)}")
1890
1892
  console.print()
1891
1893
 
1892
1894
  # Show ad group details
1893
- impact_color = "red" if ag.impact_level in ("BLOCKED", "CRITICAL") else "yellow"
1894
- console.print(f"[bold]Ad Group:[/bold] {ag.ad_group_name}")
1895
- console.print(f"[bold]Campaign:[/bold] {ag.campaign_name} ({ag.country})")
1895
+ impact_color = "red" if analysis.impact_level in ("BLOCKED", "CRITICAL") else "yellow"
1896
+ console.print(f"[bold]Ad Group:[/bold] {analysis.ad_group_name}")
1897
+ console.print(f"[bold]Campaign:[/bold] {analysis.campaign_name} ({analysis.country})")
1896
1898
  console.print()
1897
- cpa_str = f"{ag.cpa_goal:.2f} {ag.currency}"
1899
+ cpa_str = f"{analysis.cpa_goal:.2f} {analysis.currency}"
1898
1900
  console.print(f" CPA Goal: [{impact_color}]{cpa_str}[/{impact_color}]")
1899
- if ag.avg_cpa:
1900
- console.print(f" Actual Avg CPA: {ag.avg_cpa:.2f} {ag.currency}")
1901
- console.print(f" Impressions 7d: {ag.impressions_7d:,}")
1902
- console.print(f" Impressions 30d: {ag.impressions_30d:,}")
1903
- console.print(f" Taps 30d: {ag.taps_30d:,}")
1904
- console.print(f" Installs 30d: {ag.installs_30d:,}")
1905
- console.print(f" Impact: [{impact_color}]{ag.impact_level}[/{impact_color}]")
1901
+ if analysis.avg_cpa:
1902
+ console.print(f" Actual Avg CPA: {analysis.avg_cpa:.2f} {analysis.currency}")
1903
+ console.print(f" Impressions 7d: {analysis.impressions_7d:,}")
1904
+ console.print(f" Impressions 30d: {analysis.impressions_30d:,}")
1905
+ console.print(f" Taps 30d: {analysis.taps_30d:,}")
1906
+ console.print(f" Installs 30d: {analysis.installs_30d:,}")
1907
+ console.print(f" Impact: [{impact_color}]{analysis.impact_level}[/{impact_color}]")
1906
1908
  console.print()
1907
1909
 
1908
1910
  action = (
@@ -1928,20 +1930,20 @@ def analyze_cpa_cap_impact(
1928
1930
  with spinner("Removing CPA goal..."):
1929
1931
  # Update ad group with cpa_goal set to remove it
1930
1932
  # Note: The API may require a specific way to clear the CPA goal
1931
- client.campaigns(ag.campaign_id).ad_groups.update(
1932
- ag.ad_group_id,
1933
+ client.campaigns(analysis.campaign_id).ad_groups.update(
1934
+ analysis.ad_group_id,
1933
1935
  AdGroupUpdate(cpa_goal=None),
1934
1936
  )
1935
- print_success(f"Removed CPA goal from '{ag.ad_group_name}'")
1937
+ print_success(f"Removed CPA goal from '{analysis.ad_group_name}'")
1936
1938
  changes_made += 1
1937
1939
  except AppleSearchAdsError as e:
1938
1940
  print_error("Failed to remove CPA goal", str(e))
1939
1941
  console.print()
1940
1942
  elif action in ("i", "increase"):
1941
1943
  # Prompt for new CPA goal
1942
- suggested = ag.cpa_goal * Decimal("1.5") # Suggest 50% increase
1944
+ suggested = analysis.cpa_goal * Decimal("1.5") # Suggest 50% increase
1943
1945
  new_goal_str = typer.prompt(
1944
- f"New CPA goal ({ag.currency})",
1946
+ f"New CPA goal ({analysis.currency})",
1945
1947
  default=f"{suggested:.2f}",
1946
1948
  )
1947
1949
  try:
@@ -1953,13 +1955,17 @@ def analyze_cpa_cap_impact(
1953
1955
  with spinner("Updating CPA goal..."):
1954
1956
  from asa_api_client.models import CpaGoal as CpaGoalModel
1955
1957
 
1956
- client.campaigns(ag.campaign_id).ad_groups.update(
1957
- ag.ad_group_id,
1958
+ client.campaigns(analysis.campaign_id).ad_groups.update(
1959
+ analysis.ad_group_id,
1958
1960
  AdGroupUpdate(
1959
- cpa_goal=CpaGoalModel(amount=Money(amount=str(new_goal), currency=ag.currency))
1961
+ cpa_goal=CpaGoalModel(
1962
+ amount=Money(amount=str(new_goal), currency=analysis.currency)
1963
+ )
1960
1964
  ),
1961
1965
  )
1962
- print_success(f"Updated CPA goal: {ag.cpa_goal:.2f} → {new_goal:.2f} {ag.currency}")
1966
+ print_success(
1967
+ f"Updated CPA goal: {analysis.cpa_goal:.2f} → {new_goal:.2f} {analysis.currency}"
1968
+ )
1963
1969
  changes_made += 1
1964
1970
  except ValueError:
1965
1971
  print_warning(f"Invalid input '{new_goal_str}', skipping")
@@ -24,6 +24,10 @@ LOCALE_COLUMNS = ["language_code", "language", "app_name", "device_classes"]
24
24
 
25
25
  @app.command("list")
26
26
  def list_product_pages(
27
+ adam_id: Annotated[
28
+ int,
29
+ typer.Option("--adam-id", "-a", help="App Store app ID (adamId)"),
30
+ ],
27
31
  limit: Annotated[
28
32
  int,
29
33
  typer.Option("--limit", "-l", help="Maximum number of results"),
@@ -33,18 +37,18 @@ def list_product_pages(
33
37
  typer.Option("--format", "-f", help="Output format"),
34
38
  ] = OutputFormat.TABLE,
35
39
  ) -> None:
36
- """List custom product pages.
40
+ """List custom product pages for an app.
37
41
 
38
42
  Examples:
39
- asa product-pages list
40
- asa product-pages list --format json
43
+ asa product-pages list --adam-id 123456789
44
+ asa product-pages list -a 123456789 --format json
41
45
  """
42
46
  client = get_client()
43
47
 
44
48
  try:
45
49
  with client:
46
50
  with spinner("Fetching product pages..."):
47
- result = client.product_pages.list(limit=limit)
51
+ result = client.product_pages(adam_id).list(limit=limit)
48
52
 
49
53
  if not result.data:
50
54
  print_warning("No product pages found")
@@ -73,18 +77,22 @@ def get_product_page(
73
77
  str,
74
78
  typer.Argument(help="Product page ID"),
75
79
  ],
80
+ adam_id: Annotated[
81
+ int,
82
+ typer.Option("--adam-id", "-a", help="App Store app ID (adamId)"),
83
+ ],
76
84
  ) -> None:
77
85
  """Show details for a single custom product page.
78
86
 
79
87
  Example:
80
- asa product-pages get <product-page-id>
88
+ asa product-pages get <product-page-id> --adam-id 123456789
81
89
  """
82
90
  client = get_client()
83
91
 
84
92
  try:
85
93
  with client:
86
94
  with spinner("Fetching product page..."):
87
- pp = client.product_pages.get(product_page_id)
95
+ pp = client.product_pages(adam_id).get(product_page_id)
88
96
 
89
97
  print_result_panel(
90
98
  f"Product page {pp.id}",
@@ -106,6 +114,10 @@ def product_page_locales(
106
114
  str,
107
115
  typer.Argument(help="Product page ID"),
108
116
  ],
117
+ adam_id: Annotated[
118
+ int,
119
+ typer.Option("--adam-id", "-a", help="App Store app ID (adamId)"),
120
+ ],
109
121
  format: Annotated[
110
122
  OutputFormat,
111
123
  typer.Option("--format", "-f", help="Output format"),
@@ -114,14 +126,14 @@ def product_page_locales(
114
126
  """List the per-locale details of a custom product page.
115
127
 
116
128
  Example:
117
- asa product-pages locales <product-page-id>
129
+ asa product-pages locales <product-page-id> --adam-id 123456789
118
130
  """
119
131
  client = get_client()
120
132
 
121
133
  try:
122
134
  with client:
123
135
  with spinner("Fetching locale details..."):
124
- details = client.product_pages.get_locale_details(product_page_id)
136
+ details = client.product_pages(adam_id).get_locale_details(product_page_id)
125
137
 
126
138
  if not details:
127
139
  print_warning("No locale details found")
@@ -122,7 +122,7 @@ def handle_api_error(e: AppleSearchAdsError) -> None:
122
122
  Args:
123
123
  e: The exception to handle.
124
124
  """
125
- from asa_api_client.exceptions import ValidationError
125
+ from asa_api_client.exceptions import PartialFailureError, ValidationError
126
126
 
127
127
  details_parts = []
128
128
 
@@ -139,10 +139,18 @@ def handle_api_error(e: AppleSearchAdsError) -> None:
139
139
  for err in errors:
140
140
  details_parts.append(f" {field}: {err}")
141
141
 
142
+ # Show per-item error entries for partial failures (2xx with error block)
143
+ if isinstance(e, PartialFailureError) and e.details:
144
+ for item in e.details:
145
+ field_name = item.get("field")
146
+ code = item.get("messageCode") or item.get("code")
147
+ msg = item.get("message") or code or json.dumps(item)
148
+ prefix = f"{field_name}: " if field_name else ""
149
+ suffix = f" ({code})" if code and code != msg else ""
150
+ details_parts.append(f" {prefix}{msg}{suffix}")
151
+
142
152
  # Show response body if available (for debugging)
143
153
  if e.response_body:
144
- import json
145
-
146
154
  # Remove internal _request field before displaying
147
155
  body_copy = {k: v for k, v in e.response_body.items() if not k.startswith("_")}
148
156
  body_str = json.dumps(body_copy, indent=2)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "asa-api-cli"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Command-line interface for Apple Search Ads campaign management"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -28,7 +28,7 @@ classifiers = [
28
28
  "Typing :: Typed",
29
29
  ]
30
30
  dependencies = [
31
- "asa-api-client>=0.2.1",
31
+ "asa-api-client>=0.6.1",
32
32
  "typer>=0.13.0",
33
33
  "rich>=13.9.0",
34
34
  "pydantic-ai>=0.1.0",
@@ -39,6 +39,9 @@ dependencies = [
39
39
  asa = "asa_api_cli:app"
40
40
 
41
41
  [project.optional-dependencies]
42
+ analyze = [
43
+ "asa-api-client[cli]>=0.6.1",
44
+ ]
42
45
  dev = [
43
46
  "pytest>=8.3.0",
44
47
  "pytest-asyncio>=0.24.0",
@@ -64,6 +67,7 @@ python_version = "3.13"
64
67
  strict = true
65
68
  ignore_missing_imports = true
66
69
  warn_unused_ignores = false
70
+ plugins = ["pydantic.mypy"]
67
71
 
68
72
  [tool.ruff]
69
73
  target-version = "py313"