From bf4248d8695a11a4cff6781a65da46c98b36e2f4 Mon Sep 17 00:00:00 2001 From: Sam Petherbridge Date: Tue, 9 Jun 2026 13:47:48 +1000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20apps,=20geo,=20and=20countrie?= =?UTF-8?q?s=20lookup=20commands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surface three read-only v5 reference resources from asa-api-client that the CLI previously could not reach: - asa apps search — find advertisable apps and their adamId - asa geo search -c — find geo locations for campaign targeting - asa countries list — list supported countries and ad languages All support --format table/json/csv. Phase 1 of #32. --- asa_api_cli/apps.py | 81 +++++++++++++++++++++++++++++++++++ asa_api_cli/countries.py | 84 ++++++++++++++++++++++++++++++++++++ asa_api_cli/geo.py | 92 ++++++++++++++++++++++++++++++++++++++++ asa_api_cli/main.py | 6 +++ tests/test_cli.py | 30 +++++++++++++ 5 files changed, 293 insertions(+) create mode 100644 asa_api_cli/apps.py create mode 100644 asa_api_cli/countries.py create mode 100644 asa_api_cli/geo.py diff --git a/asa_api_cli/apps.py b/asa_api_cli/apps.py new file mode 100644 index 0000000..3657fe0 --- /dev/null +++ b/asa_api_cli/apps.py @@ -0,0 +1,81 @@ +"""App lookup CLI commands.""" + +from typing import Annotated, Any + +import typer +from asa_api_client.exceptions import AppleSearchAdsError + +from asa_api_cli.utils import ( + EXIT_ERROR, + OutputFormat, + get_client, + handle_api_error, + output_data, + print_warning, + spinner, +) + +app = typer.Typer(help="Search the App Store for advertisable apps") + +APP_COLUMNS = ["adam_id", "app_name", "developer_name", "countries"] + + +@app.command("search") +def search_apps( + query: Annotated[ + str, + typer.Argument(help="Search term (app name or keyword)"), + ], + own: Annotated[ + bool, + typer.Option("--own", help="Only return apps owned by your org"), + ] = False, + limit: Annotated[ + int, + typer.Option("--limit", "-l", help="Maximum number of results"), + ] = 50, + format: Annotated[ + OutputFormat, + typer.Option("--format", "-f", help="Output format"), + ] = OutputFormat.TABLE, +) -> None: + """Search for iOS apps eligible for advertising. + + Returns each app's adamId — the identifier you need to create campaigns. + + Examples: + asa apps search "weather" + asa apps search "my app" --own + asa apps search photo --limit 10 --format json + """ + client = get_client() + + try: + with client: + with spinner("Searching App Store..."): + results = client.apps.search(query=query, return_own_apps=own, limit=limit) + + if not results: + print_warning("No apps found") + return + + rows: list[dict[str, Any]] = [ + { + "adam_id": app_info.adam_id, + "app_name": app_info.app_name, + "developer_name": app_info.developer_name or "-", + "countries": ", ".join(app_info.country_or_region_codes or []) or "-", + } + for app_info in results + ] + + output_data( + rows, + APP_COLUMNS, + format, + title=f"App search: {query}", + column_labels={"adam_id": "adam ID"}, + ) + except AppleSearchAdsError as e: + handle_api_error(e) + raise typer.Exit(EXIT_ERROR) from None diff --git a/asa_api_cli/countries.py b/asa_api_cli/countries.py new file mode 100644 index 0000000..9cb4167 --- /dev/null +++ b/asa_api_cli/countries.py @@ -0,0 +1,84 @@ +"""Supported country/region reference CLI commands.""" + +from typing import Annotated, Any + +import typer +from asa_api_client.exceptions import AppleSearchAdsError +from asa_api_client.models import LanguageDetail + +from asa_api_cli.utils import ( + EXIT_ERROR, + OutputFormat, + get_client, + handle_api_error, + output_data, + print_warning, + spinner, +) + +app = typer.Typer(help="List supported countries and regions") + +COUNTRY_COLUMNS = ["country_or_region", "default_language", "supported_languages"] + + +def _language_label(detail: LanguageDetail | None) -> str: + """Render a LanguageDetail as a short label, e.g. "English (en)".""" + if detail is None: + return "-" + if detail.language and detail.language_code: + return f"{detail.language} ({detail.language_code})" + return detail.language or detail.language_code or "-" + + +@app.command("list") +def list_countries( + codes: Annotated[ + list[str] | None, + typer.Option("--code", "-c", help="Filter by ISO alpha-2 code (repeatable, e.g. -c US -c GB)"), + ] = None, + format: Annotated[ + OutputFormat, + typer.Option("--format", "-f", help="Output format"), + ] = OutputFormat.TABLE, +) -> None: + """List countries and regions supported for advertising. + + Shows each region's default and supported ad languages. + + Examples: + asa countries list + asa countries list --code US --code GB + asa countries list --format json + """ + client = get_client() + + try: + with client: + with spinner("Fetching supported countries..."): + results = client.countries_or_regions.list( + countries_or_regions=[c.upper() for c in codes] if codes else None, + ) + + if not results: + print_warning("No countries found") + return + + rows: list[dict[str, Any]] = [ + { + "country_or_region": cr.country_or_region, + "default_language": _language_label(cr.default_language), + "supported_languages": ", ".join(_language_label(lang) for lang in cr.supported_languages or []) + or "-", + } + for cr in results + ] + + output_data( + rows, + COUNTRY_COLUMNS, + format, + title="Supported countries / regions", + ) + except AppleSearchAdsError as e: + handle_api_error(e) + raise typer.Exit(EXIT_ERROR) from None diff --git a/asa_api_cli/geo.py b/asa_api_cli/geo.py new file mode 100644 index 0000000..8f34bd9 --- /dev/null +++ b/asa_api_cli/geo.py @@ -0,0 +1,92 @@ +"""Geographic location lookup CLI commands.""" + +from typing import Annotated, Any + +import typer +from asa_api_client.exceptions import AppleSearchAdsError + +from asa_api_cli.utils import ( + EXIT_ERROR, + OutputFormat, + enum_value, + get_client, + handle_api_error, + output_data, + print_warning, + spinner, +) + +app = typer.Typer(help="Search geographic locations for targeting") + +GEO_COLUMNS = ["id", "display_name", "entity", "country_or_region", "admin_area", "locality"] + + +@app.command("search") +def search_geo( + query: Annotated[ + str, + typer.Argument(help="Place name to search for (e.g. California, London)"), + ], + country: Annotated[ + str, + typer.Option("--country", "-c", help="Country code to search within (e.g. US, GB)"), + ], + entity: Annotated[ + str | None, + typer.Option("--entity", "-e", help="Filter by entity type: Country, AdminArea, or Locality"), + ] = None, + limit: Annotated[ + int, + typer.Option("--limit", "-l", help="Maximum number of results"), + ] = 50, + format: Annotated[ + OutputFormat, + typer.Option("--format", "-f", help="Output format"), + ] = OutputFormat.TABLE, +) -> None: + """Search for geographic locations available for campaign targeting. + + Each result's id can be used to target the location in a campaign. + + Examples: + asa geo search California --country US + asa geo search London -c GB --entity Locality + asa geo search Bayern -c DE --format json + """ + client = get_client() + + try: + with client: + with spinner("Searching locations..."): + results = client.geo.search( + query=query, + country_code=country.upper(), + entity=entity, + limit=limit, + ) + + if not results: + print_warning("No locations found") + return + + rows: list[dict[str, Any]] = [ + { + "id": loc.id, + "display_name": loc.display_name, + "entity": enum_value(loc.entity), + "country_or_region": loc.country_or_region or "-", + "admin_area": loc.admin_area or "-", + "locality": loc.locality or "-", + } + for loc in results + ] + + output_data( + rows, + GEO_COLUMNS, + format, + title=f"Locations in {country.upper()}: {query}", + ) + except AppleSearchAdsError as e: + handle_api_error(e) + raise typer.Exit(EXIT_ERROR) from None diff --git a/asa_api_cli/main.py b/asa_api_cli/main.py index f0889fd..e4c392a 100644 --- a/asa_api_cli/main.py +++ b/asa_api_cli/main.py @@ -6,9 +6,12 @@ from asa_api_cli import ( ad_groups, + apps, auth, brand, campaigns, + countries, + geo, impression_share, keywords, optimize, @@ -29,6 +32,9 @@ app.add_typer(campaigns.app, name="campaigns", help="Manage campaigns") app.add_typer(ad_groups.app, name="ad-groups", help="Manage ad groups") app.add_typer(keywords.app, name="keywords", help="Manage keywords") +app.add_typer(apps.app, name="apps", help="Search the App Store for advertisable apps") +app.add_typer(geo.app, name="geo", help="Search geographic locations for targeting") +app.add_typer(countries.app, name="countries", help="List supported countries and regions") app.add_typer(reports.app, name="reports", help="Generate reports") app.add_typer(optimize.app, name="optimize", help="Optimization tools") app.add_typer(impression_share.app, name="impression-share", help="Impression share analysis") diff --git a/tests/test_cli.py b/tests/test_cli.py index 635b769..8374331 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -37,3 +37,33 @@ def test_auth_help() -> None: assert result.exit_code == 0 assert "show" in result.stdout assert "test" in result.stdout + + +def test_root_help_lists_lookup_commands() -> None: + """Root help should advertise the v5 lookup command groups.""" + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "apps" in result.stdout + assert "geo" in result.stdout + assert "countries" in result.stdout + + +def test_apps_help() -> None: + """Test apps subcommand help.""" + result = runner.invoke(app, ["apps", "--help"]) + assert result.exit_code == 0 + assert "search" in result.stdout + + +def test_geo_help() -> None: + """Test geo subcommand help.""" + result = runner.invoke(app, ["geo", "--help"]) + assert result.exit_code == 0 + assert "search" in result.stdout + + +def test_countries_help() -> None: + """Test countries subcommand help.""" + result = runner.invoke(app, ["countries", "--help"]) + assert result.exit_code == 0 + assert "list" in result.stdout